@webjsdev/cli 0.10.70 → 0.10.71

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/webjs.js CHANGED
@@ -99,6 +99,8 @@ const USAGE = `webjs commands:
99
99
  webjs elision [--json] [--verify] Report which component modules are elided and why each shipped one ships;
100
100
  --verify diffs SSR output with elision on vs off (exits non-zero on a divergence)
101
101
  webjs mcp Start the read-only MCP server (routes / actions / components / elision / check)
102
+ webjs source <Export> [--pkg <name>] Print the signature plus doc comment of one export from the installed @webjsdev/*
103
+ packages (typed declarations first, the authored JSDoc as the fallback); exit 1 on a miss
102
104
  webjs doctor [--json] [--strict] Verify project health (Node, tsconfig, env, vendor pins, importmap coherence, @webjsdev versions, git hook, page/layout elision, component elision, un-versioned stylesheet links).
103
105
  --json emits the structured results (with stable codes). --strict additionally fails on every remaining warning.
104
106
  Per-check severity is CONFIG: map a code to off/warn/error under "webjs": { "doctor": { "gate": {...} } }
@@ -326,6 +328,19 @@ const HELP = {
326
328
  summary: 'Start the read-only MCP server (routes / actions / components / elision / check + a docs/source knowledge layer).',
327
329
  examples: ['webjs mcp'],
328
330
  },
331
+ source: {
332
+ usage: 'webjs source <Export> [--pkg <name>]',
333
+ summary: 'Print one framework export\'s signature plus the doc comment above it, read from the installed @webjsdev/* packages (the typed declarations first, the authored JSDoc when none carries a doc; every overload). The cheap way to check a contract before opening the source file.',
334
+ options: [
335
+ { flag: '--pkg <name>', description: 'Search one package only: core, server, cli, ui or intellisense.' },
336
+ ],
337
+ notesTitle: 'Exit status',
338
+ notes: [
339
+ '0 when at least one declaration printed; 1 on a miss (the message names the packages that were searched).',
340
+ 'The read-only MCP `source` tool returns the same text for { export: "<Export>" }.',
341
+ ],
342
+ examples: ['webjs source createAuth', 'webjs source optimistic --pkg core', 'webjs source getFileStore'],
343
+ },
329
344
  version: {
330
345
  usage: 'webjs version',
331
346
  summary: 'Print the installed @webjsdev/cli version. Also available as webjs --version / -v.',
@@ -1968,6 +1983,29 @@ Full docs: https://webjs.dev/docs`);
1968
1983
  ` --from PROVIDER CDN to resolve through. One of: ${[...SUPPORTED_PROVIDERS].join(', ')}. Default: jspm.`);
1969
1984
  process.exit(1);
1970
1985
  }
1986
+ case 'source': {
1987
+ // One export's signature plus its doc comment from the installed
1988
+ // @webjsdev/* packages (#1623): the same lookup the MCP `source` tool
1989
+ // runs for its `export` arg, so a Bash-only agent (no MCP) checks a
1990
+ // contract for the price of the declaration instead of the whole file.
1991
+ // Read-only: it reads the resolved package roots and loads no module.
1992
+ const name = rest.find((a) => !a.startsWith('--'));
1993
+ const pkgAt = rest.indexOf('--pkg');
1994
+ const pkg = pkgAt >= 0 ? rest[pkgAt + 1] : undefined;
1995
+ if (!name) {
1996
+ console.error('usage: webjs source <Export> [--pkg core|server|cli|ui|intellisense]');
1997
+ process.exit(1);
1998
+ }
1999
+ const { lookupExport, resolveFrameworkRoots } = await import('@webjsdev/mcp');
2000
+ const { existsSync, readdirSync } = await import('node:fs');
2001
+ const { readFile } = await import('node:fs/promises');
2002
+ const roots = resolveFrameworkRoots(process.cwd(), { exists: existsSync });
2003
+ const readdir = (d) => readdirSync(d, { withFileTypes: true }).map((e) => ({ name: e.name, isDir: e.isDirectory() }));
2004
+ const out = await lookupExport({ roots, readFile, readdir }, name, pkg);
2005
+ console.log(out);
2006
+ if (!/^@webjsdev\//m.test(out)) process.exit(1);
2007
+ break;
2008
+ }
1971
2009
  case 'mcp': {
1972
2010
  // Read-only MCP server (#262, #415) over stdio. STDOUT is the JSON-RPC
1973
2011
  // channel, so nothing here may write to stdout: the data functions are
package/lib/app-icon.js CHANGED
@@ -43,13 +43,3 @@ export function appManifest(name) {
43
43
  icons: [{ src: '/icon.svg', sizes: 'any', type: 'image/svg+xml' }],
44
44
  }, null, 2) + '\n';
45
45
  }
