@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.
Files changed (128) hide show
  1. package/README.md +54 -3
  2. package/dist/attr-schema-validate.js +7 -7
  3. package/dist/attr-schema-validate.js.map +1 -1
  4. package/dist/core/export-json.d.ts +6 -7
  5. package/dist/core/export-json.d.ts.map +1 -1
  6. package/dist/core/export-json.js +15 -17
  7. package/dist/core/export-json.js.map +1 -1
  8. package/dist/core/index.d.ts +4 -2
  9. package/dist/core/index.d.ts.map +1 -1
  10. package/dist/core/index.js +6 -2
  11. package/dist/core/index.js.map +1 -1
  12. package/dist/core/parser-yaml.d.ts.map +1 -1
  13. package/dist/core/parser-yaml.js +36 -11
  14. package/dist/core/parser-yaml.js.map +1 -1
  15. package/dist/core/yaml-desugar.d.ts.map +1 -1
  16. package/dist/core/yaml-desugar.js +54 -5
  17. package/dist/core/yaml-desugar.js.map +1 -1
  18. package/dist/core/yaml-positions-walker.d.ts +21 -0
  19. package/dist/core/yaml-positions-walker.d.ts.map +1 -0
  20. package/dist/core/yaml-positions-walker.js +75 -0
  21. package/dist/core/yaml-positions-walker.js.map +1 -0
  22. package/dist/core/yaml-positions.d.ts +19 -0
  23. package/dist/core/yaml-positions.d.ts.map +1 -0
  24. package/dist/core/yaml-positions.js +60 -0
  25. package/dist/core/yaml-positions.js.map +1 -0
  26. package/dist/errors.d.ts +32 -9
  27. package/dist/errors.d.ts.map +1 -1
  28. package/dist/errors.js +44 -5
  29. package/dist/errors.js.map +1 -1
  30. package/dist/index.d.ts +6 -3
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +9 -2
  33. package/dist/index.js.map +1 -1
  34. package/dist/json-path.d.ts +8 -0
  35. package/dist/json-path.d.ts.map +1 -0
  36. package/dist/json-path.js +39 -0
  37. package/dist/json-path.js.map +1 -0
  38. package/dist/loader/meta-data-loader.d.ts +47 -6
  39. package/dist/loader/meta-data-loader.d.ts.map +1 -1
  40. package/dist/loader/meta-data-loader.js +126 -8
  41. package/dist/loader/meta-data-loader.js.map +1 -1
  42. package/dist/loader/meta-data-source.d.ts +6 -2
  43. package/dist/loader/meta-data-source.d.ts.map +1 -1
  44. package/dist/loader/meta-data-source.js +10 -6
  45. package/dist/loader/meta-data-source.js.map +1 -1
  46. package/dist/loader/shortcuts.d.ts +9 -0
  47. package/dist/loader/shortcuts.d.ts.map +1 -0
  48. package/dist/loader/shortcuts.js +19 -0
  49. package/dist/loader/shortcuts.js.map +1 -0
  50. package/dist/loader/sources/directory-source.d.ts +15 -0
  51. package/dist/loader/sources/directory-source.d.ts.map +1 -0
  52. package/dist/loader/sources/directory-source.js +80 -0
  53. package/dist/loader/sources/directory-source.js.map +1 -0
  54. package/dist/loader/sources/file-source.d.ts +12 -0
  55. package/dist/loader/sources/file-source.d.ts.map +1 -0
  56. package/dist/loader/sources/file-source.js +46 -0
  57. package/dist/loader/sources/file-source.js.map +1 -0
  58. package/dist/loader/sources/index.d.ts +5 -0
  59. package/dist/loader/sources/index.d.ts.map +1 -0
  60. package/dist/loader/sources/index.js +5 -0
  61. package/dist/loader/sources/index.js.map +1 -0
  62. package/dist/loader/sources/uri-source.d.ts +9 -0
  63. package/dist/loader/sources/uri-source.d.ts.map +1 -0
  64. package/dist/loader/sources/uri-source.js +42 -0
  65. package/dist/loader/sources/uri-source.js.map +1 -0
  66. package/dist/loader/validation-passes.d.ts.map +1 -1
  67. package/dist/loader/validation-passes.js +92 -28
  68. package/dist/loader/validation-passes.js.map +1 -1
  69. package/dist/naming.d.ts +15 -2
  70. package/dist/naming.d.ts.map +1 -1
  71. package/dist/naming.js +20 -6
  72. package/dist/naming.js.map +1 -1
  73. package/dist/parser-core.d.ts +17 -4
  74. package/dist/parser-core.d.ts.map +1 -1
  75. package/dist/parser-core.js +371 -44
  76. package/dist/parser-core.js.map +1 -1
  77. package/dist/parser-json.d.ts.map +1 -1
  78. package/dist/parser-json.js +10 -2
  79. package/dist/parser-json.js.map +1 -1
  80. package/dist/persistence/source/validate-source-roles.js +2 -2
  81. package/dist/persistence/source/validate-source-roles.js.map +1 -1
  82. package/dist/semantic-diff.d.ts +5 -0
  83. package/dist/semantic-diff.d.ts.map +1 -0
  84. package/dist/semantic-diff.js +49 -0
  85. package/dist/semantic-diff.js.map +1 -0
  86. package/dist/shared/meta-data.d.ts +10 -0
  87. package/dist/shared/meta-data.d.ts.map +1 -1
  88. package/dist/shared/meta-data.js +23 -0
  89. package/dist/shared/meta-data.js.map +1 -1
  90. package/dist/source.d.ts +96 -0
  91. package/dist/source.d.ts.map +1 -0
  92. package/dist/source.js +38 -0
  93. package/dist/source.js.map +1 -0
  94. package/dist/subtype-rules.js +1 -1
  95. package/dist/subtype-rules.js.map +1 -1
  96. package/dist/super-resolve.d.ts +2 -0
  97. package/dist/super-resolve.d.ts.map +1 -1
  98. package/dist/super-resolve.js +1 -1
  99. package/dist/super-resolve.js.map +1 -1
  100. package/package.json +1 -1
  101. package/src/attr-schema-validate.ts +7 -7
  102. package/src/core/export-json.ts +15 -18
  103. package/src/core/index.ts +8 -2
  104. package/src/core/parser-yaml.ts +38 -11
  105. package/src/core/yaml-desugar.ts +58 -4
  106. package/src/core/yaml-positions-walker.ts +101 -0
  107. package/src/core/yaml-positions.ts +80 -0
  108. package/src/errors.ts +57 -8
  109. package/src/index.ts +28 -3
  110. package/src/json-path.ts +46 -0
  111. package/src/loader/meta-data-loader.ts +168 -10
  112. package/src/loader/meta-data-source.ts +10 -6
  113. package/src/loader/shortcuts.ts +31 -0
  114. package/src/loader/sources/directory-source.ts +90 -0
  115. package/src/{core → loader/sources}/file-source.ts +3 -3
  116. package/src/loader/sources/index.ts +6 -0
  117. package/src/loader/sources/uri-source.ts +44 -0
  118. package/src/loader/validation-passes.ts +96 -29
  119. package/src/naming.ts +39 -7
  120. package/src/parser-core.ts +412 -46
  121. package/src/parser-json.ts +11 -2
  122. package/src/persistence/source/validate-source-roles.ts +2 -2
  123. package/src/semantic-diff.ts +48 -0
  124. package/src/shared/meta-data.ts +28 -0
  125. package/src/source.ts +99 -0
  126. package/src/subtype-rules.ts +1 -1
  127. package/src/super-resolve.ts +3 -1
  128. package/src/core/file-meta-data-loader.ts +0 -89
