@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.
Files changed (47) hide show
  1. package/dist/src/commands/upgrade.d.ts.map +1 -1
  2. package/dist/src/commands/upgrade.js +37 -13
  3. package/dist/src/commands/upgrade.js.map +1 -1
  4. package/dist/src/commands/verify.d.ts.map +1 -1
  5. package/dist/src/commands/verify.js +22 -2
  6. package/dist/src/commands/verify.js.map +1 -1
  7. package/dist/src/lib/advisory.d.ts +7 -0
  8. package/dist/src/lib/advisory.d.ts.map +1 -1
  9. package/dist/src/lib/advisory.js +14 -0
  10. package/dist/src/lib/advisory.js.map +1 -1
  11. package/dist/src/lib/anti-patterns.d.ts.map +1 -1
  12. package/dist/src/lib/anti-patterns.js +6 -0
  13. package/dist/src/lib/anti-patterns.js.map +1 -1
  14. package/dist/src/lib/authored-source.d.ts +22 -0
  15. package/dist/src/lib/authored-source.d.ts.map +1 -0
  16. package/dist/src/lib/authored-source.js +46 -0
  17. package/dist/src/lib/authored-source.js.map +1 -0
  18. package/dist/src/lib/base-url-advisory.d.ts.map +1 -1
  19. package/dist/src/lib/base-url-advisory.js +7 -66
  20. package/dist/src/lib/base-url-advisory.js.map +1 -1
  21. package/dist/src/lib/jsx-construct.d.ts +55 -0
  22. package/dist/src/lib/jsx-construct.d.ts.map +1 -0
  23. package/dist/src/lib/jsx-construct.js +148 -0
  24. package/dist/src/lib/jsx-construct.js.map +1 -0
  25. package/dist/src/lib/load-metaobjects-config.d.ts +13 -0
  26. package/dist/src/lib/load-metaobjects-config.d.ts.map +1 -1
  27. package/dist/src/lib/load-metaobjects-config.js +54 -5
  28. package/dist/src/lib/load-metaobjects-config.js.map +1 -1
  29. package/dist/src/lib/removed-prop-advisory.d.ts +31 -0
  30. package/dist/src/lib/removed-prop-advisory.d.ts.map +1 -0
  31. package/dist/src/lib/removed-prop-advisory.js +91 -0
  32. package/dist/src/lib/removed-prop-advisory.js.map +1 -0
  33. package/dist/src/lib/wrangler.d.ts +17 -2
  34. package/dist/src/lib/wrangler.d.ts.map +1 -1
  35. package/dist/src/lib/wrangler.js +51 -2
  36. package/dist/src/lib/wrangler.js.map +1 -1
  37. package/package.json +10 -10
  38. package/src/commands/upgrade.ts +37 -12
  39. package/src/commands/verify.ts +23 -2
  40. package/src/lib/advisory.ts +16 -0
  41. package/src/lib/anti-patterns.ts +5 -0
  42. package/src/lib/authored-source.ts +45 -0
  43. package/src/lib/base-url-advisory.ts +6 -60
  44. package/src/lib/jsx-construct.ts +124 -0
  45. package/src/lib/load-metaobjects-config.ts +53 -5
  46. package/src/lib/removed-prop-advisory.ts +113 -0
  47. 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
- * Run wrangler with the given args; return stdout. Stderr is included in the
10
- * error message when wrangler exits non-zero. `cwd` is the directory wrangler
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"}
@@ -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 stderr = e.stderr ?? "";
27
- throw new Error(`wrangler ${args.join(" ")} failed: ${stderr || e.message}`);
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;QAC9B,MAAM,IAAI,KAAK,CAAC,YAAY,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,YAAY,MAAM,IAAI,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC;IAC/E,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"}
1
+ {"version":3,"file":"wrangler.js","sourceRoot":"","sources":["../../../src/lib/wrangler.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@metaobjectsdev/cli",
3
- "version": "1.0.0-rc.6",
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.6",
55
- "@metaobjectsdev/codegen-ts-react": "1.0.0-rc.6",
56
- "@metaobjectsdev/codegen-ts-tanstack": "1.0.0-rc.6",
57
- "@metaobjectsdev/docs-site": "1.0.0-rc.6",
58
- "@metaobjectsdev/metadata": "1.0.0-rc.6",
59
- "@metaobjectsdev/migrate-ts": "1.0.0-rc.6",
60
- "@metaobjectsdev/render": "1.0.0-rc.6",
61
- "@metaobjectsdev/runtime-ts": "1.0.0-rc.6",
62
- "@metaobjectsdev/sdk": "1.0.0-rc.6",
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
  },
@@ -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 WHY and the replacement both print, and the guide is not an alternative to
152
- // them. This used to read `Retired in <since>. See <guide>` — dropping `why`
153
- // whenever a guide existed, which is almost always, and dropping `replacedBy`
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
- log.warn(` ${f.why}`);
164
- // The same suggestion list every OTHER door prints for a retirement, rather than a
165
- // second rendering of the same note that can drift from it.
166
- for (const line of retirementSuggestions(f)) log.warn(` ${line}`);
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 ` +
@@ -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([...antiPatternRows(findings), ...missingBaseUrlRows(baseUrl)]);
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 --------------------------------------
@@ -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
  // ---------------------------------------------------------------------------
@@ -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
+ }