@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.
- package/lib/create.js +43 -1
- package/lib/doctor/codes.js +67 -0
- package/lib/doctor/manifest.js +161 -0
- package/lib/doctor/policy.js +124 -0
- package/lib/doctor/probes/elision.js +111 -0
- package/lib/doctor/probes/env.js +53 -0
- package/lib/doctor/probes/framework-resolves.js +260 -0
- package/lib/doctor/probes/git-hook.js +58 -0
- package/lib/doctor/probes/importmap-coherence.js +158 -0
- package/lib/doctor/probes/node.js +37 -0
- package/lib/doctor/probes/static-asset-freshness.js +58 -0
- package/lib/doctor/probes/tsconfig.js +55 -0
- package/lib/doctor/probes/unmarked-asset-links.js +199 -0
- package/lib/doctor/probes/vendor-gitignore.js +84 -0
- package/lib/doctor/probes/vendor-pin.js +77 -0
- package/lib/doctor/probes/webjs-versions.js +85 -0
- package/lib/doctor/route-modules.js +100 -0
- package/lib/doctor/runner.js +72 -0
- package/lib/doctor/util.js +160 -0
- package/lib/doctor.js +5 -1634
- package/package.json +1 -1
- package/templates/.agents/rules/workflow.md +4 -3
- package/templates/.agents/skills/webjs/SKILL.md +46 -4
- package/templates/.agents/skills/webjs/references/built-ins.md +3 -1
- package/templates/.agents/skills/webjs/references/client-router-and-streaming.md +52 -5
- package/templates/.agents/skills/webjs/references/components.md +50 -2
- package/templates/.agents/skills/webjs/references/module-structure.md +229 -0
- package/templates/.agents/skills/webjs/references/muscle-memory-gotchas.md +27 -3
- package/templates/.agents/skills/webjs/references/optimistic-ui.md +23 -11
- package/templates/.agents/skills/webjs/references/routing-and-pages.md +36 -2
- package/templates/.agents/skills/webjs/references/runtime.md +6 -1
- package/templates/.agents/skills/webjs/references/styling.md +47 -2
- package/templates/.github/pull_request_template.md +0 -4
- package/templates/gallery/app/features/boundaries/page.ts +11 -0
- package/templates/gallery/app/features/client-router/page.ts +13 -2
- package/templates/gallery/app/features/metadata/page.ts +7 -1
- package/templates/gallery/modules/client-router/components/router-controls.ts +23 -2
- package/templates/gallery/modules/gallery/nav.ts +35 -26
- package/templates/gallery/modules/stream/components/browser/stream-demo.test.js +64 -0
- package/templates/gallery/modules/stream/components/stream-demo.ts +14 -9
- package/templates/gallery/modules/stream/utils/ui/row.ts +41 -0
- 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
|
+
}
|