@webjsdev/cli 0.10.69 → 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 +38 -0
- package/lib/app-icon.js +0 -10
- package/lib/create.js +20 -14
- package/lib/dev-reload.js +3 -1
- package/lib/doctor/codes.js +1 -0
- package/lib/doctor/probes/app-icon.js +5 -10
- package/lib/doctor/probes/dark-theme.js +91 -0
- package/lib/doctor/runner.js +2 -0
- package/lib/runtime-rewrite.js +18 -18
- package/package.json +3 -3
- package/templates/.agents/rules/workflow.md +9 -24
- package/templates/.agents/skills/webjs/SKILL.md +9 -17
- package/templates/.agents/skills/webjs/references/built-ins.md +3 -1
- package/templates/.agents/skills/webjs/references/runtime.md +5 -3
- package/templates/.agents/skills/webjs/references/styling.md +1 -1
- package/templates/.claude/hooks/nudge-uncommitted.sh +0 -7
- package/templates/AGENTS.md +45 -47
- package/templates/CLAUDE.md +12 -15
- package/templates/CONVENTIONS.md +15 -32
- package/templates/Dockerfile +7 -1
- package/templates/partials/agents-playbook-api.md +6 -14
- package/templates/partials/agents-playbook-fullstack.md +120 -566
- package/templates/scripts/clear-gallery.mjs +3 -3
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
|
@@ -444,9 +444,20 @@ export async function scaffoldApp(name, cwd, opts = {}) {
|
|
|
444
444
|
'@webjsdev/cli': 'latest',
|
|
445
445
|
'@webjsdev/core': 'latest',
|
|
446
446
|
'@webjsdev/server': 'latest',
|
|
447
|
+
// What the production BOOT runs, so production dependencies (#1606):
|
|
448
|
+
// `webjs start` runs the `webjs.start.before` steps at every boot,
|
|
449
|
+
// `webjs db migrate` (drizzle-kit) and, on a UI app, the Tailwind
|
|
450
|
+
// compile. The Dockerfile installs production dependencies only, so as
|
|
451
|
+
// devDependencies these would be missing from the image and the boot
|
|
452
|
+
// would fail. `tailwindcss` itself is declared too (#1493):
|
|
453
|
+
// public/input.css starts with `@import "tailwindcss"`, and bun's
|
|
454
|
+
// isolated linker and pnpm link only declared packages, so leaving it
|
|
455
|
+
// transitive (via @tailwindcss/cli) fails the compile with
|
|
456
|
+
// `Can't resolve 'tailwindcss'`. The api template has no CSS.
|
|
457
|
+
'drizzle-kit': '1.0.0-rc.3',
|
|
458
|
+
...(isApi ? {} : { '@tailwindcss/cli': '^4.1.0', tailwindcss: '^4.1.0' }),
|
|
447
459
|
},
|
|
448
460
|
devDependencies: {
|
|
449
|
-
'drizzle-kit': '1.0.0-rc.3',
|
|
450
461
|
...(dialect === 'postgres' ? { '@types/pg': '^8.11.0' } : {}),
|
|
451
462
|
// The TypeScript compiler, for `npm run typecheck` (webjs typecheck runs
|
|
452
463
|
// tsc --noEmit). Not needed at runtime (Node strips types in place), only
|
|
@@ -467,15 +478,6 @@ export async function scaffoldApp(name, cwd, opts = {}) {
|
|
|
467
478
|
// assertNoA11yViolations() test helper from @webjsdev/core/testing.
|
|
468
479
|
// Test-only: dynamically imported, never shipped to the app runtime.
|
|
469
480
|
'axe-core': '^4.10.0',
|
|
470
|
-
// The Tailwind v4 CLI that css:build runs to compile public/input.css into
|
|
471
|
-
// the static public/tailwind.css the layout links. UI templates only (the
|
|
472
|
-
// api template has no CSS). Build tooling, never shipped to the runtime.
|
|
473
|
-
// `tailwindcss` itself is declared too (#1493): public/input.css starts
|
|
474
|
-
// with `@import "tailwindcss"`, so the app imports that package directly.
|
|
475
|
-
// Leaving it transitive (via @tailwindcss/cli) breaks under bun's isolated
|
|
476
|
-
// linker and pnpm, which link only declared packages into the app's
|
|
477
|
-
// node_modules, so the compile fails with `Can't resolve 'tailwindcss'`.
|
|
478
|
-
...(isApi ? {} : { '@tailwindcss/cli': '^4.1.0', tailwindcss: '^4.1.0' }),
|
|
479
481
|
// tsserver plugin, wired into tsconfig below. Gives the language
|
|
480
482
|
// INTELLIGENCE (go-to-def, completions, diagnostics, hover inside html``
|
|
481
483
|
// templates) in any tsserver editor with NO editor plugin installed,
|
|
@@ -583,9 +585,13 @@ export async function scaffoldApp(name, cwd, opts = {}) {
|
|
|
583
585
|
// layout already writes asset(), so a fresh app is green on day one.
|
|
584
586
|
// Two checks are fatal with no entry here at all, NODE_VERSION and
|
|
585
587
|
// TSCONFIG_ERASABLE, because either would 500 the app at runtime;
|
|
586
|
-
//
|
|
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
|
|
587
593
|
// silence it, or "error" to make it fatal too.
|
|
588
|
-
doctor: { gate: { UNMARKED_ASSET_LINKS: 'error' } },
|
|
594
|
+
doctor: { gate: { UNMARKED_ASSET_LINKS: 'error', DARK_THEME_UNREACHABLE: 'error' } },
|
|
589
595
|
// The dependency audit's allowlist (#1492), the ONE place an accepted
|
|
590
596
|
// advisory is listed, each with the reason it is safe. `webjs audit`
|
|
591
597
|
// (the CI step below) fails on every other advisory at `level` or above,
|
|
@@ -1721,8 +1727,8 @@ ThemeToggle.register('theme-toggle');
|
|
|
1721
1727
|
`);
|
|
1722
1728
|
}
|
|
1723
1729
|
console.log(`For AI agents, read this before editing:
|
|
1724
|
-
• Read AGENTS.md
|
|
1725
|
-
|
|
1730
|
+
• Read AGENTS.md, then .agents/skills/webjs/SKILL.md. The skill is the guide
|
|
1731
|
+
to building a WebJs app and routes to focused references on demand.
|
|
1726
1732
|
• This scaffold is a minimal starting point, not a demo to prune. Grow the app
|
|
1727
1733
|
in place: add routes under app/, components under components/, and features
|
|
1728
1734
|
under modules/<feature>/, and keep server-only code behind .server.ts.
|
package/lib/dev-reload.js
CHANGED
|
@@ -68,7 +68,9 @@ export const KILL_TIMEOUT_MS = 2000;
|
|
|
68
68
|
* @returns {boolean}
|
|
69
69
|
*/
|
|
70
70
|
export function shouldIgnoreRestartPath(rel) {
|
|
71
|
-
return /(?:^|[\\/])(?:node_modules|\.git|\.webjs)(?:[\\/]|$)|(?:^|[\\/])db[\\/](?:dev\.db|migrations)/.test(rel || '')
|
|
71
|
+
return /(?:^|[\\/])(?:node_modules|\.git|\.webjs)(?:[\\/]|$)|(?:^|[\\/])db[\\/](?:dev\.db|migrations)/.test(rel || '')
|
|
72
|
+
// Tool output nothing serves, as the server's watcher skips it (watch-ignore.js).
|
|
73
|
+
|| /(?:^|[\\/])(?:coverage|\.cache|\.nyc_output|test-results|playwright-report|\.turbo)(?:[\\/]|$)|\.log$|(?:^|[\\/])\.DS_Store$|\.swp$|~$/.test(rel || '');
|
|
72
74
|
}
|
|
73
75
|
|
|
74
76
|
/**
|
package/lib/doctor/codes.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
-
import { PLACEHOLDER_MARKER
|
|
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
|
|
16
|
-
*
|
|
17
|
-
*
|
|
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)
|
|
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
|
+
}
|
package/lib/doctor/runner.js
CHANGED
|
@@ -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.
|
package/lib/runtime-rewrite.js
CHANGED
|
@@ -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` base (#595)
|
|
27
|
-
*
|
|
28
|
-
*
|
|
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
|
/**
|
|
@@ -96,16 +94,12 @@ export function bunifyProse(s) {
|
|
|
96
94
|
/**
|
|
97
95
|
* Rewrite the scaffolded Dockerfile for Bun.
|
|
98
96
|
*
|
|
99
|
-
* Base decision (acceptance criterion): a pure `oven/bun:1` image (no Node).
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
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.
|
|
97
|
+
* Base decision (acceptance criterion): a pure `oven/bun:1-slim` image (no Node).
|
|
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}
|
|
@@ -123,7 +117,9 @@ export function bunifyDockerfile(s) {
|
|
|
123
117
|
'# runs on Node 24+; for a Node base instead, swap to `node:24-alpine` and start with\n' +
|
|
124
118
|
'# `npm start`.\n',
|
|
125
119
|
)
|
|
126
|
-
|
|
120
|
+
// The slim variant: the same Bun on a Debian slim base (ca-certificates
|
|
121
|
+
// included), without the full image's extra system packages.
|
|
122
|
+
.replace('FROM node:24-alpine', 'FROM oven/bun:1-slim')
|
|
127
123
|
// Debian base: ca-certificates already present, no `apk`. Drop the alpine line.
|
|
128
124
|
.replace(
|
|
129
125
|
/# ca-certificates for outbound TLS \(e\.g\. a managed Postgres\)\. SQLite uses the\n# built-in node:sqlite \(no native module, no build toolchain needed\)\.\nRUN apk add --no-cache ca-certificates\n\n/,
|
|
@@ -131,8 +127,12 @@ export function bunifyDockerfile(s) {
|
|
|
131
127
|
)
|
|
132
128
|
// Lockfile + install (bun.lock, bun install).
|
|
133
129
|
.replace(
|
|
134
|
-
'# package-lock.json is optional (it\'s absent when the app was scaffolded with\n# --no-install); the glob keeps the COPY working with or without it.\
|
|
135
|
-
'# bun.lock is optional (absent when scaffolded with --no-install); the glob keeps\n# the COPY working with or without it. SQLite uses the built-in bun:sqlite, so no\n# native dependency or postinstall is involved.\
|
|
130
|
+
'# package-lock.json is optional (it\'s absent when the app was scaffolded with\n# --no-install); the glob keeps the COPY working with or without it.\n',
|
|
131
|
+
'# bun.lock is optional (absent when scaffolded with --no-install); the glob keeps\n# the COPY working with or without it. SQLite uses the built-in bun:sqlite, so no\n# native dependency or postinstall is involved.\n',
|
|
132
|
+
)
|
|
133
|
+
.replace(
|
|
134
|
+
'reason. The npm cache is emptied in the same layer: it is a second copy of\n# every package fetched and would otherwise ship in the image.\nCOPY package.json package-lock.json* ./\nRUN npm install --omit=dev --no-audit --no-fund && npm cache clean --force',
|
|
135
|
+
'reason. bun\'s package cache is emptied in the same layer: it is a second copy\n# of every package fetched and would otherwise ship in the image.\nCOPY package.json bun.lock* ./\nRUN bun install --production && bun pm cache rm',
|
|
136
136
|
)
|
|
137
137
|
// Healthcheck: the pure Bun image has no node; use `bun -e`. Keep the
|
|
138
138
|
// dependency-free-probe comment accurate (the probe runs under Bun now).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webjsdev/cli",
|
|
3
|
-
"version": "0.10.
|
|
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.
|
|
21
|
-
"@webjsdev/server": "^0.8.
|
|
20
|
+
"@webjsdev/mcp": "^0.1.15",
|
|
21
|
+
"@webjsdev/server": "^0.8.89",
|
|
22
22
|
"@webjsdev/ui": "^0.3.15"
|
|
23
23
|
},
|
|
24
24
|
"publishConfig": {
|
|
@@ -2,33 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
You are working on a WebJs app (AI-first, no-build, web-components-first). This
|
|
4
4
|
file is the WORKFLOW contract (git, tests, review). For HOW to build (routing,
|
|
5
|
-
components, actions, styling, the framework API), read
|
|
6
|
-
|
|
7
|
-
at https://webjs.dev/docs.
|
|
5
|
+
components, actions, styling, the framework API), read
|
|
6
|
+
`.agents/skills/webjs/SKILL.md`, which routes to focused references on demand.
|
|
7
|
+
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
then regenerate the database and grow the app in place under `app/`,
|
|
18
|
-
`components/`, and `modules/<feature>/`. `AGENTS.md` carries the
|
|
19
|
-
template-specific build steps and the order to follow.
|
|
20
|
-
- **Use the wired-up database (Drizzle), never JSON files.** For any data the app
|
|
21
|
-
stores, define a Drizzle table in `db/schema.server.ts`, then
|
|
22
|
-
`npm run db:generate` and `npm run db:migrate`. Never use a JSON file, a
|
|
23
|
-
module-scope array or Map, or localStorage as a database.
|
|
24
|
-
- **`app/` is routing-only.** Only routing files live in `app/` (page, layout,
|
|
25
|
-
route, middleware, metadata routes). Browser-safe helpers go in `lib/utils/`,
|
|
26
|
-
feature logic in `modules/`, server-only code behind `.server.ts`.
|
|
27
|
-
- **For a UI app, render and LOOK before calling it done.** Give the design
|
|
28
|
-
tokens in `public/input.css` a palette that fits the app, then open every
|
|
29
|
-
route you changed in a real browser and play through its states.
|
|
30
|
-
`npm run check` and `npm run typecheck` pass even when a layout collapses, so
|
|
31
|
-
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.
|
|
32
17
|
|
|
33
18
|
## Before starting ANY work
|
|
34
19
|
|
|
@@ -9,9 +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
|
-
|
|
13
|
-
|
|
14
|
-
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.
|
|
15
13
|
|
|
16
14
|
## What WebJs Is
|
|
17
15
|
|
|
@@ -106,12 +104,12 @@ Common bundles:
|
|
|
106
104
|
1. **Classify the change.** Route contract, data model, server mutation, auth, or only UI?
|
|
107
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.
|
|
108
106
|
3. **Put code in the narrowest owner.** Route-local first (`modules/<feature>/`), promote to `lib/` or `components/` only when reuse is real.
|
|
109
|
-
4. **Keep server-only code behind `.server.ts
|
|
107
|
+
4. **Keep server-only code behind `.server.ts`** (Core WebJs Rules 1 and 2).
|
|
110
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.
|
|
111
109
|
6. **Validate input at the boundary.** Declare `export const validate` on an action; the RPC and `route()` boundaries run it.
|
|
112
110
|
7. **Default mutations to optimistic UI** where the client can predict the result (`optimistic()` from `@webjsdev/core`).
|
|
113
|
-
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`.
|
|
114
|
-
9. **Test the narrowest meaningful layer**, and
|
|
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).
|
|
115
113
|
|
|
116
114
|
## Project Layout
|
|
117
115
|
|
|
@@ -139,7 +137,7 @@ App-internal imports use the `#` root alias (`import { db } from '#db/connection
|
|
|
139
137
|
## Core WebJs Rules (invariants)
|
|
140
138
|
|
|
141
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).
|
|
142
|
-
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).
|
|
143
141
|
3. Custom element tag names contain a hyphen. Pass the tag to `Class.register('tag-name')`.
|
|
144
142
|
4. Event (`@`), property (`.`), and boolean (`?`) holes in `html` are UNQUOTED: `@click=${fn}`, never `@click="${fn}"`.
|
|
145
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.
|
|
@@ -149,7 +147,7 @@ App-internal imports use the `#` root alias (`import { db } from '#db/connection
|
|
|
149
147
|
9. No backtick characters inside an `html\`...\`` body, even in comments (it closes the literal and 500s).
|
|
150
148
|
10. TypeScript must be erasable (`erasableSyntaxOnly: true`): no `enum`, no value `namespace`, no constructor parameter properties, no legacy decorators.
|
|
151
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).
|
|
152
|
-
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"
|
|
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.
|
|
153
151
|
|
|
154
152
|
## Export Map
|
|
155
153
|
|
|
@@ -157,7 +155,7 @@ Find the right export fast. Load the linked reference for full examples.
|
|
|
157
155
|
|
|
158
156
|
### `@webjsdev/core` (browser + isomorphic)
|
|
159
157
|
|
|
160
|
-
- `html` / `css` tagged templates. `WebComponent({ ... })` base-class factory; `prop(type?, opts?)` declares one reactive property. `register(tag
|
|
158
|
+
- `html` / `css` tagged templates. `WebComponent({ ... })` base-class factory; `prop(type?, opts?)` declares one reactive property. `Class.register('tag')` binds the tag.
|
|
161
159
|
- `signal` / `computed` reactive state, `effect(fn)` client-only reaction (returns a disposer), `batch(fn)` coalesced writes; `render(v, el)` client render.
|
|
162
160
|
- `notFound()` / `redirect(url[, status])` control-flow throws (page/layout/action only, NOT `route.ts`). `forbidden()` / `unauthorized()` render the nearest boundary.
|
|
163
161
|
- `Suspense({fallback, children})` page-level streaming; `<webjs-suspense>` component-level streaming.
|
|
@@ -274,18 +272,12 @@ Success is a 303 (PRG); failure re-renders the page at 422 with the result on `a
|
|
|
274
272
|
|
|
275
273
|
## Common Mistakes To Avoid
|
|
276
274
|
|
|
277
|
-
|
|
275
|
+
The invariants above are not repeated here; this list is the mistakes they do not already name.
|
|
276
|
+
|
|
278
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.
|
|
279
|
-
- 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.
|
|
280
|
-
- Using a `static properties` block or a class-field initializer for reactive props instead of the `WebComponent({ ... })` factory.
|
|
281
|
-
- Quoting an event / property / boolean hole (`@click="${fn}"`).
|
|
282
278
|
- Writing `fetch()` to call your own server instead of importing the action.
|
|
283
|
-
- 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.
|
|
284
|
-
- 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.
|
|
285
|
-
- 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.
|
|
286
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`).
|
|
287
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.
|
|
288
|
-
- 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.
|
|
289
281
|
- Throwing `redirect()` / `notFound()` inside a `route.ts` handler (uncaught 500). Return a `Response` there.
|
|
290
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()`.
|
|
291
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
|
|
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
|
|
|
@@ -68,7 +68,7 @@ One limit worth knowing: the rewrite belongs to `startServer`. An app embedded t
|
|
|
68
68
|
webjs create my-app --runtime bun
|
|
69
69
|
```
|
|
70
70
|
|
|
71
|
-
`--runtime` is orthogonal to `--template`, so it re-flavors either full-stack or api. A Bun scaffold emits a `bun.lock`, a pure `oven/bun:1` Dockerfile plus a bun-install CI, and bun-command agent docs. The test, db, and check tooling still runs on Node.
|
|
71
|
+
`--runtime` is orthogonal to `--template`, so it re-flavors either full-stack or api. A Bun scaffold emits a `bun.lock`, a pure `oven/bun:1-slim` Dockerfile plus a bun-install CI, and bun-command agent docs. The test, db, and check tooling still runs on Node.
|
|
72
72
|
|
|
73
73
|
## Running on Bun
|
|
74
74
|
|
|
@@ -79,13 +79,15 @@ bun install
|
|
|
79
79
|
bun run dev # or: bun run start
|
|
80
80
|
```
|
|
81
81
|
|
|
82
|
-
`bun --bun` overrides the `webjs` bin's Node shebang so the server runs on Bun, selecting the native `Bun.serve` listener and `amaro` type stripping. The app's dependencies resolve from `node_modules` exactly as on Node. The `start.before` migrate step (`webjs db migrate`) runs under Bun too. Commit the `bun.lock` for reproducible, offline installs. The scaffold's Bun Dockerfile runs `bun install` and serves via `CMD ["bun", "--bun", "run", "start"]`.
|
|
82
|
+
`bun --bun` overrides the `webjs` bin's Node shebang so the server runs on Bun, selecting the native `Bun.serve` listener and `amaro` type stripping. The app's dependencies resolve from `node_modules` exactly as on Node. The `start.before` migrate step (`webjs db migrate`) runs under Bun too. Commit the `bun.lock` for reproducible, offline installs. The scaffold's Bun Dockerfile runs `bun install --production` and serves via `CMD ["bun", "--bun", "run", "start"]`.
|
|
83
|
+
|
|
84
|
+
**What the dev watcher ignores.** On both runtimes a change to a file nothing serves never reloads the page: `*.log`, `coverage/`, `.cache/`, `test-results/`, `playwright-report/`, and anything the app's `.gitignore` ignores (except `.env*`). So `npm run dev > dev.log` in the app folder is safe.
|
|
83
85
|
|
|
84
86
|
## Deploying either runtime
|
|
85
87
|
|
|
86
88
|
Production runs `npm run start` (Node) or `bun run start` (Bun), which serves the source directly with no build step. Both speak plain HTTP/1.1, so put a reverse proxy or platform edge in front for TLS and HTTP/2 (production perf leans on HTTP/2 multiplexing plus `modulepreload` hints, not a bundle). A `start.before` migrate runs first on both runtimes.
|
|
87
89
|
|
|
88
|
-
The scaffold ships a matching Dockerfile per runtime: a Node image for the default, a pure `oven/bun:1` image for `--runtime bun`. Commit the lockfile the runtime uses (`package-lock.json` for Node, `bun.lock` for Bun) so the deploy install is reproducible and offline.
|
|
90
|
+
The scaffold ships a matching Dockerfile per runtime: a Node image for the default, a pure `oven/bun:1-slim` image for `--runtime bun`. Both install production dependencies only and leave the package manager's cache out of the image. What the boot runs is a production dependency for that reason: `drizzle-kit` (the `start.before` migrate) and, on a UI app, `@tailwindcss/cli` + `tailwindcss` (the CSS compile). Keep a tool the boot runs in `dependencies`; one moved to `devDependencies` is missing from the image and the boot fails. Commit the lockfile the runtime uses (`package-lock.json` for Node, `bun.lock` for Bun) so the deploy install is reproducible and offline.
|
|
89
91
|
|
|
90
92
|
## SQLite busy_timeout
|
|
91
93
|
|
|
@@ -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
|
|
|
@@ -30,13 +30,6 @@ fi
|
|
|
30
30
|
# Read stdin so we don't break Claude Code's hook contract.
|
|
31
31
|
cat /dev/stdin >/dev/null 2>&1 || true
|
|
32
32
|
|
|
33
|
-
# A repository with no commit yet is a first build from the scaffold: the whole
|
|
34
|
-
# build is one logical unit (CLAUDE.md), committed once at the end, so nudging
|
|
35
|
-
# mid-build would only split it. The Stop hook still asks for that commit.
|
|
36
|
-
if ! git rev-parse --verify -q HEAD >/dev/null 2>&1; then
|
|
37
|
-
exit 0
|
|
38
|
-
fi
|
|
39
|
-
|
|
40
33
|
CHANGED=$(git status --porcelain 2>/dev/null | wc -l | tr -d ' ')
|
|
41
34
|
|
|
42
35
|
if [ -z "$CHANGED" ] || [ "$CHANGED" -lt "$THRESHOLD" ]; then
|