@metaobjectsdev/metadata 1.0.1 → 1.0.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/core/parser-yaml.d.ts.map +1 -1
- package/dist/core/parser-yaml.js +4 -0
- package/dist/core/parser-yaml.js.map +1 -1
- package/dist/errors.d.ts +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +21 -0
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +6 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -3
- package/dist/index.js.map +1 -1
- package/dist/json-path.d.ts +14 -0
- package/dist/json-path.d.ts.map +1 -1
- package/dist/json-path.js +13 -0
- package/dist/json-path.js.map +1 -1
- package/dist/loader/meta-data-loader.d.ts +44 -21
- package/dist/loader/meta-data-loader.d.ts.map +1 -1
- package/dist/loader/meta-data-loader.js +112 -87
- package/dist/loader/meta-data-loader.js.map +1 -1
- package/dist/loader/sources/file-source.d.ts +7 -1
- package/dist/loader/sources/file-source.d.ts.map +1 -1
- package/dist/loader/sources/file-source.js +3 -2
- package/dist/loader/sources/file-source.js.map +1 -1
- package/dist/naming.d.ts +7 -0
- package/dist/naming.d.ts.map +1 -1
- package/dist/naming.js +10 -0
- package/dist/naming.js.map +1 -1
- package/dist/parser-core.d.ts +103 -0
- package/dist/parser-core.d.ts.map +1 -1
- package/dist/parser-core.js +189 -16
- package/dist/parser-core.js.map +1 -1
- package/dist/scope.d.ts +16 -0
- package/dist/scope.d.ts.map +1 -0
- package/dist/scope.js +78 -0
- package/dist/scope.js.map +1 -0
- package/dist/serializer-json.d.ts +1 -0
- package/dist/serializer-json.d.ts.map +1 -1
- package/dist/serializer-json.js +43 -1
- package/dist/serializer-json.js.map +1 -1
- package/package.json +1 -1
- package/src/core/parser-yaml.ts +4 -0
- package/src/errors.ts +21 -0
- package/src/index.ts +12 -4
- package/src/json-path.ts +16 -1
- package/src/loader/meta-data-loader.ts +147 -101
- package/src/loader/sources/file-source.ts +10 -2
- package/src/naming.ts +11 -0
- package/src/parser-core.ts +304 -20
- package/src/scope.ts +97 -0
- package/src/serializer-json.ts +46 -1
package/src/parser-core.ts
CHANGED
|
@@ -39,7 +39,7 @@ import {
|
|
|
39
39
|
extendsTargetCompatible,
|
|
40
40
|
EXTENDS_TARGET_MISMATCH_RULE,
|
|
41
41
|
} from "./super-resolve.js";
|
|
42
|
-
import { JsonPathBuilder } from "./json-path.js";
|
|
42
|
+
import { JsonPathBuilder, type Segment as JsonPathSegment } from "./json-path.js";
|
|
43
43
|
import { getYamlPosition, type YamlPosition } from "./core/yaml-positions.js";
|
|
44
44
|
import {
|
|
45
45
|
TYPE_ATTR,
|
|
@@ -100,6 +100,64 @@ export interface ParseOptions {
|
|
|
100
100
|
* desugar attached one, the optional `yamlPosition`.
|
|
101
101
|
*/
|
|
102
102
|
sourceFormat?: "json" | "yaml";
|
|
103
|
+
/**
|
|
104
|
+
* ADR-0055 — if true, `overlay: true` declarations are QUEUED as they are met
|
|
105
|
+
* rather than applied during the walk, and handed back as
|
|
106
|
+
* {@link ParseResult.pendingOverlays} for the loader to apply once every
|
|
107
|
+
* source has been parsed. Exactly the shape `deferSuperResolution` uses for
|
|
108
|
+
* `extends`, and for the same reason: the target may be declared in a source
|
|
109
|
+
* parsed later, or later in this same document.
|
|
110
|
+
*
|
|
111
|
+
* When absent, `buildTree` drains its own queue before returning — so a
|
|
112
|
+
* standalone `parseJson`/`parseYaml` call is order-independent within its one
|
|
113
|
+
* document. There is no eager path: queue-then-drain is the only door.
|
|
114
|
+
*/
|
|
115
|
+
deferOverlays?: boolean;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* ADR-0055 — one `overlay: true` declaration met during the walk and deferred.
|
|
120
|
+
*
|
|
121
|
+
* A deferred super hangs on its node as `model.superRef`; a deferred overlay has
|
|
122
|
+
* NO node — nothing was created — so everything the parser would have used has
|
|
123
|
+
* to travel out with it, including the module-level walk state needed to report
|
|
124
|
+
* an error against the declaration's own location long after the walk unwound.
|
|
125
|
+
*/
|
|
126
|
+
export interface PendingOverlay {
|
|
127
|
+
/** Wrapper-key type and subType. The lookup is by (type, name); subType is
|
|
128
|
+
* carried for diagnostics only — it is never consulted when matching. */
|
|
129
|
+
readonly type: string;
|
|
130
|
+
readonly subType: string;
|
|
131
|
+
readonly name: string;
|
|
132
|
+
/** The declaration body, untouched. `parseNodeInto` consumes it at
|
|
133
|
+
* application time; for YAML input it still carries the desugar's
|
|
134
|
+
* position-by-key map, so nested children keep correct positions. */
|
|
135
|
+
readonly nodeData: Record<string, unknown>;
|
|
136
|
+
/** The node the target is sought under — the accumulating root for a
|
|
137
|
+
* top-level overlay, the enclosing PLAIN node for a nested one. A live
|
|
138
|
+
* reference: the tree is mutated in place and nodes are never replaced. */
|
|
139
|
+
readonly parent: MetaData;
|
|
140
|
+
/** Accumulating root, for super resolution of anything the overlay adds. */
|
|
141
|
+
readonly accumRoot: MetaData;
|
|
142
|
+
/** Effective context package at the declaration site — needed for the
|
|
143
|
+
* package-qualified root lookup and for package inheritance of new children. */
|
|
144
|
+
readonly inheritedContextPkg: string;
|
|
145
|
+
/** `opts.sourceName` as passed to buildTree (may be undefined). */
|
|
146
|
+
readonly sourceName: string | undefined;
|
|
147
|
+
/** The resolved source id used in envelopes (`sourceName ?? "<unknown>"`). */
|
|
148
|
+
readonly sourceId: string;
|
|
149
|
+
/** The `path` string parseNodeInto receives for diagnostics. */
|
|
150
|
+
readonly path: string;
|
|
151
|
+
/** ADR-0009 parse-time envelope of the declaration itself — the `files` and
|
|
152
|
+
* `jsonPath` the eventual resolved error carries. */
|
|
153
|
+
readonly errorSource: ErrorSource;
|
|
154
|
+
/** JSONPath stack at queue time, so the module-level builder can be
|
|
155
|
+
* re-seeded before re-entering the walk. */
|
|
156
|
+
readonly pathSegments: readonly JsonPathSegment[];
|
|
157
|
+
/** FR5b — source format discriminant at queue time. */
|
|
158
|
+
readonly format: "json" | "yaml";
|
|
159
|
+
/** FR5b — the declaration's own YAML position, when the desugar had one. */
|
|
160
|
+
readonly yamlPosition?: YamlPosition;
|
|
103
161
|
}
|
|
104
162
|
|
|
105
163
|
export interface ParseResult {
|
|
@@ -114,6 +172,15 @@ export interface ParseResult {
|
|
|
114
172
|
* `code` + `source` and are surfaced unchanged. Defaults to `[]`.
|
|
115
173
|
*/
|
|
116
174
|
envelopeWarnings: LoaderWarning[];
|
|
175
|
+
/**
|
|
176
|
+
* ADR-0055 — `overlay: true` declarations queued during this parse, in
|
|
177
|
+
* encounter order. Empty unless {@link ParseOptions.deferOverlays} was set;
|
|
178
|
+
* when it was not, buildTree already drained them. Because sources are parsed
|
|
179
|
+
* sequentially and the walk is pre-order, encounter order IS "source order,
|
|
180
|
+
* then declaration order within a source" — no sort is needed and stability
|
|
181
|
+
* is by construction.
|
|
182
|
+
*/
|
|
183
|
+
pendingOverlays: PendingOverlay[];
|
|
117
184
|
}
|
|
118
185
|
|
|
119
186
|
// ---------------------------------------------------------------------------
|
|
@@ -346,8 +413,14 @@ function splitTypeKey(key: string, registry: TypeRegistry): SplitKey {
|
|
|
346
413
|
* - Absolute path (::foo::bar) → prepended with base: "acme" + "::foo" → "acme::foo::bar"
|
|
347
414
|
* - Relative parent (..) → handled in super resolution, not here
|
|
348
415
|
* - No leading :: → used as-is
|
|
416
|
+
*
|
|
417
|
+
* Exported so `declaredTopLevelKeys` (meta-data-loader.ts) — the structural
|
|
418
|
+
* pre-parse walk that must produce the SAME resolution key `rootChildResolutionKey`
|
|
419
|
+
* below computes — reuses this rather than reimplementing it. A second copy is
|
|
420
|
+
* exactly how the two silently disagreed on a relative (`::`-prefixed) `package`
|
|
421
|
+
* before task 17's fix-round-1 caught it against a real fixture.
|
|
349
422
|
*/
|
|
350
|
-
function expandPackageForPath(basePkg: string, pkgPath: string): string {
|
|
423
|
+
export function expandPackageForPath(basePkg: string, pkgPath: string): string {
|
|
351
424
|
if (basePkg.trim() === "" || !pkgPath.startsWith(PACKAGE_SEPARATOR)) {
|
|
352
425
|
return pkgPath;
|
|
353
426
|
}
|
|
@@ -415,6 +488,10 @@ let _currentSourceId: string | undefined;
|
|
|
415
488
|
let _currentFormat: "json" | "yaml" = "json";
|
|
416
489
|
let _currentYamlPosition: YamlPosition | undefined;
|
|
417
490
|
|
|
491
|
+
// ADR-0055 — sink for overlay declarations deferred out of the walk. Set at
|
|
492
|
+
// buildTree entry; same synchronous-buildTree reentrancy argument as the others.
|
|
493
|
+
let _pendingOverlays: PendingOverlay[] | undefined;
|
|
494
|
+
|
|
418
495
|
/** FR5a/FR5b — stamp the source-provenance envelope on a freshly-created
|
|
419
496
|
* node. No-op when invoked outside buildTree's setup (defensive — the
|
|
420
497
|
* module-level state will always be populated during a normal parse).
|
|
@@ -457,6 +534,7 @@ export function buildTree(parsed: unknown, opts: ParseOptions): ParseResult {
|
|
|
457
534
|
const warnings: string[] = [];
|
|
458
535
|
const errors: ParseError[] = [];
|
|
459
536
|
const envelopeWarnings: LoaderWarning[] = [];
|
|
537
|
+
const pendingOverlays: PendingOverlay[] = [];
|
|
460
538
|
const strict = opts.strict ?? false;
|
|
461
539
|
const source = opts.sourceName;
|
|
462
540
|
_deferSuperResolution = opts.deferSuperResolution === true;
|
|
@@ -465,6 +543,25 @@ export function buildTree(parsed: unknown, opts: ParseOptions): ParseResult {
|
|
|
465
543
|
// emit envelope warnings without threading another parameter through the
|
|
466
544
|
// entire walk. Safe because buildTree is fully synchronous.
|
|
467
545
|
_currentEnvelopeWarnings = envelopeWarnings;
|
|
546
|
+
// ADR-0055 — overlay declarations are queued here as the walk meets them.
|
|
547
|
+
_pendingOverlays = pendingOverlays;
|
|
548
|
+
|
|
549
|
+
/** ADR-0055 — a caller that is not deferring gets its own queue drained here,
|
|
550
|
+
* so "queue, then apply" is the only path through the parser and a single
|
|
551
|
+
* document is order-independent on its own. */
|
|
552
|
+
const finishParse = (r: MetaRoot): ParseResult => {
|
|
553
|
+
if (opts.deferOverlays !== true && pendingOverlays.length > 0) {
|
|
554
|
+
const drained = applyPendingOverlays(pendingOverlays, {
|
|
555
|
+
registry: opts.registry,
|
|
556
|
+
strict,
|
|
557
|
+
});
|
|
558
|
+
errors.push(...drained.errors);
|
|
559
|
+
warnings.push(...drained.warnings);
|
|
560
|
+
envelopeWarnings.push(...drained.envelopeWarnings);
|
|
561
|
+
pendingOverlays.length = 0;
|
|
562
|
+
}
|
|
563
|
+
return { root: r, warnings, errors, envelopeWarnings, pendingOverlays };
|
|
564
|
+
};
|
|
468
565
|
// FR5a — start a fresh JSONPath stack rooted at "$"; sourceId is the
|
|
469
566
|
// source's id (from FileSource / InMemoryStringSource via opts.sourceName).
|
|
470
567
|
// Falls back to "<unknown>" when no name was supplied (e.g. ad-hoc parseJson
|
|
@@ -596,7 +693,7 @@ export function buildTree(parsed: unknown, opts: ParseOptions): ParseResult {
|
|
|
596
693
|
rootKey,
|
|
597
694
|
);
|
|
598
695
|
_currentPath!.pop();
|
|
599
|
-
return
|
|
696
|
+
return finishParse(opts.intoRoot);
|
|
600
697
|
}
|
|
601
698
|
|
|
602
699
|
// --- Fresh root mode: create a new root from the JSON ---
|
|
@@ -621,7 +718,7 @@ export function buildTree(parsed: unknown, opts: ParseOptions): ParseResult {
|
|
|
621
718
|
rootKey,
|
|
622
719
|
) as MetaRoot;
|
|
623
720
|
_currentPath!.pop();
|
|
624
|
-
return
|
|
721
|
+
return finishParse(root);
|
|
625
722
|
} finally {
|
|
626
723
|
_deferSuperResolution = false;
|
|
627
724
|
_currentErrors = undefined;
|
|
@@ -630,6 +727,7 @@ export function buildTree(parsed: unknown, opts: ParseOptions): ParseResult {
|
|
|
630
727
|
_currentSourceId = undefined;
|
|
631
728
|
_currentFormat = "json";
|
|
632
729
|
_currentYamlPosition = undefined;
|
|
730
|
+
_pendingOverlays = undefined;
|
|
633
731
|
}
|
|
634
732
|
}
|
|
635
733
|
|
|
@@ -1070,6 +1168,100 @@ function createOrFindMetaData(
|
|
|
1070
1168
|
// merge pair (mirrors the Java parser, which searches root children by
|
|
1071
1169
|
// "pkg::name"). Nested children stay bare-name matched — they are scoped
|
|
1072
1170
|
// by their parent, and packages don't disambiguate siblings inside a node.
|
|
1171
|
+
if (isOverlayNode) {
|
|
1172
|
+
// ADR-0055 — an overlay is ALWAYS queued, never applied during the walk, and
|
|
1173
|
+
// deliberately not conditioned on whether its target happens to exist yet.
|
|
1174
|
+
//
|
|
1175
|
+
// Applying it when the base is already present and queueing only on a miss is
|
|
1176
|
+
// the "retry-on-miss" variant the ADR rejected: it would leave output
|
|
1177
|
+
// dependent on whether a base had been parsed yet, which is the fragility
|
|
1178
|
+
// being removed. G1 — every plain declaration, from every source, precedes
|
|
1179
|
+
// every overlay — only holds if the queue is unconditional.
|
|
1180
|
+
//
|
|
1181
|
+
// This node is the OUTERMOST overlay on this branch and we do NOT descend
|
|
1182
|
+
// into it, so its whole subtree — nested overlays included — rides along and
|
|
1183
|
+
// is applied as one unit (G3). Returning undefined is what keeps the caller
|
|
1184
|
+
// from addChild-ing a node that was never created; the overlay contributes
|
|
1185
|
+
// nothing until the drain.
|
|
1186
|
+
{
|
|
1187
|
+
if (_pendingOverlays !== undefined) {
|
|
1188
|
+
_pendingOverlays.push({
|
|
1189
|
+
type,
|
|
1190
|
+
subType,
|
|
1191
|
+
name,
|
|
1192
|
+
nodeData,
|
|
1193
|
+
parent,
|
|
1194
|
+
accumRoot,
|
|
1195
|
+
inheritedContextPkg,
|
|
1196
|
+
sourceName: source,
|
|
1197
|
+
sourceId: _currentSourceId ?? "<unknown>",
|
|
1198
|
+
path,
|
|
1199
|
+
errorSource: errSource(),
|
|
1200
|
+
pathSegments: _currentPath?.snapshot() ?? [],
|
|
1201
|
+
format: _currentFormat,
|
|
1202
|
+
...(_currentYamlPosition !== undefined ? { yamlPosition: _currentYamlPosition } : {}),
|
|
1203
|
+
});
|
|
1204
|
+
return undefined;
|
|
1205
|
+
}
|
|
1206
|
+
// No queue means we are already INSIDE applyPendingOverlays, applying a
|
|
1207
|
+
// queued unit. A nested overlay within that unit resolves find-or-fail
|
|
1208
|
+
// right here (§2.2): by now its parent is complete, and there is no later
|
|
1209
|
+
// pass left to defer to. This is also the door a caller outside any
|
|
1210
|
+
// buildTree run would take.
|
|
1211
|
+
const nested = findOverlayTarget(parent, type, name, nodeData, inheritedContextPkg);
|
|
1212
|
+
if (nested === undefined) {
|
|
1213
|
+
errors.push(
|
|
1214
|
+
new ParseError(overlayNoTargetMessage(type, name), {
|
|
1215
|
+
code: "ERR_OVERLAY_NO_TARGET",
|
|
1216
|
+
source: errSource(),
|
|
1217
|
+
}),
|
|
1218
|
+
);
|
|
1219
|
+
return undefined;
|
|
1220
|
+
}
|
|
1221
|
+
nested.setIsMerge(true);
|
|
1222
|
+
parseNodeInto(nodeData, nested, accumRoot, inheritedContextPkg, registry, warnings, errors, strict, source, path);
|
|
1223
|
+
return nested;
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
// Only the non-overlay path needs the target here; a queued overlay resolves
|
|
1228
|
+
// its own target at application time, against the completed tree.
|
|
1229
|
+
const existing = findOverlayTarget(parent, type, name, nodeData, inheritedContextPkg);
|
|
1230
|
+
|
|
1231
|
+
// Default: no operator → silently reuse existing or create new.
|
|
1232
|
+
if (existing !== undefined) {
|
|
1233
|
+
parseNodeInto(nodeData, existing, accumRoot, inheritedContextPkg, registry, warnings, errors, strict, source, path);
|
|
1234
|
+
return existing;
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
// Not found (or unnamed) → create new
|
|
1238
|
+
return parseNodeFresh(type, subType, nodeData, accumRoot, inheritedContextPkg, registry, warnings, errors, strict, source, path, parent.type, parent);
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
// ---------------------------------------------------------------------------
|
|
1242
|
+
// ADR-0055 — deferred overlay application.
|
|
1243
|
+
// ---------------------------------------------------------------------------
|
|
1244
|
+
|
|
1245
|
+
/** The node an `overlay: true` declaration re-opens, or undefined.
|
|
1246
|
+
*
|
|
1247
|
+
* ADR-0039 sanctioned own read: an overlay targets the AUTHORED declaration
|
|
1248
|
+
* layer (an own child), never the resolved/inherited view.
|
|
1249
|
+
*
|
|
1250
|
+
* ROOT-LEVEL lookups are PACKAGE-QUALIFIED: two files declaring the same
|
|
1251
|
+
* (type, name) under different packages are DISTINCT root nodes, never a merge
|
|
1252
|
+
* pair (mirrors the Java parser, which searches root children by "pkg::name").
|
|
1253
|
+
* Nested children stay bare-name matched — they are scoped by their parent, and
|
|
1254
|
+
* packages do not disambiguate siblings inside a node.
|
|
1255
|
+
*
|
|
1256
|
+
* One implementation, shared by the walk and by {@link applyPendingOverlays},
|
|
1257
|
+
* so "what does this overlay target" cannot drift between the two. */
|
|
1258
|
+
function findOverlayTarget(
|
|
1259
|
+
parent: MetaData,
|
|
1260
|
+
type: string,
|
|
1261
|
+
name: string,
|
|
1262
|
+
nodeData: Record<string, unknown>,
|
|
1263
|
+
inheritedContextPkg: string,
|
|
1264
|
+
): MetaData | undefined {
|
|
1073
1265
|
let existing = name !== "" ? parent.ownChildByTypeAndName(type, name) : undefined;
|
|
1074
1266
|
if (existing !== undefined && parent instanceof MetaRoot) {
|
|
1075
1267
|
const candidateKey = rootChildResolutionKey(nodeData, inheritedContextPkg, name);
|
|
@@ -1081,27 +1273,119 @@ function createOrFindMetaData(
|
|
|
1081
1273
|
.find((c) => c.type === type && c.name === name && c.resolutionKey() === candidateKey);
|
|
1082
1274
|
}
|
|
1083
1275
|
}
|
|
1276
|
+
return existing;
|
|
1277
|
+
}
|
|
1084
1278
|
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
return
|
|
1279
|
+
/** The one wording for a missing overlay target, shared by the defensive
|
|
1280
|
+
* in-walk path and the deferred pass, so the two cannot drift. */
|
|
1281
|
+
function overlayNoTargetMessage(type: string, name: string): string {
|
|
1282
|
+
return `Overlay operation requested for [${type}:${name}] but no existing metadata found to merge into`;
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
/** The declaration's own address, for the resolved envelope's `referrer`. */
|
|
1286
|
+
function overlayReferrer(item: PendingOverlay): string {
|
|
1287
|
+
if (item.parent instanceof MetaRoot) {
|
|
1288
|
+
return rootChildResolutionKey(item.nodeData, item.inheritedContextPkg, item.name);
|
|
1095
1289
|
}
|
|
1290
|
+
// ADR-0029 addressing — a nested overlay is named relative to its parent.
|
|
1291
|
+
return `${item.parent.name}.${item.name}`;
|
|
1292
|
+
}
|
|
1096
1293
|
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1294
|
+
/**
|
|
1295
|
+
* ADR-0055 — apply queued `overlay: true` declarations against the complete tree.
|
|
1296
|
+
*
|
|
1297
|
+
* Called once, after every source has been parsed and BEFORE deferred super
|
|
1298
|
+
* resolution (G5), with every source's queue concatenated in parse order — which
|
|
1299
|
+
* is already "source order, then declaration order within a source" (G2).
|
|
1300
|
+
*
|
|
1301
|
+
* Each element is applied independently: a missing target is recorded and the
|
|
1302
|
+
* element skipped, so one bad overlay no longer takes its whole source down with
|
|
1303
|
+
* it (the eager throw aborted the entire document, losing every sibling
|
|
1304
|
+
* declaration and cascading into ERR_UNRESOLVED_SUPER).
|
|
1305
|
+
*/
|
|
1306
|
+
export function applyPendingOverlays(
|
|
1307
|
+
pending: readonly PendingOverlay[],
|
|
1308
|
+
opts: { registry: TypeRegistry; strict?: boolean },
|
|
1309
|
+
): { errors: ParseError[]; warnings: string[]; envelopeWarnings: LoaderWarning[] } {
|
|
1310
|
+
const errors: ParseError[] = [];
|
|
1311
|
+
const warnings: string[] = [];
|
|
1312
|
+
const envelopeWarnings: LoaderWarning[] = [];
|
|
1313
|
+
const strict = opts.strict ?? false;
|
|
1314
|
+
|
|
1315
|
+
for (const item of pending) {
|
|
1316
|
+
try {
|
|
1317
|
+
// Re-enter the walk state this declaration was queued under. The walk that
|
|
1318
|
+
// built it has unwound, so without this anything constructed now would be
|
|
1319
|
+
// stamped with the wrong provenance and an error would name the wrong
|
|
1320
|
+
// location (or none at all).
|
|
1321
|
+
_currentPath = JsonPathBuilder.fromSegments(item.pathSegments);
|
|
1322
|
+
_currentSourceId = item.sourceId;
|
|
1323
|
+
_currentFormat = item.format;
|
|
1324
|
+
_currentYamlPosition = item.yamlPosition;
|
|
1325
|
+
_currentErrors = errors;
|
|
1326
|
+
_currentEnvelopeWarnings = envelopeWarnings;
|
|
1327
|
+
// Anything the overlay contributes may `extends` a node in any source; the
|
|
1328
|
+
// loader resolves every ref after this pass.
|
|
1329
|
+
_deferSuperResolution = true;
|
|
1330
|
+
|
|
1331
|
+
const target = findOverlayTarget(
|
|
1332
|
+
item.parent,
|
|
1333
|
+
item.type,
|
|
1334
|
+
item.name,
|
|
1335
|
+
item.nodeData,
|
|
1336
|
+
item.inheritedContextPkg,
|
|
1337
|
+
);
|
|
1338
|
+
if (target === undefined) {
|
|
1339
|
+
errors.push(
|
|
1340
|
+
new ParseError(overlayNoTargetMessage(item.type, item.name), {
|
|
1341
|
+
code: "ERR_OVERLAY_NO_TARGET",
|
|
1342
|
+
// ADR-0009 FR5d — a reference that did not resolve, reported with the
|
|
1343
|
+
// declaration's own files/jsonPath.
|
|
1344
|
+
source: resolvedSource(
|
|
1345
|
+
item.errorSource,
|
|
1346
|
+
overlayReferrer(item),
|
|
1347
|
+
`${item.type}:${item.name}`,
|
|
1348
|
+
),
|
|
1349
|
+
}),
|
|
1350
|
+
);
|
|
1351
|
+
continue; // parseNodeInto was never entered — no partial state to unwind
|
|
1352
|
+
}
|
|
1353
|
+
target.setIsMerge(true);
|
|
1354
|
+
parseNodeInto(
|
|
1355
|
+
item.nodeData,
|
|
1356
|
+
target,
|
|
1357
|
+
item.accumRoot,
|
|
1358
|
+
item.inheritedContextPkg,
|
|
1359
|
+
opts.registry,
|
|
1360
|
+
warnings,
|
|
1361
|
+
errors,
|
|
1362
|
+
strict,
|
|
1363
|
+
item.sourceName,
|
|
1364
|
+
item.path,
|
|
1365
|
+
);
|
|
1366
|
+
} catch (err) {
|
|
1367
|
+
// Per-element, as the loader already does per-source: a strict-mode
|
|
1368
|
+
// reportProblem or a registry error must not abandon the remaining queue.
|
|
1369
|
+
errors.push(
|
|
1370
|
+
err instanceof ParseError
|
|
1371
|
+
? err
|
|
1372
|
+
: new ParseError(
|
|
1373
|
+
`Failed to apply overlay for [${item.type}:${item.name}]: ${String(err)}`,
|
|
1374
|
+
{ code: "ERR_UNKNOWN", source: item.errorSource },
|
|
1375
|
+
),
|
|
1376
|
+
);
|
|
1377
|
+
} finally {
|
|
1378
|
+
_currentPath = undefined;
|
|
1379
|
+
_currentSourceId = undefined;
|
|
1380
|
+
_currentFormat = "json";
|
|
1381
|
+
_currentYamlPosition = undefined;
|
|
1382
|
+
_currentErrors = undefined;
|
|
1383
|
+
_currentEnvelopeWarnings = undefined;
|
|
1384
|
+
_deferSuperResolution = false;
|
|
1385
|
+
}
|
|
1101
1386
|
}
|
|
1102
1387
|
|
|
1103
|
-
|
|
1104
|
-
return parseNodeFresh(type, subType, nodeData, accumRoot, inheritedContextPkg, registry, warnings, errors, strict, source, path, parent.type, parent);
|
|
1388
|
+
return { errors, warnings, envelopeWarnings };
|
|
1105
1389
|
}
|
|
1106
1390
|
|
|
1107
1391
|
// ---------------------------------------------------------------------------
|
package/src/scope.ts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// server/typescript/packages/metadata/src/scope.ts
|
|
2
|
+
//
|
|
3
|
+
// FR-023 §4.3 — the scope-pattern grammar. Moved here from
|
|
4
|
+
// `@metaobjectsdev/sdk` so `codegen-ts`'s publisher generator
|
|
5
|
+
// (`sharedModelFile()`) can select its exports with the same patterns
|
|
6
|
+
// without taking a dependency on sdk. `@metaobjectsdev/sdk` re-exports
|
|
7
|
+
// `compileScope` / `matchesScope` / `Scope` / `CompiledScope` from here
|
|
8
|
+
// unchanged, so existing importers of the scope API from sdk keep working.
|
|
9
|
+
//
|
|
10
|
+
// A pure, no-I/O module deciding whether a fully-qualified node name falls
|
|
11
|
+
// inside a consumer's declared `include`/`exclude` scope. Source resolution
|
|
12
|
+
// and discovery (later phase-1 tasks) build on this; a cross-language
|
|
13
|
+
// conformance corpus pins its semantics, so exact pattern behavior matters.
|
|
14
|
+
//
|
|
15
|
+
// Uses no `node:` imports — stays browser-safe like the rest of the root
|
|
16
|
+
// entry (see `test/browser-safety.test.ts`).
|
|
17
|
+
import { PACKAGE_SEPARATOR } from "./shared/structural.js";
|
|
18
|
+
import { ParseError } from "./errors.js";
|
|
19
|
+
import { codeSource } from "./source.js";
|
|
20
|
+
|
|
21
|
+
/** A consumer-side output filter over fully-qualified node names. */
|
|
22
|
+
export interface Scope {
|
|
23
|
+
/** Absent or empty means "everything". */
|
|
24
|
+
readonly include?: readonly string[];
|
|
25
|
+
/** Applied after `include`. */
|
|
26
|
+
readonly exclude?: readonly string[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface CompiledScope {
|
|
30
|
+
readonly include: readonly RegExp[];
|
|
31
|
+
readonly exclude: readonly RegExp[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** One package segment: any run of characters containing no separator char. */
|
|
35
|
+
const SEGMENT = "[^:]+";
|
|
36
|
+
/** One or more segments, separator-joined — the `**` expansion. */
|
|
37
|
+
const SEGMENTS = `${SEGMENT}(?:${PACKAGE_SEPARATOR}${SEGMENT})*`;
|
|
38
|
+
|
|
39
|
+
function escapeLiteral(text: string): string {
|
|
40
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Compile one segment. `**` spans segments; `*` never crosses a separator. */
|
|
44
|
+
function compileSegment(segment: string, pattern: string): string {
|
|
45
|
+
if (segment.length === 0) {
|
|
46
|
+
throw new ParseError(`empty segment in scope pattern "${pattern}"`, {
|
|
47
|
+
code: "ERR_SCOPE_PATTERN_INVALID",
|
|
48
|
+
source: codeSource("compileSegment"),
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
// A segment surviving the split on the two-character PACKAGE_SEPARATOR
|
|
52
|
+
// ("::") can still contain a lone ":" when the pattern has an odd colon
|
|
53
|
+
// run — e.g. "acme:::Order".split("::") => ["acme", ":Order"]. SEGMENT
|
|
54
|
+
// ([^:]+) already excludes ":" from a well-formed segment, so a leftover
|
|
55
|
+
// ":" here means the separator was malformed, not that ":" is meant
|
|
56
|
+
// literally. Left unchecked, escapeLiteral treats it as a literal
|
|
57
|
+
// character and compiles a regex requiring three colons in a row — which
|
|
58
|
+
// no legal "::"-joined fully-qualified name can ever contain, so the
|
|
59
|
+
// pattern silently matches nothing instead of failing loud.
|
|
60
|
+
if (segment.includes(":")) {
|
|
61
|
+
throw new ParseError(
|
|
62
|
+
`scope pattern "${pattern}" has a malformed separator (an odd run of ":") — segments are joined by "::", never a single ":"`,
|
|
63
|
+
{ code: "ERR_SCOPE_PATTERN_INVALID", source: codeSource("compileSegment") },
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
if (segment === "**") return `(?:${SEGMENTS})`;
|
|
67
|
+
// `*` inside a segment matches any characters except the separator char.
|
|
68
|
+
return segment.split("*").map(escapeLiteral).join("[^:]*");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function compilePattern(pattern: string): RegExp {
|
|
72
|
+
if (pattern.length === 0) {
|
|
73
|
+
throw new ParseError(`scope pattern must not be empty`, {
|
|
74
|
+
code: "ERR_SCOPE_PATTERN_INVALID",
|
|
75
|
+
source: codeSource("compilePattern"),
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
const body = pattern
|
|
79
|
+
.split(PACKAGE_SEPARATOR)
|
|
80
|
+
.map((segment) => compileSegment(segment, pattern))
|
|
81
|
+
.join(PACKAGE_SEPARATOR);
|
|
82
|
+
return new RegExp(`^${body}$`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function compileScope(scope: Scope): CompiledScope {
|
|
86
|
+
return {
|
|
87
|
+
include: (scope.include ?? []).map(compilePattern),
|
|
88
|
+
exclude: (scope.exclude ?? []).map(compilePattern),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** True when `fqn` is inside the scope. An empty `include` means everything. */
|
|
93
|
+
export function matchesScope(fqn: string, compiled: CompiledScope): boolean {
|
|
94
|
+
const included = compiled.include.length === 0 || compiled.include.some((re) => re.test(fqn));
|
|
95
|
+
if (!included) return false;
|
|
96
|
+
return !compiled.exclude.some((re) => re.test(fqn));
|
|
97
|
+
}
|
package/src/serializer-json.ts
CHANGED
|
@@ -32,7 +32,8 @@ import {
|
|
|
32
32
|
DEFAULT_SOURCE_KIND,
|
|
33
33
|
PHYSICAL_NAME_ATTR_BY_KIND,
|
|
34
34
|
} from "./persistence/source/source-constants.js";
|
|
35
|
-
import { TYPE_SOURCE } from "./shared/base-types.js";
|
|
35
|
+
import { TYPE_METADATA, TYPE_SOURCE, SUBTYPE_ROOT } from "./shared/base-types.js";
|
|
36
|
+
import { packageOfResolutionKey } from "./naming.js";
|
|
36
37
|
|
|
37
38
|
const SOURCE_RDB_FUSED_KEY = `${TYPE_SOURCE}${TYPE_SUBTYPE_SEPARATOR}${SOURCE_SUBTYPE_RDB}`;
|
|
38
39
|
|
|
@@ -301,3 +302,47 @@ function sortAttrValue(value: unknown): unknown {
|
|
|
301
302
|
}
|
|
302
303
|
return value;
|
|
303
304
|
}
|
|
305
|
+
|
|
306
|
+
// ---------------------------------------------------------------------------
|
|
307
|
+
// serializeSharedDocument — the FR-023 shared-model artifact form
|
|
308
|
+
//
|
|
309
|
+
// One canonical-JSON `metadata.root` document holding top-level nodes from any
|
|
310
|
+
// number of packages: NO root `package`, and every top-level node carries its own
|
|
311
|
+
// explicit `package` (a root-level child may name its package — ADR-0029's
|
|
312
|
+
// addressing model — so the document re-loads to the same resolution keys in
|
|
313
|
+
// every port). Each node is its canonicalSerialize form: raw own-layer, `extends`
|
|
314
|
+
// preserved (not flattened), attribute keys alphabetized, the FR-016 physical-name
|
|
315
|
+
// rewrite applied. Top-level nodes are sorted by resolution key; each node's
|
|
316
|
+
// children keep their authored order. Body key order: name, package, then the
|
|
317
|
+
// canonical rest. Byte-identical to Python's `serialize_shared_document`.
|
|
318
|
+
// ---------------------------------------------------------------------------
|
|
319
|
+
|
|
320
|
+
export function serializeSharedDocument(nodes: readonly MetaData[]): string {
|
|
321
|
+
const sorted = [...nodes].sort((a, b) => {
|
|
322
|
+
const ka = a.resolutionKey();
|
|
323
|
+
const kb = b.resolutionKey();
|
|
324
|
+
return ka < kb ? -1 : ka > kb ? 1 : 0;
|
|
325
|
+
});
|
|
326
|
+
const children = sorted.map((node) => {
|
|
327
|
+
const pkg = packageOfResolutionKey(node.resolutionKey());
|
|
328
|
+
if (pkg === "") {
|
|
329
|
+
throw new Error(
|
|
330
|
+
`serializeSharedDocument: ${node.resolutionKey()} has no package; a shared document carries only packaged nodes`,
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
const parsed = JSON.parse(canonicalSerialize(node)) as Record<string, Record<string, unknown>>;
|
|
334
|
+
const [fused, body] = Object.entries(parsed)[0]!;
|
|
335
|
+
// The node's own `package` (if it declared one) is replaced by the RESOLVED
|
|
336
|
+
// one: a node inheriting its file's root package declares none of its own.
|
|
337
|
+
const ordered: Record<string, unknown> = {
|
|
338
|
+
[RESERVED_KEY_NAME]: body[RESERVED_KEY_NAME],
|
|
339
|
+
[RESERVED_KEY_PACKAGE]: pkg,
|
|
340
|
+
};
|
|
341
|
+
for (const [key, value] of Object.entries(body)) {
|
|
342
|
+
if (key !== RESERVED_KEY_NAME && key !== RESERVED_KEY_PACKAGE) ordered[key] = value;
|
|
343
|
+
}
|
|
344
|
+
return { [fused]: ordered };
|
|
345
|
+
});
|
|
346
|
+
const doc = { [fusedKey(TYPE_METADATA, SUBTYPE_ROOT)]: { [RESERVED_KEY_CHILDREN]: children } };
|
|
347
|
+
return JSON.stringify(doc, null, 2) + "\n";
|
|
348
|
+
}
|