@metaobjectsdev/cli 1.0.10 → 1.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/dist/src/commands/eject.d.ts.map +1 -1
  2. package/dist/src/commands/eject.js +5 -1
  3. package/dist/src/commands/eject.js.map +1 -1
  4. package/dist/src/commands/gen.d.ts.map +1 -1
  5. package/dist/src/commands/gen.js +7 -0
  6. package/dist/src/commands/gen.js.map +1 -1
  7. package/dist/src/commands/migrate.d.ts.map +1 -1
  8. package/dist/src/commands/migrate.js +1 -0
  9. package/dist/src/commands/migrate.js.map +1 -1
  10. package/dist/src/commands/verify.d.ts.map +1 -1
  11. package/dist/src/commands/verify.js +38 -1
  12. package/dist/src/commands/verify.js.map +1 -1
  13. package/dist/src/index.d.ts.map +1 -1
  14. package/dist/src/index.js +11 -0
  15. package/dist/src/index.js.map +1 -1
  16. package/dist/src/lib/args.d.ts +11 -0
  17. package/dist/src/lib/args.d.ts.map +1 -1
  18. package/dist/src/lib/args.js +2 -0
  19. package/dist/src/lib/args.js.map +1 -1
  20. package/dist/src/lib/fk-index-advisory.d.ts.map +1 -1
  21. package/dist/src/lib/fk-index-advisory.js +4 -1
  22. package/dist/src/lib/fk-index-advisory.js.map +1 -1
  23. package/dist/src/lib/name-lint.d.ts +12 -0
  24. package/dist/src/lib/name-lint.d.ts.map +1 -0
  25. package/dist/src/lib/name-lint.js +95 -0
  26. package/dist/src/lib/name-lint.js.map +1 -0
  27. package/dist/src/lib/runtime-boundary-advisory.d.ts +24 -0
  28. package/dist/src/lib/runtime-boundary-advisory.d.ts.map +1 -0
  29. package/dist/src/lib/runtime-boundary-advisory.js +47 -0
  30. package/dist/src/lib/runtime-boundary-advisory.js.map +1 -0
  31. package/package.json +10 -10
  32. package/src/commands/eject.ts +6 -1
  33. package/src/commands/gen.ts +9 -0
  34. package/src/commands/migrate.ts +1 -0
  35. package/src/commands/verify.ts +46 -1
  36. package/src/index.ts +11 -0
  37. package/src/lib/args.ts +9 -0
  38. package/src/lib/fk-index-advisory.ts +4 -1
  39. package/src/lib/name-lint.ts +106 -0
  40. package/src/lib/runtime-boundary-advisory.ts +74 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"name-lint.js","sourceRoot":"","sources":["../../../src/lib/name-lint.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Generated code that imports the owned HTTP-adapter copy from OUTSIDE its own
3
+ * workspace package.
4
+ *
5
+ * Since 1.0.9 an ejected `entity` / `routes` / `routes-hono` generator points its output
6
+ * at `codegen/runtime/` (the adapter source `meta eject` copied) by a path relative to
7
+ * the output directory. In a single-package project that is exactly right. In a monorepo
8
+ * whose output sits inside a workspace package (`apps/api/src/generated`), the path climbs
9
+ * out of that package: `tsc` rejects the file (`TS6059 … is not under 'rootDir'`), and the
10
+ * copy's own bare imports (`qs`, `fastify`) do not resolve from the repository root. The
11
+ * `meta gen` run itself is green; the adopter's build is the first thing that disagrees.
12
+ *
13
+ * Found upgrading a pnpm monorepo. This names the package and the two ways out. It is a
14
+ * warning and never changes an exit code.
15
+ */
16
+ export interface RuntimeBoundaryFinding {
17
+ /** Project-relative directory of the workspace package the generated file sits in. */
18
+ packageDir: string;
19
+ /** One generated file (project-relative) that crosses the boundary. */
20
+ example: string;
21
+ }
22
+ export declare function findRuntimeBoundaryCrossings(projectRoot: string, generatedFiles: readonly string[]): RuntimeBoundaryFinding[];
23
+ export declare function runtimeBoundaryWarnings(findings: readonly RuntimeBoundaryFinding[]): string[];
24
+ //# sourceMappingURL=runtime-boundary-advisory.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runtime-boundary-advisory.d.ts","sourceRoot":"","sources":["../../../src/lib/runtime-boundary-advisory.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;GAcG;AACH,MAAM,WAAW,sBAAsB;IACrC,sFAAsF;IACtF,UAAU,EAAE,MAAM,CAAC;IACnB,uEAAuE;IACvE,OAAO,EAAE,MAAM,CAAC;CACjB;AAeD,wBAAgB,4BAA4B,CAC1C,WAAW,EAAE,MAAM,EACnB,cAAc,EAAE,SAAS,MAAM,EAAE,GAChC,sBAAsB,EAAE,CAqB1B;AAED,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,SAAS,sBAAsB,EAAE,GAAG,MAAM,EAAE,CAQ7F"}
@@ -0,0 +1,47 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { dirname, join, relative, resolve, sep } from "node:path";
3
+ import { OWNED_RUNTIME_DIR, HTTP_RUNTIME_PACKAGE } from "@metaobjectsdev/codegen-ts";
4
+ const IMPORT_SPEC = /\bfrom\s+["'](\.{1,2}\/[^"']+)["']/g;
5
+ function nearestPackageDir(fileDir, projectRoot) {
6
+ for (let d = fileDir;; d = dirname(d)) {
7
+ if (d === projectRoot || !d.startsWith(projectRoot + sep))
8
+ return undefined;
9
+ if (existsSync(join(d, "package.json")))
10
+ return d;
11
+ }
12
+ }
13
+ function isInside(child, parent) {
14
+ return child === parent || child.startsWith(parent + sep);
15
+ }
16
+ export function findRuntimeBoundaryCrossings(projectRoot, generatedFiles) {
17
+ const root = resolve(projectRoot);
18
+ const runtimeRoot = join(root, OWNED_RUNTIME_DIR);
19
+ const byPackage = new Map();
20
+ for (const file of generatedFiles) {
21
+ if (!/\.(ts|tsx|mts)$/.test(file) || !existsSync(file))
22
+ continue;
23
+ const abs = resolve(file);
24
+ const pkgDir = nearestPackageDir(dirname(abs), root);
25
+ if (pkgDir === undefined || isInside(runtimeRoot, pkgDir) || byPackage.has(pkgDir))
26
+ continue;
27
+ const text = readFileSync(abs, "utf8");
28
+ for (const m of text.matchAll(IMPORT_SPEC)) {
29
+ if (isInside(resolve(dirname(abs), m[1]), runtimeRoot)) {
30
+ byPackage.set(pkgDir, {
31
+ packageDir: relative(root, pkgDir).split(sep).join("/"),
32
+ example: relative(root, abs).split(sep).join("/"),
33
+ });
34
+ break;
35
+ }
36
+ }
37
+ }
38
+ return [...byPackage.values()];
39
+ }
40
+ export function runtimeBoundaryWarnings(findings) {
41
+ return findings.map((f) => `generated code in ${f.packageDir}/ imports the owned HTTP adapter from ${OWNED_RUNTIME_DIR}/, ` +
42
+ `outside that workspace package (e.g. ${f.example}); its build will not resolve it. ` +
43
+ `Either keep importing the published adapter — pass \`runtimeImport: "${HTTP_RUNTIME_PACKAGE}"\` ` +
44
+ `to the ejected entity/routes generators — or move ${OWNED_RUNTIME_DIR}/ inside ${f.packageDir}/ ` +
45
+ `and point \`runtimeImport\` at it.`);
46
+ }
47
+ //# sourceMappingURL=runtime-boundary-advisory.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runtime-boundary-advisory.js","sourceRoot":"","sources":["../../../src/lib/runtime-boundary-advisory.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.10",
3
+ "version": "1.0.11",
4
4
  "description": "CLI for MetaObjects: scaffold, codegen, migrate, and drift-detection commands.",
5
5
  "type": "module",
6
6
  "main": "./dist/src/index.js",
@@ -51,15 +51,15 @@
51
51
  "@biomejs/js-api": "^0.7.0",
52
52
  "@biomejs/wasm-nodejs": "^1.9.4",
53
53
  "@libsql/kysely-libsql": "^0.4.0",
54
- "@metaobjectsdev/codegen-ts": "1.0.10",
55
- "@metaobjectsdev/codegen-ts-react": "1.0.10",
56
- "@metaobjectsdev/codegen-ts-tanstack": "1.0.10",
57
- "@metaobjectsdev/docs-site": "1.0.10",
58
- "@metaobjectsdev/metadata": "1.0.10",
59
- "@metaobjectsdev/migrate-ts": "1.0.10",
60
- "@metaobjectsdev/render": "1.0.10",
61
- "@metaobjectsdev/runtime-ts": "1.0.10",
62
- "@metaobjectsdev/sdk": "1.0.10",
54
+ "@metaobjectsdev/codegen-ts": "1.0.11",
55
+ "@metaobjectsdev/codegen-ts-react": "1.0.11",
56
+ "@metaobjectsdev/codegen-ts-tanstack": "1.0.11",
57
+ "@metaobjectsdev/docs-site": "1.0.11",
58
+ "@metaobjectsdev/metadata": "1.0.11",
59
+ "@metaobjectsdev/migrate-ts": "1.0.11",
60
+ "@metaobjectsdev/render": "1.0.11",
61
+ "@metaobjectsdev/runtime-ts": "1.0.11",
62
+ "@metaobjectsdev/sdk": "1.0.11",
63
63
  "@toon-format/toon": "^2.3.0",
64
64
  "jiti": "^2.4.0"
65
65
  },
