@speclynx/apidom-reference 6.1.0 → 6.1.1

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 (30) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/dist/apidom-reference.browser.js +424 -317
  3. package/dist/apidom-reference.browser.min.js +1 -1
  4. package/package.json +29 -29
  5. package/src/bundle/strategies/asyncapi-2/visitor.cjs +5 -0
  6. package/src/bundle/strategies/asyncapi-2/visitor.mjs +5 -0
  7. package/src/bundle/strategies/openapi-2/visitor.cjs +4 -0
  8. package/src/bundle/strategies/openapi-2/visitor.mjs +4 -0
  9. package/src/bundle/strategies/openapi-3-0/visitor.cjs +97 -82
  10. package/src/bundle/strategies/openapi-3-0/visitor.mjs +97 -82
  11. package/src/bundle/strategies/openapi-3-1/visitor.cjs +5 -0
  12. package/src/bundle/strategies/openapi-3-1/visitor.mjs +5 -0
  13. package/src/dereference/strategies/asyncapi-2/visitor.cjs +5 -0
  14. package/src/dereference/strategies/asyncapi-2/visitor.mjs +5 -0
  15. package/src/dereference/strategies/openapi-2/visitor.cjs +10 -0
  16. package/src/dereference/strategies/openapi-2/visitor.mjs +10 -0
  17. package/src/dereference/strategies/openapi-3-0/visitor.cjs +5 -0
  18. package/src/dereference/strategies/openapi-3-0/visitor.mjs +5 -0
  19. package/src/dereference/strategies/openapi-3-1/visitor.cjs +5 -0
  20. package/src/dereference/strategies/openapi-3-1/visitor.mjs +5 -0
  21. package/src/dereference/strategies/overlay-1/extends.cjs +52 -28
  22. package/src/dereference/strategies/overlay-1/extends.mjs +52 -28
  23. package/src/dereference/strategies/overlay-1/index.cjs +22 -10
  24. package/src/dereference/strategies/overlay-1/index.mjs +23 -11
  25. package/src/dereference/strategies/overlay-1/visitor.cjs +27 -2
  26. package/src/dereference/strategies/overlay-1/visitor.mjs +27 -3
  27. package/src/parse/parsers/overlay-json-1/extends.cjs +33 -13
  28. package/src/parse/parsers/overlay-json-1/extends.mjs +33 -14
  29. package/types/bundle/strategies/openapi-3-0/visitor.d.ts +8 -3
  30. package/types/dereference/strategies/overlay-1/visitor.d.ts +1 -1
