@metaobjectsdev/cli 1.0.9-rc.4 → 1.0.9-rc.6
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/README.md +15 -0
- package/dist/src/commands/eject.d.ts +17 -0
- package/dist/src/commands/eject.d.ts.map +1 -1
- package/dist/src/commands/eject.js +87 -2
- package/dist/src/commands/eject.js.map +1 -1
- package/dist/src/commands/gen.d.ts.map +1 -1
- package/dist/src/commands/gen.js +18 -5
- package/dist/src/commands/gen.js.map +1 -1
- package/dist/src/commands/generator-help.d.ts +21 -0
- package/dist/src/commands/generator-help.d.ts.map +1 -0
- package/dist/src/commands/generator-help.js +43 -0
- package/dist/src/commands/generator-help.js.map +1 -0
- package/dist/src/commands/generator.d.ts +36 -0
- package/dist/src/commands/generator.d.ts.map +1 -0
- package/dist/src/commands/generator.js +333 -0
- package/dist/src/commands/generator.js.map +1 -0
- package/dist/src/commands/init.d.ts.map +1 -1
- package/dist/src/commands/init.js +10 -1
- package/dist/src/commands/init.js.map +1 -1
- package/dist/src/commands/verify.d.ts.map +1 -1
- package/dist/src/commands/verify.js +5 -3
- package/dist/src/commands/verify.js.map +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +8 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/lib/advisory.d.ts +14 -0
- package/dist/src/lib/advisory.d.ts.map +1 -1
- package/dist/src/lib/advisory.js +64 -5
- package/dist/src/lib/advisory.js.map +1 -1
- package/dist/src/lib/catalog-listing.d.ts +14 -4
- package/dist/src/lib/catalog-listing.d.ts.map +1 -1
- package/dist/src/lib/catalog-listing.js +56 -9
- package/dist/src/lib/catalog-listing.js.map +1 -1
- package/dist/src/lib/install-set.d.ts +12 -2
- package/dist/src/lib/install-set.d.ts.map +1 -1
- package/dist/src/lib/install-set.js +14 -2
- package/dist/src/lib/install-set.js.map +1 -1
- package/dist/src/lib/output.d.ts +1 -0
- package/dist/src/lib/output.d.ts.map +1 -1
- package/dist/src/lib/output.js +2 -0
- package/dist/src/lib/output.js.map +1 -1
- package/dist/src/lib/runtime-eject.d.ts +80 -0
- package/dist/src/lib/runtime-eject.d.ts.map +1 -0
- package/dist/src/lib/runtime-eject.js +212 -0
- package/dist/src/lib/runtime-eject.js.map +1 -0
- package/dist/src/lib/unknown-attr-warnings.d.ts +18 -0
- package/dist/src/lib/unknown-attr-warnings.d.ts.map +1 -0
- package/dist/src/lib/unknown-attr-warnings.js +43 -0
- package/dist/src/lib/unknown-attr-warnings.js.map +1 -0
- package/package.json +10 -10
- package/src/commands/eject.ts +103 -3
- package/src/commands/gen.ts +18 -5
- package/src/commands/generator-help.ts +47 -0
- package/src/commands/generator.ts +390 -0
- package/src/commands/init.ts +10 -1
- package/src/commands/verify.ts +5 -4
- package/src/index.ts +8 -0
- package/src/lib/advisory.ts +71 -5
- package/src/lib/catalog-listing.ts +68 -7
- package/src/lib/install-set.ts +19 -2
- package/src/lib/output.ts +3 -0
- package/src/lib/runtime-eject.ts +263 -0
- package/src/lib/unknown-attr-warnings.ts +48 -0
|
@@ -21,6 +21,7 @@ import { join } from "node:path";
|
|
|
21
21
|
import type { MetaData } from "@metaobjectsdev/metadata";
|
|
22
22
|
import {
|
|
23
23
|
runGen,
|
|
24
|
+
stableNameIndex,
|
|
24
25
|
type GeneratorRegistryEntry,
|
|
25
26
|
type Layer,
|
|
26
27
|
type MetaobjectsGenConfig,
|
|
@@ -28,12 +29,17 @@ import {
|
|
|
28
29
|
import * as coreTpl from "@metaobjectsdev/codegen-ts";
|
|
29
30
|
import * as reactTpl from "@metaobjectsdev/codegen-ts-react";
|
|
30
31
|
import * as tanstackTpl from "@metaobjectsdev/codegen-ts-tanstack";
|
|
31
|
-
import { listCatalog, packageOf } from "./catalog.js";
|
|
32
|
+
import { composeCatalog, listCatalog, packageOf } from "./catalog.js";
|
|
32
33
|
import {
|
|
33
34
|
buildLibraryRows, renderLibraryText,
|
|
34
35
|
type LibraryCatalogRow, type LibraryProjectContext,
|
|
35
36
|
} from "./library-listing.js";
|
|
36
37
|
import { installSetFor } from "./install-set.js";
|
|
38
|
+
import {
|
|
39
|
+
RUNTIME_ENTRIES, resolveRuntimePackage, runtimeClosure, runtimeCopyStatus, runtimeInstall,
|
|
40
|
+
type RuntimeCopyRow,
|
|
41
|
+
} from "./runtime-eject.js";
|
|
42
|
+
import { OWNED_RUNTIME_DIR } from "@metaobjectsdev/codegen-ts";
|
|
37
43
|
import { readPackageManifest, declaredDependencyNames } from "./package-manifest.js";
|
|
38
44
|
|
|
39
45
|
/**
|
|
@@ -67,6 +73,13 @@ export interface GeneratorCatalogRow {
|
|
|
67
73
|
ejectable: boolean;
|
|
68
74
|
/** Whether this project already owns a copy. `null` with no project. */
|
|
69
75
|
owned: boolean | null;
|
|
76
|
+
/**
|
|
77
|
+
* ADR-0034 Amendment 3 (2026-09-24): the HTTP-adapter source this generator's output
|
|
78
|
+
* imports, as far as the project owns a copy of it under `codegen/runtime/` — each
|
|
79
|
+
* file compared to the installed runtime package. Absent when the generator's output
|
|
80
|
+
* imports no adapter, or the project owns none of it.
|
|
81
|
+
*/
|
|
82
|
+
runtimeCopy?: RuntimeCopyRow[];
|
|
70
83
|
};
|
|
71
84
|
project?: {
|
|
72
85
|
/** Wired in this project's `generators: [...]`. */
|
|
@@ -237,12 +250,44 @@ const PROBE_DB_IMPORT_PLACEHOLDER = "./db";
|
|
|
237
250
|
// the listing
|
|
238
251
|
// ---------------------------------------------------------------------------
|
|
239
252
|
|
|
253
|
+
/**
|
|
254
|
+
* Per EJECTED generator that hands over adapter source, the project's copies of the files
|
|
255
|
+
* it reaches. Only generators the project owns a copy of: `routes` and `routes-hono`
|
|
256
|
+
* reach overlapping adapter files, so attributing by reach alone reported "owned runtime"
|
|
257
|
+
* on `routes-hono` in a project that had only ever ejected `routes`.
|
|
258
|
+
*/
|
|
259
|
+
async function ownedRuntimeByGenerator(
|
|
260
|
+
projectRoot: string,
|
|
261
|
+
ownedNames: ReadonlySet<string>,
|
|
262
|
+
): Promise<Map<string, { files: RuntimeCopyRow[]; packages: string[] }>> {
|
|
263
|
+
const out = new Map<string, { files: RuntimeCopyRow[]; packages: string[] }>();
|
|
264
|
+
const status = await runtimeCopyStatus(projectRoot);
|
|
265
|
+
if (status.length === 0) return out;
|
|
266
|
+
const { srcRoot } = resolveRuntimePackage();
|
|
267
|
+
for (const [name, modules] of Object.entries(RUNTIME_ENTRIES)) {
|
|
268
|
+
if (!ownedNames.has(name)) continue;
|
|
269
|
+
const closure = runtimeClosure(srcRoot, modules);
|
|
270
|
+
const reached = new Set(closure.files.map((f) => `${OWNED_RUNTIME_DIR}/${f}`));
|
|
271
|
+
const files = status.filter((r) => reached.has(r.path));
|
|
272
|
+
if (files.length > 0) out.set(name, { files, packages: closure.packages });
|
|
273
|
+
}
|
|
274
|
+
return out;
|
|
275
|
+
}
|
|
276
|
+
|
|
240
277
|
export async function buildCatalogListing(opts: CatalogListingOpts = {}): Promise<CatalogRow[]> {
|
|
241
278
|
const rows: CatalogRow[] = [];
|
|
279
|
+
const runtimeCopies = opts.project === undefined
|
|
280
|
+
? new Map<string, { files: RuntimeCopyRow[]; packages: string[] }>()
|
|
281
|
+
: await ownedRuntimeByGenerator(opts.project.projectRoot, opts.project.ownedNames);
|
|
242
282
|
|
|
243
283
|
for (const entry of listCatalog()) {
|
|
244
284
|
const template = entry.ejectable ? readTemplate(entry.name) : undefined;
|
|
245
|
-
|
|
285
|
+
// A project that owns this generator's adapter copy installs what the COPY imports,
|
|
286
|
+
// not the runtime package — the same trade `meta eject` prints.
|
|
287
|
+
const owned = runtimeCopies.get(entry.name);
|
|
288
|
+
const install = owned === undefined
|
|
289
|
+
? installSetFor([entry])
|
|
290
|
+
: installSetFor([entry], [], { generators: new Set([entry.name]), ...runtimeInstall(owned.packages) });
|
|
246
291
|
const pkg = packageOf(entry.name) ?? "";
|
|
247
292
|
|
|
248
293
|
const row: GeneratorCatalogRow = {
|
|
@@ -266,6 +311,7 @@ export async function buildCatalogListing(opts: CatalogListingOpts = {}): Promis
|
|
|
266
311
|
kind: entry.ejectable ? "reference-template" : "package-only",
|
|
267
312
|
ejectable: entry.ejectable,
|
|
268
313
|
owned: opts.project === undefined ? null : opts.project.ownedNames.has(entry.name),
|
|
314
|
+
...(owned !== undefined ? { runtimeCopy: owned.files } : {}),
|
|
269
315
|
},
|
|
270
316
|
};
|
|
271
317
|
|
|
@@ -324,16 +370,22 @@ export async function buildCatalogListing(opts: CatalogListingOpts = {}): Promis
|
|
|
324
370
|
* The stable names wired in a config.
|
|
325
371
|
*
|
|
326
372
|
* `generators: [...]` holds constructed Generator objects (and, since ADR-0021 #1,
|
|
327
|
-
* bare stable-name strings). A constructed generator
|
|
328
|
-
*
|
|
329
|
-
*
|
|
330
|
-
*
|
|
373
|
+
* bare stable-name strings). A constructed generator calls itself by its IMPLEMENTATION
|
|
374
|
+
* name (`entity-file`, `routes-file`), which is not the catalog key (`entity`,
|
|
375
|
+
* `routes`) — so an owned copy wired from `./codegen/generators/entity` used to read as
|
|
376
|
+
* not wired while `barrel` and `names`, whose two names coincide, did. Resolved through
|
|
377
|
+
* `stableNameIndex`, the same impl-name → stable-name map the `requires` gate uses. A
|
|
378
|
+
* generator whose name maps to no catalog entry is somebody's own and contributes nothing.
|
|
331
379
|
*/
|
|
332
380
|
export function wiredGeneratorNames(config: MetaobjectsGenConfig): Set<string> {
|
|
381
|
+
const index = stableNameIndex(composeCatalog());
|
|
333
382
|
const names = new Set<string>();
|
|
334
383
|
for (const spec of config.generators ?? []) {
|
|
335
384
|
if (typeof spec === "string") names.add(spec);
|
|
336
|
-
else if (typeof spec?.name === "string")
|
|
385
|
+
else if (typeof spec?.name === "string") {
|
|
386
|
+
const stable = index.get(spec.name);
|
|
387
|
+
if (stable !== undefined) names.add(stable);
|
|
388
|
+
}
|
|
337
389
|
}
|
|
338
390
|
return names;
|
|
339
391
|
}
|
|
@@ -408,6 +460,15 @@ export function renderCatalogText(rows: CatalogRow[], probed: boolean): string {
|
|
|
408
460
|
if (!r.source.ejectable) marks.push("package-only");
|
|
409
461
|
if (r.project?.wired) marks.push("WIRED");
|
|
410
462
|
if (r.source.owned) marks.push("owned");
|
|
463
|
+
if (r.source.runtimeCopy !== undefined) {
|
|
464
|
+
// The adapter source this generator's output imports, compared to the package.
|
|
465
|
+
const differing = r.source.runtimeCopy.filter((c) => c.verdict === "differs").length;
|
|
466
|
+
marks.push(
|
|
467
|
+
differing === 0
|
|
468
|
+
? "owned runtime: identical to the package"
|
|
469
|
+
: `owned runtime: ${differing} file(s) DIFFER from the package — meta eject --list`,
|
|
470
|
+
);
|
|
471
|
+
}
|
|
411
472
|
if (probed && r.project?.wouldEmit !== null && r.project?.wouldEmit !== undefined) {
|
|
412
473
|
const needs = r.project.needsConfig;
|
|
413
474
|
marks.push(
|
package/src/lib/install-set.ts
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
import { createRequire } from "node:module";
|
|
21
21
|
import { existsSync, readFileSync } from "node:fs";
|
|
22
22
|
import { dirname, join } from "node:path";
|
|
23
|
-
import type
|
|
23
|
+
import { HTTP_RUNTIME_PACKAGE, type GeneratorRegistryEntry } from "@metaobjectsdev/codegen-ts";
|
|
24
24
|
import { cliVersion } from "./version.js";
|
|
25
25
|
import { packageOf } from "./catalog.js";
|
|
26
26
|
import type { PackageManifest } from "./package-manifest.js";
|
|
@@ -63,6 +63,13 @@ export function peerRangesOf(packageName: string): Record<string, string> {
|
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
/** What an owned adapter copy changes about the install set — see `installSetFor`. */
|
|
67
|
+
export interface OwnedRuntimeInstall {
|
|
68
|
+
generators: ReadonlySet<string>;
|
|
69
|
+
runtime: ReadonlyMap<string, string | undefined>;
|
|
70
|
+
dev: ReadonlyMap<string, string | undefined>;
|
|
71
|
+
}
|
|
72
|
+
|
|
66
73
|
export interface InstallSet {
|
|
67
74
|
/** Build-time packages, `name@range`. */
|
|
68
75
|
dev: string[];
|
|
@@ -114,6 +121,10 @@ export function installSetFor(
|
|
|
114
121
|
* catalog does not know them). Build-time, so dev — unless the runtime set already
|
|
115
122
|
* installs the package, which satisfies the build too. */
|
|
116
123
|
templatePackages: readonly string[] = [],
|
|
124
|
+
/** ADR-0034 Amendment 3 (2026-09-24): generators whose adapter SOURCE was copied into the
|
|
125
|
+
* project, and what that copy imports. Their output no longer imports the runtime package,
|
|
126
|
+
* so it is not installed on their account; the copy's own imports are, instead. */
|
|
127
|
+
ownedRuntime?: OwnedRuntimeInstall,
|
|
117
128
|
): InstallSet {
|
|
118
129
|
const version = cliVersion();
|
|
119
130
|
const dev = new Map<string, string | undefined>();
|
|
@@ -123,7 +134,10 @@ export function installSetFor(
|
|
|
123
134
|
const pkg = packageOf(entry.name);
|
|
124
135
|
if (pkg !== undefined) addPackage(dev, pkg, `^${version}`);
|
|
125
136
|
|
|
126
|
-
for (const rt of entry.runtimePackages ?? [])
|
|
137
|
+
for (const rt of entry.runtimePackages ?? []) {
|
|
138
|
+
if (ownedRuntime?.generators.has(entry.name) && rt === HTTP_RUNTIME_PACKAGE) continue;
|
|
139
|
+
addPackage(runtime, rt, `^${version}`);
|
|
140
|
+
}
|
|
127
141
|
|
|
128
142
|
if (entry.runtimePeers !== undefined && entry.runtimePeers.length > 0) {
|
|
129
143
|
// Ranges come from whichever runtime package declares these as peers — the union
|
|
@@ -137,6 +151,9 @@ export function installSetFor(
|
|
|
137
151
|
}
|
|
138
152
|
}
|
|
139
153
|
|
|
154
|
+
for (const [name, range] of ownedRuntime?.runtime ?? []) addPackage(runtime, name, range);
|
|
155
|
+
for (const [name, range] of ownedRuntime?.dev ?? []) addPackage(dev, name, range);
|
|
156
|
+
|
|
140
157
|
for (const p of templatePackages) {
|
|
141
158
|
if (!runtime.has(p)) addPackage(dev, p, `^${version}`);
|
|
142
159
|
}
|
package/src/lib/output.ts
CHANGED
|
@@ -261,6 +261,7 @@ export function genResultToData(result: GenResultShape): {
|
|
|
261
261
|
summary: string;
|
|
262
262
|
help: string[];
|
|
263
263
|
antiPatterns: AdvisorySection<AdvisoryFindingRow>;
|
|
264
|
+
warnings?: string[];
|
|
264
265
|
} {
|
|
265
266
|
const counts = result.files.reduce<Record<GenFileStatus, number>>(
|
|
266
267
|
(a, f) => ((a[f.status] = (a[f.status] ?? 0) + 1), a),
|
|
@@ -308,6 +309,8 @@ export function genResultToData(result: GenResultShape): {
|
|
|
308
309
|
summary,
|
|
309
310
|
help,
|
|
310
311
|
antiPatterns,
|
|
312
|
+
// Present only when there is something to say, so a clean run's payload is unchanged.
|
|
313
|
+
...(result.warnings.length > 0 ? { warnings: result.warnings } : {}),
|
|
311
314
|
};
|
|
312
315
|
}
|
|
313
316
|
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
// ADR-0034 Amendment 3 (2026-09-24 ruling) — ejecting a helper hands over ALL of its code.
|
|
2
|
+
//
|
|
3
|
+
// `meta eject routes` used to copy a generator whose OUTPUT still imported the HTTP adapter
|
|
4
|
+
// from `@metaobjectsdev/runtime-ts` — the mount helpers, filter parser, error envelopes and
|
|
5
|
+
// pagination, which is where most route defects have lived. Owning the generator while the
|
|
6
|
+
// code it emits calls into a package is half of owning it: a defect in the adapter still
|
|
7
|
+
// waits on an upstream release.
|
|
8
|
+
//
|
|
9
|
+
// So eject also copies the adapter SOURCE the output depends on — the transitive closure of
|
|
10
|
+
// relative imports from the entry module each generator's output names — into
|
|
11
|
+
// `codegen/runtime/`, laid out exactly as the package's own `src/` so every relative import
|
|
12
|
+
// in it resolves unchanged. Package imports are left as they are: what the closure reaches
|
|
13
|
+
// outside the package is `@metaobjectsdev/metadata` (core: the filter-op vocabulary and the
|
|
14
|
+
// canonical serializer) and third-party libraries (drizzle-orm, fastify, hono, zod, qs).
|
|
15
|
+
//
|
|
16
|
+
// The copies are VERBATIM. That is what makes "is my copy still the package's?" a byte
|
|
17
|
+
// comparison and "what did upstream change?" a plain `diff -r` against the installed
|
|
18
|
+
// package's `src/`, with no header to strip first.
|
|
19
|
+
|
|
20
|
+
import { createRequire } from "node:module";
|
|
21
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
22
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
23
|
+
import { dirname, join, relative, sep } from "node:path";
|
|
24
|
+
import {
|
|
25
|
+
HTTP_RUNTIME_MODULES,
|
|
26
|
+
HTTP_RUNTIME_PACKAGE,
|
|
27
|
+
OWNED_RUNTIME_DIR,
|
|
28
|
+
type HttpRuntimeModule,
|
|
29
|
+
} from "@metaobjectsdev/codegen-ts";
|
|
30
|
+
import { compareOwnedCopy, type OwnedComparison } from "./owned-copy.js";
|
|
31
|
+
import { cliVersion } from "./version.js";
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The ejectable generators whose OUTPUT imports the adapter tier, and the adapter modules
|
|
35
|
+
* it imports. `entity` is here for its allowlist TYPES only, which is one file.
|
|
36
|
+
*/
|
|
37
|
+
export const RUNTIME_ENTRIES: Readonly<Record<string, readonly HttpRuntimeModule[]>> = {
|
|
38
|
+
routes: ["drizzle-fastify"],
|
|
39
|
+
"routes-hono": ["hono"],
|
|
40
|
+
entity: ["allowlists"],
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/** True iff ejecting `name` also copies adapter source. */
|
|
44
|
+
export function ejectsRuntime(name: string): boolean {
|
|
45
|
+
return Object.hasOwn(RUNTIME_ENTRIES, name);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface RuntimePackage {
|
|
49
|
+
/** `<root>/src` — the source the copies are taken from and compared against. */
|
|
50
|
+
srcRoot: string;
|
|
51
|
+
manifest: {
|
|
52
|
+
dependencies?: Record<string, string>;
|
|
53
|
+
peerDependencies?: Record<string, string>;
|
|
54
|
+
devDependencies?: Record<string, string>;
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The installed `@metaobjectsdev/runtime-ts`: its `src/` and its manifest.
|
|
60
|
+
*
|
|
61
|
+
* Resolves the package ENTRY and walks up to the manifest that names it, never
|
|
62
|
+
* `"<pkg>/package.json"`: the exports map does not export that subpath, and under real Node
|
|
63
|
+
* asking for it throws ERR_PACKAGE_PATH_NOT_EXPORTED (see `install-set.ts`). `src/` ships
|
|
64
|
+
* in the published package (`files: ["dist", "src", ...]`).
|
|
65
|
+
*/
|
|
66
|
+
export function resolveRuntimePackage(): RuntimePackage {
|
|
67
|
+
const req = createRequire(import.meta.url);
|
|
68
|
+
let dir = dirname(req.resolve(HTTP_RUNTIME_PACKAGE));
|
|
69
|
+
for (let hops = 0; hops < 8; hops++) {
|
|
70
|
+
const candidate = join(dir, "package.json");
|
|
71
|
+
if (existsSync(candidate)) {
|
|
72
|
+
const manifest = JSON.parse(readFileSync(candidate, "utf8")) as RuntimePackage["manifest"] & { name?: string };
|
|
73
|
+
if (manifest.name === HTTP_RUNTIME_PACKAGE) {
|
|
74
|
+
const srcRoot = join(dir, "src");
|
|
75
|
+
if (!existsSync(srcRoot)) {
|
|
76
|
+
throw new Error(`${HTTP_RUNTIME_PACKAGE} at ${dir} ships no src/ — cannot copy the adapter source.`);
|
|
77
|
+
}
|
|
78
|
+
return { srcRoot, manifest };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const up = dirname(dir);
|
|
82
|
+
if (up === dir) break;
|
|
83
|
+
dir = up;
|
|
84
|
+
}
|
|
85
|
+
throw new Error(`could not locate the installed ${HTTP_RUNTIME_PACKAGE} package.`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Relative (`./`, `../`) and bare module specifiers a source file imports or re-exports. */
|
|
89
|
+
function specifiersOf(source: string): string[] {
|
|
90
|
+
const out: string[] = [];
|
|
91
|
+
// `import … from "x"` and the re-export forms (`export { … } from`, `export * from`) only —
|
|
92
|
+
// a bare `export function f() { return Array.from("…") }` must not read as an import.
|
|
93
|
+
const STATEMENT =
|
|
94
|
+
/(?:^|\n)\s*(?:import\s+[^;"']*?\s+from|export\s+(?:type\s+)?(?:\*(?:\s+as\s+\w+)?|\{[^}]*\})\s*from)\s*["']([^"']+)["']/g;
|
|
95
|
+
for (const m of source.matchAll(STATEMENT)) {
|
|
96
|
+
if (m[1] !== undefined) out.push(m[1]);
|
|
97
|
+
}
|
|
98
|
+
for (const m of source.matchAll(/(?:^|\n)\s*import\s*["']([^"']+)["']/g)) {
|
|
99
|
+
if (m[1] !== undefined) out.push(m[1]);
|
|
100
|
+
}
|
|
101
|
+
return out;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** `@scope/name/sub` → `@scope/name`; `name/sub` → `name`. */
|
|
105
|
+
function packageNameOf(spec: string): string {
|
|
106
|
+
const parts = spec.split("/");
|
|
107
|
+
return spec.startsWith("@") ? parts.slice(0, 2).join("/") : parts[0]!;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function toPosix(p: string): string {
|
|
111
|
+
return p.split(sep).join("/");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export interface RuntimeClosure {
|
|
115
|
+
/** Source files, POSIX paths relative to the package's `src/`, sorted. */
|
|
116
|
+
files: string[];
|
|
117
|
+
/** Packages the closure imports (never `node:` builtins), sorted. */
|
|
118
|
+
packages: string[];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Every file the given adapter modules reach through relative imports, and what else they import. */
|
|
122
|
+
export function runtimeClosure(srcRoot: string, modules: readonly HttpRuntimeModule[]): RuntimeClosure {
|
|
123
|
+
const files = new Set<string>();
|
|
124
|
+
const packages = new Set<string>();
|
|
125
|
+
const queue = modules.map((m) => `${HTTP_RUNTIME_MODULES[m].file}.ts`);
|
|
126
|
+
while (queue.length > 0) {
|
|
127
|
+
const rel = queue.shift()!;
|
|
128
|
+
if (files.has(rel)) continue;
|
|
129
|
+
const abs = join(srcRoot, rel);
|
|
130
|
+
if (!existsSync(abs)) {
|
|
131
|
+
throw new Error(`${HTTP_RUNTIME_PACKAGE}: ${rel} is imported but not shipped in src/.`);
|
|
132
|
+
}
|
|
133
|
+
files.add(rel);
|
|
134
|
+
for (const spec of specifiersOf(readFileSync(abs, "utf8"))) {
|
|
135
|
+
if (spec.startsWith("./") || spec.startsWith("../")) {
|
|
136
|
+
const target = toPosix(join(dirname(rel), spec)).replace(/\.js$/, ".ts");
|
|
137
|
+
queue.push(target.endsWith(".ts") ? target : `${target}.ts`);
|
|
138
|
+
} else if (!spec.startsWith("node:")) {
|
|
139
|
+
packages.add(packageNameOf(spec));
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return { files: [...files].sort(), packages: [...packages].sort() };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export type RuntimeFileStatus = "created" | "preserved" | "replaced";
|
|
147
|
+
|
|
148
|
+
export interface RuntimeFileResult {
|
|
149
|
+
/** Project-relative path of the copy. */
|
|
150
|
+
path: string;
|
|
151
|
+
status: RuntimeFileStatus;
|
|
152
|
+
/** How the copy ALREADY on disk compared to the package's source. Absent when there was none. */
|
|
153
|
+
comparison?: OwnedComparison | undefined;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export interface RuntimeEjectResult {
|
|
157
|
+
files: RuntimeFileResult[];
|
|
158
|
+
/** Packages the copied source imports — what the adopter now installs directly. */
|
|
159
|
+
packages: string[];
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Copy the adapter source `names` emit imports of into `<cwd>/codegen/runtime/`.
|
|
164
|
+
* Never overwrites a file without `force`, the same rule the generator copy keeps.
|
|
165
|
+
*/
|
|
166
|
+
export async function ejectRuntime(cwd: string, names: readonly string[], force: boolean): Promise<RuntimeEjectResult> {
|
|
167
|
+
const modules = [...new Set(names.flatMap((n) => RUNTIME_ENTRIES[n] ?? []))];
|
|
168
|
+
if (modules.length === 0) return { files: [], packages: [] };
|
|
169
|
+
const { srcRoot } = resolveRuntimePackage();
|
|
170
|
+
const closure = runtimeClosure(srcRoot, modules);
|
|
171
|
+
const files: RuntimeFileResult[] = [];
|
|
172
|
+
for (const rel of closure.files) {
|
|
173
|
+
const source = await readFile(join(srcRoot, rel), "utf8");
|
|
174
|
+
const path = `${OWNED_RUNTIME_DIR}/${rel}`;
|
|
175
|
+
const abs = join(cwd, path);
|
|
176
|
+
const existing = existsSync(abs) ? await readFile(abs, "utf8") : undefined;
|
|
177
|
+
const comparison = existing === undefined ? undefined : await compareOwnedCopy(existing, source);
|
|
178
|
+
if (existing !== undefined && !force) {
|
|
179
|
+
files.push({ path, status: "preserved", comparison });
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
await mkdir(dirname(abs), { recursive: true });
|
|
183
|
+
await writeFile(abs, source, "utf8");
|
|
184
|
+
files.push({ path, status: existing === undefined ? "created" : "replaced", comparison });
|
|
185
|
+
}
|
|
186
|
+
return { files, packages: closure.packages };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* The install lines the copied source needs, with ranges read from the runtime package's
|
|
191
|
+
* own manifest (never restated here). A `@metaobjectsdev/*` package moves with the CLI.
|
|
192
|
+
* A package that ships no types of its own gets its `@types/*` sibling as a dev dependency
|
|
193
|
+
* when the runtime package itself develops against one.
|
|
194
|
+
*/
|
|
195
|
+
export function runtimeInstall(packages: readonly string[]): {
|
|
196
|
+
runtime: Map<string, string | undefined>;
|
|
197
|
+
dev: Map<string, string | undefined>;
|
|
198
|
+
} {
|
|
199
|
+
const { manifest } = resolveRuntimePackage();
|
|
200
|
+
const known = { ...manifest.peerDependencies, ...manifest.dependencies };
|
|
201
|
+
const runtime = new Map<string, string | undefined>();
|
|
202
|
+
const dev = new Map<string, string | undefined>();
|
|
203
|
+
for (const p of packages) {
|
|
204
|
+
runtime.set(p, p.startsWith("@metaobjectsdev/") ? `^${cliVersion()}` : known[p]);
|
|
205
|
+
const typesPkg = `@types/${p.replace(/^@/, "").replace("/", "__")}`;
|
|
206
|
+
const typesRange = manifest.devDependencies?.[typesPkg];
|
|
207
|
+
if (typesRange !== undefined) dev.set(typesPkg, typesRange);
|
|
208
|
+
}
|
|
209
|
+
return { runtime, dev };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** One owned adapter file, compared to the package it came from. */
|
|
213
|
+
export interface RuntimeCopyRow {
|
|
214
|
+
path: string;
|
|
215
|
+
verdict: OwnedComparison["verdict"] | "not-in-package";
|
|
216
|
+
localOnly: number;
|
|
217
|
+
referenceOnly: number;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function listTs(dir: string): string[] {
|
|
221
|
+
const out: string[] = [];
|
|
222
|
+
for (const entry of readdirSync(dir)) {
|
|
223
|
+
const abs = join(dir, entry);
|
|
224
|
+
if (statSync(abs).isDirectory()) out.push(...listTs(abs));
|
|
225
|
+
else if (entry.endsWith(".ts")) out.push(abs);
|
|
226
|
+
}
|
|
227
|
+
return out;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Every file under `<cwd>/codegen/runtime/`, compared to the installed package's source.
|
|
232
|
+
* Empty when the project owns no adapter copy. `not-in-package` is a file the package does
|
|
233
|
+
* not ship — your own addition, or one upstream deleted or renamed.
|
|
234
|
+
*/
|
|
235
|
+
export async function runtimeCopyStatus(cwd: string): Promise<RuntimeCopyRow[]> {
|
|
236
|
+
const root = join(cwd, OWNED_RUNTIME_DIR);
|
|
237
|
+
if (!existsSync(root)) return [];
|
|
238
|
+
let srcRoot: string;
|
|
239
|
+
try {
|
|
240
|
+
srcRoot = resolveRuntimePackage().srcRoot;
|
|
241
|
+
} catch {
|
|
242
|
+
return [];
|
|
243
|
+
}
|
|
244
|
+
const rows: RuntimeCopyRow[] = [];
|
|
245
|
+
for (const abs of listTs(root).sort()) {
|
|
246
|
+
const rel = toPosix(relative(root, abs));
|
|
247
|
+
const path = `${OWNED_RUNTIME_DIR}/${rel}`;
|
|
248
|
+
const upstream = join(srcRoot, rel);
|
|
249
|
+
if (!existsSync(upstream)) {
|
|
250
|
+
rows.push({ path, verdict: "not-in-package", localOnly: 0, referenceOnly: 0 });
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
const cmp = await compareOwnedCopy(readFileSync(abs, "utf8"), readFileSync(upstream, "utf8"));
|
|
254
|
+
rows.push({ path, verdict: cmp.verdict, localOnly: cmp.localOnly, referenceOnly: cmp.referenceOnly });
|
|
255
|
+
}
|
|
256
|
+
return rows;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** The command that shows exactly how one owned file differs from the installed package's. */
|
|
260
|
+
export function runtimeDiffCommand(path: string): string {
|
|
261
|
+
const rel = path.startsWith(`${OWNED_RUNTIME_DIR}/`) ? path.slice(OWNED_RUNTIME_DIR.length + 1) : path;
|
|
262
|
+
return `diff -u node_modules/${HTTP_RUNTIME_PACKAGE}/src/${rel} ${OWNED_RUNTIME_DIR}/${rel}`;
|
|
263
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { relative, isAbsolute } from "node:path";
|
|
2
|
+
import { unknownAttributeFindings, type LoadMemoryOptions } from "@metaobjectsdev/sdk";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The trailing advice on every unknown-attribute warning `meta gen` prints. `gen` has no
|
|
6
|
+
* `--strict` flag of its own — `meta verify` is the strict door (ADR-0023), so that is what
|
|
7
|
+
* the warning names.
|
|
8
|
+
*/
|
|
9
|
+
export const UNKNOWN_ATTR_GEN_ADVICE =
|
|
10
|
+
"meta gen loads leniently and generated anyway, but `meta verify` rejects this metadata " +
|
|
11
|
+
"(ADR-0023): fix or remove the attribute — a typo'd one (`isAbstrakt`, `@requird`) " +
|
|
12
|
+
"silently changes what is generated.";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* `meta gen`'s unknown-attribute warnings: one line per ADR-0023 finding a STRICT load of
|
|
16
|
+
* the same collection raises, naming the attribute, the node and the file. Empty when the
|
|
17
|
+
* model is clean; the caller prints {@link UNKNOWN_ATTR_GEN_ADVICE} once after them.
|
|
18
|
+
*
|
|
19
|
+
* `gen` itself keeps loading leniently and keeps its exit code; this only ends the silence
|
|
20
|
+
* between it and `meta verify`, which fails on the very same file. Files are printed
|
|
21
|
+
* relative to `projectRoot` when the loader recorded an absolute path.
|
|
22
|
+
*/
|
|
23
|
+
export async function unknownAttrWarnings(
|
|
24
|
+
configDir: string,
|
|
25
|
+
options: LoadMemoryOptions,
|
|
26
|
+
projectRoot: string,
|
|
27
|
+
): Promise<string[]> {
|
|
28
|
+
let findings;
|
|
29
|
+
try {
|
|
30
|
+
findings = await unknownAttributeFindings(configDir, options);
|
|
31
|
+
} catch {
|
|
32
|
+
// The lenient load already succeeded; a strict re-load that cannot even complete has
|
|
33
|
+
// nothing more precise to say, and must never turn an advisory into a failure.
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
return findings.map((f) => {
|
|
37
|
+
const source = f.source as { files?: readonly string[]; jsonPath?: string };
|
|
38
|
+
const files = (source.files ?? [])
|
|
39
|
+
.filter((p) => p.length > 0)
|
|
40
|
+
.map((p) => (isAbsolute(p) ? relative(projectRoot, p) : p));
|
|
41
|
+
const where = [
|
|
42
|
+
files.length > 0 ? files.join(", ") : undefined,
|
|
43
|
+
source.jsonPath !== undefined && source.jsonPath.length > 0 ? `at ${source.jsonPath}` : undefined,
|
|
44
|
+
].filter((w): w is string => w !== undefined).join(" ");
|
|
45
|
+
const head = `${f.code}: ${f.message}`;
|
|
46
|
+
return where.length > 0 ? `${head}\n in ${where}` : head;
|
|
47
|
+
});
|
|
48
|
+
}
|