@metaobjectsdev/cli 1.0.0-rc.5 → 1.0.0-rc.7
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/docs.d.ts.map +1 -1
- package/dist/src/commands/docs.js +41 -9
- package/dist/src/commands/docs.js.map +1 -1
- package/dist/src/commands/gen.d.ts +5 -0
- package/dist/src/commands/gen.d.ts.map +1 -1
- package/dist/src/commands/gen.js +12 -1
- package/dist/src/commands/gen.js.map +1 -1
- package/dist/src/commands/init.d.ts.map +1 -1
- package/dist/src/commands/init.js +27 -3
- package/dist/src/commands/init.js.map +1 -1
- package/dist/src/commands/migrate.d.ts +5 -0
- package/dist/src/commands/migrate.d.ts.map +1 -1
- package/dist/src/commands/migrate.js +10 -4
- package/dist/src/commands/migrate.js.map +1 -1
- package/dist/src/commands/prompt-snapshot.d.ts.map +1 -1
- package/dist/src/commands/prompt-snapshot.js +2 -1
- package/dist/src/commands/prompt-snapshot.js.map +1 -1
- package/dist/src/commands/types.d.ts.map +1 -1
- package/dist/src/commands/types.js +12 -3
- package/dist/src/commands/types.js.map +1 -1
- package/dist/src/commands/upgrade.d.ts.map +1 -1
- package/dist/src/commands/upgrade.js +62 -4
- 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 +69 -21
- package/dist/src/commands/verify.js.map +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +37 -18
- package/dist/src/index.js.map +1 -1
- package/dist/src/lib/advisory.d.ts +6 -0
- package/dist/src/lib/advisory.d.ts.map +1 -1
- package/dist/src/lib/advisory.js +13 -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 +8 -4
- package/dist/src/lib/anti-patterns.js.map +1 -1
- package/dist/src/lib/args.d.ts +23 -12
- package/dist/src/lib/args.d.ts.map +1 -1
- package/dist/src/lib/args.js +33 -22
- package/dist/src/lib/args.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 +37 -0
- package/dist/src/lib/base-url-advisory.d.ts.map +1 -0
- package/dist/src/lib/base-url-advisory.js +168 -0
- package/dist/src/lib/base-url-advisory.js.map +1 -0
- package/dist/src/lib/codegen-drift.d.ts +8 -0
- package/dist/src/lib/codegen-drift.d.ts.map +1 -1
- package/dist/src/lib/codegen-drift.js +43 -10
- package/dist/src/lib/codegen-drift.js.map +1 -1
- package/dist/src/lib/detect-stack.d.ts +17 -0
- package/dist/src/lib/detect-stack.d.ts.map +1 -1
- package/dist/src/lib/detect-stack.js +64 -2
- package/dist/src/lib/detect-stack.js.map +1 -1
- package/dist/src/lib/docs-drift.d.ts.map +1 -1
- package/dist/src/lib/docs-drift.js +16 -7
- package/dist/src/lib/docs-drift.js.map +1 -1
- package/dist/src/lib/git-ignore.d.ts +19 -0
- package/dist/src/lib/git-ignore.d.ts.map +1 -1
- package/dist/src/lib/git-ignore.js +46 -3
- package/dist/src/lib/git-ignore.js.map +1 -1
- package/dist/src/lib/ignored-scaffold-check.d.ts +12 -0
- package/dist/src/lib/ignored-scaffold-check.d.ts.map +1 -0
- package/dist/src/lib/ignored-scaffold-check.js +75 -0
- package/dist/src/lib/ignored-scaffold-check.js.map +1 -0
- package/dist/src/lib/load-error.d.ts +46 -0
- package/dist/src/lib/load-error.d.ts.map +1 -0
- package/dist/src/lib/load-error.js +51 -0
- package/dist/src/lib/load-error.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/manifest-ignored-check.d.ts.map +1 -1
- package/dist/src/lib/manifest-ignored-check.js +19 -6
- package/dist/src/lib/manifest-ignored-check.js.map +1 -1
- package/dist/src/lib/output.d.ts +13 -1
- package/dist/src/lib/output.d.ts.map +1 -1
- package/dist/src/lib/output.js +26 -8
- package/dist/src/lib/output.js.map +1 -1
- package/dist/src/lib/rel-posix.d.ts +5 -0
- package/dist/src/lib/rel-posix.d.ts.map +1 -0
- package/dist/src/lib/rel-posix.js +23 -0
- package/dist/src/lib/rel-posix.js.map +1 -0
- package/dist/src/lib/replay-remedy.d.ts +2 -0
- package/dist/src/lib/replay-remedy.d.ts.map +1 -0
- package/dist/src/lib/replay-remedy.js +69 -0
- package/dist/src/lib/replay-remedy.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/docs.ts +51 -12
- package/src/commands/gen.ts +14 -1
- package/src/commands/init.ts +27 -3
- package/src/commands/migrate.ts +12 -4
- package/src/commands/prompt-snapshot.ts +2 -1
- package/src/commands/types.ts +12 -3
- package/src/commands/upgrade.ts +69 -4
- package/src/commands/verify.ts +74 -23
- package/src/index.ts +37 -18
- package/src/lib/advisory.ts +15 -0
- package/src/lib/anti-patterns.ts +7 -5
- package/src/lib/args.ts +60 -39
- package/src/lib/authored-source.ts +45 -0
- package/src/lib/base-url-advisory.ts +178 -0
- package/src/lib/codegen-drift.ts +52 -9
- package/src/lib/detect-stack.ts +73 -2
- package/src/lib/docs-drift.ts +17 -7
- package/src/lib/git-ignore.ts +47 -3
- package/src/lib/ignored-scaffold-check.ts +78 -0
- package/src/lib/load-error.ts +89 -0
- package/src/lib/load-metaobjects-config.ts +53 -5
- package/src/lib/manifest-ignored-check.ts +21 -6
- package/src/lib/output.ts +36 -8
- package/src/lib/rel-posix.ts +25 -0
- package/src/lib/replay-remedy.ts +78 -0
- 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
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
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
|
-
|
|
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
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
import { existsSync } from "node:fs";
|
|
19
19
|
import { join } from "node:path";
|
|
20
20
|
import { log } from "./log.js";
|
|
21
|
-
import { isGitIgnored } from "./git-ignore.js";
|
|
21
|
+
import { gitIgnoreSource, isGitIgnored } from "./git-ignore.js";
|
|
22
22
|
|
|
23
23
|
/** Project-relative path of the manifest — the one artifact of `.gen-state/` that is
|
|
24
24
|
* meant to be committed. */
|
|
@@ -40,13 +40,28 @@ export function warnIfManifestIgnored(cwd: string): void {
|
|
|
40
40
|
// per-user ignore file answers it just as truly as a committed .gitignore does.
|
|
41
41
|
if (isGitIgnored(cwd, HASH_MANIFEST_REL, { honourGlobalExcludes: true }) !== true) return;
|
|
42
42
|
|
|
43
|
+
// WHERE the rule actually lives, asked of git rather than assumed. This used to name
|
|
44
|
+
// `.metaobjects/.gitignore` and the literal pattern `.gen-state/`, which is only one of
|
|
45
|
+
// the places the exclusion can sit: an adopter estate had it in the ROOT `.gitignore` as
|
|
46
|
+
// `.metaobjects/.gen-state/`, so following this advisory to the letter edited a file that
|
|
47
|
+
// did not hold the rule, changed nothing, and printed this same line again — the loop
|
|
48
|
+
// shape. `git check-ignore -v` knows the answer, so nothing is guessed. When git cannot
|
|
49
|
+
// say, the wording degrades to the generic form rather than asserting a location.
|
|
50
|
+
const src = gitIgnoreSource(cwd, HASH_MANIFEST_REL, { honourGlobalExcludes: true });
|
|
51
|
+
const where =
|
|
52
|
+
src !== undefined
|
|
53
|
+
? `The rule is '${src.pattern}' at ${src.file}:${src.line} — replace it there with ` +
|
|
54
|
+
`a '/*' form plus a '!' negation for the manifest ` +
|
|
55
|
+
`(e.g. '${src.pattern.replace(/\/$/, "")}/*' and '!${src.pattern.replace(/\/$/, "")}/.hashes.json')`
|
|
56
|
+
: `Find the rule with 'git check-ignore -v ${HASH_MANIFEST_REL}' and replace it with ` +
|
|
57
|
+
`a '/*' form plus a '!' negation for the manifest`;
|
|
58
|
+
|
|
43
59
|
log.warn(
|
|
44
60
|
`${HASH_MANIFEST_REL} is git-ignored, so it never reaches another machine — ` +
|
|
45
61
|
`on a fresh clone or CI runner 'meta gen' cannot tell your hand edits from its ` +
|
|
46
|
-
`own stale output, and will refuse to overwrite rather than guess.
|
|
47
|
-
|
|
48
|
-
`
|
|
49
|
-
`
|
|
50
|
-
`commit the manifest. If you deliberately do not commit generated output, ignore this.`,
|
|
62
|
+
`own stale output, and will refuse to overwrite rather than guess. ${where} ` +
|
|
63
|
+
`(the glob matters — git will not descend into an excluded directory, so a negation ` +
|
|
64
|
+
`INSIDE an excluded directory can never apply), then commit the manifest. ` +
|
|
65
|
+
`If you deliberately do not commit generated output, ignore this.`,
|
|
51
66
|
);
|
|
52
67
|
}
|
package/src/lib/output.ts
CHANGED
|
@@ -31,6 +31,10 @@ export type GenFileStatus =
|
|
|
31
31
|
| "conflict"
|
|
32
32
|
| "unchanged"
|
|
33
33
|
| "refused"
|
|
34
|
+
/** `--baseline=adopt`: the file's existing content became the merge base and
|
|
35
|
+
* nothing was written. Never folded into "unchanged" — the run's product is the
|
|
36
|
+
* manifest, and a summary that showed these as unchanged would hide it. */
|
|
37
|
+
| "adopted"
|
|
34
38
|
| "removed";
|
|
35
39
|
|
|
36
40
|
export interface GenFileEntry {
|
|
@@ -57,6 +61,14 @@ export interface GenResultShape {
|
|
|
57
61
|
* structured payload carries every finding.
|
|
58
62
|
*/
|
|
59
63
|
antiPatterns?: AdvisorySection<AdvisoryFindingRow>;
|
|
64
|
+
/**
|
|
65
|
+
* How many generators the config wired. Required, because the alternative is a
|
|
66
|
+
* message that GUESSES: with no generators wired, a run over a project with five
|
|
67
|
+
* entities emitted nothing and reported "no entities to generate … author entities
|
|
68
|
+
* in this project's metadata sources", sending the reader to edit a file that was
|
|
69
|
+
* already correct. The output's own first line (`gen: []`) already knew.
|
|
70
|
+
*/
|
|
71
|
+
generatorCount: number;
|
|
60
72
|
}
|
|
61
73
|
|
|
62
74
|
const GEN_GLYPHS: Record<GenFileStatus, string> = {
|
|
@@ -66,6 +78,7 @@ const GEN_GLYPHS: Record<GenFileStatus, string> = {
|
|
|
66
78
|
conflict: "✗",
|
|
67
79
|
unchanged: "=",
|
|
68
80
|
refused: "⚠",
|
|
81
|
+
adopted: "⊕",
|
|
69
82
|
removed: "−",
|
|
70
83
|
};
|
|
71
84
|
|
|
@@ -76,6 +89,7 @@ const GEN_WORDS: Record<GenFileStatus, string> = {
|
|
|
76
89
|
conflict: "CONFLICT",
|
|
77
90
|
unchanged: "UNCHANGED",
|
|
78
91
|
refused: "REFUSED",
|
|
92
|
+
adopted: "ADOPTED",
|
|
79
93
|
removed: "REMOVED",
|
|
80
94
|
};
|
|
81
95
|
|
|
@@ -106,7 +120,7 @@ export function formatGenResult(result: GenResultShape, opts: FormatOptions): st
|
|
|
106
120
|
acc[f.status] = (acc[f.status] ?? 0) + 1;
|
|
107
121
|
return acc;
|
|
108
122
|
},
|
|
109
|
-
{ new: 0, overwrite: 0, merged: 0, conflict: 0, unchanged: 0, refused: 0, removed: 0 },
|
|
123
|
+
{ new: 0, overwrite: 0, merged: 0, conflict: 0, unchanged: 0, refused: 0, adopted: 0, removed: 0 },
|
|
110
124
|
);
|
|
111
125
|
const parts: string[] = [];
|
|
112
126
|
// "written" stays the sum of new + overwrite: that number was always correct and
|
|
@@ -118,6 +132,10 @@ export function formatGenResult(result: GenResultShape, opts: FormatOptions): st
|
|
|
118
132
|
if (counts.conflict > 0) parts.push(`${counts.conflict} conflict`);
|
|
119
133
|
if (counts.unchanged > 0) parts.push(`${counts.unchanged} unchanged`);
|
|
120
134
|
if (counts.refused > 0) parts.push(`${counts.refused} refused`);
|
|
135
|
+
// `--baseline=adopt`: recorded as the merge base, nothing written. Counted separately
|
|
136
|
+
// from `unchanged` because the run's product is the manifest, and an adopter who cannot
|
|
137
|
+
// see how many files it now covers has no way to review what they are about to commit.
|
|
138
|
+
if (counts.adopted > 0) parts.push(`${counts.adopted} adopted`);
|
|
121
139
|
if (counts.removed > 0) parts.push(`${counts.removed} removed`);
|
|
122
140
|
lines.push("", ` ${parts.join(", ")}`, "");
|
|
123
141
|
|
|
@@ -220,7 +238,7 @@ export function genResultToData(result: GenResultShape): {
|
|
|
220
238
|
} {
|
|
221
239
|
const counts = result.files.reduce<Record<GenFileStatus, number>>(
|
|
222
240
|
(a, f) => ((a[f.status] = (a[f.status] ?? 0) + 1), a),
|
|
223
|
-
{ new: 0, overwrite: 0, merged: 0, conflict: 0, unchanged: 0, refused: 0, removed: 0 },
|
|
241
|
+
{ new: 0, overwrite: 0, merged: 0, conflict: 0, unchanged: 0, refused: 0, adopted: 0, removed: 0 },
|
|
224
242
|
);
|
|
225
243
|
const parts: string[] = [];
|
|
226
244
|
// Same split as the text formatter: `written` is new + overwrite, and the
|
|
@@ -231,13 +249,23 @@ export function genResultToData(result: GenResultShape): {
|
|
|
231
249
|
if (counts.conflict) parts.push(`${counts.conflict} conflict`);
|
|
232
250
|
if (counts.unchanged) parts.push(`${counts.unchanged} unchanged`);
|
|
233
251
|
if (counts.refused) parts.push(`${counts.refused} refused`);
|
|
252
|
+
if (counts.adopted) parts.push(`${counts.adopted} adopted`);
|
|
234
253
|
if (counts.removed) parts.push(`${counts.removed} removed`);
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
254
|
+
// Two ways to emit nothing, and they send the reader to different files. Name the
|
|
255
|
+
// one that is actually true: a config wiring no generators cannot produce output
|
|
256
|
+
// however good the metadata is, and telling its author to go write entities is a
|
|
257
|
+
// false statement about a project that already has them.
|
|
258
|
+
const noGenerators = result.generatorCount === 0;
|
|
259
|
+
const summary = result.files.length !== 0
|
|
260
|
+
? parts.join(", ")
|
|
261
|
+
: noGenerators
|
|
262
|
+
? "no generators are wired in metaobjects.config.ts, so nothing was generated"
|
|
263
|
+
: `no entities to generate in ${result.outDir}`;
|
|
264
|
+
const help = result.files.length !== 0
|
|
265
|
+
? ["typecheck the generated code with `npx tsc`", "create your database tables with `meta migrate --from-db --db <url> --dialect <sqlite|postgres> --slug init --apply`"]
|
|
266
|
+
: noGenerators
|
|
267
|
+
? ["add generators to the `generators: []` array in metaobjects.config.ts — `meta eject --list` names every generator you can own, and `meta init` scaffolds the usual set"]
|
|
268
|
+
: ["author entities in this project's metadata sources then re-run `meta gen`"];
|
|
241
269
|
// An absent section is STATED, never omitted: a reader must be able to tell
|
|
242
270
|
// "the scan found nothing" from "the scan never ran".
|
|
243
271
|
const antiPatterns = result.antiPatterns
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// One spelling of "a path, with forward slashes".
|
|
2
|
+
//
|
|
3
|
+
// Six modules under lib/ carried this rule inline — `git-ignore`, `docs-drift` (twice),
|
|
4
|
+
// `anti-patterns`, `base-url-advisory` and `ignored-scaffold-check` — and one of the six
|
|
5
|
+
// was spelled differently: `.split("\\").join("/")` rather than `.split(sep).join("/")`.
|
|
6
|
+
// On Windows those agree, because `sep` IS a backslash. On POSIX they do not: `sep` is
|
|
7
|
+
// already "/" so the correct form is a no-op, while splitting on a literal backslash
|
|
8
|
+
// rewrites a filename that legitimately CONTAINS one. That is the drift six copies of a
|
|
9
|
+
// one-line rule invite, and it had already happened.
|
|
10
|
+
//
|
|
11
|
+
// Comparisons against git's output are the reason the rule exists at all: `git check-ignore`
|
|
12
|
+
// and `git ls-files` speak forward slashes on every platform, so a Windows path has to be
|
|
13
|
+
// normalized before it can be looked up in a set built from git's answer.
|
|
14
|
+
|
|
15
|
+
import { relative, sep } from "node:path";
|
|
16
|
+
|
|
17
|
+
/** A path with the platform separator replaced by "/" — a no-op on POSIX. */
|
|
18
|
+
export function toPosix(p: string): string {
|
|
19
|
+
return p.split(sep).join("/");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** `abs` relative to `root`, forward-slashed. */
|
|
23
|
+
export function relPosix(root: string, abs: string): string {
|
|
24
|
+
return toPosix(relative(root, abs));
|
|
25
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { MigrationApplyError } from "@metaobjectsdev/migrate-ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* What to tell someone whose committed chain will not apply to an empty database.
|
|
5
|
+
*
|
|
6
|
+
* The message this replaces prescribed ONE fix — *"fix this with a NEW migration that
|
|
7
|
+
* creates the missing object"* — and it cannot work, in any case. Migrations apply in
|
|
8
|
+
* timestamp order, so anything authored now sorts AFTER the migration that failed; the
|
|
9
|
+
* object is still missing when that file runs. Two estates found it from opposite
|
|
10
|
+
* directions and it is the same defect both times:
|
|
11
|
+
*
|
|
12
|
+
* - a chain whose FIRST migration needs a schema an out-of-band script creates (the third
|
|
13
|
+
* migration emits `CREATE SCHEMA IF NOT EXISTS`, the first predates it). A new migration
|
|
14
|
+
* sorts last, after the one that needs the schema.
|
|
15
|
+
* - a project where **another tool owns schema creation** — drizzle-kit's chain builds the
|
|
16
|
+
* tables, `.metaobjects/migrations/` holds later incremental patches. Following the
|
|
17
|
+
* printed remedy literally means hand-authoring every `CREATE TABLE` into the MetaObjects
|
|
18
|
+
* chain, i.e. duplicating a schema this project does not own, which is the opposite of
|
|
19
|
+
* what the agent context teaches two paragraphs into its own Principles.
|
|
20
|
+
*
|
|
21
|
+
* The old message assumed MetaObjects is the only schema authority and offered no third
|
|
22
|
+
* possibility. These name the fixes that exist, and the head-of-chain case gets the extra
|
|
23
|
+
* sentence that is only true there: nothing in this chain could have created what the
|
|
24
|
+
* first migration needs, so the base schema comes from outside it.
|
|
25
|
+
*
|
|
26
|
+
* "Applied migrations are immutable" is kept, because it is true and it is the reason the
|
|
27
|
+
* obvious fix is not offered first — but it is stated as the CONDITION it actually is
|
|
28
|
+
* (a checksum guard on databases that already ran the file), not as a blanket ban, because
|
|
29
|
+
* a migration no database has applied yet is edited in place every day.
|
|
30
|
+
*/
|
|
31
|
+
/**
|
|
32
|
+
* Is this the migrate engine's own apply failure?
|
|
33
|
+
*
|
|
34
|
+
* By NAME, not `instanceof`. Two physical copies of `@metaobjectsdev/migrate-ts` in one
|
|
35
|
+
* process — a globally-installed or linked `meta` alongside a project-local dependency —
|
|
36
|
+
* give the class object and the instance different identities, so `instanceof` returns
|
|
37
|
+
* false for a real error and the remedy silently degrades to its un-positioned form. That
|
|
38
|
+
* is the same class-identity defect that split ts-poet's `Code` in 0.21.6 and drove the
|
|
39
|
+
* exported node guards in `metadata`. Unreachable through today's single import; costs one
|
|
40
|
+
* line to make unreachable by construction.
|
|
41
|
+
*/
|
|
42
|
+
function isApplyError(err: unknown): err is MigrationApplyError {
|
|
43
|
+
return err instanceof MigrationApplyError
|
|
44
|
+
|| (err instanceof Error && err.name === "MigrationApplyError"
|
|
45
|
+
&& typeof (err as MigrationApplyError).migration === "string"
|
|
46
|
+
&& typeof (err as MigrationApplyError).index === "number");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function replayRemedy(err: unknown): string[] {
|
|
50
|
+
const applyErr = isApplyError(err) ? err : undefined;
|
|
51
|
+
const head = applyErr !== undefined && applyErr.index === 0;
|
|
52
|
+
const which =
|
|
53
|
+
applyErr === undefined
|
|
54
|
+
? "the committed chain does not apply to an empty database."
|
|
55
|
+
: `'${applyErr.migration}' (migration ${applyErr.index + 1} of ${applyErr.pendingCount}) ` +
|
|
56
|
+
`does not apply to an empty database.`;
|
|
57
|
+
|
|
58
|
+
const out = [which];
|
|
59
|
+
if (head) {
|
|
60
|
+
out.push(
|
|
61
|
+
"It is the FIRST migration in the chain, so nothing here could have created what it " +
|
|
62
|
+
"needs — this chain has never built a database from empty, and its base schema comes " +
|
|
63
|
+
"from outside it.",
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
out.push(
|
|
67
|
+
"A NEW migration cannot fix this: migrations apply in timestamp order, so anything you " +
|
|
68
|
+
"author now sorts after the file that failed.",
|
|
69
|
+
);
|
|
70
|
+
out.push(
|
|
71
|
+
"The fixes that do work: if another tool or an out-of-band script owns schema creation " +
|
|
72
|
+
"for this project, --replay asserts a property this chain does not have — don't wire " +
|
|
73
|
+
"it. Otherwise correct the migration in place if no database has applied it yet " +
|
|
74
|
+
"(the ledger's checksum guard is what stops you once one has), or re-baseline from a " +
|
|
75
|
+
"live database with `meta migrate baseline --from-db --db <url>`.",
|
|
76
|
+
);
|
|
77
|
+
return out;
|
|
78
|
+
}
|
package/src/lib/wrangler.ts
CHANGED
|
@@ -24,8 +24,56 @@ export function buildWranglerExecuteArgs(opts: WranglerExecuteOptions): string[]
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
/**
|
|
27
|
-
*
|
|
28
|
-
*
|
|
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
|
|
43
|
-
throw new Error(`wrangler ${args.join(" ")} failed: ${
|
|
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
|
|