@webjsdev/cli 0.10.37 → 0.10.39
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 +116 -3
- package/lib/clear-placeholders.js +98 -0
- package/lib/create.js +156 -131
- package/lib/db-hints.js +34 -0
- package/lib/design-bar.js +67 -0
- package/lib/doctor.js +122 -4
- package/lib/runtime-rewrite.js +4 -3
- package/lib/saas-template.js +45 -6
- package/package.json +1 -1
- package/templates/.agents/rules/workflow.md +33 -15
- package/templates/.claude/hooks/design-review-before-stop.sh +36 -0
- package/templates/.claude/hooks/route-skills.sh +35 -0
- package/templates/.claude/settings.json +14 -0
- package/templates/.claude/skills/webjs-design-review/SKILL.md +84 -0
- package/templates/.cursorrules +33 -15
- package/templates/.github/copilot-instructions.md +33 -15
- package/templates/AGENTS.md +41 -24
- package/templates/CONVENTIONS.md +60 -29
- package/templates/LAYOUT-REFERENCE.md +96 -0
- package/templates/public/tailwind-browser.js +0 -947
package/bin/webjs.js
CHANGED
|
@@ -3,6 +3,8 @@ import { resolve, join, dirname } from 'node:path';
|
|
|
3
3
|
import { spawn } from 'node:child_process';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
import { resolveBin } from '../lib/resolve-bin.js';
|
|
6
|
+
import { dbGenerateTtyHint } from '../lib/db-hints.js';
|
|
7
|
+
import { DESIGN_REMINDER, hasUiLayout } from '../lib/design-bar.js';
|
|
6
8
|
import { checkNodeInline, nodeInlineMessage } from '../lib/node-preflight.js';
|
|
7
9
|
import { loadAppEnv, resolvePort } from '../lib/port.js';
|
|
8
10
|
import { planDevSupervisor } from '../lib/dev-supervisor.js';
|
|
@@ -45,7 +47,7 @@ const USAGE = `webjs commands:
|
|
|
45
47
|
(--no-hot: run in-process, no hot-reload supervisor)
|
|
46
48
|
webjs start [--port 8080] Start production server (serves source directly, no build step)
|
|
47
49
|
webjs test [--server|--browser] Run server + browser tests
|
|
48
|
-
webjs check [--json]
|
|
50
|
+
webjs check [--json] [--clear-placeholders] Run correctness checks (--json emits structured violations; --clear-placeholders strips scaffold markers)
|
|
49
51
|
webjs mcp Start the read-only MCP server (routes / actions / components / check)
|
|
50
52
|
webjs doctor Verify project health (Node, tsconfig, env, vendor pins, importmap coherence, @webjsdev versions, git hook, page/layout elision)
|
|
51
53
|
webjs types Generate .webjs/routes.d.ts (typed Route union + per-route params)
|
|
@@ -124,6 +126,21 @@ async function main() {
|
|
|
124
126
|
const { assertNodeVersion } = await import('@webjsdev/server');
|
|
125
127
|
assertNodeVersion({ onFail: 'exit' });
|
|
126
128
|
}
|
|
129
|
+
// #954: `dev` / `start` need `@webjsdev/core` resolvable FROM the app dir. A
|
|
130
|
+
// fresh git worktree has no node_modules (git worktrees do not copy it), so
|
|
131
|
+
// the app's pages otherwise fail deep in SSR with a raw
|
|
132
|
+
// `ERR_MODULE_NOT_FOUND: Cannot find package '@webjsdev/core'`. Probe up front
|
|
133
|
+
// and surface the cause + remedy instead. No-op (a cheap resolve) when the
|
|
134
|
+
// framework resolves, so the happy-path boot is untouched.
|
|
135
|
+
if (cmd === 'dev' || cmd === 'start') {
|
|
136
|
+
const { checkFrameworkResolves } = await import('../lib/doctor.js');
|
|
137
|
+
const probe = checkFrameworkResolves(process.cwd());
|
|
138
|
+
if (probe.status !== 'pass') {
|
|
139
|
+
console.error(`[webjs] ${probe.message}`);
|
|
140
|
+
if (probe.fix) console.error(`[webjs] Fix: ${probe.fix}`);
|
|
141
|
+
process.exit(1);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
127
144
|
switch (cmd) {
|
|
128
145
|
case 'dev': {
|
|
129
146
|
// If we're already inside the reload child (node --watch or bun --hot),
|
|
@@ -240,8 +257,31 @@ async function main() {
|
|
|
240
257
|
);
|
|
241
258
|
process.exit(1);
|
|
242
259
|
}
|
|
243
|
-
|
|
244
|
-
|
|
260
|
+
// For `generate` off a non-TTY, capture stderr (teed straight through) so
|
|
261
|
+
// the rename-prompt dead-end can be detected: drizzle-kit reports it on
|
|
262
|
+
// stderr but exits 0, so the exit code is useless. Every other case keeps
|
|
263
|
+
// plain inherit, and an interactive terminal still answers the prompt.
|
|
264
|
+
const captureStderr = sub === 'generate' && !process.stdin.isTTY;
|
|
265
|
+
const child = spawn(process.execPath, [dkPath, ...kitArgs, ...args], {
|
|
266
|
+
stdio: captureStderr ? ['inherit', 'inherit', 'pipe'] : 'inherit',
|
|
267
|
+
cwd: process.cwd(),
|
|
268
|
+
});
|
|
269
|
+
let errText = '';
|
|
270
|
+
if (captureStderr && child.stderr) {
|
|
271
|
+
child.stderr.on('data', (chunk) => { errText += chunk; process.stderr.write(chunk); });
|
|
272
|
+
}
|
|
273
|
+
// Read the captured stderr on `close`, NOT `exit`: `exit` can fire before
|
|
274
|
+
// the stderr pipe has drained its final chunk, which would miss the prompt
|
|
275
|
+
// signature (and tee the raw error AFTER the hint). `close` fires once all
|
|
276
|
+
// stdio has flushed, and still carries the exit code.
|
|
277
|
+
child.on('close', (code) => {
|
|
278
|
+
// Surface the escape hatch when `generate` dead-ends on a rename prompt
|
|
279
|
+
// with no TTY, instead of leaving the raw drizzle-kit error as the last
|
|
280
|
+
// word. Interactive and successful runs print nothing extra.
|
|
281
|
+
const hint = dbGenerateTtyHint(sub, process.stdin.isTTY, errText);
|
|
282
|
+
if (hint) console.error(hint);
|
|
283
|
+
process.exit(code ?? 0);
|
|
284
|
+
});
|
|
245
285
|
break;
|
|
246
286
|
}
|
|
247
287
|
case 'ui': {
|
|
@@ -372,6 +412,29 @@ async function main() {
|
|
|
372
412
|
case 'check': {
|
|
373
413
|
const { checkConventions, RULES } = await import('@webjsdev/server/check');
|
|
374
414
|
|
|
415
|
+
// --clear-placeholders: acknowledge the whole scaffold gallery in one
|
|
416
|
+
// command (strip the marker comment lines, keep the demo code), instead of
|
|
417
|
+
// one hand-edit per file. The gate then reflects only real violations.
|
|
418
|
+
if (rest.includes('--clear-placeholders')) {
|
|
419
|
+
const { clearPlaceholders } = await import('../lib/clear-placeholders.js');
|
|
420
|
+
const report = clearPlaceholders(process.cwd());
|
|
421
|
+
const total = report.reduce((n, r) => n + r.markers, 0);
|
|
422
|
+
if (report.length === 0) {
|
|
423
|
+
console.log('webjs check: no scaffold-placeholder markers found (nothing to clear).');
|
|
424
|
+
} else {
|
|
425
|
+
console.log(`webjs check: cleared ${total} scaffold-placeholder marker(s) across ${report.length} file(s):`);
|
|
426
|
+
for (const r of report) console.log(` ${r.file}`);
|
|
427
|
+
console.log('\nThe demo code is kept. Delete any gallery route/module you do not want, then re-run `webjs check`.');
|
|
428
|
+
// Re-surface the design bar the cleared markers carried. The
|
|
429
|
+
// layout/home marker was the just-in-time "adapt this chrome" reminder;
|
|
430
|
+
// stripping it silently is how an app ends up shipping the scaffold
|
|
431
|
+
// shell. Print the bar so clearing the markers cannot quietly drop it,
|
|
432
|
+
// but only for a UI app (the api template has no layout / no chrome).
|
|
433
|
+
if (hasUiLayout(process.cwd())) console.log(DESIGN_REMINDER);
|
|
434
|
+
}
|
|
435
|
+
break;
|
|
436
|
+
}
|
|
437
|
+
|
|
375
438
|
if (rest.includes('--rules')) {
|
|
376
439
|
console.log('webjs check, correctness rules:');
|
|
377
440
|
console.log(' Every rule catches code that is wrong to ship: a crash, a');
|
|
@@ -406,12 +469,28 @@ async function main() {
|
|
|
406
469
|
console.log('webjs check: all checks pass ✓');
|
|
407
470
|
} else {
|
|
408
471
|
console.log(`webjs check: ${violations.length} violation(s) found\n`);
|
|
472
|
+
// A fresh scaffold trips no-scaffold-placeholder on every unadapted demo
|
|
473
|
+
// file at once. Printing an identical block per file drowns the real
|
|
474
|
+
// feature violations, so collapse the sentinel to ONE grouped summary
|
|
475
|
+
// (with a one-command clear) and print every other rule per-violation.
|
|
476
|
+
// The returned violations and --json are unchanged; only this human
|
|
477
|
+
// printout groups, so agents/tools still see one entry per file.
|
|
478
|
+
const PLACEHOLDER = 'no-scaffold-placeholder';
|
|
479
|
+
const placeholders = violations.filter((v) => v.rule === PLACEHOLDER);
|
|
409
480
|
for (const v of violations) {
|
|
481
|
+
if (v.rule === PLACEHOLDER) continue;
|
|
410
482
|
console.log(` ✗ [${v.rule}] ${v.file}`);
|
|
411
483
|
console.log(` ${v.message}`);
|
|
412
484
|
if (v.fix) console.log(` Fix: ${v.fix}`);
|
|
413
485
|
console.log();
|
|
414
486
|
}
|
|
487
|
+
if (placeholders.length > 0) {
|
|
488
|
+
console.log(` ✗ [${PLACEHOLDER}] ${placeholders.length} file(s) still carry scaffold example content:`);
|
|
489
|
+
for (const v of placeholders) console.log(` ${v.file}`);
|
|
490
|
+
console.log(' Fix: adapt or delete each file, then remove its marker comment line.');
|
|
491
|
+
console.log(' Keeping the gallery? Run `webjs check --clear-placeholders` to clear all markers at once.');
|
|
492
|
+
console.log();
|
|
493
|
+
}
|
|
415
494
|
process.exit(1);
|
|
416
495
|
}
|
|
417
496
|
break;
|
|
@@ -582,6 +661,30 @@ Full docs: https://docs.webjs.dev`);
|
|
|
582
661
|
(download ? ' (downloading bundles)' : '') + '...',
|
|
583
662
|
);
|
|
584
663
|
const result = await pinAll(appDir, { download, from: explicitFrom });
|
|
664
|
+
if (result.droppedUnresolvable && !result.pins?.length) {
|
|
665
|
+
// The scan FOUND bare specifiers but every one was dropped because
|
|
666
|
+
// it is not installed under node_modules, so no local version could
|
|
667
|
+
// be read. Name them and point at the remedy instead of the
|
|
668
|
+
// misleading "no bare imports found" message (#953).
|
|
669
|
+
const list = result.droppedUnresolvable;
|
|
670
|
+
// Root package name from a bare specifier: `@scope/pkg/sub` -> `@scope/pkg`,
|
|
671
|
+
// `pkg/sub` -> `pkg`. This is the thing the user must `npm install`.
|
|
672
|
+
const rootPkg = (s) => {
|
|
673
|
+
const parts = s.split('/');
|
|
674
|
+
return s.startsWith('@') ? parts.slice(0, 2).join('/') : parts[0];
|
|
675
|
+
};
|
|
676
|
+
console.error(
|
|
677
|
+
`Pin: found ${list.length} bare-specifier import${list.length === 1 ? '' : 's'} in ` +
|
|
678
|
+
`client code under ${appDir}, but could not resolve a version for ` +
|
|
679
|
+
`${list.length === 1 ? 'it' : 'them'} (not installed under node_modules):`,
|
|
680
|
+
);
|
|
681
|
+
for (const s of list) console.error(` ${s}`);
|
|
682
|
+
console.error(
|
|
683
|
+
`Install the package first (e.g. \`npm install ${rootPkg(list[0])}\`), then rerun ` +
|
|
684
|
+
`\`webjs vendor pin\`. No pin file written.`,
|
|
685
|
+
);
|
|
686
|
+
process.exit(1);
|
|
687
|
+
}
|
|
585
688
|
if (result.noBareImports) {
|
|
586
689
|
// Scanner found zero bare-specifier imports in client-
|
|
587
690
|
// reachable source. Without this branch pinAll would write
|
|
@@ -627,6 +730,16 @@ Full docs: https://docs.webjs.dev`);
|
|
|
627
730
|
(downloaded ? ` + ${downloaded} bundle${downloaded === 1 ? '' : 's'}` : '') + '.';
|
|
628
731
|
const pruneMsg = pruned.length ? ` Pruned ${pruned.length} orphan${pruned.length === 1 ? '' : 's'}.` : '';
|
|
629
732
|
console.log(pinMsg + pruneMsg);
|
|
733
|
+
if (result.droppedUnresolvable?.length) {
|
|
734
|
+
// A partial pin: some specifiers resolved, others were dropped for a
|
|
735
|
+
// missing local version. Name the skipped ones so it is not silent.
|
|
736
|
+
console.warn(
|
|
737
|
+
`[webjs] Skipped ${result.droppedUnresolvable.length} import` +
|
|
738
|
+
`${result.droppedUnresolvable.length === 1 ? '' : 's'} with no installed version ` +
|
|
739
|
+
`(install then rerun to pin ${result.droppedUnresolvable.length === 1 ? 'it' : 'them'}):`,
|
|
740
|
+
);
|
|
741
|
+
for (const s of result.droppedUnresolvable) console.warn(` ${s}`);
|
|
742
|
+
}
|
|
630
743
|
|
|
631
744
|
// Make the pins committable. Vendoring is opt-in, so the pins the
|
|
632
745
|
// user just wrote are meant for source control; a `.gitignore`
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// `webjs check --clear-placeholders`: acknowledge the scaffold gallery in one
|
|
2
|
+
// command instead of hand-editing every demo file. A fresh scaffold trips
|
|
3
|
+
// `no-scaffold-placeholder` on every unadapted file at once, and the rule's
|
|
4
|
+
// sanctioned "deliberately keep it, then delete the marker line" path otherwise
|
|
5
|
+
// means one manual edit per file. This strips the marker comment lines so the
|
|
6
|
+
// gate goes green while the demo CODE is kept verbatim (it does NOT prune the
|
|
7
|
+
// gallery; deleting a demo you do not want stays a deliberate `rm`).
|
|
8
|
+
import { readFileSync, writeFileSync, readdirSync, statSync } from 'node:fs';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
|
|
11
|
+
// Assembled so THIS source does not itself carry the contiguous literal (the
|
|
12
|
+
// check scans raw source, and this file ships in the published CLI).
|
|
13
|
+
export const MARKER = 'webjs-scaffold-' + 'placeholder';
|
|
14
|
+
|
|
15
|
+
const SKIP_DIRS = new Set(['node_modules', '.git', '.webjs', 'graphify-out', 'dist']);
|
|
16
|
+
|
|
17
|
+
// Every scaffold marker's text ends with this closing clause (wrapped across
|
|
18
|
+
// lines or not), so it precisely bounds the marker sentence. See create.js /
|
|
19
|
+
// api-gallery.js where the markers are emitted.
|
|
20
|
+
const TERMINATOR = 'the marker remains';
|
|
21
|
+
|
|
22
|
+
// Strip comment punctuation and collapse whitespace so a marker wrapped across
|
|
23
|
+
// several `//` or `<!-- -->` lines can be matched as one normalized sentence.
|
|
24
|
+
const norm = (line) => line.replace(/<!--|-->|\/\//g, ' ').replace(/\s+/g, ' ').trim().toLowerCase();
|
|
25
|
+
const isLineComment = (line) => /^\s*\/\//.test(line);
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Pure: drop exactly the marker COMMENT that carries the token, not just the
|
|
29
|
+
* token line and NOT the adjacent educational comment/code. The marker is not
|
|
30
|
+
* always one line (the layout footer is a multi-line `<!-- ... -->`, and the
|
|
31
|
+
* global-* markers wrap across `//` lines), and it is often immediately followed
|
|
32
|
+
* by a SEPARATE comment paragraph, so a greedy "remove the whole comment run"
|
|
33
|
+
* over-removes. The marker sentence always ends in TERMINATOR, so extend from
|
|
34
|
+
* the token line only to the line that completes that clause (or, if the clause
|
|
35
|
+
* is somehow absent, remove just the token line rather than over-reaching).
|
|
36
|
+
* @param {string} content
|
|
37
|
+
* @param {string} [marker]
|
|
38
|
+
* @returns {{ content: string, removed: number, markers: number }}
|
|
39
|
+
*/
|
|
40
|
+
export function stripPlaceholderMarkers(content, marker = MARKER) {
|
|
41
|
+
const lines = content.split('\n');
|
|
42
|
+
const kept = [];
|
|
43
|
+
let removed = 0;
|
|
44
|
+
let markers = 0;
|
|
45
|
+
for (let i = 0; i < lines.length; i++) {
|
|
46
|
+
const line = lines[i];
|
|
47
|
+
if (!line.includes(marker)) { kept.push(line); continue; }
|
|
48
|
+
markers += 1;
|
|
49
|
+
let acc = norm(line);
|
|
50
|
+
let end = i;
|
|
51
|
+
if (!acc.includes(TERMINATOR)) {
|
|
52
|
+
// Extend over the marker's own comment (contiguous `//`, or up to the HTML
|
|
53
|
+
// `-->`) until the closing clause completes. Stop at the comment boundary
|
|
54
|
+
// if the clause never appears, so an edited marker under-removes, never
|
|
55
|
+
// eats the following paragraph or code.
|
|
56
|
+
const html = line.includes('<!--');
|
|
57
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
58
|
+
if (!html && !isLineComment(lines[j])) break;
|
|
59
|
+
acc += ' ' + norm(lines[j]);
|
|
60
|
+
end = j;
|
|
61
|
+
if (acc.includes(TERMINATOR) || (html && lines[j].includes('-->'))) break;
|
|
62
|
+
}
|
|
63
|
+
if (!acc.includes(TERMINATOR) && !html) end = i; // clause absent: token line only
|
|
64
|
+
}
|
|
65
|
+
removed += end - i + 1;
|
|
66
|
+
i = end;
|
|
67
|
+
}
|
|
68
|
+
return { content: kept.join('\n'), removed, markers };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Walk an app root, strip the marker comments in place, and return a per-file
|
|
73
|
+
* report of how many markers were cleared (and how many lines that removed).
|
|
74
|
+
* Only `.ts`/`.js`/`.mts`/`.mjs` files that actually carry the marker are
|
|
75
|
+
* rewritten.
|
|
76
|
+
* @param {string} root
|
|
77
|
+
* @param {{ marker?: string, write?: (path: string, content: string) => void }} [opts]
|
|
78
|
+
* @returns {Array<{ file: string, markers: number, removed: number }>}
|
|
79
|
+
*/
|
|
80
|
+
export function clearPlaceholders(root, opts = {}) {
|
|
81
|
+
const marker = opts.marker ?? MARKER;
|
|
82
|
+
const write = opts.write ?? ((p, c) => writeFileSync(p, c));
|
|
83
|
+
const report = [];
|
|
84
|
+
(function walk(dir) {
|
|
85
|
+
for (const name of readdirSync(dir)) {
|
|
86
|
+
if (SKIP_DIRS.has(name)) continue;
|
|
87
|
+
const full = join(dir, name);
|
|
88
|
+
if (statSync(full).isDirectory()) { walk(full); continue; }
|
|
89
|
+
if (!/\.m?[jt]s$/.test(name)) continue;
|
|
90
|
+
const src = readFileSync(full, 'utf8');
|
|
91
|
+
if (!src.includes(marker)) continue;
|
|
92
|
+
const { content, removed, markers } = stripPlaceholderMarkers(src, marker);
|
|
93
|
+
write(full, content);
|
|
94
|
+
report.push({ file: full, markers, removed });
|
|
95
|
+
}
|
|
96
|
+
})(root);
|
|
97
|
+
return report;
|
|
98
|
+
}
|