@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
@@ -141,10 +141,14 @@ function resolveCliPkg(specifier: string): string {
141
141
  * the supported home of the generators with no ownable copy), so nothing upstream of here
142
142
  * can tell a correct import from a removed one.
143
143
  *
144
- * It reads the config SOURCE rather than catching the throw because the throw happens inside
145
- * a generator factory call with no reference back to the import that produced it, and only a
146
- * config declaring one of these in `generators: [...]` throws at all — a stale type-only
147
- * import would fail silently at typecheck-time instead.
144
+ * It reads the config SOURCE because that is the arm that can name the FILE and the exact
145
+ * import. `removedGeneratorRuntimeError` below is the second arm, and an adopter estate is
146
+ * why there are two: the import does not have to be in the config. This project's config
147
+ * imported an OWNED generator — `./codegen/generators/entityFileTyped`, the shape `meta
148
+ * eject` itself produces — and THAT file carried the stale
149
+ * `import { entityFile } from "@metaobjectsdev/codegen-ts/generators"`. The source scan saw
150
+ * a clean config and the adopter got exactly the bare TypeError this check exists to
151
+ * prevent: `(0 , _generators.entityFile) is not a function`, exit 2, no file, no remedy.
148
152
  */
149
153
  const REMOVED_GENERATOR_EXPORTS = ["entityFile", "queriesFile", "routesFile", "barrel"] as const;
150
154
  const GENERATORS_SUBPATH = "@metaobjectsdev/codegen-ts/generators";
@@ -180,6 +184,39 @@ export function removedGeneratorImportError(source: string): string | undefined
180
184
  );
181
185
  }
182
186
 
