@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.
Files changed (72) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/bundle/bundle.js +476 -250
  3. package/dist/bundle/index.js +1 -6
  4. package/dist/bundle/plugins/browser.js +4 -9
  5. package/dist/bundle/plugins/fetch-urls/index.js +76 -49
  6. package/dist/bundle/plugins/node.js +5 -11
  7. package/dist/bundle/plugins/parse-json/index.js +30 -23
  8. package/dist/bundle/plugins/parse-yaml/index.js +31 -24
  9. package/dist/bundle/plugins/read-files/index.js +50 -29
  10. package/dist/bundle/value-generator.js +118 -49
  11. package/dist/dereference/dereference.js +66 -36
  12. package/dist/dereference/index.js +1 -5
  13. package/dist/diff/apply.js +69 -36
  14. package/dist/diff/diff.js +68 -32
  15. package/dist/diff/index.js +4 -9
  16. package/dist/diff/merge.js +122 -60
  17. package/dist/diff/trie.js +100 -77
  18. package/dist/diff/utils.js +96 -41
  19. package/dist/helpers/convert-to-local-ref.js +30 -23
  20. package/dist/helpers/escape-json-pointer.js +7 -6
  21. package/dist/helpers/get-schemas.js +59 -32
  22. package/dist/helpers/get-segments-from-path.js +13 -9
  23. package/dist/helpers/get-value-by-path.js +38 -22
  24. package/dist/helpers/is-file-path.js +19 -9
  25. package/dist/helpers/is-http-url.js +20 -11
  26. package/dist/helpers/is-json-object.js +29 -15
  27. package/dist/helpers/is-yaml.js +17 -6
  28. package/dist/helpers/json-path-utils.js +30 -15
  29. package/dist/helpers/normalize.js +27 -26
  30. package/dist/helpers/resolve-reference-path.js +28 -16
  31. package/dist/helpers/set-value-at-path.js +72 -26
  32. package/dist/helpers/to-relative-path.js +40 -28
  33. package/dist/helpers/unescape-json-pointer.js +8 -6
  34. package/dist/magic-proxy/index.js +1 -6
  35. package/dist/magic-proxy/proxy.js +245 -186
  36. package/dist/types.js +1 -1
  37. package/package.json +5 -7
  38. package/dist/bundle/bundle.js.map +0 -7
  39. package/dist/bundle/index.js.map +0 -7
  40. package/dist/bundle/plugins/browser.js.map +0 -7
  41. package/dist/bundle/plugins/fetch-urls/index.js.map +0 -7
  42. package/dist/bundle/plugins/node.js.map +0 -7
  43. package/dist/bundle/plugins/parse-json/index.js.map +0 -7
  44. package/dist/bundle/plugins/parse-yaml/index.js.map +0 -7
  45. package/dist/bundle/plugins/read-files/index.js.map +0 -7
  46. package/dist/bundle/value-generator.js.map +0 -7
  47. package/dist/dereference/dereference.js.map +0 -7
  48. package/dist/dereference/index.js.map +0 -7
  49. package/dist/diff/apply.js.map +0 -7
  50. package/dist/diff/diff.js.map +0 -7
  51. package/dist/diff/index.js.map +0 -7
  52. package/dist/diff/merge.js.map +0 -7
  53. package/dist/diff/trie.js.map +0 -7
  54. package/dist/diff/utils.js.map +0 -7
  55. package/dist/helpers/convert-to-local-ref.js.map +0 -7
  56. package/dist/helpers/escape-json-pointer.js.map +0 -7
  57. package/dist/helpers/get-schemas.js.map +0 -7
  58. package/dist/helpers/get-segments-from-path.js.map +0 -7
  59. package/dist/helpers/get-value-by-path.js.map +0 -7
  60. package/dist/helpers/is-file-path.js.map +0 -7
  61. package/dist/helpers/is-http-url.js.map +0 -7
  62. package/dist/helpers/is-json-object.js.map +0 -7
  63. package/dist/helpers/is-yaml.js.map +0 -7
  64. package/dist/helpers/json-path-utils.js.map +0 -7
  65. package/dist/helpers/normalize.js.map +0 -7
  66. package/dist/helpers/resolve-reference-path.js.map +0 -7
  67. package/dist/helpers/set-value-at-path.js.map +0 -7
  68. package/dist/helpers/to-relative-path.js.map +0 -7
  69. package/dist/helpers/unescape-json-pointer.js.map +0 -7
  70. package/dist/magic-proxy/index.js.map +0 -7
  71. package/dist/magic-proxy/proxy.js.map +0 -7
  72. package/dist/types.js.map +0 -7
