@metaobjectsdev/cli 1.0.0-rc.1 → 1.0.0-rc.3
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/eject.d.ts +15 -1
- package/dist/src/commands/eject.d.ts.map +1 -1
- package/dist/src/commands/eject.js +98 -7
- package/dist/src/commands/eject.js.map +1 -1
- package/dist/src/lib/owned-copy.d.ts +20 -0
- package/dist/src/lib/owned-copy.d.ts.map +1 -0
- package/dist/src/lib/owned-copy.js +107 -0
- package/dist/src/lib/owned-copy.js.map +1 -0
- package/package.json +12 -10
- package/src/commands/eject.ts +132 -8
- package/src/lib/owned-copy.ts +122 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type OwnedComparison } from "../lib/owned-copy.js";
|
|
1
2
|
/** Every ejectable name, in registry order (stable — matches `meta eject --list`). */
|
|
2
3
|
export declare function ejectableNames(): string[];
|
|
3
4
|
export interface EjectOptions {
|
|
@@ -16,7 +17,20 @@ export interface EjectResult {
|
|
|
16
17
|
packageName: string;
|
|
17
18
|
/** Advisory lines about packages the ejected file imports but the project lacks. */
|
|
18
19
|
dependencyNotes: string[];
|
|
19
|
-
status: "created" | "preserved";
|
|
20
|
+
status: "created" | "preserved" | "replaced";
|
|
21
|
+
/**
|
|
22
|
+
* How the file ALREADY on disk compares to the reference template this CLI ships.
|
|
23
|
+
* `undefined` when there was no file.
|
|
24
|
+
*
|
|
25
|
+
* An owned copy is the one artifact ADR-0034 hands an adopter and then never speaks
|
|
26
|
+
* about again. It can sit any number of releases behind the engine it runs against —
|
|
27
|
+
* on the public reference app three of them were ~five minor lines stale, carrying a
|
|
28
|
+
* bare `ts-poet` import (the 0.21.6 split-tree defect), the pre-#248 subtype
|
|
29
|
+
* persistability check and a missing `isWriteThrough` branch — and every gate was
|
|
30
|
+
* green, because nothing compares an owned copy to anything. Reporting this is the
|
|
31
|
+
* cheapest thing that makes that condition observable.
|
|
32
|
+
*/
|
|
33
|
+
comparison?: OwnedComparison | undefined;
|
|
20
34
|
}
|
|
21
35
|
/**
|
|
22
36
|
* An ejected file is ordinary source in the adopter's repo, so its imports must be
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"eject.d.ts","sourceRoot":"","sources":["../../../src/commands/eject.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"eject.d.ts","sourceRoot":"","sources":["../../../src/commands/eject.ts"],"names":[],"mappings":"AAcA,OAAO,EAAoB,KAAK,eAAe,EAAE,MAAM,sBAAsB,CAAC;AA+C9E,sFAAsF;AACtF,wBAAgB,cAAc,IAAI,MAAM,EAAE,CAEzC;AA2CD,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb;sFACkF;IAClF,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,8EAA8E;IAC9E,UAAU,EAAE,MAAM,CAAC;IACnB,iFAAiF;IACjF,WAAW,EAAE,MAAM,CAAC;IACpB,oFAAoF;IACpF,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B,MAAM,EAAE,SAAS,GAAG,WAAW,GAAG,UAAU,CAAC;IAC7C;;;;;;;;;;;OAWG;IACH,UAAU,CAAC,EAAE,eAAe,GAAG,SAAS,CAAC;CAC1C;AAoBD;;;;;;;;;;;;;GAaG;AACH,wBAAsB,0BAA0B,CAAC,GAAG,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAkBvG;AAED,wBAAsB,cAAc,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC,CAsC7E;AAmED,wBAAsB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CA4G/E"}
|
|
@@ -12,6 +12,7 @@ import * as tanstackTpl from "@metaobjectsdev/codegen-ts-tanstack";
|
|
|
12
12
|
import { parseEjectArgs } from "../lib/args.js";
|
|
13
13
|
import { log } from "../lib/log.js";
|
|
14
14
|
import { declaredDependencyNames, readPackageManifest } from "../lib/package-manifest.js";
|
|
15
|
+
import { compareOwnedCopy } from "../lib/owned-copy.js";
|
|
15
16
|
// Mirrors `OWNED_GENERATORS_DIR` in init.ts's `writeOwnedGenerators` — same directory,
|
|
16
17
|
// same never-clobber-without-consent contract. Kept as its own local constant rather
|
|
17
18
|
// than shared: eject is a standalone operation on ANY name, not a byproduct of init,
|
|
@@ -157,20 +158,71 @@ export async function ejectGenerator(opts) {
|
|
|
157
158
|
packageName: source.packageName,
|
|
158
159
|
dependencyNotes: notes,
|
|
159
160
|
};
|
|
160
|
-
|
|
161
|
-
|
|
161
|
+
const existing = (await fileExists(abs)) ? await readFile(abs, "utf8") : undefined;
|
|
162
|
+
const comparison = existing === undefined ? undefined : await compareOwnedCopy(existing, templateSource);
|
|
163
|
+
if (!opts.force && existing !== undefined) {
|
|
164
|
+
return { ...common, status: "preserved", comparison };
|
|
162
165
|
}
|
|
163
166
|
await mkdir(join(opts.cwd, OWNED_GENERATORS_DIR), { recursive: true });
|
|
164
167
|
await writeFile(abs, templateSource, "utf8");
|
|
165
|
-
return {
|
|
168
|
+
return {
|
|
169
|
+
...common,
|
|
170
|
+
status: existing === undefined ? "created" : "replaced",
|
|
171
|
+
comparison,
|
|
172
|
+
};
|
|
166
173
|
}
|
|
167
|
-
|
|
174
|
+
/**
|
|
175
|
+
* `--list` doubles as the staleness report for what this project ALREADY owns.
|
|
176
|
+
*
|
|
177
|
+
* A listing of names you could eject is the less useful half. The half nobody had is
|
|
178
|
+
* "which of the copies I own have fallen behind" — the condition that let a reference
|
|
179
|
+
* app run generators ~five minor lines old, carrying three retired patterns, with every
|
|
180
|
+
* gate green. Marking each owned copy `identical` / `differs` makes it a one-command
|
|
181
|
+
* answer instead of a diff nobody thinks to run.
|
|
182
|
+
*/
|
|
183
|
+
async function listOutput(cwd) {
|
|
168
184
|
const lines = [];
|
|
169
185
|
lines.push("Ejectable generators (copy any of these into codegen/generators/ and own it):");
|
|
170
186
|
lines.push("");
|
|
187
|
+
let anyStale = false;
|
|
171
188
|
for (const source of SOURCES) {
|
|
172
189
|
lines.push(`${source.packageName}:`);
|
|
173
|
-
|
|
190
|
+
for (const name of source.names) {
|
|
191
|
+
const abs = join(cwd, OWNED_GENERATORS_DIR, `${name}.ts`);
|
|
192
|
+
if (!(await fileExists(abs))) {
|
|
193
|
+
lines.push(` ${name}`);
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
let ref;
|
|
197
|
+
try {
|
|
198
|
+
ref = await readFile(join(source.root(), `${name}.ts`), "utf8");
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
lines.push(` ${name} [owned]`);
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
const owned = await readFile(abs, "utf8");
|
|
205
|
+
const cmp = await compareOwnedCopy(owned, ref);
|
|
206
|
+
if (cmp.verdict === "identical") {
|
|
207
|
+
lines.push(` ${name} [owned — identical to the reference]`);
|
|
208
|
+
}
|
|
209
|
+
else if (cmp.verdict === "reformatted") {
|
|
210
|
+
lines.push(` ${name} [owned — same content as the reference, your formatting]`);
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
anyStale = true;
|
|
214
|
+
lines.push(` ${name} [owned — DIFFERS: ${cmp.referenceOnly} line(s) behind, ` +
|
|
215
|
+
`${cmp.localOnly} line(s) of your own]`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
lines.push("");
|
|
219
|
+
}
|
|
220
|
+
if (anyStale) {
|
|
221
|
+
lines.push("Both counts ignore formatting: each file is re-formatted and its lines sorted " +
|
|
222
|
+
"before comparing, so re-wrapping and import order never show up here.");
|
|
223
|
+
lines.push(" \"behind\" — lines the reference has that your copy does not. Upstream moved.");
|
|
224
|
+
lines.push(" \"of your own\" — lines your copy has that the reference does not. Your customization.");
|
|
225
|
+
lines.push("`meta eject <name>` prints the diff command that shows which lines they are.");
|
|
174
226
|
lines.push("");
|
|
175
227
|
}
|
|
176
228
|
lines.push("Run: meta eject <name>");
|
|
@@ -186,7 +238,7 @@ export async function ejectCommand(args, cwd) {
|
|
|
186
238
|
return 2;
|
|
187
239
|
}
|
|
188
240
|
if (flags.list) {
|
|
189
|
-
log.info(listOutput());
|
|
241
|
+
log.info(await listOutput(cwd));
|
|
190
242
|
return 0;
|
|
191
243
|
}
|
|
192
244
|
if (flags.name === undefined) {
|
|
@@ -196,7 +248,46 @@ export async function ejectCommand(args, cwd) {
|
|
|
196
248
|
try {
|
|
197
249
|
const result = await ejectGenerator({ cwd, name: flags.name, force: flags.force });
|
|
198
250
|
if (result.status === "preserved") {
|
|
199
|
-
|
|
251
|
+
// "already exists — left untouched" was the whole message, and it answered the
|
|
252
|
+
// question nobody has. What an owner needs to know is whether their copy still
|
|
253
|
+
// matches what this CLI ships — the only way an owned generator's staleness is
|
|
254
|
+
// ever observable, since no gate compares the two.
|
|
255
|
+
if (result.comparison?.verdict === "identical") {
|
|
256
|
+
log.info(`${result.path} already exists and is IDENTICAL to the ${result.packageName} ` +
|
|
257
|
+
"reference template — nothing to do.");
|
|
258
|
+
}
|
|
259
|
+
else if (result.comparison?.verdict === "reformatted") {
|
|
260
|
+
// Worth its own branch: this is the state a project that formats what it owns
|
|
261
|
+
// is in permanently, and calling it DIFFERS taught every one of them to ignore
|
|
262
|
+
// the line that is supposed to warn them.
|
|
263
|
+
log.info(`${result.path} already exists and has the SAME CONTENT as the ` +
|
|
264
|
+
`${result.packageName} reference template, in your own formatting — ` +
|
|
265
|
+
"nothing to do.");
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
log.info(`${result.path} already exists and DIFFERS from the ${result.packageName} ` +
|
|
269
|
+
`reference template (${result.comparison?.referenceOnly ?? 0} line(s) behind it, ` +
|
|
270
|
+
`${result.comparison?.localOnly ?? 0} line(s) of your own) — left untouched.`);
|
|
271
|
+
log.info(" Formatting is not counted: both files are re-formatted and their lines " +
|
|
272
|
+
"sorted before comparing, so re-wrapping and import order never show up.");
|
|
273
|
+
log.info(" That difference is either YOUR customization or upstream having moved on. " +
|
|
274
|
+
"See which, before deciding:");
|
|
275
|
+
log.info(` diff -u node_modules/${result.packageName}/src/reference/${flags.name}.ts ${result.path}`);
|
|
276
|
+
log.info(" To take upstream changes AND keep your customization, three-way merge them — " +
|
|
277
|
+
"`git merge-file --diff3 <your copy> <the reference you ejected from> <the reference above>`. " +
|
|
278
|
+
"`--force` does NOT merge: it replaces the file and your customization with it.");
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
else if (result.status === "replaced") {
|
|
282
|
+
// Never let a --force over a modified file be silent: this is the step that
|
|
283
|
+
// destroys an adopter's customization, and the file's own header is often the
|
|
284
|
+
// only record that the customization was deliberate.
|
|
285
|
+
log.info(`Ejected "${flags.name}" -> ${result.path}, REPLACING the file that was there` +
|
|
286
|
+
(result.comparison?.verdict === "differs"
|
|
287
|
+
? ` and DISCARDING ${result.comparison.localOnly} line(s) it had that the reference does not.`
|
|
288
|
+
: result.comparison?.verdict === "reformatted"
|
|
289
|
+
? " (its content was already the reference's — only your formatting is gone)."
|
|
290
|
+
: " (it was already identical to the reference)."));
|
|
200
291
|
}
|
|
201
292
|
else {
|
|
202
293
|
log.info(`Ejected "${flags.name}" -> ${result.path}. You own it now (ADR-0034 scaffold-and-own).`);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"eject.js","sourceRoot":"","sources":["../../../src/commands/eject.ts"],"names":[],"mappings":"AAAA,sFAAsF;AACtF,sFAAsF;AACtF,6FAA6F;AAC7F,wFAAwF;AACxF,uEAAuE;AACvE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AACpE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAC/C,OAAO,KAAK,OAAO,MAAM,4BAA4B,CAAC;AACtD,OAAO,KAAK,QAAQ,MAAM,kCAAkC,CAAC;AAC7D,OAAO,KAAK,WAAW,MAAM,qCAAqC,CAAC;AACnE,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,EAAE,GAAG,EAAE,MAAM,eAAe,CAAC;AACpC,OAAO,EAAE,uBAAuB,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;
|
|
1
|
+
{"version":3,"file":"eject.js","sourceRoot":"","sources":["../../../src/commands/eject.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export type OwnedVerdict =
|
|
2
|
+
/** Byte-for-byte the reference. */
|
|
3
|
+
"identical"
|
|
4
|
+
/** Same content, different layout — a formatter ran over it, nothing else. */
|
|
5
|
+
| "reformatted"
|
|
6
|
+
/** Genuine content difference: your edits, upstream's, or both. */
|
|
7
|
+
| "differs";
|
|
8
|
+
export interface OwnedComparison {
|
|
9
|
+
verdict: OwnedVerdict;
|
|
10
|
+
/** Canonical lines the COPY has that the reference does not — your edits, and what
|
|
11
|
+
* `--force` would destroy. */
|
|
12
|
+
localOnly: number;
|
|
13
|
+
/** Canonical lines the REFERENCE has that the copy does not — how far behind it is. */
|
|
14
|
+
referenceOnly: number;
|
|
15
|
+
}
|
|
16
|
+
/** The canonical line multiset of one file, sorted. Exported for the tests that pin
|
|
17
|
+
* each formatter-insensitivity claim above against real reformatted input. */
|
|
18
|
+
export declare function canonicalLines(src: string): Promise<string[]>;
|
|
19
|
+
export declare function compareOwnedCopy(owned: string, reference: string): Promise<OwnedComparison>;
|
|
20
|
+
//# sourceMappingURL=owned-copy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"owned-copy.d.ts","sourceRoot":"","sources":["../../../src/lib/owned-copy.ts"],"names":[],"mappings":"AAqCA,MAAM,MAAM,YAAY;AACtB,mCAAmC;AACjC,WAAW;AACb,8EAA8E;GAC5E,aAAa;AACf,mEAAmE;GACjE,SAAS,CAAC;AAEd,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,YAAY,CAAC;IACtB;mCAC+B;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,uFAAuF;IACvF,aAAa,EAAE,MAAM,CAAC;CACvB;AAuBD;+EAC+E;AAC/E,wBAAsB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAmBnE;AAeD,wBAAsB,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,CAUjG"}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// How an OWNED generator copy compares to the reference template this CLI ships.
|
|
2
|
+
//
|
|
3
|
+
// ADR-0034 hands an adopter a file and then never speaks about it again, so `meta
|
|
4
|
+
// eject` and `meta eject --list` are the only places that condition is ever observable.
|
|
5
|
+
// The first version of this compared BYTES and counted lines the copy had that the
|
|
6
|
+
// reference did not. On a project that runs its own formatter over the code it owns —
|
|
7
|
+
// which is most of them, and it is the whole point that these are ordinary files in
|
|
8
|
+
// their repo — that answer is useless: every copy reads DIFFERS forever, and the count
|
|
9
|
+
// is dominated by re-wrapping and import sorting rather than by anything anyone wrote.
|
|
10
|
+
// One adopter estate had all of its owned copies reporting double-digit differences
|
|
11
|
+
// while being, in content, exactly the reference.
|
|
12
|
+
//
|
|
13
|
+
// So the comparison is made on a CANONICAL form instead, and the verdict grew a third
|
|
14
|
+
// value — `reformatted`, meaning "same content, your layout". The canonical form is
|
|
15
|
+
// produced by re-formatting both sides through the SAME formatter, deliberately at
|
|
16
|
+
// `lineWidth: 1` so every construct explodes onto its own line, then sorting those
|
|
17
|
+
// lines. That composition is what makes it survive each way a formatter rewrites a
|
|
18
|
+
// file without changing it:
|
|
19
|
+
//
|
|
20
|
+
// indentation, wrapping, quote style, semicolons → the formatter normalizes them
|
|
21
|
+
// trailing commas → `trailingCommas: "none"` + the
|
|
22
|
+
// per-line strip below (reordering
|
|
23
|
+
// a list moves which member is last)
|
|
24
|
+
// import statement AND specifier order → exploded one per line, then sorted
|
|
25
|
+
//
|
|
26
|
+
// The cost is deliberate and stated: sorting lines also makes the comparison blind to
|
|
27
|
+
// a pure REORDERING of real code. That is the right trade for a report line whose job
|
|
28
|
+
// is "have you fallen behind upstream" — a reorder with no content change is not
|
|
29
|
+
// staleness — and every message that carries this verdict also prints the `diff`
|
|
30
|
+
// command, which is not blind to anything.
|
|
31
|
+
//
|
|
32
|
+
// Fail-safe: if the formatter is unavailable or rejects the input (an adopter's copy is
|
|
33
|
+
// theirs and may not even parse), canonicalization degrades to trimmed raw lines. The
|
|
34
|
+
// verdict then reads `differs` where it might have read `reformatted` — the pre-existing
|
|
35
|
+
// behaviour, never a false `reformatted`.
|
|
36
|
+
import { Biome, Distribution } from "@biomejs/js-api";
|
|
37
|
+
// One instance, created lazily and shared. Cache the PROMISE so concurrent first calls
|
|
38
|
+
// (--list compares every owned copy) share one in-flight create rather than racing.
|
|
39
|
+
let _biome;
|
|
40
|
+
function getBiome() {
|
|
41
|
+
if (!_biome) {
|
|
42
|
+
_biome = Biome.create({ distribution: Distribution.NODE })
|
|
43
|
+
.then((biome) => {
|
|
44
|
+
biome.applyConfiguration({
|
|
45
|
+
formatter: { enabled: true, indentStyle: "space", indentWidth: 2, lineWidth: 1 },
|
|
46
|
+
javascript: { formatter: { quoteStyle: "double", semicolons: "always", trailingCommas: "none" } },
|
|
47
|
+
});
|
|
48
|
+
return biome;
|
|
49
|
+
})
|
|
50
|
+
// A comparison is a REPORT LINE. If the formatter cannot start, say less — never
|
|
51
|
+
// fail the command the adopter actually ran.
|
|
52
|
+
.catch(() => undefined);
|
|
53
|
+
}
|
|
54
|
+
return _biome;
|
|
55
|
+
}
|
|
56
|
+
/** The canonical line multiset of one file, sorted. Exported for the tests that pin
|
|
57
|
+
* each formatter-insensitivity claim above against real reformatted input. */
|
|
58
|
+
export async function canonicalLines(src) {
|
|
59
|
+
let text = src;
|
|
60
|
+
const biome = await getBiome();
|
|
61
|
+
if (biome !== undefined) {
|
|
62
|
+
try {
|
|
63
|
+
const result = biome.formatContent(src, { filePath: "in-memory.ts" });
|
|
64
|
+
if (result.diagnostics.length === 0)
|
|
65
|
+
text = result.content;
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
/* keep the raw text — see the fail-safe note above */
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return text
|
|
72
|
+
.split("\n")
|
|
73
|
+
// The trailing-comma strip is per LINE and only meaningful because every construct
|
|
74
|
+
// is already on its own line: reordering an import list changes which specifier is
|
|
75
|
+
// last, and only the last one lacks a comma.
|
|
76
|
+
.map((l) => l.trim().replace(/,$/, ""))
|
|
77
|
+
.filter((l) => l !== "")
|
|
78
|
+
.sort();
|
|
79
|
+
}
|
|
80
|
+
/** How many members of `a` are not in `b`, counting duplicates. */
|
|
81
|
+
function countMissing(a, b) {
|
|
82
|
+
const remaining = new Map();
|
|
83
|
+
for (const l of b)
|
|
84
|
+
remaining.set(l, (remaining.get(l) ?? 0) + 1);
|
|
85
|
+
let missing = 0;
|
|
86
|
+
for (const l of a) {
|
|
87
|
+
const n = remaining.get(l) ?? 0;
|
|
88
|
+
if (n > 0)
|
|
89
|
+
remaining.set(l, n - 1);
|
|
90
|
+
else
|
|
91
|
+
missing++;
|
|
92
|
+
}
|
|
93
|
+
return missing;
|
|
94
|
+
}
|
|
95
|
+
export async function compareOwnedCopy(owned, reference) {
|
|
96
|
+
if (owned === reference)
|
|
97
|
+
return { verdict: "identical", localOnly: 0, referenceOnly: 0 };
|
|
98
|
+
const [a, b] = await Promise.all([canonicalLines(owned), canonicalLines(reference)]);
|
|
99
|
+
const localOnly = countMissing(a, b);
|
|
100
|
+
const referenceOnly = countMissing(b, a);
|
|
101
|
+
return {
|
|
102
|
+
verdict: localOnly === 0 && referenceOnly === 0 ? "reformatted" : "differs",
|
|
103
|
+
localOnly,
|
|
104
|
+
referenceOnly,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
//# sourceMappingURL=owned-copy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"owned-copy.js","sourceRoot":"","sources":["../../../src/lib/owned-copy.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.
|
|
3
|
+
"version": "1.0.0-rc.3",
|
|
4
4
|
"description": "CLI for MetaObjects: scaffold, codegen, migrate, and drift-detection commands.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/src/index.js",
|
|
@@ -48,16 +48,18 @@
|
|
|
48
48
|
"drift-detection"
|
|
49
49
|
],
|
|
50
50
|
"dependencies": {
|
|
51
|
+
"@biomejs/js-api": "^0.7.0",
|
|
52
|
+
"@biomejs/wasm-nodejs": "^1.9.4",
|
|
51
53
|
"@libsql/kysely-libsql": "^0.4.0",
|
|
52
|
-
"@metaobjectsdev/codegen-ts": "1.0.0-rc.
|
|
53
|
-
"@metaobjectsdev/codegen-ts-react": "1.0.0-rc.
|
|
54
|
-
"@metaobjectsdev/codegen-ts-tanstack": "1.0.0-rc.
|
|
55
|
-
"@metaobjectsdev/docs-site": "1.0.0-rc.
|
|
56
|
-
"@metaobjectsdev/metadata": "1.0.0-rc.
|
|
57
|
-
"@metaobjectsdev/migrate-ts": "1.0.0-rc.
|
|
58
|
-
"@metaobjectsdev/render": "1.0.0-rc.
|
|
59
|
-
"@metaobjectsdev/runtime-ts": "1.0.0-rc.
|
|
60
|
-
"@metaobjectsdev/sdk": "1.0.0-rc.
|
|
54
|
+
"@metaobjectsdev/codegen-ts": "1.0.0-rc.3",
|
|
55
|
+
"@metaobjectsdev/codegen-ts-react": "1.0.0-rc.3",
|
|
56
|
+
"@metaobjectsdev/codegen-ts-tanstack": "1.0.0-rc.3",
|
|
57
|
+
"@metaobjectsdev/docs-site": "1.0.0-rc.3",
|
|
58
|
+
"@metaobjectsdev/metadata": "1.0.0-rc.3",
|
|
59
|
+
"@metaobjectsdev/migrate-ts": "1.0.0-rc.3",
|
|
60
|
+
"@metaobjectsdev/render": "1.0.0-rc.3",
|
|
61
|
+
"@metaobjectsdev/runtime-ts": "1.0.0-rc.3",
|
|
62
|
+
"@metaobjectsdev/sdk": "1.0.0-rc.3",
|
|
61
63
|
"@toon-format/toon": "^2.3.0",
|
|
62
64
|
"jiti": "^2.4.0"
|
|
63
65
|
},
|
package/src/commands/eject.ts
CHANGED
|
@@ -12,6 +12,7 @@ import * as tanstackTpl from "@metaobjectsdev/codegen-ts-tanstack";
|
|
|
12
12
|
import { parseEjectArgs } from "../lib/args.js";
|
|
13
13
|
import { log } from "../lib/log.js";
|
|
14
14
|
import { declaredDependencyNames, readPackageManifest } from "../lib/package-manifest.js";
|
|
15
|
+
import { compareOwnedCopy, type OwnedComparison } from "../lib/owned-copy.js";
|
|
15
16
|
|
|
16
17
|
// Mirrors `OWNED_GENERATORS_DIR` in init.ts's `writeOwnedGenerators` — same directory,
|
|
17
18
|
// same never-clobber-without-consent contract. Kept as its own local constant rather
|
|
@@ -121,7 +122,20 @@ export interface EjectResult {
|
|
|
121
122
|
packageName: string;
|
|
122
123
|
/** Advisory lines about packages the ejected file imports but the project lacks. */
|
|
123
124
|
dependencyNotes: string[];
|
|
124
|
-
status: "created" | "preserved";
|
|
125
|
+
status: "created" | "preserved" | "replaced";
|
|
126
|
+
/**
|
|
127
|
+
* How the file ALREADY on disk compares to the reference template this CLI ships.
|
|
128
|
+
* `undefined` when there was no file.
|
|
129
|
+
*
|
|
130
|
+
* An owned copy is the one artifact ADR-0034 hands an adopter and then never speaks
|
|
131
|
+
* about again. It can sit any number of releases behind the engine it runs against —
|
|
132
|
+
* on the public reference app three of them were ~five minor lines stale, carrying a
|
|
133
|
+
* bare `ts-poet` import (the 0.21.6 split-tree defect), the pre-#248 subtype
|
|
134
|
+
* persistability check and a missing `isWriteThrough` branch — and every gate was
|
|
135
|
+
* green, because nothing compares an owned copy to anything. Reporting this is the
|
|
136
|
+
* cheapest thing that makes that condition observable.
|
|
137
|
+
*/
|
|
138
|
+
comparison?: OwnedComparison | undefined;
|
|
125
139
|
}
|
|
126
140
|
|
|
127
141
|
/** The `@metaobjectsdev/*` packages an ejected template imports, read from the file
|
|
@@ -199,22 +213,82 @@ export async function ejectGenerator(opts: EjectOptions): Promise<EjectResult> {
|
|
|
199
213
|
dependencyNotes: notes,
|
|
200
214
|
};
|
|
201
215
|
|
|
202
|
-
|
|
203
|
-
|
|
216
|
+
const existing = (await fileExists(abs)) ? await readFile(abs, "utf8") : undefined;
|
|
217
|
+
const comparison =
|
|
218
|
+
existing === undefined ? undefined : await compareOwnedCopy(existing, templateSource);
|
|
219
|
+
|
|
220
|
+
if (!opts.force && existing !== undefined) {
|
|
221
|
+
return { ...common, status: "preserved", comparison };
|
|
204
222
|
}
|
|
205
223
|
|
|
206
224
|
await mkdir(join(opts.cwd, OWNED_GENERATORS_DIR), { recursive: true });
|
|
207
225
|
await writeFile(abs, templateSource, "utf8");
|
|
208
|
-
return {
|
|
226
|
+
return {
|
|
227
|
+
...common,
|
|
228
|
+
status: existing === undefined ? "created" : "replaced",
|
|
229
|
+
comparison,
|
|
230
|
+
};
|
|
209
231
|
}
|
|
210
232
|
|
|
211
|
-
|
|
233
|
+
/**
|
|
234
|
+
* `--list` doubles as the staleness report for what this project ALREADY owns.
|
|
235
|
+
*
|
|
236
|
+
* A listing of names you could eject is the less useful half. The half nobody had is
|
|
237
|
+
* "which of the copies I own have fallen behind" — the condition that let a reference
|
|
238
|
+
* app run generators ~five minor lines old, carrying three retired patterns, with every
|
|
239
|
+
* gate green. Marking each owned copy `identical` / `differs` makes it a one-command
|
|
240
|
+
* answer instead of a diff nobody thinks to run.
|
|
241
|
+
*/
|
|
242
|
+
async function listOutput(cwd: string): Promise<string> {
|
|
212
243
|
const lines: string[] = [];
|
|
213
244
|
lines.push("Ejectable generators (copy any of these into codegen/generators/ and own it):");
|
|
214
245
|
lines.push("");
|
|
246
|
+
let anyStale = false;
|
|
215
247
|
for (const source of SOURCES) {
|
|
216
248
|
lines.push(`${source.packageName}:`);
|
|
217
|
-
|
|
249
|
+
for (const name of source.names) {
|
|
250
|
+
const abs = join(cwd, OWNED_GENERATORS_DIR, `${name}.ts`);
|
|
251
|
+
if (!(await fileExists(abs))) {
|
|
252
|
+
lines.push(` ${name}`);
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
let ref: string;
|
|
256
|
+
try {
|
|
257
|
+
ref = await readFile(join(source.root(), `${name}.ts`), "utf8");
|
|
258
|
+
} catch {
|
|
259
|
+
lines.push(` ${name} [owned]`);
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
const owned = await readFile(abs, "utf8");
|
|
263
|
+
const cmp = await compareOwnedCopy(owned, ref);
|
|
264
|
+
if (cmp.verdict === "identical") {
|
|
265
|
+
lines.push(` ${name} [owned — identical to the reference]`);
|
|
266
|
+
} else if (cmp.verdict === "reformatted") {
|
|
267
|
+
lines.push(` ${name} [owned — same content as the reference, your formatting]`);
|
|
268
|
+
} else {
|
|
269
|
+
anyStale = true;
|
|
270
|
+
lines.push(
|
|
271
|
+
` ${name} [owned — DIFFERS: ${cmp.referenceOnly} line(s) behind, ` +
|
|
272
|
+
`${cmp.localOnly} line(s) of your own]`,
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
lines.push("");
|
|
277
|
+
}
|
|
278
|
+
if (anyStale) {
|
|
279
|
+
lines.push(
|
|
280
|
+
"Both counts ignore formatting: each file is re-formatted and its lines sorted " +
|
|
281
|
+
"before comparing, so re-wrapping and import order never show up here.",
|
|
282
|
+
);
|
|
283
|
+
lines.push(
|
|
284
|
+
" \"behind\" — lines the reference has that your copy does not. Upstream moved.",
|
|
285
|
+
);
|
|
286
|
+
lines.push(
|
|
287
|
+
" \"of your own\" — lines your copy has that the reference does not. Your customization.",
|
|
288
|
+
);
|
|
289
|
+
lines.push(
|
|
290
|
+
"`meta eject <name>` prints the diff command that shows which lines they are.",
|
|
291
|
+
);
|
|
218
292
|
lines.push("");
|
|
219
293
|
}
|
|
220
294
|
lines.push("Run: meta eject <name>");
|
|
@@ -231,7 +305,7 @@ export async function ejectCommand(args: string[], cwd: string): Promise<number>
|
|
|
231
305
|
}
|
|
232
306
|
|
|
233
307
|
if (flags.list) {
|
|
234
|
-
log.info(listOutput());
|
|
308
|
+
log.info(await listOutput(cwd));
|
|
235
309
|
return 0;
|
|
236
310
|
}
|
|
237
311
|
|
|
@@ -243,7 +317,57 @@ export async function ejectCommand(args: string[], cwd: string): Promise<number>
|
|
|
243
317
|
try {
|
|
244
318
|
const result = await ejectGenerator({ cwd, name: flags.name, force: flags.force });
|
|
245
319
|
if (result.status === "preserved") {
|
|
246
|
-
|
|
320
|
+
// "already exists — left untouched" was the whole message, and it answered the
|
|
321
|
+
// question nobody has. What an owner needs to know is whether their copy still
|
|
322
|
+
// matches what this CLI ships — the only way an owned generator's staleness is
|
|
323
|
+
// ever observable, since no gate compares the two.
|
|
324
|
+
if (result.comparison?.verdict === "identical") {
|
|
325
|
+
log.info(
|
|
326
|
+
`${result.path} already exists and is IDENTICAL to the ${result.packageName} ` +
|
|
327
|
+
"reference template — nothing to do.",
|
|
328
|
+
);
|
|
329
|
+
} else if (result.comparison?.verdict === "reformatted") {
|
|
330
|
+
// Worth its own branch: this is the state a project that formats what it owns
|
|
331
|
+
// is in permanently, and calling it DIFFERS taught every one of them to ignore
|
|
332
|
+
// the line that is supposed to warn them.
|
|
333
|
+
log.info(
|
|
334
|
+
`${result.path} already exists and has the SAME CONTENT as the ` +
|
|
335
|
+
`${result.packageName} reference template, in your own formatting — ` +
|
|
336
|
+
"nothing to do.",
|
|
337
|
+
);
|
|
338
|
+
} else {
|
|
339
|
+
log.info(
|
|
340
|
+
`${result.path} already exists and DIFFERS from the ${result.packageName} ` +
|
|
341
|
+
`reference template (${result.comparison?.referenceOnly ?? 0} line(s) behind it, ` +
|
|
342
|
+
`${result.comparison?.localOnly ?? 0} line(s) of your own) — left untouched.`,
|
|
343
|
+
);
|
|
344
|
+
log.info(
|
|
345
|
+
" Formatting is not counted: both files are re-formatted and their lines " +
|
|
346
|
+
"sorted before comparing, so re-wrapping and import order never show up.",
|
|
347
|
+
);
|
|
348
|
+
log.info(
|
|
349
|
+
" That difference is either YOUR customization or upstream having moved on. " +
|
|
350
|
+
"See which, before deciding:",
|
|
351
|
+
);
|
|
352
|
+
log.info(` diff -u node_modules/${result.packageName}/src/reference/${flags.name}.ts ${result.path}`);
|
|
353
|
+
log.info(
|
|
354
|
+
" To take upstream changes AND keep your customization, three-way merge them — " +
|
|
355
|
+
"`git merge-file --diff3 <your copy> <the reference you ejected from> <the reference above>`. " +
|
|
356
|
+
"`--force` does NOT merge: it replaces the file and your customization with it.",
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
} else if (result.status === "replaced") {
|
|
360
|
+
// Never let a --force over a modified file be silent: this is the step that
|
|
361
|
+
// destroys an adopter's customization, and the file's own header is often the
|
|
362
|
+
// only record that the customization was deliberate.
|
|
363
|
+
log.info(
|
|
364
|
+
`Ejected "${flags.name}" -> ${result.path}, REPLACING the file that was there` +
|
|
365
|
+
(result.comparison?.verdict === "differs"
|
|
366
|
+
? ` and DISCARDING ${result.comparison.localOnly} line(s) it had that the reference does not.`
|
|
367
|
+
: result.comparison?.verdict === "reformatted"
|
|
368
|
+
? " (its content was already the reference's — only your formatting is gone)."
|
|
369
|
+
: " (it was already identical to the reference)."),
|
|
370
|
+
);
|
|
247
371
|
} else {
|
|
248
372
|
log.info(`Ejected "${flags.name}" -> ${result.path}. You own it now (ADR-0034 scaffold-and-own).`);
|
|
249
373
|
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// How an OWNED generator copy compares to the reference template this CLI ships.
|
|
2
|
+
//
|
|
3
|
+
// ADR-0034 hands an adopter a file and then never speaks about it again, so `meta
|
|
4
|
+
// eject` and `meta eject --list` are the only places that condition is ever observable.
|
|
5
|
+
// The first version of this compared BYTES and counted lines the copy had that the
|
|
6
|
+
// reference did not. On a project that runs its own formatter over the code it owns —
|
|
7
|
+
// which is most of them, and it is the whole point that these are ordinary files in
|
|
8
|
+
// their repo — that answer is useless: every copy reads DIFFERS forever, and the count
|
|
9
|
+
// is dominated by re-wrapping and import sorting rather than by anything anyone wrote.
|
|
10
|
+
// One adopter estate had all of its owned copies reporting double-digit differences
|
|
11
|
+
// while being, in content, exactly the reference.
|
|
12
|
+
//
|
|
13
|
+
// So the comparison is made on a CANONICAL form instead, and the verdict grew a third
|
|
14
|
+
// value — `reformatted`, meaning "same content, your layout". The canonical form is
|
|
15
|
+
// produced by re-formatting both sides through the SAME formatter, deliberately at
|
|
16
|
+
// `lineWidth: 1` so every construct explodes onto its own line, then sorting those
|
|
17
|
+
// lines. That composition is what makes it survive each way a formatter rewrites a
|
|
18
|
+
// file without changing it:
|
|
19
|
+
//
|
|
20
|
+
// indentation, wrapping, quote style, semicolons → the formatter normalizes them
|
|
21
|
+
// trailing commas → `trailingCommas: "none"` + the
|
|
22
|
+
// per-line strip below (reordering
|
|
23
|
+
// a list moves which member is last)
|
|
24
|
+
// import statement AND specifier order → exploded one per line, then sorted
|
|
25
|
+
//
|
|
26
|
+
// The cost is deliberate and stated: sorting lines also makes the comparison blind to
|
|
27
|
+
// a pure REORDERING of real code. That is the right trade for a report line whose job
|
|
28
|
+
// is "have you fallen behind upstream" — a reorder with no content change is not
|
|
29
|
+
// staleness — and every message that carries this verdict also prints the `diff`
|
|
30
|
+
// command, which is not blind to anything.
|
|
31
|
+
//
|
|
32
|
+
// Fail-safe: if the formatter is unavailable or rejects the input (an adopter's copy is
|
|
33
|
+
// theirs and may not even parse), canonicalization degrades to trimmed raw lines. The
|
|
34
|
+
// verdict then reads `differs` where it might have read `reformatted` — the pre-existing
|
|
35
|
+
// behaviour, never a false `reformatted`.
|
|
36
|
+
import { Biome, Distribution } from "@biomejs/js-api";
|
|
37
|
+
|
|
38
|
+
export type OwnedVerdict =
|
|
39
|
+
/** Byte-for-byte the reference. */
|
|
40
|
+
| "identical"
|
|
41
|
+
/** Same content, different layout — a formatter ran over it, nothing else. */
|
|
42
|
+
| "reformatted"
|
|
43
|
+
/** Genuine content difference: your edits, upstream's, or both. */
|
|
44
|
+
| "differs";
|
|
45
|
+
|
|
46
|
+
export interface OwnedComparison {
|
|
47
|
+
verdict: OwnedVerdict;
|
|
48
|
+
/** Canonical lines the COPY has that the reference does not — your edits, and what
|
|
49
|
+
* `--force` would destroy. */
|
|
50
|
+
localOnly: number;
|
|
51
|
+
/** Canonical lines the REFERENCE has that the copy does not — how far behind it is. */
|
|
52
|
+
referenceOnly: number;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// One instance, created lazily and shared. Cache the PROMISE so concurrent first calls
|
|
56
|
+
// (--list compares every owned copy) share one in-flight create rather than racing.
|
|
57
|
+
let _biome: Promise<Biome | undefined> | undefined;
|
|
58
|
+
|
|
59
|
+
function getBiome(): Promise<Biome | undefined> {
|
|
60
|
+
if (!_biome) {
|
|
61
|
+
_biome = Biome.create({ distribution: Distribution.NODE })
|
|
62
|
+
.then((biome) => {
|
|
63
|
+
biome.applyConfiguration({
|
|
64
|
+
formatter: { enabled: true, indentStyle: "space", indentWidth: 2, lineWidth: 1 },
|
|
65
|
+
javascript: { formatter: { quoteStyle: "double", semicolons: "always", trailingCommas: "none" } },
|
|
66
|
+
});
|
|
67
|
+
return biome;
|
|
68
|
+
})
|
|
69
|
+
// A comparison is a REPORT LINE. If the formatter cannot start, say less — never
|
|
70
|
+
// fail the command the adopter actually ran.
|
|
71
|
+
.catch(() => undefined);
|
|
72
|
+
}
|
|
73
|
+
return _biome;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** The canonical line multiset of one file, sorted. Exported for the tests that pin
|
|
77
|
+
* each formatter-insensitivity claim above against real reformatted input. */
|
|
78
|
+
export async function canonicalLines(src: string): Promise<string[]> {
|
|
79
|
+
let text = src;
|
|
80
|
+
const biome = await getBiome();
|
|
81
|
+
if (biome !== undefined) {
|
|
82
|
+
try {
|
|
83
|
+
const result = biome.formatContent(src, { filePath: "in-memory.ts" });
|
|
84
|
+
if (result.diagnostics.length === 0) text = result.content;
|
|
85
|
+
} catch {
|
|
86
|
+
/* keep the raw text — see the fail-safe note above */
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return text
|
|
90
|
+
.split("\n")
|
|
91
|
+
// The trailing-comma strip is per LINE and only meaningful because every construct
|
|
92
|
+
// is already on its own line: reordering an import list changes which specifier is
|
|
93
|
+
// last, and only the last one lacks a comma.
|
|
94
|
+
.map((l) => l.trim().replace(/,$/, ""))
|
|
95
|
+
.filter((l) => l !== "")
|
|
96
|
+
.sort();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** How many members of `a` are not in `b`, counting duplicates. */
|
|
100
|
+
function countMissing(a: string[], b: string[]): number {
|
|
101
|
+
const remaining = new Map<string, number>();
|
|
102
|
+
for (const l of b) remaining.set(l, (remaining.get(l) ?? 0) + 1);
|
|
103
|
+
let missing = 0;
|
|
104
|
+
for (const l of a) {
|
|
105
|
+
const n = remaining.get(l) ?? 0;
|
|
106
|
+
if (n > 0) remaining.set(l, n - 1);
|
|
107
|
+
else missing++;
|
|
108
|
+
}
|
|
109
|
+
return missing;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export async function compareOwnedCopy(owned: string, reference: string): Promise<OwnedComparison> {
|
|
113
|
+
if (owned === reference) return { verdict: "identical", localOnly: 0, referenceOnly: 0 };
|
|
114
|
+
const [a, b] = await Promise.all([canonicalLines(owned), canonicalLines(reference)]);
|
|
115
|
+
const localOnly = countMissing(a, b);
|
|
116
|
+
const referenceOnly = countMissing(b, a);
|
|
117
|
+
return {
|
|
118
|
+
verdict: localOnly === 0 && referenceOnly === 0 ? "reformatted" : "differs",
|
|
119
|
+
localOnly,
|
|
120
|
+
referenceOnly,
|
|
121
|
+
};
|
|
122
|
+
}
|