package/src/index.ts CHANGED
@@ -150,13 +150,37 @@ export { resolveSuperRef } from "./super-resolve.js";
150
150
  // Loader hierarchy
151
151
  export { MetaDataLoader } from "./loader/meta-data-loader.js";
152
152
  export type { LoadOptions, LoadResult, LoadingState } from "./loader/meta-data-loader.js";
153
- export { InMemorySource } from "./loader/meta-data-source.js";
153
+ export { InMemoryStringSource } from "./loader/meta-data-source.js";
154
154
  export type { MetaDataSource, MetaDataFormat } from "./loader/meta-data-source.js";
155
155
 
156
+ // Module-level loader shortcuts — delegate to MetaDataLoader.from* static
157
+ // factories. The shortcuts honor the ergonomic per-port pattern (TS and
158
+ // Python expose both class statics AND module-level functions); Java/C# stay
159
+ // class-only. Browser safety is preserved: the underlying sources are
160
+ // loaded via dynamic import inside MetaDataLoader.from*.
161
+ export {
162
+ loadDirectory,
163
+ loadUris,
164
+ loadString,
165
+ } from "./loader/shortcuts.js";
166
+
156
167
  // Errors
157
168
  export { ParseError, MetaModelError, ERROR_CODES } from "./errors.js";
