@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 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] Run correctness checks on the app (--json emits structured violations)
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
- const child = spawn(process.execPath, [dkPath, ...kitArgs, ...args], { stdio: 'inherit', cwd: process.cwd() });
244
- child.on('exit', (code) => process.exit(code ?? 0));
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
+ }