187
+ /**
188
+ * The same diagnostic, recovered from the THROW instead of the source — for the case the
189
+ * source scan structurally cannot see: a removed export imported by a module the config
190
+ * imports, rather than by the config itself.
191
+ *
192
+ * It cannot name the file (the TypeError carries no reference back to the import that
193
+ * produced it — that limitation is real, it is just not a reason to say nothing), so it
194
+ * names what it does know: the symbol, the removal, and the one-command remedy. Matching is
195
+ * on the SYMBOL, not the message shape, because the shape is the transpiler's: Bun says
196
+ * `(0 , _generators.entityFile) is not a function` and Node ESM says
197
+ * `entityFile is not a function`.
198
+ */
199
+ export function removedGeneratorRuntimeError(err: unknown): string | undefined {
200
+ const message = err instanceof Error ? err.message : String(err ?? "");
201
+ if (!/is not a function/.test(message)) return undefined;
202
+ const hit = REMOVED_GENERATOR_EXPORTS.find((n) => new RegExp(`\\b${n}\\b`).test(message));
203
+ if (hit === undefined) return undefined;
204
+ const template = hit.replace(/File$/, "");
205
+ return (
206
+ `${hit}() is not a function — 1.0 REMOVED it from "${GENERATORS_SUBPATH}" ` +
207
+ `(ADR-0034 scaffold-and-own): these generators are yours to own, so the package no ` +
208
+ `longer ships them as an import. Your metaobjects.config.ts does not import it, so ` +
209
+ `the import is in a module the config imports — most often an OWNED generator under ` +
210
+ `codegen/generators/, which is exactly what 'meta eject' writes.\n` +
211
+ ` Fix — copy it into your repo:\n meta eject ${template}\n` +
212
+ ` then change that module's import to: ` +
213
+ `import { ${hit} } from "./codegen/generators/${template}";\n` +
214
+ ` The subpath itself is fine — promptRender, outputParser, routesFileHono and the ` +
215
+ `rest still live there.\n` +
216
+ ` Guide: docs/features/migrations/0.x-to-1.0.md §11`
217
+ );
218
+ }
219
+
183
220
  function rewriteImportSpecifiers(source: string, aliasMap: Record<string, string>): string {
184
221
  let result = source;
185
222
  for (const [specifier, resolvedPath] of Object.entries(aliasMap)) {
@@ -461,7 +498,18 @@ export async function loadMetaobjectsConfig(projectRoot: string): Promise<Metaob
461
498
  const prepareStackTraceBefore = Error.prepareStackTrace;
462
499
  const stackTraceLimitBefore = Error.stackTraceLimit;
463
500
  try {
464
- const raw = (await jiti.import(loadPath)) as MetaobjectsGenConfig | { default: MetaobjectsGenConfig };
501
+ let raw: MetaobjectsGenConfig | { default: MetaobjectsGenConfig };
502
+ try {
503
+ raw = (await jiti.import(loadPath)) as MetaobjectsGenConfig | { default: MetaobjectsGenConfig };
504
+ } catch (err) {
505
+ // The second arm of the ADR-0034 diagnostic: a removed export imported by a module the
506
+ // config imports rather than by the config itself. Rethrown as the migration message,
507
+ // because what reaches the adopter otherwise is `(0 , _generators.entityFile) is not a
508
+ // function` — the exact unactionable failure the source scan above exists to prevent.
509
+ const runtime = removedGeneratorRuntimeError(err);
510
+ if (runtime !== undefined) throw new Error(runtime);
511
+ throw err;
512
+ }
465
513
  // jiti's interopDefault doesn't always unwrap the default export when accessed
466
514
  // across module boundaries — explicitly unwrap if present.
467
515
  const cfg = (raw && typeof raw === "object" && "default" in raw && raw.default
@@ -0,0 +1,113 @@
1
+ import { readdirSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { relPosix } from "./rel-posix.js";
4
+ import { looksBundled } from "./authored-source.js";
5
+ import { constructText, topLevelAttrs } from "./jsx-construct.js";
6
+
7
+ /**
8
+ * F99 — a provider still mounted with a prop 1.0 renamed away.
9
+ *
10
+ * `<EntityFetcherProvider value={f}>` does not typecheck, and both the migration note and
11
+ * the base-URL advisory beside this one rest a safety argument on that fact. The argument
12
+ * holds only where `tsc` is in the gate chain, and in two of nine adopter estates it was
13
+ * not: `vite build` transpiles without typechecking, and a provider with the wrong prop
14
+ * still RENDERS — its children mount, and nothing fails until a component that actually
15
+ * calls a generated hook runs, in a browser, at which point the fetcher is undefined.
16
+ *
17
+ * One estate lost a round to it, misreading a red render smoke test as the release
18
+ * candidate breaking. The second shipped the identical construct and carried it through a
19
+ * whole pass. Two of nine, same component, same prop — that is a class, and the rename
20
+ * was ours.
21
+ *
22
+ * Unlike the base-URL advisory this fires at ANY `apiPrefix`, because there is no
23
+ * configuration under which the old prop reaches the provider. It is still a WARNING and
24
+ * never touches an exit code: the scan cannot see a re-export or an aliased local
25
+ * component, so it must not be the thing that fails a build.
26
+ */
27
+ export interface RemovedPropFinding {
28
+ file: string;
29
+ line: number;
30
+ construct: string;
31
+ /** The prop that no longer exists. */
32
+ prop: string;
33
+ message: string;
34
+ }
35
+
36
+ const IGNORE_SEGMENTS = new Set([
37
+ "node_modules", ".git", "dist", "build", ".next", ".turbo", ".output",
38
+ "coverage", ".metaobjects", ".wrangler",
39
+ ]);
40
+ const SCAN_EXT = /\.(?:ts|tsx|js|jsx|mjs)$/;
41
+ const SKIP_FILE = /(?:\.d\.ts$|\.test\.|\.spec\.)/;
42
+
43
+ /**
44
+ * What was renamed, and to what.
45
+ *
46
+ * Deliberately a table of ONE. A rename earns a row here only when an adopter has actually
47
+ * shipped the old spelling past their own gates — the value is in naming the specific
48
+ * construct two estates got wrong, not in mirroring the changelog. `open` also gates the
49
+ * cheap `includes()` prefilter in the walk, so a new row needs its marker adding there.
50
+ */
51
+ const RENAMES: readonly {
52
+ construct: string;
53
+ open: RegExp;
54
+ from: string;
55
+ to: string;
56
+ since: string;
57
+ }[] = [
58
+ {
59
+ construct: "<EntityFetcherProvider>",
60
+ open: /<EntityFetcherProvider\b/g,
61
+ from: "value",
62
+ to: "fetcher",
63
+ since: "1.0",
64
+ },
65
+ ];
66
+
67
+ function scanFile(rel: string, src: string): RemovedPropFinding[] {
68
+ const out: RemovedPropFinding[] = [];
69
+ for (const { construct, open, from, to, since } of RENAMES) {
70
+ open.lastIndex = 0;
71
+ let m: RegExpExecArray | null;
72
+ while ((m = open.exec(src)) !== null) {
73
+ if (!topLevelAttrs(constructText(src, m.index, "jsx")).has(from)) continue;
74
+ const line = src.slice(0, m.index).split("\n").length;
75
+ out.push({
76
+ file: rel, line, construct, prop: from,
77
+ message:
78
+ `${rel}:${line} — ${construct} is mounted with \`${from}\`, which ${since} ` +
79
+ `renamed to \`${to}\`. The provider still renders, so nothing fails until a ` +
80
+ `component calls a generated hook and finds no fetcher. \`tsc --noEmit\` names ` +
81
+ `this exactly; a build that only transpiles does not. Rename ${from} → ${to} — ` +
82
+ `and check baseUrl while you are here, since it is optional and defaults to "".`,
83
+ });
84
+ }
85
+ }
86
+ return out;
87
+ }
88
+
89
+ function walk(dir: string, root: string, acc: RemovedPropFinding[]): void {
90
+ let entries;
91
+ try { entries = readdirSync(dir, { withFileTypes: true }); } catch { return; }
92
+ for (const e of entries) {
93
+ if (IGNORE_SEGMENTS.has(e.name)) continue;
94
+ const abs = join(dir, e.name);
95
+ // Symlinked directories are NOT followed — see base-url-advisory.ts for the 41-finding
96
+ // ELOOP this prevents.
97
+ if (e.isDirectory()) { walk(abs, root, acc); continue; }
98
+ if (!SCAN_EXT.test(e.name) || SKIP_FILE.test(e.name)) continue;
99
+ let src: string;
100
+ try { src = readFileSync(abs, "utf8"); } catch { continue; }
101
+ if (!src.includes("EntityFetcherProvider")) continue;
102
+ // A bundle is not authored source (F89) — the fix belongs in the file it was built from.
103
+ if (looksBundled(src)) continue;
104
+ acc.push(...scanFile(relPosix(root, abs), src));
105
+ }
106
+ }
107
+
108
+ /** Scan a project tree for providers still using a prop 1.0 renamed away. */
109
+ export function scanForRemovedProps(root: string): RemovedPropFinding[] {
110
+ const acc: RemovedPropFinding[] = [];
111
+ walk(root, root, acc);
112
+ return acc;
113
+ }
@@ -24,8 +24,56 @@ export function buildWranglerExecuteArgs(opts: WranglerExecuteOptions): string[]
24
24
  }
25
25
 
26
26
  /**
27
- * Run wrangler with the given args; return stdout. Stderr is included in the
28
- * error message when wrangler exits non-zero. `cwd` is the directory wrangler
27
+ * The reason to report when wrangler exits non-zero.
28
+ *
29
+ * **`--json` puts wrangler's error on STDOUT, and warnings on stderr.** This function
30
+ * exists because the runner used to report stderr alone, so a live D1 gate failing for
31
+ * want of a `CLOUDFLARE_API_TOKEN` told the adopter its cause was
32
+ * `▲ [WARNING] … "unsafe" fields are experimental` — a warning, named as the reason a
33
+ * schema gate failed, with the real cause discarded. Found by running an estate's own
34
+ * `verify:prod-schema` against production for the first time.
35
+ *
36
+ * Order: the structured error wrangler wrote, then stderr, then raw stdout, then the
37
+ * process's own message. Stderr is still consulted — a genuine wrangler failure that
38
+ * never reaches stdout must not be swallowed to fix the opposite mistake.
39
+ */
40
+ export function wranglerFailureReason(
41
+ stdout: string,
42
+ stderr: string,
43
+ fallback: string,
44
+ ): string {
45
+ const structured = structuredWranglerError(stdout);
46
+ if (structured !== undefined) return structured;
47
+ const err = stderr.trim();
48
+ if (err.length > 0) return err;
49
+ const out = stdout.trim();
50
+ if (out.length > 0) return out;
51
+ return fallback;
52
+ }
53
+
54
+ /** wrangler's two `--json` error shapes: `{error:{text}}` / `{error}` and the
55
+ * `[{success:false,error}]` execute envelope. Anything else reads as absent. */
56
+ function structuredWranglerError(stdout: string): string | undefined {
57
+ let parsed: unknown;
58
+ try {
59
+ parsed = JSON.parse(stdout);
60
+ } catch {
61
+ return undefined;
62
+ }
63
+ const node = Array.isArray(parsed) ? parsed[0] : parsed;
64
+ if (node === null || typeof node !== "object") return undefined;
65
+ const { error } = node as { error?: unknown };
66
+ if (typeof error === "string" && error.trim().length > 0) return error.trim();
67
+ if (error !== null && typeof error === "object") {
68
+ const { text } = error as { text?: unknown };
69
+ if (typeof text === "string" && text.trim().length > 0) return text.trim();
70
+ }
71
+ return undefined;
72
+ }
73
+
74
+ /**
75
+ * Run wrangler with the given args; return stdout. The failure REASON is chosen by
76
+ * `wranglerFailureReason` when wrangler exits non-zero. `cwd` is the directory wrangler
29
77
  * runs in (defaults to process.cwd() — caller should pass the project root).
30
78
  */
31
79
  export type WranglerRunner = (args: string[], cwd: string) => Promise<{ stdout: string; stderr: string }>;
@@ -39,8 +87,8 @@ export const defaultWranglerRunner: WranglerRunner = async (args, cwd) => {
39
87
  if (e.code === "ENOENT") {
40
88
  throw new Error(`wrangler not found on PATH; install it: 'npm i -D wrangler'`);
41
89
  }
42
- const stderr = e.stderr ?? "";
43
- throw new Error(`wrangler ${args.join(" ")} failed: ${stderr || e.message}`);
90
+ const reason = wranglerFailureReason(e.stdout ?? "", e.stderr ?? "", e.message);
91
+ throw new Error(`wrangler ${args.join(" ")} failed: ${reason}`);
44
92
  }
45
93
  };
46
94