@metaobjectsdev/cli 1.0.9 → 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.
- package/dist/src/commands/deps.d.ts.map +1 -1
- package/dist/src/commands/deps.js +9 -8
- package/dist/src/commands/deps.js.map +1 -1
- package/dist/src/commands/docs.d.ts.map +1 -1
- package/dist/src/commands/docs.js +19 -12
- package/dist/src/commands/docs.js.map +1 -1
- package/dist/src/commands/eject.d.ts.map +1 -1
- package/dist/src/commands/eject.js +8 -3
- package/dist/src/commands/eject.js.map +1 -1
- package/dist/src/commands/export.d.ts.map +1 -1
- package/dist/src/commands/export.js +3 -2
- package/dist/src/commands/export.js.map +1 -1
- package/dist/src/commands/gen.d.ts.map +1 -1
- package/dist/src/commands/gen.js +15 -7
- package/dist/src/commands/gen.js.map +1 -1
- package/dist/src/commands/generator.d.ts.map +1 -1
- package/dist/src/commands/generator.js +3 -2
- package/dist/src/commands/generator.js.map +1 -1
- package/dist/src/commands/init.d.ts.map +1 -1
- package/dist/src/commands/init.js +5 -2
- package/dist/src/commands/init.js.map +1 -1
- package/dist/src/commands/migrate.d.ts.map +1 -1
- package/dist/src/commands/migrate.js +31 -23
- package/dist/src/commands/migrate.js.map +1 -1
- package/dist/src/commands/prompt-snapshot.d.ts.map +1 -1
- package/dist/src/commands/prompt-snapshot.js +5 -4
- package/dist/src/commands/prompt-snapshot.js.map +1 -1
- package/dist/src/commands/types.d.ts.map +1 -1
- package/dist/src/commands/types.js +7 -1
- package/dist/src/commands/types.js.map +1 -1
- package/dist/src/commands/upgrade.d.ts.map +1 -1
- package/dist/src/commands/upgrade.js +4 -3
- package/dist/src/commands/upgrade.js.map +1 -1
- package/dist/src/commands/verify.d.ts.map +1 -1
- package/dist/src/commands/verify.js +73 -24
- package/dist/src/commands/verify.js.map +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +14 -2
- package/dist/src/index.js.map +1 -1
- package/dist/src/lib/args.d.ts +11 -0
- package/dist/src/lib/args.d.ts.map +1 -1
- package/dist/src/lib/args.js +19 -6
- package/dist/src/lib/args.js.map +1 -1
- package/dist/src/lib/catalog-listing.d.ts.map +1 -1
- package/dist/src/lib/catalog-listing.js +5 -1
- package/dist/src/lib/catalog-listing.js.map +1 -1
- package/dist/src/lib/dependency-sync.d.ts.map +1 -1
- package/dist/src/lib/dependency-sync.js +4 -3
- package/dist/src/lib/dependency-sync.js.map +1 -1
- package/dist/src/lib/error-text.d.ts +2 -0
- package/dist/src/lib/error-text.d.ts.map +1 -0
- package/dist/src/lib/error-text.js +15 -0
- package/dist/src/lib/error-text.js.map +1 -0
- package/dist/src/lib/fk-index-advisory.d.ts.map +1 -1
- package/dist/src/lib/fk-index-advisory.js +4 -1
- package/dist/src/lib/fk-index-advisory.js.map +1 -1
- package/dist/src/lib/gen-migrate-advice.d.ts +1 -1
- package/dist/src/lib/gen-migrate-advice.d.ts.map +1 -1
- package/dist/src/lib/gen-migrate-advice.js +24 -0
- package/dist/src/lib/gen-migrate-advice.js.map +1 -1
- package/dist/src/lib/git-ignore.d.ts.map +1 -1
- package/dist/src/lib/git-ignore.js +2 -1
- package/dist/src/lib/git-ignore.js.map +1 -1
- package/dist/src/lib/name-lint.d.ts +12 -0
- package/dist/src/lib/name-lint.d.ts.map +1 -0
- package/dist/src/lib/name-lint.js +95 -0
- package/dist/src/lib/name-lint.js.map +1 -0
- package/dist/src/lib/output.d.ts +3 -2
- package/dist/src/lib/output.d.ts.map +1 -1
- package/dist/src/lib/output.js.map +1 -1
- package/dist/src/lib/requirement-check.d.ts.map +1 -1
- package/dist/src/lib/requirement-check.js +43 -2
- package/dist/src/lib/requirement-check.js.map +1 -1
- package/dist/src/lib/runtime-boundary-advisory.d.ts +24 -0
- package/dist/src/lib/runtime-boundary-advisory.d.ts.map +1 -0
- package/dist/src/lib/runtime-boundary-advisory.js +47 -0
- package/dist/src/lib/runtime-boundary-advisory.js.map +1 -0
- package/package.json +10 -10
- package/src/commands/deps.ts +9 -8
- package/src/commands/docs.ts +19 -12
- package/src/commands/eject.ts +9 -3
- package/src/commands/export.ts +3 -2
- package/src/commands/gen.ts +17 -7
- package/src/commands/generator.ts +3 -2
- package/src/commands/init.ts +5 -2
- package/src/commands/migrate.ts +33 -23
- package/src/commands/prompt-snapshot.ts +5 -4
- package/src/commands/types.ts +6 -1
- package/src/commands/upgrade.ts +4 -3
- package/src/commands/verify.ts +83 -24
- package/src/index.ts +14 -2
- package/src/lib/args.ts +28 -6
- package/src/lib/catalog-listing.ts +5 -1
- package/src/lib/dependency-sync.ts +4 -3
- package/src/lib/error-text.ts +13 -0
- package/src/lib/fk-index-advisory.ts +4 -1
- package/src/lib/gen-migrate-advice.ts +24 -1
- package/src/lib/git-ignore.ts +2 -1
- package/src/lib/name-lint.ts +106 -0
- package/src/lib/output.ts +3 -2
- package/src/lib/requirement-check.ts +46 -2
- package/src/lib/runtime-boundary-advisory.ts +74 -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
|
+
}
|
package/src/lib/output.ts
CHANGED
|
@@ -47,8 +47,9 @@ export interface GenFileEntry {
|
|
|
47
47
|
export interface GenResultShape {
|
|
48
48
|
files: GenFileEntry[];
|
|
49
49
|
outDir: string;
|
|
50
|
-
/** Absent for a value-object-only project (no DB code generated → no dialect used).
|
|
51
|
-
dialect
|
|
50
|
+
/** Absent for a value-object-only project (no DB code generated → no dialect used). The
|
|
51
|
+
* CODEGEN dialect, which may be `mysql` (a dialect `meta migrate` does not speak). */
|
|
52
|
+
dialect: Dialect | "mysql" | undefined;
|
|
52
53
|
dryRun: boolean;
|
|
53
54
|
warnings: string[];
|
|
54
55
|
/**
|
|
@@ -675,14 +675,58 @@ export function summariseRequirements(
|
|
|
675
675
|
// `exactOptionalPropertyTypes`, so the keys are absent rather than undefined.
|
|
676
676
|
};
|
|
677
677
|
|
|
678
|
-
|
|
678
|
+
// `undecided` counts only the requirements a `@disposition` could actually
|
|
679
|
+
// SETTLE. A parent is `partial` because a descendant is, so a disposition on it
|
|
680
|
+
// would have to mean "we accept that memory is incomplete" — which settles
|
|
681
|
+
// nothing, names no work, and cannot be acted on.
|
|
682
|
+
//
|
|
683
|
+
// Measured on a real ledger before this was written: one adopter's tree printed
|
|
684
|
+
// "19 recorded gap(s) with no @disposition", and SIXTEEN of the nineteen were
|
|
685
|
+
// roll-up parents (`assistant`, `memory`, `security`, `understanding`,
|
|
686
|
+
// `conversation`, `growth`, `action`). Three were leaves anyone could rule on.
|
|
687
|
+
// Overstating by 6x is not cosmetic: a count nobody can action is a count
|
|
688
|
+
// everybody learns to skip, and the real gaps then ride along inside it
|
|
689
|
+
// invisibly — the opposite of what the line exists to do.
|
|
690
|
+
//
|
|
691
|
+
// Ancestry comes from the scan's own dotted PATHS rather than a second
|
|
692
|
+
// traversal, so this cannot disagree with how every other check addresses a
|
|
693
|
+
// node. `::` carries no dot, so splitting on "." keeps a package-qualified root
|
|
694
|
+
// segment intact. EVERY ancestor is excluded, not only the immediate parent:
|
|
695
|
+
// real ledgers are five deep, and an L1 node three levels above the work is no
|
|
696
|
+
// more actionable than the L2 one.
|
|
697
|
+
//
|
|
698
|
+
// A descendant marks its ancestors whether or not it is itself DISPOSED, and
|
|
699
|
+
// that asymmetry is the point. Once the only outstanding leaf under a parent has
|
|
700
|
+
// been ruled on, nothing beneath that parent is owed — so the parent must not be
|
|
701
|
+
// resurrected as a fresh decision by the roll-up status the ruled-on child gave
|
|
702
|
+
// it. Skipping disposed descendants here would do exactly that: it was the first
|
|
703
|
+
// implementation, and the test for the disposed-child tree caught it.
|
|
704
|
+
const rollUpAncestors = new Set<string>();
|
|
705
|
+
for (const { node, path } of scan.addressed) {
|
|
706
|
+
if (!node.hasOutstandingWork()) continue;
|
|
707
|
+
const segments = path.split(".");
|
|
708
|
+
for (let i = 1; i < segments.length; i++) {
|
|
709
|
+
rollUpAncestors.add(segments.slice(0, i).join("."));
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
for (const { node: req, path } of scan.addressed) {
|
|
679
714
|
if (req.subType === REQUIREMENT_SUBTYPE_ARCHITECTURAL) summary.architectural++;
|
|
680
715
|
else summary.functional++;
|
|
681
716
|
|
|
682
717
|
const status = req.status();
|
|
718
|
+
// byStatus is UNCHANGED on purpose: a roll-up parent is still genuinely
|
|
719
|
+
// `partial`, and the status breakdown is where that belongs. Only the
|
|
720
|
+
// "decisions owed" line narrows.
|
|
683
721
|
if (status !== undefined) summary.byStatus[status] = (summary.byStatus[status] ?? 0) + 1;
|
|
684
722
|
|
|
685
|
-
if (
|
|
723
|
+
if (
|
|
724
|
+
req.hasOutstandingWork() &&
|
|
725
|
+
req.disposition() === undefined &&
|
|
726
|
+
!rollUpAncestors.has(path)
|
|
727
|
+
) {
|
|
728
|
+
summary.undecided++;
|
|
729
|
+
}
|
|
686
730
|
if (req.disposition() === REQUIREMENT_DISPOSITION_DEFERRED && req.trackedBy().length === 0) {
|
|
687
731
|
summary.deferredUntracked++;
|
|
688
732
|
}
|
|
@@ -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
|
+
}
|