@scalar/json-magic 0.12.2 → 0.12.4
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 +15 -0
- package/dist/bundle/bundle.js +476 -250
- package/dist/bundle/index.js +1 -6
- package/dist/bundle/plugins/browser.js +4 -9
- package/dist/bundle/plugins/fetch-urls/index.js +76 -49
- package/dist/bundle/plugins/node.js +5 -11
- package/dist/bundle/plugins/parse-json/index.js +30 -23
- package/dist/bundle/plugins/parse-yaml/index.js +31 -24
- package/dist/bundle/plugins/read-files/index.js +50 -29
- package/dist/bundle/value-generator.js +118 -49
- package/dist/dereference/dereference.js +66 -36
- package/dist/dereference/index.js +1 -5
- package/dist/diff/apply.js +69 -36
- package/dist/diff/diff.js +68 -32
- package/dist/diff/index.js +4 -9
- package/dist/diff/merge.js +122 -60
- package/dist/diff/trie.js +100 -77
- package/dist/diff/utils.js +96 -41
- package/dist/helpers/convert-to-local-ref.js +30 -23
- package/dist/helpers/escape-json-pointer.js +7 -6
- package/dist/helpers/get-schemas.js +59 -32
- package/dist/helpers/get-segments-from-path.js +13 -9
- package/dist/helpers/get-value-by-path.js +38 -22
- package/dist/helpers/is-file-path.js +19 -9
- package/dist/helpers/is-http-url.js +20 -11
- package/dist/helpers/is-json-object.js +29 -15
- package/dist/helpers/is-yaml.js +17 -6
- package/dist/helpers/json-path-utils.js +30 -15
- package/dist/helpers/normalize.js +27 -26
- package/dist/helpers/resolve-reference-path.js +28 -16
- package/dist/helpers/set-value-at-path.js +72 -26
- package/dist/helpers/to-relative-path.js +40 -28
- package/dist/helpers/unescape-json-pointer.js +8 -6
- package/dist/magic-proxy/index.js +1 -6
- package/dist/magic-proxy/proxy.js +245 -186
- package/dist/types.js +1 -1
- package/package.json +5 -7
- package/dist/bundle/bundle.js.map +0 -7
- package/dist/bundle/index.js.map +0 -7
- package/dist/bundle/plugins/browser.js.map +0 -7
- package/dist/bundle/plugins/fetch-urls/index.js.map +0 -7
- package/dist/bundle/plugins/node.js.map +0 -7
- package/dist/bundle/plugins/parse-json/index.js.map +0 -7
- package/dist/bundle/plugins/parse-yaml/index.js.map +0 -7
- package/dist/bundle/plugins/read-files/index.js.map +0 -7
- package/dist/bundle/value-generator.js.map +0 -7
- package/dist/dereference/dereference.js.map +0 -7
- package/dist/dereference/index.js.map +0 -7
- package/dist/diff/apply.js.map +0 -7
- package/dist/diff/diff.js.map +0 -7
- package/dist/diff/index.js.map +0 -7
- package/dist/diff/merge.js.map +0 -7
- package/dist/diff/trie.js.map +0 -7
- package/dist/diff/utils.js.map +0 -7
- package/dist/helpers/convert-to-local-ref.js.map +0 -7
- package/dist/helpers/escape-json-pointer.js.map +0 -7
- package/dist/helpers/get-schemas.js.map +0 -7
- package/dist/helpers/get-segments-from-path.js.map +0 -7
- package/dist/helpers/get-value-by-path.js.map +0 -7
- package/dist/helpers/is-file-path.js.map +0 -7
- package/dist/helpers/is-http-url.js.map +0 -7
- package/dist/helpers/is-json-object.js.map +0 -7
- package/dist/helpers/is-yaml.js.map +0 -7
- package/dist/helpers/json-path-utils.js.map +0 -7
- package/dist/helpers/normalize.js.map +0 -7
- package/dist/helpers/resolve-reference-path.js.map +0 -7
- package/dist/helpers/set-value-at-path.js.map +0 -7
- package/dist/helpers/to-relative-path.js.map +0 -7
- package/dist/helpers/unescape-json-pointer.js.map +0 -7
- package/dist/magic-proxy/index.js.map +0 -7
- package/dist/magic-proxy/proxy.js.map +0 -7
- package/dist/types.js.map +0 -7
package/dist/bundle/bundle.js
CHANGED
|
@@ -1,267 +1,493 @@
|
|
|
1
|
-
import { isObject } from
|
|
2
|
-
import { convertToLocalRef } from
|
|
3
|
-
import { getId, getSchemas } from
|
|
4
|
-
import { getValueByPath } from
|
|
5
|
-
import { isFilePath } from
|
|
6
|
-
import { isHttpUrl } from
|
|
7
|
-
import { resolveReferencePath } from
|
|
8
|
-
import { setValueAtPath } from
|
|
9
|
-
import { toRelativePath } from
|
|
10
|
-
import { escapeJsonPointer } from
|
|
11
|
-
import { getSegmentsFromPath } from
|
|
12
|
-
import { getHash, uniqueValueGeneratorFactory } from
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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';
|
|
4
|
+
import { getValueByPath } from '../helpers/get-value-by-path.js';
|
|
5
|
+
import { isFilePath } from '../helpers/is-file-path.js';
|
|
6
|
+
import { isHttpUrl } from '../helpers/is-http-url.js';
|
|
7
|
+
import { resolveReferencePath } from '../helpers/resolve-reference-path.js';
|
|
8
|
+
import { setValueAtPath } from '../helpers/set-value-at-path.js';
|
|
9
|
+
import { toRelativePath } from '../helpers/to-relative-path.js';
|
|
10
|
+
import { escapeJsonPointer } from '../helpers/escape-json-pointer.js';
|
|
11
|
+
import { getSegmentsFromPath } from '../helpers/get-segments-from-path.js';
|
|
12
|
+
import { getHash, uniqueValueGeneratorFactory } from './value-generator.js';
|
|
13
|
+
/** Type guard to check if a value is an object with a $ref property */
|
|
14
|
+
const hasRef = (value) => isObject(value) && '$ref' in value && typeof value['$ref'] === 'string';
|
|
15
|
+
/**
|
|
16
|
+
* Checks if a string is a local reference (starts with #)
|
|
17
|
+
* @param value - The reference string to check
|
|
18
|
+
* @returns true if the string is a local reference, false otherwise
|
|
19
|
+
* @example
|
|
20
|
+
* ```ts
|
|
21
|
+
* isLocalRef('#/components/schemas/User') // true
|
|
22
|
+
* isLocalRef('https://example.com/schema.json') // false
|
|
23
|
+
* isLocalRef('./local-schema.json') // false
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export function isLocalRef(value) {
|
|
27
|
+
return value.startsWith('#');
|
|
16
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* Resolves a string by finding and executing the appropriate plugin.
|
|
31
|
+
* @param value - The string to resolve (URL, file path, etc)
|
|
32
|
+
* @param plugins - Array of plugins that can handle different types of strings
|
|
33
|
+
* @returns A promise that resolves to either the content or an error result
|
|
34
|
+
* @example
|
|
35
|
+
* // Using a URL plugin
|
|
36
|
+
* await resolveContents('https://example.com/schema.json', [urlPlugin])
|
|
37
|
+
* // Using a file plugin
|
|
38
|
+
* await resolveContents('./schemas/user.json', [filePlugin])
|
|
39
|
+
* // No matching plugin returns { ok: false }
|
|
40
|
+
* await resolveContents('#/components/schemas/User', [urlPlugin, filePlugin])
|
|
41
|
+
*/
|
|
17
42
|
function resolveContents(value, plugins) {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
}
|
|
26
|
-
function prefixInternalRef(input, prefix) {
|
|
27
|
-
if (!isLocalRef(input)) {
|
|
28
|
-
throw "Please provide an internal ref";
|
|
29
|
-
}
|
|
30
|
-
return `#/${prefix.map(escapeJsonPointer).join("/")}${input.substring(1)}`;
|
|
43
|
+
const plugin = plugins.find((p) => p.validate(value));
|
|
44
|
+
if (plugin) {
|
|
45
|
+
return plugin.exec(value);
|
|
46
|
+
}
|
|
47
|
+
return Promise.resolve({
|
|
48
|
+
ok: false,
|
|
49
|
+
});
|
|
31
50
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
51
|
+
/**
|
|
52
|
+
* Prefixes an internal JSON reference with a given path prefix.
|
|
53
|
+
* Takes a local reference (starting with #) and prepends the provided prefix segments.
|
|
54
|
+
*
|
|
55
|
+
* @param input - The internal reference string to prefix (must start with #)
|
|
56
|
+
* @param prefix - Array of path segments to prepend to the reference
|
|
57
|
+
* @returns The prefixed reference string
|
|
58
|
+
* @throws Error if input is not a local reference
|
|
59
|
+
* @example
|
|
60
|
+
* prefixInternalRef('#/components/schemas/User', ['definitions'])
|
|
61
|
+
* // Returns: '#/definitions/components/schemas/User'
|
|
62
|
+
*/
|
|
63
|
+
export function prefixInternalRef(input, prefix) {
|
|
64
|
+
if (!isLocalRef(input)) {
|
|
65
|
+
throw 'Please provide an internal ref';
|
|
45
66
|
}
|
|
46
|
-
|
|
47
|
-
}
|
|
67
|
+
return `#/${prefix.map(escapeJsonPointer).join('/')}${input.substring(1)}`;
|
|
48
68
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
69
|
+
/**
|
|
70
|
+
* Updates internal references in an object by adding a prefix to their paths.
|
|
71
|
+
* Recursively traverses the input object and modifies any local $ref references
|
|
72
|
+
* by prepending the given prefix to their paths. This is used when embedding external
|
|
73
|
+
* documents to maintain correct reference paths relative to the main document.
|
|
74
|
+
*
|
|
75
|
+
* @param input - The object to update references in
|
|
76
|
+
* @param prefix - Array of path segments to prepend to internal reference paths
|
|
77
|
+
* @returns void
|
|
78
|
+
* @example
|
|
79
|
+
* ```ts
|
|
80
|
+
* const input = {
|
|
81
|
+
* foo: {
|
|
82
|
+
* $ref: '#/components/schemas/User'
|
|
83
|
+
* }
|
|
84
|
+
* }
|
|
85
|
+
* prefixInternalRefRecursive(input, ['definitions'])
|
|
86
|
+
* // Result:
|
|
87
|
+
* // {
|
|
88
|
+
* // foo: {
|
|
89
|
+
* // $ref: '#/definitions/components/schemas/User'
|
|
90
|
+
* // }
|
|
91
|
+
* // }
|
|
92
|
+
* ```
|
|
93
|
+
*/
|
|
94
|
+
export function prefixInternalRefRecursive(input, prefix) {
|
|
95
|
+
if (Array.isArray(input)) {
|
|
96
|
+
input.forEach((el) => prefixInternalRefRecursive(el, prefix));
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (!isObject(input)) {
|
|
100
|
+
return;
|
|
59
101
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
documentKey,
|
|
68
|
-
bundleLocalRefs,
|
|
69
|
-
processedNodes
|
|
70
|
-
);
|
|
71
|
-
} else if (bundleLocalRefs) {
|
|
72
|
-
resolveAndCopyReferences(
|
|
73
|
-
targetDocument,
|
|
74
|
-
sourceDocument,
|
|
75
|
-
node["$ref"].substring(1),
|
|
76
|
-
externalRefsKey,
|
|
77
|
-
documentKey,
|
|
78
|
-
bundleLocalRefs,
|
|
79
|
-
processedNodes
|
|
80
|
-
);
|
|
81
|
-
}
|
|
102
|
+
Object.values(input).forEach((el) => prefixInternalRefRecursive(el, prefix));
|
|
103
|
+
if (typeof input === 'object' && '$ref' in input && typeof input['$ref'] === 'string') {
|
|
104
|
+
const ref = input['$ref'];
|
|
105
|
+
if (!isLocalRef(ref)) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
input['$ref'] = prefixInternalRef(ref, prefix);
|
|
82
109
|
}
|
|
83
|
-
|
|
84
|
-
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Resolves and copies referenced values from a source document to a target document.
|
|
113
|
+
* This function traverses the document and copies referenced values to the target document,
|
|
114
|
+
* while tracking processed references to avoid duplicates. It only processes references
|
|
115
|
+
* that belong to the same external document.
|
|
116
|
+
*
|
|
117
|
+
* @param targetDocument - The document to copy referenced values to
|
|
118
|
+
* @param sourceDocument - The source document containing the references
|
|
119
|
+
* @param referencePath - The JSON pointer path to the reference
|
|
120
|
+
* @param externalRefsKey - The key used for external references (e.g. 'x-ext')
|
|
121
|
+
* @param documentKey - The key identifying the external document
|
|
122
|
+
* @param bundleLocalRefs - Also bundles the local refs
|
|
123
|
+
* @param processedNodes - Set of already processed nodes to prevent duplicates
|
|
124
|
+
* @example
|
|
125
|
+
* ```ts
|
|
126
|
+
* const source = {
|
|
127
|
+
* components: {
|
|
128
|
+
* schemas: {
|
|
129
|
+
* User: {
|
|
130
|
+
* $ref: '#/x-ext/users~1schema/definitions/Person'
|
|
131
|
+
* }
|
|
132
|
+
* }
|
|
133
|
+
* }
|
|
134
|
+
* }
|
|
135
|
+
*
|
|
136
|
+
* const target = {}
|
|
137
|
+
* resolveAndCopyReferences(
|
|
138
|
+
* target,
|
|
139
|
+
* source,
|
|
140
|
+
* '/components/schemas/User',
|
|
141
|
+
* 'x-ext',
|
|
142
|
+
* 'users/schema'
|
|
143
|
+
* )
|
|
144
|
+
* // Result: target will contain the User schema with resolved references
|
|
145
|
+
* ```
|
|
146
|
+
*/
|
|
147
|
+
export const resolveAndCopyReferences = (targetDocument, sourceDocument, referencePath, externalRefsKey, documentKey, bundleLocalRefs = false, processedNodes = new Set()) => {
|
|
148
|
+
const referencedValue = getValueByPath(sourceDocument, getSegmentsFromPath(referencePath)).value;
|
|
149
|
+
if (processedNodes.has(referencedValue)) {
|
|
150
|
+
return;
|
|
85
151
|
}
|
|
86
|
-
|
|
87
|
-
|
|
152
|
+
processedNodes.add(referencedValue);
|
|
153
|
+
setValueAtPath(targetDocument, referencePath, referencedValue);
|
|
154
|
+
// Do the same for each local ref
|
|
155
|
+
const traverse = (node) => {
|
|
156
|
+
if (!node || typeof node !== 'object') {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
if ('$ref' in node && typeof node['$ref'] === 'string') {
|
|
160
|
+
// We only process references from the same external document because:
|
|
161
|
+
// 1. Other documents will be handled in separate recursive branches
|
|
162
|
+
// 2. The source document only contains the current document's content
|
|
163
|
+
// This prevents undefined behavior and maintains proper document boundaries
|
|
164
|
+
if (node['$ref'].startsWith(`#/${externalRefsKey}/${escapeJsonPointer(documentKey)}`)) {
|
|
165
|
+
resolveAndCopyReferences(targetDocument, sourceDocument, node['$ref'].substring(1), externalRefsKey, documentKey, bundleLocalRefs, processedNodes);
|
|
166
|
+
}
|
|
167
|
+
// Bundle the local refs as well
|
|
168
|
+
else if (bundleLocalRefs) {
|
|
169
|
+
resolveAndCopyReferences(targetDocument, sourceDocument, node['$ref'].substring(1), externalRefsKey, documentKey, bundleLocalRefs, processedNodes);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
for (const value of Object.values(node)) {
|
|
173
|
+
traverse(value);
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
traverse(referencedValue);
|
|
88
177
|
};
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
178
|
+
/**
|
|
179
|
+
* Extension keys used for bundling external references in OpenAPI documents.
|
|
180
|
+
* These custom extensions help maintain the structure and traceability of bundled documents.
|
|
181
|
+
*/
|
|
182
|
+
export const extensions = {
|
|
183
|
+
/**
|
|
184
|
+
* Custom OpenAPI extension key used to store external references.
|
|
185
|
+
* This key will contain all bundled external documents.
|
|
186
|
+
* The x-ext key is used to maintain a clean separation between the main
|
|
187
|
+
* OpenAPI document and its bundled external references.
|
|
188
|
+
*/
|
|
189
|
+
externalDocuments: 'x-ext',
|
|
190
|
+
/**
|
|
191
|
+
* Custom OpenAPI extension key used to maintain a mapping between
|
|
192
|
+
* hashed keys and their original URLs in x-ext.
|
|
193
|
+
* This mapping is essential for tracking the source of bundled references
|
|
194
|
+
*/
|
|
195
|
+
externalDocumentsMappings: 'x-ext-urls',
|
|
103
196
|
};
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
if
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
197
|
+
/**
|
|
198
|
+
* Bundles an OpenAPI specification by resolving all external references.
|
|
199
|
+
* This function traverses the input object recursively and embeds external $ref
|
|
200
|
+
* references into an x-ext section. External references can be URLs or local files.
|
|
201
|
+
* The original $refs are updated to point to their embedded content in the x-ext section.
|
|
202
|
+
* If the input is an object, it will be modified in place by adding an x-ext
|
|
203
|
+
* property to store resolved external references.
|
|
204
|
+
*
|
|
205
|
+
* @param input - The OpenAPI specification to bundle. Can be either an object or string.
|
|
206
|
+
* If a string is provided, it will be resolved using the provided plugins.
|
|
207
|
+
* If no plugin can process the input, the onReferenceError hook will be invoked
|
|
208
|
+
* and an error will be emitted to the console.
|
|
209
|
+
* @param config - Configuration object containing plugins and options for bundling OpenAPI specifications
|
|
210
|
+
* @returns A promise that resolves to the bundled specification with all references embedded
|
|
211
|
+
* @example
|
|
212
|
+
* // Example with object input
|
|
213
|
+
* const spec = {
|
|
214
|
+
* paths: {
|
|
215
|
+
* '/users': {
|
|
216
|
+
* $ref: 'https://example.com/schemas/users.yaml'
|
|
217
|
+
* }
|
|
218
|
+
* }
|
|
219
|
+
* }
|
|
220
|
+
*
|
|
221
|
+
* const bundled = await bundle(spec, {
|
|
222
|
+
* plugins: [fetchUrls()],
|
|
223
|
+
* treeShake: true,
|
|
224
|
+
* urlMap: true,
|
|
225
|
+
* hooks: {
|
|
226
|
+
* onResolveStart: (ref) => console.log('Resolving:', ref.$ref),
|
|
227
|
+
* onResolveSuccess: (ref) => console.log('Resolved:', ref.$ref),
|
|
228
|
+
* onResolveError: (ref) => console.log('Failed to resolve:', ref.$ref)
|
|
229
|
+
* }
|
|
230
|
+
* })
|
|
231
|
+
* // Result:
|
|
232
|
+
* // {
|
|
233
|
+
* // paths: {
|
|
234
|
+
* // '/users': {
|
|
235
|
+
* // $ref: '#/x-ext/abc123'
|
|
236
|
+
* // }
|
|
237
|
+
* // },
|
|
238
|
+
* // 'x-ext': {
|
|
239
|
+
* // 'abc123': {
|
|
240
|
+
* // // Resolved content from users.yaml
|
|
241
|
+
* // }
|
|
242
|
+
* // },
|
|
243
|
+
* // 'x-ext-urls': {
|
|
244
|
+
* // 'https://example.com/schemas/users.yaml': 'abc123'
|
|
245
|
+
* // }
|
|
246
|
+
* // }
|
|
247
|
+
*
|
|
248
|
+
* // Example with URL input
|
|
249
|
+
* const bundledFromUrl = await bundle('https://example.com/openapi.yaml', {
|
|
250
|
+
* plugins: [fetchUrls()],
|
|
251
|
+
* treeShake: true,
|
|
252
|
+
* urlMap: true,
|
|
253
|
+
* hooks: {
|
|
254
|
+
* onResolveStart: (ref) => console.log('Resolving:', ref.$ref),
|
|
255
|
+
* onResolveSuccess: (ref) => console.log('Resolved:', ref.$ref),
|
|
256
|
+
* onResolveError: (ref) => console.log('Failed to resolve:', ref.$ref)
|
|
257
|
+
* }
|
|
258
|
+
* })
|
|
259
|
+
* // The function will first fetch the OpenAPI spec from the URL,
|
|
260
|
+
* // then bundle all its external references into the x-ext section
|
|
261
|
+
*/
|
|
262
|
+
export async function bundle(input, config) {
|
|
263
|
+
// Set the default external documents key and mappings key if not provided in the config
|
|
264
|
+
config.externalDocumentsKey = config.externalDocumentsKey ?? extensions.externalDocuments;
|
|
265
|
+
config.externalDocumentsMappingsKey = config.externalDocumentsMappingsKey ?? extensions.externalDocumentsMappings;
|
|
266
|
+
// Cache for storing promises of resolved external references (URLs and local files)
|
|
267
|
+
// to avoid duplicate fetches/reads of the same resource
|
|
268
|
+
const cache = config.cache ?? new Map();
|
|
269
|
+
const loaderPlugins = config.plugins.filter((it) => it.type === 'loader');
|
|
270
|
+
const lifecyclePlugin = config.plugins.filter((it) => it.type === 'lifecycle');
|
|
271
|
+
/**
|
|
272
|
+
* Resolves the input value by either returning it directly if it's not a string,
|
|
273
|
+
* or attempting to resolve it using the provided plugins if it is a string.
|
|
274
|
+
* @returns The resolved input data or throws an error if resolution fails
|
|
275
|
+
*/
|
|
276
|
+
const resolveInput = async () => {
|
|
277
|
+
if (typeof input !== 'string') {
|
|
278
|
+
return input;
|
|
279
|
+
}
|
|
280
|
+
const result = await resolveContents(input, loaderPlugins);
|
|
281
|
+
if (result.ok && typeof result.data === 'object') {
|
|
282
|
+
return result.data;
|
|
283
|
+
}
|
|
284
|
+
throw new Error('Failed to resolve input: Please provide a valid string value or pass a loader to process the input');
|
|
180
285
|
};
|
|
181
|
-
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
286
|
+
// Resolve the input specification, which could be either a direct object or a string URL/path
|
|
287
|
+
const rawSpecification = await resolveInput();
|
|
288
|
+
// Document root used to write all external documents
|
|
289
|
+
// We need this when we want to do a partial bundle of a document
|
|
290
|
+
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
|
+
// Determines if the bundling operation is partial.
|
|
294
|
+
// Partial bundling occurs when:
|
|
295
|
+
// - A root document is provided that is different from the raw specification being bundled, or
|
|
296
|
+
// - A maximum depth is specified in the config.
|
|
297
|
+
// In these cases, only a subset of the document may be bundled.
|
|
298
|
+
const isPartialBundling = (config.root !== undefined && config.root !== rawSpecification) || config.depth !== undefined;
|
|
299
|
+
// Set of nodes that have already been processed during bundling to prevent duplicate processing
|
|
300
|
+
const processedNodes = config.visitedNodes ?? new Set();
|
|
301
|
+
// Determines the initial origin path for the bundler based on the input type.
|
|
302
|
+
// For string inputs that are URLs or file paths, uses the input as the origin.
|
|
303
|
+
// For non-string inputs or other string types, returns an '/' as a root path.
|
|
304
|
+
const getDefaultOrigin = () => {
|
|
305
|
+
// Id field is the first priority
|
|
306
|
+
const id = getId(documentRoot);
|
|
307
|
+
if (id) {
|
|
308
|
+
return id;
|
|
193
309
|
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
}
|
|
197
|
-
const [prefix, path = ""] = ref.split("#", 2);
|
|
198
|
-
const resolvedPath = resolveReferencePath(id ?? origin, prefix);
|
|
199
|
-
const relativePath = toRelativePath(resolvedPath, defaultOrigin);
|
|
200
|
-
const compressedPath = await generate(relativePath);
|
|
201
|
-
const seen = cache.has(relativePath);
|
|
202
|
-
if (!seen) {
|
|
203
|
-
cache.set(relativePath, resolveContents(resolvedPath, loaderPlugins));
|
|
204
|
-
}
|
|
205
|
-
await executeHooks("onResolveStart", root);
|
|
206
|
-
const result = await cache.get(relativePath);
|
|
207
|
-
if (result.ok) {
|
|
208
|
-
if (!seen) {
|
|
209
|
-
if (!isChunk) {
|
|
210
|
-
prefixInternalRefRecursive(result.data, [extensions.externalDocuments, compressedPath]);
|
|
211
|
-
}
|
|
212
|
-
await bundler(result.data, isChunk ? origin : resolvedPath, isChunk, depth + 1, [
|
|
213
|
-
config.externalDocumentsKey,
|
|
214
|
-
compressedPath,
|
|
215
|
-
documentRoot[config.externalDocumentsMappingsKey]
|
|
216
|
-
]);
|
|
217
|
-
setValueAtPath(
|
|
218
|
-
documentRoot,
|
|
219
|
-
`/${config.externalDocumentsMappingsKey}/${escapeJsonPointer(compressedPath)}`,
|
|
220
|
-
relativePath
|
|
221
|
-
);
|
|
310
|
+
if (config.origin) {
|
|
311
|
+
return config.origin;
|
|
222
312
|
}
|
|
223
|
-
if (
|
|
224
|
-
|
|
225
|
-
documentRoot,
|
|
226
|
-
{ [config.externalDocumentsKey]: { [compressedPath]: result.data } },
|
|
227
|
-
prefixInternalRef(`#${path}`, [config.externalDocumentsKey, compressedPath]).substring(1),
|
|
228
|
-
config.externalDocumentsKey,
|
|
229
|
-
compressedPath
|
|
230
|
-
);
|
|
231
|
-
} else if (!seen) {
|
|
232
|
-
setValueAtPath(documentRoot, `/${config.externalDocumentsKey}/${compressedPath}`, result.data);
|
|
313
|
+
if (typeof input !== 'string') {
|
|
314
|
+
return '/';
|
|
233
315
|
}
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
return;
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
);
|
|
316
|
+
if (isHttpUrl(input) || isFilePath(input)) {
|
|
317
|
+
return input;
|
|
318
|
+
}
|
|
319
|
+
return '/';
|
|
320
|
+
};
|
|
321
|
+
const defaultOrigin = getDefaultOrigin();
|
|
322
|
+
// Create the cache to store the compressed values to their map values
|
|
323
|
+
if (documentRoot[config.externalDocumentsMappingsKey] === undefined) {
|
|
324
|
+
documentRoot[config.externalDocumentsMappingsKey] = {};
|
|
244
325
|
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
326
|
+
const { generate } = uniqueValueGeneratorFactory(config.compress ?? getHash, documentRoot[config.externalDocumentsMappingsKey]);
|
|
327
|
+
/**
|
|
328
|
+
* Executes lifecycle hooks defined both in the bundler configuration and any extended lifecycle plugins.
|
|
329
|
+
* This utility function ensures that all relevant hooks for a given event type are called in order:
|
|
330
|
+
* - First, the hook directly provided via the config (if present)
|
|
331
|
+
* - Then, all matching hooks from registered lifecycle plugins (if present)
|
|
332
|
+
*
|
|
333
|
+
* Hooks are awaited in sequence for the given event type and argument list.
|
|
334
|
+
*
|
|
335
|
+
* @param type The hook event type, corresponding to a key of Config['hooks'].
|
|
336
|
+
* @param args Arguments to pass to the hook function, matching HookFn<T>.
|
|
337
|
+
*/
|
|
338
|
+
const executeHooks = async (type, ...args) => {
|
|
339
|
+
// Run hook defined directly in config, if present
|
|
340
|
+
const hook = config.hooks?.[type];
|
|
341
|
+
if (hook) {
|
|
342
|
+
await hook(...args);
|
|
343
|
+
}
|
|
344
|
+
// Additionally run the hook for every lifecycle plugin, if present
|
|
345
|
+
for (const plugin of lifecyclePlugin) {
|
|
346
|
+
const pluginHook = plugin[type];
|
|
347
|
+
if (pluginHook) {
|
|
348
|
+
await pluginHook(...args);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
const bundler = async (root, origin = defaultOrigin, isChunkParent = false, depth = 0, currentPath = [], parent = null) => {
|
|
353
|
+
// If a maximum depth is set in the config, stop bundling when the current depth reaches or exceeds it
|
|
354
|
+
if (config.depth !== undefined && depth > config.depth) {
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
if (!isObject(root) && !Array.isArray(root)) {
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
// Skip if this node has already been processed to prevent infinite recursion
|
|
361
|
+
// and duplicate processing of the same node
|
|
362
|
+
if (processedNodes.has(root)) {
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
// Mark this node as processed before continuing
|
|
366
|
+
processedNodes.add(root);
|
|
367
|
+
const context = {
|
|
368
|
+
path: currentPath,
|
|
369
|
+
resolutionCache: cache,
|
|
370
|
+
parentNode: parent,
|
|
371
|
+
rootNode: documentRoot,
|
|
372
|
+
loaders: loaderPlugins,
|
|
373
|
+
};
|
|
374
|
+
await executeHooks('onBeforeNodeProcess', root, context);
|
|
375
|
+
const id = getId(root);
|
|
376
|
+
if (hasRef(root)) {
|
|
377
|
+
const ref = root['$ref'];
|
|
378
|
+
const isChunk = '$global' in root && typeof root['$global'] === 'boolean' && root['$global'];
|
|
379
|
+
// Try to convert the reference to a local reference if possible
|
|
380
|
+
// This handles cases where the reference points to a local schema using $id or $anchor
|
|
381
|
+
// If it can be converted to a local reference, we do not need to bundle it
|
|
382
|
+
// and can skip further processing for this reference
|
|
383
|
+
// In case of partial bundling, we still need to ensure that all dependencies
|
|
384
|
+
// of the local reference are bundled to create a complete and self-contained partial bundle
|
|
385
|
+
// This is important to maintain the integrity of the partial bundle
|
|
386
|
+
const localRef = convertToLocalRef(ref, id ?? origin, schemas);
|
|
387
|
+
if (localRef !== undefined) {
|
|
388
|
+
if (isPartialBundling) {
|
|
389
|
+
const segments = getSegmentsFromPath(`/${localRef}`);
|
|
390
|
+
const parent = segments.length > 0 ? getValueByPath(documentRoot, segments.slice(0, -1)).value : undefined;
|
|
391
|
+
const targetValue = getValueByPath(documentRoot, segments);
|
|
392
|
+
// When doing partial bundling, we need to recursively bundle all dependencies
|
|
393
|
+
// referenced by this local reference to ensure the partial bundle is complete.
|
|
394
|
+
// This includes not just the direct reference but also all its dependencies,
|
|
395
|
+
// creating a complete and self-contained partial bundle.
|
|
396
|
+
await bundler(targetValue.value, targetValue.context, isChunkParent, depth + 1, segments, parent);
|
|
397
|
+
}
|
|
398
|
+
await executeHooks('onAfterNodeProcess', root, context);
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
const [prefix, path = ''] = ref.split('#', 2);
|
|
402
|
+
// Combine the current origin with the new path to resolve relative references
|
|
403
|
+
// correctly within the context of the external file being processed
|
|
404
|
+
const resolvedPath = resolveReferencePath(id ?? origin, prefix);
|
|
405
|
+
const relativePath = toRelativePath(resolvedPath, defaultOrigin);
|
|
406
|
+
// Generate a unique compressed path for the external document
|
|
407
|
+
// This is used as a key to store and reference the bundled external document
|
|
408
|
+
// The compression helps reduce the overall file size of the bundled document
|
|
409
|
+
const compressedPath = await generate(relativePath);
|
|
410
|
+
const seen = cache.has(relativePath);
|
|
411
|
+
if (!seen) {
|
|
412
|
+
cache.set(relativePath, resolveContents(resolvedPath, loaderPlugins));
|
|
413
|
+
}
|
|
414
|
+
await executeHooks('onResolveStart', root);
|
|
415
|
+
// Resolve the remote document
|
|
416
|
+
const result = await cache.get(relativePath);
|
|
417
|
+
if (result.ok) {
|
|
418
|
+
// Process the result only once to avoid duplicate processing and prevent multiple prefixing
|
|
419
|
+
// of internal references, which would corrupt the reference paths
|
|
420
|
+
if (!seen) {
|
|
421
|
+
// Skip prefixing for chunks since they are meant to be self-contained and their
|
|
422
|
+
// internal references should remain relative to their original location. Chunks
|
|
423
|
+
// are typically used for modular components that need to maintain their own
|
|
424
|
+
// reference context without being affected by the main document's structure.
|
|
425
|
+
if (!isChunk) {
|
|
426
|
+
// Update internal references in the resolved document to use the correct base path.
|
|
427
|
+
// When we embed external documents, their internal references need to be updated to
|
|
428
|
+
// maintain the correct path context relative to the main document. This is crucial
|
|
429
|
+
// because internal references in the external document are relative to its original
|
|
430
|
+
// location, but when embedded, they need to be relative to their new location in
|
|
431
|
+
// the main document's x-ext section. Without this update, internal references
|
|
432
|
+
// would point to incorrect locations and break the document structure.
|
|
433
|
+
prefixInternalRefRecursive(result.data, [extensions.externalDocuments, compressedPath]);
|
|
434
|
+
}
|
|
435
|
+
// Recursively process the resolved content
|
|
436
|
+
// to handle any nested references it may contain. We pass the resolvedPath as the new origin
|
|
437
|
+
// to ensure any relative references within this content are resolved correctly relative to
|
|
438
|
+
// their new location in the bundled document.
|
|
439
|
+
await bundler(result.data, isChunk ? origin : resolvedPath, isChunk, depth + 1, [
|
|
440
|
+
config.externalDocumentsKey,
|
|
441
|
+
compressedPath,
|
|
442
|
+
documentRoot[config.externalDocumentsMappingsKey],
|
|
443
|
+
]);
|
|
444
|
+
// Store the mapping between hashed keys and original URLs in x-ext-urls
|
|
445
|
+
// This allows tracking which external URLs were bundled and their corresponding locations
|
|
446
|
+
setValueAtPath(documentRoot, `/${config.externalDocumentsMappingsKey}/${escapeJsonPointer(compressedPath)}`, relativePath);
|
|
447
|
+
}
|
|
448
|
+
if (config.treeShake === true) {
|
|
449
|
+
// Store only the subtree that is actually used
|
|
450
|
+
// This optimizes the bundle size by only including the parts of the external document
|
|
451
|
+
// that are referenced, rather than the entire document
|
|
452
|
+
resolveAndCopyReferences(documentRoot, { [config.externalDocumentsKey]: { [compressedPath]: result.data } }, prefixInternalRef(`#${path}`, [config.externalDocumentsKey, compressedPath]).substring(1), config.externalDocumentsKey, compressedPath);
|
|
453
|
+
}
|
|
454
|
+
else if (!seen) {
|
|
455
|
+
// Store the external document in the main document's x-ext key
|
|
456
|
+
// When tree shaking is disabled, we include the entire external document
|
|
457
|
+
// This preserves all content and is faster since we don't need to analyze and copy
|
|
458
|
+
// specific parts. This approach is ideal when storing the result in memory
|
|
459
|
+
// as it avoids the overhead of tree shaking operations
|
|
460
|
+
setValueAtPath(documentRoot, `/${config.externalDocumentsKey}/${compressedPath}`, result.data);
|
|
461
|
+
}
|
|
462
|
+
// Update the $ref to point to the embedded document in x-ext
|
|
463
|
+
// This is necessary because we need to maintain the correct path context
|
|
464
|
+
// for the embedded document while preserving its internal structure
|
|
465
|
+
root.$ref = prefixInternalRef(`#${path}`, [config.externalDocumentsKey, compressedPath]);
|
|
466
|
+
await executeHooks('onResolveSuccess', root);
|
|
467
|
+
await executeHooks('onAfterNodeProcess', root, context);
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
await executeHooks('onResolveError', root);
|
|
471
|
+
await executeHooks('onAfterNodeProcess', root, context);
|
|
472
|
+
return console.warn(`Failed to resolve external reference "${resolvedPath}". The reference may be invalid, inaccessible, or missing a loader for this type of reference.`);
|
|
473
|
+
}
|
|
474
|
+
// Recursively traverse all child properties of the current object to resolve nested $ref references.
|
|
475
|
+
// This step ensures that any $refs located deeper within the object hierarchy are discovered and processed.
|
|
476
|
+
// We explicitly skip the extension keys (x-ext and x-ext-urls) to avoid reprocessing already bundled or mapped content.
|
|
477
|
+
for (const key in root) {
|
|
478
|
+
if (key === config.externalDocumentsKey || key === config.externalDocumentsMappingsKey) {
|
|
479
|
+
continue;
|
|
480
|
+
}
|
|
481
|
+
await bundler(root[key], id ?? origin, isChunkParent, depth + 1, [...currentPath, key], root);
|
|
482
|
+
}
|
|
483
|
+
await executeHooks('onAfterNodeProcess', root, context);
|
|
484
|
+
};
|
|
485
|
+
await bundler(rawSpecification);
|
|
486
|
+
// Keep urlMappings when doing partial bundling to track hash values and handle collisions
|
|
487
|
+
// For full bundling without urlMap config, remove the mappings to clean up the output
|
|
488
|
+
if (!config.urlMap && !isPartialBundling) {
|
|
489
|
+
// Remove the external document mappings from the output when doing a full bundle without urlMap config
|
|
490
|
+
delete documentRoot[config.externalDocumentsMappingsKey];
|
|
250
491
|
}
|
|
251
|
-
|
|
252
|
-
};
|
|
253
|
-
await bundler(rawSpecification);
|
|
254
|
-
if (!config.urlMap && !isPartialBundling) {
|
|
255
|
-
delete documentRoot[config.externalDocumentsMappingsKey];
|
|
256
|
-
}
|
|
257
|
-
return rawSpecification;
|
|
492
|
+
return rawSpecification;
|
|
258
493
|
}
|
|
259
|
-
export {
|
|
260
|
-
bundle,
|
|
261
|
-
extensions,
|
|
262
|
-
isLocalRef,
|
|
263
|
-
prefixInternalRef,
|
|
264
|
-
prefixInternalRefRecursive,
|
|
265
|
-
resolveAndCopyReferences
|
|
266
|
-
};
|
|
267
|
-
//# sourceMappingURL=bundle.js.map
|