@speclynx/apidom-reference 4.13.0 → 4.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.
Files changed (29) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +99 -3
  3. package/dist/apidom-reference.browser.js +2076 -216
  4. package/dist/apidom-reference.browser.min.js +1 -1
  5. package/package.json +33 -28
  6. package/src/bundle/strategies/arazzo-1/index.cjs +94 -0
  7. package/src/bundle/strategies/arazzo-1/index.mjs +81 -0
  8. package/src/bundle/strategies/arazzo-1/visitor.cjs +417 -0
  9. package/src/bundle/strategies/arazzo-1/visitor.mjs +409 -0
  10. package/src/bundle/strategies/openapi-2/visitor.cjs +8 -16
  11. package/src/bundle/strategies/openapi-2/visitor.mjs +8 -16
  12. package/src/bundle/strategies/openapi-3-0/visitor.cjs +10 -18
  13. package/src/bundle/strategies/openapi-3-0/visitor.mjs +10 -18
  14. package/src/bundle/strategies/openapi-3-1/index.cjs +59 -2
  15. package/src/bundle/strategies/openapi-3-1/index.mjs +54 -2
  16. package/src/bundle/strategies/openapi-3-1/visitor.cjs +811 -0
  17. package/src/bundle/strategies/openapi-3-1/visitor.mjs +803 -0
  18. package/src/configuration/saturated.cjs +10 -9
  19. package/src/configuration/saturated.mjs +2 -1
  20. package/src/util/url.cjs +26 -4
  21. package/src/util/url.mjs +23 -2
  22. package/types/apidom-reference.d.ts +10 -0
  23. package/types/bundle/strategies/arazzo-1/index.d.ts +31 -0
  24. package/types/bundle/strategies/arazzo-1/visitor.d.ts +90 -0
  25. package/types/bundle/strategies/openapi-2/visitor.d.ts +2 -2
  26. package/types/bundle/strategies/openapi-3-0/visitor.d.ts +2 -2
  27. package/types/bundle/strategies/openapi-3-1/index.d.ts +6 -1
  28. package/types/bundle/strategies/openapi-3-1/visitor.d.ts +141 -0
  29. package/types/util/url.d.ts +8 -0
