@webjsdev/cli 0.10.55 → 0.10.57

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.
Files changed (42) hide show
  1. package/lib/create.js +43 -1
  2. package/lib/doctor/codes.js +67 -0
  3. package/lib/doctor/manifest.js +161 -0
  4. package/lib/doctor/policy.js +124 -0
  5. package/lib/doctor/probes/elision.js +111 -0
  6. package/lib/doctor/probes/env.js +53 -0
  7. package/lib/doctor/probes/framework-resolves.js +260 -0
  8. package/lib/doctor/probes/git-hook.js +58 -0
  9. package/lib/doctor/probes/importmap-coherence.js +158 -0
  10. package/lib/doctor/probes/node.js +37 -0
  11. package/lib/doctor/probes/static-asset-freshness.js +58 -0
  12. package/lib/doctor/probes/tsconfig.js +55 -0
  13. package/lib/doctor/probes/unmarked-asset-links.js +199 -0
  14. package/lib/doctor/probes/vendor-gitignore.js +84 -0
  15. package/lib/doctor/probes/vendor-pin.js +77 -0
  16. package/lib/doctor/probes/webjs-versions.js +85 -0
  17. package/lib/doctor/route-modules.js +100 -0
  18. package/lib/doctor/runner.js +72 -0
  19. package/lib/doctor/util.js +160 -0
  20. package/lib/doctor.js +5 -1634
  21. package/package.json +1 -1
  22. package/templates/.agents/rules/workflow.md +4 -3
  23. package/templates/.agents/skills/webjs/SKILL.md +46 -4
  24. package/templates/.agents/skills/webjs/references/built-ins.md +3 -1
  25. package/templates/.agents/skills/webjs/references/client-router-and-streaming.md +52 -5
  26. package/templates/.agents/skills/webjs/references/components.md +50 -2
  27. package/templates/.agents/skills/webjs/references/module-structure.md +229 -0
  28. package/templates/.agents/skills/webjs/references/muscle-memory-gotchas.md +27 -3
  29. package/templates/.agents/skills/webjs/references/optimistic-ui.md +23 -11
  30. package/templates/.agents/skills/webjs/references/routing-and-pages.md +36 -2
  31. package/templates/.agents/skills/webjs/references/runtime.md +6 -1
  32. package/templates/.agents/skills/webjs/references/styling.md +47 -2
  33. package/templates/.github/pull_request_template.md +0 -4
  34. package/templates/gallery/app/features/boundaries/page.ts +11 -0
  35. package/templates/gallery/app/features/client-router/page.ts +13 -2
  36. package/templates/gallery/app/features/metadata/page.ts +7 -1
  37. package/templates/gallery/modules/client-router/components/router-controls.ts +23 -2
  38. package/templates/gallery/modules/gallery/nav.ts +35 -26
  39. package/templates/gallery/modules/stream/components/browser/stream-demo.test.js +64 -0
  40. package/templates/gallery/modules/stream/components/stream-demo.ts +14 -9
  41. package/templates/gallery/modules/stream/utils/ui/row.ts +41 -0
  42. package/templates/gallery/test/rate-limit/rate-limit.test.ts +2 -1
