@webjsdev/cli 0.10.41 → 0.10.43
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 +345 -20
- package/lib/create.js +6 -1
- package/lib/doctor.js +43 -1
- package/lib/lean-copy.js +43 -0
- package/lib/saas-template.js +5 -1
- package/package.json +1 -1
- package/templates/.agents/skills/webjs/SKILL.md +1 -0
- package/templates/.agents/skills/webjs/references/ui-kit.md +68 -0
package/bin/webjs.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { resolve, join, dirname } from 'node:path';
|
|
3
3
|
import { spawn } from 'node:child_process';
|
|
4
|
+
import { readFileSync } from 'node:fs';
|
|
4
5
|
import { fileURLToPath } from 'node:url';
|
|
5
6
|
import { resolveBin } from '../lib/resolve-bin.js';
|
|
6
7
|
import { dbGenerateTtyHint } from '../lib/db-hints.js';
|
|
@@ -11,6 +12,11 @@ import { planDevSupervisor } from '../lib/dev-supervisor.js';
|
|
|
11
12
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
12
13
|
const [cmd, ...rest] = process.argv.slice(2);
|
|
13
14
|
|
|
15
|
+
// A `--help`/`-h` or `--version`/`-v` request (top-level or after a subcommand)
|
|
16
|
+
// must not be gated by the Node-version preflight, so it works on an old Node.
|
|
17
|
+
const wantsHelp = cmd === '--help' || cmd === '-h' || rest.includes('--help') || rest.includes('-h');
|
|
18
|
+
const wantsVersion = cmd === '--version' || cmd === '-v' || cmd === 'version';
|
|
19
|
+
|
|
14
20
|
// Node-version preflight (issue #238), INLINE and dependency-free.
|
|
15
21
|
// This MUST run before any `import @webjsdev/server`: importing the server
|
|
16
22
|
// package links `src/dev.js`, which references Node 24+ builtins, so on an old
|
|
@@ -19,8 +25,9 @@ const [cmd, ...rest] = process.argv.slice(2);
|
|
|
19
25
|
// `../lib/node-preflight.js`, which imports nothing), depending only on
|
|
20
26
|
// `process.versions.node`. The richer `assertNodeVersion` import inside main()
|
|
21
27
|
// stays as belt-and-suspenders for the link-ok (>= 22.13) cases.
|
|
22
|
-
// `help` / no-arg
|
|
23
|
-
|
|
28
|
+
// `help` / no-arg / any `--help`|`-h` / `version`|`--version`|`-v` request is
|
|
29
|
+
// exempt so a user on an old Node can still read usage and the version.
|
|
30
|
+
if (cmd !== 'help' && cmd !== undefined && !wantsHelp && !wantsVersion) {
|
|
24
31
|
let engines = '>=24.0.0';
|
|
25
32
|
try {
|
|
26
33
|
const { readFileSync } = await import('node:fs');
|
|
@@ -47,8 +54,10 @@ const USAGE = `webjs commands:
|
|
|
47
54
|
webjs start [--port 8080] Start production server (serves source directly, no build step)
|
|
48
55
|
webjs test [--server|--browser] Run server + browser tests
|
|
49
56
|
webjs check [--json] Run correctness checks (--json emits structured violations)
|
|
57
|
+
webjs routes [--json|--table] [--no-headers] Print the route table (path / owner file / methods). Default tree; --json matches the MCP list_routes shape; --no-headers drops the --table header
|
|
50
58
|
webjs mcp Start the read-only MCP server (routes / actions / components / check)
|
|
51
|
-
webjs doctor
|
|
59
|
+
webjs doctor [--json] [--strict] Verify project health (Node, tsconfig, env, vendor pins, importmap coherence, @webjsdev versions, git hook, page/layout elision).
|
|
60
|
+
--json emits the structured results (with stable codes). --strict also fails the exit on warnings
|
|
52
61
|
webjs types Generate .webjs/routes.d.ts (typed Route union + per-route params)
|
|
53
62
|
webjs typecheck [tsc args...] Type-check the app with the project's tsc --noEmit (non-zero on errors)
|
|
54
63
|
webjs create <name> [--template full-stack|api|saas] [--db sqlite|postgres] [--runtime node|bun] [--no-install] Scaffold a new webjs app
|
|
@@ -70,7 +79,189 @@ const USAGE = `webjs commands:
|
|
|
70
79
|
--download: also downloads bundles for offline production
|
|
71
80
|
webjs vendor unpin <pkg> Remove a specific package from the pin file
|
|
72
81
|
webjs vendor list Show pinned packages with versions and URLs
|
|
73
|
-
webjs
|
|
82
|
+
webjs version Print the installed @webjsdev/cli version (also: webjs --version / -v)
|
|
83
|
+
webjs help [command] Show this help, or per-command usage + examples (e.g. webjs help routes).
|
|
84
|
+
The flag forms work too: webjs --help / -h for this banner, webjs <command> --help / -h for one command`;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Per-command help: usage line, one-line summary, an Options table, and an
|
|
88
|
+
* Examples block (#975). `webjs help <cmd>` (and `webjs <cmd> --help`) renders
|
|
89
|
+
* this so an agent sees the exact flags + worked examples instead of guessing
|
|
90
|
+
* from the one-line USAGE row. Keyed by the top-level command; `db` / `ui` /
|
|
91
|
+
* `vendor` document their subcommand shape. Every entry that takes flags lists
|
|
92
|
+
* them in `options` so the flag surface is machine-readable, matching the Remix
|
|
93
|
+
* CLI's per-command Options section.
|
|
94
|
+
* @type {Record<string, { usage: string, summary: string, options?: Array<{ flag: string, description: string }>, examples: string[] }>}
|
|
95
|
+
*/
|
|
96
|
+
const HELP = {
|
|
97
|
+
dev: {
|
|
98
|
+
usage: 'webjs dev [--port <n>] [--no-hot]',
|
|
99
|
+
summary: 'Start the dev server with live reload (source is the runtime, no build step).',
|
|
100
|
+
options: [
|
|
101
|
+
{ flag: '--port <n>', description: 'Port to listen on (else PORT, else 8080).' },
|
|
102
|
+
{ flag: '--no-hot', description: 'Run in-process, without the hot-reload supervisor.' },
|
|
103
|
+
],
|
|
104
|
+
examples: ['webjs dev', 'webjs dev --port 3000', 'webjs dev --no-hot'],
|
|
105
|
+
},
|
|
106
|
+
start: {
|
|
107
|
+
usage: 'webjs start [--port <n>]',
|
|
108
|
+
summary: 'Start the production server (serves source directly, plain HTTP/1.1).',
|
|
109
|
+
options: [
|
|
110
|
+
{ flag: '--port <n>', description: 'Port to listen on (else PORT, else 8080).' },
|
|
111
|
+
],
|
|
112
|
+
examples: ['webjs start', 'webjs start --port 8080', 'PORT=8080 webjs start'],
|
|
113
|
+
},
|
|
114
|
+
test: {
|
|
115
|
+
usage: 'webjs test [--server] [--browser] [--watch]',
|
|
116
|
+
summary: 'Run the app test suites (server-side node:test and/or browser via web-test-runner).',
|
|
117
|
+
options: [
|
|
118
|
+
{ flag: '--server', description: 'Run only the server-side tests.' },
|
|
119
|
+
{ flag: '--browser', description: 'Run only the browser tests.' },
|
|
120
|
+
{ flag: '--watch', description: 'Re-run on change.' },
|
|
121
|
+
],
|
|
122
|
+
examples: ['webjs test', 'webjs test --server', 'webjs test --browser --watch'],
|
|
123
|
+
},
|
|
124
|
+
check: {
|
|
125
|
+
usage: 'webjs check [--rules] [--json]',
|
|
126
|
+
summary: 'Run the correctness checks (report-only, no autofix). Exits non-zero on any violation.',
|
|
127
|
+
options: [
|
|
128
|
+
{ flag: '--json', description: 'Emit the structured violations + summary as JSON (agent-friendly).' },
|
|
129
|
+
{ flag: '--rules', description: 'List the correctness rules instead of running them.' },
|
|
130
|
+
],
|
|
131
|
+
examples: ['webjs check', 'webjs check --json', 'webjs check --rules'],
|
|
132
|
+
},
|
|
133
|
+
routes: {
|
|
134
|
+
usage: 'webjs routes [--json | --table] [--no-headers]',
|
|
135
|
+
summary: 'Print the route table: each page/route path, its owner file, and (for route handlers) its HTTP methods.',
|
|
136
|
+
options: [
|
|
137
|
+
{ flag: '--json', description: 'Emit { pages, apis } as JSON (byte-identical to the MCP list_routes tool).' },
|
|
138
|
+
{ flag: '--table', description: 'Flat aligned KIND / PATH / METHODS / FILE columns.' },
|
|
139
|
+
{ flag: '--no-headers', description: 'Omit the header row (only with --table); easier to pipe.' },
|
|
140
|
+
],
|
|
141
|
+
examples: ['webjs routes', 'webjs routes --table', 'webjs routes --table --no-headers', 'webjs routes --json'],
|
|
142
|
+
},
|
|
143
|
+
doctor: {
|
|
144
|
+
usage: 'webjs doctor [--json] [--strict]',
|
|
145
|
+
summary: 'Verify project health. Each result carries a stable code so an agent branches on the failure kind.',
|
|
146
|
+
options: [
|
|
147
|
+
{ flag: '--json', description: 'Emit the DoctorResult[] (with stable codes) + a summary as JSON.' },
|
|
148
|
+
{ flag: '--strict', description: 'Also fail the exit on warnings, not just hard failures.' },
|
|
149
|
+
],
|
|
150
|
+
examples: ['webjs doctor', 'webjs doctor --json', 'webjs doctor --strict', 'webjs doctor --json --strict'],
|
|
151
|
+
},
|
|
152
|
+
types: {
|
|
153
|
+
usage: 'webjs types',
|
|
154
|
+
summary: 'Generate .webjs/routes.d.ts (the typed Route href union + per-route params).',
|
|
155
|
+
examples: ['webjs types'],
|
|
156
|
+
},
|
|
157
|
+
typecheck: {
|
|
158
|
+
usage: 'webjs typecheck [tsc args...]',
|
|
159
|
+
summary: "Type-check the app with the project's own tsc --noEmit. Extra args pass through to tsc.",
|
|
160
|
+
examples: ['webjs typecheck', 'webjs typecheck --watch'],
|
|
161
|
+
},
|
|
162
|
+
create: {
|
|
163
|
+
usage: 'webjs create <name> [--template full-stack|api|saas] [--db sqlite|postgres] [--runtime node|bun] [--no-install]',
|
|
164
|
+
summary: 'Scaffold a new app. Defaults: full-stack template, Drizzle + SQLite, Node runtime.',
|
|
165
|
+
options: [
|
|
166
|
+
{ flag: '--template <t>', description: 'full-stack (default), api, or saas.' },
|
|
167
|
+
{ flag: '--db <d>', description: 'sqlite (default) or postgres.' },
|
|
168
|
+
{ flag: '--runtime <r>', description: 'node (default) or bun.' },
|
|
169
|
+
{ flag: '--no-install', description: 'Skip the package-manager install step.' },
|
|
170
|
+
],
|
|
171
|
+
examples: [
|
|
172
|
+
'webjs create my-app',
|
|
173
|
+
'webjs create my-api --template api',
|
|
174
|
+
'webjs create my-saas --template saas --db postgres',
|
|
175
|
+
'webjs create my-app --runtime bun',
|
|
176
|
+
],
|
|
177
|
+
},
|
|
178
|
+
db: {
|
|
179
|
+
usage: 'webjs db <generate|migrate|push|studio|seed>',
|
|
180
|
+
summary: 'Database tasks (wraps drizzle-kit); seed runs db/seed.server.ts.',
|
|
181
|
+
examples: ['webjs db generate', 'webjs db migrate', 'webjs db studio', 'webjs db seed'],
|
|
182
|
+
},
|
|
183
|
+
ui: {
|
|
184
|
+
usage: 'webjs ui <init|add|list|view|diff|info> [names...]',
|
|
185
|
+
summary: 'AI-first component library CLI. Requires @webjsdev/ui installed in the project.',
|
|
186
|
+
examples: ['webjs ui init', 'webjs ui add button card', 'webjs ui list'],
|
|
187
|
+
},
|
|
188
|
+
vendor: {
|
|
189
|
+
usage: 'webjs vendor <pin|unpin|list|audit|outdated|update> [--from <provider>] [--download]',
|
|
190
|
+
summary: 'Pin client-side npm packages into .webjs/vendor/importmap.json.',
|
|
191
|
+
options: [
|
|
192
|
+
{ flag: '--from <provider>', description: 'jspm (default), jsdelivr, unpkg, or skypack.' },
|
|
193
|
+
{ flag: '--download', description: 'Also download the bundles for offline production (with pin).' },
|
|
194
|
+
],
|
|
195
|
+
examples: ['webjs vendor pin', 'webjs vendor pin --download', 'webjs vendor list', 'webjs vendor outdated'],
|
|
196
|
+
},
|
|
197
|
+
mcp: {
|
|
198
|
+
usage: 'webjs mcp',
|
|
199
|
+
summary: 'Start the read-only MCP server (routes / actions / components / check + a docs/source knowledge layer).',
|
|
200
|
+
examples: ['webjs mcp'],
|
|
201
|
+
},
|
|
202
|
+
version: {
|
|
203
|
+
usage: 'webjs version',
|
|
204
|
+
summary: 'Print the installed @webjsdev/cli version. Also available as webjs --version / -v.',
|
|
205
|
+
examples: ['webjs version', 'webjs --version'],
|
|
206
|
+
},
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Render `webjs help <cmd>` to stdout: usage, summary, an Options table (when
|
|
211
|
+
* the command takes flags), and Examples, in the Remix-CLI section shape. A
|
|
212
|
+
* universal `-h, --help` option row is appended so every command advertises it.
|
|
213
|
+
* Returns true if the command was found, false otherwise (so the caller can
|
|
214
|
+
* exit non-zero on an unknown help topic, matching the Remix CLI).
|
|
215
|
+
* @param {string} name
|
|
216
|
+
* @returns {boolean}
|
|
217
|
+
*/
|
|
218
|
+
function printCommandHelp(name) {
|
|
219
|
+
const h = HELP[name];
|
|
220
|
+
if (!h) {
|
|
221
|
+
console.error(`Unknown help topic "${name}".\n`);
|
|
222
|
+
console.error(USAGE);
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
console.log(`Usage: ${h.usage}\n`);
|
|
226
|
+
console.log(` ${h.summary}\n`);
|
|
227
|
+
// A command that forwards to an external tool does NOT show webjs's own help
|
|
228
|
+
// for `--help`; word its `-h, --help` row to say so, rather than the generic
|
|
229
|
+
// "Show this help." which would be false for those three commands.
|
|
230
|
+
const tool = HELP_FLAG_PASSTHROUGH_TOOL[name];
|
|
231
|
+
const helpRow = tool
|
|
232
|
+
? { flag: '-h, --help', description: `Forwarded to ${tool} (this command wraps it).` }
|
|
233
|
+
: { flag: '-h, --help', description: 'Show this help.' };
|
|
234
|
+
const options = [...(h.options || []), helpRow];
|
|
235
|
+
const width = Math.max(...options.map((o) => o.flag.length));
|
|
236
|
+
console.log('Options:');
|
|
237
|
+
for (const o of options) console.log(` ${o.flag.padEnd(width)} ${o.description}`);
|
|
238
|
+
console.log('\nExamples:');
|
|
239
|
+
for (const ex of h.examples) console.log(` ${ex}`);
|
|
240
|
+
return true;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Commands that forward their remaining args to an external CLI, so a trailing
|
|
245
|
+
* `--help` / `-h` should reach THAT tool's own help, not webjs's. Maps the
|
|
246
|
+
* command to the tool it wraps (used both to skip the help-flag intercept and
|
|
247
|
+
* to word the `-h, --help` row in that command's help accurately).
|
|
248
|
+
* @type {Record<string, string>}
|
|
249
|
+
*/
|
|
250
|
+
const HELP_FLAG_PASSTHROUGH_TOOL = { typecheck: 'tsc', db: 'drizzle-kit', ui: '@webjsdev/ui' };
|
|
251
|
+
const HELP_FLAG_PASSTHROUGH = new Set(Object.keys(HELP_FLAG_PASSTHROUGH_TOOL));
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* The installed `@webjsdev/cli` version, read from this package's own
|
|
255
|
+
* package.json. Falls back to `0.0.0` if unreadable (never throws).
|
|
256
|
+
* @returns {string}
|
|
257
|
+
*/
|
|
258
|
+
function readCliVersion() {
|
|
259
|
+
try {
|
|
260
|
+
return JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8')).version || '0.0.0';
|
|
261
|
+
} catch {
|
|
262
|
+
return '0.0.0';
|
|
263
|
+
}
|
|
264
|
+
}
|
|
74
265
|
|
|
75
266
|
/** @param {string[]} args */
|
|
76
267
|
function flag(args, name, def) {
|
|
@@ -116,12 +307,34 @@ async function startDevParallelTasks(commands, cwd) {
|
|
|
116
307
|
}
|
|
117
308
|
|
|
118
309
|
async function main() {
|
|
119
|
-
//
|
|
310
|
+
// `--version` / `-v` (top level): print the installed CLI version and exit.
|
|
311
|
+
if (cmd === '--version' || cmd === '-v') {
|
|
312
|
+
console.log(readCliVersion());
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
// `--help` / `-h` (#975): a top-level flag (webjs --help / -h) prints the
|
|
316
|
+
// banner; the same flag AFTER a subcommand (webjs routes --help) prints that
|
|
317
|
+
// command's help and short-circuits it. Handled before the Node preflight so
|
|
318
|
+
// it works even on an old Node. A command that forwards its args to an
|
|
319
|
+
// external CLI (typecheck/db/ui, in HELP_FLAG_PASSTHROUGH) is skipped so the
|
|
320
|
+
// tool's own --help still reaches it; an UNRECOGNISED command is not
|
|
321
|
+
// intercepted either, so it falls through to the Unknown-command error
|
|
322
|
+
// (exit 1) rather than a silent 0.
|
|
323
|
+
if (cmd === '--help' || cmd === '-h') {
|
|
324
|
+
console.log(USAGE);
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
if (cmd && HELP[cmd] && !HELP_FLAG_PASSTHROUGH.has(cmd) && (rest.includes('--help') || rest.includes('-h'))) {
|
|
328
|
+
printCommandHelp(cmd);
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Node preflight: WebJs needs Node 24+ (built-in TS strip + recursive fs.watch).
|
|
120
333
|
// Run before any subcommand so an older Node fails fast with a clear,
|
|
121
334
|
// actionable message naming the found + required version, exiting non-zero
|
|
122
|
-
// instead of crashing cryptically later. `help`
|
|
123
|
-
// old Node can still read usage.
|
|
124
|
-
if (cmd !== 'help' && cmd !== undefined) {
|
|
335
|
+
// instead of crashing cryptically later. `help` / a help or version request
|
|
336
|
+
// is exempt so a user on an old Node can still read usage and the version.
|
|
337
|
+
if (cmd !== 'help' && cmd !== undefined && !wantsHelp && !wantsVersion) {
|
|
125
338
|
const { assertNodeVersion } = await import('@webjsdev/server');
|
|
126
339
|
assertNodeVersion({ onFail: 'exit' });
|
|
127
340
|
}
|
|
@@ -461,14 +674,6 @@ async function main() {
|
|
|
461
674
|
// app's concern, not a broken toolchain).
|
|
462
675
|
const { runDoctorChecks } = await import('../lib/doctor.js');
|
|
463
676
|
const results = await runDoctorChecks(process.cwd());
|
|
464
|
-
const marker = { pass: '[pass]', warn: '[warn]', fail: '[fail]' };
|
|
465
|
-
console.log('webjs doctor: project-health checklist\n');
|
|
466
|
-
for (const r of results) {
|
|
467
|
-
console.log(` ${marker[r.status]} ${r.name}`);
|
|
468
|
-
console.log(` ${r.message}`);
|
|
469
|
-
if (r.fix && r.status !== 'pass') console.log(` Fix: ${r.fix}`);
|
|
470
|
-
console.log();
|
|
471
|
-
}
|
|
472
677
|
const counts = results.reduce((acc, r) => {
|
|
473
678
|
acc[r.status] = (acc[r.status] || 0) + 1;
|
|
474
679
|
return acc;
|
|
@@ -476,15 +681,121 @@ async function main() {
|
|
|
476
681
|
const pass = counts.pass || 0;
|
|
477
682
|
const warn = counts.warn || 0;
|
|
478
683
|
const fail = counts.fail || 0;
|
|
684
|
+
// `--strict` also fails the exit on warnings, so an agent can gate on a
|
|
685
|
+
// fully-clean toolchain (drift / staleness / pin freshness) in a fix loop,
|
|
686
|
+
// not just on a hard toolchain break. Default keeps warnings non-fatal.
|
|
687
|
+
const strict = rest.includes('--strict');
|
|
688
|
+
const failing = fail > 0 || (strict && warn > 0);
|
|
689
|
+
|
|
690
|
+
// --json emits the raw DoctorResult[] (each carries a stable `code`) plus
|
|
691
|
+
// a summary, so an agent consumes structured data instead of scraping the
|
|
692
|
+
// text. Shape mirrors `check --json`: a top-level array-bearing object
|
|
693
|
+
// with a `summary` count. The non-zero exit is preserved (an agent gates
|
|
694
|
+
// on the exit code AND parses the report).
|
|
695
|
+
if (rest.includes('--json')) {
|
|
696
|
+
console.log(JSON.stringify({
|
|
697
|
+
results,
|
|
698
|
+
summary: { pass, warn, fail, strict, ok: !failing },
|
|
699
|
+
}));
|
|
700
|
+
if (failing) process.exit(1);
|
|
701
|
+
break;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
const marker = { pass: '[pass]', warn: '[warn]', fail: '[fail]' };
|
|
705
|
+
console.log('webjs doctor: project-health checklist\n');
|
|
706
|
+
for (const r of results) {
|
|
707
|
+
console.log(` ${marker[r.status]} ${r.name} (${r.code})`);
|
|
708
|
+
console.log(` ${r.message}`);
|
|
709
|
+
if (r.fix && r.status !== 'pass') console.log(` Fix: ${r.fix}`);
|
|
710
|
+
console.log();
|
|
711
|
+
}
|
|
479
712
|
console.log(` ${pass} passed, ${warn} warning(s), ${fail} failed.`);
|
|
480
|
-
if (
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
713
|
+
if (failing) {
|
|
714
|
+
const reason = fail > 0
|
|
715
|
+
? `${fail} hard check(s) failed. Fix the toolchain issue(s) above.`
|
|
716
|
+
: `${warn} warning(s) found and --strict was set.`;
|
|
717
|
+
console.error(`\nwebjs doctor: ${reason}`);
|
|
484
718
|
process.exit(1);
|
|
485
719
|
}
|
|
486
720
|
break;
|
|
487
721
|
}
|
|
722
|
+
case 'routes': {
|
|
723
|
+
// Print the app route table to stdout (#975): every page (path, owner
|
|
724
|
+
// file, dynamic params) and every route.{js,ts} API handler (path, owner
|
|
725
|
+
// file, HTTP methods). Reuses the ONE route walker (`buildRouteTable`, the
|
|
726
|
+
// same walk that backs `webjs types` and the dev server) and the shared
|
|
727
|
+
// `projectRoutes` projector, so `--json` is byte-identical to the MCP
|
|
728
|
+
// `list_routes` tool. Read-only: no module load, no autofix.
|
|
729
|
+
const { buildRouteTable } = await import('@webjsdev/server');
|
|
730
|
+
const { projectRoutes } = await import('@webjsdev/mcp/routes-report');
|
|
731
|
+
const { extractRouteMethods } = await import('@webjsdev/mcp');
|
|
732
|
+
const { readFile } = await import('node:fs/promises');
|
|
733
|
+
const appDir = process.cwd();
|
|
734
|
+
const table = await buildRouteTable(appDir);
|
|
735
|
+
const report = await projectRoutes(table, { appDir, readFile, extractRouteMethods });
|
|
736
|
+
|
|
737
|
+
// --json: the machine contract, identical to the MCP `list_routes` shape.
|
|
738
|
+
if (rest.includes('--json')) {
|
|
739
|
+
console.log(JSON.stringify(report));
|
|
740
|
+
break;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
const { pages, apis } = report;
|
|
744
|
+
// A page is reached via GET (its server render); a route.{js,ts} exposes
|
|
745
|
+
// exactly its exported verbs. Present both as one path -> methods -> file
|
|
746
|
+
// view. Dynamic pages append their param names.
|
|
747
|
+
const pageMethods = 'GET';
|
|
748
|
+
|
|
749
|
+
// --table: flat, aligned columns (KIND / PATH / METHODS / FILE), the
|
|
750
|
+
// easiest shape for an agent to scan or a human to grep. `--no-headers`
|
|
751
|
+
// drops the header row so the output pipes cleanly into awk/cut.
|
|
752
|
+
if (rest.includes('--table')) {
|
|
753
|
+
/** @type {Array<[string,string,string,string]>} */
|
|
754
|
+
const rows = [];
|
|
755
|
+
if (!rest.includes('--no-headers')) rows.push(['KIND', 'PATH', 'METHODS', 'FILE']);
|
|
756
|
+
for (const p of pages) {
|
|
757
|
+
rows.push(['page', p.path + (p.params ? ` [${p.params.join(', ')}]` : ''), pageMethods, p.file]);
|
|
758
|
+
}
|
|
759
|
+
for (const a of apis) {
|
|
760
|
+
rows.push(['api', a.path, a.methods.join(', ') || '(none)', a.file]);
|
|
761
|
+
}
|
|
762
|
+
// With --no-headers and no routes there are no rows; nothing to print.
|
|
763
|
+
if (rows.length === 0) break;
|
|
764
|
+
const widths = [0, 1, 2].map((c) => Math.max(...rows.map((r) => r[c].length)));
|
|
765
|
+
for (const r of rows) {
|
|
766
|
+
console.log(
|
|
767
|
+
`${r[0].padEnd(widths[0])} ${r[1].padEnd(widths[1])} ${r[2].padEnd(widths[2])} ${r[3]}`,
|
|
768
|
+
);
|
|
769
|
+
}
|
|
770
|
+
break;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
// Default: a grouped tree.
|
|
774
|
+
console.log(`webjs routes: ${pages.length} page(s), ${apis.length} API route(s)\n`);
|
|
775
|
+
if (pages.length) {
|
|
776
|
+
console.log('Pages');
|
|
777
|
+
const pathW = Math.max(...pages.map((p) => p.path.length));
|
|
778
|
+
for (const p of pages) {
|
|
779
|
+
const params = p.params ? ` ${p.params.map((n) => `[${n}]`).join(' ')}` : '';
|
|
780
|
+
console.log(` ${p.path.padEnd(pathW)} ${p.file}${params}`);
|
|
781
|
+
}
|
|
782
|
+
console.log();
|
|
783
|
+
}
|
|
784
|
+
if (apis.length) {
|
|
785
|
+
console.log('API routes');
|
|
786
|
+
const pathW = Math.max(...apis.map((a) => a.path.length));
|
|
787
|
+
const methW = Math.max(...apis.map((a) => (a.methods.join(', ') || '(none)').length));
|
|
788
|
+
for (const a of apis) {
|
|
789
|
+
const methods = a.methods.join(', ') || '(none)';
|
|
790
|
+
console.log(` ${a.path.padEnd(pathW)} ${methods.padEnd(methW)} ${a.file}`);
|
|
791
|
+
}
|
|
792
|
+
console.log();
|
|
793
|
+
}
|
|
794
|
+
if (!pages.length && !apis.length) {
|
|
795
|
+
console.log(' No routes found. Add an app/page.ts or an app/**/route.ts.');
|
|
796
|
+
}
|
|
797
|
+
break;
|
|
798
|
+
}
|
|
488
799
|
case 'types': {
|
|
489
800
|
// Generate `.webjs/routes.d.ts` from the app's `app/` routes (#258),
|
|
490
801
|
// narrowing the @webjsdev/core `Route` href union + per-route `params`.
|
|
@@ -899,7 +1210,21 @@ Full docs: https://docs.webjs.dev`);
|
|
|
899
1210
|
});
|
|
900
1211
|
break;
|
|
901
1212
|
}
|
|
1213
|
+
case 'version':
|
|
1214
|
+
// Print the installed CLI version (#975). Also reachable as the top-level
|
|
1215
|
+
// `webjs --version` / `-v` flag, handled at the top of main().
|
|
1216
|
+
console.log(readCliVersion());
|
|
1217
|
+
break;
|
|
902
1218
|
case 'help':
|
|
1219
|
+
// `webjs help <cmd>` prints that command's usage + Options + Examples
|
|
1220
|
+
// (#975); a bare `webjs help` prints the full banner. An unknown topic
|
|
1221
|
+
// exits non-zero (printCommandHelp returns false), matching the Remix CLI.
|
|
1222
|
+
if (rest[0]) {
|
|
1223
|
+
if (!printCommandHelp(rest[0])) process.exit(1);
|
|
1224
|
+
} else {
|
|
1225
|
+
console.log(USAGE);
|
|
1226
|
+
}
|
|
1227
|
+
break;
|
|
903
1228
|
case undefined:
|
|
904
1229
|
console.log(USAGE);
|
|
905
1230
|
break;
|
package/lib/create.js
CHANGED
|
@@ -18,6 +18,7 @@ import { existsSync } from 'node:fs';
|
|
|
18
18
|
import { createRequire } from 'node:module';
|
|
19
19
|
import { spawnSync } from 'node:child_process';
|
|
20
20
|
import { bunifyProse, bunifyDockerfile, bunifyCompose, bunifyCi } from './runtime-rewrite.js';
|
|
21
|
+
import { leanComponentSource } from './lean-copy.js';
|
|
21
22
|
|
|
22
23
|
/**
|
|
23
24
|
* Detect which package manager invoked us. Reads `npm_config_user_agent`,
|
|
@@ -120,12 +121,16 @@ async function readUiComponent(name) {
|
|
|
120
121
|
const raw = await readFile(src, 'utf8');
|
|
121
122
|
// The registry component imports cn() via a relative `../lib/utils.ts`; rewrite
|
|
122
123
|
// it to the scaffolded app's aliased path (cn lives at lib/utils/cn.ts).
|
|
123
|
-
|
|
124
|
+
const rewritten = raw
|
|
124
125
|
.replaceAll("'../lib/utils.ts'", "'#lib/utils/cn.ts'")
|
|
125
126
|
.replaceAll('"../lib/utils.ts"', '"#lib/utils/cn.ts"')
|
|
126
127
|
// onBeforeCache lives in its own client-only module so cn() stays pure (#819).
|
|
127
128
|
.replaceAll("'../lib/dom.ts'", "'#lib/utils/dom.ts'")
|
|
128
129
|
.replaceAll('"../lib/dom.ts"', '"#lib/utils/dom.ts"');
|
|
130
|
+
// Strip the worked @example from a Tier-1 helper (same as `webjs ui add`), so
|
|
131
|
+
// the scaffolded component is lean and the example is served on demand. The
|
|
132
|
+
// shared helper is used by the saas-template copier too, so they cannot drift.
|
|
133
|
+
return leanComponentSource(rewritten, name);
|
|
129
134
|
}
|
|
130
135
|
|
|
131
136
|
/**
|
package/lib/doctor.js
CHANGED
|
@@ -44,9 +44,48 @@ import { checkNodeInline } from './node-preflight.js';
|
|
|
44
44
|
|
|
45
45
|
/**
|
|
46
46
|
* @typedef {'pass' | 'warn' | 'fail'} DoctorStatus
|
|
47
|
-
* @typedef {{ name: string, status: DoctorStatus, message: string, fix?: string }} DoctorResult
|
|
47
|
+
* @typedef {{ name: string, code: string, status: DoctorStatus, message: string, fix?: string }} DoctorResult
|
|
48
48
|
*/
|
|
49
49
|
|
|
50
|
+
/**
|
|
51
|
+
* Stable machine-readable code per check (#975), so an agent consuming
|
|
52
|
+
* `webjs doctor --json` branches on the failure KIND, not the human message
|
|
53
|
+
* text (which is free to change). The `name` stays the display identity (some
|
|
54
|
+
* are kebab-case, two are prose); the `code` is the durable contract, a
|
|
55
|
+
* SCREAMING_SNAKE_CASE constant that never changes for a given check. Attached
|
|
56
|
+
* centrally in `runDoctorChecks` so every check function stays focused on its
|
|
57
|
+
* own logic. Mirrors Remix's `DoctorFindingCode` enum (its `doctor/types.ts`).
|
|
58
|
+
*
|
|
59
|
+
* Keyed by each check's `name`. A missing entry falls back to a name-derived
|
|
60
|
+
* code (see `codeForName`), but every shipped check is listed here explicitly
|
|
61
|
+
* and a drift test asserts each result carries one of these codes.
|
|
62
|
+
* @type {Record<string, string>}
|
|
63
|
+
*/
|
|
64
|
+
export const DOCTOR_CODES = {
|
|
65
|
+
'node-version': 'NODE_VERSION',
|
|
66
|
+
'tsconfig-erasable': 'TSCONFIG_ERASABLE',
|
|
67
|
+
'env-drift': 'ENV_DRIFT',
|
|
68
|
+
'vendor-pin': 'VENDOR_PIN',
|
|
69
|
+
'vendor-gitignore': 'VENDOR_GITIGNORE',
|
|
70
|
+
'webjs-versions': 'WEBJS_VERSIONS',
|
|
71
|
+
'framework-resolve': 'FRAMEWORK_RESOLVE',
|
|
72
|
+
'importmap-coherence': 'IMPORTMAP_COHERENCE',
|
|
73
|
+
'git-hook': 'GIT_HOOK',
|
|
74
|
+
'Page/layout elision (carrier hygiene)': 'ELISION_CARRIERS',
|
|
75
|
+
'Static build outputs (dev.regenerate freshness)': 'STATIC_ASSET_FRESHNESS',
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The stable code for a check name: the explicit `DOCTOR_CODES` entry, else a
|
|
80
|
+
* best-effort derivation (uppercased, non-alphanumerics collapsed to `_`) so a
|
|
81
|
+
* newly-added check that forgets its map entry still gets a non-empty code.
|
|
82
|
+
* @param {string} name
|
|
83
|
+
* @returns {string}
|
|
84
|
+
*/
|
|
85
|
+
export function codeForName(name) {
|
|
86
|
+
return DOCTOR_CODES[name] || name.toUpperCase().replace(/[^A-Z0-9]+/g, '_').replace(/^_+|_+$/g, '');
|
|
87
|
+
}
|
|
88
|
+
|
|
50
89
|
/**
|
|
51
90
|
* Read the CLI package's own `engines.node` so the required Node major lives in
|
|
52
91
|
* one place (mirrors how `bin/webjs.js` sources it). Falls back to `>=24.0.0`.
|
|
@@ -1020,5 +1059,8 @@ export async function runDoctorChecks(appDir, opts = {}) {
|
|
|
1020
1059
|
checkElisionCarriers(appDir),
|
|
1021
1060
|
checkStaticAssetFreshness(appDir),
|
|
1022
1061
|
]);
|
|
1062
|
+
// Attach the stable machine code to every result (#975). Centralized here so
|
|
1063
|
+
// each check function stays free of the code-contract concern.
|
|
1064
|
+
for (const r of results) r.code = codeForName(r.name);
|
|
1023
1065
|
return results;
|
|
1024
1066
|
}
|
package/lib/lean-copy.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The scaffold's lean-copy of a ui component (#983).
|
|
3
|
+
*
|
|
4
|
+
* `webjs create` copies a few `@webjsdev/ui` registry components into a
|
|
5
|
+
* generated app. To match what `webjs ui add` writes, a Tier-1 helper's worked
|
|
6
|
+
* `@example` is stripped (the example is served on demand by `webjs ui view` /
|
|
7
|
+
* the MCP `ui` tool), while a Tier-2 element file is kept whole. Both scaffold
|
|
8
|
+
* copiers (`create.js` and `saas-template.js`) go through THIS one helper so
|
|
9
|
+
* they cannot drift.
|
|
10
|
+
*
|
|
11
|
+
* The strip primitives live in `@webjsdev/ui/registry/extract`; if that subpath
|
|
12
|
+
* cannot be resolved, this degrades to a no-op (keep the example) so the strip
|
|
13
|
+
* is never a reason `webjs create` fails.
|
|
14
|
+
*
|
|
15
|
+
* @module lean-copy
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
let _mod = null;
|
|
19
|
+
|
|
20
|
+
async function loadPrimitives() {
|
|
21
|
+
if (_mod) return _mod;
|
|
22
|
+
try {
|
|
23
|
+
const m = await import('@webjsdev/ui/registry/extract');
|
|
24
|
+
_mod = { stripExample: m.stripExample, isCustomElementSource: m.isCustomElementSource };
|
|
25
|
+
} catch {
|
|
26
|
+
_mod = { stripExample: (s) => s, isCustomElementSource: () => true };
|
|
27
|
+
}
|
|
28
|
+
return _mod;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Return the component source as `webjs ui add` would write it: a Tier-1 helper
|
|
33
|
+
* has its worked `@example` stripped and a pointer left; a Tier-2 element is
|
|
34
|
+
* returned unchanged.
|
|
35
|
+
*
|
|
36
|
+
* @param {string} source the component source (imports already rewritten)
|
|
37
|
+
* @param {string} name the component name (for the pointer)
|
|
38
|
+
* @returns {Promise<string>}
|
|
39
|
+
*/
|
|
40
|
+
export async function leanComponentSource(source, name) {
|
|
41
|
+
const { stripExample, isCustomElementSource } = await loadPrimitives();
|
|
42
|
+
return isCustomElementSource(source) ? source : stripExample(source, name);
|
|
43
|
+
}
|
package/lib/saas-template.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import { mkdir, writeFile, readFile } from 'node:fs/promises';
|
|
7
7
|
import { bunifyProse } from './runtime-rewrite.js';
|
|
8
|
+
import { leanComponentSource } from './lean-copy.js';
|
|
8
9
|
import { existsSync } from 'node:fs';
|
|
9
10
|
import { join, resolve, dirname } from 'node:path';
|
|
10
11
|
import { fileURLToPath } from 'node:url';
|
|
@@ -25,7 +26,7 @@ async function readUiComponent(name) {
|
|
|
25
26
|
const raw = await readFile(src, 'utf8');
|
|
26
27
|
// The registry component imports cn() via a relative `../lib/utils.ts`; rewrite
|
|
27
28
|
// it to the scaffolded app's aliased path (cn lives at lib/utils/cn.ts).
|
|
28
|
-
|
|
29
|
+
const rewritten = raw
|
|
29
30
|
.replaceAll("'../lib/utils.ts'", "'#lib/utils/cn.ts'")
|
|
30
31
|
.replaceAll('"../lib/utils.ts"', '"#lib/utils/cn.ts"')
|
|
31
32
|
// onBeforeCache lives in its own client-only module so cn() stays pure (#819).
|
|
@@ -33,6 +34,9 @@ async function readUiComponent(name) {
|
|
|
33
34
|
// (which resolves to a nonexistent components/lib/dom.ts) and fails typecheck.
|
|
34
35
|
.replaceAll("'../lib/dom.ts'", "'#lib/utils/dom.ts'")
|
|
35
36
|
.replaceAll('"../lib/dom.ts"', '"#lib/utils/dom.ts"');
|
|
37
|
+
// Strip a Tier-1 helper's worked @example (same as create.js + `webjs ui add`)
|
|
38
|
+
// so switch / checkbox are lean, not just the full-stack base set (#983).
|
|
39
|
+
return leanComponentSource(rewritten, name);
|
|
36
40
|
}
|
|
37
41
|
|
|
38
42
|
/** Copy named registry components into `<appDir>/components/ui/`. */
|
package/package.json
CHANGED
|
@@ -44,6 +44,7 @@ Classify the task first, then load the smallest useful reference set. Each refer
|
|
|
44
44
|
| Tailwind, light-DOM tag-prefix rule, tokens, fixed headers, no-reflow layout | `references/styling.md` |
|
|
45
45
|
| Client router, prefetch, frames, view transitions, Suspense streaming | `references/client-router-and-streaming.md` |
|
|
46
46
|
| Optimistic UI for a user-facing mutation | `references/optimistic-ui.md` |
|
|
47
|
+
| The `@webjsdev/ui` component kit (a `components.json` is present): class helpers, tokens, `add` / `view`, the MCP `ui` tool | `references/ui-kit.md` |
|
|
47
48
|
| TypeScript at runtime, erasable syntax, full-stack types | `references/typescript.md` |
|
|
48
49
|
| Unit, browser, e2e tests, the `handle()` harness, Bun parity | `references/testing.md` |
|
|
49
50
|
| Auth, caching, env vars, rate limit, file storage, the `webjs` config block | `references/built-ins.md` |
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# The `@webjsdev/ui` component kit
|
|
2
|
+
|
|
3
|
+
Load this when the app has a `components.json` (it uses `@webjsdev/ui`, the
|
|
4
|
+
shadcn-style kit for WebJs). The source is copied into your repo (`components/ui/`),
|
|
5
|
+
so you own and edit it. Two tiers:
|
|
6
|
+
|
|
7
|
+
- **Tier 1, class helpers (23 components).** Pure functions returning Tailwind
|
|
8
|
+
class strings (`buttonClass({ variant })`, `cardClass()`), composed with
|
|
9
|
+
whatever native element you write. Reach for these instead of expanding
|
|
10
|
+
Tailwind by hand: the call site is a fraction of the tokens and the class list
|
|
11
|
+
cannot drift.
|
|
12
|
+
- **Tier 2, stateful custom elements (9 components).** `<ui-dialog>`, `<ui-tabs>`,
|
|
13
|
+
`<ui-dropdown-menu>`, and friends own their ARIA (focus trap, roving tabindex,
|
|
14
|
+
`aria-controls` / `inert`, live regions). Write the tag and the accessible
|
|
15
|
+
behaviour comes with it. Do NOT hand-roll these; the wiring is easy to get
|
|
16
|
+
subtly wrong.
|
|
17
|
+
|
|
18
|
+
## The workflow: query for the structure, do not guess it
|
|
19
|
+
|
|
20
|
+
`add` copies a Tier-1 component's class helpers plus a lean header (what each
|
|
21
|
+
helper is, the accessibility obligations) and a one-line pointer. It does NOT
|
|
22
|
+
copy the worked structural example, because that example is guidance you consume
|
|
23
|
+
once while composing, not code that should sit in your repo. Get the full
|
|
24
|
+
paste-ready structure on demand:
|
|
25
|
+
|
|
26
|
+
- **MCP `ui` tool** (preferred when available): call `ui` with no args for the
|
|
27
|
+
kit inventory (each component's tier, helper signatures, npm deps); pass
|
|
28
|
+
`{ name: "accordion" }` for one component's helper signatures, the paste-ready
|
|
29
|
+
structural example, the accessibility header, and deps.
|
|
30
|
+
- **CLI**: `webjs ui list` (inventory), `webjs ui view <name>` (the projected
|
|
31
|
+
view plus the full source). Same data as the MCP tool (one shared projector).
|
|
32
|
+
|
|
33
|
+
So the loop is: `add` the component, then query `ui <name>` (MCP) or
|
|
34
|
+
`webjs ui view <name>` for the accessible structure, paste it, and fill it in.
|
|
35
|
+
|
|
36
|
+
## Setup and resolution
|
|
37
|
+
|
|
38
|
+
- `webjs ui init` writes `components.json`, `lib/utils.ts`, and the CSS design
|
|
39
|
+
tokens the helpers render against (`--background`, `--foreground`,
|
|
40
|
+
`--destructive`, ...). It HARD-FAILS if the tokens cannot be written, so a
|
|
41
|
+
clean exit means the kit is styled. `add` self-heals the tokens if they go
|
|
42
|
+
missing.
|
|
43
|
+
- Resolution is LOCAL-FIRST: `init` / `add` / `list` / `view` read the registry
|
|
44
|
+
that ships inside the installed `@webjsdev/ui`, with no network. This pins you
|
|
45
|
+
to the installed version; run `webjs ui diff` to see where your local copies
|
|
46
|
+
drift from the upstream (that command alone compares against the live registry).
|
|
47
|
+
|
|
48
|
+
## Inventory (run `webjs ui list` or the MCP `ui` tool for the authoritative, current set)
|
|
49
|
+
|
|
50
|
+
**Tier 1 (class helpers):** accordion, alert, aspect-ratio, avatar, badge,
|
|
51
|
+
breadcrumb, button, card, checkbox, collapsible, input, kbd, label,
|
|
52
|
+
native-select, pagination, popover, progress, radio-group, separator, skeleton,
|
|
53
|
+
switch, table, textarea.
|
|
54
|
+
|
|
55
|
+
**Tier 2 (custom elements, own their ARIA):** alert-dialog, dialog,
|
|
56
|
+
dropdown-menu, hover-card, sonner, tabs, tooltip, plus toggle and toggle-group
|
|
57
|
+
(these two register an element AND export a `*Class` helper).
|
|
58
|
+
|
|
59
|
+
## Idioms
|
|
60
|
+
|
|
61
|
+
- A helper is a function, so compose it: `class=${buttonClass({ variant: 'outline' })}`.
|
|
62
|
+
The unquoted `${...}` is a normal `html` attribute hole.
|
|
63
|
+
- Tier-1 helpers assume the design tokens exist; if a component paints unstyled,
|
|
64
|
+
the tokens are missing (re-run `webjs ui init` or let `add` self-heal them).
|
|
65
|
+
- Custom elements are display-only-safe at SSR and hydrate in the browser, the
|
|
66
|
+
standard WebJs component model (`references/components.md`).
|
|
67
|
+
|
|
68
|
+
Full per-package reference lives in the installed `@webjsdev/ui/AGENTS.md`.
|