158
169
  export type { ErrorCode } from "./errors.js";
159
170
 
171
+ // FR5a — loader error envelope + source-on-node (ADR-0009).
172
+ // Re-exported from the package root so consumers that catch + repackage
173
+ // ParseErrors (or narrow on `err.source.format === "json"`) can import the
174
+ // envelope types alongside the runtime discriminator.
175
+ export type {
176
+ ErrorSource,
177
+ LoaderError,
178
+ LoaderWarning,
179
+ NodeContext,
180
+ Contributor,
181
+ } from "./source.js";
182
+ export { codeSource } from "./source.js";
183
+
160
184
  // Attribute-schema validation pass (Phase A3)
161
185
  export { validateAttrSchema } from "./attr-schema-validate.js";
162
186
  export type { AttrSchemaValidationResult } from "./attr-schema-validate.js";
@@ -164,9 +188,10 @@ export type { AttrSchemaValidationResult } from "./attr-schema-validate.js";
164
188
  // Naming — hoisted from runtime-ts in v0.2.3 so multiple consumers (runtime-ts, migrate-ts, codegen-ts)
165
189
  // share identical name resolution. See spec §4.1.
166
190
  export {
167
- toSnakeCase, pluralize,
191
+ toSnakeCase, toKebabCase, pluralize,
192
+ applyColumnNamingStrategy, DEFAULT_COLUMN_NAMING_STRATEGY,
168
193
  resolveTableName, resolveColumnName, resolveTableSchema,
169
194
  buildNameMap,
170
195
  stripPackage,
171
196
  } from "./naming.js";
172
- export type { EntityNameMap } from "./naming.js";
197
+ export type { EntityNameMap, ColumnNamingStrategy } from "./naming.js";
@@ -0,0 +1,46 @@
1
+ // server/typescript/packages/metadata/src/json-path.ts
2
+ //
3
+ // FR5a / ADR-0009 — Canonical JSONPath builder.
4
+ //
5
+ // Construction rules (cross-port-aligned):
6
+ // - Root is `$`.
7
+ // - Object keys matching /^[A-Za-z_][A-Za-z0-9_]*$/ use dot notation: `.foo`.
8
+ // - All other keys use single-quoted bracket form: `['my-key']`, `['@attr']`.
9
+ // - Array indices use bracket form: `[N]`.
10
+ // - No trailing dots, no whitespace.
11
+
12
+ const IDENT_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
13
+
14
+ type Segment =
15
+ | { kind: "key"; value: string }
16
+ | { kind: "index"; value: number };
17
+
18
+ export class JsonPathBuilder {
19
+ private readonly segments: Segment[] = [];
20
+
21
+ pushKey(key: string): void {
22
+ this.segments.push({ kind: "key", value: key });
23
+ }
24
+
25
+ pushIndex(idx: number): void {
26
+ this.segments.push({ kind: "index", value: idx });
27
+ }
28
+
29
+ pop(): void {
30
+ this.segments.pop();
31
+ }
32
+
33
+ toString(): string {
34
+ let out = "$";
35
+ for (const seg of this.segments) {
36
+ if (seg.kind === "index") {
37
+ out += `[${seg.value}]`;
38
+ } else if (IDENT_RE.test(seg.value)) {
39
+ out += `.${seg.value}`;
40
+ } else {
41
+ out += `['${seg.value.replace(/'/g, "\\'")}']`;
42
+ }
43
+ }
44
+ return out;
45
+ }
46
+ }
@@ -14,15 +14,34 @@ import { coreProviders } from "../core-types.js";
14
14
  import { composeRegistry } from "../provider.js";
15
15
  import { TYPE_METADATA, SUBTYPE_ROOT } from "../shared/base-types.js";
16
16
  import { ParseError } from "../errors.js";
17
+ import type { LoaderWarning } from "../source.js";
18
+ import { codeSource, resolvedSource } from "../source.js";
17
19
  import { parseJson } from "../parser-json.js";
18
20
  import { validateDataGridSortFields, validateFilterableHasIndex, validateOriginPaths, validateDataGridFilterValues, validateFieldObjectStorage, validateTemplatePayloadRefs } from "./validation-passes.js";
19
21
  import { validateSourceRoles } from "../persistence/source/validate-source-roles.js";
20
22
  import { resolveDeferredSupers } from "../super-resolve.js";
21
23
  import { validateSubtypeRules } from "../subtype-rules.js";
22
24
  import { validateAttrSchema } from "../attr-schema-validate.js";