@@ -1,267 +1,493 @@
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
- const hasRef = (value) => isObject(value) && "$ref" in value && typeof value["$ref"] === "string";
14
- function isLocalRef(value) {
15
- return value.startsWith("#");
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
- const plugin = plugins.find((p) => p.validate(value));
19
- if (plugin) {
20
- return plugin.exec(value);
21
- }
22
- return Promise.resolve({
23
- ok: false
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
- function prefixInternalRefRecursive(input, prefix) {
33
- if (Array.isArray(input)) {
34
- input.forEach((el) => prefixInternalRefRecursive(el, prefix));
35
- return;
36
- }
37
- if (!isObject(input)) {
38
- return;
39
- }
40
- Object.values(input).forEach((el) => prefixInternalRefRecursive(el, prefix));
41
- if (typeof input === "object" && "$ref" in input && typeof input["$ref"] === "string") {
42
- const ref = input["$ref"];
43
- if (!isLocalRef(ref)) {
44
- return;
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
- input["$ref"] = prefixInternalRef(ref, prefix);
47
- }
67
+ return `#/${prefix.map(escapeJsonPointer).join('/')}${input.substring(1)}`;
48
68
  }
49
- const resolveAndCopyReferences = (targetDocument, sourceDocument, referencePath, externalRefsKey, documentKey, bundleLocalRefs = false, processedNodes = /* @__PURE__ */ new Set()) => {
50
- const referencedValue = getValueByPath(sourceDocument, getSegmentsFromPath(referencePath)).value;
51
- if (processedNodes.has(referencedValue)) {
52
- return;
53
- }
54
- processedNodes.add(referencedValue);
55
- setValueAtPath(targetDocument, referencePath, referencedValue);
56
- const traverse = (node) => {
57
- if (!node || typeof node !== "object") {
58
- return;
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
- if ("$ref" in node && typeof node["$ref"] === "string") {
61
- if (node["$ref"].startsWith(`#/${externalRefsKey}/${escapeJsonPointer(documentKey)}`)) {
62
- resolveAndCopyReferences(
63
- targetDocument,
64
- sourceDocument,
65
- node["$ref"].substring(1),
66
- externalRefsKey,
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
- for (const value of Object.values(node)) {
84
- traverse(value);
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
- traverse(referencedValue);
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
- const extensions = {
90
- /**
91
- * Custom OpenAPI extension key used to store external references.
92
- * This key will contain all bundled external documents.
93
- * The x-ext key is used to maintain a clean separation between the main
94
- * OpenAPI document and its bundled external references.
95
- */
96
- externalDocuments: "x-ext",
97
- /**
98
- * Custom OpenAPI extension key used to maintain a mapping between
99
- * hashed keys and their original URLs in x-ext.
100
- * This mapping is essential for tracking the source of bundled references
101
- */
102
- externalDocumentsMappings: "x-ext-urls"
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
- async function bundle(input, config) {
105
- config.externalDocumentsKey = config.externalDocumentsKey ?? extensions.externalDocuments;
106
- config.externalDocumentsMappingsKey = config.externalDocumentsMappingsKey ?? extensions.externalDocumentsMappings;
107
- const cache = config.cache ?? /* @__PURE__ */ new Map();
108
- const loaderPlugins = config.plugins.filter((it) => it.type === "loader");
109
- const lifecyclePlugin = config.plugins.filter((it) => it.type === "lifecycle");
110
- const resolveInput = async () => {
111
- if (typeof input !== "string") {
112
- return input;
113
- }
114
- const result = await resolveContents(input, loaderPlugins);
115
- if (result.ok && typeof result.data === "object") {
116
- return result.data;
117
- }
118
- throw new Error(
119
- "Failed to resolve input: Please provide a valid string value or pass a loader to process the input"
120
- );
121
- };
122
- const rawSpecification = await resolveInput();
123
- const documentRoot = config.root ?? rawSpecification;
124
- const schemas = getSchemas(documentRoot);
125
- const isPartialBundling = config.root !== void 0 && config.root !== rawSpecification || config.depth !== void 0;
126
- const processedNodes = config.visitedNodes ?? /* @__PURE__ */ new Set();
127
- const getDefaultOrigin = () => {
128
- const id = getId(documentRoot);
129
- if (id) {
130
- return id;
131
- }
132
- if (config.origin) {
133
- return config.origin;
134
- }
135
- if (typeof input !== "string") {
136
- return "/";
137
- }
138
- if (isHttpUrl(input) || isFilePath(input)) {
139
- return input;
140
- }
141
- return "/";
142
- };
143
- const defaultOrigin = getDefaultOrigin();
144
- if (documentRoot[config.externalDocumentsMappingsKey] === void 0) {
145
- documentRoot[config.externalDocumentsMappingsKey] = {};
146
- }
147
- const { generate } = uniqueValueGeneratorFactory(
148
- config.compress ?? getHash,
149
- documentRoot[config.externalDocumentsMappingsKey]
150
- );
151
- const executeHooks = async (type, ...args) => {
152
- const hook = config.hooks?.[type];
153
- if (hook) {
154
- await hook(...args);
155
- }
156
- for (const plugin of lifecyclePlugin) {
157
- const pluginHook = plugin[type];
158
- if (pluginHook) {
159
- await pluginHook(...args);
160
- }
161
- }
162
- };
163
- const bundler = async (root, origin = defaultOrigin, isChunkParent = false, depth = 0, currentPath = [], parent = null) => {
164
- if (config.depth !== void 0 && depth > config.depth) {
165
- return;
166
- }
167
- if (!isObject(root) && !Array.isArray(root)) {
168
- return;
169
- }
170
- if (processedNodes.has(root)) {
171
- return;
172
- }
173
- processedNodes.add(root);
174
- const context = {
175
- path: currentPath,
176
- resolutionCache: cache,
177
- parentNode: parent,
178
- rootNode: documentRoot,
179
- loaders: loaderPlugins
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
- await executeHooks("onBeforeNodeProcess", root, context);
182
- const id = getId(root);
183
- if (hasRef(root)) {
184
- const ref = root["$ref"];
185
- const isChunk = "$global" in root && typeof root["$global"] === "boolean" && root["$global"];
186
- const localRef = convertToLocalRef(ref, id ?? origin, schemas);
187
- if (localRef !== void 0) {
188
- if (isPartialBundling) {
189
- const segments = getSegmentsFromPath(`/${localRef}`);
190
- const parent2 = segments.length > 0 ? getValueByPath(documentRoot, segments.slice(0, -1)).value : void 0;
191
- const targetValue = getValueByPath(documentRoot, segments);
192
- await bundler(targetValue.value, targetValue.context, isChunkParent, depth + 1, segments, parent2);
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
- await executeHooks("onAfterNodeProcess", root, context);
195
- return;
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 (config.treeShake === true) {
224
- resolveAndCopyReferences(
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
- root.$ref = prefixInternalRef(`#${path}`, [config.externalDocumentsKey, compressedPath]);
235
- await executeHooks("onResolveSuccess", root);
236
- await executeHooks("onAfterNodeProcess", root, context);
237
- return;
238
- }
239
- await executeHooks("onResolveError", root);
240
- await executeHooks("onAfterNodeProcess", root, context);
241
- return console.warn(
242
- `Failed to resolve external reference "${resolvedPath}". The reference may be invalid, inaccessible, or missing a loader for this type of reference.`
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
- for (const key in root) {
246
- if (key === config.externalDocumentsKey || key === config.externalDocumentsMappingsKey) {
247
- continue;
248
- }
249
- await bundler(root[key], id ?? origin, isChunkParent, depth + 1, [...currentPath, key], root);
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
- await executeHooks("onAfterNodeProcess", root, context);
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