@@ -0,0 +1,803 @@
1
+ import { propEq, none } from 'ramda';
2
+ import { isElement, isObjectElement, isStringElement, ObjectElement, AnnotationElement, cloneDeep, cloneShallow } from '@speclynx/apidom-datamodel';
3
+ import { toValue, toYAML, fixedFields } from '@speclynx/apidom-core';
4
+ import { ApiDOMStructuredError } from '@speclynx/apidom-error';
5
+ import { traverseAsync } from '@speclynx/apidom-traverse';
6
+ import { evaluate as jsonPointerEvaluate, escape, parse as parseJSONPointer, URIFragmentIdentifier } from '@speclynx/apidom-json-pointer';
7
+ import { ComponentsElement, ComponentsSchemasElement, ComponentsResponsesElement, ComponentsParametersElement, ComponentsExamplesElement, ComponentsRequestBodiesElement, ComponentsHeadersElement, ComponentsSecuritySchemesElement, ComponentsLinksElement, ComponentsCallbacksElement, ComponentsPathItemsElement, isReferenceElement, isReferenceLikeElement, isPathItemElement, refract, refractReference, refractPathItem } from '@speclynx/apidom-ns-openapi-3-1';
8
+ import BundleError from "../../../errors/BundleError.mjs";
9
+ import UnresolvableReferenceError from "../../../errors/UnresolvableReferenceError.mjs";
10
+ import MaximumBundleDepthError from "../../../errors/MaximumBundleDepthError.mjs";
11
+ import MaximumResolveDepthError from "../../../errors/MaximumResolveDepthError.mjs";
12
+ import EvaluationJsonSchemaUriError from "../../../errors/EvaluationJsonSchemaUriError.mjs";
13
+ import * as url from "../../../util/url.mjs";
14
+ import parse from "../../../parse/index.mjs";
15
+ import Reference from "../../../Reference.mjs";
16
+ import File from "../../../File.mjs";
17
+ import { isAnchor, uriToAnchor, evaluate as $anchorEvaluate } from "../../../dereference/strategies/openapi-3-1/selectors/$anchor.mjs";
18
+ import { evaluate as uriEvaluate } from "../../../dereference/strategies/openapi-3-1/selectors/uri.mjs";
19
+ import { resolveSchema$refField, resolveSchema$idField, maybeRefractToSchemaElement } from "../../../dereference/strategies/openapi-3-1/util.mjs";
20
+ import { toPascalCase, sanitizeComponentName, uniqueName as resolveUniqueName } from "../../util.mjs";
21
+ /**
22
+ * Maps the `referenced-element` meta value (set during refraction) to the
23
+ * Components Object field where the referenced element should be hoisted.
24
+ * Field names are sourced from the Components Object fixed fields so they
25
+ * cannot drift from the namespace definition.
26
+ */
27
+ const cf = fixedFields(ComponentsElement, {
28
+ indexed: true
29
+ });
30
+ const componentFieldByReferencedElement = {
31
+ schema: cf.schemas.name,
32
+ response: cf.responses.name,
33
+ parameter: cf.parameters.name,
34
+ example: cf.examples.name,
35
+ requestBody: cf.requestBodies.name,
36
+ header: cf.headers.name,
37
+ securityScheme: cf.securitySchemes.name,
38
+ link: cf.links.name,
39
+ callback: cf.callbacks.name,
40
+ pathItem: cf.pathItems.name
41
+ };
42
+
43
+ /**
44
+ * Maps a Components Object field to the named-content element the namespace
45
+ * uses for it, so a bundle-created field matches a parsed/refracted one
46
+ * (e.g. carries the `components-schemas` class) rather than being a generic
47
+ * object.
48
+ */
49
+ const componentFieldElementByField = {
50
+ [cf.schemas.name]: ComponentsSchemasElement,
51
+ [cf.responses.name]: ComponentsResponsesElement,
52
+ [cf.parameters.name]: ComponentsParametersElement,
53
+ [cf.examples.name]: ComponentsExamplesElement,
54
+ [cf.requestBodies.name]: ComponentsRequestBodiesElement,
55
+ [cf.headers.name]: ComponentsHeadersElement,
56
+ [cf.securitySchemes.name]: ComponentsSecuritySchemesElement,
57
+ [cf.links.name]: ComponentsLinksElement,
58
+ [cf.callbacks.name]: ComponentsCallbacksElement,
59
+ [cf.pathItems.name]: ComponentsPathItemsElement
60
+ };
61
+
62
+ /**
63
+ * @public
64
+ */
65
+
66
+ /**
67
+ * @public
68
+ */
69
+ class OpenAPI3_1BundleVisitor {
70
+ reference;
71
+ options;
72
+
73
+ /**
74
+ * The base URI (without hash) of the entry document. A reference is
75
+ * considered internal when it resolves to this URI and external otherwise.
76
+ * Derived from the shared refSet's root reference.
77
+ */
78
+ get entryURI() {
79
+ return url.stripHash(this.reference.refSet?.rootRef?.uri ?? '');
80
+ }
81
+
82
+ /**
83
+ * The entry parse result. Derived from the shared refSet's root reference.
84
+ */
85
+ get entryParseResult() {
86
+ return this.reference.refSet.rootRef.value;
87
+ }
88
+
89
+ /**
90
+ * The entry document element. Hoisted external fragments are placed into its
91
+ * `components` object. The `canBundle` guard guarantees it is an
92
+ * OpenApi3_1Element.
93
+ */
94
+ get entryResult() {
95
+ return this.entryParseResult.result;
96
+ }
97
+
98
+ /**
99
+ * Shared across the entry document and every external document visitor.
100
+ * Guarantees each external fragment is hoisted exactly once and gives us a
101
+ * stable internal pointer to rewrite every referencing element to.
102
+ */
103
+ assignments;
104
+
105
+ /**
106
+ * Component names already reserved per Components Object field. Reserved
107
+ * before recursion so collision suffixing (`Pet`, `Pet-2`, ...) accounts for
108
+ * components that are still being bundled.
109
+ */
110
+ reservedNames;
111
+
112
+ /**
113
+ * Caches refracted fragments keyed by source element and target element type.
114
+ * The type is part of the key because the same source can be refracted into
115
+ * different semantic elements depending on the referencing context (e.g. a
116
+ * generic object referenced once as a Parameter and once as a Response).
117
+ */
118
+ refractCache;
119
+ constructor({
120
+ reference,
121
+ options,
122
+ assignments = new Map(),
123
+ reservedNames = new Map(),
124
+ refractCache = new WeakMap()
125
+ }) {
126
+ this.reference = reference;
127
+ this.options = options;
128
+ this.assignments = assignments;
129
+ this.reservedNames = reservedNames;
130
+ this.refractCache = refractCache;
131
+ }
132
+ getRefracted(source, type) {
133
+ return this.refractCache.get(source)?.get(type);
134
+ }
135
+ setRefracted(source, type, refracted) {
136
+ let byType = this.refractCache.get(source);
137
+ if (byType === undefined) {
138
+ byType = new Map();
139
+ this.refractCache.set(source, byType);
140
+ }
141
+ byType.set(type, refracted);
142
+ }
143
+ toBaseURI(uri) {
144
+ return url.resolve(this.reference.uri, url.sanitize(url.stripHash(uri)));
145
+ }
146
+ async toReference(uri) {
147
+ // detect maximum depth of resolution
148
+ if (this.reference.depth >= this.options.resolve.maxDepth) {
149
+ throw new MaximumResolveDepthError(`Maximum resolution depth of ${this.options.resolve.maxDepth} has been exceeded by file "${this.reference.uri}"`, {
150
+ maxDepth: this.options.resolve.maxDepth,
151
+ uri: this.reference.uri
152
+ });
153
+ }
154
+ const baseURI = this.toBaseURI(uri);
155
+ const {
156
+ refSet
157
+ } = this.reference;
158
+
159
+ // we've already processed this Reference in past
160
+ if (refSet.has(baseURI)) {
161
+ return refSet.find(propEq(baseURI, 'uri'));
162
+ }
163
+ const parseResult = await parse(url.unsanitize(baseURI), {
164
+ ...this.options,
165
+ parse: {
166
+ ...this.options.parse,
167
+ mediaType: 'text/plain'
168
+ }
169
+ });
170
+ const reference = new Reference({
171
+ uri: baseURI,
172
+ value: parseResult,
173
+ depth: this.reference.depth + 1
174
+ });
175
+ refSet.add(reference);
176
+ return reference;
177
+ }
178
+
179
+ /**
180
+ * Normalizes a self-reference (e.g. `./root.json#/components/schemas/Pet`)
181
+ * to a bare fragment (`#/components/schemas/Pet`) so the bundled document stays
182
+ * transferable. Bare fragments are returned unchanged.
183
+ */
184
+ normalizeSelfReference(ref) {
185
+ return ref.startsWith('#') ? ref : url.getHash(ref);
186
+ }
187
+
188
+ /**
189
+ * Handles an error according to the `bundle.continueOnError` option.
190
+ *
191
+ * For new errors: wraps in UnresolvableReferenceError with structured
192
+ * context. For errors already wrapped by a nested visitor: prepends the
193
+ * current hop to the trace.
194
+ *
195
+ * Inner/intermediate visitors always throw to let the trace accumulate. Only
196
+ * the entry document visitor respects continueOnError (callback/swallow/throw).
197
+ */
198
+ handleError(message, error, referencingElement, refFieldName, refFieldValue, visitorPath) {
199
+ // a collision reported as `error` is a deliberate hard stop with no
200
+ // resolution-failure analog, so pass it through unchanged. A maxDepth
201
+ // breach (MaximumBundleDepthError, which also extends BundleError) is a
202
+ // resolution failure and flows through below — wrapped and subject to
203
+ // continueOnError, like dereferencing.
204
+ if (error instanceof BundleError && !(error instanceof MaximumBundleDepthError)) {
205
+ throw error;
206
+ }
207
+ const {
208
+ continueOnError
209
+ } = this.options.bundle;
210
+ const isEntryDocument = url.stripHash(this.reference.refSet?.rootRef?.uri ?? '') === this.reference.uri;
211
+ const uri = this.reference.uri;
212
+ const type = referencingElement.element;
213
+ const codeFrame = toYAML(referencingElement);
214
+ const location = visitorPath.formatPath();
215
+ const hop = {
216
+ uri,
217
+ type,
218
+ refFieldName,
219
+ refFieldValue,
220
+ location,
221
+ codeFrame
222
+ };
223
+ let unresolvedError;
224
+ if (error instanceof UnresolvableReferenceError) {
225
+ // @ts-ignore
226
+ error.trace = [hop, ...error.trace];
227
+ unresolvedError = error;
228
+ } else {
229
+ unresolvedError = new UnresolvableReferenceError(message, {
230
+ cause: error,
231
+ type,
232
+ uri,
233
+ location,
234
+ codeFrame,
235
+ refFieldName,
236
+ refFieldValue,
237
+ trace: []
238
+ });
239
+ }
240
+ if (!isEntryDocument || continueOnError === false) throw unresolvedError;
241
+ if (typeof continueOnError === 'function') continueOnError(unresolvedError);
242
+ }
243
+
244
+ /**
245
+ * Lazily creates the Components Object (and the requested field within it)
246
+ * on the entry document, returning the field where a fragment is hoisted.
247
+ */
248
+ ensureComponentsField(field) {
249
+ const entryResult = this.entryResult;
250
+ let components = entryResult.components;
251
+ if (!isObjectElement(components)) {
252
+ components = new ComponentsElement();
253
+ entryResult.components = components;
254
+ }
255
+ let fieldElement = components.get(field);
256
+ if (!isObjectElement(fieldElement)) {
257
+ const FieldElement = componentFieldElementByField[field] ?? ObjectElement;
258
+ fieldElement = new FieldElement();
259
+ components.set(field, fieldElement);
260
+ }
261
+ return fieldElement;
262
+ }
263
+
264
+ /**
265
+ * Derives a component name from the referenced JSON Pointer's last token,
266
+ * falling back to the referenced file's basename.
267
+ */
268
+ basenameOf(jsonPointer, baseURI) {
269
+ const tokens = parseJSONPointer(jsonPointer).tree;
270
+ const lastToken = tokens.length > 0 ? tokens[tokens.length - 1] : '';
271
+ return lastToken || url.getBasename(baseURI) || 'Schema';
272
+ }
273
+
274
+ /**
275
+ * Computes the base component name (before collision suffixing) according to
276
+ * the configured naming strategy.
277
+ */
278
+ baseName(element, field, jsonPointer, baseURI) {
279
+ // strategy specific options take precedence over the top-level bundle options
280
+ const componentNamesStrategy = this.options.bundle.strategyOpts['openapi-3-1']?.componentNamesStrategy ?? this.options.bundle.componentNamesStrategy;
281
+ if (typeof componentNamesStrategy === 'function') {
282
+ const resolved = componentNamesStrategy({
283
+ element,
284
+ field,
285
+ jsonPointer,
286
+ baseURI
287
+ });
288
+ if (typeof resolved === 'string' && resolved !== '') {
289
+ return resolved;
290
+ }
291
+ return this.basenameOf(jsonPointer, baseURI);
292
+ }
293
+ if (componentNamesStrategy === 'title') {
294
+ const title = isObjectElement(element) ? toValue(element.get('title')) : undefined;
295
+ if (typeof title === 'string' && title.trim() !== '') {
296
+ const name = sanitizeComponentName(toPascalCase(title));
297
+ if (name !== '') return name;
298
+ }
299
+ // fall back to basename when no usable title is present
300
+ }
301
+ return this.basenameOf(jsonPointer, baseURI);
302
+ }
303
+
304
+ /**
305
+ * Computes a collision-free component name within the target field. A name is
306
+ * taken if it's already placed in components or reserved by a component that
307
+ * is still being bundled (reserved before recursion).
308
+ */
309
+ uniqueName(candidate, field) {
310
+ const fieldElement = this.ensureComponentsField(field);
311
+ const reserved = this.reservedNames.get(field) ?? new Set();
312
+ return resolveUniqueName(candidate, name => fieldElement.hasKey(name) || reserved.has(name));
313
+ }
314
+
315
+ /**
316
+ * Reports a collision-forced rename according to the configured severity.
317
+ * A rename means two distinct targets resolved to the same name; each keeps
318
+ * its own component (and origin), so report the rename per severity.
319
+ */
320
+ reportNameCollision(preferredName, componentName, field) {
321
+ // strategy specific options take precedence over the top-level bundle options
322
+ const onComponentNameCollision = this.options.bundle.strategyOpts['openapi-3-1']?.onComponentNameCollision ?? this.options.bundle.onComponentNameCollision;
323
+ if (componentName === preferredName || onComponentNameCollision === 'off') {
324
+ return;
325
+ }
326
+ const message = `Component "${preferredName}" in components/${field} is referenced with the same name but different content. Renamed to "${componentName}".`;
327
+ if (onComponentNameCollision === 'error') {
328
+ throw new BundleError(message);
329
+ }
330
+ const annotation = new AnnotationElement(message);
331
+ annotation.classes.push('warning');
332
+ annotation.code = 'bundle-component-name-collision';
333
+ // append (never prepend) so the result element stays ahead of annotations
334
+ this.entryParseResult.content.push(annotation);
335
+ }
336
+ async ReferenceElement(path) {
337
+ const referencingElement = path.node;
338
+ const $ref = toValue(referencingElement.$ref);
339
+ const retrievalURI = this.toBaseURI($ref);
340
+ const isInternalReference = this.entryURI === retrievalURI;
341
+ const isExternalReference = !isInternalReference;
342
+ if (isInternalReference) {
343
+ // normalize self-references to a bare fragment so the bundled
344
+ // document stays transferable; bare fragments are left untouched
345
+ referencingElement.set('$ref', this.normalizeSelfReference($ref));
346
+ path.skip();
347
+ return;
348
+ }
349
+
350
+ // honor the resolve.external switch
351
+ if (!this.options.resolve.external && isExternalReference) {
352
+ path.skip();
353
+ return;
354
+ }
355
+ const $refBaseURI = url.resolve(retrievalURI, $ref);
356
+ const jsonPointer = URIFragmentIdentifier.fromURIReference($refBaseURI);
357
+
358
+ // the target bucket depends on the referencing context, so it's part of the
359
+ // identity: the same external target referenced as e.g. both a parameter and
360
+ // a response must be hoisted into both components fields. The refractor sets
361
+ // `referenced-element` for every Reference Object position; the `schemas`
362
+ // fallback is a defensive default for the (unexpected) untagged case.
363
+ const referencedElementType = referencingElement.meta.get('referenced-element');
364
+ const field = componentFieldByReferencedElement[referencedElementType] ?? componentFieldByReferencedElement.schema;
365
+ const canonicalKey = `${field}\t${retrievalURI}#${jsonPointer}`;
366
+
367
+ // already hoisted (or being hoisted) — just rewrite the pointer.
368
+ //
369
+ // this is also how circular references terminate: the assignment is
370
+ // reserved BEFORE recursing into the fragment (see below), so a reference
371
+ // that cycles back (directly or indirectly) finds its pointer already
372
+ // reserved here and is rewritten without recursing. Cycles are deliberately
373
+ // preserved as internal `$ref`s pointing at each other — that is valid,
374
+ // serializable output; resolving the cycle (if desired) is dereferencing's
375
+ // job, not bundling's.
376
+ if (this.assignments.has(canonicalKey)) {
377
+ referencingElement.set('$ref', this.assignments.get(canonicalKey));
378
+ path.skip();
379
+ return;
380
+ }
381
+ try {
382
+ // detect maximum depth of bundling
383
+ if (this.reference.depth >= this.options.bundle.maxDepth) {
384
+ throw new MaximumBundleDepthError(`Maximum bundle depth of "${this.options.bundle.maxDepth}" has been exceeded in file "${this.reference.uri}"`, {
385
+ maxDepth: this.options.bundle.maxDepth,
386
+ uri: this.reference.uri
387
+ });
388
+ }
389
+ const reference = await this.toReference($ref);
390
+
391
+ // possibly non-semantic fragment
392
+ let referencedElement = jsonPointerEvaluate(reference.value.result, jsonPointer);
393
+
394
+ // apply semantics to the fragment so nested references become Reference Objects
395
+ if (referencedElement.element !== referencedElementType && !isReferenceElement(referencedElement)) {
396
+ const cached = this.getRefracted(referencedElement, referencedElementType);
397
+ if (cached !== undefined) {
398
+ referencedElement = cached;
399
+ } else if (isReferenceLikeElement(referencedElement)) {
400
+ const sourceElement = referencedElement;
401
+ referencedElement = refractReference(referencedElement);
402
+ referencedElement.meta.set('referenced-element', referencedElementType);
403
+ this.setRefracted(sourceElement, referencedElementType, referencedElement);
404
+ } else {
405
+ const sourceElement = referencedElement;
406
+ referencedElement = refract(referencedElement, {
407
+ element: referencedElementType
408
+ });
409
+ this.setRefracted(sourceElement, referencedElementType, referencedElement);
410
+ }
411
+ }
412
+ const preferredName = this.baseName(referencedElement, field, jsonPointer, retrievalURI);
413
+ const componentName = this.uniqueName(preferredName, field);
414
+ const internalPointer = `#/components/${field}/${escape(componentName)}`;
415
+ this.reportNameCollision(preferredName, componentName, field);
416
+
417
+ // reserve the assignment and component name before recursing so cyclic
418
+ // references terminate
419
+ this.assignments.set(canonicalKey, internalPointer);
420
+ if (!this.reservedNames.has(field)) this.reservedNames.set(field, new Set());
421
+ this.reservedNames.get(field).add(componentName);
422
+
423
+ // own a copy of the fragment and bundle its own external references
424
+ const hoistedElement = cloneDeep(referencedElement);
425
+ const visitor = new OpenAPI3_1BundleVisitor({
426
+ reference,
427
+ options: this.options,
428
+ assignments: this.assignments,
429
+ reservedNames: this.reservedNames,
430
+ refractCache: this.refractCache
431
+ });
432
+ const bundledElement = await traverseAsync(hoistedElement, visitor, {
433
+ mutable: true
434
+ });
435
+
436
+ // annotate the hoisted fragment with info about its origin
437
+ if (isElement(bundledElement)) {
438
+ bundledElement.meta.set('ref-origin', reference.uri);
439
+ }
440
+
441
+ // place the bundled fragment into the entry document's components
442
+ this.ensureComponentsField(field).set(componentName, bundledElement);
443
+
444
+ // rewrite the referencing element to point at the hoisted fragment
445
+ referencingElement.set('$ref', internalPointer);
446
+ path.skip();
447
+ } catch (error) {
448
+ this.handleError(`Error while bundling Reference Object. Cannot resolve $ref "${$ref}": ${error.message}`, error, referencingElement, '$ref', $ref, path);
449
+ path.skip();
450
+ }
451
+ }
452
+
453
+ /**
454
+ * Unlike OpenAPI 3.0 (which has no `components.pathItems` field and therefore
455
+ * inlines external Path Items in place), OpenAPI 3.1 hoists an external Path
456
+ * Item Object into `components.pathItems` and rewrites the referencing `$ref`
457
+ * to an internal pointer — mirroring how Reference Objects are bundled.
458
+ * Sibling fields next to the `$ref` (e.g. `summary`, `description`) are kept
459
+ * on the referencing element.
460
+ */
461
+ async PathItemElement(path) {
462
+ const pathItemElement = path.node;
463
+
464
+ // ignore Path Item Objects without a $ref field
465
+ if (!isStringElement(pathItemElement.$ref)) {
466
+ return;
467
+ }
468
+ const $ref = toValue(pathItemElement.$ref);
469
+ const retrievalURI = this.toBaseURI($ref);
470
+ const isInternalReference = this.entryURI === retrievalURI;
471
+ const isExternalReference = !isInternalReference;
472
+ if (isInternalReference) {
473
+ // normalize self-references to a bare fragment so the bundled
474
+ // document stays transferable; bare fragments are left untouched
475
+ pathItemElement.set('$ref', this.normalizeSelfReference($ref));
476
+ path.skip();
477
+ return;
478
+ }
479
+
480
+ // honor the resolve.external switch
481
+ if (!this.options.resolve.external && isExternalReference) {
482
+ path.skip();
483
+ return;
484
+ }
485
+ const $refBaseURI = url.resolve(retrievalURI, $ref);
486
+ const jsonPointer = URIFragmentIdentifier.fromURIReference($refBaseURI);
487
+ const field = cf.pathItems.name;
488
+ const canonicalKey = `${field}\t${retrievalURI}#${jsonPointer}`;
489
+
490
+ // already hoisted (or being hoisted) — just rewrite the pointer. The
491
+ // assignment is reserved BEFORE recursing, so a circular Path Item chain
492
+ // terminates here with an internal pointer instead of recursing forever.
493
+ if (this.assignments.has(canonicalKey)) {
494
+ pathItemElement.set('$ref', this.assignments.get(canonicalKey));
495
+ path.skip();
496
+ return;
497
+ }
498
+ try {
499
+ // detect maximum depth of bundling
500
+ if (this.reference.depth >= this.options.bundle.maxDepth) {
501
+ throw new MaximumBundleDepthError(`Maximum bundle depth of "${this.options.bundle.maxDepth}" has been exceeded in file "${this.reference.uri}"`, {
502
+ maxDepth: this.options.bundle.maxDepth,
503
+ uri: this.reference.uri
504
+ });
505
+ }
506
+ const reference = await this.toReference($ref);
507
+ let referencedElement = jsonPointerEvaluate(reference.value.result, jsonPointer);
508
+
509
+ // apply Path Item semantics to the referenced fragment
510
+ if (!isPathItemElement(referencedElement)) {
511
+ const cached = this.getRefracted(referencedElement, 'pathItem');
512
+ if (cached !== undefined) {
513
+ referencedElement = cached;
514
+ } else {
515
+ const sourceElement = referencedElement;
516
+ referencedElement = refractPathItem(referencedElement);
517
+ this.setRefracted(sourceElement, 'pathItem', referencedElement);
518
+ }
519
+ }
520
+ const preferredName = this.baseName(referencedElement, field, jsonPointer, retrievalURI);
521
+ const componentName = this.uniqueName(preferredName, field);
522
+ const internalPointer = `#/components/${field}/${escape(componentName)}`;
523
+ this.reportNameCollision(preferredName, componentName, field);
524
+
525
+ // reserve the assignment and component name before recursing so cyclic
526
+ // references terminate
527
+ this.assignments.set(canonicalKey, internalPointer);
528
+ if (!this.reservedNames.has(field)) this.reservedNames.set(field, new Set());
529
+ this.reservedNames.get(field).add(componentName);
530
+
531
+ // own a copy of the fragment and bundle its own external references
532
+ const hoistedElement = cloneDeep(referencedElement);
533
+ const visitor = new OpenAPI3_1BundleVisitor({
534
+ reference,
535
+ options: this.options,
536
+ assignments: this.assignments,
537
+ reservedNames: this.reservedNames,
538
+ refractCache: this.refractCache
539
+ });
540
+ const bundledElement = await traverseAsync(hoistedElement, visitor, {
541
+ mutable: true
542
+ });
543
+
544
+ // a `$ref` surviving on the hoisted Path Item means it pointed at another
545
+ // Path Item (a chain or cycle); it has already been rewritten to an
546
+ // internal `#/components/pathItems/...` pointer by the child traversal
547
+ bundledElement.meta.set('ref-origin', reference.uri);
548
+
549
+ // place the bundled Path Item into the entry document's components
550
+ this.ensureComponentsField(field).set(componentName, bundledElement);
551
+
552
+ // rewrite the referencing Path Item to point at the hoisted one, keeping
553
+ // any sibling fields (e.g. summary, description) next to the $ref
554
+ pathItemElement.set('$ref', internalPointer);
555
+ path.skip();
556
+ } catch (error) {
557
+ this.handleError(`Error while bundling Path Item Object. Cannot resolve $ref "${$ref}": ${error.message}`, error, pathItemElement, '$ref', $ref, path);
558
+ path.skip();
559
+ }
560
+ }
561
+
562
+ /**
563
+ * Schema Objects in OpenAPI 3.1 are a JSON Schema 2020-12 dialect, so they are
564
+ * bundled per the JSON Schema Compound Document rules: the external schema
565
+ * RESOURCE is embedded verbatim into `components.schemas` (carrying its `$id`),
566
+ * and the referencing `$ref` is left UNCHANGED — it keeps resolving against the
567
+ * embedded resource's `$id`. This mirrors the canonical hyperjump
568
+ * `json-schema-bundle` behavior (the whole resource is embedded once, keyed by
569
+ * its `$id`; references are not rewritten).
570
+ */
571
+ async SchemaElement(path) {
572
+ const referencingElement = path.node;
573
+
574
+ // skip current referencing schema as $ref keyword was not defined
575
+ if (!isStringElement(referencingElement.$ref)) {
576
+ return;
577
+ }
578
+ const $ref = toValue(referencingElement.$ref);
579
+ try {
580
+ // compute baseURI using rules around $id and $ref keywords. The current
581
+ // document's URI is the retrieval URI — derive it directly (not via
582
+ // toReference) so an internal $ref needs no resolution and never trips the
583
+ // resolve.maxDepth guard, matching how ReferenceElement returns early.
584
+ const retrievalURI = this.reference.uri;
585
+ const $refBaseURI = resolveSchema$refField(retrievalURI, referencingElement);
586
+ const $refBaseURIStrippedHash = url.stripHash($refBaseURI);
587
+ const file = new File({
588
+ uri: $refBaseURIStrippedHash
589
+ });
590
+ const isUnknownURI = none(r => r.canRead(file), this.options.resolve.resolvers);
591
+ const isURL = !isUnknownURI;
592
+ const isInternalReference = url.stripHash(this.reference.uri) === $refBaseURIStrippedHash;
593
+ const isExternalReference = !isInternalReference;
594
+
595
+ // internal Schema Object $refs resolve against the in-document $id graph;
596
+ // leaving them untouched keeps that resolution valid. Unlike Reference
597
+ // Objects, a JSON Schema $ref is NOT normalized to a bare fragment.
598
+ if (isInternalReference) {
599
+ return;
600
+ }
601
+
602
+ // honor the resolve.external switch
603
+ if (!this.options.resolve.external && isExternalReference) {
604
+ return;
605
+ }
606
+
607
+ // detect maximum depth of bundling before fetching the external resource,
608
+ // matching ReferenceElement/PathItemElement (which check before toReference)
609
+ if (this.reference.depth >= this.options.bundle.maxDepth) {
610
+ throw new MaximumBundleDepthError(`Maximum bundle depth of "${this.options.bundle.maxDepth}" has been exceeded in file "${this.reference.uri}"`, {
611
+ maxDepth: this.options.bundle.maxDepth,
612
+ uri: this.reference.uri
613
+ });
614
+ }
615
+
616
+ // locate the external schema resource the $ref targets, reusing the
617
+ // dereference strategy's classification. First try to resolve the target
618
+ // as a canonical URI / URL against the current document's $id graph; if
619
+ // that fails, fall back to fetching the external document and resolving
620
+ // the fragment there as a $anchor or JSON Pointer.
621
+ let schemaReference = this.reference;
622
+ try {
623
+ const referenceAsSchema = maybeRefractToSchemaElement(schemaReference.value.result);
624
+ uriEvaluate($refBaseURI, referenceAsSchema);
625
+ } catch (error) {
626
+ if (isURL && error instanceof EvaluationJsonSchemaUriError) {
627
+ if (isAnchor(uriToAnchor($refBaseURI))) {
628
+ schemaReference = await this.toReference(url.unsanitize($refBaseURI));
629
+ const selector = uriToAnchor($refBaseURI);
630
+ const referenceAsSchema = maybeRefractToSchemaElement(schemaReference.value.result);
631
+ $anchorEvaluate(selector, referenceAsSchema);
632
+ } else {
633
+ schemaReference = await this.toReference(url.unsanitize($refBaseURI));
634
+ const selector = URIFragmentIdentifier.fromURIReference($refBaseURI);
635
+ const referenceAsSchema = maybeRefractToSchemaElement(schemaReference.value.result);
636
+ jsonPointerEvaluate(referenceAsSchema, selector);
637
+ }
638
+ } else {
639
+ throw error;
640
+ }
641
+ }
642
+
643
+ // a $ref that resolves (by $id/$anchor/pointer) within the current document
644
+ // is internal even when its URI form (e.g. a URN or absolute $id) is not a
645
+ // bare fragment. Leave it untouched — it resolves against the in-document
646
+ // $id graph, and embedding the current document into itself would be wrong.
647
+ if (url.stripHash(schemaReference.uri) === url.stripHash(this.reference.uri)) {
648
+ return;
649
+ }
650
+
651
+ // the resource to embed is the WHOLE external document (its root schema),
652
+ // identified by its $id. A $ref into a fragment of the document embeds the
653
+ // entire document resource once; the fragment keeps resolving against the
654
+ // embedded $id.
655
+ const resourceRoot = maybeRefractToSchemaElement(schemaReference.value.result);
656
+ const resourceBaseURI = resolveSchema$idField(url.stripHash(schemaReference.uri), resourceRoot) ?? url.stripHash(schemaReference.uri);
657
+ const field = cf.schemas.name;
658
+ const canonicalKey = `${field}\t${resourceBaseURI}`;
659
+
660
+ // resource already embedded (or being embedded) — leave the referencing
661
+ // $ref untouched and stop. This also terminates circular external schema
662
+ // references, since the assignment is reserved BEFORE recursion.
663
+ if (this.assignments.has(canonicalKey)) {
664
+ path.skip();
665
+ return;
666
+ }
667
+
668
+ // own a copy of the resource and ensure it carries a $id so the unchanged
669
+ // $ref still resolves against it once embedded
670
+ const embeddedElement = cloneDeep(resourceRoot);
671
+ if (!isStringElement(embeddedElement.$id)) {
672
+ embeddedElement.set('$id', resourceBaseURI);
673
+ }
674
+
675
+ // the embedded thing is the WHOLE resource (keyed by its $id), so its name
676
+ // is derived from the resource URI, not the $ref fragment — passing a root
677
+ // pointer makes baseName fall back to the resource basename
678
+ const preferredName = this.baseName(embeddedElement, field, '', resourceBaseURI);
679
+ const componentName = this.uniqueName(preferredName, field);
680
+ this.reportNameCollision(preferredName, componentName, field);
681
+
682
+ // reserve the assignment and component name before recursing so circular
683
+ // external schema references terminate
684
+ this.assignments.set(canonicalKey, `#/components/${field}/${escape(componentName)}`);
685
+ if (!this.reservedNames.has(field)) this.reservedNames.set(field, new Set());
686
+ this.reservedNames.get(field).add(componentName);
687
+
688
+ // bundle external references contained within the embedded resource;
689
+ // nested external resources land flat in the same components.schemas
690
+ const visitor = new OpenAPI3_1BundleVisitor({
691
+ reference: schemaReference,
692
+ options: this.options,
693
+ assignments: this.assignments,
694
+ reservedNames: this.reservedNames,
695
+ refractCache: this.refractCache
696
+ });
697
+ const bundledElement = await traverseAsync(embeddedElement, visitor, {
698
+ mutable: true
699
+ });
700
+
701
+ // annotate the embedded resource with info about its origin
702
+ bundledElement.meta.set('ref-origin', schemaReference.uri);
703
+
704
+ // place the embedded resource into the entry document's components
705
+ this.ensureComponentsField(field).set(componentName, bundledElement);
706
+
707
+ // the referencing $ref is left UNCHANGED — it resolves against the
708
+ // embedded resource's $id
709
+ path.skip();
710
+ } catch (error) {
711
+ this.handleError(`Error while bundling Schema Object. Cannot resolve $ref "${$ref}": ${error.message}`, error, referencingElement, '$ref', $ref, path);
712
+ path.skip();
713
+ }
714
+ }
715
+
716
+ /**
717
+ * Example Object `externalValue` points at arbitrary external content that
718
+ * has no place in the Components Object. The referenced value is inlined into
719
+ * the Example's `value` field.
720
+ */
721
+ async ExampleElement(path) {
722
+ const exampleElement = path.node;
723
+
724
+ // ignore Example Objects without an externalValue field
725
+ if (!isStringElement(exampleElement.externalValue)) {
726
+ return;
727
+ }
728
+
729
+ // value and externalValue fields are mutually exclusive
730
+ if (exampleElement.hasKey('value') && isStringElement(exampleElement.externalValue)) {
731
+ throw new ApiDOMStructuredError('ExampleElement value and externalValue fields are mutually exclusive', {
732
+ value: toValue(exampleElement.value),
733
+ externalValue: toValue(exampleElement.externalValue)
734
+ });
735
+ }
736
+ const externalValue = toValue(exampleElement.externalValue);
737
+ const retrievalURI = this.toBaseURI(externalValue);
738
+ const isInternalReference = this.entryURI === retrievalURI;
739
+ const isExternalReference = !isInternalReference;
740
+
741
+ // honor the resolve.external switch
742
+ if (!this.options.resolve.external && isExternalReference) {
743
+ return;
744
+ }
745
+ try {
746
+ const reference = await this.toReference(externalValue);
747
+ const valueElement = cloneShallow(reference.value.result);
748
+ valueElement.meta.set('ref-origin', reference.uri);
749
+ const exampleElementCopy = cloneShallow(exampleElement);
750
+ exampleElementCopy.value = valueElement;
751
+ exampleElementCopy.remove('externalValue');
752
+ path.replaceWith(exampleElementCopy);
753
+ } catch (error) {
754
+ this.handleError(`Error while bundling Example Object. Cannot resolve externalValue "${externalValue}": ${error.message}`, error, exampleElement, 'externalValue', externalValue, path);
755
+ }
756
+ }
757
+
758
+ /**
759
+ * Link Object `operationRef` points at an Operation Object, which has no
760
+ * Components Object field in OpenAPI 3.1 and therefore cannot be hoisted or
761
+ * inlined. To keep the link resolvable from the bundled document, an external
762
+ * `operationRef` is rewritten to its absolute URI. Internal `operationRef`
763
+ * values and `operationId` links are left untouched.
764
+ */
765
+ async LinkElement(path) {
766
+ const linkElement = path.node;
767
+
768
+ // operationRef and operationId fields are mutually exclusive
769
+ if (isStringElement(linkElement.operationRef) && isStringElement(linkElement.operationId)) {
770
+ throw new ApiDOMStructuredError('LinkElement operationRef and operationId fields are mutually exclusive', {
771
+ operationRef: toValue(linkElement.operationRef),
772
+ operationId: toValue(linkElement.operationId)
773
+ });
774
+ }
775
+
776
+ // only external operationRef links need adjusting
777
+ if (!isStringElement(linkElement.operationRef)) {
778
+ return;
779
+ }
780
+ const operationRef = toValue(linkElement.operationRef);
781
+ const retrievalURI = this.toBaseURI(operationRef);
782
+ const isInternalReference = this.entryURI === retrievalURI;
783
+ if (isInternalReference) {
784
+ // normalize a self-reference operationRef to a bare fragment so the bundled
785
+ // document stays transferable; bare fragments are left untouched
786
+ linkElement.set('operationRef', this.normalizeSelfReference(operationRef));
787
+ return;
788
+ }
789
+
790
+ // honor the resolve.external switch
791
+ if (!this.options.resolve.external) {
792
+ return;
793
+ }
794
+
795
+ // rewrite to an absolute URI so the link keeps resolving once the document
796
+ // is treated as self-contained
797
+ const absoluteURI = url.resolve(this.reference.uri, operationRef);
798
+ if (absoluteURI !== operationRef) {
799
+ linkElement.set('operationRef', absoluteURI);
800
+ }
801
+ }
802
+ }
803
+ export default OpenAPI3_1BundleVisitor;