23
- import type { MetaDataSource } from "./meta-data-source.js";
25
+ import type { MetaDataFormat, MetaDataSource } from "./meta-data-source.js";
26
+ import { InMemoryStringSource } from "./meta-data-source.js";
24
27
  import type { ParseOptions, ParseResult } from "../parser-core.js";
25
28
 
29
+ // Local mirror of DirectorySource's options shape. Deliberately inlined here
30
+ // (instead of `import type`'d from ./sources/directory-source.js) so the
31
+ // browser-safety crawler — which walks every `import|export from` it sees,
32
+ // type-only or not — never follows a path into a node:fs-using file.
33
+ // Keep field-for-field in sync with `DirectoryOptions` in `./sources/directory-source.ts`.
34
+ type DirectoryFactoryOptions = {
35
+ exclude?: string[];
36
+ recurse?: boolean;
37
+ };
38
+
39
+ // YAML parser and node:fs-backed Source impls are loaded lazily (dynamic
40
+ // import) inside the methods that need them. Reason: the package root
41
+ // (src/index.ts) re-exports MetaDataLoader and must stay browser-safe —
42
+ // the browser-safety test asserts that no file reachable from index.ts
43
+ // statically imports `yaml` or `node:fs(/promises)`.
44
+
26
45
  // ---------------------------------------------------------------------------
27
46
  // Public API types
28
47
  // ---------------------------------------------------------------------------