@@ -194,6 +194,11 @@ class OpenAPI3_0DereferenceVisitor {
194
194
  async ReferenceElement(path) {
195
195
  const referencingElement = path.node;
196
196
 
197
+ // ignore Reference Object without a string $ref, e.g. an empty YAML value
198
+ if (!isStringElement(referencingElement.$ref)) {
199
+ return;
200
+ }
201
+
197
202
  // skip current referencing element as it's already been access
198
203
  if (this.indirections.includes(referencingElement)) {
199
204
  path.skip();
@@ -228,6 +228,11 @@ class OpenAPI3_1DereferenceVisitor {
228
228
  async ReferenceElement(path) {
229
229
  const referencingElement = path.node;
230
230
 
231
+ // ignore Reference Object without a string $ref, e.g. an empty YAML value
232
+ if (!(0, _apidomDatamodel.isStringElement)(referencingElement.$ref)) {
233
+ return;
234
+ }
235
+
231
236
  // skip current referencing element as it's already been accessed
232
237
  if (this.indirections.includes(referencingElement)) {
233
238
  path.skip();
@@ -219,6 +219,11 @@ class OpenAPI3_1DereferenceVisitor {
219
219
  async ReferenceElement(path) {
220
220
  const referencingElement = path.node;
221
221
 
222
+ // ignore Reference Object without a string $ref, e.g. an empty YAML value
223
+ if (!isStringElement(referencingElement.$ref)) {
224
+ return;
225
+ }
226
+
222
227
  // skip current referencing element as it's already been accessed
223
228
  if (this.indirections.includes(referencingElement)) {
224
229
  path.skip();
@@ -13,6 +13,8 @@ var _File = _interopRequireDefault(require("../../../File.cjs"));
13
13
  var url = _interopRequireWildcard(require("../../../util/url.cjs"));
14
14
  var _util = require("../../../options/util.cjs");
15
15
  var _index = _interopRequireWildcard(require("../../index.cjs"));
16
+ const STRATEGY_NAME = 'overlay-1';
17
+
16
18
  /**
17
19
  * Dereferences the `extends` target document from an Overlay document's ParseResult.
18
20
  *
@@ -54,39 +56,61 @@ async function dereferenceExtends(parseResult, parseResultRetrievalURI, options)
54
56
  const extendsDereferenceResult = new _apidomDatamodel.ParseResultElement();
55
57
  extendsDereferenceResult.classes.push('extends');
56
58
  extendsDereferenceResult.setMetaProperty('retrievalURI', retrievalURI);
57
- try {
58
- let extendsDereferenced;
59
59
 
60
- // check if extends was already parsed (e.g., during parse phase with extends: true)
61
- const existingParseResult = extendsElement.meta.get('parseResult');
62
- if ((0, _apidomDatamodel.isParseResultElement)(existingParseResult)) {
63
- // use existing parsed result - just dereference it (no re-fetch/re-parse)
64
- extendsDereferenced = await (0, _index.dereferenceApiDOM)(existingParseResult, (0, _util.merge)(options, {
65
- parse: {
66
- mediaType: 'text/plain' // allow dereference strategy detection via ApiDOM inspection
67
- },
68
- resolve: {
69
- baseURI: retrievalURI
70
- }
71
- }));
72
- } else {
73
- // no existing parse result - fetch, parse, and dereference
74
- extendsDereferenced = await (0, _index.default)(retrievalURI, (0, _util.merge)(options, {
75
- parse: {
76
- mediaType: 'text/plain' // allow parser plugin detection
60
+ // URIs of the Overlays currently being dereferenced, from the root down to the current one
61
+ const ancestors = [...(options?.dereference?.strategyOpts?.[STRATEGY_NAME]?.extendsAncestors ?? []), file.uri];
62
+ const nestedOptions = {
63
+ dereference: {
64
+ strategyOpts: {
65
+ [STRATEGY_NAME]: {
66
+ extendsAncestors: ancestors
77
67
  }
78
- }));
68
+ }
79
69
  }
70
+ };
80
71
 
81
- // merge dereferenced result into our wrapper
82
- for (const item of extendsDereferenced) {
83
- extendsDereferenceResult.push(item);
84
- }
85
- } catch (error) {
86
- const message = error instanceof Error ? error.message : String(error);
87
- const annotation = new _apidomDatamodel.AnnotationElement(`Error dereferencing extends target "${retrievalURI}": ${message}`);
88
- annotation.classes.push('error');
72
+ // skip if the target is still being dereferenced higher up the chain (cycle detection)
73
+ if (ancestors.includes(retrievalURI)) {
74
+ const annotation = new _apidomDatamodel.AnnotationElement(`Extends target "${retrievalURI}" has already been visited. Skipping to prevent cycle`);
75
+ annotation.classes.push('warning');
89
76
  extendsDereferenceResult.push(annotation);
77
+ } else {
78
+ try {
79
+ let extendsDereferenced;
80
+
81
+ // check if extends was already parsed (e.g., during parse phase with extends: true)
82
+ const existingParseResult = extendsElement.meta.get('parseResult');
83
+ if ((0, _apidomDatamodel.isParseResultElement)(existingParseResult)) {
84
+ // use existing parsed result - just dereference it (no re-fetch/re-parse)
85
+ extendsDereferenced = await (0, _index.dereferenceApiDOM)(existingParseResult, (0, _util.merge)(options, {
86
+ ...nestedOptions,
87
+ parse: {
88
+ mediaType: 'text/plain' // allow dereference strategy detection via ApiDOM inspection
89
+ },
90
+ resolve: {
91
+ baseURI: retrievalURI
92
+ }
93
+ }));
94
+ } else {
95
+ // no existing parse result - fetch, parse, and dereference
96
+ extendsDereferenced = await (0, _index.default)(retrievalURI, (0, _util.merge)(options, {
97
+ ...nestedOptions,
98
+ parse: {
99
+ mediaType: 'text/plain' // allow parser plugin detection
100
+ }
101
+ }));
102
+ }
103
+
104
+ // merge dereferenced result into our wrapper
105
+ for (const item of extendsDereferenced) {
106
+ extendsDereferenceResult.push(item);
107
+ }
108
+ } catch (error) {
109
+ const message = error instanceof Error ? error.message : String(error);
110
+ const annotation = new _apidomDatamodel.AnnotationElement(`Error dereferencing extends target "${retrievalURI}": ${message}`);
111
+ annotation.classes.push('error');
112
+ extendsDereferenceResult.push(annotation);
113
+ }
90
114
  }
91
115
 
92
116
  // always attach result to extends element meta (even on failure - contains annotations)
@@ -5,6 +5,8 @@ import File from "../../../File.mjs";
5
5
  import * as url from "../../../util/url.mjs";
6
6
  import { merge as mergeOptions } from "../../../options/util.mjs";
7
7
  import dereference, { dereferenceApiDOM } from "../../index.mjs";
8
+ const STRATEGY_NAME = 'overlay-1';
9
+
8
10
  /**
9
11
  * Dereferences the `extends` target document from an Overlay document's ParseResult.
10
12
  *
@@ -46,39 +48,61 @@ export async function dereferenceExtends(parseResult, parseResultRetrievalURI, o
46
48
  const extendsDereferenceResult = new ParseResultElement();
47
49
  extendsDereferenceResult.classes.push('extends');
48
50
  extendsDereferenceResult.setMetaProperty('retrievalURI', retrievalURI);
49
- try {
50
- let extendsDereferenced;
51
51
 
52
- // check if extends was already parsed (e.g., during parse phase with extends: true)
53
- const existingParseResult = extendsElement.meta.get('parseResult');
54
- if (isParseResultElement(existingParseResult)) {
55
- // use existing parsed result - just dereference it (no re-fetch/re-parse)
56
- extendsDereferenced = await dereferenceApiDOM(existingParseResult, mergeOptions(options, {
57
- parse: {
58
- mediaType: 'text/plain' // allow dereference strategy detection via ApiDOM inspection
59
- },
60
- resolve: {
61
- baseURI: retrievalURI
62
- }
63
- }));
64
- } else {
65
- // no existing parse result - fetch, parse, and dereference
66
- extendsDereferenced = await dereference(retrievalURI, mergeOptions(options, {
67
- parse: {
68
- mediaType: 'text/plain' // allow parser plugin detection
52
+ // URIs of the Overlays currently being dereferenced, from the root down to the current one
53
+ const ancestors = [...(options?.dereference?.strategyOpts?.[STRATEGY_NAME]?.extendsAncestors ?? []), file.uri];
54
+ const nestedOptions = {
55
+ dereference: {
56
+ strategyOpts: {
57
+ [STRATEGY_NAME]: {
58
+ extendsAncestors: ancestors
69
59
  }
70
- }));
60
+ }
71
61
  }
62
+ };
72
63
 
73
- // merge dereferenced result into our wrapper
74
- for (const item of extendsDereferenced) {
75
- extendsDereferenceResult.push(item);
76
- }
77
- } catch (error) {
78
- const message = error instanceof Error ? error.message : String(error);
79
- const annotation = new AnnotationElement(`Error dereferencing extends target "${retrievalURI}": ${message}`);
80
- annotation.classes.push('error');
64
+ // skip if the target is still being dereferenced higher up the chain (cycle detection)
65
+ if (ancestors.includes(retrievalURI)) {
66
+ const annotation = new AnnotationElement(`Extends target "${retrievalURI}" has already been visited. Skipping to prevent cycle`);
67
+ annotation.classes.push('warning');
81
68
  extendsDereferenceResult.push(annotation);
69
+ } else {
70
+ try {
71
+ let extendsDereferenced;
72
+
73
+ // check if extends was already parsed (e.g., during parse phase with extends: true)
74
+ const existingParseResult = extendsElement.meta.get('parseResult');
75
+ if (isParseResultElement(existingParseResult)) {
76
+ // use existing parsed result - just dereference it (no re-fetch/re-parse)
77
+ extendsDereferenced = await dereferenceApiDOM(existingParseResult, mergeOptions(options, {
78
+ ...nestedOptions,
79
+ parse: {
80
+ mediaType: 'text/plain' // allow dereference strategy detection via ApiDOM inspection
81
+ },
82
+ resolve: {
83
+ baseURI: retrievalURI
84
+ }
85
+ }));
86
+ } else {
87
+ // no existing parse result - fetch, parse, and dereference
88
+ extendsDereferenced = await dereference(retrievalURI, mergeOptions(options, {
89
+ ...nestedOptions,
90
+ parse: {
91
+ mediaType: 'text/plain' // allow parser plugin detection
92
+ }
93
+ }));
94
+ }
95
+
96
+ // merge dereferenced result into our wrapper
97
+ for (const item of extendsDereferenced) {
98
+ extendsDereferenceResult.push(item);
99
+ }
100
+ } catch (error) {
101
+ const message = error instanceof Error ? error.message : String(error);
102
+ const annotation = new AnnotationElement(`Error dereferencing extends target "${retrievalURI}": ${message}`);
103
+ annotation.classes.push('error');
104
+ extendsDereferenceResult.push(annotation);
105
+ }
82
106
  }
83
107
 
84
108
  // always attach result to extends element meta (even on failure - contains annotations)
@@ -74,25 +74,37 @@ class Overlay1DereferenceStrategy extends _DereferenceStrategy.default {
74
74
  })).forEach(ref => mutableRefSet.add(ref));
75
75
  reference = mutableRefSet.find(ref => ref.uri === file.uri);
76
76
  }
77
- const dereferencedElement = reference.value;
78
-
79
- /**
80
- * Reusable Action References only occur within `actions`, so the root element is never
81
- * replaced and traversing it keeps reported locations relative to the document.
82
- */
83
77
  const visitor = new _visitor.default({
84
78
  reference,
85
79
  options
86
80
  });
87
- await (0, _apidomTraverse.traverseAsync)(dereferencedElement.result, visitor, {
88
- mutable: true
89
- });
81
+ /**
82
+ * The reference for the dereferenced file is traversed rather than the refSet's root
83
+ * reference: dereferencing the extends target shares the refSet, whose root is then
84
+ * the extending Overlay. The reference may hold a lone element.
85
+ */
86
+ let dereferencedElement = reference.value;
87
+
88
+ /**
89
+ * A ParseResult may also carry the extends target parsed in the parse phase, whose
90
+ * references belong to that document, so only its result is traversed. Reusable Action
91
+ * References only occur within `actions`, so the result itself is never replaced.
92
+ */
93
+ if ((0, _apidomDatamodel.isParseResultElement)(dereferencedElement)) {
94
+ await (0, _apidomTraverse.traverseAsync)(dereferencedElement.result, visitor, {
95
+ mutable: true
96
+ });
97
+ } else {
98
+ dereferencedElement = await (0, _apidomTraverse.traverseAsync)(dereferencedElement, visitor, {
99
+ mutable: true
100
+ });
101
+ }
90
102
 
91
103
  /**
92
104
  * Dereference extends target if option is enabled.
93
105
  */
94
106
  const shouldDereferenceExtends = options?.dereference?.strategyOpts?.[this.name]?.extends ?? options?.dereference?.strategyOpts?.extends;
95
- if (shouldDereferenceExtends) {
107
+ if (shouldDereferenceExtends && (0, _apidomDatamodel.isParseResultElement)(dereferencedElement)) {
96
108
  await (0, _extends.dereferenceExtends)(dereferencedElement, reference.uri, options);
97
109
  }
98
110
 
@@ -1,4 +1,4 @@
1
- import { cloneDeep } from '@speclynx/apidom-datamodel';
1
+ import { cloneDeep, isParseResultElement } from '@speclynx/apidom-datamodel';
2
2
  import { isOverlay1Element, mediaTypes } from '@speclynx/apidom-ns-overlay-1';
3
3
  import { traverseAsync } from '@speclynx/apidom-traverse';
4
4
  import DereferenceStrategy from "../DereferenceStrategy.mjs";
@@ -54,25 +54,37 @@ class Overlay1DereferenceStrategy extends DereferenceStrategy {
54
54
  })).forEach(ref => mutableRefSet.add(ref));
55
55
  reference = mutableRefSet.find(ref => ref.uri === file.uri);
56
56
  }
57
- const dereferencedElement = reference.value;
58
-
59
- /**
60
- * Reusable Action References only occur within `actions`, so the root element is never
61
- * replaced and traversing it keeps reported locations relative to the document.
62
- */
63
57
  const visitor = new Overlay1DereferenceVisitor({
64
58
  reference,
65
59
  options
66
60
  });
67
- await traverseAsync(dereferencedElement.result, visitor, {
68
- mutable: true
69
- });
61
+ /**
62
+ * The reference for the dereferenced file is traversed rather than the refSet's root
63
+ * reference: dereferencing the extends target shares the refSet, whose root is then
64
+ * the extending Overlay. The reference may hold a lone element.
65
+ */
66
+ let dereferencedElement = reference.value;
67
+
68
+ /**
69
+ * A ParseResult may also carry the extends target parsed in the parse phase, whose
70
+ * references belong to that document, so only its result is traversed. Reusable Action
71
+ * References only occur within `actions`, so the result itself is never replaced.
72
+ */
73
+ if (isParseResultElement(dereferencedElement)) {
74
+ await traverseAsync(dereferencedElement.result, visitor, {
75
+ mutable: true
76
+ });
77
+ } else {
78
+ dereferencedElement = await traverseAsync(dereferencedElement, visitor, {
79
+ mutable: true
80
+ });
81
+ }
70
82
 
71
83
  /**
72
84
  * Dereference extends target if option is enabled.
73
85
  */
74
86
  const shouldDereferenceExtends = options?.dereference?.strategyOpts?.[this.name]?.extends ?? options?.dereference?.strategyOpts?.extends;
75
- if (shouldDereferenceExtends) {
87
+ if (shouldDereferenceExtends && isParseResultElement(dereferencedElement)) {
76
88
  await dereferenceExtends(dereferencedElement, reference.uri, options);
77
89
  }
78
90
 
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
 
3
+ var _interopRequireWildcard = require("@babel/runtime-corejs3/helpers/interopRequireWildcard").default;
3
4
  var _interopRequireDefault = require("@babel/runtime-corejs3/helpers/interopRequireDefault").default;
4
5
  Object.defineProperty(exports, "__esModule", {
5
6
  value: true
@@ -9,8 +10,10 @@ var _apidomDatamodel = require("@speclynx/apidom-datamodel");
9
10
  var _apidomCore = require("@speclynx/apidom-core");
10
11
  var _apidomError = require("@speclynx/apidom-error");
11
12
  var _apidomJsonPointer = require("@speclynx/apidom-json-pointer");
13
+ var _apidomTraverse = require("@speclynx/apidom-traverse");
12
14
  var _apidomNsOverlay = require("@speclynx/apidom-ns-overlay-1");
13
15
  var _UnresolvableReferenceError = _interopRequireDefault(require("../../../errors/UnresolvableReferenceError.cjs"));
16
+ var url = _interopRequireWildcard(require("../../../util/url.cjs"));
14
17
  /**
15
18
  * @public
16
19
  */
@@ -37,7 +40,14 @@ class Overlay1DereferenceVisitor {
37
40
  return;
38
41
  }
39
42
  const $ref = (0, _apidomCore.toValue)(referencingElement.$ref);
40
- const overlayElement = this.reference.value.result;
43
+ // the reference may hold a lone element; same-document references resolve against the
44
+ // document registered in the refSet under the reference's URI without fragment
45
+ const {
46
+ refSet
47
+ } = this.reference;
48
+ const documentURI = url.stripHash(this.reference.uri);
49
+ const documentReference = refSet.find(ref => ref.uri === documentURI) ?? this.reference;
50
+ const overlayElement = documentReference.value.result;
41
51
  const actionElement = new _apidomNsOverlay.ActionElement();
42
52
  try {
43
53
  if (typeof $ref !== 'string') {
@@ -108,11 +118,26 @@ class Overlay1DereferenceVisitor {
108
118
  const {
109
119
  continueOnError
110
120
  } = this.options.dereference;
121
+
122
+ // find element location by identity in the document tree;
123
+ // falls back to the visitor path when the document doesn't contain the element
124
+ let location;
125
+ if ((0, _apidomDatamodel.isElement)(overlayElement)) {
126
+ (0, _apidomTraverse.traverse)(overlayElement, {
127
+ enter: p => {
128
+ if (p.node === referencingElement) {
129
+ location = p.formatPath();
130
+ p.stop();
131
+ }
132
+ }
133
+ });
134
+ }
135
+ location ??= path.formatPath();
111
136
  const unresolvedError = new _UnresolvableReferenceError.default(`Error while dereferencing Reusable Action Reference "${$ref}": ${error.message}`, {
112
137
  cause: error,
113
138
  type: referencingElement.element,
114
139
  uri: this.reference.uri,
115
- location: path.formatPath(),
140
+ location,
116
141
  codeFrame: (0, _apidomCore.toYAML)(referencingElement),
117
142
  refFieldName: '$ref',
118
143
  refFieldValue: $ref,
@@ -1,9 +1,11 @@
1
- import { SourceMapElement, cloneDeep, hasElementSourceMap } from '@speclynx/apidom-datamodel';
1
+ import { SourceMapElement, cloneDeep, hasElementSourceMap, isElement } from '@speclynx/apidom-datamodel';
2
2
  import { toValue, toYAML } from '@speclynx/apidom-core';
3
3
  import { ApiDOMStructuredError } from '@speclynx/apidom-error';
4
4
  import { evaluate as jsonPointerEvaluate, URIFragmentIdentifier } from '@speclynx/apidom-json-pointer';
5
+ import { traverse } from '@speclynx/apidom-traverse';
5
6
  import { ActionElement, isActionElement, isReusableActionElement } from '@speclynx/apidom-ns-overlay-1';
6
7
  import UnresolvableReferenceError from "../../../errors/UnresolvableReferenceError.mjs";
8
+ import * as url from "../../../util/url.mjs";
7
9
  /**
8
10
  * @public
9
11
  */
@@ -29,7 +31,14 @@ class Overlay1DereferenceVisitor {
29
31
  return;
30
32
  }
31
33
  const $ref = toValue(referencingElement.$ref);
32
- const overlayElement = this.reference.value.result;
34
+ // the reference may hold a lone element; same-document references resolve against the
35
+ // document registered in the refSet under the reference's URI without fragment
36
+ const {
37
+ refSet
38
+ } = this.reference;
39
+ const documentURI = url.stripHash(this.reference.uri);
40
+ const documentReference = refSet.find(ref => ref.uri === documentURI) ?? this.reference;
41
+ const overlayElement = documentReference.value.result;
33
42
  const actionElement = new ActionElement();
34
43
  try {
35
44
  if (typeof $ref !== 'string') {
@@ -100,11 +109,26 @@ class Overlay1DereferenceVisitor {
100
109
  const {
101
110
  continueOnError
102
111
  } = this.options.dereference;
112
+
113
+ // find element location by identity in the document tree;
114
+ // falls back to the visitor path when the document doesn't contain the element
115
+ let location;
116
+ if (isElement(overlayElement)) {
117
+ traverse(overlayElement, {
118
+ enter: p => {
119
+ if (p.node === referencingElement) {
120
+ location = p.formatPath();
121
+ p.stop();
122
+ }
123
+ }
124
+ });
125
+ }
126
+ location ??= path.formatPath();
103
127
  const unresolvedError = new UnresolvableReferenceError(`Error while dereferencing Reusable Action Reference "${$ref}": ${error.message}`, {
104
128
  cause: error,
105
129
  type: referencingElement.element,
106
130
  uri: this.reference.uri,
107
- location: path.formatPath(),
131
+ location,
108
132
  codeFrame: toYAML(referencingElement),
109
133
  refFieldName: '$ref',
110
134
  refFieldValue: $ref,
@@ -13,6 +13,9 @@ var _File = _interopRequireDefault(require("../../../File.cjs"));
13
13
  var url = _interopRequireWildcard(require("../../../util/url.cjs"));
14
14
  var _util = require("../../../options/util.cjs");
15
15
  var _index = _interopRequireDefault(require("../../index.cjs"));
16
+ // shared key for recursion state (works across JSON/YAML parsers)
17
+ const OVERLAY_RECURSION_KEY = 'overlay-1';
18
+
16
19
  /**
17
20
  * Parses the `extends` target document from an Overlay document's ParseResult.
18
21
  *
@@ -49,21 +52,38 @@ async function parseExtends(parseResult, parseResultRetrievalURI, options) {
49
52
  const extendsParseResult = new _apidomDatamodel.ParseResultElement();
50
53
  extendsParseResult.classes.push('extends');
51
54
  extendsParseResult.setMetaProperty('retrievalURI', retrievalURI);
52
- try {
53
- const targetParseResult = await (0, _index.default)(retrievalURI, (0, _util.merge)(options, {
54
- parse: {
55
- mediaType: 'text/plain' // force auto-detection
55
+
56
+ // URIs of the Overlays currently being parsed, from the root down to the current one
57
+ const ancestors = [...(options?.parse?.parserOpts?.[OVERLAY_RECURSION_KEY]?.extendsAncestors ?? []), file.uri];
58
+
59
+ // skip if the target is still being parsed higher up the chain (cycle detection)
60
+ if (ancestors.includes(retrievalURI)) {
61
+ const annotation = new _apidomDatamodel.AnnotationElement(`Extends target "${retrievalURI}" has already been visited. Skipping to prevent cycle`);
62
+ annotation.classes.push('warning');
63
+ extendsParseResult.push(annotation);
64
+ } else {
65
+ try {
66
+ const targetParseResult = await (0, _index.default)(retrievalURI, (0, _util.merge)(options, {
67
+ parse: {
68
+ mediaType: 'text/plain',
69
+ // force auto-detection
70
+ parserOpts: {
71
+ [OVERLAY_RECURSION_KEY]: {
72
+ extendsAncestors: ancestors
73
+ }
74
+ }
75
+ }
76
+ }));
77
+ // merge parsed result into our wrapper
78
+ for (const item of targetParseResult) {
79
+ extendsParseResult.push(item);
56
80
  }
57
- }));
58
- // merge parsed result into our wrapper
59
- for (const item of targetParseResult) {
60
- extendsParseResult.push(item);
81
+ } catch (error) {
82
+ const message = error instanceof Error ? error.message : String(error);
83
+ const annotation = new _apidomDatamodel.AnnotationElement(`Error parsing extends target "${retrievalURI}": ${message}`);
84
+ annotation.classes.push('error');
85
+ extendsParseResult.push(annotation);
61
86
  }
62
- } catch (error) {
63
- const message = error instanceof Error ? error.message : String(error);
64
- const annotation = new _apidomDatamodel.AnnotationElement(`Error parsing extends target "${retrievalURI}": ${message}`);
65
- annotation.classes.push('error');
66
- extendsParseResult.push(annotation);
67
87
  }
68
88
 
69
89
  // always attach result to extends element meta (even on failure - contains annotations)
@@ -4,7 +4,9 @@ import { toValue } from '@speclynx/apidom-core';
4
4
  import File from "../../../File.mjs";
5
5
  import * as url from "../../../util/url.mjs";
6
6
  import { merge as mergeOptions } from "../../../options/util.mjs";
7
- import parse from "../../index.mjs";
7
+ import parse from "../../index.mjs"; // shared key for recursion state (works across JSON/YAML parsers)
8
+ const OVERLAY_RECURSION_KEY = 'overlay-1';
9
+
8
10
  /**
9
11
  * Parses the `extends` target document from an Overlay document's ParseResult.
10
12
  *
@@ -41,21 +43,38 @@ export async function parseExtends(parseResult, parseResultRetrievalURI, options
41
43
  const extendsParseResult = new ParseResultElement();
42
44
  extendsParseResult.classes.push('extends');
43
45
  extendsParseResult.setMetaProperty('retrievalURI', retrievalURI);
44
- try {
45
- const targetParseResult = await parse(retrievalURI, mergeOptions(options, {
46
- parse: {
47
- mediaType: 'text/plain' // force auto-detection
46
+
47
+ // URIs of the Overlays currently being parsed, from the root down to the current one
48
+ const ancestors = [...(options?.parse?.parserOpts?.[OVERLAY_RECURSION_KEY]?.extendsAncestors ?? []), file.uri];
49
+
50
+ // skip if the target is still being parsed higher up the chain (cycle detection)
51
+ if (ancestors.includes(retrievalURI)) {
52
+ const annotation = new AnnotationElement(`Extends target "${retrievalURI}" has already been visited. Skipping to prevent cycle`);
53
+ annotation.classes.push('warning');
54
+ extendsParseResult.push(annotation);
55
+ } else {
56
+ try {
57
+ const targetParseResult = await parse(retrievalURI, mergeOptions(options, {
58
+ parse: {
59
+ mediaType: 'text/plain',
60
+ // force auto-detection
61
+ parserOpts: {
62
+ [OVERLAY_RECURSION_KEY]: {
63
+ extendsAncestors: ancestors
64
+ }
65
+ }
66
+ }
67
+ }));
68
+ // merge parsed result into our wrapper
69
+ for (const item of targetParseResult) {
70
+ extendsParseResult.push(item);
48
71
  }
49
- }));
50
- // merge parsed result into our wrapper
51
- for (const item of targetParseResult) {
52
- extendsParseResult.push(item);
72
+ } catch (error) {
73
+ const message = error instanceof Error ? error.message : String(error);
74
+ const annotation = new AnnotationElement(`Error parsing extends target "${retrievalURI}": ${message}`);
75
+ annotation.classes.push('error');
76
+ extendsParseResult.push(annotation);
53
77
  }
54
- } catch (error) {
55
- const message = error instanceof Error ? error.message : String(error);
56
- const annotation = new AnnotationElement(`Error parsing extends target "${retrievalURI}": ${message}`);
57
- annotation.classes.push('error');
58
- extendsParseResult.push(annotation);
59
78
  }
60
79
 
61
80
  // always attach result to extends element meta (even on failure - contains annotations)
@@ -110,9 +110,14 @@ declare class OpenAPI3_0BundleVisitor {
110
110
  * Path Item Object is not a Components Object field in OpenAPI 3.0
111
111
  * (unlike 3.1 which has `components.pathItems`). An external Path Item
112
112
  * reference therefore cannot be hoisted; it is inlined in place instead,
113
- * after bundling its own external references.
114
- */
115
- PathItemElement(path: Path<Element>): Promise<void>;
113
+ * after bundling its own external references. It is inlined on leave, once
114
+ * the sibling fields of the referencing Path Item have been bundled, so the
115
+ * traversal doesn't descend into the inlined copy and inline the `$ref` left
116
+ * in it to break a cycle again.
117
+ */
118
+ readonly PathItemElement: {
119
+ leave: (path: Path<Element>) => Promise<void>;
120
+ };
116
121
  /**
117
122
  * Example Object `externalValue` points at arbitrary external content that
118
123
  * has no place in the Components Object. The referenced value is inlined into
@@ -1,5 +1,5 @@
1
1
  import { Element } from '@speclynx/apidom-datamodel';
2
- import type { Path } from '@speclynx/apidom-traverse';
2
+ import { type Path } from '@speclynx/apidom-traverse';
3
3
  import Reference from '../../../Reference.ts';
4
4
  import type { ReferenceOptions } from '../../../options/index.ts';
5
5
  /**