@scalar/json-magic 0.13.5 → 0.15.0

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/CHANGELOG.md CHANGED
@@ -1,5 +1,29 @@
1
1
  # @scalar/json-magic
2
2
 
3
+ ## 0.15.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#10206](https://github.com/scalar/scalar/pull/10206): Preserve authored references between embedded schema resources when the containing document has no declared identity. This keeps references valid across `$id` scopes and when exporting the bundle to a different retrieval URL.
8
+ - [#10274](https://github.com/scalar/scalar/pull/10274): Add a standard-agnostic `join` utility with configurable merge strategies and conflict reporting to `@scalar/json-magic/join`. Use it in the OpenAPI parser while retaining OpenAPI upgrades, component prefixes, and OpenAPI conflict reports in the parser.
9
+ - [#10206](https://github.com/scalar/scalar/pull/10206): Add generic document identity hooks for bundling and an explicit root URI option for reference proxies. Honor OpenAPI 3.2 `$self` through an OpenAPI plugin in workspace-store, including external documents and partial bundles, and enable it in OpenAPI bundling callers.
10
+
11
+ URI resolution now honors root-relative and protocol-relative URLs, query/fragment references, and trailing-slash directory bases for all bundler consumers. Absolute non-HTTP identifiers remain unchanged instead of becoming filesystem paths; loader support is unchanged. Relative HTTP references retain query strings and fragments and are emitted only when they round-trip to the original URL.
12
+
13
+ Preserve authored reference spellings through serialized partial bundles and editable exports, while keeping older OpenAPI resolution and configured loader restrictions unchanged.
14
+
15
+ Keep references matching authored root schema identifiers intact so schema labels and anchors retain their existing behavior.
16
+
17
+ ## 0.14.0
18
+
19
+ ### Minor Changes
20
+
21
+ - [#10241](https://github.com/scalar/scalar/pull/10241): Expose helpers for indexing locally embedded `$id` and `$anchor` resources and resolving a reference to its local path.
22
+
23
+ ### Patch Changes
24
+
25
+ - [#10257](https://github.com/scalar/scalar/pull/10257): Resolve a relative reference reached through a local pointer during a partial bundle against the document origin; it was resolved against an empty base and failed.
26
+
3
27
  ## 0.13.5
4
28
 
5
29
  ### Patch Changes
package/README.md CHANGED
@@ -37,6 +37,7 @@ There is no root export. Every module is imported from its own entry point, so y
37
37
  | `@scalar/json-magic/bundle/plugins/node` | `fetchUrls`, `parseJson`, `parseYaml`, `readFiles` |
38
38
  | `@scalar/json-magic/bundle/value-generator` | `getHash`, `generateUniqueValue`, `uniqueValueGeneratorFactory` |
39
39
  | `@scalar/json-magic/dereference` | `dereference` |
40
+ | `@scalar/json-magic/join` | `join`, plus the `JoinOptions`, `JoinStrategy`, `JoinContext`, `JoinConflict` and `JoinResult` types |
40
41
  | `@scalar/json-magic/diff` | `diff`, `merge`, `apply`, the `Difference` type |
41
42
  | `@scalar/json-magic/magic-proxy` | `createMagicProxy`, `getRaw` |
42
43
  | `@scalar/json-magic/helpers/*` | Small standalone helpers, see [Helpers](#helpers) |
@@ -50,10 +51,71 @@ There is no root export. Every module is imported from its own entry point, so y
50
51
  | Resolve every `$ref`, internal and external, in one call | [`dereference`](#dereference) |
51
52
  | Compare two documents and merge concurrent edits | [`diff`](#diff) |
52
53
 
54
+ ## join
55
+
56
+ `join` combines JSON objects without assuming a document standard. It merges objects recursively and replaces arrays and scalar values with those from later inputs. It does not mutate inputs, upgrade document versions, resolve references, or rename definitions. Literal keys such as `__proto__`, `constructor`, and `prototype` are preserved as own data properties without changing object prototypes.
57
+
58
+ ```ts
59
+ import { join } from '@scalar/json-magic/join'
60
+
61
+ const result = join(
62
+ [
63
+ { title: 'First', catalog: { apple: { price: 2 } }, labels: [{ name: 'fruit' }] },
64
+ { title: 'Second', catalog: { pear: { price: 3 } }, labels: [{ name: 'fruit' }] },
65
+ ],
66
+ {
67
+ strategy: ({ path }) => {
68
+ if (path[0] === 'catalog' && path.length === 2) {
69
+ return 'conflict'
70
+ }
71
+ if (path[0] === 'labels') {
72
+ return { uniqueBy: 'name' }
73
+ }
74
+ return 'merge'
75
+ },
76
+ },
77
+ )
78
+
79
+ if (result.ok) {
80
+ console.log(result.document) // Both catalog entries, title "Second", one fruit label
81
+ } else {
82
+ console.log(result.conflicts) // Example: [{ path: ['catalog', 'apple'] }]
83
+ }
84
+ ```
85
+
86
+ The optional `strategy` callback receives `{ path, current, incoming }` for each visited field. Paths are arrays of literal keys, so a key containing `/` remains one segment. The root always merges; returning `replace` or `conflict` for an object treats that object as a whole and does not visit its children.
87
+
88
+ | Strategy | Behavior |
89
+ | --- | --- |
90
+ | `merge` (default) | Recursively merge objects; replace other values using the later input. |
91
+ | `merge-by-index` | Recursively merge objects and arrays, combining array entries at matching indexes and retaining trailing entries. |
92
+ | `skip` | Ignore this incoming field, leaving any existing value unchanged. |
93
+ | `replace` | Replace the entire value, including objects. |
94
+ | `conflict` | Report a duplicate key, even if both values are equal or the earlier value is null. |
95
+ | `{ uniqueBy: 'name' }` | Combine arrays, retaining the first item for each identity property value. Items without that property remain distinct. Use scalar identity values. Non-array incoming values replace the existing value. |
96
+
97
+ An empty input list returns `{ ok: true, document: {} }`. Conflicts return `{ ok: false, conflicts }` without a partial document. Inputs must be acyclic JSON objects.
98
+
99
+ For OpenAPI, use `join` from `@scalar/openapi-parser`, which supplies OpenAPI rules, version upgrades, and optional component prefixes. Other formats can supply their own rules; this module does not include an AsyncAPI adapter.
100
+
53
101
  ## bundle
54
102
 
55
103
  `bundle` walks a JSON object, resolves every external `$ref` (URLs, local files, or anything a custom loader plugin can handle) and embeds the result into the document itself. The original `$ref` values are rewritten to point at the embedded copies, so the output is a single self-contained document.
56
104
 
105
+ Document formats can supply a `resolveDocument` lifecycle hook returning `{ baseUri, metadata }`. The bundler resolves relative references against that base URI and retains the supplied root metadata when tree shaking. The hook also applies to cached and previously bundled documents. Without a hook, the retrieval URI remains the document base. JSON Schema `$id` values resolve against their enclosing base.
106
+
107
+ When reading qualified root references with `createMagicProxy`, pass the canonical URI as `documentUri`. Interpretation of format-specific identity fields belongs in the caller or a plugin.
108
+
109
+ URI resolution applies to every bundling path, including descriptions without `$self`:
110
+
111
+ - Absolute scheme-bearing references (`https:`, `file:`, `urn:`, `mailto:`, or custom schemes) retain their identity. This does not enable fetching those schemes; loader plugins still decide what they support.
112
+ - Hierarchical URI bases use URL resolution. Root-relative references replace the pathname; protocol-relative references replace the host. A new document path drops the base query and fragment, while query-only or fragment-only references keep the applicable parts of the base.
113
+ - A base ending in `/` denotes a directory. Without the slash, the final segment is a document name. Scheme-less paths continue to use filesystem resolution, including Windows drive paths.
114
+ - Opaque identities such as URNs support absolute and fragment references, but cannot provide a directory for a relative document path. Such a resolution throws rather than inventing a local path.
115
+ - Relativization preserves query strings, fragments, and directory slashes. HTTP references become relative only when resolving them again reproduces the original URL. Non-HTTP URIs remain absolute: the generic helper has no document registry, so limiting this rule to known `$self` values would corrupt other identifiers.
116
+
117
+ These are intentional URI compatibility changes, rather than behavior limited to OpenAPI `$self`.
118
+
57
119
  External documents are stored under the `x-ext` key, and the mapping between the generated keys and their original URLs is stored under `x-ext-urls`. Both keys are configurable, see [Options](#options).
58
120
 
59
121
  ### Quick start
@@ -1,4 +1,5 @@
1
1
  import type { UnknownObject } from '../types.js';
2
+ import { type DocumentResolver } from './document-references.js';
2
3
  /**
3
4
  * Checks if a string is a local reference (starts with #)
4
5
  * @param value - The reference string to check
@@ -93,7 +94,7 @@ export declare function prefixInternalRefRecursive(input: unknown, prefix: strin
93
94
  * // Result: target will contain the User schema with resolved references
94
95
  * ```
95
96
  */
96
- export declare const resolveAndCopyReferences: (targetDocument: unknown, sourceDocument: unknown, referencePath: string, externalRefsKey: string, documentKey: string, bundleLocalRefs?: boolean, processedNodes?: Set<unknown>) => void;
97
+ export declare const resolveAndCopyReferences: (targetDocument: unknown, sourceDocument: unknown, referencePath: string, externalRefsKey: string, documentKey: string, bundleLocalRefs?: boolean, processedNodes?: Set<unknown>, documentMetadata?: Record<string, unknown>) => void;
97
98
  /**
98
99
  * A loader plugin for resolving external references during bundling.
99
100
  * Loader plugins are responsible for handling specific types of external references,
@@ -235,6 +236,11 @@ type Config = {
235
236
  * Allows tracking the progress and status of reference resolution.
236
237
  */
237
238
  hooks?: Partial<{
239
+ /** Called once a complete document is available, including cached and saved documents.
240
+ * The first resolver returning an identity supplies its base URI and retained metadata.
241
+ * This hook is synchronous because identities are indexed before following references.
242
+ */
243
+ resolveDocument: DocumentResolver;
238
244
  /**
239
245
  * Optional hook called when the bundler starts resolving a $ref.
240
246
  * Useful for tracking or logging the beginning of a reference resolution.
@@ -1 +1 @@
1
- {"version":3,"file":"bundle.d.ts","sourceRoot":"","sources":["../../src/bundle/bundle.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"bundle.d.ts","sourceRoot":"","sources":["../../src/bundle/bundle.ts"],"names":[],"mappings":"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"}
@@ -1,6 +1,5 @@
1
1
  import { isObject } from '@scalar/helpers/object/is-object';
2
- import { convertToLocalRef } from '../helpers/convert-to-local-ref.js';
3
- import { getId, getSchemas } from '../helpers/get-schemas.js';
2
+ import { getId } from '../helpers/get-schemas.js';
4
3
  import { getValueByPath } from '../helpers/get-value-by-path.js';
5
4
  import { isFilePath } from '../helpers/is-file-path.js';
6
5
  import { isHttpUrl } from '../helpers/is-http-url.js';
@@ -9,6 +8,7 @@ import { setValueAtPath } from '../helpers/set-value-at-path.js';
9
8
  import { toRelativePath } from '../helpers/to-relative-path.js';
10
9
  import { escapeJsonPointer } from '../helpers/escape-json-pointer.js';
11
10
  import { getSegmentsFromPath } from '../helpers/get-segments-from-path.js';
11
+ import { documentReferences } from './document-references.js';
12
12
  import { getHash, uniqueValueGeneratorFactory } from './value-generator.js';
13
13
  /** Type guard to check if a value is an object with a $ref property */
14
14
  const hasRef = (value) => isObject(value) && '$ref' in value && typeof value['$ref'] === 'string';
@@ -144,13 +144,35 @@ export function prefixInternalRefRecursive(input, prefix) {
144
144
  * // Result: target will contain the User schema with resolved references
145
145
  * ```
146
146
  */
147
- export const resolveAndCopyReferences = (targetDocument, sourceDocument, referencePath, externalRefsKey, documentKey, bundleLocalRefs = false, processedNodes = new Set()) => {
147
+ export const resolveAndCopyReferences = (targetDocument, sourceDocument, referencePath, externalRefsKey, documentKey, bundleLocalRefs = false, processedNodes = new Set(), documentMetadata = {}) => {
148
148
  const referencedValue = getValueByPath(sourceDocument, getSegmentsFromPath(referencePath)).value;
149
149
  if (processedNodes.has(referencedValue)) {
150
150
  return;
151
151
  }
152
152
  processedNodes.add(referencedValue);
153
- setValueAtPath(targetDocument, referencePath, referencedValue);
153
+ // Only relocated copies need canonical metadata; keep the authored source unchanged.
154
+ const segments = getSegmentsFromPath(referencePath);
155
+ const copiedValue = segments.length === 2 && isObject(referencedValue) && Object.keys(documentMetadata).length > 0
156
+ ? { ...referencedValue, ...documentMetadata }
157
+ : referencedValue;
158
+ setValueAtPath(targetDocument, referencePath, copiedValue);
159
+ // Keep every enclosing base when copying a subtree, so later partial bundles
160
+ // resolve against the same document and schema resources as the original.
161
+ for (let length = 2; length < segments.length; length++) {
162
+ const ancestorPath = segments.slice(0, length);
163
+ const ancestor = getValueByPath(sourceDocument, ancestorPath).value;
164
+ if (!isObject(ancestor)) {
165
+ continue;
166
+ }
167
+ if (length === 2) {
168
+ for (const [key, value] of Object.entries(documentMetadata)) {
169
+ setValueAtPath(targetDocument, `/${[...ancestorPath, key].map(escapeJsonPointer).join('/')}`, value);
170
+ }
171
+ }
172
+ if (typeof ancestor.$id === 'string') {
173
+ setValueAtPath(targetDocument, `/${[...ancestorPath, '$id'].map(escapeJsonPointer).join('/')}`, ancestor.$id);
174
+ }
175
+ }
154
176
  // Do the same for each local ref
155
177
  const traverse = (node) => {
156
178
  if (!node || typeof node !== 'object') {
@@ -162,11 +184,11 @@ export const resolveAndCopyReferences = (targetDocument, sourceDocument, referen
162
184
  // 2. The source document only contains the current document's content
163
185
  // This prevents undefined behavior and maintains proper document boundaries
164
186
  if (node['$ref'].startsWith(`#/${externalRefsKey}/${escapeJsonPointer(documentKey)}`)) {
165
- resolveAndCopyReferences(targetDocument, sourceDocument, node['$ref'].substring(1), externalRefsKey, documentKey, bundleLocalRefs, processedNodes);
187
+ resolveAndCopyReferences(targetDocument, sourceDocument, node['$ref'].substring(1), externalRefsKey, documentKey, bundleLocalRefs, processedNodes, documentMetadata);
166
188
  }
167
189
  // Bundle the local refs as well
168
190
  else if (bundleLocalRefs) {
169
- resolveAndCopyReferences(targetDocument, sourceDocument, node['$ref'].substring(1), externalRefsKey, documentKey, bundleLocalRefs, processedNodes);
191
+ resolveAndCopyReferences(targetDocument, sourceDocument, node['$ref'].substring(1), externalRefsKey, documentKey, bundleLocalRefs, processedNodes, documentMetadata);
170
192
  }
171
193
  }
172
194
  for (const value of Object.values(node)) {
@@ -288,8 +310,6 @@ export async function bundle(input, config) {
288
310
  // Document root used to write all external documents
289
311
  // We need this when we want to do a partial bundle of a document
290
312
  const documentRoot = config.root ?? rawSpecification;
291
- // Extract all $id and $anchor values from the document to identify local schemas
292
- const schemas = getSchemas(documentRoot);
293
313
  // Determines if the bundling operation is partial.
294
314
  // Partial bundling occurs when:
295
315
  // - A root document is provided that is different from the raw specification being bundled, or
@@ -302,11 +322,6 @@ export async function bundle(input, config) {
302
322
  // For string inputs that are URLs or file paths, uses the input as the origin.
303
323
  // For non-string inputs or other string types, returns an '/' as a root path.
304
324
  const getDefaultOrigin = () => {
305
- // Id field is the first priority
306
- const id = getId(documentRoot);
307
- if (id) {
308
- return id;
309
- }
310
325
  if (config.origin) {
311
326
  return config.origin;
312
327
  }
@@ -318,12 +333,48 @@ export async function bundle(input, config) {
318
333
  }
319
334
  return '/';
320
335
  };
321
- const defaultOrigin = getDefaultOrigin();
336
+ const references = documentReferences(config.externalDocumentsKey, (document, retrievalUri) => {
337
+ for (const resolver of [
338
+ config.hooks?.resolveDocument,
339
+ ...lifecyclePlugin.map((plugin) => plugin.resolveDocument),
340
+ ]) {
341
+ const identity = resolver?.(document, retrievalUri);
342
+ if (identity !== undefined) {
343
+ return identity;
344
+ }
345
+ }
346
+ return undefined;
347
+ });
348
+ references.register(documentRoot, getDefaultOrigin());
349
+ const defaultOrigin = references.origin(documentRoot) ?? getDefaultOrigin();
350
+ const hasRootIdentity = references.identity(documentRoot) !== undefined || getId(documentRoot) !== undefined;
351
+ const referenceToRoot = (pointer, sourceOrigin) => {
352
+ return hasRootIdentity && references.isSchemaResource(sourceOrigin) && sourceOrigin !== defaultOrigin
353
+ ? `${defaultOrigin}${pointer}`
354
+ : pointer;
355
+ };
322
356
  // Create the cache to store the compressed values to their map values
323
357
  if (documentRoot[config.externalDocumentsMappingsKey] === undefined) {
324
358
  documentRoot[config.externalDocumentsMappingsKey] = {};
325
359
  }
326
360
  const { generate } = uniqueValueGeneratorFactory(config.compress ?? getHash, documentRoot[config.externalDocumentsMappingsKey]);
361
+ // Index supplied and previously bundled documents before resolving canonical identities.
362
+ const storedDocuments = documentRoot[config.externalDocumentsKey];
363
+ if (isObject(storedDocuments)) {
364
+ for (const [key, document] of Object.entries(storedDocuments)) {
365
+ const retrievalUri = resolveReferencePath(defaultOrigin, documentRoot[config.externalDocumentsMappingsKey][key] ?? key);
366
+ references.register(document, retrievalUri, [config.externalDocumentsKey, key]);
367
+ }
368
+ }
369
+ for (const [uri, pending] of cache) {
370
+ const result = await pending;
371
+ if (result.ok) {
372
+ const retrievalUri = resolveReferencePath(defaultOrigin, uri);
373
+ const relativeUri = toRelativePath(retrievalUri, defaultOrigin);
374
+ const key = await generate(relativeUri);
375
+ references.register(result.data, retrievalUri, [config.externalDocumentsKey, key]);
376
+ }
377
+ }
327
378
  /**
328
379
  * Executes lifecycle hooks defined both in the bundler configuration and any extended lifecycle plugins.
329
380
  * This utility function ensures that all relevant hooks for a given event type are called in order:
@@ -367,6 +418,7 @@ export async function bundle(input, config) {
367
418
  // A node with its own `$id` establishes a new base for resolving relative references within it.
368
419
  // Fall back to the origin inherited from the parent document otherwise.
369
420
  const id = getId(root);
421
+ const nodeOrigin = references.origin(root) ?? (id ? resolveReferencePath(origin, id) : origin);
370
422
  const context = {
371
423
  path: currentPath,
372
424
  referencedFromPath,
@@ -374,7 +426,7 @@ export async function bundle(input, config) {
374
426
  parentNode: parent,
375
427
  rootNode: documentRoot,
376
428
  loaders: loaderPlugins,
377
- origin: id ?? origin,
429
+ origin: nodeOrigin,
378
430
  };
379
431
  await executeHooks('onBeforeNodeProcess', root, context);
380
432
  if (hasRef(root)) {
@@ -387,17 +439,48 @@ export async function bundle(input, config) {
387
439
  // In case of partial bundling, we still need to ensure that all dependencies
388
440
  // of the local reference are bundled to create a complete and self-contained partial bundle
389
441
  // This is important to maintain the integrity of the partial bundle
390
- const localRef = convertToLocalRef(ref, id ?? origin, schemas);
442
+ const local = references.resolve(ref, nodeOrigin);
443
+ const localRef = local?.path;
391
444
  if (localRef !== undefined) {
445
+ // A fragment under an embedded $id resolves within that schema, not the document.
446
+ // Without a declared root identity, retain references to root resources rather
447
+ // than baking a retrieval location into an otherwise portable bundle.
448
+ const preserveRootResourceReference = !hasRootIdentity &&
449
+ local.document === documentRoot &&
450
+ references.isSchemaResource(nodeOrigin) &&
451
+ nodeOrigin !== defaultOrigin;
452
+ if (!local.preserveReference && !preserveRootResourceReference) {
453
+ root.$ref = referenceToRoot(localRef ? `#/${localRef}` : '#', nodeOrigin);
454
+ }
392
455
  if (isPartialBundling) {
393
456
  const segments = getSegmentsFromPath(`/${localRef}`);
394
457
  const parent = segments.length > 0 ? getValueByPath(documentRoot, segments.slice(0, -1)).value : undefined;
395
- const targetValue = getValueByPath(documentRoot, segments);
458
+ const targetValue = {
459
+ value: local.value,
460
+ context: isObject(local.value) ? references.origin(local.value) : nodeOrigin,
461
+ };
396
462
  // When doing partial bundling, we need to recursively bundle all dependencies
397
463
  // referenced by this local reference to ensure the partial bundle is complete.
398
464
  // This includes not just the direct reference but also all its dependencies,
399
465
  // creating a complete and self-contained partial bundle.
400
- await bundler(targetValue.value, targetValue.context, isChunkParent, depth + 1, segments, parent, referencedFromPath);
466
+ //
467
+ // The pointer addresses the root document, so the target's base is the nearest `$id` on
468
+ // the way to it, else the document origin. `getValueByPath` reports no `$id` as an empty
469
+ // string, and handing that on as the origin left a relative reference under the target
470
+ // (a chunk reference written relative to the document) with nothing to resolve against.
471
+ await bundler(targetValue.value, targetValue.context || defaultOrigin, isChunkParent, depth + 1, segments, parent, referencedFromPath);
472
+ }
473
+ if (local.documentPath.length > 0) {
474
+ const origin = isObject(local.document) || Array.isArray(local.document) ? references.origin(local.document) : nodeOrigin;
475
+ await bundler(local.document, origin, isChunkParent, depth + 1, local.documentPath);
476
+ if (config.treeShake) {
477
+ const [key, documentKey] = local.documentPath;
478
+ resolveAndCopyReferences(documentRoot, { [key]: { [documentKey]: local.document } }, `/${localRef}`, key, documentKey, false, new Set(), references.identity(local.document)?.metadata);
479
+ }
480
+ else {
481
+ const metadata = references.identity(local.document)?.metadata;
482
+ setValueAtPath(documentRoot, `/${local.documentPath.map(escapeJsonPointer).join('/')}`, isObject(local.document) && metadata ? { ...local.document, ...metadata } : local.document);
483
+ }
401
484
  }
402
485
  await executeHooks('onAfterNodeProcess', root, context);
403
486
  return;
@@ -405,7 +488,7 @@ export async function bundle(input, config) {
405
488
  const [prefix, path = ''] = ref.split('#', 2);
406
489
  // Combine the current origin with the new path to resolve relative references
407
490
  // correctly within the context of the external file being processed
408
- const resolvedPath = resolveReferencePath(id ?? origin, prefix);
491
+ const resolvedPath = resolveReferencePath(nodeOrigin, prefix);
409
492
  const relativePath = toRelativePath(resolvedPath, defaultOrigin);
410
493
  // Generate a unique compressed path for the external document
411
494
  // This is used as a key to store and reference the bundled external document
@@ -422,19 +505,10 @@ export async function bundle(input, config) {
422
505
  // Process the result only once to avoid duplicate processing and prevent multiple prefixing
423
506
  // of internal references, which would corrupt the reference paths
424
507
  if (!seen) {
425
- // Skip prefixing for chunks since they are meant to be self-contained and their
426
- // internal references should remain relative to their original location. Chunks
427
- // are typically used for modular components that need to maintain their own
428
- // reference context without being affected by the main document's structure.
508
+ // Index the whole external document before following references within it.
509
+ // Chunks retain the referring context because they are already self-contained.
429
510
  if (!isChunk) {
430
- // Update internal references in the resolved document to use the correct base path.
431
- // When we embed external documents, their internal references need to be updated to
432
- // maintain the correct path context relative to the main document. This is crucial
433
- // because internal references in the external document are relative to its original
434
- // location, but when embedded, they need to be relative to their new location in
435
- // the main document's x-ext section. Without this update, internal references
436
- // would point to incorrect locations and break the document structure.
437
- prefixInternalRefRecursive(result.data, [extensions.externalDocuments, compressedPath]);
511
+ references.register(result.data, resolvedPath, [config.externalDocumentsKey, compressedPath]);
438
512
  }
439
513
  // Recursively process the resolved content
440
514
  // to handle any nested references it may contain. We pass the resolvedPath as the new origin
@@ -451,7 +525,7 @@ export async function bundle(input, config) {
451
525
  // Store only the subtree that is actually used
452
526
  // This optimizes the bundle size by only including the parts of the external document
453
527
  // that are referenced, rather than the entire document
454
- resolveAndCopyReferences(documentRoot, { [config.externalDocumentsKey]: { [compressedPath]: result.data } }, prefixInternalRef(`#${path}`, [config.externalDocumentsKey, compressedPath]).substring(1), config.externalDocumentsKey, compressedPath);
528
+ resolveAndCopyReferences(documentRoot, { [config.externalDocumentsKey]: { [compressedPath]: result.data } }, prefixInternalRef(`#${path}`, [config.externalDocumentsKey, compressedPath]).substring(1), config.externalDocumentsKey, compressedPath, false, new Set(), references.identity(result.data)?.metadata);
455
529
  }
456
530
  else if (!seen) {
457
531
  // Store the external document in the main document's x-ext key
@@ -459,12 +533,13 @@ export async function bundle(input, config) {
459
533
  // This preserves all content and is faster since we don't need to analyze and copy
460
534
  // specific parts. This approach is ideal when storing the result in memory
461
535
  // as it avoids the overhead of tree shaking operations
462
- setValueAtPath(documentRoot, `/${config.externalDocumentsKey}/${compressedPath}`, result.data);
536
+ const metadata = references.identity(result.data)?.metadata;
537
+ setValueAtPath(documentRoot, `/${config.externalDocumentsKey}/${compressedPath}`, isObject(result.data) && metadata ? { ...result.data, ...metadata } : result.data);
463
538
  }
464
539
  // Update the $ref to point to the embedded document in x-ext
465
540
  // This is necessary because we need to maintain the correct path context
466
541
  // for the embedded document while preserving its internal structure
467
- root.$ref = prefixInternalRef(`#${path}`, [config.externalDocumentsKey, compressedPath]);
542
+ root.$ref = referenceToRoot(prefixInternalRef(`#${path}`, [config.externalDocumentsKey, compressedPath]), nodeOrigin);
468
543
  await executeHooks('onResolveSuccess', root);
469
544
  await executeHooks('onAfterNodeProcess', root, context);
470
545
  return;
@@ -480,7 +555,7 @@ export async function bundle(input, config) {
480
555
  if (key === config.externalDocumentsKey || key === config.externalDocumentsMappingsKey) {
481
556
  continue;
482
557
  }
483
- await bundler(root[key], id ?? origin, isChunkParent, depth + 1, [...currentPath, key], root, referencedFromPath);
558
+ await bundler(root[key], nodeOrigin, isChunkParent, depth + 1, [...currentPath, key], root, referencedFromPath);
484
559
  }
485
560
  await executeHooks('onAfterNodeProcess', root, context);
486
561
  };
@@ -0,0 +1,30 @@
1
+ /** Format-specific document identity, supplied by the caller. */
2
+ export type DocumentIdentity = {
3
+ /** Declared base URI, resolved relative to the retrieval URI when necessary. */
4
+ baseUri: string;
5
+ /** Root properties retained on copies embedded in the bundle; never applied to the source document. */
6
+ metadata?: Record<string, unknown>;
7
+ };
8
+ /** Reads document identity without imposing a document format on the bundler. */
9
+ export type DocumentResolver = (document: unknown, retrievalUri: string) => DocumentIdentity | undefined;
10
+ /** Resolution metadata survives moving documents into the bundle. */
11
+ type DocumentReferences = {
12
+ identity: (document: unknown) => DocumentIdentity | undefined;
13
+ register: (document: unknown, retrievalUri: string, path?: string[]) => void;
14
+ isSchemaResource: (uri: string) => boolean;
15
+ origin: (node: object) => string | undefined;
16
+ resolve: (ref: string, base: string) => {
17
+ path: string;
18
+ value: unknown;
19
+ preserveReference: boolean;
20
+ document: unknown;
21
+ documentPath: string[];
22
+ } | undefined;
23
+ };
24
+ /**
25
+ * Indexes complete documents before following their references. A declared identity
26
+ * can point to content already in memory even when that URI cannot be fetched.
27
+ */
28
+ export declare const documentReferences: (externalDocumentsKey: string, resolveDocument?: DocumentResolver) => DocumentReferences;
29
+ export {};
30
+ //# sourceMappingURL=document-references.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"document-references.d.ts","sourceRoot":"","sources":["../../src/bundle/document-references.ts"],"names":[],"mappings":"AAQA,iEAAiE;AACjE,MAAM,MAAM,gBAAgB,GAAG;IAC7B,gFAAgF;IAChF,OAAO,EAAE,MAAM,CAAA;IACf,uGAAuG;IACvG,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CACnC,CAAA;AAED,iFAAiF;AACjF,MAAM,MAAM,gBAAgB,GAAG,CAAC,QAAQ,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,KAAK,gBAAgB,GAAG,SAAS,CAAA;AAaxG,qEAAqE;AACrE,KAAK,kBAAkB,GAAG;IACxB,QAAQ,EAAE,CAAC,QAAQ,EAAE,OAAO,KAAK,gBAAgB,GAAG,SAAS,CAAA;IAC7D,QAAQ,EAAE,CAAC,QAAQ,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,EAAE,KAAK,IAAI,CAAA;IAC5E,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAA;IAC1C,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAA;IAC5C,OAAO,EAAE,CACP,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,MAAM,KAEV;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,OAAO,CAAC;QAAC,iBAAiB,EAAE,OAAO,CAAC;QAAC,QAAQ,EAAE,OAAO,CAAC;QAAC,YAAY,EAAE,MAAM,EAAE,CAAA;KAAE,GACvG,SAAS,CAAA;CACd,CAAA;AAED;;;GAGG;AACH,eAAO,MAAM,kBAAkB,GAC7B,sBAAsB,MAAM,EAC5B,kBAAkB,gBAAgB,KACjC,kBAoGF,CAAA"}
@@ -0,0 +1,104 @@
1
+ import { isObject } from '@scalar/helpers/object/is-object';
2
+ import { escapeJsonPointer } from '../helpers/escape-json-pointer.js';
3
+ import { getId } from '../helpers/get-schemas.js';
4
+ import { getSegmentsFromPath } from '../helpers/get-segments-from-path.js';
5
+ import { getValueByPath } from '../helpers/get-value-by-path.js';
6
+ import { resolveReferencePath } from '../helpers/resolve-reference-path.js';
7
+ /**
8
+ * Indexes complete documents before following their references. A declared identity
9
+ * can point to content already in memory even when that URI cannot be fetched.
10
+ */
11
+ export const documentReferences = (externalDocumentsKey, resolveDocument) => {
12
+ const identities = new Map();
13
+ const resources = new Map();
14
+ const origins = new WeakMap();
15
+ const bundledResources = new Map();
16
+ const register = (document, retrievalUri, path = []) => {
17
+ const identity = resolveDocument?.(document, retrievalUri);
18
+ const base = identity === undefined ? retrievalUri : resolveReferencePath(retrievalUri, identity.baseUri);
19
+ if (identity !== undefined) {
20
+ identities.set(document, { ...identity, baseUri: base });
21
+ }
22
+ const resource = { value: document, path, schema: false, embedded: path.length > 0, document, documentPath: path };
23
+ // Retrieval aliases retain compatibility with callers that supply local copies.
24
+ resources.set(retrievalUri, resource);
25
+ resources.set(base, resource);
26
+ if (path.length > 1) {
27
+ bundledResources.set(path[1], resource);
28
+ }
29
+ // Shared nodes use the first traversal path and base, including for $id and $anchor
30
+ // registration. This also prevents cycles from being indexed repeatedly.
31
+ const visited = new WeakSet();
32
+ const visit = (value, origin, location, inheritedIdentifier = '') => {
33
+ if (value === null || typeof value !== 'object' || visited.has(value)) {
34
+ return;
35
+ }
36
+ visited.add(value);
37
+ const id = getId(value);
38
+ const identifier = id ?? inheritedIdentifier;
39
+ const current = id === undefined ? origin : resolveReferencePath(origin, id);
40
+ origins.set(value, current);
41
+ const anchor = isObject(value) && typeof value.$anchor === 'string' ? value.$anchor : undefined;
42
+ if (id !== undefined || anchor !== undefined) {
43
+ const schemaResource = {
44
+ value,
45
+ path: location,
46
+ schema: true,
47
+ identifier,
48
+ embedded: path.length > 0,
49
+ document,
50
+ documentPath: path,
51
+ };
52
+ if (id !== undefined) {
53
+ resources.set(current, schemaResource);
54
+ }
55
+ if (anchor !== undefined) {
56
+ resources.set(`${current}#${anchor}`, schemaResource);
57
+ }
58
+ }
59
+ for (const [key, child] of Object.entries(value)) {
60
+ if (key !== externalDocumentsKey) {
61
+ visit(child, current, [...location, key], identifier);
62
+ }
63
+ }
64
+ };
65
+ visit(document, base, path);
66
+ };
67
+ return {
68
+ identity: (document) => identities.get(document),
69
+ register,
70
+ isSchemaResource: (uri) => resources.get(uri)?.schema === true,
71
+ origin: (node) => origins.get(node),
72
+ resolve: (ref, base) => {
73
+ const [prefix, fragment = ''] = ref.split('#', 2);
74
+ const uri = prefix ? resolveReferencePath(base, prefix) : base;
75
+ const pointer = fragment.startsWith('/') ? getSegmentsFromPath(fragment) : [];
76
+ const bundled = (!prefix || resources.get(uri)?.path.length === 0) && pointer[0] === externalDocumentsKey
77
+ ? bundledResources.get(pointer[1])
78
+ : undefined;
79
+ const resource = bundled ?? resources.get(fragment && !fragment.startsWith('/') ? `${uri}#${fragment}` : uri);
80
+ if (!resource) {
81
+ return undefined;
82
+ }
83
+ const segments = bundled ? pointer.slice(2) : pointer;
84
+ const location = [...resource.path, ...segments];
85
+ const value = getValueByPath(resource.value, segments).value;
86
+ // Missing targets must stay unresolved instead of becoming pointers to absent values.
87
+ const isUnresolved = value === undefined;
88
+ // Fragment-only schema references are relative to their own resource, even when embedded.
89
+ const isLocalSchemaReference = resource.schema && !prefix;
90
+ // References matching an authored root schema identifier remain usable by downstream consumers.
91
+ // Preserve relative identifiers too: rewriting them changes schema names shown by renderers.
92
+ const isDeclaredRootSchemaReference = !resource.embedded && resource.schema && Boolean(prefix) && prefix === resource.identifier;
93
+ // Fragment-only document references already address the root without relocation.
94
+ const isLocalRootDocumentReference = !resource.embedded && !resource.schema && !prefix;
95
+ return {
96
+ path: location.map(escapeJsonPointer).join('/'),
97
+ value,
98
+ document: resource.document,
99
+ documentPath: resource.documentPath,
100
+ preserveReference: isUnresolved || isLocalSchemaReference || isDeclaredRootSchemaReference || isLocalRootDocumentReference,
101
+ };
102
+ },
103
+ };
104
+ };
@@ -1,4 +1,5 @@
1
1
  export { resolveReferencePath } from '../helpers/resolve-reference-path.js';
2
2
  export type { LifecyclePlugin, LoaderPlugin, Plugin, ResolveResult } from './bundle.js';
3
3
  export { bundle, extensions, isLocalRef, prefixInternalRef, prefixInternalRefRecursive, resolveAndCopyReferences, } from './bundle.js';
4
+ export type { DocumentIdentity, DocumentResolver } from './document-references.js';
4
5
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/bundle/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,kCAAkC,CAAA;AAEvE,YAAY,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AACpF,OAAO,EACL,MAAM,EACN,UAAU,EACV,UAAU,EACV,iBAAiB,EACjB,0BAA0B,EAC1B,wBAAwB,GACzB,MAAM,UAAU,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/bundle/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,kCAAkC,CAAA;AAEvE,YAAY,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AACpF,OAAO,EACL,MAAM,EACN,UAAU,EACV,UAAU,EACV,iBAAiB,EACjB,0BAA0B,EAC1B,wBAAwB,GACzB,MAAM,UAAU,CAAA;AACjB,YAAY,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"convert-to-local-ref.d.ts","sourceRoot":"","sources":["../../src/helpers/convert-to-local-ref.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,eAAO,MAAM,iBAAiB,GAC5B,KAAK,MAAM,EACX,gBAAgB,MAAM,EACtB,SAAS,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,KAC3B,MAAM,GAAG,SA8BX,CAAA"}
1
+ {"version":3,"file":"convert-to-local-ref.d.ts","sourceRoot":"","sources":["../../src/helpers/convert-to-local-ref.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,eAAO,MAAM,iBAAiB,GAC5B,KAAK,MAAM,EACX,gBAAgB,MAAM,EACtB,SAAS,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,KAC3B,MAAM,GAAG,SA+BX,CAAA"}
@@ -18,7 +18,8 @@ export const convertToLocalRef = (ref, currentContext, schemas) => {
18
18
  }
19
19
  // If the pathOrAnchor is a JSON pointer, we need to append it to the baseUrl
20
20
  if (pathOrAnchor.startsWith('/')) {
21
- return `${schemas.get(baseUrl)}${pathOrAnchor}`;
21
+ const rootPath = schemas.get(baseUrl);
22
+ return rootPath ? `${rootPath}${pathOrAnchor}` : pathOrAnchor.slice(1);
22
23
  }
23
24
  // If the pathOrAnchor is an anchor, we need to return the anchor
24
25
  return schemas.get(`${baseUrl}#${pathOrAnchor}`);
@@ -1,6 +1,9 @@
1
1
  /**
2
2
  * Resolves a reference path by combining a base path with a relative path.
3
- * Handles both remote URLs and local file paths.
3
+ * Scheme-bearing references keep their identity, including non-fetchable URNs.
4
+ * Hierarchical URI bases use URL resolution (query, fragment, and directory semantics);
5
+ * an opaque base such as a URN cannot resolve a relative path and throws.
6
+ * Scheme-less inputs retain filesystem resolution, including Windows drive paths.
4
7
  *
5
8
  * @param base - The base path (can be a URL or local file path)
6
9
  * @param relativePath - The relative path to resolve against the base
@@ -1 +1 @@
1
- {"version":3,"file":"resolve-reference-path.d.ts","sourceRoot":"","sources":["../../src/helpers/resolve-reference-path.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,oBAAoB,GAAI,MAAM,MAAM,EAAE,cAAc,MAAM,WAYtE,CAAA"}
1
+ {"version":3,"file":"resolve-reference-path.d.ts","sourceRoot":"","sources":["../../src/helpers/resolve-reference-path.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,oBAAoB,GAAI,MAAM,MAAM,EAAE,cAAc,MAAM,KAAG,MAgBzE,CAAA"}
@@ -1,8 +1,11 @@
1
1
  import path from 'pathe';
2
- import { isHttpUrl } from '../helpers/is-http-url.js';
2
+ const hasScheme = (value) => /^[a-z][a-z0-9+.-]*:/i.test(value) && !/^[a-z]:[\\/]/i.test(value);
3
3
  /**
4
4
  * Resolves a reference path by combining a base path with a relative path.
5
- * Handles both remote URLs and local file paths.
5
+ * Scheme-bearing references keep their identity, including non-fetchable URNs.
6
+ * Hierarchical URI bases use URL resolution (query, fragment, and directory semantics);
7
+ * an opaque base such as a URN cannot resolve a relative path and throws.
8
+ * Scheme-less inputs retain filesystem resolution, including Windows drive paths.
6
9
  *
7
10
  * @param base - The base path (can be a URL or local file path)
8
11
  * @param relativePath - The relative path to resolve against the base
@@ -17,13 +20,16 @@ import { isHttpUrl } from '../helpers/is-http-url.js';
17
20
  * // Returns: '/path/to/user.json'
18
21
  */
19
22
  export const resolveReferencePath = (base, relativePath) => {
20
- if (isHttpUrl(relativePath)) {
23
+ if (hasScheme(relativePath)) {
21
24
  return relativePath;
22
25
  }
23
- if (isHttpUrl(base)) {
24
- const baseUrl = new URL(base);
25
- baseUrl.pathname = path.posix.resolve('/', path.dirname(baseUrl.pathname), relativePath);
26
- return baseUrl.toString();
26
+ if (hasScheme(base)) {
27
+ return new URL(relativePath, base).href;
27
28
  }
28
- return path.resolve(path.dirname(base), relativePath);
29
+ if (!relativePath) {
30
+ return base;
31
+ }
32
+ const directory = /[\\/]$/.test(base) ? base : path.dirname(base);
33
+ const resolved = path.resolve(directory, relativePath);
34
+ return /[\\/]$/.test(relativePath) && !resolved.endsWith('/') ? `${resolved}/` : resolved;
29
35
  };
@@ -1 +1 @@
1
- {"version":3,"file":"to-relative-path.d.ts","sourceRoot":"","sources":["../../src/helpers/to-relative-path.ts"],"names":[],"mappings":"AAIA;;;;;;;GAOG;AACH,eAAO,MAAM,cAAc,GAAI,OAAO,MAAM,EAAE,MAAM,MAAM,WAmCzD,CAAA"}
1
+ {"version":3,"file":"to-relative-path.d.ts","sourceRoot":"","sources":["../../src/helpers/to-relative-path.ts"],"names":[],"mappings":"AAIA;;;;;;;GAOG;AACH,eAAO,MAAM,cAAc,GAAI,OAAO,MAAM,EAAE,MAAM,MAAM,KAAG,MA6C5D,CAAA"}
@@ -9,6 +9,12 @@ import { isHttpUrl } from '../helpers/is-http-url.js';
9
9
  * - Otherwise, computes the relative path between two local paths.
10
10
  */
11
11
  export const toRelativePath = (input, base) => {
12
+ // This format-agnostic helper has no document-identity registry. Preserve every
13
+ // non-HTTP URI, not only known $self values: file:, urn:, and custom schemes must
14
+ // not become filesystem paths. Loader plugins decide whether a URI is fetchable.
15
+ if (URL.canParse(input) && !isHttpUrl(input) && !/^[a-z]:[\\/]/i.test(input)) {
16
+ return input;
17
+ }
12
18
  // Both input and base are remote URLs
13
19
  if (isHttpUrl(input) && isHttpUrl(base)) {
14
20
  const inputUrl = new URL(input);
@@ -18,10 +24,14 @@ export const toRelativePath = (input, base) => {
18
24
  return input;
19
25
  }
20
26
  // Get the directory of the base URL pathname (not the file itself)
21
- const baseDir = path.dirname(path.posix.resolve('/', baseUrl.pathname));
27
+ const baseDir = baseUrl.pathname.endsWith('/') ? baseUrl.pathname : path.dirname(baseUrl.pathname);
22
28
  const inputPath = path.posix.resolve('/', inputUrl.pathname);
23
29
  // Return the relative path from baseDir to inputPath
24
- return path.posix.relative(baseDir, inputPath);
30
+ const relativePath = path.posix.relative(baseDir, inputPath);
31
+ const suffix = relativePath && inputUrl.pathname.endsWith('/') ? '/' : '';
32
+ const relativeUri = `${relativePath}${suffix}${inputUrl.search}${inputUrl.hash}`;
33
+ // Keep the absolute URI when path normalization would change its identity.
34
+ return new URL(relativeUri, base).href === inputUrl.href ? relativeUri : input;
25
35
  }
26
36
  // Base is a remote URL, input is a local path
27
37
  if (isHttpUrl(base)) {
@@ -36,7 +46,7 @@ export const toRelativePath = (input, base) => {
36
46
  return input;
37
47
  }
38
48
  // Both input and base are local paths; return the relative path
39
- const baseDir = path.dirname(path.resolve(base));
49
+ const baseDir = base.endsWith('/') ? path.resolve(base) : path.dirname(path.resolve(base));
40
50
  const inputPath = path.resolve(input);
41
51
  return path.relative(baseDir, inputPath);
42
52
  };
@@ -0,0 +1,3 @@
1
+ export type { JoinConflict, JoinContext, JoinOptions, JoinResult, JoinStrategy } from './join.js';
2
+ export { join } from './join.js';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/join/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,YAAY,EAAE,WAAW,EAAE,WAAW,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAA;AAC9F,OAAO,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAA"}
@@ -0,0 +1 @@
1
+ export { join } from './join.js';
@@ -0,0 +1,36 @@
1
+ import type { UnknownObject } from '../types.js';
2
+ /** Controls how a value is combined with the value from earlier inputs. */
3
+ export type JoinStrategy = 'merge' | 'merge-by-index' | 'replace' | 'conflict' | 'skip' | {
4
+ uniqueBy: string;
5
+ };
6
+ /** Context for choosing a strategy. Paths are segments, so keys containing slashes stay intact. */
7
+ export type JoinContext = {
8
+ path: readonly string[];
9
+ current: unknown;
10
+ incoming: unknown;
11
+ };
12
+ /** Customize merging without coupling it to a document format. */
13
+ export type JoinOptions = {
14
+ strategy?: (context: JoinContext) => JoinStrategy;
15
+ };
16
+ /** A duplicate value at a location marked as a conflict by the caller. */
17
+ export type JoinConflict = {
18
+ path: string[];
19
+ };
20
+ /** Conflicts prevent returning a partially joined document. */
21
+ export type JoinResult = {
22
+ ok: true;
23
+ document: UnknownObject;
24
+ } | {
25
+ ok: false;
26
+ conflicts: JoinConflict[];
27
+ };
28
+ /**
29
+ * Join JSON objects without interpreting any document standard or resolving references.
30
+ * Objects merge recursively; arrays and other values are replaced by later inputs.
31
+ * A strategy can instead replace an entire object, reject duplicate keys (even equal
32
+ * values), or combine arrays by a property, retaining the first occurrence.
33
+ * Inputs must be acyclic JSON objects and are never mutated or shared with the result.
34
+ */
35
+ export declare const join: (inputs: readonly UnknownObject[], options?: JoinOptions) => JoinResult;
36
+ //# sourceMappingURL=join.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"join.d.ts","sourceRoot":"","sources":["../../src/join/join.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AAE7C,2EAA2E;AAC3E,MAAM,MAAM,YAAY,GAAG,OAAO,GAAG,gBAAgB,GAAG,SAAS,GAAG,UAAU,GAAG,MAAM,GAAG;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAA;AAE9G,mGAAmG;AACnG,MAAM,MAAM,WAAW,GAAG;IACxB,IAAI,EAAE,SAAS,MAAM,EAAE,CAAA;IACvB,OAAO,EAAE,OAAO,CAAA;IAChB,QAAQ,EAAE,OAAO,CAAA;CAClB,CAAA;AAED,kEAAkE;AAClE,MAAM,MAAM,WAAW,GAAG;IACxB,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,YAAY,CAAA;CAClD,CAAA;AAED,0EAA0E;AAC1E,MAAM,MAAM,YAAY,GAAG;IAAE,IAAI,EAAE,MAAM,EAAE,CAAA;CAAE,CAAA;AAE7C,+DAA+D;AAC/D,MAAM,MAAM,UAAU,GAAG;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,EAAE,aAAa,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,SAAS,EAAE,YAAY,EAAE,CAAA;CAAE,CAAA;AAazG;;;;;;GAMG;AACH,eAAO,MAAM,IAAI,GAAI,QAAQ,SAAS,aAAa,EAAE,EAAE,UAAS,WAAgB,KAAG,UAmElF,CAAA"}
@@ -0,0 +1,76 @@
1
+ import { isObject } from '@scalar/helpers/object/is-object';
2
+ /** Copy literal JSON keys as own data properties, without invoking prototype setters. */
3
+ const copy = (value) => {
4
+ if (Array.isArray(value)) {
5
+ return value.map(copy);
6
+ }
7
+ if (isObject(value)) {
8
+ return Object.fromEntries(Object.entries(value).map(([key, child]) => [key, copy(child)]));
9
+ }
10
+ return value;
11
+ };
12
+ /**
13
+ * Join JSON objects without interpreting any document standard or resolving references.
14
+ * Objects merge recursively; arrays and other values are replaced by later inputs.
15
+ * A strategy can instead replace an entire object, reject duplicate keys (even equal
16
+ * values), or combine arrays by a property, retaining the first occurrence.
17
+ * Inputs must be acyclic JSON objects and are never mutated or shared with the result.
18
+ */
19
+ export const join = (inputs, options = {}) => {
20
+ const conflicts = [];
21
+ const skipped = Symbol('skipped');
22
+ const mergeFields = (target, source, path) => {
23
+ for (const [key, value] of Object.entries(source)) {
24
+ const hasKey = Object.hasOwn(target, key);
25
+ const merged = combine(hasKey ? Reflect.get(target, key) : undefined, value, [...path, key], hasKey);
26
+ if (merged !== skipped) {
27
+ // Defining an own property preserves literal __proto__ keys without changing the prototype.
28
+ Object.defineProperty(target, key, { value: merged, enumerable: true, configurable: true, writable: true });
29
+ }
30
+ }
31
+ };
32
+ const combine = (current, incoming, path, exists) => {
33
+ const strategy = options.strategy?.({ path, current, incoming }) ?? 'merge';
34
+ if (strategy === 'skip') {
35
+ return skipped;
36
+ }
37
+ if (strategy === 'conflict' && exists) {
38
+ conflicts.push({ path });
39
+ return current;
40
+ }
41
+ if (typeof strategy === 'object' && Array.isArray(incoming)) {
42
+ const seen = new Set();
43
+ return [...(Array.isArray(current) ? current : []), ...incoming]
44
+ .filter((item) => {
45
+ const key = isObject(item) && Object.hasOwn(item, strategy.uniqueBy) ? item[strategy.uniqueBy] : undefined;
46
+ // Items without the identity property remain distinct.
47
+ if (key === undefined) {
48
+ return true;
49
+ }
50
+ if (seen.has(key)) {
51
+ return false;
52
+ }
53
+ seen.add(key);
54
+ return true;
55
+ })
56
+ .map(copy);
57
+ }
58
+ if (strategy === 'merge-by-index' && Array.isArray(incoming)) {
59
+ const result = Array.isArray(current) ? current : [];
60
+ mergeFields(result, incoming, path);
61
+ return result;
62
+ }
63
+ if ((strategy === 'merge' || strategy === 'merge-by-index') && isObject(incoming)) {
64
+ const result = isObject(current) ? current : {};
65
+ mergeFields(result, incoming, path);
66
+ return result;
67
+ }
68
+ return copy(incoming);
69
+ };
70
+ // The root always merges; strategies apply to fields within each document.
71
+ const document = {};
72
+ for (const input of inputs) {
73
+ mergeFields(document, input, []);
74
+ }
75
+ return conflicts.length ? { ok: false, conflicts } : { ok: true, document };
76
+ };
@@ -39,6 +39,8 @@ import type { UnknownObject } from '../types.js';
39
39
  */
40
40
  export declare const createMagicProxy: <T extends Record<keyof T & symbol, unknown>, S extends UnknownObject>(target: T, options?: Partial<{
41
41
  showInternal: boolean;
42
+ /** Canonical URI identifying the root document for qualified references. */
43
+ documentUri: string;
42
44
  }>, args?: {
43
45
  /**
44
46
  * The root object for resolving local JSON references.
@@ -1 +1 @@
1
- {"version":3,"file":"proxy.d.ts","sourceRoot":"","sources":["../../src/magic-proxy/proxy.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AAQ5C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,eAAO,MAAM,gBAAgB,GAAI,CAAC,SAAS,MAAM,CAAC,MAAM,CAAC,GAAG,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,SAAS,aAAa,EACnG,QAAQ,CAAC,EACT,UAAU,OAAO,CAAC;IAAE,YAAY,EAAE,OAAO,CAAA;CAAE,CAAC,EAC5C,OAAM;IACJ;;OAEG;IACH,IAAI,EAAE,CAAC,GAAG,CAAC,CAAA;IACX;;;;OAIG;IACH,UAAU,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC,CAAA;IAC9B;;OAEG;IACH,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC3B;;OAEG;IACH,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC5B;;;;OAIG;IACH,cAAc,EAAE,MAAM,CAAA;CAOvB,KACA,CAkNF,CAAA;AAMD;;;;;;;;;GASG;AACH,wBAAgB,MAAM,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,CAUnC"}
1
+ {"version":3,"file":"proxy.d.ts","sourceRoot":"","sources":["../../src/magic-proxy/proxy.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AAQ5C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,eAAO,MAAM,gBAAgB,GAAI,CAAC,SAAS,MAAM,CAAC,MAAM,CAAC,GAAG,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,SAAS,aAAa,EACnG,QAAQ,CAAC,EACT,UAAU,OAAO,CAAC;IAChB,YAAY,EAAE,OAAO,CAAA;IACrB,4EAA4E;IAC5E,WAAW,EAAE,MAAM,CAAA;CACpB,CAAC,EACF,OAAM;IACJ;;OAEG;IACH,IAAI,EAAE,CAAC,GAAG,CAAC,CAAA;IACX;;;;OAIG;IACH,UAAU,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC,CAAA;IAC9B;;OAEG;IACH,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC3B;;OAEG;IACH,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC5B;;;;OAIG;IACH,cAAc,EAAE,MAAM,CAAA;CAOvB,KACA,CAkNF,CAAA;AAMD;;;;;;;;;GASG;AACH,wBAAgB,MAAM,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,CAUnC"}
@@ -50,7 +50,7 @@ export const createMagicProxy = (target, options, args = {
50
50
  root: target,
51
51
  proxyCache: new WeakMap(),
52
52
  cache: new Map(),
53
- schemas: getSchemas(target),
53
+ schemas: getSchemas(target, '', [], new Map(options?.documentUri ? [[options.documentUri, '']] : [])),
54
54
  currentContext: '',
55
55
  }) => {
56
56
  if (!isObject(target) && !Array.isArray(target)) {
package/package.json CHANGED
@@ -10,7 +10,7 @@
10
10
  "url": "git+https://github.com/scalar/scalar.git",
11
11
  "directory": "packages/json-magic"
12
12
  },
13
- "version": "0.13.5",
13
+ "version": "0.15.0",
14
14
  "engines": {
15
15
  "node": ">=22"
16
16
  },
@@ -51,6 +51,16 @@
51
51
  "types": "./dist/helpers/escape-json-pointer.d.ts",
52
52
  "default": "./dist/helpers/escape-json-pointer.js"
53
53
  },
54
+ "./helpers/convert-to-local-ref": {
55
+ "import": "./dist/helpers/convert-to-local-ref.js",
56
+ "types": "./dist/helpers/convert-to-local-ref.d.ts",
57
+ "default": "./dist/helpers/convert-to-local-ref.js"
58
+ },
59
+ "./helpers/get-schemas": {
60
+ "import": "./dist/helpers/get-schemas.js",
61
+ "types": "./dist/helpers/get-schemas.d.ts",
62
+ "default": "./dist/helpers/get-schemas.js"
63
+ },
54
64
  "./helpers/get-segments-from-path": {
55
65
  "import": "./dist/helpers/get-segments-from-path.js",
56
66
  "types": "./dist/helpers/get-segments-from-path.d.ts",
@@ -96,6 +106,11 @@
96
106
  "types": "./dist/helpers/unescape-json-pointer.d.ts",
97
107
  "default": "./dist/helpers/unescape-json-pointer.js"
98
108
  },
109
+ "./join": {
110
+ "import": "./dist/join/index.js",
111
+ "types": "./dist/join/index.d.ts",
112
+ "default": "./dist/join/index.js"
113
+ },
99
114
  "./magic-proxy": {
100
115
  "import": "./dist/magic-proxy/index.js",
101
116
  "types": "./dist/magic-proxy/index.d.ts",
@@ -107,10 +122,10 @@
107
122
  "CHANGELOG.md"
108
123
  ],
109
124
  "dependencies": {
125
+ "@scalar/helpers": "0.13.0",
110
126
  "pathe": "^2.0.3",
111
127
  "undici": "7.24.4",
112
- "yaml": "^2.9.0",
113
- "@scalar/helpers": "0.12.0"
128
+ "yaml": "^2.9.0"
114
129
  },
115
130
  "devDependencies": {
116
131
  "fastify": "^5.11.2",