@metaobjectsdev/cli 1.0.0-rc.6 → 1.0.0-rc.8
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/dist/src/commands/upgrade.d.ts.map +1 -1
- package/dist/src/commands/upgrade.js +37 -13
- package/dist/src/commands/upgrade.js.map +1 -1
- package/dist/src/commands/verify.d.ts.map +1 -1
- package/dist/src/commands/verify.js +22 -2
- package/dist/src/commands/verify.js.map +1 -1
- package/dist/src/lib/advisory.d.ts +7 -0
- package/dist/src/lib/advisory.d.ts.map +1 -1
- package/dist/src/lib/advisory.js +14 -0
- package/dist/src/lib/advisory.js.map +1 -1
- package/dist/src/lib/anti-patterns.d.ts.map +1 -1
- package/dist/src/lib/anti-patterns.js +6 -0
- package/dist/src/lib/anti-patterns.js.map +1 -1
- package/dist/src/lib/authored-source.d.ts +22 -0
- package/dist/src/lib/authored-source.d.ts.map +1 -0
- package/dist/src/lib/authored-source.js +46 -0
- package/dist/src/lib/authored-source.js.map +1 -0
- package/dist/src/lib/base-url-advisory.d.ts.map +1 -1
- package/dist/src/lib/base-url-advisory.js +7 -66
- package/dist/src/lib/base-url-advisory.js.map +1 -1
- package/dist/src/lib/jsx-construct.d.ts +55 -0
- package/dist/src/lib/jsx-construct.d.ts.map +1 -0
- package/dist/src/lib/jsx-construct.js +148 -0
- package/dist/src/lib/jsx-construct.js.map +1 -0
- package/dist/src/lib/load-metaobjects-config.d.ts +13 -0
- package/dist/src/lib/load-metaobjects-config.d.ts.map +1 -1
- package/dist/src/lib/load-metaobjects-config.js +54 -5
- package/dist/src/lib/load-metaobjects-config.js.map +1 -1
- package/dist/src/lib/removed-prop-advisory.d.ts +31 -0
- package/dist/src/lib/removed-prop-advisory.d.ts.map +1 -0
- package/dist/src/lib/removed-prop-advisory.js +91 -0
- package/dist/src/lib/removed-prop-advisory.js.map +1 -0
- package/dist/src/lib/wrangler.d.ts +17 -2
- package/dist/src/lib/wrangler.d.ts.map +1 -1
- package/dist/src/lib/wrangler.js +51 -2
- package/dist/src/lib/wrangler.js.map +1 -1
- package/package.json +10 -10
- package/src/commands/upgrade.ts +37 -12
- package/src/commands/verify.ts +23 -2
- package/src/lib/advisory.ts +16 -0
- package/src/lib/anti-patterns.ts +5 -0
- package/src/lib/authored-source.ts +45 -0
- package/src/lib/base-url-advisory.ts +6 -60
- package/src/lib/jsx-construct.ts +124 -0
- package/src/lib/load-metaobjects-config.ts +53 -5
- package/src/lib/removed-prop-advisory.ts +113 -0
- package/src/lib/wrangler.ts +52 -4
|
@@ -6,8 +6,23 @@ export interface WranglerExecuteOptions {
|
|
|
6
6
|
}
|
|
7
7
|
export declare function buildWranglerExecuteArgs(opts: WranglerExecuteOptions): string[];
|
|
8
8
|
/**
|
|
9
|
-
*
|
|
10
|
-
*
|
|
9
|
+
* The reason to report when wrangler exits non-zero.
|
|
10
|
+
*
|
|
11
|
+
* **`--json` puts wrangler's error on STDOUT, and warnings on stderr.** This function
|
|
12
|
+
* exists because the runner used to report stderr alone, so a live D1 gate failing for
|
|
13
|
+
* want of a `CLOUDFLARE_API_TOKEN` told the adopter its cause was
|
|
14
|
+
* `▲ [WARNING] … "unsafe" fields are experimental` — a warning, named as the reason a
|
|
15
|
+
* schema gate failed, with the real cause discarded. Found by running an estate's own
|
|
16
|
+
* `verify:prod-schema` against production for the first time.
|
|
17
|
+
*
|
|
18
|
+
* Order: the structured error wrangler wrote, then stderr, then raw stdout, then the
|
|
19
|
+
* process's own message. Stderr is still consulted — a genuine wrangler failure that
|
|
20
|
+
* never reaches stdout must not be swallowed to fix the opposite mistake.
|
|
21
|
+
*/
|
|
22
|
+
export declare function wranglerFailureReason(stdout: string, stderr: string, fallback: string): string;
|
|
23
|
+
/**
|
|
24
|
+
* Run wrangler with the given args; return stdout. The failure REASON is chosen by
|
|
25
|
+
* `wranglerFailureReason` when wrangler exits non-zero. `cwd` is the directory wrangler
|
|
11
26
|
* runs in (defaults to process.cwd() — caller should pass the project root).
|
|
12
27
|
*/
|
|
13
28
|
export type WranglerRunner = (args: string[], cwd: string) => Promise<{
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"wrangler.d.ts","sourceRoot":"","sources":["../../../src/lib/wrangler.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,sBAAsB;IACrC,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,OAAO,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,GAAG,SAAS,CAAC;CAChC;AAED,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,sBAAsB,GAAG,MAAM,EAAE,CAW/E;AAED;;;;GAIG;AACH,MAAM,MAAM,cAAc,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC;AAE1G,eAAO,MAAM,qBAAqB,EAAE,cAYnC,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAIjE"}
|
|
1
|
+
{"version":3,"file":"wrangler.d.ts","sourceRoot":"","sources":["../../../src/lib/wrangler.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,sBAAsB;IACrC,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,OAAO,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,GAAG,SAAS,CAAC;CAChC;AAED,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,sBAAsB,GAAG,MAAM,EAAE,CAW/E;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,GACf,MAAM,CAQR;AAsBD;;;;GAIG;AACH,MAAM,MAAM,cAAc,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC;AAE1G,eAAO,MAAM,qBAAqB,EAAE,cAYnC,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAIjE"}
|
package/dist/src/lib/wrangler.js
CHANGED
|
@@ -13,6 +13,55 @@ export function buildWranglerExecuteArgs(opts) {
|
|
|
13
13
|
}
|
|
14
14
|
return args;
|
|
15
15
|
}
|
|
16
|
+
/**
|
|
17
|
+
* The reason to report when wrangler exits non-zero.
|
|
18
|
+
*
|
|
19
|
+
* **`--json` puts wrangler's error on STDOUT, and warnings on stderr.** This function
|
|
20
|
+
* exists because the runner used to report stderr alone, so a live D1 gate failing for
|
|
21
|
+
* want of a `CLOUDFLARE_API_TOKEN` told the adopter its cause was
|
|
22
|
+
* `▲ [WARNING] … "unsafe" fields are experimental` — a warning, named as the reason a
|
|
23
|
+
* schema gate failed, with the real cause discarded. Found by running an estate's own
|
|
24
|
+
* `verify:prod-schema` against production for the first time.
|
|
25
|
+
*
|
|
26
|
+
* Order: the structured error wrangler wrote, then stderr, then raw stdout, then the
|
|
27
|
+
* process's own message. Stderr is still consulted — a genuine wrangler failure that
|
|
28
|
+
* never reaches stdout must not be swallowed to fix the opposite mistake.
|
|
29
|
+
*/
|
|
30
|
+
export function wranglerFailureReason(stdout, stderr, fallback) {
|
|
31
|
+
const structured = structuredWranglerError(stdout);
|
|
32
|
+
if (structured !== undefined)
|
|
33
|
+
return structured;
|
|
34
|
+
const err = stderr.trim();
|
|
35
|
+
if (err.length > 0)
|
|
36
|
+
return err;
|
|
37
|
+
const out = stdout.trim();
|
|
38
|
+
if (out.length > 0)
|
|
39
|
+
return out;
|
|
40
|
+
return fallback;
|
|
41
|
+
}
|
|
42
|
+
/** wrangler's two `--json` error shapes: `{error:{text}}` / `{error}` and the
|
|
43
|
+
* `[{success:false,error}]` execute envelope. Anything else reads as absent. */
|
|
44
|
+
function structuredWranglerError(stdout) {
|
|
45
|
+
let parsed;
|
|
46
|
+
try {
|
|
47
|
+
parsed = JSON.parse(stdout);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
const node = Array.isArray(parsed) ? parsed[0] : parsed;
|
|
53
|
+
if (node === null || typeof node !== "object")
|
|
54
|
+
return undefined;
|
|
55
|
+
const { error } = node;
|
|
56
|
+
if (typeof error === "string" && error.trim().length > 0)
|
|
57
|
+
return error.trim();
|
|
58
|
+
if (error !== null && typeof error === "object") {
|
|
59
|
+
const { text } = error;
|
|
60
|
+
if (typeof text === "string" && text.trim().length > 0)
|
|
61
|
+
return text.trim();
|
|
62
|
+
}
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
16
65
|
export const defaultWranglerRunner = async (args, cwd) => {
|
|
17
66
|
try {
|
|
18
67
|
const { stdout, stderr } = await execFile("wrangler", args, { cwd, maxBuffer: 16 * 1024 * 1024 });
|
|
@@ -23,8 +72,8 @@ export const defaultWranglerRunner = async (args, cwd) => {
|
|
|
23
72
|
if (e.code === "ENOENT") {
|
|
24
73
|
throw new Error(`wrangler not found on PATH; install it: 'npm i -D wrangler'`);
|
|
25
74
|
}
|
|
26
|
-
const
|
|
27
|
-
throw new Error(`wrangler ${args.join(" ")} failed: ${
|
|
75
|
+
const reason = wranglerFailureReason(e.stdout ?? "", e.stderr ?? "", e.message);
|
|
76
|
+
throw new Error(`wrangler ${args.join(" ")} failed: ${reason}`);
|
|
28
77
|
}
|
|
29
78
|
};
|
|
30
79
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"wrangler.js","sourceRoot":"","sources":["../../../src/lib/wrangler.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,IAAI,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAC5D,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAEtC,MAAM,QAAQ,GAAG,SAAS,CAAC,UAAU,CAAC,CAAC;AASvC,MAAM,UAAU,wBAAwB,CAAC,IAA4B;IACnE,MAAM,IAAI,GAAa;QACrB,IAAI,EAAE,SAAS,EAAE,IAAI,CAAC,OAAO;QAC7B,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,SAAS;QACpC,QAAQ;QACR,WAAW,EAAE,IAAI,CAAC,OAAO;KAC1B,CAAC;IACF,IAAI,IAAI,CAAC,UAAU,KAAK,SAAS,EAAE,CAAC;QAClC,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC;IACzC,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AASD,MAAM,CAAC,MAAM,qBAAqB,GAAmB,KAAK,EAAE,IAAI,EAAE,GAAG,EAAE,EAAE;IACvE,IAAI,CAAC;QACH,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,QAAQ,CAAC,UAAU,EAAE,IAAI,EAAE,EAAE,GAAG,EAAE,SAAS,EAAE,EAAE,GAAG,IAAI,GAAG,IAAI,EAAE,CAAC,CAAC;QAClG,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;IAC5B,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,CAAC,GAAG,GAAmE,CAAC;QAC9E,IAAI,CAAC,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;YACxB,MAAM,IAAI,KAAK,CAAC,6DAA6D,CAAC,CAAC;QACjF,CAAC;QACD,MAAM,MAAM,GAAG,CAAC,CAAC,MAAM,IAAI,EAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"wrangler.js","sourceRoot":"","sources":["../../../src/lib/wrangler.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,IAAI,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAC5D,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAEtC,MAAM,QAAQ,GAAG,SAAS,CAAC,UAAU,CAAC,CAAC;AASvC,MAAM,UAAU,wBAAwB,CAAC,IAA4B;IACnE,MAAM,IAAI,GAAa;QACrB,IAAI,EAAE,SAAS,EAAE,IAAI,CAAC,OAAO;QAC7B,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,SAAS;QACpC,QAAQ;QACR,WAAW,EAAE,IAAI,CAAC,OAAO;KAC1B,CAAC;IACF,IAAI,IAAI,CAAC,UAAU,KAAK,SAAS,EAAE,CAAC;QAClC,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC;IACzC,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,qBAAqB,CACnC,MAAc,EACd,MAAc,EACd,QAAgB;IAEhB,MAAM,UAAU,GAAG,uBAAuB,CAAC,MAAM,CAAC,CAAC;IACnD,IAAI,UAAU,KAAK,SAAS;QAAE,OAAO,UAAU,CAAC;IAChD,MAAM,GAAG,GAAG,MAAM,CAAC,IAAI,EAAE,CAAC;IAC1B,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,GAAG,CAAC;IAC/B,MAAM,GAAG,GAAG,MAAM,CAAC,IAAI,EAAE,CAAC;IAC1B,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,GAAG,CAAC;IAC/B,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED;iFACiF;AACjF,SAAS,uBAAuB,CAAC,MAAc;IAC7C,IAAI,MAAe,CAAC;IACpB,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;IAC9B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,MAAM,IAAI,GAAG,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC;IACxD,IAAI,IAAI,KAAK,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ;QAAE,OAAO,SAAS,CAAC;IAChE,MAAM,EAAE,KAAK,EAAE,GAAG,IAA2B,CAAC;IAC9C,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,KAAK,CAAC,IAAI,EAAE,CAAC;IAC9E,IAAI,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAChD,MAAM,EAAE,IAAI,EAAE,GAAG,KAA2B,CAAC;QAC7C,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC;YAAE,OAAO,IAAI,CAAC,IAAI,EAAE,CAAC;IAC7E,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AASD,MAAM,CAAC,MAAM,qBAAqB,GAAmB,KAAK,EAAE,IAAI,EAAE,GAAG,EAAE,EAAE;IACvE,IAAI,CAAC;QACH,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,QAAQ,CAAC,UAAU,EAAE,IAAI,EAAE,EAAE,GAAG,EAAE,SAAS,EAAE,EAAE,GAAG,IAAI,GAAG,IAAI,EAAE,CAAC,CAAC;QAClG,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;IAC5B,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,CAAC,GAAG,GAAmE,CAAC;QAC9E,IAAI,CAAC,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;YACxB,MAAM,IAAI,KAAK,CAAC,6DAA6D,CAAC,CAAC;QACjF,CAAC;QACD,MAAM,MAAM,GAAG,qBAAqB,CAAC,CAAC,CAAC,MAAM,IAAI,EAAE,EAAE,CAAC,CAAC,MAAM,IAAI,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC;QAChF,MAAM,IAAI,KAAK,CAAC,YAAY,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,YAAY,MAAM,EAAE,CAAC,CAAC;IAClE,CAAC;AACH,CAAC,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,0BAA0B,CAAC,KAAa;IACtD,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,OAAO,CAAC;QAAE,OAAO,KAAK,CAAC;IAC7C,MAAM,IAAI,GAAG,KAAK,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;IAC7D,OAAO,uCAAuC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC5D,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@metaobjectsdev/cli",
|
|
3
|
-
"version": "1.0.0-rc.
|
|
3
|
+
"version": "1.0.0-rc.8",
|
|
4
4
|
"description": "CLI for MetaObjects: scaffold, codegen, migrate, and drift-detection commands.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/src/index.js",
|
|
@@ -51,15 +51,15 @@
|
|
|
51
51
|
"@biomejs/js-api": "^0.7.0",
|
|
52
52
|
"@biomejs/wasm-nodejs": "^1.9.4",
|
|
53
53
|
"@libsql/kysely-libsql": "^0.4.0",
|
|
54
|
-
"@metaobjectsdev/codegen-ts": "1.0.0-rc.
|
|
55
|
-
"@metaobjectsdev/codegen-ts-react": "1.0.0-rc.
|
|
56
|
-
"@metaobjectsdev/codegen-ts-tanstack": "1.0.0-rc.
|
|
57
|
-
"@metaobjectsdev/docs-site": "1.0.0-rc.
|
|
58
|
-
"@metaobjectsdev/metadata": "1.0.0-rc.
|
|
59
|
-
"@metaobjectsdev/migrate-ts": "1.0.0-rc.
|
|
60
|
-
"@metaobjectsdev/render": "1.0.0-rc.
|
|
61
|
-
"@metaobjectsdev/runtime-ts": "1.0.0-rc.
|
|
62
|
-
"@metaobjectsdev/sdk": "1.0.0-rc.
|
|
54
|
+
"@metaobjectsdev/codegen-ts": "1.0.0-rc.8",
|
|
55
|
+
"@metaobjectsdev/codegen-ts-react": "1.0.0-rc.8",
|
|
56
|
+
"@metaobjectsdev/codegen-ts-tanstack": "1.0.0-rc.8",
|
|
57
|
+
"@metaobjectsdev/docs-site": "1.0.0-rc.8",
|
|
58
|
+
"@metaobjectsdev/metadata": "1.0.0-rc.8",
|
|
59
|
+
"@metaobjectsdev/migrate-ts": "1.0.0-rc.8",
|
|
60
|
+
"@metaobjectsdev/render": "1.0.0-rc.8",
|
|
61
|
+
"@metaobjectsdev/runtime-ts": "1.0.0-rc.8",
|
|
62
|
+
"@metaobjectsdev/sdk": "1.0.0-rc.8",
|
|
63
63
|
"@toon-format/toon": "^2.3.0",
|
|
64
64
|
"jiti": "^2.4.0"
|
|
65
65
|
},
|
package/src/commands/upgrade.ts
CHANGED
|
@@ -126,6 +126,14 @@ export async function upgradeCommand(args: string[], cwd: string): Promise<numbe
|
|
|
126
126
|
...(flags.maxVersion !== undefined ? { maxVersion: flags.maxVersion } : {}),
|
|
127
127
|
};
|
|
128
128
|
|
|
129
|
+
// One retirement RULE, however many occurrences it has. An estate hit this with 24
|
|
130
|
+
// `@forge*` attributes, and the per-occurrence rationale turned the output into ~96
|
|
131
|
+
// lines saying one thing four times over — the wall that buries the instruction it is
|
|
132
|
+
// made of, which is the exact failure `runGen`'s no-manifest refusal aggregates to
|
|
133
|
+
// avoid. Keyed by the rendered text, so two rules that happen to read identically
|
|
134
|
+
// collapse and two that differ anywhere do not.
|
|
135
|
+
const rules = new Map<string, { why: string; suggestions: string[]; subjects: Set<string> }>();
|
|
136
|
+
|
|
129
137
|
for (const file of files) {
|
|
130
138
|
const rel = relative(projectRoot, file);
|
|
131
139
|
const before = await readFile(file, "utf8");
|
|
@@ -148,22 +156,24 @@ export async function upgradeCommand(args: string[], cwd: string): Promise<numbe
|
|
|
148
156
|
log.info(`\n${rel}`);
|
|
149
157
|
for (const c of r.changes) log.info(` ${c.line}: @${c.from} → ${c.to}`);
|
|
150
158
|
for (const f of r.refusals) {
|
|
151
|
-
// The
|
|
152
|
-
//
|
|
153
|
-
//
|
|
154
|
-
// unconditionally. For the `@forge*` entries that removed the only actionable
|
|
155
|
-
// sentence there is ("opt the provider in explicitly"), and for an authored
|
|
156
|
-
// `<type>.base` it announced a retirement of something that was never authorable —
|
|
157
|
-
// telling the reader they used to have a working feature. Hence "no longer accepted
|
|
158
|
-
// as of", which is true of both, with the carefully-written `why` beside it.
|
|
159
|
+
// The occurrence line is per-line information: file, line, subject, the value found.
|
|
160
|
+
// The RATIONALE that goes with it is per RULE and prints once, after the listing —
|
|
161
|
+
// see `rules` below.
|
|
159
162
|
log.warn(
|
|
160
163
|
` ${f.line}: ${f.subject}${f.value !== undefined ? `: ${f.value}` : ""} — needs a decision ` +
|
|
161
164
|
`(no longer accepted as of ${f.since}).`,
|
|
162
165
|
);
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
166
|
+
const key = `${f.why}\u0000${retirementSuggestions(f).join("\u0000")}`;
|
|
167
|
+
const rule = rules.get(key);
|
|
168
|
+
if (rule === undefined) {
|
|
169
|
+
rules.set(key, {
|
|
170
|
+
why: f.why,
|
|
171
|
+
suggestions: retirementSuggestions(f),
|
|
172
|
+
subjects: new Set([f.subject]),
|
|
173
|
+
});
|
|
174
|
+
} else {
|
|
175
|
+
rule.subjects.add(f.subject);
|
|
176
|
+
}
|
|
167
177
|
}
|
|
168
178
|
|
|
169
179
|
totalChanges += r.changes.length;
|
|
@@ -175,6 +185,21 @@ export async function upgradeCommand(args: string[], cwd: string): Promise<numbe
|
|
|
175
185
|
}
|
|
176
186
|
|
|
177
187
|
log.info("");
|
|
188
|
+
|
|
189
|
+
// The WHY and the options, once per rule. Both print, and the guide is not an
|
|
190
|
+
// alternative to them: this used to read `Retired in <since>. See <guide>`, dropping
|
|
191
|
+
// `why` whenever a guide existed — which is almost always — and dropping `replacedBy`
|
|
192
|
+
// unconditionally. For the `@forge*` entries that removed the only actionable sentence
|
|
193
|
+
// there is ("opt the provider in explicitly"). The subject list is what lets a reader
|
|
194
|
+
// connect the paragraph back to the lines above it now that it prints away from them.
|
|
195
|
+
for (const rule of rules.values()) {
|
|
196
|
+
const subjects = [...rule.subjects].sort().join(", ");
|
|
197
|
+
log.warn(`\n ${subjects} — why, and what to do:`);
|
|
198
|
+
log.warn(` ${rule.why}`);
|
|
199
|
+
for (const line of rule.suggestions) log.warn(` ${line}`);
|
|
200
|
+
}
|
|
201
|
+
if (rules.size > 0) log.info("");
|
|
202
|
+
|
|
178
203
|
if (notChecked.length > 0) {
|
|
179
204
|
log.warn(
|
|
180
205
|
`${notChecked.length} file(s) could not be parsed and were NOT checked — fix these ` +
|
package/src/commands/verify.ts
CHANGED
|
@@ -12,13 +12,14 @@ import { parseVerifyArgs, type MigrateFlags } from "../lib/args.js";
|
|
|
12
12
|
import { log } from "../lib/log.js";
|
|
13
13
|
import { emitStructured, type OutputFormat } from "../lib/format.js";
|
|
14
14
|
import {
|
|
15
|
-
antiPatternRows, missingBaseUrlRows, ranSection, skippedSection, warnCapped,
|
|
15
|
+
antiPatternRows, missingBaseUrlRows, removedPropRows, ranSection, skippedSection, warnCapped,
|
|
16
16
|
type AdvisoryDiagnosticRow, type AdvisoryFindingRow, type AdvisorySection,
|
|
17
17
|
} from "../lib/advisory.js";
|
|
18
18
|
import { warnIfAgentContextStale } from "../lib/agent-context-staleness.js";
|
|
19
19
|
import { warnIfManifestIgnored } from "../lib/manifest-ignored-check.js";
|
|
20
20
|
import { scanSourceForAntiPatterns } from "../lib/anti-patterns.js";
|
|
21
21
|
import { scanForMissingBaseUrl, type BaseUrlFinding } from "../lib/base-url-advisory.js";
|
|
22
|
+
import { scanForRemovedProps, type RemovedPropFinding } from "../lib/removed-prop-advisory.js";
|
|
22
23
|
import { replayRemedy } from "../lib/replay-remedy.js";
|
|
23
24
|
import { FileProvider } from "../lib/file-provider.js";
|
|
24
25
|
import { derivePayloadFieldTree } from "../lib/payload-field-tree.js";
|
|
@@ -735,8 +736,21 @@ export async function verifyCommand(
|
|
|
735
736
|
// Same discipline as above: an advisory scan never breaks verify. The anti-pattern
|
|
736
737
|
// half already ran, so its rows are still reported rather than lost to this catch.
|
|
737
738
|
}
|
|
739
|
+
// F99 — the other half of the same migration: the adopter who never renamed at all.
|
|
740
|
+
// Its own try/catch for the same reason, so one scanner throwing cannot take the
|
|
741
|
+
// other's rows with it.
|
|
742
|
+
let removedProps: RemovedPropFinding[] = [];
|
|
743
|
+
try {
|
|
744
|
+
removedProps = scanForRemovedProps(projectRoot);
|
|
745
|
+
} catch {
|
|
746
|
+
// advisory only — never breaks verify.
|
|
747
|
+
}
|
|
738
748
|
|
|
739
|
-
antiPatternSection = ranSection([
|
|
749
|
+
antiPatternSection = ranSection([
|
|
750
|
+
...antiPatternRows(findings),
|
|
751
|
+
...missingBaseUrlRows(baseUrl),
|
|
752
|
+
...removedPropRows(removedProps),
|
|
753
|
+
]);
|
|
740
754
|
if (findings.length > 0) {
|
|
741
755
|
log.warn(
|
|
742
756
|
`meta verify — ${findings.length} place(s) hand-roll what MetaObjects can model ` +
|
|
@@ -751,6 +765,13 @@ export async function verifyCommand(
|
|
|
751
765
|
);
|
|
752
766
|
warnCapped(baseUrl.map((f) => ` ${f.message}`), flags.limit, { structured });
|
|
753
767
|
}
|
|
768
|
+
if (removedProps.length > 0) {
|
|
769
|
+
log.warn(
|
|
770
|
+
`meta verify — ${removedProps.length} provider(s) still mounted with a prop 1.0 ` +
|
|
771
|
+
`renamed away (advisory — does not fail the build, but the runtime will):`,
|
|
772
|
+
);
|
|
773
|
+
warnCapped(removedProps.map((f) => ` ${f.message}`), flags.limit, { structured });
|
|
774
|
+
}
|
|
754
775
|
}
|
|
755
776
|
|
|
756
777
|
// -- template (prompt / output) drift --------------------------------------
|
package/src/lib/advisory.ts
CHANGED
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
import { log } from "./log.js";
|
|
24
24
|
import type { AntiPatternFinding } from "./anti-patterns.js";
|
|
25
25
|
import type { BaseUrlFinding } from "./base-url-advisory.js";
|
|
26
|
+
import type { RemovedPropFinding } from "./removed-prop-advisory.js";
|
|
26
27
|
|
|
27
28
|
/**
|
|
28
29
|
* How many advisory lines TEXT output prints before it truncates.
|
|
@@ -134,6 +135,21 @@ export function missingBaseUrlRows(findings: readonly BaseUrlFinding[]): Advisor
|
|
|
134
135
|
}));
|
|
135
136
|
}
|
|
136
137
|
|
|
138
|
+
/**
|
|
139
|
+
* F99 findings in that same row shape, for the same reason. `rule` carries the prop, so a
|
|
140
|
+
* consumer filtering the advisory list does not have to parse the message to learn WHICH
|
|
141
|
+
* rename fired — there will eventually be more than one.
|
|
142
|
+
*/
|
|
143
|
+
export function removedPropRows(findings: readonly RemovedPropFinding[]): AdvisoryFindingRow[] {
|
|
144
|
+
return findings.map((f) => ({
|
|
145
|
+
file: f.file,
|
|
146
|
+
line: f.line,
|
|
147
|
+
rule: `removed-prop:${f.prop}`,
|
|
148
|
+
construct: f.construct,
|
|
149
|
+
message: f.message,
|
|
150
|
+
}));
|
|
151
|
+
}
|
|
152
|
+
|
|
137
153
|
// ---------------------------------------------------------------------------
|
|
138
154
|
// text output
|
|
139
155
|
// ---------------------------------------------------------------------------
|
package/src/lib/anti-patterns.ts
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
19
19
|
import { join } from "node:path";
|
|
20
20
|
import { relPosix } from "./rel-posix.js";
|
|
21
|
+
import { looksBundled } from "./authored-source.js";
|
|
21
22
|
|
|
22
23
|
export interface AntiPatternFinding {
|
|
23
24
|
file: string; // path relative to the scan root (posix-ish, sep-normalized)
|
|
@@ -259,6 +260,10 @@ function walk(dir: string, root: string, ignore: readonly RegExp[], acc: AntiPat
|
|
|
259
260
|
}
|
|
260
261
|
// Skip MetaObjects-generated output — it legitimately contains AVG/CHECK etc.
|
|
261
262
|
if (text.slice(0, 600).includes(GENERATED_MARKER)) continue;
|
|
263
|
+
// …and skip a BUNDLE, which is nobody's source. The 512KB gate above is about cost, not
|
|
264
|
+
// authorship: a 300KB minified file passes it while being just as unfixable. Same rule
|
|
265
|
+
// the base-URL advisory applies, from the same module. See lib/authored-source.ts.
|
|
266
|
+
if (looksBundled(text)) continue;
|
|
262
267
|
const isSql = e.name.endsWith(".sql");
|
|
263
268
|
const lines = text.split("\n");
|
|
264
269
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Is this file text a HUMAN wrote, or a bundler's output?
|
|
3
|
+
*
|
|
4
|
+
* Both advisory scanners — the base-URL one and the anti-pattern one — exist to teach the
|
|
5
|
+
* author of a file something about the file. A bundle is not authored: nobody can act on a
|
|
6
|
+
* finding in it, the fix belongs in the source it was built from, and naming a generated
|
|
7
|
+
* artifact spends the scanners' whole false-positive budget in one line.
|
|
8
|
+
*
|
|
9
|
+
* The rule is a property of the ARTIFACT, not of a directory name. An estate proved why:
|
|
10
|
+
* its build output is `public/`, which no ignore list would have guessed (the lists cover
|
|
11
|
+
* `dist`, `build`, `.next`, `.output`, `out`), so `meta verify` reported
|
|
12
|
+
* `public/main.js:243 — <EntityFetcherProvider> is mounted with no baseUrl` while
|
|
13
|
+
* `client/src/main.tsx:26` — the source, the file a person edits — passes `baseUrl="/api"`
|
|
14
|
+
* correctly. Adding `public` to a list would have fixed that estate and nobody else.
|
|
15
|
+
*
|
|
16
|
+
* The two scanners keep their own walkers, deliberately (they diverge on eight axes and
|
|
17
|
+
* unifying them needs ~6 options for two callers). What they share is this judgment, so it
|
|
18
|
+
* lives here once rather than being decided twice.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Bundlers emit lines in the thousands-to-hundreds-of-thousands of characters; the estate
|
|
23
|
+
* bundle that produced the false positive has a longest line of 266,226. Authored source
|
|
24
|
+
* does not reach 5,000 — the threshold sits an order of magnitude above the longest line
|
|
25
|
+
* any human writes and an order of magnitude below what minification produces, so it is a
|
|
26
|
+
* wide gap rather than a tuned one.
|
|
27
|
+
*
|
|
28
|
+
* NOT `>= 512KB`, which `anti-patterns.ts` already applies for a different reason (cost):
|
|
29
|
+
* that gate is about how much work a scan does, and a 300KB bundle sails through it while
|
|
30
|
+
* being exactly as unauthored as a 700KB one.
|
|
31
|
+
*/
|
|
32
|
+
const BUNDLED_LINE_LENGTH = 5000;
|
|
33
|
+
|
|
34
|
+
/** True when `text` looks like build output rather than source someone edits. */
|
|
35
|
+
export function looksBundled(text: string): boolean {
|
|
36
|
+
// Scan for a long line without splitting the whole file: a 700KB bundle would otherwise
|
|
37
|
+
// allocate an array of it, and this runs per file on every `meta verify` / `meta gen`.
|
|
38
|
+
let lineStart = 0;
|
|
39
|
+
for (let i = 0; i < text.length; i++) {
|
|
40
|
+
if (text.charCodeAt(i) !== 10 /* \n */) continue;
|
|
41
|
+
if (i - lineStart >= BUNDLED_LINE_LENGTH) return true;
|
|
42
|
+
lineStart = i + 1;
|
|
43
|
+
}
|
|
44
|
+
return text.length - lineStart >= BUNDLED_LINE_LENGTH;
|
|
45
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { readdirSync, readFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { relPosix } from "./rel-posix.js";
|
|
4
|
+
import { looksBundled } from "./authored-source.js";
|
|
5
|
+
import { constructText } from "./jsx-construct.js";
|
|
4
6
|
|
|
5
7
|
/**
|
|
6
8
|
* F52 — a provider mounted with no `baseUrl` on a project whose `apiPrefix` is not empty.
|
|
@@ -51,66 +53,6 @@ const PROVIDERS: readonly { construct: string; open: RegExp; kind: "jsx" | "call
|
|
|
51
53
|
{ construct: "provideEntityFetcher()", open: /provideEntityFetcher\s*\(/g, kind: "call" },
|
|
52
54
|
];
|
|
53
55
|
|
|
54
|
-
/**
|
|
55
|
-
* The text of the opening construct that begins at `start`.
|
|
56
|
-
*
|
|
57
|
-
* JSX: up to the `>` that closes the opening TAG. Call this a one-file tokenizer rather
|
|
58
|
-
* than a bracket count, because a bracket count is what the first version was and it was
|
|
59
|
-
* wrong in both directions:
|
|
60
|
-
*
|
|
61
|
-
* <EntityFetcherProvider title="a > b" fetcher={f} baseUrl="/api">
|
|
62
|
-
* → the `>` inside the STRING ended the tag before `baseUrl`, warning about a
|
|
63
|
-
* correct provider.
|
|
64
|
-
* <EntityFetcherProvider fetcher={mk(")")}>
|
|
65
|
-
* → the unbalanced `)` inside a string meant depth never returned to 0, so the
|
|
66
|
-
* 4000-char cap swallowed an unrelated `baseUrl` later in the file and SUPPRESSED
|
|
67
|
-
* a real finding.
|
|
68
|
-
*
|
|
69
|
-
* So string literals (all three quote styles, with escapes), line comments and block
|
|
70
|
-
* comments are skipped rather than counted. A regex literal is deliberately NOT handled —
|
|
71
|
-
* telling `/` division from a regex needs real parsing, and a regex inside a JSX opening
|
|
72
|
-
* tag is vanishingly rare next to the cost of getting it wrong.
|
|
73
|
-
*
|
|
74
|
-
* Call: to the `)` balancing the argument list, so a nested object or arrow does not end
|
|
75
|
-
* it early.
|
|
76
|
-
*
|
|
77
|
-
* Both bail after a generous cap rather than scanning a whole minified file when a
|
|
78
|
-
* construct is unterminated.
|
|
79
|
-
*/
|
|
80
|
-
function constructText(src: string, start: number, kind: "jsx" | "call"): string {
|
|
81
|
-
const limit = Math.min(src.length, start + 4000);
|
|
82
|
-
let depth = 0;
|
|
83
|
-
for (let i = start; i < limit; i++) {
|
|
84
|
-
const c = src[i]!;
|
|
85
|
-
|
|
86
|
-
// --- skip what is not code -------------------------------------------------
|
|
87
|
-
if (c === '"' || c === "'" || c === "`") {
|
|
88
|
-
i++;
|
|
89
|
-
while (i < limit && src[i] !== c) { if (src[i] === "\\") i++; i++; }
|
|
90
|
-
continue;
|
|
91
|
-
}
|
|
92
|
-
if (c === "/" && src[i + 1] === "/") {
|
|
93
|
-
while (i < limit && src[i] !== "\n") i++;
|
|
94
|
-
continue;
|
|
95
|
-
}
|
|
96
|
-
if (c === "/" && src[i + 1] === "*") {
|
|
97
|
-
i += 2;
|
|
98
|
-
while (i < limit && !(src[i] === "*" && src[i + 1] === "/")) i++;
|
|
99
|
-
i++;
|
|
100
|
-
continue;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// --- code ------------------------------------------------------------------
|
|
104
|
-
if (kind === "jsx" && c === ">" && depth === 0) return src.slice(start, i + 1);
|
|
105
|
-
if (c === "{" || c === "(" || c === "[") depth++;
|
|
106
|
-
else if (c === "}" || c === ")" || c === "]") {
|
|
107
|
-
if (kind === "call" && depth === 1 && c === ")") return src.slice(start, i + 1);
|
|
108
|
-
depth--;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
return src.slice(start, limit);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
56
|
function scanFile(rel: string, src: string, apiPrefix: string): BaseUrlFinding[] {
|
|
115
57
|
const out: BaseUrlFinding[] = [];
|
|
116
58
|
for (const { construct, open, kind } of PROVIDERS) {
|
|
@@ -155,6 +97,10 @@ function walk(dir: string, root: string, acc: BaseUrlFinding[], apiPrefix: strin
|
|
|
155
97
|
let src: string;
|
|
156
98
|
try { src = readFileSync(abs, "utf8"); } catch { continue; }
|
|
157
99
|
if (!src.includes("EntityFetcher")) continue;
|
|
100
|
+
// A bundle is not authored source: the fix belongs in the file it was built FROM, and
|
|
101
|
+
// an estate whose output directory is `public/` proved that no ignore list guesses the
|
|
102
|
+
// name. See lib/authored-source.ts.
|
|
103
|
+
if (looksBundled(src)) continue;
|
|
158
104
|
acc.push(...scanFile(relPosix(root, abs), src, apiPrefix));
|
|
159
105
|
}
|
|
160
106
|
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The text of one opening JSX tag or call expression, and its top-level attribute names.
|
|
3
|
+
*
|
|
4
|
+
* Lifted out of `base-url-advisory.ts` when a second advisory (F99, the removed `value`
|
|
5
|
+
* prop) needed exactly the same tokenizer. This is a shared PRIMITIVE, not a shared walker
|
|
6
|
+
* — the two advisories keep their own directory walks deliberately, because they disagree
|
|
7
|
+
* about what to scan and have diverged on purpose.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The text of the construct that begins at `start`.
|
|
12
|
+
*
|
|
13
|
+
* JSX: up to the `>` that closes the opening TAG. Call this a one-file tokenizer rather
|
|
14
|
+
* than a bracket count, because a bracket count is what the first version was and it was
|
|
15
|
+
* wrong in both directions:
|
|
16
|
+
*
|
|
17
|
+
* <EntityFetcherProvider title="a > b" fetcher={f} baseUrl="/api">
|
|
18
|
+
* → the `>` inside the STRING ended the tag before `baseUrl`, warning about a
|
|
19
|
+
* correct provider.
|
|
20
|
+
* <EntityFetcherProvider fetcher={mk(")")}>
|
|
21
|
+
* → the unbalanced `)` inside a string meant depth never returned to 0, so the
|
|
22
|
+
* 4000-char cap swallowed an unrelated `baseUrl` later in the file and SUPPRESSED
|
|
23
|
+
* a real finding.
|
|
24
|
+
*
|
|
25
|
+
* So string literals (all three quote styles, with escapes), line comments and block
|
|
26
|
+
* comments are skipped rather than counted. A regex literal is deliberately NOT handled —
|
|
27
|
+
* telling `/` division from a regex needs real parsing, and a regex inside a JSX opening
|
|
28
|
+
* tag is vanishingly rare next to the cost of getting it wrong.
|
|
29
|
+
*
|
|
30
|
+
* Call: to the `)` balancing the argument list, so a nested object or arrow does not end
|
|
31
|
+
* it early.
|
|
32
|
+
*
|
|
33
|
+
* Both bail after a generous cap rather than scanning a whole minified file when a
|
|
34
|
+
* construct is unterminated.
|
|
35
|
+
*/
|
|
36
|
+
export function constructText(src: string, start: number, kind: "jsx" | "call"): string {
|
|
37
|
+
const limit = Math.min(src.length, start + 4000);
|
|
38
|
+
let depth = 0;
|
|
39
|
+
for (let i = start; i < limit; i++) {
|
|
40
|
+
const c = src[i]!;
|
|
41
|
+
|
|
42
|
+
// --- skip what is not code -------------------------------------------------
|
|
43
|
+
if (c === '"' || c === "'" || c === "`") {
|
|
44
|
+
i++;
|
|
45
|
+
while (i < limit && src[i] !== c) { if (src[i] === "\\") i++; i++; }
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (c === "/" && src[i + 1] === "/") {
|
|
49
|
+
while (i < limit && src[i] !== "\n") i++;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (c === "/" && src[i + 1] === "*") {
|
|
53
|
+
i += 2;
|
|
54
|
+
while (i < limit && !(src[i] === "*" && src[i + 1] === "/")) i++;
|
|
55
|
+
i++;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// --- code ------------------------------------------------------------------
|
|
60
|
+
if (kind === "jsx" && c === ">" && depth === 0) return src.slice(start, i + 1);
|
|
61
|
+
if (c === "{" || c === "(" || c === "[") depth++;
|
|
62
|
+
else if (c === "}" || c === ")" || c === "]") {
|
|
63
|
+
if (kind === "call" && depth === 1 && c === ")") return src.slice(start, i + 1);
|
|
64
|
+
depth--;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return src.slice(start, limit);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The attribute names written directly on an opening tag — depth 0 only.
|
|
72
|
+
*
|
|
73
|
+
* Depth is the whole point. `constructText` returns the tag INCLUDING every nested
|
|
74
|
+
* expression, so a substring test for `value=` convicts all of these, none of which pass
|
|
75
|
+
* `value` to the tag:
|
|
76
|
+
*
|
|
77
|
+
* <P fetcher={mk({ value: 1 })}> an object key
|
|
78
|
+
* <P fetcher={o.value === 1 ? a : b}> a comparison — note `=` is the first of `===`
|
|
79
|
+
* <P fetcher={<Inner value={9} />}> a DIFFERENT component's prop
|
|
80
|
+
*
|
|
81
|
+
* Strings and comments are skipped for the same reason they are in `constructText`, so a
|
|
82
|
+
* `title="pass value={x} here"` is prose, not an attribute.
|
|
83
|
+
*
|
|
84
|
+
* An identifier counts only when the next non-space character is a single `=` (not `=>`,
|
|
85
|
+
* not `==`). A valueless boolean attribute (`<P disabled>`) is therefore not reported —
|
|
86
|
+
* correct here, since every prop these advisories care about takes a value.
|
|
87
|
+
*/
|
|
88
|
+
export function topLevelAttrs(tag: string): Set<string> {
|
|
89
|
+
const out = new Set<string>();
|
|
90
|
+
let depth = 0;
|
|
91
|
+
// Start past the tag name so the component's own name is never read as an attribute.
|
|
92
|
+
let i = 0;
|
|
93
|
+
while (i < tag.length && !/\s/.test(tag[i]!)) i++;
|
|
94
|
+
|
|
95
|
+
for (; i < tag.length; i++) {
|
|
96
|
+
const c = tag[i]!;
|
|
97
|
+
if (c === '"' || c === "'" || c === "`") {
|
|
98
|
+
i++;
|
|
99
|
+
while (i < tag.length && tag[i] !== c) { if (tag[i] === "\\") i++; i++; }
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (c === "/" && tag[i + 1] === "/") { while (i < tag.length && tag[i] !== "\n") i++; continue; }
|
|
103
|
+
if (c === "/" && tag[i + 1] === "*") {
|
|
104
|
+
i += 2;
|
|
105
|
+
while (i < tag.length && !(tag[i] === "*" && tag[i + 1] === "/")) i++;
|
|
106
|
+
i++;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (c === "{" || c === "(" || c === "[") { depth++; continue; }
|
|
110
|
+
if (c === "}" || c === ")" || c === "]") { depth--; continue; }
|
|
111
|
+
if (depth !== 0) continue;
|
|
112
|
+
|
|
113
|
+
if (/[A-Za-z_$]/.test(c)) {
|
|
114
|
+
const start = i;
|
|
115
|
+
while (i < tag.length && /[A-Za-z0-9_$-]/.test(tag[i]!)) i++;
|
|
116
|
+
const name = tag.slice(start, i);
|
|
117
|
+
let j = i;
|
|
118
|
+
while (j < tag.length && /\s/.test(tag[j]!)) j++;
|
|
119
|
+
if (tag[j] === "=" && tag[j + 1] !== "=" && tag[j + 1] !== ">") out.add(name);
|
|
120
|
+
i--; // the for-loop's i++ re-reads the delimiter we stopped on
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return out;
|
|
124
|
+
}
|