Document that package.json aliases may target only direct subfolders of src #21
@@ -1,36 +0,0 @@
|
||||
---
|
||||
description: Containerfiles build the frontend in a node:24-alpine stage with npm ci and vp build; CI uses setup-vp and runs vp check, vp build, vp test (plus lit-check)
|
||||
---
|
||||
|
||||
## Containerfiles
|
||||
|
||||
- The frontend build stage is `docker.io/library/node:24-alpine`, replacing
|
||||
`denoland/deno:alpine`. Vite+ publishes `linux-x64-musl` binaries, so Alpine
|
||||
works. (`ghcr.io/voidzero-dev/vite-plus` is a glibc alternative that would
|
||||
need a pinned digest; not used.)
|
||||
- The stage copies `package.json` and `package-lock.json` first, runs
|
||||
`npm ci`, then copies the sources and configs and runs `npx vp build`.
|
||||
- The runtime stage copies the built `js/` and `css/` directories (not single
|
||||
files) from the node stage.
|
||||
- No Deno binary, `deno install`, or `deno task` remains in any stage.
|
||||
|
||||
```dockerfile
|
||||
FROM docker.io/library/node:24-alpine AS frontend
|
||||
WORKDIR /src/frontend
|
||||
COPY frontend/package.json frontend/package-lock.json ./
|
||||
RUN npm ci
|
||||
COPY frontend/ ./
|
||||
RUN npx vp build
|
||||
```
|
||||
|
||||
## CI
|
||||
|
||||
- GitHub repos (temeddix): `.github/workflows`. Capsulizers repos (Gitea):
|
||||
`.gitea/workflows`. Don't move a repo's CI between hosts.
|
||||
- Install with `voidzero-dev/setup-vp`, then run `vp check`, `vp build`, and
|
||||
`vp test`; Lit repos also run `npm run lit-check`
|
||||
([[lit-analyzer-ts6]]).
|
||||
- Repos that ran CI on a self-hosted runner keep it.
|
||||
- Until `@cunarist/eslint-plugin-*` are on npm, `npm ci` fails in CI. That is
|
||||
expected and stated in the PR; run `npm install` once they are published
|
||||
and commit the refreshed lockfile.
|
||||
@@ -1,20 +0,0 @@
|
||||
---
|
||||
description: Code that must ship as standalone JS (memona framed_find.js, brain stream-recovery.js) is written as .ts, compiled at build time, and its output gitignored
|
||||
---
|
||||
|
||||
Some apps ship plain JavaScript that runs outside the main bundle: memona's
|
||||
`framed_find.js` and brain's `stream-recovery.js`. Under the TS-only rule
|
||||
([[ts-only-files-and-imports]]) they are handled like this (2026-10-16):
|
||||
|
||||
- The source is a `.ts` file, linted and type-checked like everything else.
|
||||
- The `.js` is produced at build time by the existing build (a Vite input or
|
||||
a build step in `vite.config.ts` or `scripts/`), never committed.
|
||||
- The output path is gitignored and, if it lands in a linted folder, listed
|
||||
in `lint.ignorePatterns`; never in `overrides`, never with disable comments.
|
||||
- Whatever loads the file (Rust `include_str!`, a container `COPY`, an HTML
|
||||
`<script>`) keeps the same path and name as before, so behavior is
|
||||
unchanged. Builds that need the file must run the compile first; check
|
||||
that the Containerfile and CI order the steps that way.
|
||||
|
||||
Don't commit a hand-maintained `.js` copy "for convenience", and don't turn
|
||||
it into `.mjs` or `.cjs`.
|
||||
@@ -1,41 +0,0 @@
|
||||
---
|
||||
description: Apps use the npm lint plugins' configs, port their old deno-lint-plugin.ts rules into scripts/project-lint.ts, never override source files, and refactor instead of raising limits
|
||||
---
|
||||
|
||||
## Plugins
|
||||
|
||||
Apps consume the published plugins as npm devDependencies, through `jsPlugins`
|
||||
and the configs equivalent to what their Deno setup enabled:
|
||||
|
||||
- `@cunarist/eslint-plugin-explicit-types`: `concrete`, `naming`, `absence`
|
||||
- `@cunarist/eslint-plugin-modules`: `modules`
|
||||
- `@cunarist/eslint-plugin-lit`: `core`, `strict`, `dom-ref`,
|
||||
`reactive-controller`, `naming`
|
||||
|
||||
## Repo-local rules
|
||||
|
||||
The local `scripts/deno-lint-plugin.ts` that home, eximium, stockvibe, brain,
|
||||
memona, and memona-home carried is ported into `scripts/project-lint.ts`
|
||||
([[project-lint-plugin]]) under the `project` namespace, same rule names and
|
||||
behavior. Depending on the repo: `no-self-sync`, `no-sync-in-render`,
|
||||
`app-module-conventions`, `always-wildcard-export`, `no-class-extends`,
|
||||
`no-lit-nothing`, `no-title-attribute`, `prefer-wa-components`,
|
||||
`no-satisfies`, `no-unknown`, `restricted-types`, `no-line-lint-ignore`,
|
||||
`no-app-disposables-in-component`, `no-crepe-react-ui`. `@hugoalh` rules map to
|
||||
Oxlint built-ins in `configs.project`.
|
||||
|
||||
memona's `deno-lint-plugin.ts` and its `coding-standards` memory are the house
|
||||
style the explicit-types rules came from; they win any conflict.
|
||||
|
||||
## No overrides, refactor instead
|
||||
|
||||
Apps use **no `overrides` for source files at all** (the rule-registry
|
||||
exception exists only in plugin repos). Oxlint's default limits stay, and
|
||||
violations are refactored without behavior change in the same PR: barrels for
|
||||
`max-dependencies`, splits for `max-lines`/`max-lines-per-function`/
|
||||
`max-classes-per-file`, an options object for `max-params`. Long Lit
|
||||
`render()` templates are split into smaller template functions or
|
||||
components. See [[lint-overrides-policy]].
|
||||
|
||||
If a hand-written exception still looks truly necessary, ask the user first;
|
||||
when they're away, leave it failing or refactor, and log the question.
|
||||
@@ -1,43 +0,0 @@
|
||||
---
|
||||
description: Frontends build with vp build in Vite app mode, keeping the exact output paths the Rust backend and HTML expect; emptyOutDir false when outDir holds committed files
|
||||
---
|
||||
|
||||
`deno bundle` was replaced by `vp build` in **Vite app mode** (not
|
||||
`build.lib`). Behavior must not change, so the output keeps the file names and
|
||||
locations the backend serves and the HTML references.
|
||||
|
||||
Typical shape for the Rust-backed frontends (home, eximium, stockvibe,
|
||||
memona-home), where `frontend/` builds into `backend/static`:
|
||||
|
||||
```ts
|
||||
build: {
|
||||
outDir: "../backend/static",
|
||||
emptyOutDir: false, // the folder holds committed files (index.html, ...)
|
||||
rollupOptions: {
|
||||
input: { bundle: "src/index.ts" },
|
||||
output: { entryFileNames: "js/[name].js" }, // -> js/bundle.js
|
||||
},
|
||||
},
|
||||
```
|
||||
|
||||
- **Fixed names, no hashes:** set `entryFileNames` (and `assetFileNames`
|
||||
where assets are emitted) explicitly. Vite's hashed defaults would 404,
|
||||
since Rust and the templates request `js/bundle.js` and `css/theme.css`.
|
||||
- **`emptyOutDir: false` whenever `outDir` is outside the Vite root or holds
|
||||
committed files.** Never let it default to true there: it would wipe them.
|
||||
- Output stays gitignored, so `.gitignore`, `.containerignore`, and the Rust
|
||||
side need no edits; home's startup check for `js/bundle.js` and
|
||||
`css/theme.css` keeps passing.
|
||||
- **CSS is never imported from TS** (no `import "./theme.css"`); the HTML
|
||||
links the stylesheet. How the CSS file is produced is per repo; read the
|
||||
repo's `vite.config.ts` before changing it.
|
||||
- Copy whole `js/` and `css/` directories in Containerfiles, not single files,
|
||||
so code-split chunks ship too ([[app-containers-and-ci]]).
|
||||
- Where a repo serves an `index.html` entry (brain's dashboard), use plain app
|
||||
mode with `index.html` as input, `base` set to the mount path, and `outDir:
|
||||
"dist"` with the default `emptyOutDir`.
|
||||
- `vp dev` must work too, and the decorators plugin must cover build, dev, and
|
||||
test ([[decorators-standard-accessor]]).
|
||||
|
||||
Check the repo's own migration PR for its final values before editing; these
|
||||
are the conventions it followed, not a config to paste.
|
||||
@@ -17,15 +17,14 @@ properties, no decorators) and relative `.ts` imports.
|
||||
phase. It copies exactly `src/*.ts`, `package.json` (`"type": "module"`
|
||||
makes Node load the sources as ES modules), `profiles.json`, and
|
||||
`prompt.md`. **A new runtime file or subdirectory must be added there**, or
|
||||
it is missing at run time. `src/` is flat; a subfolder would not be copied.
|
||||
it is missing at run time. Runtime code in `src/` is flat; a subfolder would
|
||||
not be copied. The only subfolder, `src/testing/`, is test-only.
|
||||
- `tests/entrypoints.test.ts` runs each phase under real Node with a cleared
|
||||
environment and expects its first "X is not set" error, which proves the
|
||||
whole import graph loads without a build. Keep it in sync with the phases.
|
||||
- **None of the app build conventions apply here:** no `vp build`, no
|
||||
frontend or Rust/Tauri output paths, no Containerfile, no decorators
|
||||
plugin, no lit-check, no generated JS ([[app-vite-build-output]],
|
||||
[[app-containers-and-ci]], [[app-generated-js-as-ts]] describe the other
|
||||
apps). Don't add any of them.
|
||||
plugin, no lit-check, no generated JS. Don't add any of them.
|
||||
- **CI** (`.gitea/workflows/check.yml`) runs on the self-hosted runner's
|
||||
default image as root: `npm ci`, `npx vp check`, `npx vp test`. It does not
|
||||
use `setup-vp`. YAML stays declarative ([[yaml-stays-declarative]]).
|
||||
|
||||
@@ -4,7 +4,7 @@ description: Isolation and credential tests need root and use test.skipIf(!IS_RO
|
||||
|
||||
Some tests exercise real UID isolation, process cleanup, and `prepare.ts`
|
||||
credential cleanup. They need root and are written as
|
||||
`test.skipIf(!IS_ROOT)(...)` (`IS_ROOT` from `src/testing.ts`), so they show
|
||||
`test.skipIf(!IS_ROOT)(...)` (`IS_ROOT` from `#testing`), so they show
|
||||
as skipped instead of silently passing.
|
||||
|
||||
- As a normal user `vp test` passes 56 and skips 5. In CI (self-hosted
|
||||
@@ -17,9 +17,9 @@ as skipped instead of silently passing.
|
||||
`codex`/`claude`/`npx` binaries and a fake Gitea through
|
||||
`protect-source.sh` -> `prepare.ts` -> `run.ts` -> `post.ts`; repeat that
|
||||
for changes to the phase flow ([[bot-agents-runtime-no-build]]).
|
||||
- `src/testing.ts` holds the shared test helpers and is the `#testing` file
|
||||
alias. Every test imports it as `#testing`, never `./testing.ts`, and it
|
||||
re-exports the source helpers `tests/` needs, since `tests/` may not use
|
||||
`../`. Tests follow
|
||||
- `src/testing/index.ts` holds the shared test helpers (user IDs, temp dirs,
|
||||
`runCommand`) behind the `#testing` alias. Every test imports it as
|
||||
`#testing`. It imports no source module, so `tests/` (which may not use
|
||||
`../`) never reaches internals. Tests follow
|
||||
[[tests-layout-vitest]] (unit tests in `src/*.test.ts`, repo-level ones in
|
||||
`tests/`).
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
---
|
||||
description: Keep TC39 standard decorators with accessor; Oxc can't lower them, so scripts/decorators.ts (esbuild Vite plugin) does; never legacy decorators
|
||||
---
|
||||
|
||||
Applies to every repo that uses decorators (the Lit frontends).
|
||||
|
||||
**Decision (2026-10-16):** keep standard (TC39) decorators with `accessor`,
|
||||
exactly as the Deno code had them. No legacy decorators.
|
||||
|
||||
```ts
|
||||
@customElement("app-card")
|
||||
export class AppCard extends LitElement {
|
||||
@property() accessor name = ""; // keep this form
|
||||
}
|
||||
```
|
||||
|
||||
**Gotcha, verified 2026-10-16:** Vite 8's Oxc transformer parses standard
|
||||
decorators but can **not** lower them, so `vp build`, `vp dev`, and `vp test`
|
||||
emit decorator syntax browsers and Node don't run. Check again on each Vite+
|
||||
upgrade; when Oxc can lower them, delete the plugin and the `esbuild`
|
||||
devDependency.
|
||||
|
||||
**Fix in use:** `scripts/decorators.ts`, a small Vite plugin backed by
|
||||
`esbuild` (devDependency), registered in `vite.config.ts` `plugins` so it
|
||||
covers build, dev server, and Vitest alike:
|
||||
|
||||
- `enforce: "pre"`, so it runs before Oxc;
|
||||
- transforms only `.ts` modules whose source contains `@`;
|
||||
- `esbuild.transform(code, { loader: "ts", target: "es2022", sourcemap,
|
||||
sourcefile: id })`, returning `{ code, map }`.
|
||||
|
||||
It is tooling under `scripts/`: no tests, and it must pass every lint rule.
|
||||
|
||||
**The options as presented to the user:**
|
||||
|
||||
| Option | What | Why not |
|
||||
| ------ | ------------------------------------------------------------------------------------------------------- | ----------------------------------------------- |
|
||||
| A | Legacy decorators: two tsconfig flags (`experimentalDecorators`, ...) and a codemod removing `accessor` | the user ruled out legacy decorators |
|
||||
| B | Wait for Oxc to lower `accessor` decorators | not available yet; blocks the migration |
|
||||
| C | No decorators: `static properties = {...}` + `declare` fields | rewrites every component |
|
||||
| D | A transform step in the Vite build | chosen, with esbuild as the transformer (above) |
|
||||
|
||||
Don't add `experimentalDecorators` to `tsconfig.json`, and don't strip
|
||||
`accessor` from fields.
|
||||
@@ -9,7 +9,6 @@ description: Work on a branch, plain imperative commits without AI trailers, one
|
||||
- Commit messages are plain imperative sentences ("Port the import-check
|
||||
rules to an ESLint plugin"). No conventional-commit prefixes, no AI
|
||||
trailers (`Co-Authored-By: Claude`, "Generated with ..."), no emoji.
|
||||
- A version bump is its own commit titled `Version X.Y.Z` ([[publishing-ci-only]]).
|
||||
- When several agents share a branch, each works in its own `git worktree`,
|
||||
runs `git pull --rebase` before pushing, and retries a rejected push after
|
||||
rebasing. Remove the worktree and temp branch afterwards.
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
---
|
||||
description: Icons are .ts modules exporting a typed Lit svg template (memona's style), one per file behind an index.ts; never .svg imports as text
|
||||
---
|
||||
|
||||
memona's way of declaring icons is the standard (2026-10-14). An icon is a
|
||||
`.ts` module that exports a Lit `TemplateResult` holding the inline SVG:
|
||||
|
||||
```ts
|
||||
// src/icons/lock.ts
|
||||
import { html, type TemplateResult } from "lit";
|
||||
|
||||
/** Font Awesome Free's solid `lock`, as its SVG source. */
|
||||
export const lockIcon: TemplateResult = html`<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 384 512"
|
||||
>
|
||||
<path fill="currentColor" d="M128 96l0 64 ..." />
|
||||
</svg>`;
|
||||
```
|
||||
|
||||
```ts
|
||||
// usage
|
||||
import { lockIcon } from "#icons";
|
||||
render() { return html`<span class="icon">${lockIcon}</span>`; }
|
||||
```
|
||||
|
||||
- One icon per file, camelCase name with an `Icon` suffix, a doc comment
|
||||
saying what it depicts, exported through `src/icons/index.ts`.
|
||||
- Use `fill="currentColor"` so CSS controls the colour.
|
||||
- Keep a third-party licence comment that the SVG source carries (Font
|
||||
Awesome Free's `<!--! ... -->` notice) inside the template.
|
||||
|
||||
**Replaced pattern:** eximium, stockvibe, and memona-home imported Font
|
||||
Awesome `.svg` files as text in `icons.ts`:
|
||||
|
||||
```ts
|
||||
import lock from "@fortawesome/fontawesome-free/svgs/solid/lock.svg" with { type: "text" }; // no
|
||||
```
|
||||
|
||||
That breaks [[ts-only-files-and-imports]] (non-`.ts` import plus import
|
||||
attributes) and was converted to memona's style during the frontend
|
||||
migration. Before writing new icon code in any repo, read how that repo (or
|
||||
memona) already declares icons and match it.
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
description: An alias owns its target (folder index.ts = folder and below, any other file = that file); no overlap; outside an area only the exact alias reaches it; ../ is banned in every file; no folder names in the rule
|
||||
description: An alias owns its target (folder index.ts = folder and below, any other file = that file); no overlap; outside an area only the exact alias reaches it; ../ is banned in every file; locally, aliases target only direct src/ subfolders
|
||||
---
|
||||
|
||||
## `#` aliases own their targets
|
||||
@@ -11,7 +11,9 @@ unbuilt. `vp pack` inlines them, so they never reach package consumers.
|
||||
|
||||
One rule for every file, never tied to folder or file names (user rule
|
||||
2026-10-17; it replaces the earlier "aliases only point at direct `src/`
|
||||
subfolders" rule and the "modules plugin checks only aliased folders" scope):
|
||||
subfolders" rule as the general rule, and the "modules plugin checks only
|
||||
aliased folders" scope; this repo keeps the stricter rule as a local
|
||||
convention, see the last section):
|
||||
|
||||
- **An alias may point anywhere** and owns its target. An alias to a
|
||||
folder's `index.ts` owns that folder and everything below it; an alias to
|
||||
@@ -77,9 +79,24 @@ Relative imports are written with the `.ts` extension.
|
||||
|
||||
- `scripts/` has no subfolders and no `index.ts`.
|
||||
- Each tool is one file, `scripts/<name>.ts`, imported directly by file:
|
||||
`import { decorators } from "./scripts/decorators.ts";` in
|
||||
`import projectLint from "./scripts/project-lint.ts";` in
|
||||
`vite.config.ts`. No alias owns `scripts/`, so these `./` imports are free.
|
||||
- Code in a former `scripts/<tool>/` folder is merged or renamed into flat
|
||||
`scripts/<name>.ts` files. Non-TS assets (HTML, images, fixtures) move to
|
||||
where they belong (next to the tool's output, `.agents/`, or a gitignored
|
||||
scratch dir). A subfolder that is really an app moves under an alias.
|
||||
|
||||
## Local convention: aliases target direct `src/` subfolders
|
||||
|
||||
This is a convention of this repo only, stricter than the general rule above
|
||||
(which the published modules plugin checks and which allows more). It is not
|
||||
a plugin rule.
|
||||
|
||||
- Every `package.json` `imports` alias points only at the `index.ts` of a
|
||||
direct subfolder of `src/`, e.g. `"#helpers": "./src/helpers/index.ts"`.
|
||||
No alias to a single file, a deeper folder, or anything outside `src/`.
|
||||
Here the one alias is `"#testing": "./src/testing/index.ts"`.
|
||||
- Code outside `src/` (`scripts/`, `tests/`, `vite.config.ts`) gets no alias
|
||||
of its own and imports its own files by `./` relative paths. It reaches
|
||||
`src/` only through the aliases, since `../` is banned.
|
||||
- No lint rule checks this; check `imports` by hand when adding an alias.
|
||||
|
||||
@@ -25,7 +25,7 @@ covered by the tests, inside the same migration PR.
|
||||
| ------------------------- | ------------------------------------------------------- |
|
||||
| `import/max-dependencies` | import a barrel (`#ui`) instead of many direct modules |
|
||||
| `max-lines` | split the file by responsibility |
|
||||
| `max-lines-per-function` | extract helpers; split a long Lit template into parts |
|
||||
| `max-lines-per-function` | extract helpers |
|
||||
| `max-classes-per-file` | one class per file |
|
||||
| `max-params` | pass one options object |
|
||||
| `prefer-set-has` | `const K: ReadonlySet<string> = new Set([...])`, `.has` |
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
---
|
||||
description: lit-analyzer runs from scripts/lit-check.ts, which maps "typescript" to the aliased typescript-6 devDependency; npm run lit-check in CI
|
||||
---
|
||||
|
||||
Applies to Lit repos that ran `cunarist/deno-lit-analyzer` under Deno.
|
||||
|
||||
lit-analyzer is built on TypeScript's JavaScript compiler API and is run
|
||||
against TypeScript 6 (the open question was whether it works with TS 7 /
|
||||
tsgo). The repo installs TS 6 under another name and redirects lit-analyzer
|
||||
to it, so the rest of the toolchain is unaffected (decided 2026-10-16):
|
||||
|
||||
```jsonc
|
||||
// package.json
|
||||
"scripts": { "lit-check": "node scripts/lit-check.ts" },
|
||||
"devDependencies": { "typescript-6": "npm:typescript@^6", "lit-analyzer": "..." }
|
||||
```
|
||||
|
||||
```ts
|
||||
// scripts/lit-check.ts (shape)
|
||||
import { registerHooks } from "node:module";
|
||||
|
||||
registerHooks({
|
||||
resolve(specifier, context, next) {
|
||||
// when lit-analyzer asks for "typescript", hand it TS 6
|
||||
return next(specifier === "typescript" ? "typescript-6" : specifier, context);
|
||||
},
|
||||
});
|
||||
// then run lit-analyzer
|
||||
```
|
||||
|
||||
- Node runs the `.ts` script directly (type stripping), so no build step.
|
||||
- Import the package entry, or spawn the package's bin with
|
||||
`node:child_process`, rather than `await import("lit-analyzer/cli.js")`.
|
||||
`project/no-asset-imports` allows `.js` (and `.mts`/`.mjs`) subpaths, but the CLI file runs on
|
||||
import and bypasses the package's public entry.
|
||||
- CI runs `npm run lit-check` as its own step.
|
||||
- It is tooling under `scripts/`: no tests; a narrow `overrides` entry for it
|
||||
(e.g. `no-console`) is allowed if truly needed.
|
||||
|
||||
Don't replace this with a global TypeScript downgrade, a second
|
||||
`tsconfig`, or a `.js` wrapper script.
|
||||
@@ -11,15 +11,12 @@ Oxlint plugin with `meta.name` `"project"`, loaded through `jsPlugins` in
|
||||
plugin configs: Deno parity rules, extras, per-rule options, and opt-outs,
|
||||
built-in Oxlint ids included (`curly`, `id-match`, `import/no-cycle`, ...).
|
||||
- **Custom rules**, enabled as `project/<rule>`. Every repo has
|
||||
`no-disable-comments` and `no-asset-imports`. Apps also port the rules of
|
||||
their old `scripts/deno-lint-plugin.ts` here under the same names and
|
||||
behavior (`no-self-sync`, `app-module-conventions`, `restricted-types`, ...).
|
||||
`no-disable-comments` and `no-asset-imports`; bot-agents has no others.
|
||||
|
||||
`scripts/` stays flat; don't add `scripts/rules/`. A file that would pass
|
||||
`max-lines` (300) splits into flat themed files sharing the prefix:
|
||||
`scripts/project-lint.ts` keeps the plugin object and the rule list and
|
||||
imports the others (`project-lint-modules.ts`, `project-lint-types.ts`,
|
||||
`project-lint-lit.ts`, ...).
|
||||
imports the others (`project-lint-modules.ts`, `project-lint-types.ts`, ...).
|
||||
|
||||
```ts
|
||||
import type { DummyRuleMap } from "vite-plus/lint";
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
---
|
||||
description: Packages publish only from CI when a GitHub release vX.Y.Z is published, via npm trusted publishing; never npm publish by hand; notes live only on the release page
|
||||
---
|
||||
|
||||
Standing convention (2026-10-12), applied to every package:
|
||||
|
||||
- **Only CI publishes.** Publishing a GitHub release `vX.Y.Z` triggers
|
||||
`.github/workflows/publish.yml`. Never run `npm publish` locally and never
|
||||
push a bare `v*` tag. Use the `release` skill, and only when the user asked
|
||||
for a release.
|
||||
- **The workflow** checks that the tag equals `v` + `package.json` `version`,
|
||||
runs the full CI as a reusable workflow (every Node version in the matrix),
|
||||
then `vp pack` and `npm publish --access public --provenance`. A release
|
||||
marked pre-release publishes under the `next` dist-tag, others `latest`.
|
||||
It runs on Node 24 (npm 11, which trusted publishing needs) with no cache.
|
||||
- **Auth is npm trusted publishing** (OIDC, `id-token: write`), which also
|
||||
attaches provenance. npm only allows a trusted publisher on a package that
|
||||
already exists, so the very first release bootstraps with a short-lived
|
||||
token in the `NPM_TOKEN` repo secret (the workflow uses it only when set).
|
||||
Afterwards the user configures the trusted publisher for `publish.yml`,
|
||||
deletes the secret, and sets the package to disallow tokens.
|
||||
- **Release notes live only on the GitHub release page**
|
||||
(`--generate-notes`, edited there). There is no `CHANGELOG.md`; never add
|
||||
one.
|
||||
- **The version lives in two places in plugin packages:** `package.json` and
|
||||
`meta.version` in `src/plugin/index.ts`; `tests/package-meta.test.ts` fails if they
|
||||
differ.
|
||||
- **Before 1.0**, a change that breaks users (renamed rule id, a rule that
|
||||
fires on more code, removed behavior) is a minor bump.
|
||||
- **Before a release**, pack the package and lint a known-bad and a known-good
|
||||
file from separate consumer projects, once through `vp lint` with
|
||||
`jsPlugins` and once through ESLint 9 with the configs. On the lit plugin
|
||||
this caught two bugs that 313 unit tests missed.
|
||||
|
||||
The cunarist lint plugins (explicit-types, modules, lit) are published
|
||||
together, with a token the user provides; the old `deno-*` repos are archived
|
||||
only after that, one at a time.
|
||||
@@ -15,12 +15,11 @@ Decided 2026-10-14 for all repos.
|
||||
`#plugin` for `./src/plugin/index.ts`), never internals and never `../`,
|
||||
which is banned in every file. `vp test tests` runs just these
|
||||
([[imports-aliases-barrels]]). In bot-agents, `tests/` imports only
|
||||
`#testing` (`./src/testing.ts`), which re-exports the few source helpers
|
||||
those tests need.
|
||||
`#testing` (`./src/testing/index.ts`), plain helpers that import no source
|
||||
module.
|
||||
- **No in-source tests** (`import.meta.vitest`).
|
||||
- **`scripts/` gets no tests** (2026-10-15): it is repo tooling, not
|
||||
production code (`project-lint.ts`, `decorators.ts`, `lit-check.ts`,
|
||||
release helpers).
|
||||
production code (`project-lint.ts`, `protect-source.sh`).
|
||||
- Test files follow every lint rule; no relaxed overrides for tests.
|
||||
|
||||
## Migrating Deno tests
|
||||
|
||||
@@ -42,7 +42,7 @@ import { scenes } from "#data/scenes"; // yes: scenes.ts
|
||||
import CSS from TS, not even for Vite. (This also removes most of the
|
||||
`import/no-unassigned-import` hits the `suspicious` category reports.)
|
||||
- **JSON data:** turn it into a `.ts` module exporting a typed constant.
|
||||
- **SVG:** a `.ts` module exporting a Lit template; see [[icons-memona-style]].
|
||||
- **SVG:** a `.ts` module exporting the markup.
|
||||
- **`package.json` fields at runtime:** never `import pkg from
|
||||
"./package.json"` and there is no `#package` alias. Write the value out in
|
||||
`.ts` and add a test in `tests/` that reads `package.json` with `node:fs`
|
||||
@@ -50,5 +50,4 @@ import { scenes } from "#data/scenes"; // yes: scenes.ts
|
||||
- **Reading a file is not importing it.** `readFileSync` + `JSON.parse` is
|
||||
fine in tooling and tests.
|
||||
- **A tool's CLI file:** prefer the package entry or spawning the package's
|
||||
bin over importing `pkg/cli.js`, which runs the CLI as a side effect
|
||||
([[lit-analyzer-ts6]]).
|
||||
bin over importing `pkg/cli.js`, which runs the CLI as a side effect.
|
||||
|
||||
@@ -29,8 +29,11 @@ Inside an area, relative imports of siblings and subfolders are free. `../`
|
||||
is banned in every file, owned or not
|
||||
(`import/no-relative-parent-imports`). Files no alias owns need no `index.ts`
|
||||
and no alias, and import `./` siblings and subfolders freely. The one alias
|
||||
is `#testing` (`./src/testing.ts`): every test imports it by alias, and
|
||||
`tests/` reaches `src/` only through it. `scripts/` is flat: one file per
|
||||
is `#testing` (`./src/testing/index.ts`): every test imports it by alias, and
|
||||
`tests/` reaches `src/` only through it. Local convention, stricter than the
|
||||
|
|
||||
general rule above: in this repo, aliases point only at a direct subfolder of
|
||||
`src/` (`"#helpers": "./src/helpers/index.ts"`), and code outside `src/` uses
|
||||
`./` relative imports. `scripts/` is flat: one file per
|
||||
|
bot
commented
`AGENTS.md` says every test imports `#testing`, but `src/credentials.test.ts`, `src/opencode.test.ts`, and `src/profiles.test.ts` do not. Limit this to `tests/` (or make the statement true) so the alias policy stays accurate.
> Profile: `codex` · `gpt-5.6-terra`
|
||||
tool, no subfolders and no `index.ts`, imported directly by file path
|
||||
([.agents/memory/imports-aliases-barrels.md](.agents/memory/imports-aliases-barrels.md)).
|
||||
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
"description": "A Gitea composite action that runs a coding agent for approved issue and pull request automation.",
|
||||
"type": "module",
|
||||
"imports": {
|
||||
"#testing": "./src/testing.ts"
|
||||
"#testing": "./src/testing/index.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/core": "^0.17.0",
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
// Test support: the current user's IDs and a fresh temporary directory, plus
|
||||
// the source helpers that the repository-level tests in tests/ need. Those
|
||||
// tests sit outside src/ and reach it only through the `#testing` alias.
|
||||
import { mkdtemp } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import process from "node:process";
|
||||
|
||||
export function currentUid(): number {
|
||||
return process.getuid?.() ?? -1;
|
||||
}
|
||||
|
||||
export function currentGid(): number {
|
||||
return process.getgid?.() ?? -1;
|
||||
}
|
||||
|
||||
// The isolation tests switch UIDs, which only root may do.
|
||||
export const IS_ROOT: boolean = currentUid() === 0;
|
||||
|
||||
// Symlinks and POSIX modes, as these tests use them, need a Unix host.
|
||||
export const IS_WINDOWS: boolean = process.platform === "win32";
|
||||
|
||||
export function makeTempDir(): Promise<string> {
|
||||
return mkdtemp(join(tmpdir(), "bot-agents-test-"));
|
||||
}
|
||||
|
||||
export { output } from "./command.ts";
|
||||
export { isNotFound } from "./errors.ts";
|
||||
export { isMapping } from "./json.ts";
|
||||
export type { JsonObject } from "./json.ts";
|
||||
@@ -0,0 +1,47 @@
|
||||
// Test support: the current user's IDs, a fresh temporary directory, and a
|
||||
// plain command runner. It imports no source module, so the repository-level
|
||||
// tests in tests/, which reach src/ only through `#testing`, never touch
|
||||
// internals.
|
||||
import { spawn } from "node:child_process";
|
||||
import { once } from "node:events";
|
||||
import { mkdtemp } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import process from "node:process";
|
||||
import { text } from "node:stream/consumers";
|
||||
|
||||
export function currentUid(): number {
|
||||
return process.getuid?.() ?? -1;
|
||||
}
|
||||
|
||||
export function currentGid(): number {
|
||||
return process.getgid?.() ?? -1;
|
||||
}
|
||||
|
||||
// The isolation tests switch UIDs, which only root may do.
|
||||
export const IS_ROOT: boolean = currentUid() === 0;
|
||||
|
||||
// Symlinks and POSIX modes, as these tests use them, need a Unix host.
|
||||
export const IS_WINDOWS: boolean = process.platform === "win32";
|
||||
|
||||
export function makeTempDir(): Promise<string> {
|
||||
return mkdtemp(join(tmpdir(), "bot-agents-test-"));
|
||||
}
|
||||
|
||||
export interface CommandResult {
|
||||
success: boolean;
|
||||
stderr: string;
|
||||
}
|
||||
|
||||
// Runs a command with stdin closed and only the given environment, and
|
||||
// collects its stderr.
|
||||
export async function runCommand(
|
||||
command: string,
|
||||
args: string[],
|
||||
env: Record<string, string>,
|
||||
): Promise<CommandResult> {
|
||||
const child = spawn(command, args, { env, stdio: ["ignore", "ignore", "pipe"] });
|
||||
const [stderr, closed] = await Promise.all([text(child.stderr), once(child, "close")]);
|
||||
const exitCode: unknown = closed[0];
|
||||
return { success: exitCode === 0, stderr };
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import { fileURLToPath } from "node:url";
|
||||
|
||||
import { expect, test } from "vite-plus/test";
|
||||
|
||||
import { output } from "#testing";
|
||||
import { runCommand } from "#testing";
|
||||
|
||||
// Each phase runs as `node src/<phase>.ts` with no build step. Node loads the
|
||||
// whole import graph, stripping types, before the phase reads its first
|
||||
@@ -16,12 +16,7 @@ const PHASES: [phase: string, firstVariable: string][] = [
|
||||
|
||||
test.each(PHASES)("Node loads the %s phase", async (phase, firstVariable) => {
|
||||
const path = fileURLToPath(new URL(`../src/${phase}.ts`, import.meta.url));
|
||||
const result = await output(process.execPath, {
|
||||
args: [path],
|
||||
env: { PATH: process.env.PATH ?? "" },
|
||||
clearEnv: true,
|
||||
stdin: "null",
|
||||
});
|
||||
const result = await runCommand(process.execPath, [path], { PATH: process.env.PATH ?? "" });
|
||||
expect(result.success).toBe(false);
|
||||
expect(new TextDecoder().decode(result.stderr)).toContain(`${firstVariable} is not set`);
|
||||
expect(result.stderr).toContain(`${firstVariable} is not set`);
|
||||
});
|
||||
|
||||
+13
-8
@@ -4,8 +4,12 @@ import process from "node:process";
|
||||
import { assert, expect, test } from "vite-plus/test";
|
||||
import { parse } from "yaml";
|
||||
|
||||
import { isMapping, isNotFound, makeTempDir, output } from "#testing";
|
||||
import type { JsonObject } from "#testing";
|
||||
import { makeTempDir, runCommand } from "#testing";
|
||||
|
||||
type JsonObject = Record<string, unknown>;
|
||||
|
||||
const isMapping = (value: unknown): value is JsonObject =>
|
||||
typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
|
||||
const mapping = (value: unknown, at: string): JsonObject => {
|
||||
assert(isMapping(value), at);
|
||||
@@ -73,12 +77,13 @@ test("the source protection script stages immutable action files", async () => {
|
||||
const temp = await makeTempDir();
|
||||
const outputFile = `${temp}/output`;
|
||||
try {
|
||||
const result = await output("/bin/bash", {
|
||||
args: ["scripts/protect-source.sh"],
|
||||
env: { ACTION_PATH: process.cwd(), GITHUB_OUTPUT: outputFile, RUNNER_TEMP: temp },
|
||||
stderr: "piped",
|
||||
const result = await runCommand("/bin/bash", ["scripts/protect-source.sh"], {
|
||||
...process.env,
|
||||
ACTION_PATH: process.cwd(),
|
||||
GITHUB_OUTPUT: outputFile,
|
||||
RUNNER_TEMP: temp,
|
||||
});
|
||||
assert(result.success, new TextDecoder().decode(result.stderr));
|
||||
assert(result.success, result.stderr);
|
||||
|
||||
const path = (await readFile(outputFile, "utf8")).trim().slice(5);
|
||||
const files = [
|
||||
@@ -147,7 +152,7 @@ test("uses the standard Skills installer without custom discovery", async () =>
|
||||
assert(runner.includes("skills add obra/superpowers"));
|
||||
const removed = await stat("superpowers.ts").then(
|
||||
() => false,
|
||||
(error: unknown) => isNotFound(error),
|
||||
(error: unknown) => error instanceof Error && "code" in error && error.code === "ENOENT",
|
||||
);
|
||||
assert(removed, "superpowers.ts should be removed");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user
This new rule says
../is banned in every file, but it remains intests/entrypoints.test.ts,tests/workflow.test.ts, andsrc/run.ts; the listed import rule does not check URL strings. Remove those traversals or narrow and enforce the rule to match its actual scope.