@@ -0,0 +1,160 @@
1
+ import { statSync, readdirSync } from 'node:fs';
2
+ import { readFile } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+
5
+ /**
6
+ * Read the CLI package's own `engines.node` so the required Node major lives in
7
+ * one place (mirrors how `bin/webjs.js` sources it). Falls back to `>=24.0.0`.
8
+ * @param {string} cliDir directory of THIS file's package (lib/ -> package root)
9
+ * @returns {Promise<string>}
10
+ */
11
+ export async function readEngines(cliDir) {
12
+ try {
13
+ const pkg = JSON.parse(await readFile(join(cliDir, '..', 'package.json'), 'utf8'));
14
+ return pkg?.engines?.node || '>=24.0.0';
15
+ } catch {
16
+ return '>=24.0.0';
17
+ }
18
+ }
19
+
20
+ /**
21
+ * Strip `//` line comments, block comments, and trailing commas from a JSONC
22
+ * string so a tsconfig (which permits all three) parses with `JSON.parse`.
23
+ * Deliberately simple: it does not honor comment-looking sequences inside
24
+ * string values, which is acceptable for a tsconfig (paths rarely contain `//`
25
+ * or block-comment markers, and the worst case is a parse failure the caller
26
+ * already degrades to a WARN).
27
+ * @param {string} text
28
+ * @returns {string}
29
+ */
30
+ export function stripJsonc(text) {
31
+ let out = '';
32
+ let inString = false;
33
+ let stringQuote = '';
34
+ for (let i = 0; i < text.length; i++) {
35
+ const ch = text[i];
36
+ const next = text[i + 1];
37
+ if (inString) {
38
+ out += ch;
39
+ if (ch === '\\') {
40
+ // Copy the escaped char verbatim so an escaped quote does not end the string.
41
+ out += text[i + 1] || '';
42
+ i++;
43
+ } else if (ch === stringQuote) {
44
+ inString = false;
45
+ }
46
+ continue;
47
+ }
48
+ if (ch === '"' || ch === "'") {
49
+ inString = true;
50
+ stringQuote = ch;
51
+ out += ch;
52
+ continue;
53
+ }
54
+ if (ch === '/' && next === '/') {
55
+ while (i < text.length && text[i] !== '\n') i++;
56
+ out += '\n';
57
+ continue;
58
+ }
59
+ if (ch === '/' && next === '*') {
60
+ i += 2;
61
+ while (i < text.length && !(text[i] === '*' && text[i + 1] === '/')) i++;
62
+ i++; // land on the '/'
63
+ continue;
64
+ }
65
+ out += ch;
66
+ }
67
+ // Drop trailing commas before } or ].
68
+ return out.replace(/,(\s*[}\]])/g, '$1');
69
+ }
70
+
71
+ /**
72
+ * Parse a `.env`-style file into the SET of KEY names it declares. A simple
73
+ * `KEY=value` line parse: comments (`#`) and blank lines are skipped, and only
74
+ * the key before the first `=` is taken (the value is irrelevant for drift).
75
+ * @param {string} text
76
+ * @returns {Set<string>}
77
+ */
78
+ export function parseEnvKeys(text) {
79
+ const keys = new Set();
80
+ for (const raw of text.split(/\r?\n/)) {
81
+ const line = raw.trim();
82
+ if (!line || line.startsWith('#')) continue;
83
+ const eq = line.indexOf('=');
84
+ if (eq <= 0) continue;
85
+ let key = line.slice(0, eq).trim();
86
+ // Tolerate a leading `export ` (a common .env.example convention).
87
+ if (key.startsWith('export ')) key = key.slice('export '.length).trim();
88
+ if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) keys.add(key);
89
+ }
90
+ return keys;
91
+ }
92
+
93
+ // Directories never worth walking for the CSS-freshness advisory (mirrors
94
+ // dev-regenerate's IGNORE_DIRS): build output, deps, VCS + framework caches.
95
+ const FRESHNESS_IGNORE = new Set(['node_modules', '.git', '.webjs', 'dist', '.next', 'coverage']);
96
+
97
+ /**
98
+ * Newest mtime (ms) of any FILE under a path (a file's own, or the max over the
99
+ * files in a directory tree, skipping dependencies / dotfiles). Directory-node
100
+ * mtimes are NOT counted, matching dev-regenerate's walker: a content edit only
101
+ * shows through the file mtime, and a directory mtime is a flaky moving target.
102
+ * A missing path is 0. Best-effort: never throws.
103
+ * @param {string} abs
104
+ * @returns {number}
105
+ */
106
+ export function newestMtimeMs(abs) {
107
+ let st;
108
+ try { st = statSync(abs); } catch { return 0; }
109
+ if (!st.isDirectory()) return st.mtimeMs;
110
+ let newest = 0;
111
+ let entries;
112
+ try { entries = readdirSync(abs, { withFileTypes: true }); } catch { return newest; }
113
+ for (const e of entries) {
114
+ if (e.name.startsWith('.') || FRESHNESS_IGNORE.has(e.name)) continue;
115
+ // Skip symlinks: following one can cycle into unbounded recursion (a stack
116
+ // overflow here) or escape into node_modules. Same tradeoff as the server
117
+ // walker in dev-regenerate.js.
118
+ if (e.isSymbolicLink()) continue;
119
+ const m = newestMtimeMs(join(abs, e.name));
120
+ if (m > newest) newest = m;
121
+ }
122
+ return newest;
123
+ }
124
+
125
+ /**
126
+ * Whether the `<link>` tag at `idx` is commented out, so dead markup is never
127
+ * reported as a live finding.
128
+ *
129
+ * A DELIMITED comment is decided by an unclosed opener behind the tag. Neither
130
+ * `<!--` nor `/*` nests, so "nearest opener beats nearest closer" is exact, and
131
+ * it covers a multi-line block whose interior lines carry no marker of their
132
+ * own (what an editor's toggle-block-comment writes). A `//` has no closer, so
133
+ * it is decided from the tag's own line: a `//` inside an href later in the
134
+ * line cannot match, because the line does not START with it.
135
+ *
136
+ * Do NOT replace this with a lexer. Two attempts did, and both shipped bugs a
137
+ * stateless test cannot have: a line-blanking regex killed any line holding a
138
+ * protocol-relative url, and a quote-tracking walk inverted string/code
139
+ * polarity on a nested ``html`...` `` inside a `${}` hole (one quote char
140
+ * cannot model nesting), so an unbalanced apostrophe in template text
141
+ * desynchronized the rest of the file. This check does not need to lex
142
+ * JavaScript. If it ever genuinely does, export `redactStringsAndTemplates`
143
+ * from `@webjsdev/server` (`src/js-scan.js`, fuzz-tested differentially against
144
+ * a real TypeScript parse) rather than growing a third one here.
145
+ *
146
+ * Residual gap: a tag behind a `//` that trails real code on the same line
147
+ * stays reported. Rare, and it fails toward reporting rather than toward the
148
+ * silent inertness both lexers produced.
149
+ *
150
+ * @param {string} src
151
+ * @param {number} idx index of the tag's `<`
152
+ * @returns {boolean}
153
+ */
154
+ export function isCommentedOut(src, idx) {
155
+ const before = src.slice(0, idx);
156
+ if (before.lastIndexOf('<!--') > before.lastIndexOf('-->')) return true;
157
+ if (before.lastIndexOf('/*') > before.lastIndexOf('*/')) return true;
158
+ const lineStart = before.lastIndexOf('\n') + 1;
159
+ return before.slice(lineStart).trimStart().startsWith('//');
160
+ }