Document that package.json aliases may target only direct subfolders of src (#21)
Follow-up to #20, which was merged before this landed. bot-agents has no `package.json` `imports` aliases (`src/` is flat and everything imports relatively), so no code changes. This records the new rule in the docs: - `.agents/memory/imports-aliases-barrels.md`: aliases may target only a direct subfolder of `src/` (its `index.ts` or a `#x/*` pattern); nothing outside `src/`, not `src/` itself, not deeper folders; such code imports relatively. - `.agents/memory/tests-layout-vitest.md`: `tests/` reach the public entry through a relative import, not a `#plugin` alias. - `.agents/skills/migrate-deno-repo/SKILL.md` and `AGENTS.md`: same rule. `vp check`, `vp test`, and `damem doctor` pass. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Reviewed-on: #21 Reviewed-by: bot <noreply@capsulizers.com> Co-authored-by: Danny Kim <temeddix@gmail.com> Co-committed-by: Danny Kim <temeddix@gmail.com>
This commit was merged in pull request #21.
This commit is contained in:
@@ -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-import-check`: `import-check`
|
||||
- `@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.
|
||||
@@ -1,20 +1,21 @@
|
||||
---
|
||||
description: bot-agents lint has prefer-readonly-parameter-types off (Node/web types can't satisfy it) and one scripts/** override for no-unsafe-enum-comparison; nothing else
|
||||
description: bot-agents lint has prefer-readonly-parameter-types and the size/complexity rules off (Deno had no limits) and no overrides; nothing else
|
||||
---
|
||||
|
||||
The repo follows [[lint-config-shape]] with two recorded exceptions, both
|
||||
present when the migration PR was approved:
|
||||
The repo follows [[lint-config-shape]] with one recorded exception, present
|
||||
when the migration PR was approved:
|
||||
|
||||
- `typescript/prefer-readonly-parameter-types` is `"off"` in
|
||||
`scripts/project-lint.ts`, as in the explicit-types template, because
|
||||
Node/web types (`Response`, `Stats`, `RequestInit`) can never satisfy it.
|
||||
- `vite.config.ts` has one `overrides` entry, for `scripts/**` only:
|
||||
`typescript/no-unsafe-enum-comparison` off, because the lint plugin compares
|
||||
AST node `type`s, which the AST types declare as an enum. This is the
|
||||
tooling exception [[lint-overrides-policy]] allows.
|
||||
- `vite.config.ts` has no `overrides`. The `scripts/**` override for
|
||||
`typescript/no-unsafe-enum-comparison` was removed (2026-10-16):
|
||||
`scripts/project-lint.ts` compares node types with `AST_NODE_TYPES` members
|
||||
from `@typescript-eslint/types` instead of string literals
|
||||
([[lint-overrides-policy]]).
|
||||
|
||||
Oxlint's default size limits apply to `src/`; the migration split `run.ts`
|
||||
into `run-context.ts`, `agents.ts`, and `agent-{claude,codex,opencode}.ts`,
|
||||
and `post.ts` into `post.ts` + `post-artifacts.ts`, rather than raising them.
|
||||
Sequential awaits in loops became recursion or a `reduce` chain to stay
|
||||
sequential. Add no other opt-out without the user.
|
||||
Size and complexity limits match Deno, which had none here, so every Oxlint
|
||||
limit rule is `"off"` ([[lint-config-shape]]). `run.ts` and `post.ts` keep
|
||||
main's single-file layout (2026-10-17 revert of the size splits). Sequential
|
||||
awaits in loops became recursion or a `reduce` chain (`no-await-in-loop`) to
|
||||
stay sequential. Add no other opt-out without the user.
|
||||
|
||||
+14
-7
@@ -1,5 +1,5 @@
|
||||
---
|
||||
description: bot-agents migration decisions: live @main action for six repos, JSON profiles, Node 24.2+, Deno kept only for agents, no @cunarist plugins needed
|
||||
description: bot-agents migration decisions: live @main action for six repos, JSON profiles, Node 24.2+, Deno kept only for agents, no eslint-plugin-* packages needed
|
||||
---
|
||||
|
||||
Decisions for `commons/bot-agents` (Capsulizers Gitea), from the Deno to
|
||||
@@ -12,16 +12,23 @@ Node + Vite+ migration (PR #20, `node-migration`, 2026-10-15/16):
|
||||
consumer (`uses: .../commons/bot-agents@<branch>`) on a throwaway PR.
|
||||
- **Profiles are JSON.** `profiles.yml` became `profiles.json` with the same
|
||||
content (encrypted `v1:` values unchanged), read with `JSON.parse`. That
|
||||
removed the only runtime dependency (`@std/yaml`); keep it dependency-free.
|
||||
- **Node 24.2+**, not 22: `src/post.ts` uses `import.meta.main`. `engines.node`
|
||||
is `>=24.2`, which the user accepted for this repo
|
||||
([[bot-agents-runtime-no-build]]).
|
||||
removed the only runtime dependency (`@std/yaml`); keep it free of runtime
|
||||
dependencies (`@eslint/core` and the other devDependencies serve only the
|
||||
tooling).
|
||||
- **Node 24.2+**, not 22: `src/post.ts` uses `import.meta.main`. There is no
|
||||
`engines` (user decision 2026-10-18): the `node:24-bookworm` job image
|
||||
decides the version ([[bot-agents-runtime-no-build]]).
|
||||
- **Deno stays in the action's global npm install** (`action.yml`), only
|
||||
because the agents still check Deno-based consumer repos (memona,
|
||||
memona-home). Remove it once those repos have moved; it is not a
|
||||
dependency of this repo's TypeScript.
|
||||
- **No `@cunarist/eslint-plugin-*`.** The Deno setup enabled no plugin, so
|
||||
this repo depends on none and CI passes without them being published.
|
||||
- **No `eslint-plugin-*` packages** (`eslint-plugin-explicit-types`,
|
||||
`eslint-plugin-import-structure`, `eslint-plugin-lit-robust`; unscoped
|
||||
since 2026-10-18, formerly `@cunarist/...`). The Deno setup enabled no
|
||||
plugin, so this repo depends on none and CI passes without them being
|
||||
published. Planned (2026-10-17): once the plugins are on npm, add
|
||||
`eslint-plugin-import-structure` (rule prefix `import-structure/`) here for
|
||||
its import checks.
|
||||
- **Stricter input checks were accepted** in the migration: Gitea `/user` and
|
||||
comment responses, the runtime manifest, and the outcome's profile type are
|
||||
validated; some type errors became `TypeError` with the same messages.
|
||||
@@ -10,22 +10,22 @@ dependency**. Hence `tsconfig.json` has `erasableSyntaxOnly` and
|
||||
properties, no decorators) and relative `.ts` imports.
|
||||
|
||||
- **Image:** the steps assume the `node:24-bookworm` job container. Node must
|
||||
be 24.2+ for `import.meta.main` in `post.ts`; `check.yml` prints
|
||||
`node --version` to confirm the cached image's minor.
|
||||
be 24.2+ for `import.meta.main` in `post.ts`. That image is the only Node
|
||||
pin (`package.json` has no `engines`); `check.yml` prints `node --version`
|
||||
to confirm the cached image's minor.
|
||||
- **Root-owned source copy:** `scripts/protect-source.sh` copies the action
|
||||
into `$RUNNER_TEMP` so an agent editing the checkout can't swap a later
|
||||
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,6 +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; 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.
|
||||
@@ -1,45 +0,0 @@
|
||||
---
|
||||
description: Every repo moved from Deno to Node + Vite+ in Oct 2026 because Cloudflare bought Deno; nothing goes back to Deno
|
||||
---
|
||||
|
||||
On 2026-10-12 the user moved every project from Deno to Node.js + Vite+
|
||||
(`vp`). Cloudflare bought Deno on 2026-10-09; the runtime gets roughly 12
|
||||
months of maintenance and Deno Deploy shuts down around April 2027. None of the
|
||||
repos used Deploy or KV, so the move is about the toolchain, not hosting.
|
||||
|
||||
Do not reintroduce `deno.json`, `deno.lock`, `Deno.*` APIs, `jsr:` or `npm:`
|
||||
specifiers, `mod.ts` entries, or `deno task` anywhere, docs and scripts
|
||||
included.
|
||||
|
||||
## Order the migration followed
|
||||
|
||||
1. The custom Deno lint plugins were ported to npm first, as
|
||||
`@cunarist/eslint-plugin-explicit-types`, `-import-check`, and `-lit`.
|
||||
`cunarist/eslint-plugin-explicit-types` is the template every other repo
|
||||
copies (config shape, `scripts/project-lint.ts`, AGENTS.md, notes).
|
||||
2. Then `commons/bot-agents` (six repos use it as an action), then the apps:
|
||||
home, eximium, stockvibe, brain, memona, memona-home.
|
||||
3. deno-practice, interview-practice, web-landing, and vemium were left out
|
||||
(archivable). Old `cunarist/deno-*` repos are archived one at a time, and
|
||||
only after their `eslint-plugin-*` replacement is published to npm.
|
||||
|
||||
## What "migrated" means
|
||||
|
||||
- Same behavior. A migration PR changes tooling, not features.
|
||||
- Lint coverage is at least what Deno enforced: [[lint-parity-never-shrinks]].
|
||||
- Only three config files and only `.ts`: [[toolchain-node-vite-plus]],
|
||||
[[ts-only-files-and-imports]].
|
||||
- Deno.test suites became Vitest: [[tests-layout-vitest]].
|
||||
|
||||
The custom plugins' Deno names map like this:
|
||||
|
||||
| Deno | npm |
|
||||
| ----------------------------------------------- | ---------------------------------------- |
|
||||
| `jsr:@cunarist/deno-lint-plugin-explicit-types` | `@cunarist/eslint-plugin-explicit-types` |
|
||||
| `jsr:@cunarist/deno-import-check` (`./lint`) | `@cunarist/eslint-plugin-import-check` |
|
||||
| `jsr:@cunarist/deno-lint-plugin-lit` | `@cunarist/eslint-plugin-lit` |
|
||||
| `scripts/deno-lint-plugin.ts` (per app) | `scripts/project-lint.ts` (per app) |
|
||||
|
||||
Until those packages are on npm, a repo that depends on them fails `npm ci` in
|
||||
CI; that is expected and stated in the PR, never worked around by committing
|
||||
tarballs or `file:` dependencies.
|
||||
@@ -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,48 +1,104 @@
|
||||
---
|
||||
description: Shared code comes through # aliases in package.json imports, each directory's entry is index.ts, barrels keep imports few; never ../ out of a directory
|
||||
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
|
||||
## `#` aliases own their targets
|
||||
|
||||
Shared code is imported through Node subpath imports declared in
|
||||
`package.json` `imports`, never by a relative path that climbs out of a
|
||||
directory:
|
||||
`package.json` `imports`. Node, TypeScript, Vite, Vite+'s config bundler, and
|
||||
Oxlint's plugin loader all resolve them natively, so `.ts` source runs
|
||||
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 as the general rule, and the "import-structure 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
|
||||
any other file owns only that file.
|
||||
- **No overlap.** No file may be owned by 2 aliases: no alias inside another
|
||||
alias's area, and no 2 aliases with the same target. The import-structure
|
||||
plugin's `enforce-layer-order` reports it once at the top of every file 2
|
||||
aliases claim. Fix by merging the aliases or moving files apart.
|
||||
- **Outside an area, only the exact alias reaches it.** A deeper alias
|
||||
specifier (`#a/one.ts`, `no-barrel-bypass`) and a relative path into the
|
||||
area (`./a/one.ts`, `no-relative-bypass`; `./b/f.ts` to a file alias,
|
||||
`prefer-alias-import`, fixed to the alias) are bypasses.
|
||||
- **Inside an area, relative imports of siblings and subfolders are free.**
|
||||
- **A `../` import is banned in every file**, owned or not (`no-parent-import`).
|
||||
Runtime file URLs (`new URL("../prompt.md", import.meta.url)`) are not
|
||||
imports and are allowed.
|
||||
Anything above a file needs an alias.
|
||||
- **Files no alias owns** need no `index.ts` and no alias, and import `./`
|
||||
siblings and subfolders freely.
|
||||
- Unchanged: no `/*` subpath patterns, the key order of `imports` is the
|
||||
layer order `enforce-layer-order` checks (add a new alias at the right
|
||||
layer), and entry files are `index.ts` only.
|
||||
|
||||
```jsonc
|
||||
"imports": {
|
||||
"#helpers": "./src/helpers/index.ts",
|
||||
"#components/*": "./src/components/*"
|
||||
"#a": "./a/index.ts", // owns a/**
|
||||
"#f": "./b/f.ts", // owns only b/f.ts
|
||||
"#a-sub": "./a/sub/index.ts", // no: inside #a
|
||||
"#a2": "./a/index.ts" // no: same target as #a
|
||||
}
|
||||
```
|
||||
|
||||
```ts
|
||||
import { typeNameTail } from "#helpers"; // yes
|
||||
import { typeNameTail } from "../helpers/ast.ts"; // no: import/no-relative-parent-imports
|
||||
// in main.ts, which no alias owns
|
||||
import { a } from "#a"; // yes
|
||||
import { one } from "#a/one.ts"; // no: deeper alias specifier
|
||||
import { one } from "./a/one.ts"; // no: relative path into #a
|
||||
import { f } from "#f"; // yes
|
||||
import { f } from "./b/f.ts"; // no: relative path to a file alias
|
||||
import { g } from "./b/g.ts"; // yes: unowned
|
||||
// in a/one.ts, owned by #a
|
||||
import { two } from "./two.ts"; // yes: sibling
|
||||
import { deep } from "./sub/deep.ts"; // yes: subfolder of the same area
|
||||
import { x } from "../x.ts"; // no: no ../ anywhere
|
||||
```
|
||||
|
||||
Node, TypeScript, Vite, Vite+'s config bundler, and Oxlint's plugin loader
|
||||
all resolve them natively, so `.ts` source runs unbuilt. `vp pack` inlines them,
|
||||
so they never reach package consumers.
|
||||
|
||||
- Deno's trailing-slash mappings (`"#components/"`) don't exist in Node; they
|
||||
become patterns, `"#components/*": "./src/components/*"`.
|
||||
- Aliases point at `.ts` files only. There is no `#package` alias
|
||||
([[ts-only-files-and-imports]]).
|
||||
- In repos linted by import-check, the key order of `imports` is the layer
|
||||
order `enforce-layer-order` checks; add a new alias at the right layer.
|
||||
- Lint `jsPlugins` entries in `vite.config.ts` stay path strings, even when
|
||||
the file has an alias.
|
||||
- Keep test-only helpers in their own alias (e.g. `#testing`) so they never
|
||||
land in a bundle.
|
||||
|
||||
## `index.ts` entries and barrels
|
||||
## Barrels
|
||||
|
||||
- Every directory's entry file is `index.ts`, never `mod.ts` (decided
|
||||
2026-10-12; import-check's `enforce-mod-file` now requires `index.ts`).
|
||||
- A directory used from outside exposes its API through its `index.ts`
|
||||
barrel, and callers import the barrel (`#ui`), not files inside it
|
||||
(`no-barrel-bypass`).
|
||||
- An area used from outside exposes its API through its alias target, and
|
||||
callers import the alias (`#ui`), not files inside it.
|
||||
- Barrels are also the standard fix for `import/max-dependencies`: one barrel
|
||||
import replaces many direct ones ([[lint-overrides-policy]]).
|
||||
- `import/no-cycle` is on; a barrel must not create a cycle.
|
||||
|
||||
Relative imports inside one directory (`./sibling.ts`) are fine, with the
|
||||
`.ts` extension written out.
|
||||
Relative imports are written with the `.ts` extension.
|
||||
|
||||
## `scripts/` is flat (user rule, 2026-10-16)
|
||||
|
||||
- `scripts/` has no subfolders and no `index.ts`.
|
||||
- Each tool is one file, `scripts/<name>.ts`, imported directly by file:
|
||||
`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 import-structure 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.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
description: vite.config.ts lint uses Oxlint categories at error with default limits, names no rule itself, and spreads configs from the plugins and scripts/project-lint.ts
|
||||
description: vite.config.ts lint uses Oxlint categories at error with Deno size limits, names no rule itself, and spreads configs from the plugins and scripts/project-lint.ts
|
||||
---
|
||||
|
||||
The lint config is short and category-driven (user decisions 2026-10-14/15):
|
||||
@@ -8,7 +8,7 @@ turn on whole categories, then list only what parity or a real need adds.
|
||||
```ts
|
||||
lint: {
|
||||
plugins: ["typescript", "unicorn", "oxc", "import"],
|
||||
jsPlugins: ["@cunarist/eslint-plugin-explicit-types", "./scripts/project-lint.ts"],
|
||||
jsPlugins: ["eslint-plugin-explicit-types", "./scripts/project-lint.ts"],
|
||||
categories: { correctness: "error", suspicious: "error", pedantic: "error", perf: "error" },
|
||||
rules: { ...explicitTypes.configs.concrete.rules, ...projectLint.configs.project.rules },
|
||||
ignorePatterns: ["src/generated/**"], // generated files only, see below
|
||||
@@ -19,14 +19,15 @@ lint: {
|
||||
- **Categories:** `correctness`, `suspicious`, `pedantic`, `perf` at `error`.
|
||||
`style` and `restriction` are never turned on wholesale; pick single rules
|
||||
from them in `configs.project`. Extra rules that categories bring are fine.
|
||||
- **Default limits stay** (2026-10-16): pedantic's `max-lines` 300,
|
||||
`max-lines-per-function` 50, `max-classes-per-file` 1,
|
||||
`max-dependencies` 10, `max-params` 3, `complexity` 20. Don't raise them to
|
||||
make code pass; refactor (see [[lint-overrides-policy]]). Apps follow this
|
||||
strictly. The plugin template already turns off `max-lines`,
|
||||
`max-lines-per-function`, `typescript/prefer-readonly-parameter-types`, and
|
||||
`typescript/no-unsafe-enum-comparison` because AST visitors fight them; do
|
||||
not add new opt-outs anywhere without asking the user.
|
||||
- **Size limits match Deno** (2026-10-17, replaces the 2026-10-16 "default
|
||||
limits stay"): repos behave as they did on Deno. bot-agents ran `deno lint`
|
||||
with no config, so it had no size or complexity limit, and every Oxlint
|
||||
limit rule (`complexity`, `max-lines`, `max-params`, `id-length`,
|
||||
`no-nested-ternary`, `max-lines-per-function`, `max-classes-per-file`,
|
||||
`max-depth`, `max-nested-callbacks`, `max-statements`,
|
||||
`import/max-dependencies`, `unicorn/max-nested-calls`) is `"off"` in
|
||||
`configs.project`. No file is split for size. Do not add new opt-outs
|
||||
anywhere without asking the user.
|
||||
- **`vite.config.ts` names no rule.** Every rule id, built-in ones included,
|
||||
lives in `configs.project` of `scripts/project-lint.ts`
|
||||
([[project-lint-plugin]]). `vite.config.ts` only spreads configs.
|
||||
@@ -39,8 +40,8 @@ lint: {
|
||||
Hand-written code is never ignored.
|
||||
- **No lint disable comments**: [[lint-no-disable-comments]].
|
||||
- **Parity:** [[lint-parity-never-shrinks]]. **Naming:** [[naming-id-match]].
|
||||
- Don't add a second import sorter; import-check's `enforce-import-order` is
|
||||
the only one, and Oxfmt's `sortImports` stays off.
|
||||
- Don't add a second import sorter; the import-structure plugin's
|
||||
`enforce-import-order` is the only one, and Oxfmt's `sortImports` stays off.
|
||||
|
||||
`perf` came last: in the template it flagged nine `.includes` on constant
|
||||
arrays, fixed as `ReadonlySet` + `.has()` rather than by turning a rule off.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
description: No lint overrides for source files; narrow ones only for scripts/**; generated files go in ignorePatterns; refactor (barrels, splits) instead of raising limits
|
||||
description: No lint overrides for source files; narrow ones only for scripts/**; generated files go in ignorePatterns; size limits match Deno, so nothing is split for size
|
||||
---
|
||||
|
||||
Final policy (2026-10-16). The user considers per-file `overrides` bad
|
||||
@@ -18,28 +18,16 @@ practice.
|
||||
|
||||
## Fix the code instead
|
||||
|
||||
Oxlint's defaults stay; violations are refactored without behavior change,
|
||||
covered by the tests, inside the same migration PR.
|
||||
Category rule violations are fixed in the code without behavior change,
|
||||
covered by the tests, inside the same migration PR; e.g. `prefer-set-has`
|
||||
becomes `const K: ReadonlySet<string> = new Set([...])` with `.has`.
|
||||
|
||||
| Rule | Usual fix |
|
||||
| ------------------------- | ------------------------------------------------------- |
|
||||
| `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-classes-per-file` | one class per file |
|
||||
| `max-params` | pass one options object |
|
||||
| `prefer-set-has` | `const K: ReadonlySet<string> = new Set([...])`, `.has` |
|
||||
Size and complexity limits are not refactored for: they match what Deno
|
||||
enforced (2026-10-17, replacing the earlier "refactor to Oxlint's defaults"),
|
||||
which for bot-agents is none, so those rules are `"off"`
|
||||
([[lint-config-shape]]).
|
||||
|
||||
```ts
|
||||
// before: 14 imports, over the limit of 10
|
||||
import { a } from "#ui/button";
|
||||
import { b } from "#ui/dialog";
|
||||
// after: one barrel
|
||||
import { a, b } from "#ui";
|
||||
```
|
||||
|
||||
Measured on memona before the move (Oxlint 1.87, default limits): 921
|
||||
pedantic hits in 274 files, of which `max-lines-per-function` 206,
|
||||
`max-lines` 145, `max-classes-per-file` 59, `max-dependencies` 45 (median 14
|
||||
imports, worst 29). The user chose refactoring over raising limits or
|
||||
matching memona's old hugoalh limits (2048 lines per file, no function limit).
|
||||
Alias rule: an alias may point anywhere and owns its target (a folder's
|
||||
`index.ts` owns the folder and below, any other file owns only itself); no
|
||||
two aliases may own the same file, and `../` is banned everywhere
|
||||
([[imports-aliases-barrels]]).
|
||||
|
||||
@@ -29,7 +29,7 @@ Before removing any line from `configs.project`, find what else covers it.
|
||||
in packages.
|
||||
- **Vacuous:** JSX rules (no `.tsx` exists) and Deno-only rules.
|
||||
- **`@hugoalh` rules** map to Oxlint built-ins in `configs.project`.
|
||||
- **import-check's CLI cycle check** → `import/no-cycle`.
|
||||
- **deno-import-check's CLI cycle check** → `import/no-cycle`.
|
||||
|
||||
The template's list in `configs.project` is the baseline to copy: `curly`,
|
||||
`guard-for-in`, `no-console`, `no-empty`, `no-regex-spaces`,
|
||||
|
||||
@@ -1,40 +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.
|
||||
- **Gotcha:** the obvious `await import("lit-analyzer/cli.js")` fails
|
||||
`project/no-non-ts-imports` (a `.js` subpath). Import the package entry, or
|
||||
spawn the package's bin with `node:child_process`, instead.
|
||||
- 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.
|
||||
@@ -1,46 +1,29 @@
|
||||
---
|
||||
description: package.json key order is metadata, scripts, exports, imports, devDependencies, dependencies; standard fields only; no peerDependencies or packageManager
|
||||
description: package.json keys follow Oxfmt's standard order (vp check --fix); standard fields only; no peerDependencies or packageManager
|
||||
---
|
||||
|
||||
## Key order (2026-10-14)
|
||||
## Key order (2026-10-16)
|
||||
|
||||
Metadata first, then `scripts`, `exports`, `imports`, `devDependencies`,
|
||||
`dependencies`. Oxfmt would re-sort the file, so `vite.config.ts` sets
|
||||
`fmt: { sortPackageJson: false }`; keep it, and keep the order by hand.
|
||||
Oxfmt's standard `package.json` sorting is accepted: there is no
|
||||
`fmt: { sortPackageJson: false }`, and `vp check --fix` sets the order. This
|
||||
replaces the earlier hand-written order (metadata, `scripts`, `exports`,
|
||||
`imports`, `devDependencies`, `dependencies`).
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"name": "@cunarist/eslint-plugin-x",
|
||||
"version": "0.1.0",
|
||||
"description": "...",
|
||||
"keywords": ["eslint", "eslint-plugin", "eslintplugin", "oxlint"],
|
||||
"license": "MIT",
|
||||
"repository": { "type": "git", "url": "git+https://github.com/..." },
|
||||
"type": "module",
|
||||
"files": ["dist"],
|
||||
"engines": { "node": ">=22" },
|
||||
"eslint-doc-generator": {/* tool options that can only live here */},
|
||||
"scripts": { "docs": "eslint-doc-generator" },
|
||||
"exports": { ".": "./dist/index.js" },
|
||||
"imports": { "#helpers": "./src/helpers/index.ts" },
|
||||
"devDependencies": { "vite-plus": "^1.1.0" },
|
||||
"dependencies": {},
|
||||
}
|
||||
```
|
||||
|
||||
The `imports` key order can carry meaning: import-check's
|
||||
The `imports` key order can carry meaning: the import-structure plugin's
|
||||
`enforce-layer-order` reads it as the layer order. Never reorder it casually.
|
||||
|
||||
## Fields
|
||||
|
||||
- **Standard fields only.** `engines` has only `node` (never `engines.bun`).
|
||||
- **Standard fields only.** App repos have no `engines` field (user decision
|
||||
2026-10-18; images pin Node, [[toolchain-node-vite-plus]]). Published
|
||||
packages keep `engines` with only `node` (never `engines.bun`).
|
||||
A tool-owned key (`eslint-doc-generator`) is allowed only because the tool
|
||||
reads nothing else and no other config file is allowed.
|
||||
- **No `packageManager`.** npm is detected from `package-lock.json`.
|
||||
- **No `peerDependencies`, ever** (user preference, 2026-10-14). If something
|
||||
is needed at runtime, it goes in `dependencies`.
|
||||
- **`dependencies`** = what runs or is type-referenced at runtime by consumers
|
||||
(the lit plugin ships `parse5`, `oxc-parser`, `oxc-resolver`, and
|
||||
(the lit-robust plugin ships `parse5`, `oxc-parser`, `oxc-resolver`, and
|
||||
`@eslint/core` for its public types; they stay external, never bundled).
|
||||
- **`devDependencies`** = everything else: `vite-plus`, `eslint`,
|
||||
`@typescript-eslint/parser`, `eslint-doc-generator`, `esbuild`,
|
||||
@@ -49,3 +32,9 @@ The `imports` key order can carry meaning: import-check's
|
||||
|
||||
Refresh the lockfile with `npm install` (or `npm install
|
||||
--package-lock-only` for a version bump) and commit it with the change.
|
||||
|
||||
Alias rule: an alias may point anywhere and owns its target (a folder's
|
||||
`index.ts` owns the folder and everything below it; any other file owns only
|
||||
itself). No two aliases may own the same file, outside an area only the exact
|
||||
alias reaches it, and `../` is banned in every file
|
||||
([[imports-aliases-barrels]]).
|
||||
|
||||
@@ -11,11 +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-non-ts-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.
|
||||
|
||||
Rules and plugin stay in this one file; don't add `scripts/rules/`.
|
||||
`scripts/` stays flat; don't add `scripts/rules/`. If the plugin ever needs
|
||||
more files, they sit flat beside it and share the prefix
|
||||
(`project-lint-modules.ts`, ...); `scripts/project-lint.ts` keeps the plugin
|
||||
object and the rule list.
|
||||
|
||||
```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/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, import-check, lit) are published
|
||||
together, with a token the user provides; the old `deno-*` repos are archived
|
||||
only after that, one at a time.
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
description: Vitest via vp test; unit tests sit next to code as *.test.ts, regression/integration tests in tests/ via the public entry; scripts/ has no tests
|
||||
description: Vitest via vp test; unit tests sit next to code as *.test.ts, regression/integration tests in tests/ via the public entry (through its # alias, never ../); scripts/ has no tests
|
||||
---
|
||||
|
||||
Decided 2026-10-14 for all repos.
|
||||
@@ -11,12 +11,15 @@ Decided 2026-10-14 for all repos.
|
||||
config needed. Packages publish only `dist/`, so colocated tests cost nothing
|
||||
in the tarball.
|
||||
- **Regression and integration tests** live in the top-level `tests/` folder
|
||||
and use only the public entry point (an alias like `#plugin`), never
|
||||
internals. `vp test tests` runs just these.
|
||||
and use only the public entry point, imported through its `#` alias (e.g.
|
||||
`#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/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
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
---
|
||||
description: Node.js 22+ with Vite+ (vp) and npm; only package.json, tsconfig.json, vite.config.ts plus a committed package-lock.json
|
||||
description: Node.js (no engines in apps; images pin 24) with Vite+ (vp) and npm; only package.json, tsconfig.json, vite.config.ts plus a committed package-lock.json
|
||||
---
|
||||
|
||||
## The stack
|
||||
|
||||
- **Runtime:** Node.js 22 or newer. `engines` is `{ "node": ">=22" }` and
|
||||
nothing else. Containers and the publish job use Node 24 (LTS); a repo may
|
||||
need more where a feature demands it (bot-agents uses Node 24.2+).
|
||||
- **Runtime:** Node.js. App repos have no `engines` field (user decision
|
||||
2026-10-18): the container or runner image decides the version, so the
|
||||
Containerfile base image (`node:24-alpine`) and CI's `node-version: "24"`
|
||||
are the pins. Published packages (the lint plugins) keep their own
|
||||
`engines.node` floor. `npm run lit-check` runs
|
||||
`node scripts/lit-check.ts`, which needs Node 22.18+ (unflagged type
|
||||
stripping and `module.registerHooks`); Node 24 satisfies it. Without
|
||||
`engines` or a `.node-version`, local `vp` uses its default Node
|
||||
(`vp env default`). bot-agents has no Containerfile or setup step: the
|
||||
self-hosted runner's `node:24-bookworm` job image runs the action and its
|
||||
checks, and `src/post.ts` needs 24.2+ (`import.meta.main`).
|
||||
- **Bun is not supported** (dropped 2026-10-15). No Bun CI legs, docs, or
|
||||
runtime branches. Oxlint's `RuleTester` refuses to run under Bun anyway, and
|
||||
`vp` brings its own Node, the way uv brings its own Python.
|
||||
@@ -44,8 +52,8 @@ before any push. CI installs the toolchain with `voidzero-dev/setup-vp`.
|
||||
|
||||
- `vp check` does **not** report `isolatedDeclarations` errors (TS9010); only
|
||||
`vp pack` does, so packages run `vp pack` in CI on every PR.
|
||||
- Oxfmt sorts `package.json` keys by default, which destroys the required key
|
||||
order. Set `fmt: { sortPackageJson: false }` ([[package-json-shape]]).
|
||||
- Oxfmt sorts `package.json` keys by default; that order is accepted, so
|
||||
there is no `fmt` option for it ([[package-json-shape]]).
|
||||
- Oxlint and Oxfmt skip what `.gitignore` lists; that is the only reason
|
||||
`dist/` and `node_modules/` are not linted. Keep build output gitignored.
|
||||
- Vite+ bundles `vite.config.ts` itself and resolves `#` imports in it.
|
||||
|
||||
@@ -1,28 +1,39 @@
|
||||
---
|
||||
description: Every file is .ts (no js/mjs/cjs/jsx/tsx, configs included) and source imports only .ts; CSS comes from HTML, JSON/SVG become .ts modules
|
||||
description: Every file is .ts (no js/mjs/cjs/jsx/tsx, configs included); imports name only .ts files or .js/.mts/.mjs package subpaths; CSS comes from HTML, JSON/SVG become .ts modules
|
||||
---
|
||||
|
||||
The user's rule, from the Deno days and restated for Node (2026-10-12): "No
|
||||
JS, not even JSX/TSX, only TS." Source, scripts, tests, and configs are all
|
||||
plain `.ts`. Build output in a gitignored folder is the only JS.
|
||||
|
||||
`tsconfig.json`: no `allowJs`, no `jsx`, and the template's strict flags
|
||||
(`verbatimModuleSyntax`, `erasableSyntaxOnly`, `allowImportingTsExtensions`,
|
||||
`isolatedDeclarations` in packages, `exactOptionalPropertyTypes`,
|
||||
`noUncheckedIndexedAccess`). If a tool or framework seems to need a JS or TSX
|
||||
file, stop and flag it instead of adding one.
|
||||
`tsconfig.json`: no `allowJs`, no `jsx`, and exactly this strictness set (user
|
||||
decision, 2026-10-18): `strict` (written out), `noUncheckedIndexedAccess`,
|
||||
`noPropertyAccessFromIndexSignature`, `noImplicitReturns`, `noUnusedLocals`,
|
||||
`noUnusedParameters`, `noImplicitOverride`, `noFallthroughCasesInSwitch`,
|
||||
`erasableSyntaxOnly`, and `allowUnreachableCode: false` plus
|
||||
`allowUnusedLabels: false` (user decision, 2026-10-18; on in the Deno setup),
|
||||
plus the mechanical `verbatimModuleSyntax`, `allowImportingTsExtensions`, and
|
||||
`noEmit` (packages also keep `declaration` and `isolatedDeclarations`).
|
||||
`exactOptionalPropertyTypes` is off everywhere: write `{ key: maybeUndefined }`
|
||||
rather than leaving the key out to please it. If a tool or framework seems to
|
||||
need a JS or TSX file, stop and flag it instead of adding one.
|
||||
|
||||
## Only `.ts` imports (2026-10-14)
|
||||
## No asset imports (2026-10-14, widened 2026-10-16 and 2026-10-17)
|
||||
|
||||
`project/no-non-ts-imports` (in `scripts/project-lint.ts`) fails:
|
||||
`project/no-asset-imports` (in `scripts/project-lint.ts`; it was
|
||||
`no-non-ts-imports` before 2026-10-16) fails:
|
||||
|
||||
- a relative, absolute, `#`, or package-subpath specifier whose last segment
|
||||
has a non-`.ts` extension: `.css`, `.json`, `.svg`, `.js`, `.mjs`, `.tsx`;
|
||||
has an extension other than `.ts`, `.js`, `.mts`, or `.mjs`: `.css`,
|
||||
`.json`, `.svg`, `.jsx`, `.tsx`, and every other asset;
|
||||
- any `?query` suffix (`?raw`, `?url`, `?inline`);
|
||||
- any import attributes (`with { type: "json" }`, `with { type: "text" }`).
|
||||
|
||||
Bare packages (`lit`, `@types/json-schema`, `lodash.debounce`), `#` aliases,
|
||||
and `node:` builtins pass. `@fortawesome/x/svgs/a.svg` fails.
|
||||
`.js`, `.mts`, and `.mjs` are allowed (user decision, 2026-10-17) for package
|
||||
subpaths such as `lit/decorators.js` or `pdfjs-dist/legacy/build/pdf.mjs`;
|
||||
our own files are still `.ts` only. `.jsx` and `.tsx` stay banned. Bare
|
||||
packages (`lit`, `@types/json-schema`, `lodash.debounce`), `#` aliases, and
|
||||
`node:` builtins pass. `@fortawesome/x/svgs/a.svg` fails.
|
||||
|
||||
```ts
|
||||
import "./theme.css"; // no
|
||||
@@ -37,12 +48,12 @@ 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`
|
||||
and fails on drift (the plugin repos do this for `meta.name`/`meta.version`).
|
||||
- **Reading a file is not importing it.** `readFileSync` + `JSON.parse` is
|
||||
fine in tooling and tests.
|
||||
- **A tool's `.js` entry:** don't `import("pkg/cli.js")`; use the package
|
||||
entry or spawn the package's bin ([[lit-analyzer-ts6]]).
|
||||
- **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.
|
||||
|
||||
Reference in New Issue
Block a user