@@ -41,7 +60,13 @@ export interface LoadOptions {
41
60
 
42
61
  export interface LoadResult {
43
62
  root: MetaRoot;
44
- warnings: string[];
63
+ /** Cross-port-aligned warning envelopes per ADR-0009.
64
+ * FR5a creates the channel; FR5c (overlay-merge duplicate detection)
65
+ * will be the first feature to populate it. Legacy string warnings
66
+ * collected during parse/validation are wrapped at the loader boundary
67
+ * with `code: "WARN_LEGACY"` and `source: { format: "code" }` so the
68
+ * channel always presents the envelope shape to consumers. */
69
+ warnings: LoaderWarning[];
45
70
  errors: Error[];
46
71
  }
47
72
 
@@ -75,6 +100,71 @@ export class MetaDataLoader {
75
100
  return composeRegistry(coreProviders);
76
101
  }
77
102
 
103
+ // ---------------------------------------------------------------------------
104
+ // Static factories — the 99% case (cross-language consistent)
105
+ // ---------------------------------------------------------------------------
106
+
107
+ /**
108
+ * Load every supported file (`.json` / `.yaml` / `.yml`) under `dir` in
109
+ * deterministic ordinal-basename order. Recurses by default.
110
+ *
111
+ * Convenience for the typical "load a directory of metadata" path. The
112
+ * `DirectorySource` impl is loaded lazily to keep the package root
113
+ * browser-safe (the underlying source uses node:fs).
114
+ *
115
+ * A missing/unreadable directory is surfaced as a collected entry in
116
+ * `result.errors`; the loader returns a synthetic empty root rather than
117
+ * throwing — preserves the `meta export` CLI exit-code contract.
118
+ */
119
+ static async fromDirectory(
120
+ dir: string,
121
+ opts?: DirectoryFactoryOptions & LoadOptions,
122
+ ): Promise<LoadResult> {
123
+ const { exclude, recurse, ...loaderOpts } = opts ?? {};
124
+ // Conditional spreads honor exactOptionalPropertyTypes — only forward keys
125
+ // when the caller supplied a value, so DirectorySource's own defaults apply.
126
+ const dirOpts: DirectoryFactoryOptions = {
127
+ ...(exclude !== undefined && { exclude }),
128
+ ...(recurse !== undefined && { recurse }),
129
+ };
130
+ const { DirectorySource } = await import("./sources/directory-source.js");
131
+ const loader = new MetaDataLoader(loaderOpts);
132
+ try {
133
+ const sources = await new DirectorySource(dir, dirOpts).expand();
134
+ return loader.load(sources);
135
+ } catch (err) {
136
+ // Match the pre-unification contract: a missing/unreadable directory is
137
+ // surfaced as a collected error on the LoadResult, not a throw. The
138
+ // pipeline still completes with a synthetic empty root.
139
+ const emptyResult = await loader.load([]);
140
+ const expandErr =
141
+ err instanceof Error
142
+ ? err
143
+ : new Error(`MetaDataLoader.fromDirectory: ${String(err)}`);
144
+ return { ...emptyResult, errors: [expandErr, ...emptyResult.errors] };
145
+ }
146
+ }
147
+
148
+ /**
149
+ * Load each URI as a {@link UriSource}. Supports `file://`, `http://`,
150
+ * `https://` schemes. The source impl is loaded lazily to keep the package
151
+ * root browser-safe.
152
+ */
153
+ static async fromUris(uris: string[], opts?: LoadOptions): Promise<LoadResult> {
154
+ const { UriSource } = await import("./sources/uri-source.js");
155
+ const sources = uris.map((u) => new UriSource(u));
156
+ return new MetaDataLoader(opts).load(sources);
157
+ }
158
+
159
+ /** Load a single in-memory string of the given format. */
160
+ static async fromString(
161
+ content: string,
162
+ format: MetaDataFormat,
163
+ opts?: LoadOptions,
164
+ ): Promise<LoadResult> {
165
+ return new MetaDataLoader(opts).load([new InMemoryStringSource(content, { format })]);
166
+ }
167
+
78
168
  // ---------------------------------------------------------------------------
79
169
  // Lifecycle
80
170
  // ---------------------------------------------------------------------------
@@ -156,10 +246,16 @@ export class MetaDataLoader {
156
246
  // ---------------------------------------------------------------------------
157
247
 
158
248
  /**
159
- * Parse one source's raw content into a ParseResult. The base loader handles
160
- * JSON only; a non-JSON format throws. Subclasses override this seam to add
161
- * formats e.g. FileMetaDataLoader (in @metaobjectsdev/metadata/core) adds YAML.
162
- * This keeps the browser-safe base loader free of the YAML parser.
249
+ * Parse one source's raw content into a ParseResult. Dispatches on the
250
+ * source's declared `format` `"json"` runs the canonical JSON parser,
251
+ * `"yaml"` desugars the authoring YAML into canonical JSON via parseYaml.
252
+ * Cross-language consistent: the same format vocabulary is honored by the
253
+ * Java / C# / Python MetaDataLoaders.
254
+ *
255
+ * The YAML parser is loaded lazily so the browser-safe root entry never
256
+ * statically pulls in the `yaml` dependency — see the module-header comment.
257
+ * `parseYaml` is preloaded inside `load()` if any source declares YAML
258
+ * format so the call here can stay synchronous.
163
259
  */
164
260
  protected parseSource(
165
261
  content: string,
@@ -169,12 +265,35 @@ export class MetaDataLoader {
169
265
  if (source.format === "json") {
170
266
  return parseJson(content, parseOpts);
171
267
  }
268
+ if (source.format === "yaml") {
269
+ const fn = MetaDataLoader._yamlParser;
270
+ if (fn === undefined) {
271
+ throw new Error(
272
+ `MetaDataLoader: YAML parser was not preloaded — this is an internal bug. ` +
273
+ `Source "${source.id}" declares format "yaml".`,
274
+ );
275
+ }
276
+ return fn(content, parseOpts);
277
+ }
172
278
  throw new Error(
173
- `MetaDataLoader parses JSON only; format "${source.format}" for source ` +
174
- `"${source.id}" requires FileMetaDataLoader (from @metaobjectsdev/metadata/core)`,
279
+ `MetaDataLoader: unsupported source format "${source.format}" ` +
280
+ `on source "${source.id}"`,
175
281
  );
176
282
  }
177
283
 
284
+ // Cached lazy YAML parser — populated by _ensureYamlParser() before
285
+ // parseSource is invoked on a YAML source. Module-level cache (one import
286
+ // per process) keeps the per-load cost negligible.
287
+ private static _yamlParser:
288
+ | ((content: string, opts: ParseOptions) => ParseResult)
289
+ | undefined;
290
+
291
+ private static async _ensureYamlParser(): Promise<void> {
292
+ if (MetaDataLoader._yamlParser !== undefined) return;
293
+ const mod = await import("../core/parser-yaml.js");
294
+ MetaDataLoader._yamlParser = mod.parseYaml;
295
+ }
296
+
178
297
  // ---------------------------------------------------------------------------
179
298
  // load — async pipeline over MetaDataSource[]
180
299
  // ---------------------------------------------------------------------------
@@ -202,6 +321,19 @@ export class MetaDataLoader {
202
321
  this._state = "loading";
203
322
  const warnings: string[] = [];
204
323
  const errors: Error[] = [];
324
+ // FR5c — envelope-shaped warnings (WARN_DUPLICATE_DECLARATION et al.)
325
+ // surface here untouched. Distinct from the legacy `warnings: string[]`
326
+ // channel: those are wrapped in a WARN_LEGACY envelope at the boundary,
327
+ // while these already carry their own code + source.
328
+ const envelopeWarnings: LoaderWarning[] = [];
329
+
330
+ // Pre-load the YAML parser via dynamic import if any source declares
331
+ // YAML format. This keeps `parseSource` synchronous and the package root
332
+ // (src/index.ts) browser-safe — yaml is never statically imported from a
333
+ // file reachable from the package entry. See `_ensureYamlParser`.
334
+ if (sources.some((s) => s.format === "yaml")) {
335
+ await MetaDataLoader._ensureYamlParser();
336
+ }
205
337
 
206
338
  let root: MetaRoot | undefined;
207
339
 
@@ -236,6 +368,10 @@ export class MetaDataLoader {
236
368
  const parseResult = this.parseSource(content, source, parseOpts);
237
369
  warnings.push(...parseResult.warnings);
238
370
  errors.push(...parseResult.errors);
371
+ // FR5c — collect envelope-shaped warnings (already carry code +
372
+ // source). The legacy `warnings` channel still flows into the
373
+ // WARN_LEGACY-wrapping path below for unchanged behavior.
374
+ envelopeWarnings.push(...parseResult.envelopeWarnings);
239
375
  root = parseResult.root;
240
376
  } catch (err) {
241
377
  errors.push(
@@ -252,10 +388,17 @@ export class MetaDataLoader {
252
388
  if (root !== undefined) {
253
389
  const failures = resolveDeferredSupers(root);
254
390
  for (const failure of failures) {
391
+ // FR5d — emit format=resolved with referrer + target. The referrer's
392
+ // parse-time source supplies files + jsonPath (the location of the
393
+ // broken `extends:` on disk); referrer = the declaring node's FQN;
394
+ // target = the unresolved supertype ref.
255
395
  errors.push(
256
396
  new ParseError(
257
397
  `the SuperClass '${failure.ref}' does not exist (referenced by ${failure.nodeFqn})`,
258
- { code: "ERR_UNRESOLVED_SUPER" },
398
+ {
399
+ code: "ERR_UNRESOLVED_SUPER",
400
+ source: resolvedSource(failure.source, failure.nodeFqn, failure.ref),
401
+ },
259
402
  ),
260
403
  );
261
404
  }
@@ -316,6 +459,21 @@ export class MetaDataLoader {
316
459
  }
317
460
 
318
461
  this._root = root;
319
- return { root, warnings, errors };
462
+ // Wrap legacy string warnings collected from parser-core / validators in
463
+ // LoaderWarning envelopes at the loader boundary. The parser/validator
464
+ // surface keeps its `string[]` shape internally (parser-core is shared
465
+ // with parseJson() / parseYaml() callers who consume string warnings
466
+ // directly). FR5c-onward sites emit proper envelopes via parseResult.
467
+ // envelopeWarnings (collected above) — those surface unchanged.
468
+ const wrappedLegacy: LoaderWarning[] = warnings.map((msg) => ({
469
+ code: "WARN_LEGACY",
470
+ message: msg,
471
+ source: codeSource("MetaDataLoader"),
472
+ }));
473
+ return {
474
+ root,
475
+ warnings: [...envelopeWarnings, ...wrappedLegacy],
476
+ errors,
477
+ };
320
478
  }
321
479
  }
@@ -1,8 +1,8 @@
1
1
  // MetaDataSource — the raw-document unit consumed by the loader pipeline.
2
2
  //
3
- // A loader (FileMetaDataLoader, later UrlMetaDataLoader) discovers/acquires
4
- // sources; the MetaDataLoader pipeline calls read() on each. read() is async
5
- // so file/URL sources can do I/O; InMemorySource resolves immediately.
3
+ // A loader (MetaDataLoader, fed by FileSource/DirectorySource/UriSource/
4
+ // InMemoryStringSource) parses each source's content. read() is async so file
5
+ // and URI sources can do I/O; InMemoryStringSource resolves immediately.
6
6
 
7
7
  /** Format of a source's content. Selects the parser. */
8
8
  export type MetaDataFormat = "json" | "yaml";
@@ -17,15 +17,19 @@ export interface MetaDataSource {
17
17
  read(): Promise<string>;
18
18
  }
19
19
 
20
- /** A metadata source backed by an in-memory string. */
21
- export class InMemorySource implements MetaDataSource {
20
+ /**
21
+ * A metadata source backed by an in-memory string. The default identity is
22
+ * `"<inline>"` — matches the cross-language convention shared by the Java /
23
+ * C# / Python ports.
24
+ */
25
+ export class InMemoryStringSource implements MetaDataSource {
22
26
  readonly id: string;
23
27
  readonly format: MetaDataFormat;
24
28
  private readonly _content: string;
25
29
 
26
30
  constructor(content: string, opts?: { id?: string; format?: MetaDataFormat }) {
27
31
  this._content = content;
28
- this.id = opts?.id ?? "<in-memory>";
32
+ this.id = opts?.id ?? "<inline>";
29
33
  this.format = opts?.format ?? "json";
30
34
  }
31
35
 
@@ -0,0 +1,31 @@
1
+ // Module-level loader shortcuts — one-liners that delegate to the
2
+ // MetaDataLoader.from* static factories. Per the cross-language convention:
3
+ // TS and Python expose both class statics AND module-level shortcuts; Java
4
+ // and C# stay class-only.
5
+ //
6
+ // These functions do not statically import any node:fs- or yaml-using file;
7
+ // MetaDataLoader.from* loads the underlying source impls via dynamic import,
8
+ // preserving the package root's browser-safety contract.
9
+
10
+ import { MetaDataLoader } from "./meta-data-loader.js";
11
+ import type { LoadOptions, LoadResult } from "./meta-data-loader.js";
12
+ import type { MetaDataFormat } from "./meta-data-source.js";
13
+
14
+ export function loadDirectory(
15
+ dir: string,
16
+ opts?: { exclude?: string[]; recurse?: boolean } & LoadOptions,
17
+ ): Promise<LoadResult> {
18
+ return MetaDataLoader.fromDirectory(dir, opts);
19
+ }
20
+
21
+ export function loadUris(uris: string[], opts?: LoadOptions): Promise<LoadResult> {
22
+ return MetaDataLoader.fromUris(uris, opts);
23
+ }
24
+
25
+ export function loadString(
26
+ content: string,
27
+ format: MetaDataFormat,
28
+ opts?: LoadOptions,
29
+ ): Promise<LoadResult> {
30
+ return MetaDataLoader.fromString(content, format, opts);
31
+ }
@@ -0,0 +1,90 @@
1
+ // DirectorySource — expands a directory into a sorted list of FileSource.
2
+ //
3
+ // Discovers .json / .yaml / .yml files (case-insensitive on extension).
4
+ // Recurses by default — matches Java/Python/C# DirectorySource behavior.
5
+ // Sort order is ordinal-by-basename so the overlay merge is deterministic
6
+ // across environments and language ports.
7
+
8
+ import { readdir, stat } from "node:fs/promises";
9
+ import { basename, extname, join } from "node:path";
10
+ import { FileSource } from "./file-source.js";
11
+ import type { MetaDataSource } from "../meta-data-source.js";
12
+
13
+ export interface DirectoryOptions {
14
+ /** Filename patterns to exclude. Supports literal match and `*` / `**` globs. */
15
+ exclude?: string[];
16
+ /** Recurse into subdirectories. Default: true. */
17
+ recurse?: boolean;
18
+ }
19
+
20
+ const SUPPORTED_EXTS = new Set([".json", ".yaml", ".yml"]);
21
+
22
+ /** Minimal glob matcher supporting `*` (any chars except `/`) and `**` (any chars). */
23
+ function matchSimpleGlob(pattern: string, value: string): boolean {
24
+ const regexStr = pattern
25
+ .replace(/[.+^${}()|[\]\\]/g, "\\$&")
26
+ .replace(/\*\*/g, "::DOUBLESTAR::")
27
+ .replace(/\*/g, "[^/]*")
28
+ .replace(/::DOUBLESTAR::/g, ".*");
29
+ return new RegExp(`^${regexStr}$`).test(value);
30
+ }
31
+
32
+ export class DirectorySource {
33
+ constructor(
34
+ public readonly directory: string,
35
+ public readonly opts: DirectoryOptions = {},
36
+ ) {}
37
+
38
+ async expand(): Promise<MetaDataSource[]> {
39
+ const recurse = this.opts.recurse ?? true;
40
+ const exclude = this.opts.exclude ?? [];
41
+
42
+ const files = await this.collect(this.directory, recurse);
43
+ const filtered: string[] = [];
44
+ for (const p of files) {
45
+ if (!SUPPORTED_EXTS.has(extname(p).toLowerCase())) continue;
46
+ const name = basename(p);
47
+ if (exclude.some((pat) => matchSimpleGlob(pat, name))) continue;
48
+ filtered.push(p);
49
+ }
50
+
51
+ // Sort by basename (ordinal) for deterministic overlay order. The
52
+ // pre-unification FileMetaDataLoader sorted readdir entries (basenames)
53
+ // for the same reason — that contract carries forward here.
54
+ filtered.sort((a, b) => {
55
+ const an = basename(a);
56
+ const bn = basename(b);
57
+ return an < bn ? -1 : an > bn ? 1 : 0;
58
+ });
59
+
60
+ return filtered.map((p) => new FileSource(p));
61
+ }
62
+
63
+ private async collect(dir: string, recurse: boolean): Promise<string[]> {
64
+ let entries: string[];
65
+ try {
66
+ entries = await readdir(dir);
67
+ } catch (err) {
68
+ throw new Error(
69
+ `DirectorySource: cannot read ${dir}: ${(err as Error).message}`,
70
+ );
71
+ }
72
+ const out: string[] = [];
73
+ for (const entry of entries) {
74
+ const full = join(dir, entry);
75
+ let s;
76
+ try {
77
+ s = await stat(full);
78
+ } catch {
79
+ // Entry vanished between readdir and stat (TOCTOU) or is not accessible.
80
+ continue;
81
+ }
82
+ if (s.isDirectory()) {
83
+ if (recurse) out.push(...(await this.collect(full, recurse)));
84
+ } else if (s.isFile()) {
85
+ out.push(full);
86
+ }
87
+ }
88
+ return out;
89
+ }
90
+ }
@@ -1,9 +1,9 @@
1
1
  // FileSource — a MetaDataSource backed by a file on disk. Server-side only
2
- // (touches node:fs); lives under src/core/ so the browser-safe root never
3
- // imports it.
2
+ // (touches node:fs); lives under src/loader/sources/ alongside the other
3
+ // MetaDataSource implementations.
4
4
 
5
5
  import { basename, extname } from "node:path";
6
- import type { MetaDataFormat, MetaDataSource } from "../loader/meta-data-source.js";
6
+ import type { MetaDataFormat, MetaDataSource } from "../meta-data-source.js";
7
7
 
8
8
  /** Infer a source format from a file extension. `.yaml`/`.yml` → "yaml";
9
9
  * everything else (including `.json`) → "json", the canonical default. */
@@ -0,0 +1,6 @@
1
+ // Barrel re-export for the MetaDataSource implementations.
2
+
3
+ export { FileSource } from "./file-source.js";
4
+ export { DirectorySource } from "./directory-source.js";
5
+ export type { DirectoryOptions } from "./directory-source.js";
6
+ export { UriSource } from "./uri-source.js";
@@ -0,0 +1,44 @@
1
+ // UriSource — a MetaDataSource that fetches content from a URI.
2
+ // Supports file://, http://, and https:// schemes.
3
+
4
+ import { readFile } from "node:fs/promises";
5
+ import { fileURLToPath } from "node:url";
6
+ import { extname } from "node:path";
7
+ import type { MetaDataFormat, MetaDataSource } from "../meta-data-source.js";
8
+
9
+ function inferFormat(uri: string): MetaDataFormat {
10
+ // URL constructor handles file://, http://, https:// uniformly.
11
+ // Strip query/fragment by reading .pathname.
12
+ try {
13
+ const pathname = new URL(uri).pathname;
14
+ const ext = extname(pathname).toLowerCase();
15
+ return ext === ".yaml" || ext === ".yml" ? "yaml" : "json";
16
+ } catch {
17
+ // If the URI isn't a valid URL, fall back to JSON.
18
+ return "json";
19
+ }
20
+ }
21
+
22
+ export class UriSource implements MetaDataSource {
23
+ readonly id: string;
24
+ readonly format: MetaDataFormat;
25
+
26
+ constructor(public readonly uri: string, format?: MetaDataFormat) {
27
+ this.id = uri;
28
+ this.format = format ?? inferFormat(uri);
29
+ }
30
+
31
+ async read(): Promise<string> {
32
+ if (this.uri.startsWith("file://")) {
33
+ return readFile(fileURLToPath(this.uri), "utf-8");
34
+ }
35
+ if (this.uri.startsWith("http://") || this.uri.startsWith("https://")) {
36
+ const res = await fetch(this.uri);
37
+ if (!res.ok) {
38
+ throw new Error(`UriSource: ${this.uri} -> HTTP ${res.status}`);
39
+ }
40
+ return res.text();
41
+ }
42
+ throw new Error(`UriSource: unsupported scheme on ${this.uri}`);
43
+ }
44
+ }