@metaobjectsdev/metadata 0.6.0 → 0.7.0-rc.2
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 +54 -3
- package/dist/attr-schema-validate.js +7 -7
- package/dist/attr-schema-validate.js.map +1 -1
- package/dist/core/export-json.d.ts +6 -7
- package/dist/core/export-json.d.ts.map +1 -1
- package/dist/core/export-json.js +15 -17
- package/dist/core/export-json.js.map +1 -1
- package/dist/core/index.d.ts +4 -2
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +6 -2
- package/dist/core/index.js.map +1 -1
- package/dist/core/parser-yaml.d.ts.map +1 -1
- package/dist/core/parser-yaml.js +36 -11
- package/dist/core/parser-yaml.js.map +1 -1
- package/dist/core/yaml-desugar.d.ts.map +1 -1
- package/dist/core/yaml-desugar.js +54 -5
- package/dist/core/yaml-desugar.js.map +1 -1
- package/dist/core/yaml-positions-walker.d.ts +21 -0
- package/dist/core/yaml-positions-walker.d.ts.map +1 -0
- package/dist/core/yaml-positions-walker.js +75 -0
- package/dist/core/yaml-positions-walker.js.map +1 -0
- package/dist/core/yaml-positions.d.ts +19 -0
- package/dist/core/yaml-positions.d.ts.map +1 -0
- package/dist/core/yaml-positions.js +60 -0
- package/dist/core/yaml-positions.js.map +1 -0
- package/dist/errors.d.ts +32 -9
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +44 -5
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +6 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -2
- package/dist/index.js.map +1 -1
- package/dist/json-path.d.ts +8 -0
- package/dist/json-path.d.ts.map +1 -0
- package/dist/json-path.js +39 -0
- package/dist/json-path.js.map +1 -0
- package/dist/loader/meta-data-loader.d.ts +47 -6
- package/dist/loader/meta-data-loader.d.ts.map +1 -1
- package/dist/loader/meta-data-loader.js +126 -8
- package/dist/loader/meta-data-loader.js.map +1 -1
- package/dist/loader/meta-data-source.d.ts +6 -2
- package/dist/loader/meta-data-source.d.ts.map +1 -1
- package/dist/loader/meta-data-source.js +10 -6
- package/dist/loader/meta-data-source.js.map +1 -1
- package/dist/loader/shortcuts.d.ts +9 -0
- package/dist/loader/shortcuts.d.ts.map +1 -0
- package/dist/loader/shortcuts.js +19 -0
- package/dist/loader/shortcuts.js.map +1 -0
- package/dist/loader/sources/directory-source.d.ts +15 -0
- package/dist/loader/sources/directory-source.d.ts.map +1 -0
- package/dist/loader/sources/directory-source.js +80 -0
- package/dist/loader/sources/directory-source.js.map +1 -0
- package/dist/loader/sources/file-source.d.ts +12 -0
- package/dist/loader/sources/file-source.d.ts.map +1 -0
- package/dist/loader/sources/file-source.js +46 -0
- package/dist/loader/sources/file-source.js.map +1 -0
- package/dist/loader/sources/index.d.ts +5 -0
- package/dist/loader/sources/index.d.ts.map +1 -0
- package/dist/loader/sources/index.js +5 -0
- package/dist/loader/sources/index.js.map +1 -0
- package/dist/loader/sources/uri-source.d.ts +9 -0
- package/dist/loader/sources/uri-source.d.ts.map +1 -0
- package/dist/loader/sources/uri-source.js +42 -0
- package/dist/loader/sources/uri-source.js.map +1 -0
- package/dist/loader/validation-passes.d.ts.map +1 -1
- package/dist/loader/validation-passes.js +92 -28
- package/dist/loader/validation-passes.js.map +1 -1
- package/dist/naming.d.ts +15 -2
- package/dist/naming.d.ts.map +1 -1
- package/dist/naming.js +20 -6
- package/dist/naming.js.map +1 -1
- package/dist/parser-core.d.ts +17 -4
- package/dist/parser-core.d.ts.map +1 -1
- package/dist/parser-core.js +371 -44
- package/dist/parser-core.js.map +1 -1
- package/dist/parser-json.d.ts.map +1 -1
- package/dist/parser-json.js +10 -2
- package/dist/parser-json.js.map +1 -1
- package/dist/persistence/source/validate-source-roles.js +2 -2
- package/dist/persistence/source/validate-source-roles.js.map +1 -1
- package/dist/semantic-diff.d.ts +5 -0
- package/dist/semantic-diff.d.ts.map +1 -0
- package/dist/semantic-diff.js +49 -0
- package/dist/semantic-diff.js.map +1 -0
- package/dist/shared/meta-data.d.ts +10 -0
- package/dist/shared/meta-data.d.ts.map +1 -1
- package/dist/shared/meta-data.js +23 -0
- package/dist/shared/meta-data.js.map +1 -1
- package/dist/source.d.ts +96 -0
- package/dist/source.d.ts.map +1 -0
- package/dist/source.js +38 -0
- package/dist/source.js.map +1 -0
- package/dist/subtype-rules.js +1 -1
- package/dist/subtype-rules.js.map +1 -1
- package/dist/super-resolve.d.ts +2 -0
- package/dist/super-resolve.d.ts.map +1 -1
- package/dist/super-resolve.js +1 -1
- package/dist/super-resolve.js.map +1 -1
- package/package.json +1 -1
- package/src/attr-schema-validate.ts +7 -7
- package/src/core/export-json.ts +15 -18
- package/src/core/index.ts +8 -2
- package/src/core/parser-yaml.ts +38 -11
- package/src/core/yaml-desugar.ts +58 -4
- package/src/core/yaml-positions-walker.ts +101 -0
- package/src/core/yaml-positions.ts +80 -0
- package/src/errors.ts +57 -8
- package/src/index.ts +28 -3
- package/src/json-path.ts +46 -0
- package/src/loader/meta-data-loader.ts +168 -10
- package/src/loader/meta-data-source.ts +10 -6
- package/src/loader/shortcuts.ts +31 -0
- package/src/loader/sources/directory-source.ts +90 -0
- package/src/{core → loader/sources}/file-source.ts +3 -3
- package/src/loader/sources/index.ts +6 -0
- package/src/loader/sources/uri-source.ts +44 -0
- package/src/loader/validation-passes.ts +96 -29
- package/src/naming.ts +39 -7
- package/src/parser-core.ts +412 -46
- package/src/parser-json.ts +11 -2
- package/src/persistence/source/validate-source-roles.ts +2 -2
- package/src/semantic-diff.ts +48 -0
- package/src/shared/meta-data.ts +28 -0
- package/src/source.ts +99 -0
- package/src/subtype-rules.ts +1 -1
- package/src/super-resolve.ts +3 -1
- package/src/core/file-meta-data-loader.ts +0 -89
package/src/parser-json.ts
CHANGED
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
// the canonical object to the shared buildTree (parser-core.ts).
|
|
5
5
|
|
|
6
6
|
import { ParseError } from "./errors.js";
|
|
7
|
-
import { buildTree
|
|
7
|
+
import { buildTree } from "./parser-core.js";
|
|
8
8
|
import type { ParseOptions, ParseResult } from "./parser-core.js";
|
|
9
|
+
import type { ErrorSource } from "./source.js";
|
|
9
10
|
|
|
10
11
|
export type { ParseOptions, ParseResult } from "./parser-core.js";
|
|
11
12
|
|
|
@@ -18,9 +19,17 @@ export function parseJson(content: string, opts: ParseOptions): ParseResult {
|
|
|
18
19
|
try {
|
|
19
20
|
parsed = JSON.parse(normalizedContent);
|
|
20
21
|
} catch (err) {
|
|
22
|
+
// FR5a / ADR-0009 — pre-buildTree errors can't reach the parser's
|
|
23
|
+
// module-level JsonPathBuilder; build a minimal json-source envelope
|
|
24
|
+
// rooted at "$" so cross-port callers see a consistent shape.
|
|
25
|
+
const source: ErrorSource = {
|
|
26
|
+
format: "json",
|
|
27
|
+
files: [opts.sourceName ?? "<unknown>"],
|
|
28
|
+
jsonPath: "$",
|
|
29
|
+
};
|
|
21
30
|
throw new ParseError(
|
|
22
31
|
`Invalid JSON: ${(err as Error).message}`,
|
|
23
|
-
{
|
|
32
|
+
{ code: "ERR_MALFORMED_JSON", source },
|
|
24
33
|
);
|
|
25
34
|
}
|
|
26
35
|
|
|
@@ -36,14 +36,14 @@ export function validateSourceRoles(root: MetaData): ParseError[] {
|
|
|
36
36
|
errors.push(
|
|
37
37
|
new ParseError(
|
|
38
38
|
`object "${obj.name}" declares ${sources.length} source(s) but none has role "${SOURCE_ROLE_PRIMARY}"`,
|
|
39
|
-
{ code: "ERR_SOURCE_NO_PRIMARY" },
|
|
39
|
+
{ code: "ERR_SOURCE_NO_PRIMARY", source: obj.source },
|
|
40
40
|
),
|
|
41
41
|
);
|
|
42
42
|
} else if (primaryCount > 1) {
|
|
43
43
|
errors.push(
|
|
44
44
|
new ParseError(
|
|
45
45
|
`object "${obj.name}" declares ${primaryCount} sources with role "${SOURCE_ROLE_PRIMARY}"; exactly one is required`,
|
|
46
|
-
{ code: "ERR_SOURCE_MULTIPLE_PRIMARY" },
|
|
46
|
+
{ code: "ERR_SOURCE_MULTIPLE_PRIMARY", source: obj.source },
|
|
47
47
|
),
|
|
48
48
|
);
|
|
49
49
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// server/typescript/packages/metadata/src/semantic-diff.ts
|
|
2
|
+
//
|
|
3
|
+
// FR5a / ADR-0009 — Cross-port-aligned semantic-equality compare for metadata
|
|
4
|
+
// trees. Returns `true` if the two inputs differ in any semantically-meaningful
|
|
5
|
+
// way (excluding `source`, which is loader output).
|
|
6
|
+
//
|
|
7
|
+
// Algorithm (ADR-0009 §semantic_diff):
|
|
8
|
+
// 1. Sort attrs lexicographically; compare attr-by-attr; values by canonical
|
|
9
|
+
// structural equality (key-order independent, whitespace-insensitive).
|
|
10
|
+
// 2. Children are compared as ordered sequences.
|
|
11
|
+
// 3. Reserved structural keys (name, package, extends, abstract, overlay,
|
|
12
|
+
// isArray, value) participate like attrs.
|
|
13
|
+
// 4. `source` excluded from the diff.
|
|
14
|
+
|
|
15
|
+
type Tree = Record<string, unknown>;
|
|
16
|
+
|
|
17
|
+
const EXCLUDED = new Set(["source"]);
|
|
18
|
+
|
|
19
|
+
function isObject(v: unknown): v is Tree {
|
|
20
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function equal(a: unknown, b: unknown): boolean {
|
|
24
|
+
if (a === b) return true;
|
|
25
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
26
|
+
if (a.length !== b.length) return false;
|
|
27
|
+
for (let i = 0; i < a.length; i++) {
|
|
28
|
+
if (!equal(a[i], b[i])) return false;
|
|
29
|
+
}
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
if (isObject(a) && isObject(b)) {
|
|
33
|
+
const aKeys = Object.keys(a).filter((k) => !EXCLUDED.has(k)).sort();
|
|
34
|
+
const bKeys = Object.keys(b).filter((k) => !EXCLUDED.has(k)).sort();
|
|
35
|
+
if (aKeys.length !== bKeys.length) return false;
|
|
36
|
+
for (let i = 0; i < aKeys.length; i++) {
|
|
37
|
+
if (aKeys[i] !== bKeys[i]) return false;
|
|
38
|
+
if (!equal(a[aKeys[i]!], b[bKeys[i]!])) return false;
|
|
39
|
+
}
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Returns `true` if the inputs differ in any semantically-meaningful way. */
|
|
46
|
+
export function semanticDiff(a: Tree, b: Tree): boolean {
|
|
47
|
+
return !equal(a, b);
|
|
48
|
+
}
|
package/src/shared/meta-data.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type { DataType } from "../data-type.js";
|
|
|
5
5
|
import type { MetaAttr } from "../core/attr/meta-attr.js";
|
|
6
6
|
import { inferAttrSubType } from "../serializer-json.js";
|
|
7
7
|
import { attrClassFor } from "../attr-class-map.js";
|
|
8
|
+
import type { ErrorSource } from "../source.js";
|
|
8
9
|
|
|
9
10
|
export type AttrValue = string | number | boolean | string[] | AttrObject;
|
|
10
11
|
|
|
@@ -57,6 +58,14 @@ export abstract class MetaData {
|
|
|
57
58
|
// construction (for field/attr nodes). Read via MetaField/MetaAttr.dataType.
|
|
58
59
|
protected _dataType?: DataType;
|
|
59
60
|
|
|
61
|
+
// ADR-0009 provenance. Always populated; defaults to `{ format: "code" }`
|
|
62
|
+
// for programmatic construction (tests, factories, in-code builders). The
|
|
63
|
+
// JSON parser overwrites via setSource() during tree walk; future phases
|
|
64
|
+
// (overlay merge, super resolution) may overwrite with merged/resolved
|
|
65
|
+
// variants. Excluded from canonical JSON serialization — loader-derived
|
|
66
|
+
// state, not metadata.
|
|
67
|
+
private _source: ErrorSource = { format: "code" };
|
|
68
|
+
|
|
60
69
|
// Per-instance read cache: only populated once the node is frozen.
|
|
61
70
|
private readonly _cache = new Map<string, unknown>();
|
|
62
71
|
|
|
@@ -171,6 +180,25 @@ export abstract class MetaData {
|
|
|
171
180
|
this._dataType = dt;
|
|
172
181
|
}
|
|
173
182
|
|
|
183
|
+
// ---------------------------------------------------------------------------
|
|
184
|
+
// source (ADR-0009 provenance)
|
|
185
|
+
// ---------------------------------------------------------------------------
|
|
186
|
+
|
|
187
|
+
/** Provenance envelope for this node. Always populated. See ADR-0009. */
|
|
188
|
+
get source(): ErrorSource {
|
|
189
|
+
return this._source;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Loader-internal: assign provenance. Called by parser and merge phases as
|
|
194
|
+
* they build the tree. Honors the frozen-guard like other setters.
|
|
195
|
+
* @internal
|
|
196
|
+
*/
|
|
197
|
+
setSource(s: ErrorSource): void {
|
|
198
|
+
this._assertNotFrozen();
|
|
199
|
+
this._source = s;
|
|
200
|
+
}
|
|
201
|
+
|
|
174
202
|
// ---------------------------------------------------------------------------
|
|
175
203
|
// isAbstract
|
|
176
204
|
// ---------------------------------------------------------------------------
|
package/src/source.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// server/typescript/packages/metadata/src/source.ts
|
|
2
|
+
//
|
|
3
|
+
// FR5a — Loader error envelope + source-on-node (ADR-0009).
|
|
4
|
+
//
|
|
5
|
+
// Cross-port-aligned types: every metaobjects port emits the same envelope
|
|
6
|
+
// shape so a tool consuming errors from multiple language ports can compare
|
|
7
|
+
// them byte-identically.
|
|
8
|
+
|
|
9
|
+
/** Discriminated union over the provenance variants a metadata node or error
|
|
10
|
+
* can carry. See ADR-0009 §Decision for the canonical shape.
|
|
11
|
+
*
|
|
12
|
+
* FR5b note (finalized 2026-05-27, all four ports): buildTree-emitted errors
|
|
13
|
+
* from a YAML input emit `format: "yaml"` and carry the optional
|
|
14
|
+
* `yamlPosition` (line/col from the desugar's position map). The
|
|
15
|
+
* `yamlPosition` field is also declared optionally on the `format: "json"`
|
|
16
|
+
* variant for backward shape-compat with any FR5b-interim envelopes still
|
|
17
|
+
* serialized to disk; new YAML errors no longer use that variant. */
|
|
18
|
+
export type ErrorSource =
|
|
19
|
+
| { format: "json"; files: [string]; jsonPath: string;
|
|
20
|
+
yamlPosition?: { line: number; col: number } }
|
|
21
|
+
| { format: "yaml"; files: [string]; jsonPath: string;
|
|
22
|
+
yamlPosition?: { line: number; col: number } }
|
|
23
|
+
| { format: "merged"; files: string[]; jsonPath: string;
|
|
24
|
+
contributors: Contributor[] }
|
|
25
|
+
| { format: "resolved"; files: string[]; jsonPath?: string;
|
|
26
|
+
referrer?: string; target?: string }
|
|
27
|
+
| { format: "database"; dbLocation: { table: string; id: string };
|
|
28
|
+
jsonPath?: string }
|
|
29
|
+
| { format: "code"; caller?: string };
|
|
30
|
+
|
|
31
|
+
export interface Contributor {
|
|
32
|
+
file: string;
|
|
33
|
+
role: "overlay-base" | "overlay-extension" | "extends-base" | "extends-extension";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface NodeContext {
|
|
37
|
+
type?: string;
|
|
38
|
+
subtype?: string;
|
|
39
|
+
name?: string;
|
|
40
|
+
fqn?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Envelope shape every loader error conforms to. */
|
|
44
|
+
export interface LoaderError {
|
|
45
|
+
// REQUIRED — conformance-enforced.
|
|
46
|
+
code: string;
|
|
47
|
+
message: string;
|
|
48
|
+
source: ErrorSource;
|
|
49
|
+
// RECOMMENDED — optional per ADR-0009 §What ports are NOT required to do.
|
|
50
|
+
suggestions?: string[];
|
|
51
|
+
fixture?: string;
|
|
52
|
+
node?: NodeContext;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Warning envelope — same shape as LoaderError but a `WARN_*` code. */
|
|
56
|
+
export interface LoaderWarning {
|
|
57
|
+
code: string;
|
|
58
|
+
message: string;
|
|
59
|
+
source: ErrorSource;
|
|
60
|
+
suggestions?: string[];
|
|
61
|
+
fixture?: string;
|
|
62
|
+
node?: NodeContext;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Canonical synthetic envelope for programmatic / test-constructed nodes.
|
|
66
|
+
* `caller` is an optional human label (e.g. "QueriesTest.makePost"). */
|
|
67
|
+
export function codeSource(caller?: string): ErrorSource {
|
|
68
|
+
return caller ? { format: "code", caller } : { format: "code" };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** FR5d — build a `format: "resolved"` envelope from a referrer node's source
|
|
72
|
+
* envelope (typically a `format: "json"` or `format: "yaml"` parse-time
|
|
73
|
+
* source) plus the referrer FQN and unresolved target string.
|
|
74
|
+
*
|
|
75
|
+
* The resolved envelope carries:
|
|
76
|
+
* - `files`: the referrer's source files (so editors can jump to it),
|
|
77
|
+
* - `jsonPath`: the referrer's jsonPath when known (the location of the
|
|
78
|
+
* broken reference on disk),
|
|
79
|
+
* - `referrer`: the FQN of the metadata node that declared the broken
|
|
80
|
+
* reference (e.g. "myapp::content::Video"),
|
|
81
|
+
* - `target`: the unresolved reference string itself (e.g. "BaseEntity",
|
|
82
|
+
* "Program.weeks.invalid", "DoesNotExist").
|
|
83
|
+
*
|
|
84
|
+
* `referrerSource` may be any FR5a variant — we read its files/jsonPath
|
|
85
|
+
* best-effort and fall back to an empty `files: []` when the referrer's
|
|
86
|
+
* source is itself a `code`/`database` envelope. */
|
|
87
|
+
export function resolvedSource(
|
|
88
|
+
referrerSource: ErrorSource,
|
|
89
|
+
referrer: string,
|
|
90
|
+
target: string,
|
|
91
|
+
): ErrorSource {
|
|
92
|
+
const files = "files" in referrerSource ? [...referrerSource.files] : [];
|
|
93
|
+
const jsonPath = "jsonPath" in referrerSource ? referrerSource.jsonPath : undefined;
|
|
94
|
+
const out: ErrorSource = { format: "resolved", files, referrer, target };
|
|
95
|
+
if (jsonPath !== undefined) {
|
|
96
|
+
(out as { jsonPath?: string }).jsonPath = jsonPath;
|
|
97
|
+
}
|
|
98
|
+
return out;
|
|
99
|
+
}
|
package/src/subtype-rules.ts
CHANGED
|
@@ -35,7 +35,7 @@ function walk(model: MetaData, errors: ParseError[], warnings: string[]): void {
|
|
|
35
35
|
new ParseError(
|
|
36
36
|
`value object '${model.fqn()}' must not have a primary identity ` +
|
|
37
37
|
`(use subType: "entity" for records with identity)`,
|
|
38
|
-
{ code: "ERR_SUBTYPE_RULE_VIOLATION" },
|
|
38
|
+
{ code: "ERR_SUBTYPE_RULE_VIOLATION", source: model.source },
|
|
39
39
|
),
|
|
40
40
|
);
|
|
41
41
|
} else if (
|
package/src/super-resolve.ts
CHANGED
|
@@ -100,6 +100,8 @@ export interface DeferredSuperFailure {
|
|
|
100
100
|
nodeFqn: string;
|
|
101
101
|
/** The raw super ref string that didn't resolve. */
|
|
102
102
|
ref: string;
|
|
103
|
+
/** ADR-0009 provenance envelope of the referencing node (FR5a). */
|
|
104
|
+
source: import("./source.js").ErrorSource;
|
|
103
105
|
}
|
|
104
106
|
|
|
105
107
|
/**
|
|
@@ -128,7 +130,7 @@ export function resolveDeferredSupers(root: MetaData): DeferredSuperFailure[] {
|
|
|
128
130
|
// Frozen — ignore; the loader should resolve before freeze.
|
|
129
131
|
}
|
|
130
132
|
} else {
|
|
131
|
-
failures.push({ nodeFqn: node.fqn(), ref: node.superRef });
|
|
133
|
+
failures.push({ nodeFqn: node.fqn(), ref: node.superRef, source: node.source });
|
|
132
134
|
}
|
|
133
135
|
});
|
|
134
136
|
return failures;
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
// FileMetaDataLoader — discovers file-backed MetaDataSources and runs the
|
|
2
|
-
// MetaDataLoader pipeline over them. A UrlMetaDataLoader will slot in the
|
|
3
|
-
// same way later.
|
|
4
|
-
|
|
5
|
-
import type { Stats } from "node:fs";
|
|
6
|
-
import { readdir, stat } from "node:fs/promises";
|
|
7
|
-
import { join } from "node:path";
|
|
8
|
-
import { MetaDataLoader, type LoadResult } from "../loader/meta-data-loader.js";
|
|
9
|
-
import { FileSource } from "./file-source.js";
|
|
10
|
-
import { parseYaml } from "./parser-yaml.js";
|
|
11
|
-
import type { ParseOptions, ParseResult } from "../parser-core.js";
|
|
12
|
-
import type { MetaDataSource } from "../loader/meta-data-source.js";
|
|
13
|
-
|
|
14
|
-
/** Minimal glob matcher supporting `*` (any chars except `/`) and `**` (any chars). */
|
|
15
|
-
function matchSimpleGlob(pattern: string, value: string): boolean {
|
|
16
|
-
const regexStr = pattern
|
|
17
|
-
.replace(/[.+^${}()|[\]\\]/g, "\\$&")
|
|
18
|
-
.replace(/\*\*/g, "::DOUBLESTAR::")
|
|
19
|
-
.replace(/\*/g, "[^/]*")
|
|
20
|
-
.replace(/::DOUBLESTAR::/g, ".*");
|
|
21
|
-
return new RegExp(`^${regexStr}$`).test(value);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export class FileMetaDataLoader extends MetaDataLoader {
|
|
25
|
-
/** Adds YAML parsing on top of the base loader's JSON-only `parseSource`. */
|
|
26
|
-
protected override parseSource(
|
|
27
|
-
content: string,
|
|
28
|
-
source: MetaDataSource,
|
|
29
|
-
parseOpts: ParseOptions,
|
|
30
|
-
): ParseResult {
|
|
31
|
-
if (source.format === "yaml") {
|
|
32
|
-
return parseYaml(content, parseOpts);
|
|
33
|
-
}
|
|
34
|
-
return super.parseSource(content, source, parseOpts);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Load every `.json` / `.yaml` / `.yml` file in a directory (non-recursive),
|
|
39
|
-
* in deterministic ordinal filename order.
|
|
40
|
-
* @param opts.exclude glob patterns (relative to dir) to skip — `*` / `**`.
|
|
41
|
-
*/
|
|
42
|
-
async loadDirectory(dir: string, opts?: { exclude?: string[] }): Promise<LoadResult> {
|
|
43
|
-
let entries: string[];
|
|
44
|
-
try {
|
|
45
|
-
// Sorted for deterministic multi-file load order: the overlay merge is
|
|
46
|
-
// order-sensitive (last-writer-wins on attr conflicts), so the scan must
|
|
47
|
-
// not depend on filesystem readdir order. Ordinal filename sort is
|
|
48
|
-
// reproducible across the Java/Python/C# ports.
|
|
49
|
-
entries = (await readdir(dir)).sort();
|
|
50
|
-
} catch (err) {
|
|
51
|
-
// Surface the I/O failure as a collected error via the empty-source path.
|
|
52
|
-
const emptyResult = await this.load([]);
|
|
53
|
-
return {
|
|
54
|
-
...emptyResult,
|
|
55
|
-
errors: [
|
|
56
|
-
new Error(`loadDirectory: cannot read ${dir}: ${(err as Error).message}`),
|
|
57
|
-
...emptyResult.errors,
|
|
58
|
-
],
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const excludes = opts?.exclude ?? [];
|
|
63
|
-
const paths: string[] = [];
|
|
64
|
-
for (const entry of entries) {
|
|
65
|
-
const lower = entry.toLowerCase();
|
|
66
|
-
if (!lower.endsWith(".json") && !lower.endsWith(".yaml") && !lower.endsWith(".yml")) {
|
|
67
|
-
continue;
|
|
68
|
-
}
|
|
69
|
-
const filePath = join(dir, entry);
|
|
70
|
-
let statResult: Stats;
|
|
71
|
-
try {
|
|
72
|
-
statResult = await stat(filePath);
|
|
73
|
-
} catch {
|
|
74
|
-
// Entry vanished between readdir and stat (TOCTOU) or is not accessible.
|
|
75
|
-
// Skip it rather than breaking the no-throw contract of loadDirectory.
|
|
76
|
-
continue;
|
|
77
|
-
}
|
|
78
|
-
if (!statResult.isFile()) continue;
|
|
79
|
-
if (excludes.some((p) => matchSimpleGlob(p, entry))) continue;
|
|
80
|
-
paths.push(filePath);
|
|
81
|
-
}
|
|
82
|
-
return this.loadFiles(paths);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/** Load an explicit list of file paths, in order. */
|
|
86
|
-
async loadFiles(paths: string[]): Promise<LoadResult> {
|
|
87
|
-
return this.load(paths.map((p) => new FileSource(p)));
|
|
88
|
-
}
|
|
89
|
-
}
|