@@ -256,7 +256,12 @@ export async function ejectGenerator(opts: EjectOptions): Promise<EjectResult> {
256
256
  // The adapter source the OUTPUT imports is part of what is handed over — copied whether
257
257
  // or not the generator file itself was already here, and under the same never-clobber
258
258
  // rule, so re-running eject fills in a missing copy without touching a kept one.
259
- const runtime = ejectsRuntime(opts.name)
259
+ // Except when the KEPT copy predates that hand-over (1.0.9): with no `runtimeImport`
260
+ // option its output imports the published package, so the copy would be dead code —
261
+ // a Drizzle adapter, and its dependencies, in a project that may have no database.
262
+ const keptCopyImportsPackage =
263
+ existing !== undefined && !opts.force && !existing.includes("runtimeImport");
264
+ const runtime = ejectsRuntime(opts.name) && !keptCopyImportsPackage
260
265
  ? await ejectRuntime(opts.cwd, [opts.name], opts.force === true)
261
266
  : undefined;
262
267
 
@@ -30,6 +30,7 @@ import {
30
30
  import { emitStructured } from "../lib/format.js";
31
31
  import { composeCatalog } from "../lib/catalog.js";
32
32
  import { describeError } from "../lib/error-text.js";
33
+ import { findRuntimeBoundaryCrossings, runtimeBoundaryWarnings } from "../lib/runtime-boundary-advisory.js";
33
34
 
34
35
  /**
35
36
  * Print a load failure with everything the loader's ADR-0009 envelope carried — the stable
@@ -235,6 +236,14 @@ export async function genCommand(args: string[], cwd: string, fmt: OutputFormat
235
236
  info: "",
236
237
  }));
237
238
 
239
+ // Advisory: generated code importing the owned adapter copy from outside its own
240
+ // workspace package (a monorepo) will not build. Warning only.
241
+ if (!cliConfig.dryRun) {
242
+ for (const w of runtimeBoundaryWarnings(
243
+ findRuntimeBoundaryCrossings(projectRoot, result.files.map((f) => f.path)),
244
+ )) log.warn(`warning: ${w}`);
245
+ }
246
+
238
247
  const targetDirs = Array.from(new Set(
239
248
  (forgeConfig.targets ? Object.values(forgeConfig.targets).map((t) => t.outDir) : [])
240
249
  .concat([forgeConfig.outDir]),
@@ -1418,6 +1418,7 @@ export async function runOfflineGenerate(
1418
1418
  );
1419
1419
  await writeSnapshot(path, nextSnapshot);
1420
1420
  log.info(`migrate: wrote ${res.upPath}`);
1421
+ log.info(`migrate: wrote ${res.downPath}`);
1421
1422
  return 0;
1422
1423
  }
1423
1424
 
@@ -39,6 +39,7 @@ import {
39
39
  } from "../lib/requirement-check.js";
40
40
  import { lintRequirements } from "../lib/requirement-lint.js";
41
41
  import { lintOverlays } from "../lib/overlay-lint.js";
42
+ import { lintNodeNames } from "../lib/name-lint.js";
42
43
  import { FileSource } from "@metaobjectsdev/metadata/core";
43
44
  import { resolveD1Config, resolveMigrateConfig } from "../lib/config.js";
44
45
  import {
@@ -350,6 +351,11 @@ export async function verifyCommand(
350
351
  // way every other advisory pass does.
351
352
  let overlaySection: AdvisorySection<AdvisoryDiagnosticRow> =
352
353
  skippedSection("the overlay lint did not run");
354
+ // The node-name authoring lint — its own section for the same reason: a model
355
+ // carrying a few hundred legacy names must not push another section's findings
356
+ // off the end of a capped run.
357
+ let nameSection: AdvisorySection<AdvisoryDiagnosticRow> =
358
+ skippedSection("the name lint did not run");
353
359
  // The ledger counts `meta verify` prints on every run. Undefined for a project
354
360
  // declaring no requirement.* node at all (opt-in by declaration) — the payload
355
361
  // then omits the block rather than reporting zeroes that would read as an empty
@@ -386,6 +392,10 @@ export async function verifyCommand(
386
392
  // Warnings ONLY — never changes the exit code.
387
393
  await runOverlayLintAdvisory();
388
394
 
395
+ // Whitespace in a node's `name` — loads clean on every port, almost never meant.
396
+ // Runs on every `meta verify`; warnings ONLY, never changes the exit code.
397
+ runNameLintAdvisory();
398
+
389
399
  // Advisory verify-as-teacher pass: surface hand-rolled work the metadata could
390
400
  // model. Warnings ONLY — never changes the exit code (bias to under-flagging).
391
401
  // Suppressed with --no-antipatterns or META_NO_ANTIPATTERNS=1 for the rare
@@ -434,6 +444,7 @@ export async function verifyCommand(
434
444
  requirementCounts,
435
445
  antiPatterns: antiPatternSection,
436
446
  overlays: overlaySection,
447
+ names: nameSection,
437
448
  }),
438
449
  fmt,
439
450
  );
@@ -808,6 +819,29 @@ export async function verifyCommand(
808
819
  }
809
820
  }
810
821
 
822
+ // -- node-name authoring lint (advisory) -----------------------------------
823
+ // Reads the LOADED model (see name-lint.ts). Its own section, its own cap, and
824
+ // a skip that carries its reason rather than looking like a clean scan.
825
+ function runNameLintAdvisory(): void {
826
+ if (flags.noNameLint) {
827
+ nameSection = skippedSection("suppressed by --no-name-lint");
828
+ return;
829
+ }
830
+ if (process.env.META_NO_NAME_LINT === "1") {
831
+ nameSection = skippedSection("suppressed by META_NO_NAME_LINT=1");
832
+ return;
833
+ }
834
+ const findings = lintNodeNames(root);
835
+ nameSection = ranSection(findings.map((d) => toDiagnosticRow(d, "lint")));
836
+ if (findings.length > 0) {
837
+ log.warn(
838
+ `meta verify — names: ${findings.length} authoring warning(s) ` +
839
+ `(advisory — does not fail the build):`,
840
+ );
841
+ warnCapped(findings.map(formatDiagnostic), flags.limit, { structured });
842
+ }
843
+ }
844
+
811
845
  // -- verify-as-teacher (advisory) ------------------------------------------
812
846
  // Records its result EITHER WAY — a skip carries its reason rather than looking
813
847
  // like a clean scan. Warnings only; nothing here reaches the exit code (the
@@ -1821,6 +1855,7 @@ function buildVerifyPayload(input: {
1821
1855
  requirementCounts: RequirementCounts | undefined;
1822
1856
  antiPatterns: AdvisorySection<AdvisoryFindingRow>;
1823
1857
  overlays: AdvisorySection<AdvisoryDiagnosticRow>;
1858
+ names: AdvisorySection<AdvisoryDiagnosticRow>;
1824
1859
  }): Record<string, unknown> {
1825
1860
  const ran = input.gates.filter((g) => g.ran);
1826
1861
  const failed = ran.filter((g) => !g.ok);
@@ -1840,6 +1875,9 @@ function buildVerifyPayload(input: {
1840
1875
  if (input.overlays.status === "ran" && input.overlays.total > 0) {
1841
1876
  parts.push(`${input.overlays.total} overlay authoring finding(s)`);
1842
1877
  }
1878
+ if (input.names.status === "ran" && input.names.total > 0) {
1879
+ parts.push(`${input.names.total} name authoring finding(s)`);
1880
+ }
1843
1881
 
1844
1882
  const help: string[] = [];
1845
1883
  if (failed.length > 0) {
@@ -1855,11 +1893,17 @@ function buildVerifyPayload(input: {
1855
1893
  `${input.overlays.total} unflagged cross-file redeclaration(s) — see overlays.rows[]; add overlay: true so a renamed or removed target fails loudly instead of silently becoming a new object`,
1856
1894
  );
1857
1895
  }
1896
+ if (input.names.total > 0) {
1897
+ help.push(
1898
+ `${input.names.total} node name(s) containing whitespace — see names.rows[]; rename each node, and pin @column on a field whose physical column must keep its name`,
1899
+ );
1900
+ }
1858
1901
  if (
1859
1902
  failed.length === 0 &&
1860
1903
  input.antiPatterns.total === 0 &&
1861
1904
  input.requirements.total === 0 &&
1862
- input.overlays.total === 0
1905
+ input.overlays.total === 0 &&
1906
+ input.names.total === 0
1863
1907
  ) {
1864
1908
  help.push("no drift and nothing advisory to answer — nothing to do");
1865
1909
  }
@@ -1872,6 +1916,7 @@ function buildVerifyPayload(input: {
1872
1916
  antiPatterns: input.antiPatterns,
1873
1917
  requirements: input.requirements,
1874
1918
  overlays: input.overlays,
1919
+ names: input.names,
1875
1920
  ...(input.requirementCounts !== undefined ? { requirementCounts: input.requirementCounts } : {}),
1876
1921
  // The honest boundary. Everything named here is REACHABLE — it is printed as
1877
1922
  // text on stderr — but it is not in this document, and a reader must not have
package/src/index.ts CHANGED
@@ -127,6 +127,8 @@ VERIFY FLAGS (ADR-0021 D2 — explicit subverbs; combine any; exit 1 on ANY drif
127
127
  --no-requirement-lint Suppress the advisory requirement AUTHORING lint (not the gate)
128
128
  --no-overlay-lint Suppress the advisory overlay-redeclaration AUTHORING lint
129
129
  (never a gate — this lint can't fail the build)
130
+ --no-name-lint Suppress the advisory node-name AUTHORING lint (whitespace
131
+ in a name; never a gate — this lint can't fail the build)
130
132
  --limit <n|all> How many advisory lines TEXT output prints per section before
131
133
  it truncates (default 20). Never applies to --format
132
134
  toon/json, which carry every finding and every diagnostic.
@@ -320,6 +322,8 @@ FLAGS:
320
322
  --no-requirement-lint Suppress the advisory requirement AUTHORING lint (not the gate)
321
323
  --no-overlay-lint Suppress the advisory overlay-redeclaration AUTHORING lint —
322
324
  never a gate; this lint can't fail the build
325
+ --no-name-lint Suppress the advisory node-name AUTHORING lint (whitespace in
326
+ a name) — never a gate; this lint can't fail the build
323
327
  --limit <n|all> Advisory lines TEXT output prints PER SECTION before truncating
324
328
  (default 20; per-section so the authoring lint can never push
325
329
  the gate's own warnings off the end)
@@ -350,6 +354,13 @@ redeclaration lacks 'overlay: true' — today's default merge rule reuses it
350
354
  silently, but the same unflagged redeclaration silently becomes a NEW object
351
355
  the day the target is renamed or removed upstream. Warnings only — it can
352
356
  never fail the build. Opt out with --no-overlay-lint or META_NO_OVERLAY_LINT=1.
357
+
358
+ verify also prints an ADVISORY node-name authoring lint, in its own section: any
359
+ node (object, field, identity, relationship, ...) whose name contains whitespace.
360
+ Leading or trailing whitespace is almost certainly a copy-paste accident — rename
361
+ the node, and pin @column on a field whose physical column must keep its name.
362
+ Requirement names are left to the requirement lint above. Warnings only — it can
363
+ never fail the build. Opt out with --no-name-lint or META_NO_NAME_LINT=1.
353
364
  `,
354
365
  export: `meta export — flatten loaded metadata to one canonical JSON artifact
355
366
 
package/src/lib/args.ts CHANGED
@@ -362,6 +362,13 @@ export interface VerifyFlags {
362
362
  * lint has no gate half at all — it can never fail the build).
363
363
  */
364
364
  noOverlayLint: boolean;
365
+ /**
366
+ * Suppress the advisory node-NAME authoring lint — whitespace in a node's
367
+ * `name` (a trailing space copied from legacy XML loads clean on every port).
368
+ * Advisory only, like its siblings: it has no gate half and can never fail
369
+ * the build.
370
+ */
371
+ noNameLint: boolean;
365
372
  /**
366
373
  * ADR-0023 strict-attr load opt-OUT (#96). `verify` is strict-by-default — an
367
374
  * undeclared/typo'd own `@attr` fails verify (ERR_UNKNOWN_ATTR). `--lax`
@@ -403,6 +410,7 @@ export const VERIFY_OPTIONS = {
403
410
  "no-antipatterns": { type: "boolean", default: false },
404
411
  "no-requirement-lint": { type: "boolean", default: false },
405
412
  "no-overlay-lint": { type: "boolean", default: false },
413
+ "no-name-lint": { type: "boolean", default: false },
406
414
  lax: { type: "boolean", default: false },
407
415
  "d1": { type: "string" },
408
416
  "remote": { type: "boolean", default: false },
@@ -487,6 +495,7 @@ export function parseVerifyArgs(argv: string[]): VerifyFlags {
487
495
  noAntipatterns: !!values["no-antipatterns"],
488
496
  noRequirementLint: !!values["no-requirement-lint"],
489
497
  noOverlayLint: !!values["no-overlay-lint"],
498
+ noNameLint: !!values["no-name-lint"],
490
499
  lax: !!values.lax,
491
500
  d1: values.d1 as string | undefined,
492
501
  remote: !!values.remote,
@@ -96,7 +96,10 @@ export function scanForUnindexedForeignKeys(root: MetaData, opts: UnindexedFkOpt
96
96
  });
97
97
  const fields = ref !== undefined ? ref.fields : fk.columns;
98
98
  const label = ref !== undefined ? `${entity.name}.${ref.name}` : `${entity.name} (${fk.name})`;
99
- const indexName = `by${fields.map(pascal).join("")}`;
99
+ // Entity-prefixed: an index name is schema-wide in Postgres, so a bare `by<Field>`
100
+ // suggested the SAME name to every entity sharing the FK field, and applying the advice
101
+ // verbatim failed `meta migrate` with ERR_DUPLICATE_SQL_NAME.
102
+ const indexName = `${entity.name.charAt(0).toLowerCase()}${entity.name.slice(1)}By${fields.map(pascal).join("")}`;
100
103
  const fieldList = fields.length === 1 ? fields[0]! : `[${fields.join(", ")}]`;
101
104
  out.push({
102
105
  file: files[0] ?? "",
@@ -0,0 +1,106 @@
1
+ // `meta verify` — the node-name AUTHORING lint.
2
+ //
3
+ // The loader accepts any string as a node `name`, on every port. An adopter's metadata
4
+ // carried a field named `"defaultCurrencyId "` (a trailing space, copied from legacy XML)
5
+ // and an identity named `"account_defaultCurrencyId _fk"`; both loaded with no error.
6
+ //
7
+ // WHY THIS IS A WARNING AND NOT A LOAD ERROR. It was weighed against the correction bar
8
+ // in docs/compatibility-policy.md and does not pass it: whitespace names WORK today on
9
+ // the core path — `meta migrate` emits a quoted column (`"default_currency_id "`) and the
10
+ // ObjectManager round-trips the key — and multi-word requirement names are documented to
11
+ // load. Refusing them at load would break metadata that does what its author asked, so
12
+ // the honest instrument is an advisory that names the edit.
13
+ //
14
+ // EVERY FINDING IS A WARNING, BY CONSTRUCTION — the same rule, and the same reasoning,
15
+ // as requirement-lint.ts's header. Nothing here reaches the exit code.
16
+ //
17
+ // REQUIREMENTS ARE NOT LINTED HERE. A requirement's name is its dotted ADDRESS and the
18
+ // requirement lint already owns it (WARN_REQUIREMENT_NAME_NOT_ADDRESSABLE reports
19
+ // leading/trailing whitespace; a short multi-word label such as "Order Recording" is
20
+ // deliberately NOT reported, because renaming one moves its address and its emitted
21
+ // stub file for no gain). Reporting the same name twice under two codes would make
22
+ // one edit look like two problems.
23
+ //
24
+ // IT READS THE LOADED MODEL, NEVER THE FILES — and walks DECLARATIONS (see `walk`).
25
+
26
+ import { PACKAGE_SEPARATOR, TYPE_FIELD, TYPE_REQUIREMENT, type MetaData } from "@metaobjectsdev/metadata";
27
+ import type { Diagnostic } from "./requirement-check.js";
28
+
29
+ /** Leading or trailing whitespace — the strong finding. */
30
+ export const WARN_NAME_SURROUNDING_WHITESPACE = "WARN_NAME_SURROUNDING_WHITESPACE";
31
+ /** Whitespace inside an otherwise trimmed name — the plain advisory. */
32
+ export const WARN_NAME_INTERNAL_WHITESPACE = "WARN_NAME_INTERNAL_WHITESPACE";
33
+
34
+ /**
35
+ * What counts as whitespace: the ECMAScript `\s` class. That is ASCII space, tab, LF,
36
+ * CR, VT and FF, plus U+00A0 (no-break space), U+FEFF, U+1680, U+2000–U+200A, U+2028,
37
+ * U+2029, U+202F, U+205F and U+3000. The non-ASCII members matter for this lint's
38
+ * actual source of input: a no-break space survives a copy out of a document or a
39
+ * legacy export and is invisible in every editor.
40
+ */
41
+ const WHITESPACE = /\s/u;
42
+
43
+ function warn(path: string, code: string, message: string): Diagnostic {
44
+ return { severity: "warn", code, path, message };
45
+ }
46
+
47
+ /** One segment of a node's address. A name with whitespace is quoted so the
48
+ * whitespace is visible in the printed path rather than vanishing into it. */
49
+ function segment(name: string): string {
50
+ return WHITESPACE.test(name) ? JSON.stringify(name) : name;
51
+ }
52
+
53
+ function check(node: MetaData, path: string, out: Diagnostic[]): void {
54
+ const name = node.name;
55
+ if (name === "" || !WHITESPACE.test(name)) return;
56
+
57
+ const kind = `${node.type}.${node.subType}`;
58
+ const quoted = JSON.stringify(name);
59
+ // @column is a FIELD attribute. Advising an object or identity author to pin it
60
+ // would send them looking for a slot that does not exist on the node.
61
+ const pin = node.type === TYPE_FIELD ? ", and pin @column if the physical column must keep its name" : "";
62
+ const trimmed = name.trim();
63
+
64
+ if (trimmed !== name) {
65
+ const inner = WHITESPACE.test(trimmed) ? " It also contains whitespace inside it." : "";
66
+ const target = trimmed === "" ? "" : ` (to ${JSON.stringify(trimmed)}, presumably)`;
67
+ out.push(warn(path, WARN_NAME_SURROUNDING_WHITESPACE,
68
+ `${kind} name ${quoted} has leading or trailing whitespace, which is almost certainly ` +
69
+ `unintended — rename it${target}${pin}. The whitespace is carried into every identifier, ` +
70
+ `column and wire key derived from the name.${inner}`));
71
+ return;
72
+ }
73
+
74
+ out.push(warn(path, WARN_NAME_INTERNAL_WHITESPACE,
75
+ `${kind} name ${quoted} contains whitespace. A name is an identifier: derived column names ` +
76
+ `and wire keys carry the space, and generated code cannot use it as one. Consider ` +
77
+ `renaming it${pin}.`));
78
+ }
79
+
80
+ /** A root-level node's address: package-qualified the way `resolutionKey()` is, so the
81
+ * path reads like every other reference to the node. */
82
+ function topLevelAddress(node: MetaData): string {
83
+ const pkg = node.package ?? node.fileDefaultPackage;
84
+ return pkg !== undefined && pkg !== "" ? `${pkg}${PACKAGE_SEPARATOR}${segment(node.name)}` : segment(node.name);
85
+ }
86
+
87
+ /**
88
+ * Lint the name of every named node in the loaded model, requirements excepted.
89
+ * Returns `[]` for a model with no whitespace in any name.
90
+ */
91
+ export function lintNodeNames(root: MetaData): Diagnostic[] {
92
+ const out: Diagnostic[] = [];
93
+ const walk = (n: MetaData, prefix: string): void => {
94
+ // OWN-ONLY (ADR-0039 sanctioned case): this reports each DECLARATION once, at the
95
+ // node that declares it. A field inherited through `extends` is one declaration and
96
+ // one rename; the resolving `children()` would repeat it on every subtype.
97
+ for (const c of n.ownChildren()) {
98
+ if (c.type === TYPE_REQUIREMENT) continue; // owned by requirement-lint.ts — see header
99
+ const path = prefix === "" ? topLevelAddress(c) : `${prefix}.${segment(c.name)}`;
100
+ check(c, path, out);
101
+ walk(c, path);
102
+ }
103
+ };
104
+ walk(root, "");
105
+ return out;
106
+ }
@@ -0,0 +1,74 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { dirname, join, relative, resolve, sep } from "node:path";
3
+ import { OWNED_RUNTIME_DIR, HTTP_RUNTIME_PACKAGE } from "@metaobjectsdev/codegen-ts";
4
+
5
+ /**
6
+ * Generated code that imports the owned HTTP-adapter copy from OUTSIDE its own
7
+ * workspace package.
8
+ *
9
+ * Since 1.0.9 an ejected `entity` / `routes` / `routes-hono` generator points its output
10
+ * at `codegen/runtime/` (the adapter source `meta eject` copied) by a path relative to
11
+ * the output directory. In a single-package project that is exactly right. In a monorepo
12
+ * whose output sits inside a workspace package (`apps/api/src/generated`), the path climbs
13
+ * out of that package: `tsc` rejects the file (`TS6059 … is not under 'rootDir'`), and the
14
+ * copy's own bare imports (`qs`, `fastify`) do not resolve from the repository root. The
15
+ * `meta gen` run itself is green; the adopter's build is the first thing that disagrees.
16
+ *
17
+ * Found upgrading a pnpm monorepo. This names the package and the two ways out. It is a
18
+ * warning and never changes an exit code.
19
+ */
20
+ export interface RuntimeBoundaryFinding {
21
+ /** Project-relative directory of the workspace package the generated file sits in. */
22
+ packageDir: string;
23
+ /** One generated file (project-relative) that crosses the boundary. */
24
+ example: string;
25
+ }
26
+
27
+ const IMPORT_SPEC = /\bfrom\s+["'](\.{1,2}\/[^"']+)["']/g;
28
+
29
+ function nearestPackageDir(fileDir: string, projectRoot: string): string | undefined {
30
+ for (let d = fileDir; ; d = dirname(d)) {
31
+ if (d === projectRoot || !d.startsWith(projectRoot + sep)) return undefined;
32
+ if (existsSync(join(d, "package.json"))) return d;
33
+ }
34
+ }
35
+
36
+ function isInside(child: string, parent: string): boolean {
37
+ return child === parent || child.startsWith(parent + sep);
38
+ }
39
+
40
+ export function findRuntimeBoundaryCrossings(
41
+ projectRoot: string,
42
+ generatedFiles: readonly string[],
43
+ ): RuntimeBoundaryFinding[] {
44
+ const root = resolve(projectRoot);
45
+ const runtimeRoot = join(root, OWNED_RUNTIME_DIR);
46
+ const byPackage = new Map<string, RuntimeBoundaryFinding>();
47
+ for (const file of generatedFiles) {
48
+ if (!/\.(ts|tsx|mts)$/.test(file) || !existsSync(file)) continue;
49
+ const abs = resolve(file);
50
+ const pkgDir = nearestPackageDir(dirname(abs), root);
51
+ if (pkgDir === undefined || isInside(runtimeRoot, pkgDir) || byPackage.has(pkgDir)) continue;
52
+ const text = readFileSync(abs, "utf8");
53
+ for (const m of text.matchAll(IMPORT_SPEC)) {
54
+ if (isInside(resolve(dirname(abs), m[1]!), runtimeRoot)) {
55
+ byPackage.set(pkgDir, {
56
+ packageDir: relative(root, pkgDir).split(sep).join("/"),
57
+ example: relative(root, abs).split(sep).join("/"),
58
+ });
59
+ break;
60
+ }
61
+ }
62
+ }
63
+ return [...byPackage.values()];
64
+ }
65
+
66
+ export function runtimeBoundaryWarnings(findings: readonly RuntimeBoundaryFinding[]): string[] {
67
+ return findings.map((f) =>
68
+ `generated code in ${f.packageDir}/ imports the owned HTTP adapter from ${OWNED_RUNTIME_DIR}/, ` +
69
+ `outside that workspace package (e.g. ${f.example}); its build will not resolve it. ` +
70
+ `Either keep importing the published adapter — pass \`runtimeImport: "${HTTP_RUNTIME_PACKAGE}"\` ` +
71
+ `to the ejected entity/routes generators — or move ${OWNED_RUNTIME_DIR}/ inside ${f.packageDir}/ ` +
72
+ `and point \`runtimeImport\` at it.`,
73
+ );
74
+ }