@scalar/openapi-parser 0.29.14 → 0.29.15

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,13 @@
1
1
  # @scalar/openapi-parser
2
2
 
3
+ ## 0.29.15
4
+
5
+ ### Patch Changes
6
+
7
+ - [#10643](https://github.com/scalar/scalar/pull/10643): Distinguish reference locations from schema name maps and literal data when validating, dereferencing, and collecting external references. Preserve `$ref` properties with object or boolean schemas, leave examples and default/enum/const values unchanged, and retain errors for malformed references.
8
+
9
+ Retain reference context across external files and fragments. Resolve custom operations while preserving OAuth scope names, discriminator mappings, Link payloads, and specification extension data.
10
+
3
11
  ## 0.29.14
4
12
 
5
13
  ## 0.29.13
@@ -1,8 +1,4 @@
1
1
  import type { AnyObject } from '@scalar/types/utils';
2
- /**
3
- * Walks through the specification and returns all references as an array.
4
- *
5
- * Warning: Doesn't return internal references.
6
- */
7
- export declare function getListOfReferences(specification: AnyObject): string[];
2
+ /** Collect external reference filenames without interpreting instance data as references. */
3
+ export declare const getListOfReferences: (specification: AnyObject) => string[];
8
4
  //# sourceMappingURL=get-list-of-references.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"get-list-of-references.d.ts","sourceRoot":"","sources":["../../src/utils/get-list-of-references.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAA;AAIpD;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,aAAa,EAAE,SAAS,YAmB3D"}
1
+ {"version":3,"file":"get-list-of-references.d.ts","sourceRoot":"","sources":["../../src/utils/get-list-of-references.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAA;AAIpD,6FAA6F;AAC7F,eAAO,MAAM,mBAAmB,GAAI,eAAe,SAAS,KAAG,MAAM,EAEpE,CAAA"}
@@ -1,22 +1,5 @@
1
- import { traverse } from './traverse.js';
2
- /**
3
- * Walks through the specification and returns all references as an array.
4
- *
5
- * Warning: Doesn't return internal references.
6
- */
7
- export function getListOfReferences(specification) {
8
- const references = [];
9
- // Make sure we're dealing with an object
10
- if (!specification || typeof specification !== 'object') {
11
- return references;
12
- }
13
- // Traverse the specification and collect all references
14
- traverse(specification, (value) => {
15
- if (value.$ref && typeof value.$ref === 'string' && !value.$ref.startsWith('#')) {
16
- references.push(value.$ref.split('#')[0]);
17
- }
18
- return value;
19
- });
20
- // Remove duplicates
21
- return [...new Set(references)];
22
- }
1
+ import { getReferenceTargets } from './get-reference-targets.js';
2
+ /** Collect external reference filenames without interpreting instance data as references. */
3
+ export const getListOfReferences = (specification) => [
4
+ ...new Set(getReferenceTargets(specification).map(({ filename }) => filename)),
5
+ ];
@@ -0,0 +1,14 @@
1
+ import { type ReferenceContext } from './reference-context.js';
2
+ /** A selected document fragment and the kind of object expected by its caller. */
3
+ export type ReferenceTarget = {
4
+ fragment: string;
5
+ context: ReferenceContext;
6
+ };
7
+ /** External dependencies retain their fragment and context until the file is loaded. */
8
+ type ExternalReferenceTarget = ReferenceTarget & {
9
+ filename: string;
10
+ };
11
+ /** Collect dependencies from selected fragments, following local aliases in the same context. */
12
+ export declare const getReferenceTargets: (specification: unknown, targets?: ReferenceTarget[]) => ExternalReferenceTarget[];
13
+ export {};
14
+ //# sourceMappingURL=get-reference-targets.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"get-reference-targets.d.ts","sourceRoot":"","sources":["../../src/utils/get-reference-targets.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,gBAAgB,EAAiD,MAAM,qBAAqB,CAAA;AAE1G,kFAAkF;AAClF,MAAM,MAAM,eAAe,GAAG;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,gBAAgB,CAAA;CAAE,CAAA;AAE7E,wFAAwF;AACxF,KAAK,uBAAuB,GAAG,eAAe,GAAG;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAA;AAErE,iGAAiG;AACjG,eAAO,MAAM,mBAAmB,GAC9B,eAAe,OAAO,EACtB,UAAS,eAAe,EAA4C,KACnE,uBAAuB,EA6CzB,CAAA"}
@@ -0,0 +1,45 @@
1
+ import { isObjectLike } from '@scalar/helpers/object/is-object';
2
+ import { getSegmentsFromPath } from './get-segments-from-path.js';
3
+ import { canResolveReference, getChildReferenceContext } from './reference-context.js';
4
+ /** Collect dependencies from selected fragments, following local aliases in the same context. */
5
+ export const getReferenceTargets = (specification, targets = [{ fragment: '', context: 'document' }]) => {
6
+ const references = [];
7
+ const localTargets = [];
8
+ const visited = new WeakMap();
9
+ const visitTarget = ({ fragment, context }) => {
10
+ try {
11
+ const target = getSegmentsFromPath(fragment).reduce((value, key) => (isObjectLike(value) && Object.hasOwn(value, key) ? value[key] : undefined), specification);
12
+ visit(target, context);
13
+ }
14
+ catch {
15
+ // Reference resolution reports invalid pointers; dependency collection does not validate them.
16
+ }
17
+ };
18
+ const visit = (value, context) => {
19
+ if (context === 'literal' || !isObjectLike(value) || visited.get(value)?.has(context)) {
20
+ return;
21
+ }
22
+ const contexts = visited.get(value) ?? new Set();
23
+ contexts.add(context);
24
+ visited.set(value, contexts);
25
+ if (canResolveReference(context) && typeof value.$ref === 'string' && value.$ref) {
26
+ const [filename, fragment = ''] = value.$ref.split('#', 2);
27
+ if (filename) {
28
+ references.push({ filename, fragment, context });
29
+ }
30
+ else {
31
+ localTargets.push({ fragment, context });
32
+ }
33
+ }
34
+ for (const [key, child] of Object.entries(value)) {
35
+ visit(child, getChildReferenceContext(context, key, child));
36
+ }
37
+ };
38
+ for (const target of targets) {
39
+ visitTarget(target);
40
+ }
41
+ for (const target of localTargets) {
42
+ visitTarget(target);
43
+ }
44
+ return references;
45
+ };
@@ -26,5 +26,5 @@ export type LoadOptions = {
26
26
  * It builds a filesystem representation of all loaded content and collects any errors
27
27
  * encountered during the process.
28
28
  */
29
- export declare function load(value: AnyApiDefinitionFormat, options?: LoadOptions): Promise<LoadResult>;
29
+ export declare const load: (value: AnyApiDefinitionFormat, options?: LoadOptions) => Promise<LoadResult>;
30
30
  //# sourceMappingURL=load.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"load.d.ts","sourceRoot":"","sources":["../../../src/utils/load/load.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,sBAAsB,EAAe,UAAU,EAAE,UAAU,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAA;AAMpH,MAAM,MAAM,UAAU,GAAG;IACvB,KAAK,EAAE,CAAC,KAAK,CAAC,EAAE,GAAG,KAAK,OAAO,CAAA;IAC/B,GAAG,EAAE,CAAC,KAAK,EAAE,GAAG,KAAK,GAAG,CAAA;IACxB,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,EAAE,SAAS,EAAE,MAAM,KAAK,MAAM,CAAA;IACvD,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,KAAK,MAAM,CAAA;IAC/B,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,KAAK,MAAM,CAAA;CACrC,CAAA;AAED,MAAM,MAAM,WAAW,GAAG;IACxB,OAAO,CAAC,EAAE,UAAU,EAAE,CAAA;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,UAAU,CAAC,EAAE,UAAU,CAAA;CACxB,GAAG,kBAAkB,CAAA;AAEtB;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,IAAI,CAAC,KAAK,EAAE,sBAAsB,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC,CAuHpG"}
1
+ {"version":3,"file":"load.d.ts","sourceRoot":"","sources":["../../../src/utils/load/load.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,sBAAsB,EAAe,UAAU,EAAE,UAAU,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAA;AAMpH,MAAM,MAAM,UAAU,GAAG;IACvB,KAAK,EAAE,CAAC,KAAK,CAAC,EAAE,GAAG,KAAK,OAAO,CAAA;IAC/B,GAAG,EAAE,CAAC,KAAK,EAAE,GAAG,KAAK,GAAG,CAAA;IACxB,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,EAAE,SAAS,EAAE,MAAM,KAAK,MAAM,CAAA;IACvD,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,KAAK,MAAM,CAAA;IAC/B,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,KAAK,MAAM,CAAA;CACrC,CAAA;AAED,MAAM,MAAM,WAAW,GAAG;IACxB,OAAO,CAAC,EAAE,UAAU,EAAE,CAAA;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,UAAU,CAAC,EAAE,UAAU,CAAA;CACxB,GAAG,kBAAkB,CAAA;AAEtB;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,IAAI,GAAI,OAAO,sBAAsB,EAAE,UAAU,WAAW,KAAG,OAAO,CAAC,UAAU,CAChE,CAAA"}
@@ -1,6 +1,6 @@
1
1
  import { ERRORS } from '../../configuration/index.js';
2
2
  import { getEntrypoint } from '../../utils/get-entrypoint.js';
3
- import { getListOfReferences } from '../../utils/get-list-of-references.js';
3
+ import { getReferenceTargets } from '../../utils/get-reference-targets.js';
4
4
  import { makeFilesystem } from '../../utils/make-filesystem.js';
5
5
  import { normalize } from '../../utils/normalize.js';
6
6
  /**
@@ -18,7 +18,8 @@ import { normalize } from '../../utils/normalize.js';
18
18
  * It builds a filesystem representation of all loaded content and collects any errors
19
19
  * encountered during the process.
20
20
  */
21
- export async function load(value, options) {
21
+ export const load = (value, options) => loadDocument(value, options);
22
+ const loadDocument = async (value, options, targets, documents = new Map()) => {
22
23
  const errors = [];
23
24
  // Don't load a reference twice, check the filesystem before fetching something
24
25
  if (options?.filesystem?.find((entry) => entry.filename === value)) {
@@ -30,8 +31,10 @@ export async function load(value, options) {
30
31
  }
31
32
  // Check whether the value is an URL or file path
32
33
  const plugin = options?.plugins?.find((thisPlugin) => thisPlugin.check(value));
33
- let content = normalize(value);
34
- if (plugin) {
34
+ const documentKey = typeof value === 'string' ? value : options?.filename;
35
+ const cached = documentKey === undefined ? undefined : documents.get(documentKey);
36
+ let content = cached?.content ?? normalize(value);
37
+ if (plugin && !cached) {
35
38
  try {
36
39
  content = normalize(await plugin.get(value));
37
40
  }
@@ -65,14 +68,29 @@ export async function load(value, options) {
65
68
  errors,
66
69
  };
67
70
  }
68
- let filesystem = makeFilesystem(content, {
71
+ const filesystem = makeFilesystem(content, {
69
72
  filename: options?.filename ?? null,
70
73
  });
71
74
  // Get references from file system entry, or from the content
72
75
  const newEntry = options?.filename
73
76
  ? filesystem.find((entry) => entry.filename === options?.filename)
74
77
  : getEntrypoint(filesystem);
75
- const listOfReferences = newEntry.references ?? getListOfReferences(content);
78
+ // A file may be reached later through another fragment or another object type.
79
+ const visitedTargets = cached?.targets ?? new Set();
80
+ const pendingTargets = (targets ?? [{ fragment: '', context: 'document' }]).filter((target) => {
81
+ const key = JSON.stringify(target);
82
+ if (visitedTargets.has(key)) {
83
+ return false;
84
+ }
85
+ visitedTargets.add(key);
86
+ return true;
87
+ });
88
+ if (documentKey !== undefined) {
89
+ documents.set(documentKey, { content, targets: visitedTargets });
90
+ }
91
+ const referenceTargets = getReferenceTargets(newEntry.specification, pendingTargets);
92
+ const listOfReferences = [...new Set(referenceTargets.map(({ filename }) => filename))];
93
+ newEntry.references = listOfReferences;
76
94
  // No other references
77
95
  if (listOfReferences.length === 0) {
78
96
  return {
@@ -90,30 +108,28 @@ export async function load(value, options) {
90
108
  continue;
91
109
  }
92
110
  const target = otherPlugin.check(reference) && otherPlugin.resolvePath ? otherPlugin.resolvePath(value, reference) : reference;
93
- // Don't load a reference twice, check the filesystem before fetching something
94
- if (filesystem.find((entry) => entry.filename === reference)) {
95
- continue;
96
- }
97
- const { filesystem: referencedFiles, errors: newErrors } = await load(target, {
111
+ const { filesystem: referencedFiles, errors: newErrors } = await loadDocument(target, {
98
112
  ...options,
99
113
  // Make the filename the exact same value as the $ref
100
114
  // TODO: This leads to problems, if there are multiple references with the same file name but in different folders
101
115
  filename: reference,
102
- });
116
+ }, referenceTargets
117
+ .filter(({ filename }) => filename === reference)
118
+ .map(({ fragment, context }) => ({ fragment, context })), documents);
103
119
  errors.push(...newErrors);
104
- filesystem = [
105
- ...filesystem,
106
- ...referencedFiles.map((file) => {
107
- return {
108
- ...file,
109
- isEntrypoint: false,
110
- };
111
- }),
112
- ];
120
+ for (const file of referencedFiles) {
121
+ const existing = filesystem.find((entry) => entry.filename === file.filename);
122
+ if (existing) {
123
+ existing.references = [...new Set([...existing.references, ...file.references])];
124
+ }
125
+ else {
126
+ filesystem.push({ ...file, isEntrypoint: false });
127
+ }
128
+ }
113
129
  }
114
130
  return {
115
131
  specification: getEntrypoint(filesystem)?.specification,
116
132
  filesystem,
117
133
  errors,
118
134
  };
119
- }
135
+ };