46
-
47
- /**
48
- * Whether an SVG is the WebJs brand mark earlier scaffolds shipped as
49
- * `public/favicon.svg` (a rounded square with the gallery's grey gradient).
50
- * Apps made before the placeholder still serve it as their favicon.
51
- * @param {string} svg
52
- */
53
- export function isLegacyBrandFavicon(svg) {
54
- return /aria-label="WebJs"/.test(svg) && /<linearGradient id="wj"/.test(svg);
55
- }
package/lib/create.js CHANGED
@@ -585,9 +585,13 @@ export async function scaffoldApp(name, cwd, opts = {}) {
585
585
  // layout already writes asset(), so a fresh app is green on day one.
586
586
  // Two checks are fatal with no entry here at all, NODE_VERSION and
587
587
  // TSCONFIG_ERASABLE, because either would 500 the app at runtime;
588
- // everything else keeps its default warn. Add a code with "off" to
588
+ // DARK_THEME_UNREACHABLE is at error too: AGENTS.md mandates light-dark()
589
+ // tokens, the generated layout writes them, and a layout that drops them
590
+ // leaves the stylesheet's .dark block dead and the OS setting ignored
591
+ // (#1628); elsewhere it is a design convention and stays a warning.
592
+ // Everything else keeps its default warn. Add a code with "off" to
589
593
  // silence it, or "error" to make it fatal too.
590
- doctor: { gate: { UNMARKED_ASSET_LINKS: 'error' } },
594
+ doctor: { gate: { UNMARKED_ASSET_LINKS: 'error', DARK_THEME_UNREACHABLE: 'error' } },
591
595
  // The dependency audit's allowlist (#1492), the ONE place an accepted
592
596
  // advisory is listed, each with the reason it is safe. `webjs audit`
593
597
  // (the CI step below) fails on every other advisory at `level` or above,
@@ -51,6 +51,7 @@ export const DOCTOR_CODES = {
51
51
  'Asset urls (unmarked stylesheet links)': 'UNMARKED_ASSET_LINKS',
52
52
  'workspace-overrides': 'WORKSPACE_OVERRIDES',
53
53
  'app-icon': 'APP_ICON',
54
+ 'dark-theme': 'DARK_THEME_UNREACHABLE',
54
55
  };
55
56
 
56
57
  /**
@@ -1,6 +1,6 @@
1
1
  import { existsSync, readFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
- import { PLACEHOLDER_MARKER, isLegacyBrandFavicon } from '../../app-icon.js';
3
+ import { PLACEHOLDER_MARKER } from '../../app-icon.js';
4
4
 
5
5
  /**
6
6
  * @typedef {import('../codes.js').DoctorResult} DoctorResult
@@ -12,11 +12,9 @@ function read(file) {
12
12
  }
13
13
 
14
14
  /**
15
- * Warn when the app's favicon is still the one `webjs create` shipped: the
16
- * neutral `app/icon.svg` placeholder, or the WebJs brand mark earlier
17
- * scaffolds put at `public/favicon.svg`. Either way every app made from the
18
- * scaffold shows the same tab icon, which reads as a demo rather than a
19
- * product. An app with no `app/` directory (a library, the api template with
15
+ * Warn when the app's favicon is still the neutral `app/icon.svg` placeholder
16
+ * `webjs create` shipped: every app made from the scaffold would show the same
17
+ * tab icon, which reads as a demo rather than a product. An app with no `app/` directory (a library, the api template with
20
18
  * no pages) passes.
21
19
  *
22
20
  * @param {string} appDir
@@ -25,12 +23,9 @@ function read(file) {
25
23
  export function checkAppIcon(appDir) {
26
24
  const name = 'app-icon';
27
25
  if (!existsSync(join(appDir, 'app'))) return { name, status: 'pass', message: 'no app/ directory to analyse' };
28
- const fix = 'Replace app/icon.svg with an icon for this app (a simple symbol for what it is, in its own colours, legible at 16px), and delete public/favicon.svg plus any metadata.icons that still points at it.';
26
+ const fix = 'Replace app/icon.svg with an icon for this app (a simple symbol for what it is, in its own colours, legible at 16px).';
29
27
  if (read(join(appDir, 'app', 'icon.svg')).includes(PLACEHOLDER_MARKER)) {
30
28
  return { name, status: 'warn', message: 'app/icon.svg is still the scaffold placeholder icon', fix };
31
29
  }
32
- if (isLegacyBrandFavicon(read(join(appDir, 'public', 'favicon.svg')))) {
33
- return { name, status: 'warn', message: 'public/favicon.svg is still the WebJs mark an earlier scaffold shipped', fix };
34
- }
35
30
  return { name, status: 'pass', message: 'the app has its own icon (or declares none)' };
36
31
  }
@@ -0,0 +1,91 @@
1
+ import { existsSync, readdirSync } from 'node:fs';
2
+ import { readFile } from 'node:fs/promises';
3
+ import { join, relative } from 'node:path';
4
+
5
+ /**
6
+ * @typedef {import('../codes.js').DoctorResult} DoctorResult
7
+ */
8
+
9
+ const ROOT_LAYOUT = /^app\/layout\.(?:js|mjs|ts|mts)$/;
10
+ /** Stylesheet sources: the Tailwind input and any hand-written sheet; never the compiled output. */
11
+ const STYLE_SOURCE = /^(?:public|styles)\/(?:.+\/)?[^/]+\.css$/;
12
+ const COMPILED_OR_VENDORED = /(?:^|\/)(?:tailwind\.css|node_modules\/|\.webjs\/|dist\/)/;
13
+ /** A colour token definition: the two every palette starts from. */
14
+ const TOKEN_DEF = /--(?:background|foreground)\s*:/;
15
+ /** Any of these makes the dark half reachable. */
16
+ const DUAL_TOKENS = /light-dark\s*\(/;
17
+ const SCHEME_QUERY = /prefers-color-scheme/;
18
+ /** A head script that applies a saved or detected theme. */
19
+ const THEME_SCRIPT = /<script\b[^>]*>[\s\S]*?(?:prefers-color-scheme|localStorage|data-theme|classList\.(?:add|toggle)\(\s*['"`]dark['"`]|dataset\.theme)[\s\S]*?<\/script>/;
20
+
21
+ /** CSS and JS block comments and line comments, so a commented-out token or hint does not count. */
22
+ function stripComments(text) {
23
+ return text.replace(/\/\*[\s\S]*?\*\//g, '').replace(/^\s*\/\/.*$/gm, '');
24
+ }
25
+
26
+ /**
27
+ * The app's colour-token sources: the root layout (where the generated app
28
+ * writes its palette) and the stylesheet sources under `public/` and `styles/`.
29
+ * @param {string} appDir
30
+ * @returns {Promise<Array<{ rel: string, content: string }>>}
31
+ */
32
+ async function tokenSources(appDir) {
33
+ /** @type {Array<{ rel: string, content: string }>} */
34
+ const out = [];
35
+ const read = async (rel) => {
36
+ try { out.push({ rel, content: await readFile(join(appDir, rel), 'utf8') }); } catch { /* unreadable: not a source */ }
37
+ };
38
+ for (const ext of ['ts', 'js', 'mts', 'mjs']) {
39
+ const rel = `app/layout.${ext}`;
40
+ if (existsSync(join(appDir, rel))) { await read(rel); break; }
41
+ }
42
+ for (const dir of ['public', 'styles']) {
43
+ const abs = join(appDir, dir);
44
+ if (!existsSync(abs)) continue;
45
+ for (const e of readdirSync(abs, { recursive: true, withFileTypes: true })) {
46
+ if (!e.isFile() || !e.name.endsWith('.css')) continue;
47
+ const rel = relative(appDir, join(e.parentPath || abs, e.name)).split('\\').join('/');
48
+ if (STYLE_SOURCE.test(rel) && !COMPILED_OR_VENDORED.test(rel)) await read(rel);
49
+ }
50
+ }
51
+ return out;
52
+ }
53
+
54
+ /**
55
+ * `DARK_THEME_UNREACHABLE` (#1628): the app defines colour tokens but nothing
56
+ * applies a dark half. The scaffold's stylesheet keeps the ui kit's `.dark`
57
+ * token block, and `AGENTS.md` asks for `light-dark()` tokens under
58
+ * `color-scheme: light dark`; a layout that uses neither, has no
59
+ * `prefers-color-scheme` rule and runs no theme script ships dead CSS and
60
+ * ignores the OS setting, which no light-mode screenshot reveals. A design
61
+ * convention rather than a runtime break, so it lives in doctor (WARN by
62
+ * default) and the scaffold gates it `error` because its AGENTS.md mandates
63
+ * the tokens. Silent when the app defines no colour tokens at all.
64
+ * @param {string} appDir
65
+ * @returns {Promise<DoctorResult>}
66
+ */
67
+ export async function checkDarkTheme(appDir) {
68
+ const name = 'dark-theme';
69
+ if (!existsSync(join(appDir, 'app'))) {
70
+ return { name, status: 'pass', message: 'no app/ directory to analyse' };
71
+ }
72
+ const sources = await tokenSources(appDir);
73
+ const defining = sources.filter((s) => TOKEN_DEF.test(stripComments(s.content)));
74
+ if (!defining.length) {
75
+ return { name, status: 'pass', message: 'no colour tokens defined (nothing to reach)' };
76
+ }
77
+ const all = sources.map((s) => stripComments(s.content)).join('\n');
78
+ const layout = sources.find((s) => ROOT_LAYOUT.test(s.rel));
79
+ if (DUAL_TOKENS.test(all) || SCHEME_QUERY.test(all) || (layout && THEME_SCRIPT.test(layout.content))) {
80
+ return { name, status: 'pass', message: 'the dark half of the colour tokens is reachable' };
81
+ }
82
+ const where = defining.map((s) => s.rel).join(' and ');
83
+ return {
84
+ name,
85
+ status: 'warn',
86
+ message:
87
+ `colour tokens are defined in ${where} with a light value only: nothing applies a dark half (no light-dark(), no prefers-color-scheme rule, no theme script in the root layout), so the OS dark setting is ignored and any .dark block is dead CSS`,
88
+ fix:
89
+ 'Write each colour token ONCE as light-dark(LIGHT, DARK) under `color-scheme: light dark` in the root layout (the block in .agents/skills/webjs/references/styling.md), or give the tokens an `@media (prefers-color-scheme: dark)` half, or add a head <script> that applies the saved theme (data-theme / the .dark class). Then check the app in a real browser with the OS in dark mode.',
90
+ };
91
+ }
@@ -13,6 +13,7 @@ import { checkUnmarkedAssetLinks } from './probes/unmarked-asset-links.js';
13
13
  import { checkFrameworkResolves, checkFrameworkLinks } from './probes/framework-resolves.js';
14
14
  import { checkWorkspaceOverrides } from './probes/workspace-overrides.js';
15
15
  import { checkAppIcon } from './probes/app-icon.js';
16
+ import { checkDarkTheme } from './probes/dark-theme.js';
16
17
 
17
18
  /**
18
19
  * @typedef {import('./codes.js').DoctorResult} DoctorResult
@@ -68,6 +69,7 @@ export async function runDoctorChecks(appDir, opts = {}) {
68
69
  checkUnmarkedAssetLinks(appDir),
69
70
  Promise.resolve(checkWorkspaceOverrides(appDir)),
70
71
  Promise.resolve(checkAppIcon(appDir)),
72
+ checkDarkTheme(appDir),
71
73
  ]);
72
74
  // Attach the stable machine code to every result (#975). Centralized here so
73
75
  // each check function stays free of the code-contract concern.
@@ -23,11 +23,9 @@
23
23
  * - the `dev` / `start` scripts force `bun --bun` (the server is Bun),
24
24
  * - every other command stays `bun run` / `webjs ...` (runs on Node via the
25
25
  * `webjs` bin's `#!/usr/bin/env node` shebang),
26
- * - the Dockerfile is a pure `oven/bun:1-slim` base (#595, #1606). This is safe as of
27
- * `@webjsdev/cli@0.10.20` (#570): `webjs db migrate` resolves drizzle-kit
28
- * and runs it under Bun (no `npx`), so a Node-less image works. (Before
29
- * #570 shipped as `latest`, this stayed on `node:24-alpine` + a copied Bun
30
- * binary, since the installed CLI could still shell `npx`.)
26
+ * - the Dockerfile is a pure `oven/bun:1-slim` base (#595, #1606): `webjs db
27
+ * migrate` resolves drizzle-kit and runs it under Bun (no `npx`, #570), so
28
+ * a Node-less image works.
31
29
  */
32
30
 
33
31
  /**
@@ -97,15 +95,11 @@ export function bunifyProse(s) {
97
95
  * Rewrite the scaffolded Dockerfile for Bun.
98
96
  *
99
97
  * Base decision (acceptance criterion): a pure `oven/bun:1-slim` image (no Node).
100
- * Safe as of `@webjsdev/cli@0.10.20` (#570): `webjs db` / `webjs test` resolve
101
- * their tools (drizzle-kit, wtr) and spawn them with the current runtime instead
102
- * of `npx`, so the boot-time `webjs db migrate` runs under Bun with no Node
103
- * toolchain. (Before #570 was the published `latest`, this stayed on a
104
- * `node:24-alpine` base with a copied Bun binary, since the installed CLI could
105
- * still shell `npx`, which a pure Bun image lacks. #595 flipped it once the
106
- * npx-free CLI shipped.) `oven/bun:1` is Debian-based: `ca-certificates` ship in
107
- * the image, and SQLite uses the built-in bun:sqlite (no native module), so no
108
- * build toolchain is needed.
98
+ * `webjs db` / `webjs test` resolve their tools (drizzle-kit, wtr) and spawn
99
+ * them with the current runtime instead of `npx` (#570), so the boot-time
100
+ * `webjs db migrate` runs under Bun with no Node toolchain. `oven/bun:1` is
101
+ * Debian-based: `ca-certificates` ship in the image, and SQLite uses the
102
+ * built-in bun:sqlite (no native module), so no build toolchain is needed.
109
103
  *
110
104
  * @param {string} s
111
105
  * @returns {string}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webjsdev/cli",
3
- "version": "0.10.70",
3
+ "version": "0.10.71",
4
4
  "type": "module",
5
5
  "description": "The CLI for WebJs, a full-stack JavaScript framework built on web components with server-side rendering and no build step. Runs the dev and production servers, scaffolds apps, validates conventions, and drives the database. Node 24+ or Bun.",
6
6
  "bin": {
@@ -17,8 +17,8 @@
17
17
  "README.md"
18
18
  ],
19
19
  "dependencies": {
20
- "@webjsdev/mcp": "^0.1.0",
21
- "@webjsdev/server": "^0.8.88",
20
+ "@webjsdev/mcp": "^0.1.15",
21
+ "@webjsdev/server": "^0.8.89",
22
22
  "@webjsdev/ui": "^0.3.15"
23
23
  },
24
24
  "publishConfig": {
@@ -8,30 +8,12 @@ Read `AGENTS.md` first. Full hosted docs are at https://webjs.dev/docs.
8
8
 
9
9
  ## Grow the app in place (non-negotiable)
10
10
 
11
- - **Study the shipped examples, then clear them and build.** The scaffold is a
12
- starting point with a browsable showcase to learn the real idioms from, plus a
13
- database wired up. A full-stack app ships a UI feature gallery (`app/features/`,
14
- `app/examples/todo`); the api template ships a backend-features showcase
15
- (`app/api/features/`), with logic in `modules/`. Building a real app: study the
16
- parts that match your task (the skill teaches the same and SURVIVES the clear),
17
- run `npm run gallery:clear` to shed the showcase (it keeps the agent skill and
18
- the database wiring, and resets to a clean base), then regenerate the database
19
- and grow the app in place under `app/`, `components/`, and `modules/<feature>/`.
20
- `AGENTS.md` carries the full template-specific build playbook and the order to
21
- follow.
22
- - **Use the wired-up database (Drizzle), never JSON files.** For any data the app
23
- stores, define a Drizzle table in `db/schema.server.ts`, then
24
- `npm run db:generate` and `npm run db:migrate`. Never use a JSON file, a
25
- module-scope array or Map, or localStorage as a database.
26
- - **`app/` is routing-only.** Only routing files live in `app/` (page, layout,
27
- route, middleware, metadata routes). Browser-safe helpers go in `lib/utils/`,
28
- feature logic in `modules/`, server-only code behind `.server.ts`.
29
- - **For a UI app, render and LOOK before calling it done.** Define design tokens
30
- in `app/layout.ts` with a palette that fits the app
31
- (`.agents/skills/webjs/references/styling.md` is the guide), then open every
32
- route you changed in a real browser and play through its states.
33
- `npm run check` and `npm run typecheck` pass even when a layout collapses, so
34
- the browser is the real check.
11
+ Study the shipped showcase (a full-stack app's UI gallery under
12
+ `app/features/` and `app/examples/todo`, the api template's
13
+ `app/api/features/`), run `npm run gallery:clear`, then grow the app in place
14
+ under `app/`, `components/`, and `modules/<feature>/`. `AGENTS.md` carries the
15
+ build order, the data rule (the wired-up Drizzle database, never JSON files),
16
+ and the browser check for a UI app.
35
17
 
36
18
  ## Before starting ANY work
37
19
 
@@ -9,7 +9,7 @@ Use this skill for end-to-end WebJs app work. It helps you choose the right laye
9
9
 
10
10
  ## Full Documentation
11
11
 
12
- This skill is the quick guide. When you need the full API reference for a surface, load the matching file in `references/` (listed below). For even deeper framework detail, WebJs ships buildless, so the source you run IS the source you read: look in `node_modules/@webjsdev/{core,server,cli}/` (each package ships its own `AGENTS.md`). The complete hosted docs live at https://webjs.dev/docs.
12
+ This skill is the quick guide. When you need the full API reference for a surface, load the matching file in `references/` (listed below). For even deeper framework detail, WebJs ships buildless, so the source you run IS the source you read: look in `node_modules/@webjsdev/{core,server,cli}/` (each package ships its own `AGENTS.md`). To check ONE export's contract, run `npx webjs source <Export>` (for example `npx webjs source createAuth`, or `--pkg core` to narrow): it prints that export's signature and the doc comment above it from the installed package, so you read the declaration instead of the file. The MCP `source` tool does the same with `export`. Open the source file when the question is about behaviour the signature does not state. The complete hosted docs live at https://webjs.dev/docs.
13
13
 
14
14
  ## What WebJs Is
15
15
 
@@ -104,12 +104,12 @@ Common bundles:
104
104
  1. **Classify the change.** Route contract, data model, server mutation, auth, or only UI?
105
105
  2. **Start from the server.** Add the page/route and its server action or query before wiring interactive UI. A page render or a `<form>` POST should already return correct HTML before any component hydrates.
106
106
  3. **Put code in the narrowest owner.** Route-local first (`modules/<feature>/`), promote to `lib/` or `components/` only when reuse is real.
107
- 4. **Keep server-only code behind `.server.ts`.** The DB driver, secrets, and `node:*` never belong in a page, layout, or component.
107
+ 4. **Keep server-only code behind `.server.ts`** (Core WebJs Rules 1 and 2).
108
108
  5. **Add interactivity per behaviour.** Reach for a component (and a signal or `@event`) only where the UI is genuinely interactive. A display-only component is elided from the browser. Then wrap the interactive part and STOP: the static markup around it stays in the page, where it costs nothing.
109
109
  6. **Validate input at the boundary.** Declare `export const validate` on an action; the RPC and `route()` boundaries run it.
110
110
  7. **Default mutations to optimistic UI** where the client can predict the result (`optimistic()` from `@webjsdev/core`).
111
- 8. **Type every boundary from its source, never `unknown` or `any`.** The row type comes from the schema (`typeof todos.$inferSelect`), the action's input from a named `interface` and its result from `ActionResult<T>`, the routing files from `PageProps` / `LayoutProps` / `RouteHandlerContext`. `unknown` belongs on a payload nothing has vouched for yet that the next line narrows, and on a parameter of your own helper that forwards into an `html` template hole. Everywhere else, including a layout's `children`, it is a missing type. See `references/typescript.md`.
112
- 9. **Test the narrowest meaningful layer**, and render the app in a real browser for any UI change (static checks do not catch a collapsed layout).
111
+ 8. **Type every boundary from its source, never `unknown` or `any`.** The row type comes from the schema (`typeof todos.$inferSelect`), the action's input from a named `interface` and its result from `ActionResult<T>`, the routing files from `PageProps` / `LayoutProps` / `RouteHandlerContext`. `unknown` belongs on a payload nothing has vouched for yet that the next line narrows, and on a parameter of your own helper that forwards into an `html` template hole. Everywhere else, including a layout's `children`, it is a missing type. Carry a row type into a shipping component with `import type` (erased before the browser sees it). Nothing enforces this (both spellings are valid TypeScript, so `webjs check` and `tsc` pass either way), which is why it is written down. See `references/typescript.md`.
112
+ 9. **Test the narrowest meaningful layer**, and look at any UI change in a real browser (see Testing Defaults).
113
113
 
114
114
  ## Project Layout
115
115
 
@@ -137,7 +137,7 @@ App-internal imports use the `#` root alias (`import { db } from '#db/connection
137
137
  ## Core WebJs Rules (invariants)
138
138
 
139
139
  1. Server-only code lives in `.server.ts`, `route.ts`, or `middleware.ts`. Never in a page, layout, or component (it crashes the browser at module load).
140
- 2. `'use server'` exports are `async` functions returning serializer-safe values. Files without `'use server'` are server-only utilities.
140
+ 2. `'use server'` exports are `async` functions returning serializer-safe values, callable from browser code as RPC. Files without `'use server'` are server-only utilities: reach them only from `'use server'` actions, `route.ts`, or middleware. Never add `'use server'` to a file only other server code imports (the DB connection, the schema).
141
141
  3. Custom element tag names contain a hyphen. Pass the tag to `Class.register('tag-name')`.
142
142
  4. Event (`@`), property (`.`), and boolean (`?`) holes in `html` are UNQUOTED: `@click=${fn}`, never `@click="${fn}"`.
143
143
  5. Signals are the default state primitive. Import `signal` / `computed` from `@webjsdev/core`, read via `signal.get()` inside `render()`. The base-class factory `WebComponent({ ... })` is only for values riding an HTML attribute or arriving via SSR hydration.
@@ -147,7 +147,7 @@ App-internal imports use the `#` root alias (`import { db } from '#db/connection
147
147
  9. No backtick characters inside an `html\`...\`` body, even in comments (it closes the literal and 500s).
148
148
  10. TypeScript must be erasable (`erasableSyntaxOnly: true`): no `enum`, no value `namespace`, no constructor parameter properties, no legacy decorators.
149
149
  11. Reactive properties are declared ONLY through the base-class factory `extends WebComponent({ count: Number })`. Never a `static properties` block, never a class-field initializer (it clobbers the reactive accessor).
150
- 12. A form that writes binds its action: `<form action=${importedAction}>`, or a per-button `<button formaction=${importedAction}>`. A bound submitter is SELF-SUFFICIENT (#1307): the renderer puts `formmethod="post"` and `formenctype` on the button itself, so it needs no bound form around it and works inside any form or none. Quoted bindings, non-submit controls, `<input type="submit">` (the identity needs its `value`, which is also its label, so use a `<button>`), submitter `name` / `value` / `form` / static `formaction` attributes, a `.prop` spelling of any of those, `action=${fn}` off a `<form>`, a bound form with `method="get"`, a BOUND submitter's own non-post `formmethod` or unparseable `formenctype`, and a non-action function all throw. A PLAIN button's own `formmethod` / `formenctype` is a legal native override and is left alone. A page has no `action` export, so a bare `<form method="post">` is a 405.
150
+ 12. A form that writes binds its action: `<form action=${importedAction}>`, or a per-button `<button formaction=${importedAction}>`. A bound submitter is SELF-SUFFICIENT (#1307): the renderer puts `formmethod="post"` and `formenctype` on the button itself, so it needs no bound form around it and works inside any form or none. Quoted bindings, non-submit controls, `<input type="submit">` (the identity needs its `value`, which is also its label, so use a `<button>`), submitter `name` / `value` / `form` / static `formaction` attributes (the identity IS the button's name/value pair, so both halves are spoken for), a `.prop` spelling of any of those, `action=${fn}` off a `<form>`, a bound form with `method="get"` (a GET sends no body for the action to read), a BOUND submitter's own non-post `formmethod` or unparseable `formenctype`, and a non-action function all throw. A PLAIN button's own `formmethod` / `formenctype` is a legal native override and is left alone. A page has no `action` export, so a bare `<form method="post">` is a 405.
151
151
 
152
152
  ## Export Map
153
153
 
@@ -155,7 +155,7 @@ Find the right export fast. Load the linked reference for full examples.
155
155
 
156
156
  ### `@webjsdev/core` (browser + isomorphic)
157
157
 
158
- - `html` / `css` tagged templates. `WebComponent({ ... })` base-class factory; `prop(type?, opts?)` declares one reactive property. `register(tag, C)` / `Class.register('tag')`.
158
+ - `html` / `css` tagged templates. `WebComponent({ ... })` base-class factory; `prop(type?, opts?)` declares one reactive property. `Class.register('tag')` binds the tag.
159
159
  - `signal` / `computed` reactive state, `effect(fn)` client-only reaction (returns a disposer), `batch(fn)` coalesced writes; `render(v, el)` client render.
160
160
  - `notFound()` / `redirect(url[, status])` control-flow throws (page/layout/action only, NOT `route.ts`). `forbidden()` / `unauthorized()` render the nearest boundary.
161
161
  - `Suspense({fallback, children})` page-level streaming; `<webjs-suspense>` component-level streaming.
@@ -272,18 +272,12 @@ Success is a 303 (PRG); failure re-renders the page at 422 with the result on `a
272
272
 
273
273
  ## Common Mistakes To Avoid
274
274
 
275
- - Treating a page or layout like a React component and expecting its markup to hydrate. It runs server-only; put interactivity in a component.
275
+ The invariants above are not repeated here; this list is the mistakes they do not already name.
276
+
276
277
  - Promoting a whole page section to a component so that one control inside it can be interactive. The island should wrap the control and the state it reads. An oversized island ships its own JS AND un-elides every display-only component inside it, so the cost is not linear in what you moved.
277
- - Importing a `.server.ts` utility (no `'use server'`) directly into a shipping component. Its browser stub throws at load; reach it through a `'use server'` action.
278
- - Using a `static properties` block or a class-field initializer for reactive props instead of the `WebComponent({ ... })` factory.
279
- - Quoting an event / property / boolean hole (`@click="${fn}"`).
280
278
  - Writing `fetch()` to call your own server instead of importing the action.
281
- - Writing a bare `<form method="post">` and expecting a page `action` export to catch it. There is no such export; bind the action with `action=${fn}` or the submission is a 405.
282
- - Putting a submitter's `formaction=${fn}` on anything that is not a submit control, or on a button carrying its own `name` / `value`. The identity IS the button's name/value pair, so both halves are spoken for.
283
- - Writing `formmethod="get"` or `formenctype="text/plain"` on a button that BINDS an action. Neither can carry that action's body, so the pair contradicts itself and throws. On a button that binds nothing it is a legal native override and is honoured.
284
279
  - Binding an action whose file declares `export const method = 'GET'`. Form-bound actions strictly require POST (default). Binding a GET action to a form is a 405 at runtime and a `webjs check` error (`form-action-not-a-get-action`).
285
280
  - Leaving read-only RPC server query actions as default `POST`. Always export `export const method = 'GET'` for RPC data queries so arguments ride URL params, ETags/304 caching work, and CSRF is safely bypassed.
286
- - Writing `method="get"` on a bound `<form action=${fn}>`. WebJs supplies `method="post"` and `formenctype` automatically, and a bound form declaring `method="get"` is REFUSED at render (a thrown error, not a warning), because a GET sends no body for the action to read.
287
281
  - Throwing `redirect()` / `notFound()` inside a `route.ts` handler (uncaught 500). Return a `Response` there.
288
282
  - A placeholder first paint that fetches in `connectedCallback`. SSR does not call `connectedCallback`; put first-paint data in the constructor (server-known inputs) or use `async render()`.
289
283
  - A browser global (`window`, `document`, `localStorage`) in the constructor or `render()`. It throws at SSR; do browser-only work in `connectedCallback`.
@@ -296,7 +296,9 @@ Three levels, the same scale ESLint uses: `error` fails the exit, `warn` reports
296
296
 
297
297
  Two guarantees worth knowing. A result that could not check (a network or toolchain outage) is capped at `warn` and can never be escalated, so a jspm or npm outage cannot red your CI. And a malformed gate exits 1 naming the offender rather than being ignored, so a typo cannot silently un-gate the build. That covers an unknown code, a bad severity, a wrong shape (a non-object `doctor` or `gate`), and a misspelled sibling of `gate` such as `gates`, since every one of those would otherwise leave the build un-gated while the `package.json` looks gated. Under `--json` the offenders come back as a `configErrors` array alongside an empty `results`, each entry a `{ kind }` of `malformed` / `unknown-key` / `unknown-code` / `bad-severity`. Wire it up with one workflow step, `npm run doctor`, and change what is fatal in `package.json` rather than in the workflow.
298
298
 
299
- `APP_ICON` warns while the favicon is still the scaffold's: the placeholder `app/icon.svg` (marked `data-webjs-placeholder`) or the WebJs mark older scaffolds shipped at `public/favicon.svg`. Replace it with the app's own icon (`references/routing-and-pages.md`, "App icon and manifest").
299
+ `APP_ICON` warns while the favicon is still the scaffold's placeholder `app/icon.svg` (marked `data-webjs-placeholder`). Replace it with the app's own icon (`references/routing-and-pages.md`, "App icon and manifest").
300
+
301
+ `DARK_THEME_UNREACHABLE` warns when the app defines colour tokens with a light value only and nothing applies a dark half: no `light-dark()`, no `@media (prefers-color-scheme: dark)` rule, no theme script in the root layout. The scaffold gates it `error` in its `package.json` because its AGENTS.md mandates `light-dark()` tokens; the fix is the token block in `references/styling.md`.
300
302
 
301
303
  ### Dependency audit allowlist
302
304
 
@@ -193,7 +193,7 @@ Whichever form you use, a token nothing references is dropped in both, so an unu
193
193
  </style>
194
194
  ```
195
195
 
196
- `light-dark()` is a native CSS function (CSS Color 5, Baseline 2024), not a library, so nothing to import. A single-theme app drops the `[data-theme]` rules and gives each token one colour.
196
+ `light-dark()` is a native CSS function (CSS Color 5, Baseline 2024), not a library, so nothing to import. A single-theme app drops the `[data-theme]` rules and gives each token one colour. `webjs doctor` reports the floor as `DARK_THEME_UNREACHABLE` (the scaffold gates it `error`): an app whose tokens have a light value only, with no `light-dark()`, no `prefers-color-scheme` rule and no theme script in the root layout, because the scaffold stylesheet's `.dark` block would then be dead CSS and the OS dark setting ignored.
197
197
 
198
198
  **A manual theme toggle** writes `data-theme` on `<html>` (`light` / `dark`, or removes it for "follow the OS"). If you use `@webjsdev/ui` components, ALSO keep the `.dark` class in sync (the ui kit keys its own tokens off `.dark`), and apply the saved choice in a tiny inline `<script>` in the layout head so there is no first-paint flash. Verify dark mode in a real browser. Light mode passing proves nothing about dark.
199
199
 
@@ -22,58 +22,26 @@ This is what separates a working app from a broken one.
22
22
  rules (git, tests, review) are in `.agents/rules/workflow.md`; follow them
23
23
  too.
24
24
  3. **Read the framework source for exact contracts.** WebJs is 100% buildless
25
- native ES modules, so the source you run IS the source you read. When you
26
- need a precise API signature or behavior, open the package source under
27
- `node_modules/@webjsdev/*` directly (each package ships its own `AGENTS.md`).
28
- The full hosted docs are at https://webjs.dev/docs.
25
+ native ES modules, so the source you run IS the source you read. For one
26
+ export's signature and doc comment run `npx webjs source <Export>` (for
27
+ example `npx webjs source createAuth`); it reads the installed package and
28
+ prints the declaration, not the file. When you need the behaviour behind a
29
+ signature, open the package source under `node_modules/@webjsdev/*` directly
30
+ (each package ships its own `AGENTS.md`). The full hosted docs are at
31
+ https://webjs.dev/docs.
29
32
 
30
33
  {{PLAYBOOK}}
31
34
 
32
35
  ## Type everything (all templates)
33
36
 
34
- Full-stack type safety is what the `.server.ts` boundary buys you: a client
35
- component importing a server action resolves to that action's real signature at
36
- type-check time, with no build step and no code generation in between. So
37
- DERIVE the type at every boundary instead of widening it:
38
-
39
- - A database row: `export type Todo = typeof todos.$inferSelect` in
40
- `db/schema.server.ts` (`$inferInsert` for a write), carried into a
41
- browser-shipped component with `import type` (erased before it reaches the
42
- browser, so it does not trip the server-import boundary).
43
- - An action's input: a named `interface`. Its result: `ActionResult<T>`.
44
- Narrow with `if (result.success && result.data)`.
45
- - Routing files: `PageProps<'/blog/[slug]'>`, `LayoutProps`,
46
- `RouteHandlerContext`, all from `@webjsdev/core`. Run `npx webjsdev types`
47
- for the typed `Route` union and per-route `params`.
48
- - A reactive property: `prop<Student>(Object)`, `prop<Tag[]>(Array)`.
49
-
50
- Never reach for `any` or a loose `as any` cast, and do not reach for `unknown`
51
- either just because it looks safer. `unknown` is right for a payload nothing
52
- has vouched for yet, narrowed on the very next line (a `route.ts` `await
53
- req.json()`, an action's `export const validate` or a validator it delegates
54
- to, a `catch` binding), and for a parameter of YOUR OWN helper that forwards
55
- into an `html` template hole (a hole renders a string, a number, a
56
- `TemplateResult`, or an array of those, so `TemplateResult` alone is too
57
- narrow). That second case is about a value you accept, never one the framework
58
- already types. Everywhere else it is a missing type, not a safe one: `unknown`
59
- that survives into a return type, a component prop, a layout's `children`, or
60
- an action signature is the shape to fix.
61
- Nothing enforces this (both are valid TypeScript, so `webjs check` and `tsc`
62
- pass either way), which is exactly why it is written down. The full ladder,
63
- with an end-to-end example, is in
37
+ Derive the type at every boundary from its source. Never reach for `any`, and
38
+ never `unknown` where a real type exists. The rule is step 8 of the skill's "Default
39
+ Workflow"; the full ladder, with an end-to-end example, is
64
40
  `.agents/skills/webjs/references/typescript.md`.
65
41
 
66
42
  Keep server-only code (database drivers, secrets, `node:*` builtins) in
67
- `.server.ts` modules. There are exactly two kinds:
68
-
69
- - A `.server.ts` file WITH `'use server';` as its first line is a server
70
- action: WebJs exposes its exported async functions to browser code as RPC
71
- calls, so browser modules may import it directly.
72
- - A `.server.ts` file WITHOUT `'use server'` is a server-only utility:
73
- importing it from a page, layout, or component CRASHES in the browser at
74
- module load. Reach it only from `'use server'` actions, `route.ts` handlers,
75
- or middleware. Never add `'use server'` to a file only other server code
76
- imports (the DB connection, the schema).
43
+ `.server.ts` modules. The two kinds (with and without `'use server'`) are the
44
+ skill's "Core WebJs Rules" 1 and 2.
77
45
 
78
46
  ## Data (all templates)
79
47
 
@@ -9,14 +9,9 @@ is complete, WITHOUT being asked. Do not save all the work for one commit at the
9
9
  end. A finished implementation with zero commits is a mistake here, because git
10
10
  history is the user's revert and cherry-pick safety net.
11
11
 
12
- - After each completed unit whose tests pass, `git add` the related files and
13
- `git commit` with an imperative subject under 72 chars, then push. If 5+ files
14
- span more than one concern, you already waited too long.
15
- - Never commit to `main`. Work on a feature branch (the
16
- `.claude/hooks/guard-branch-context.sh` hook enforces this).
17
- - No AI-attribution trailers (`Co-Authored-By`, `Generated by`).
18
-
19
- See AGENTS.md "Git workflow" for the full contract. Two hooks back this up: the
12
+ The full git contract (branches, commit messages, attribution) is
13
+ `.agents/rules/workflow.md` "Git rules"; the
14
+ `.claude/hooks/guard-branch-context.sh` hook refuses a commit on `main`. Two hooks back this up: the
20
15
  `.claude/hooks/nudge-uncommitted.sh` PostToolUse hook reminds you while
21
16
  uncommitted changes pile up during work, and the
22
17
  `.claude/hooks/commit-before-stop.sh` Stop hook stops you from ending a turn
@@ -3,34 +3,16 @@
3
3
  The conventions for building a WebJs app live in the agent skill. **Read
4
4
  `AGENTS.md` first, then `.agents/skills/webjs/SKILL.md`** (it routes to focused
5
5
  references under `.agents/skills/webjs/references/`, loaded on demand). This file
6
- is the short version.
6
+ only says where each rule lives, so no rule is stated twice.
7
7
 
8
- ## The essentials
9
-
10
- - **`app/` is routing only.** Only routing files live there (page, layout, route,
11
- middleware, metadata routes). Feature logic goes in `modules/<feature>/`
12
- (`actions/`, `queries/`, `components/`, `utils/`); shared UI primitives go in
13
- top-level `components/`; browser-safe helpers in `lib/utils/`.
14
- - **Server-only code goes behind `.server.ts`.** Reach it from a page or component
15
- through a `'use server'` action, never by importing a server-only utility
16
- directly into browser-bound code.
17
- - **Use the wired-up database (Drizzle).** Define real models in
18
- `db/schema.server.ts`, then `npm run db:generate` and `npm run db:migrate`.
19
- Never persist to a JSON file, an in-memory array or Map, or localStorage.
20
- - **The scaffold ships a showcase to learn from.** A full-stack app ships a UI
21
- feature gallery (`app/features/`, `app/examples/todo`); the api template ships
22
- a backend-features showcase (`app/api/features/`), with logic in `modules/`.
23
- When you build a real app, study the parts that match your task (the skill
24
- teaches the same and survives the clear), run `npm run gallery:clear` to shed
25
- the showcase, then grow the app in place. `AGENTS.md` has the full
26
- template-specific playbook.
27
- - **Derive types at every boundary.** Rows from `$inferSelect`, action inputs
28
- from an `interface`, routing files from `PageProps` / `LayoutProps`. Never
29
- `any`, and never `unknown` where a real type exists.
30
- - **Progressive enhancement is the default.** Pages render as HTML, `<a>`
31
- navigates, a `<form action=${importedAction}>` submits, all with JavaScript off; opt into
32
- interactivity per behaviour inside a component.
33
- - **Commit per logical unit** as soon as it is complete, and never push to `main`.
34
-
35
- Everything else (the module architecture, the `ActionResult` envelope, styling,
36
- testing, the client router, optimistic UI) is in the skill's references.
8
+ - **Build order** (study the showcase: a full-stack app's gallery under
9
+ `app/features/` and `app/examples/todo`, the api template's
10
+ `app/api/features/`; then `npm run gallery:clear`, data, UI, verify):
11
+ the playbook in `AGENTS.md`.
12
+ - **Data** (the wired-up Drizzle database, never a JSON file, an in-memory
13
+ array or Map, or localStorage): `AGENTS.md`, "Data".
14
+ - **Layout** (`app/` is routing only, logic in `modules/<feature>/`): the
15
+ skill's "Project Layout".
16
+ - **Server boundary, progressive enhancement, typing**: the skill's "What WebJs
17
+ Is", "Core WebJs Rules", and "Default Workflow".
18
+ - **Git, tests, and `npm run ci`**: `.agents/rules/workflow.md`.
@@ -44,20 +44,12 @@ cross-origin access use the `cors()` middleware from `@webjsdev/server`; with
44
44
 
45
45
  ### 5. Verify before you call it done
46
46
 
47
- Run `npm run ci` and fix what it reports. It is one command for every gate,
48
- the step list declared in `package.json` under `webjs.ci`, with a result line
49
- per step:
50
-
51
- - `webjs check` (correctness: no browser-import or boundary violation).
52
- - `webjs doctor` (project health). It fails on whatever `package.json`
53
- `webjs.doctor.gate` marks `error`, plus the two hard toolchain checks that
54
- are fatal with no gate entry, `NODE_VERSION` and `TSCONFIG_ERASABLE`.
55
- - `webjs typecheck` (zero type errors).
56
- - A dependency audit.
57
- - The test layers for the endpoints and modules you built.
58
-
59
- The GitHub workflow runs the same list, so a green local run predicts CI.
60
- While iterating, `npm run ci -- --only Tests` runs one layer.
47
+ Run `npm run ci` and fix what it reports. It runs every gate declared under
48
+ `webjs.ci` in `package.json` (`webjs check`, `webjs doctor`, `webjs typecheck`,
49
+ a dependency audit, then the test layers for the endpoints and modules you
50
+ built), and the GitHub workflow runs the
51
+ same list; `.agents/rules/workflow.md` has what each gate checks. While
52
+ iterating, `npm run ci -- --only Tests` runs one layer.
61
53
 
62
54
  Then boot `npm run dev` and probe each endpoint for the expected status and JSON
63
55
  shape.
@@ -84,31 +84,21 @@ action without also triggering the row navigation.
84
84
 
85
85
  ### 6. Build components for interactivity
86
86
 
87
- Pages and layouts (`app/**/page.ts`, `app/**/layout.ts`) are server-only HTML
88
- generators, so put every interactive behavior inside a `WebComponent` custom
89
- element. Declare a component's reactive properties in the base-class factory,
90
- never as a class-field initializer (`items = []` clobbers the reactive
91
- accessor). Use the shorthand for primitives
87
+ Pages and layouts never hydrate, so put every interactive behavior inside a
88
+ `WebComponent` custom element, and declare its reactive properties only in the
89
+ base-class factory (the skill's "Core WebJs Rules" 11). Use the shorthand for primitives
92
90
  (`extends WebComponent({ name: String, count: Number, open: Boolean })`) and the
93
91
  `prop<T>()` helper for typed objects and arrays
94
92
  (`extends WebComponent({ items: prop<Item[]>(Array), user: prop<User>(Object) })`).
95
93
 
96
94
  ### 7. Verify before you call it done
97
95
 
98
- Run `npm run ci` and fix what it reports. It is one command for every gate,
99
- the step list declared in `package.json` under `webjs.ci`, with a result line
100
- per step:
101
-
102
- - `webjs check` (correctness: no browser-import or boundary violation).
103
- - `webjs doctor` (project health). It fails on whatever `package.json`
104
- `webjs.doctor.gate` marks `error`, plus the two hard toolchain checks that
105
- are fatal with no gate entry, `NODE_VERSION` and `TSCONFIG_ERASABLE`.
106
- - `webjs typecheck` (zero type errors).
107
- - A dependency audit.
108
- - The server, browser, and e2e test layers for the features you built.
109
-
110
- The GitHub workflow runs the same list, so a green local run predicts CI.
111
- While iterating, `npm run ci -- --only Tests` runs one layer. Then
96
+ Run `npm run ci` and fix what it reports. It runs every gate declared under
97
+ `webjs.ci` in `package.json` (`webjs check`, `webjs doctor`, `webjs typecheck`,
98
+ a dependency audit, then the server, browser, and e2e test layers for the
99
+ features you built), and the GitHub workflow runs the
100
+ same list; `.agents/rules/workflow.md` has what each gate checks. While
101
+ iterating, `npm run ci -- --only Tests` runs one layer. Then
112
102
  `npm run css:build` (compile Tailwind).
113
103
 
114
104
  Then boot `npm run dev`, confirm every page route returns HTTP 200, and open