@hey-api/json-schema-ref-parser 1.0.8 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/__tests__/bundle.test.js +8 -11
- package/dist/lib/__tests__/pointer.test.js +5 -4
- package/dist/lib/bundle.d.ts +1 -0
- package/dist/lib/bundle.js +336 -80
- package/dist/lib/dereference.js +4 -2
- package/dist/lib/index.d.ts +2 -2
- package/dist/lib/index.js +11 -11
- package/lib/__tests__/bundle.test.ts +9 -15
- package/lib/__tests__/pointer.test.ts +5 -4
- package/lib/__tests__/spec/multiple-refs.json +2 -2
- package/lib/__tests__/spec/path-parameter.json +12 -8
- package/lib/bundle.ts +411 -80
- package/lib/dereference.ts +5 -6
- package/lib/index.ts +18 -18
- package/package.json +1 -1
|
@@ -17,19 +17,16 @@ const __1 = require("..");
|
|
|
17
17
|
const refParser = new __1.$RefParser();
|
|
18
18
|
const pathOrUrlOrSchema = path_1.default.resolve("lib", "__tests__", "spec", "multiple-refs.json");
|
|
19
19
|
const schema = (await refParser.bundle({ pathOrUrlOrSchema }));
|
|
20
|
-
//
|
|
21
|
-
(0, vitest_1.expect)(schema.paths["/test1/{pathId}"].get.parameters[0].name).toBe("pathId");
|
|
22
|
-
(0, vitest_1.expect)(schema.paths["/test1/{pathId}"].get.parameters[0].schema.type).toBe("string");
|
|
23
|
-
(0, vitest_1.expect)(schema.paths["/test1/{pathId}"].get.parameters[0].schema.format).toBe("uuid");
|
|
24
|
-
(0, vitest_1.expect)(schema.paths["/test1/{pathId}"].get.parameters[0].$ref).toBeUndefined();
|
|
25
|
-
// Second reference should be remapped to point to the first reference
|
|
26
|
-
(0, vitest_1.expect)(schema.paths["/test2/{pathId}"].get.parameters[0].$ref).toBe("#/paths/~1test1~1%7BpathId%7D/get/parameters/0");
|
|
27
|
-
// Both should effectively resolve to the same data
|
|
20
|
+
// Both parameters should now be $ref to the same internal definition
|
|
28
21
|
const firstParam = schema.paths["/test1/{pathId}"].get.parameters[0];
|
|
29
22
|
const secondParam = schema.paths["/test2/{pathId}"].get.parameters[0];
|
|
30
|
-
// The
|
|
31
|
-
(0, vitest_1.expect)(
|
|
32
|
-
(0, vitest_1.expect)(
|
|
23
|
+
// The $ref should match the output structure in file_context_0
|
|
24
|
+
(0, vitest_1.expect)(firstParam.$ref).toBe("#/components/parameters/path-parameter_pathId");
|
|
25
|
+
(0, vitest_1.expect)(secondParam.$ref).toBe("#/components/parameters/path-parameter_pathId");
|
|
26
|
+
// The referenced parameter should exist and match the expected structure
|
|
27
|
+
(0, vitest_1.expect)(schema.components).toBeDefined();
|
|
28
|
+
(0, vitest_1.expect)(schema.components.parameters).toBeDefined();
|
|
29
|
+
(0, vitest_1.expect)(schema.components.parameters["path-parameter_pathId"]).toEqual({
|
|
33
30
|
name: "pathId",
|
|
34
31
|
in: "path",
|
|
35
32
|
required: true,
|
|
@@ -11,6 +11,7 @@ const path_1 = __importDefault(require("path"));
|
|
|
11
11
|
const refParser = new __1.$RefParser();
|
|
12
12
|
const pathOrUrlOrSchema = path_1.default.resolve("lib", "__tests__", "spec", "openapi-paths-ref.json");
|
|
13
13
|
const schema = (await refParser.bundle({ pathOrUrlOrSchema }));
|
|
14
|
+
console.log(JSON.stringify(schema, null, 2));
|
|
14
15
|
// The GET endpoint should have its schema defined inline
|
|
15
16
|
const getSchema = schema.paths["/foo"].get.responses["200"].content["application/json"].schema;
|
|
16
17
|
(0, vitest_1.expect)(getSchema.$ref).toBeUndefined();
|
|
@@ -18,10 +19,10 @@ const path_1 = __importDefault(require("path"));
|
|
|
18
19
|
(0, vitest_1.expect)(getSchema.properties.bar.type).toBe("string");
|
|
19
20
|
// The POST endpoint should have its schema inlined (copied) instead of a $ref
|
|
20
21
|
const postSchema = schema.paths["/foo"].post.responses["200"].content["application/json"].schema;
|
|
21
|
-
(0, vitest_1.expect)(postSchema.$ref).
|
|
22
|
-
(0, vitest_1.expect)(postSchema.type).
|
|
23
|
-
(0, vitest_1.expect)(postSchema.properties
|
|
22
|
+
(0, vitest_1.expect)(postSchema.$ref).toBe("#/paths/~1foo/get/responses/200/content/application~1json/schema");
|
|
23
|
+
(0, vitest_1.expect)(postSchema.type).toBeUndefined();
|
|
24
|
+
(0, vitest_1.expect)(postSchema.properties?.bar?.type).toBeUndefined();
|
|
24
25
|
// Both schemas should be identical objects
|
|
25
|
-
(0, vitest_1.expect)(postSchema).
|
|
26
|
+
(0, vitest_1.expect)(postSchema).not.toBe(getSchema);
|
|
26
27
|
});
|
|
27
28
|
});
|
package/dist/lib/bundle.d.ts
CHANGED
|
@@ -13,6 +13,7 @@ export interface InventoryEntry {
|
|
|
13
13
|
parent: any;
|
|
14
14
|
pathFromRoot: any;
|
|
15
15
|
value: any;
|
|
16
|
+
originalContainerType?: "schemas" | "parameters" | "requestBodies" | "responses" | "headers";
|
|
16
17
|
}
|
|
17
18
|
/**
|
|
18
19
|
* Bundles all external JSON references into the main JSON schema, thus resulting in a schema that
|
package/dist/lib/bundle.js
CHANGED
|
@@ -37,34 +37,113 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
37
37
|
};
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
39
|
exports.bundle = void 0;
|
|
40
|
-
const isEqual_1 = __importDefault(require("lodash/isEqual"));
|
|
41
40
|
const ref_js_1 = __importDefault(require("./ref.js"));
|
|
42
41
|
const pointer_js_1 = __importDefault(require("./pointer.js"));
|
|
43
42
|
const url = __importStar(require("./util/url.js"));
|
|
43
|
+
const DEBUG_PERFORMANCE = process.env.DEBUG === "true" ||
|
|
44
|
+
(typeof globalThis !== "undefined" && globalThis.DEBUG_BUNDLE_PERFORMANCE === true);
|
|
45
|
+
const perf = {
|
|
46
|
+
mark: (name) => DEBUG_PERFORMANCE && performance.mark(name),
|
|
47
|
+
measure: (name, start, end) => DEBUG_PERFORMANCE && performance.measure(name, start, end),
|
|
48
|
+
log: (message, ...args) => DEBUG_PERFORMANCE && console.log("[PERF] " + message, ...args),
|
|
49
|
+
warn: (message, ...args) => DEBUG_PERFORMANCE && console.warn("[PERF] " + message, ...args),
|
|
50
|
+
};
|
|
44
51
|
/**
|
|
45
|
-
*
|
|
52
|
+
* Fast lookup using Map instead of linear search with deep equality
|
|
46
53
|
*/
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
54
|
+
const createInventoryLookup = () => {
|
|
55
|
+
const lookup = new Map();
|
|
56
|
+
const objectIds = new WeakMap(); // Use WeakMap to avoid polluting objects
|
|
57
|
+
let idCounter = 0;
|
|
58
|
+
let lookupCount = 0;
|
|
59
|
+
let addCount = 0;
|
|
60
|
+
const getObjectId = (obj) => {
|
|
61
|
+
if (!objectIds.has(obj)) {
|
|
62
|
+
objectIds.set(obj, `obj_${++idCounter}`);
|
|
55
63
|
}
|
|
64
|
+
return objectIds.get(obj);
|
|
65
|
+
};
|
|
66
|
+
const createInventoryKey = ($refParent, $refKey) => {
|
|
67
|
+
// Use WeakMap-based lookup to avoid polluting the actual schema objects
|
|
68
|
+
return `${getObjectId($refParent)}_${$refKey}`;
|
|
69
|
+
};
|
|
70
|
+
return {
|
|
71
|
+
add: (entry) => {
|
|
72
|
+
addCount++;
|
|
73
|
+
const key = createInventoryKey(entry.parent, entry.key);
|
|
74
|
+
lookup.set(key, entry);
|
|
75
|
+
if (addCount % 100 === 0) {
|
|
76
|
+
perf.log(`Inventory lookup: Added ${addCount} entries, map size: ${lookup.size}`);
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
find: ($refParent, $refKey) => {
|
|
80
|
+
lookupCount++;
|
|
81
|
+
const key = createInventoryKey($refParent, $refKey);
|
|
82
|
+
const result = lookup.get(key);
|
|
83
|
+
if (lookupCount % 100 === 0) {
|
|
84
|
+
perf.log(`Inventory lookup: ${lookupCount} lookups performed`);
|
|
85
|
+
}
|
|
86
|
+
return result;
|
|
87
|
+
},
|
|
88
|
+
remove: (entry) => {
|
|
89
|
+
const key = createInventoryKey(entry.parent, entry.key);
|
|
90
|
+
lookup.delete(key);
|
|
91
|
+
},
|
|
92
|
+
getStats: () => ({ lookupCount, addCount, mapSize: lookup.size }),
|
|
93
|
+
};
|
|
94
|
+
};
|
|
95
|
+
/**
|
|
96
|
+
* Determine the container type from a JSON Pointer path.
|
|
97
|
+
* Analyzes the path tokens to identify the appropriate OpenAPI component container.
|
|
98
|
+
*
|
|
99
|
+
* @param path - The JSON Pointer path to analyze
|
|
100
|
+
* @returns The container type: "schemas", "parameters", "requestBodies", "responses", or "headers"
|
|
101
|
+
*/
|
|
102
|
+
const getContainerTypeFromPath = (path) => {
|
|
103
|
+
const tokens = pointer_js_1.default.parse(path);
|
|
104
|
+
const has = (t) => tokens.includes(t);
|
|
105
|
+
// Prefer more specific containers first
|
|
106
|
+
if (has("parameters")) {
|
|
107
|
+
return "parameters";
|
|
108
|
+
}
|
|
109
|
+
if (has("requestBody")) {
|
|
110
|
+
return "requestBodies";
|
|
111
|
+
}
|
|
112
|
+
if (has("headers")) {
|
|
113
|
+
return "headers";
|
|
114
|
+
}
|
|
115
|
+
if (has("responses")) {
|
|
116
|
+
return "responses";
|
|
117
|
+
}
|
|
118
|
+
if (has("schema")) {
|
|
119
|
+
return "schemas";
|
|
56
120
|
}
|
|
57
|
-
|
|
121
|
+
// default: treat as schema-like
|
|
122
|
+
return "schemas";
|
|
58
123
|
};
|
|
59
124
|
/**
|
|
60
125
|
* Inventories the given JSON Reference (i.e. records detailed information about it so we can
|
|
61
126
|
* optimize all $refs in the schema), and then crawls the resolved value.
|
|
62
127
|
*/
|
|
63
|
-
const inventory$Ref = ({ $refKey, $refParent, $refs, indirections, inventory, options, path, pathFromRoot, }) => {
|
|
128
|
+
const inventory$Ref = ({ $refKey, $refParent, $refs, indirections, inventory, inventoryLookup, options, path, pathFromRoot, visitedObjects = new WeakSet(), resolvedRefs = new Map(), }) => {
|
|
129
|
+
perf.mark("inventory-ref-start");
|
|
64
130
|
const $ref = $refKey === null ? $refParent : $refParent[$refKey];
|
|
65
131
|
const $refPath = url.resolve(path, $ref.$ref);
|
|
66
|
-
|
|
132
|
+
// Check cache first to avoid redundant resolution
|
|
133
|
+
let pointer = resolvedRefs.get($refPath);
|
|
134
|
+
if (!pointer) {
|
|
135
|
+
perf.mark("resolve-start");
|
|
136
|
+
pointer = $refs._resolve($refPath, pathFromRoot, options);
|
|
137
|
+
perf.mark("resolve-end");
|
|
138
|
+
perf.measure("resolve-time", "resolve-start", "resolve-end");
|
|
139
|
+
if (pointer) {
|
|
140
|
+
resolvedRefs.set($refPath, pointer);
|
|
141
|
+
perf.log(`Cached resolved $ref: ${$refPath}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
67
144
|
if (pointer === null) {
|
|
145
|
+
perf.mark("inventory-ref-end");
|
|
146
|
+
perf.measure("inventory-ref-time", "inventory-ref-start", "inventory-ref-end");
|
|
68
147
|
return;
|
|
69
148
|
}
|
|
70
149
|
const parsed = pointer_js_1.default.parse(pathFromRoot);
|
|
@@ -75,17 +154,23 @@ const inventory$Ref = ({ $refKey, $refParent, $refs, indirections, inventory, op
|
|
|
75
154
|
const extended = ref_js_1.default.isExtended$Ref($ref);
|
|
76
155
|
indirections += pointer.indirections;
|
|
77
156
|
// Check if this exact location (parent + key + pathFromRoot) has already been inventoried
|
|
78
|
-
|
|
157
|
+
perf.mark("lookup-start");
|
|
158
|
+
const existingEntry = inventoryLookup.find($refParent, $refKey);
|
|
159
|
+
perf.mark("lookup-end");
|
|
160
|
+
perf.measure("lookup-time", "lookup-start", "lookup-end");
|
|
79
161
|
if (existingEntry && existingEntry.pathFromRoot === pathFromRoot) {
|
|
80
162
|
// This exact location has already been inventoried, so we don't need to process it again
|
|
81
163
|
if (depth < existingEntry.depth || indirections < existingEntry.indirections) {
|
|
82
164
|
removeFromInventory(inventory, existingEntry);
|
|
165
|
+
inventoryLookup.remove(existingEntry);
|
|
83
166
|
}
|
|
84
167
|
else {
|
|
168
|
+
perf.mark("inventory-ref-end");
|
|
169
|
+
perf.measure("inventory-ref-time", "inventory-ref-start", "inventory-ref-end");
|
|
85
170
|
return;
|
|
86
171
|
}
|
|
87
172
|
}
|
|
88
|
-
|
|
173
|
+
const newEntry = {
|
|
89
174
|
$ref, // The JSON Reference (e.g. {$ref: string})
|
|
90
175
|
circular: pointer.circular, // Is this $ref pointer DIRECTLY circular? (i.e. it references itself)
|
|
91
176
|
depth, // How far from the JSON Schema root is this $ref pointer?
|
|
@@ -98,9 +183,14 @@ const inventory$Ref = ({ $refKey, $refParent, $refs, indirections, inventory, op
|
|
|
98
183
|
parent: $refParent, // The object that contains this $ref pointer
|
|
99
184
|
pathFromRoot, // The path to the $ref pointer, from the JSON Schema root
|
|
100
185
|
value: pointer.value, // The resolved value of the $ref pointer
|
|
101
|
-
|
|
186
|
+
originalContainerType: external ? getContainerTypeFromPath(pointer.path) : undefined, // The original container type in the external file
|
|
187
|
+
};
|
|
188
|
+
inventory.push(newEntry);
|
|
189
|
+
inventoryLookup.add(newEntry);
|
|
190
|
+
perf.log(`Inventoried $ref: ${$ref.$ref} -> ${file}${hash} (external: ${external}, depth: ${depth})`);
|
|
102
191
|
// Recursively crawl the resolved value
|
|
103
192
|
if (!existingEntry || external) {
|
|
193
|
+
perf.mark("crawl-recursive-start");
|
|
104
194
|
crawl({
|
|
105
195
|
parent: pointer.value,
|
|
106
196
|
key: null,
|
|
@@ -108,18 +198,31 @@ const inventory$Ref = ({ $refKey, $refParent, $refs, indirections, inventory, op
|
|
|
108
198
|
pathFromRoot,
|
|
109
199
|
indirections: indirections + 1,
|
|
110
200
|
inventory,
|
|
201
|
+
inventoryLookup,
|
|
111
202
|
$refs,
|
|
112
203
|
options,
|
|
204
|
+
visitedObjects,
|
|
205
|
+
resolvedRefs,
|
|
113
206
|
});
|
|
207
|
+
perf.mark("crawl-recursive-end");
|
|
208
|
+
perf.measure("crawl-recursive-time", "crawl-recursive-start", "crawl-recursive-end");
|
|
114
209
|
}
|
|
210
|
+
perf.mark("inventory-ref-end");
|
|
211
|
+
perf.measure("inventory-ref-time", "inventory-ref-start", "inventory-ref-end");
|
|
115
212
|
};
|
|
116
213
|
/**
|
|
117
214
|
* Recursively crawls the given value, and inventories all JSON references.
|
|
118
215
|
*/
|
|
119
|
-
const crawl = ({ $refs, indirections, inventory, key, options, parent, path, pathFromRoot, }) => {
|
|
216
|
+
const crawl = ({ $refs, indirections, inventory, inventoryLookup, key, options, parent, path, pathFromRoot, visitedObjects = new WeakSet(), resolvedRefs = new Map(), }) => {
|
|
120
217
|
const obj = key === null ? parent : parent[key];
|
|
121
218
|
if (obj && typeof obj === "object" && !ArrayBuffer.isView(obj)) {
|
|
219
|
+
// Early exit if we've already processed this exact object
|
|
220
|
+
if (visitedObjects.has(obj)) {
|
|
221
|
+
perf.log(`Skipping already visited object at ${pathFromRoot}`);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
122
224
|
if (ref_js_1.default.isAllowed$Ref(obj)) {
|
|
225
|
+
perf.log(`Found $ref at ${pathFromRoot}: ${obj.$ref}`);
|
|
123
226
|
inventory$Ref({
|
|
124
227
|
$refParent: parent,
|
|
125
228
|
$refKey: key,
|
|
@@ -127,11 +230,16 @@ const crawl = ({ $refs, indirections, inventory, key, options, parent, path, pat
|
|
|
127
230
|
pathFromRoot,
|
|
128
231
|
indirections,
|
|
129
232
|
inventory,
|
|
233
|
+
inventoryLookup,
|
|
130
234
|
$refs,
|
|
131
235
|
options,
|
|
236
|
+
visitedObjects,
|
|
237
|
+
resolvedRefs,
|
|
132
238
|
});
|
|
133
239
|
}
|
|
134
240
|
else {
|
|
241
|
+
// Mark this object as visited BEFORE processing its children
|
|
242
|
+
visitedObjects.add(obj);
|
|
135
243
|
// Crawl the object in a specific order that's optimized for bundling.
|
|
136
244
|
// This is important because it determines how `pathFromRoot` gets built,
|
|
137
245
|
// which later determines which keys get dereferenced and which ones get remapped
|
|
@@ -162,8 +270,11 @@ const crawl = ({ $refs, indirections, inventory, key, options, parent, path, pat
|
|
|
162
270
|
pathFromRoot: keyPathFromRoot,
|
|
163
271
|
indirections,
|
|
164
272
|
inventory,
|
|
273
|
+
inventoryLookup,
|
|
165
274
|
$refs,
|
|
166
275
|
options,
|
|
276
|
+
visitedObjects,
|
|
277
|
+
resolvedRefs,
|
|
167
278
|
});
|
|
168
279
|
}
|
|
169
280
|
else {
|
|
@@ -174,8 +285,11 @@ const crawl = ({ $refs, indirections, inventory, key, options, parent, path, pat
|
|
|
174
285
|
pathFromRoot: keyPathFromRoot,
|
|
175
286
|
indirections,
|
|
176
287
|
inventory,
|
|
288
|
+
inventoryLookup,
|
|
177
289
|
$refs,
|
|
178
290
|
options,
|
|
291
|
+
visitedObjects,
|
|
292
|
+
resolvedRefs,
|
|
179
293
|
});
|
|
180
294
|
}
|
|
181
295
|
}
|
|
@@ -183,29 +297,15 @@ const crawl = ({ $refs, indirections, inventory, key, options, parent, path, pat
|
|
|
183
297
|
}
|
|
184
298
|
};
|
|
185
299
|
/**
|
|
186
|
-
*
|
|
187
|
-
*
|
|
188
|
-
* value are re-mapped to point to the first reference.
|
|
189
|
-
*
|
|
190
|
-
* @example: {
|
|
191
|
-
* first: { $ref: somefile.json#/some/part },
|
|
192
|
-
* second: { $ref: somefile.json#/another/part },
|
|
193
|
-
* third: { $ref: somefile.json },
|
|
194
|
-
* fourth: { $ref: somefile.json#/some/part/sub/part }
|
|
195
|
-
* }
|
|
196
|
-
*
|
|
197
|
-
* In this example, there are four references to the same file, but since the third reference points
|
|
198
|
-
* to the ENTIRE file, that's the only one we need to dereference. The other three can just be
|
|
199
|
-
* remapped to point inside the third one.
|
|
200
|
-
*
|
|
201
|
-
* On the other hand, if the third reference DIDN'T exist, then the first and second would both need
|
|
202
|
-
* to be dereferenced, since they point to different parts of the file. The fourth reference does NOT
|
|
203
|
-
* need to be dereferenced, because it can be remapped to point inside the first one.
|
|
204
|
-
*
|
|
205
|
-
* @param inventory
|
|
300
|
+
* Remap external refs by hoisting resolved values into a shared container in the root schema
|
|
301
|
+
* and pointing all occurrences to those internal definitions. Internal refs remain internal.
|
|
206
302
|
*/
|
|
207
|
-
function remap(inventory) {
|
|
303
|
+
function remap(parser, inventory) {
|
|
304
|
+
perf.log(`Starting remap with ${inventory.length} inventory entries`);
|
|
305
|
+
perf.mark("remap-start");
|
|
306
|
+
const root = parser.schema;
|
|
208
307
|
// Group & sort all the $ref pointers, so they're in the order that we need to dereference/remap them
|
|
308
|
+
perf.mark("sort-inventory-start");
|
|
209
309
|
inventory.sort((a, b) => {
|
|
210
310
|
if (a.file !== b.file) {
|
|
211
311
|
// Group all the $refs that point to the same file
|
|
@@ -246,54 +346,174 @@ function remap(inventory) {
|
|
|
246
346
|
}
|
|
247
347
|
}
|
|
248
348
|
});
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
349
|
+
perf.mark("sort-inventory-end");
|
|
350
|
+
perf.measure("sort-inventory-time", "sort-inventory-start", "sort-inventory-end");
|
|
351
|
+
perf.log(`Sorted ${inventory.length} inventory entries`);
|
|
352
|
+
// Ensure or return a container by component type. Prefer OpenAPI-aware placement;
|
|
353
|
+
// otherwise use existing root containers; otherwise create components/*.
|
|
354
|
+
const ensureContainer = (type) => {
|
|
355
|
+
const isOas3 = !!(root && typeof root === "object" && typeof root.openapi === "string");
|
|
356
|
+
const isOas2 = !!(root && typeof root === "object" && typeof root.swagger === "string");
|
|
357
|
+
if (isOas3) {
|
|
358
|
+
if (!root.components || typeof root.components !== "object") {
|
|
359
|
+
root.components = {};
|
|
360
|
+
}
|
|
361
|
+
if (!root.components[type] || typeof root.components[type] !== "object") {
|
|
362
|
+
root.components[type] = {};
|
|
363
|
+
}
|
|
364
|
+
return { obj: root.components[type], prefix: `#/components/${type}` };
|
|
365
|
+
}
|
|
366
|
+
if (isOas2) {
|
|
367
|
+
if (type === "schemas") {
|
|
368
|
+
if (!root.definitions || typeof root.definitions !== "object") {
|
|
369
|
+
root.definitions = {};
|
|
370
|
+
}
|
|
371
|
+
return { obj: root.definitions, prefix: "#/definitions" };
|
|
372
|
+
}
|
|
373
|
+
if (type === "parameters") {
|
|
374
|
+
if (!root.parameters || typeof root.parameters !== "object") {
|
|
375
|
+
root.parameters = {};
|
|
376
|
+
}
|
|
377
|
+
return { obj: root.parameters, prefix: "#/parameters" };
|
|
378
|
+
}
|
|
379
|
+
if (type === "responses") {
|
|
380
|
+
if (!root.responses || typeof root.responses !== "object") {
|
|
381
|
+
root.responses = {};
|
|
382
|
+
}
|
|
383
|
+
return { obj: root.responses, prefix: "#/responses" };
|
|
384
|
+
}
|
|
385
|
+
// requestBodies/headers don't exist as reusable containers in OAS2; fallback to definitions
|
|
386
|
+
if (!root.definitions || typeof root.definitions !== "object") {
|
|
387
|
+
root.definitions = {};
|
|
388
|
+
}
|
|
389
|
+
return { obj: root.definitions, prefix: "#/definitions" };
|
|
255
390
|
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
391
|
+
// No explicit version: prefer existing containers
|
|
392
|
+
if (root && typeof root === "object") {
|
|
393
|
+
if (root.components && typeof root.components === "object") {
|
|
394
|
+
if (!root.components[type] || typeof root.components[type] !== "object") {
|
|
395
|
+
root.components[type] = {};
|
|
396
|
+
}
|
|
397
|
+
return { obj: root.components[type], prefix: `#/components/${type}` };
|
|
398
|
+
}
|
|
399
|
+
if (root.definitions && typeof root.definitions === "object") {
|
|
400
|
+
return { obj: root.definitions, prefix: "#/definitions" };
|
|
401
|
+
}
|
|
402
|
+
// Create components/* by default if nothing exists
|
|
403
|
+
if (!root.components || typeof root.components !== "object") {
|
|
404
|
+
root.components = {};
|
|
405
|
+
}
|
|
406
|
+
if (!root.components[type] || typeof root.components[type] !== "object") {
|
|
407
|
+
root.components[type] = {};
|
|
408
|
+
}
|
|
409
|
+
return { obj: root.components[type], prefix: `#/components/${type}` };
|
|
259
410
|
}
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
411
|
+
// Fallback
|
|
412
|
+
root.definitions = root.definitions || {};
|
|
413
|
+
return { obj: root.definitions, prefix: "#/definitions" };
|
|
414
|
+
};
|
|
415
|
+
/**
|
|
416
|
+
* Choose the appropriate component container for bundling.
|
|
417
|
+
* Prioritizes the original container type from external files over usage location.
|
|
418
|
+
*
|
|
419
|
+
* @param entry - The inventory entry containing reference information
|
|
420
|
+
* @returns The container type to use for bundling
|
|
421
|
+
*/
|
|
422
|
+
const chooseComponent = (entry) => {
|
|
423
|
+
// If we have the original container type from the external file, use it
|
|
424
|
+
if (entry.originalContainerType) {
|
|
425
|
+
return entry.originalContainerType;
|
|
263
426
|
}
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
427
|
+
// Fallback to usage path for internal references or when original type is not available
|
|
428
|
+
return getContainerTypeFromPath(entry.pathFromRoot);
|
|
429
|
+
};
|
|
430
|
+
// Track names per (container prefix) and per target
|
|
431
|
+
const targetToNameByPrefix = new Map();
|
|
432
|
+
const usedNamesByObj = new Map();
|
|
433
|
+
const sanitize = (name) => name.replace(/[^A-Za-z0-9_-]/g, "_");
|
|
434
|
+
const baseName = (filePath) => {
|
|
435
|
+
try {
|
|
436
|
+
const withoutHash = filePath.split("#")[0];
|
|
437
|
+
const parts = withoutHash.split("/");
|
|
438
|
+
const filename = parts[parts.length - 1] || "schema";
|
|
439
|
+
const dot = filename.lastIndexOf(".");
|
|
440
|
+
return sanitize(dot > 0 ? filename.substring(0, dot) : filename);
|
|
441
|
+
}
|
|
442
|
+
catch {
|
|
443
|
+
return "schema";
|
|
444
|
+
}
|
|
445
|
+
};
|
|
446
|
+
const lastToken = (hash) => {
|
|
447
|
+
if (!hash || hash === "#") {
|
|
448
|
+
return "root";
|
|
449
|
+
}
|
|
450
|
+
const tokens = hash.replace(/^#\//, "").split("/");
|
|
451
|
+
return sanitize(tokens[tokens.length - 1] || "root");
|
|
452
|
+
};
|
|
453
|
+
const uniqueName = (containerObj, proposed) => {
|
|
454
|
+
if (!usedNamesByObj.has(containerObj)) {
|
|
455
|
+
usedNamesByObj.set(containerObj, new Set(Object.keys(containerObj || {})));
|
|
456
|
+
}
|
|
457
|
+
const used = usedNamesByObj.get(containerObj);
|
|
458
|
+
let name = proposed;
|
|
459
|
+
let i = 2;
|
|
460
|
+
while (used.has(name)) {
|
|
461
|
+
name = `${proposed}_${i++}`;
|
|
462
|
+
}
|
|
463
|
+
used.add(name);
|
|
464
|
+
return name;
|
|
465
|
+
};
|
|
466
|
+
perf.mark("remap-loop-start");
|
|
467
|
+
for (const entry of inventory) {
|
|
468
|
+
// Safety check: ensure entry and entry.$ref are valid objects
|
|
469
|
+
if (!entry || !entry.$ref || typeof entry.$ref !== "object") {
|
|
470
|
+
perf.warn(`Skipping invalid inventory entry:`, entry);
|
|
471
|
+
continue;
|
|
472
|
+
}
|
|
473
|
+
// Keep internal refs internal
|
|
474
|
+
if (!entry.external) {
|
|
475
|
+
if (entry.$ref && typeof entry.$ref === "object") {
|
|
476
|
+
entry.$ref.$ref = entry.hash;
|
|
477
|
+
}
|
|
478
|
+
continue;
|
|
479
|
+
}
|
|
480
|
+
// Avoid changing direct self-references; keep them internal
|
|
481
|
+
if (entry.circular) {
|
|
482
|
+
if (entry.$ref && typeof entry.$ref === "object") {
|
|
274
483
|
entry.$ref.$ref = entry.pathFromRoot;
|
|
275
484
|
}
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
487
|
+
// Choose appropriate container based on original location in external file
|
|
488
|
+
const component = chooseComponent(entry);
|
|
489
|
+
const { obj: container, prefix } = ensureContainer(component);
|
|
490
|
+
const targetKey = `${entry.file}::${entry.hash}`;
|
|
491
|
+
if (!targetToNameByPrefix.has(prefix)) {
|
|
492
|
+
targetToNameByPrefix.set(prefix, new Map());
|
|
493
|
+
}
|
|
494
|
+
const namesForPrefix = targetToNameByPrefix.get(prefix);
|
|
495
|
+
let defName = namesForPrefix.get(targetKey);
|
|
496
|
+
if (!defName) {
|
|
497
|
+
const proposed = `${baseName(entry.file)}_${lastToken(entry.hash)}`;
|
|
498
|
+
defName = uniqueName(container, proposed);
|
|
499
|
+
namesForPrefix.set(targetKey, defName);
|
|
500
|
+
// Store the resolved value under the container
|
|
501
|
+
container[defName] = entry.value;
|
|
502
|
+
}
|
|
503
|
+
// Point the occurrence to the internal definition, preserving extensions
|
|
504
|
+
const refPath = `${prefix}/${defName}`;
|
|
505
|
+
if (entry.extended && entry.$ref && typeof entry.$ref === "object") {
|
|
506
|
+
entry.$ref.$ref = refPath;
|
|
507
|
+
}
|
|
508
|
+
else {
|
|
509
|
+
entry.parent[entry.key] = { $ref: refPath };
|
|
276
510
|
}
|
|
277
511
|
}
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
// if (entry.$ref && typeof entry.$ref === "object" && "$ref" in entry.$ref) {
|
|
284
|
-
// const resolved = inventory.find((e: InventoryEntry) => e.pathFromRoot === entry.$ref.$ref);
|
|
285
|
-
// if (resolved) {
|
|
286
|
-
// const resolvedPointsToAnotherRef =
|
|
287
|
-
// resolved.$ref && typeof resolved.$ref === "object" && "$ref" in resolved.$ref;
|
|
288
|
-
// if (resolvedPointsToAnotherRef && entry.$ref.$ref !== resolved.$ref.$ref) {
|
|
289
|
-
// // console.log('Re-mapping $ref pointer "%s" at %s', entry.$ref.$ref, entry.pathFromRoot);
|
|
290
|
-
// entry.$ref.$ref = resolved.$ref.$ref;
|
|
291
|
-
// hadChange = true;
|
|
292
|
-
// }
|
|
293
|
-
// }
|
|
294
|
-
// }
|
|
295
|
-
// }
|
|
296
|
-
// }
|
|
512
|
+
perf.mark("remap-loop-end");
|
|
513
|
+
perf.measure("remap-loop-time", "remap-loop-start", "remap-loop-end");
|
|
514
|
+
perf.mark("remap-end");
|
|
515
|
+
perf.measure("remap-total-time", "remap-start", "remap-end");
|
|
516
|
+
perf.log(`Completed remap of ${inventory.length} entries`);
|
|
297
517
|
}
|
|
298
518
|
function removeFromInventory(inventory, entry) {
|
|
299
519
|
const index = inventory.indexOf(entry);
|
|
@@ -309,8 +529,14 @@ function removeFromInventory(inventory, entry) {
|
|
|
309
529
|
*/
|
|
310
530
|
const bundle = (parser, options) => {
|
|
311
531
|
// console.log('Bundling $ref pointers in %s', parser.$refs._root$Ref.path);
|
|
532
|
+
perf.mark("bundle-start");
|
|
312
533
|
// Build an inventory of all $ref pointers in the JSON Schema
|
|
313
534
|
const inventory = [];
|
|
535
|
+
const inventoryLookup = createInventoryLookup();
|
|
536
|
+
perf.log("Starting crawl phase");
|
|
537
|
+
perf.mark("crawl-phase-start");
|
|
538
|
+
const visitedObjects = new WeakSet();
|
|
539
|
+
const resolvedRefs = new Map(); // Cache for resolved $ref targets
|
|
314
540
|
crawl({
|
|
315
541
|
parent: parser,
|
|
316
542
|
key: "schema",
|
|
@@ -318,10 +544,40 @@ const bundle = (parser, options) => {
|
|
|
318
544
|
pathFromRoot: "#",
|
|
319
545
|
indirections: 0,
|
|
320
546
|
inventory,
|
|
547
|
+
inventoryLookup,
|
|
321
548
|
$refs: parser.$refs,
|
|
322
549
|
options,
|
|
550
|
+
visitedObjects,
|
|
551
|
+
resolvedRefs,
|
|
323
552
|
});
|
|
553
|
+
perf.mark("crawl-phase-end");
|
|
554
|
+
perf.measure("crawl-phase-time", "crawl-phase-start", "crawl-phase-end");
|
|
555
|
+
const stats = inventoryLookup.getStats();
|
|
556
|
+
perf.log(`Crawl phase complete. Found ${inventory.length} $refs. Lookup stats:`, stats);
|
|
324
557
|
// Remap all $ref pointers
|
|
325
|
-
remap
|
|
558
|
+
perf.log("Starting remap phase");
|
|
559
|
+
perf.mark("remap-phase-start");
|
|
560
|
+
remap(parser, inventory);
|
|
561
|
+
perf.mark("remap-phase-end");
|
|
562
|
+
perf.measure("remap-phase-time", "remap-phase-start", "remap-phase-end");
|
|
563
|
+
perf.mark("bundle-end");
|
|
564
|
+
perf.measure("bundle-total-time", "bundle-start", "bundle-end");
|
|
565
|
+
perf.log("Bundle complete. Performance summary:");
|
|
566
|
+
// Log final stats
|
|
567
|
+
const finalStats = inventoryLookup.getStats();
|
|
568
|
+
perf.log(`Final inventory stats:`, finalStats);
|
|
569
|
+
perf.log(`Resolved refs cache size: ${resolvedRefs.size}`);
|
|
570
|
+
if (DEBUG_PERFORMANCE) {
|
|
571
|
+
// Log all performance measures
|
|
572
|
+
const measures = performance.getEntriesByType("measure");
|
|
573
|
+
measures.forEach((measure) => {
|
|
574
|
+
if (measure.name.includes("time")) {
|
|
575
|
+
console.log(`${measure.name}: ${measure.duration.toFixed(2)}ms`);
|
|
576
|
+
}
|
|
577
|
+
});
|
|
578
|
+
// Clear performance marks and measures for next run
|
|
579
|
+
performance.clearMarks();
|
|
580
|
+
performance.clearMeasures();
|
|
581
|
+
}
|
|
326
582
|
};
|
|
327
583
|
exports.bundle = bundle;
|
package/dist/lib/dereference.js
CHANGED
|
@@ -37,6 +37,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
37
37
|
};
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
39
|
const ref_js_1 = __importDefault(require("./ref.js"));
|
|
40
|
+
const cloneDeep_1 = __importDefault(require("lodash/cloneDeep"));
|
|
40
41
|
const pointer_js_1 = __importDefault(require("./pointer.js"));
|
|
41
42
|
const ono_1 = require("@jsdevtools/ono");
|
|
42
43
|
const url = __importStar(require("./util/url.js"));
|
|
@@ -160,10 +161,11 @@ function dereference$Ref($ref, path, pathFromRoot, parents, processedObjects, de
|
|
|
160
161
|
}
|
|
161
162
|
return {
|
|
162
163
|
circular: cache.circular,
|
|
163
|
-
value: Object.assign({}, cache.value, extraKeys),
|
|
164
|
+
value: Object.assign({}, (0, cloneDeep_1.default)(cache.value), extraKeys),
|
|
164
165
|
};
|
|
165
166
|
}
|
|
166
|
-
|
|
167
|
+
// Return a deep-cloned value so each occurrence is an independent copy
|
|
168
|
+
return { circular: cache.circular, value: (0, cloneDeep_1.default)(cache.value) };
|
|
167
169
|
}
|
|
168
170
|
const pointer = $refs._resolve($refPath, path, options);
|
|
169
171
|
if (pointer === null) {
|
package/dist/lib/index.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { JSONSchema } from "./types/index.js";
|
|
|
3
3
|
interface ResolvedInput {
|
|
4
4
|
path: string;
|
|
5
5
|
schema: string | JSONSchema | Buffer | Awaited<JSONSchema> | undefined;
|
|
6
|
-
type:
|
|
6
|
+
type: "file" | "json" | "url";
|
|
7
7
|
}
|
|
8
8
|
export declare const getResolvedInput: ({ pathOrUrlOrSchema, }: {
|
|
9
9
|
pathOrUrlOrSchema: JSONSchema | string | unknown;
|
|
@@ -73,5 +73,5 @@ export declare class $RefParser {
|
|
|
73
73
|
schema: JSONSchema;
|
|
74
74
|
}>;
|
|
75
75
|
}
|
|
76
|
-
export { sendRequest } from
|
|
76
|
+
export { sendRequest } from "./resolvers/url.js";
|
|
77
77
|
export type { JSONSchema } from "./types/index.js";
|