@scalar/openapi-parser 0.29.4 → 0.29.5
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 +6 -0
- package/dist/utils/join/join.d.ts +3 -3
- package/dist/utils/join/join.d.ts.map +1 -1
- package/dist/utils/join/join.js +64 -181
- package/package.json +7 -7
- package/dist/utils/join/merge-objects.d.ts +0 -21
- package/dist/utils/join/merge-objects.d.ts.map +0 -1
- package/dist/utils/join/merge-objects.js +0 -45
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# @scalar/openapi-parser
|
|
2
2
|
|
|
3
|
+
## 0.29.5
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#10274](https://github.com/scalar/scalar/pull/10274): Add a standard-agnostic `join` utility with configurable merge strategies and conflict reporting to `@scalar/json-magic/join`. Use it in the OpenAPI parser while retaining OpenAPI upgrades, component prefixes, and OpenAPI conflict reports in the parser.
|
|
8
|
+
|
|
3
9
|
## 0.29.4
|
|
4
10
|
|
|
5
11
|
## 0.29.3
|
|
@@ -25,8 +25,8 @@ type JoinResult = {
|
|
|
25
25
|
*
|
|
26
26
|
* - Merges the "info" object, paths, webhooks, tags, and servers from all input documents.
|
|
27
27
|
* - If there are conflicting paths or webhooks (same path and method), returns a list of conflicts.
|
|
28
|
-
* -
|
|
29
|
-
* -
|
|
28
|
+
* - For tags (by name) and servers (by URL), the last input document wins.
|
|
29
|
+
* - Metadata uses the first input document when fields overlap.
|
|
30
30
|
*
|
|
31
31
|
* @param inputs - Array of OpenAPI documents (UnknownObject) to join
|
|
32
32
|
* @returns {JoinResult} - { ok: true, document } if successful, or { ok: false, conflicts } if there are conflicts
|
|
@@ -44,7 +44,7 @@ type JoinResult = {
|
|
|
44
44
|
* tags: [{ name: "bar" }],
|
|
45
45
|
* servers: [{ url: "https://api2.example.com" }]
|
|
46
46
|
* }
|
|
47
|
-
* const result = join([doc1, doc2])
|
|
47
|
+
* const result = await join([doc1, doc2])
|
|
48
48
|
* // result.ok === true
|
|
49
49
|
* // result.document.info.title === "API 1"
|
|
50
50
|
* // result.document.info.description === "Second API"
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"join.d.ts","sourceRoot":"","sources":["../../../src/utils/join/join.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"join.d.ts","sourceRoot":"","sources":["../../../src/utils/join/join.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,QAAQ,IAAI,mBAAmB,EAAE,MAAM,2BAA2B,CAAA;AAChF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAA;AAgGxD,KAAK,SAAS,GACV;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAC9C;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACjD;IAAE,IAAI,EAAE,WAAW,CAAC;IAAC,aAAa,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAA;AAC9D,KAAK,UAAU,GAAG;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,EAAE,mBAAmB,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,SAAS,EAAE,SAAS,EAAE,CAAA;CAAE,CAAA;AAMrG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,eAAO,MAAM,IAAI,GAAU,QAAQ,aAAa,EAAE,EAAE,SAAS;IAAE,gBAAgB,EAAE,MAAM,EAAE,CAAA;CAAE,KAAG,OAAO,CAAC,UAAU,CA+E/G,CAAA"}
|
package/dist/utils/join/join.js
CHANGED
|
@@ -1,29 +1,8 @@
|
|
|
1
|
+
import { isObject } from '@scalar/helpers/object/is-object';
|
|
1
2
|
import { isPollutionKey } from '@scalar/helpers/object/prevent-pollution';
|
|
2
3
|
import { bundle } from '@scalar/json-magic/bundle';
|
|
3
|
-
import {
|
|
4
|
+
import { join as joinDocuments } from '@scalar/json-magic/join';
|
|
4
5
|
import { upgrade } from '../../utils/upgrade.js';
|
|
5
|
-
/**
|
|
6
|
-
* Returns the intersection of two sets as an array.
|
|
7
|
-
*
|
|
8
|
-
* @param a - The first set
|
|
9
|
-
* @param b - The second set
|
|
10
|
-
* @returns An array containing the elements present in both sets
|
|
11
|
-
*
|
|
12
|
-
* @example
|
|
13
|
-
* const setA = new Set([1, 2, 3, 4])
|
|
14
|
-
* const setB = new Set([3, 4, 5, 6])
|
|
15
|
-
* const intersection = getSetIntersection(setA, setB)
|
|
16
|
-
* // intersection: [3, 4]
|
|
17
|
-
*/
|
|
18
|
-
const getSetIntersection = (a, b) => {
|
|
19
|
-
const result = [];
|
|
20
|
-
for (const value of a) {
|
|
21
|
-
if (b.has(value)) {
|
|
22
|
-
result.push(value);
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
return result;
|
|
26
|
-
};
|
|
27
6
|
/**
|
|
28
7
|
* Returns the value if it is not nullish (or, for arrays, not empty), otherwise returns the provided default value.
|
|
29
8
|
* Useful for handling OpenAPI fields that may be missing or empty.
|
|
@@ -37,122 +16,6 @@ const withDefault = (value, defaultValue) => {
|
|
|
37
16
|
}
|
|
38
17
|
return value ?? defaultValue;
|
|
39
18
|
};
|
|
40
|
-
/**
|
|
41
|
-
* Merges multiple OpenAPI PathsObjects into a single PathsObject.
|
|
42
|
-
* - If a path does not exist in the result, it is added directly.
|
|
43
|
-
* - If a path already exists, its operations (get, post, etc.) are merged.
|
|
44
|
-
* - If the same operation (e.g., "get") exists for the same path in multiple inputs,
|
|
45
|
-
* a conflict is recorded for that path and method.
|
|
46
|
-
*
|
|
47
|
-
* @param inputs - Array of OpenAPI 3.1 PathsObjects to merge
|
|
48
|
-
* @returns An object containing the merged paths and a list of conflicts
|
|
49
|
-
*/
|
|
50
|
-
const mergePaths = (inputs) => {
|
|
51
|
-
const result = {};
|
|
52
|
-
const conflicts = [];
|
|
53
|
-
for (const paths of inputs) {
|
|
54
|
-
if (typeof paths !== 'object') {
|
|
55
|
-
continue;
|
|
56
|
-
}
|
|
57
|
-
for (const [path, pathItem] of Object.entries(paths)) {
|
|
58
|
-
if (isPollutionKey(path)) {
|
|
59
|
-
continue;
|
|
60
|
-
}
|
|
61
|
-
if (!Object.hasOwn(result, path) || !result[path]) {
|
|
62
|
-
// If the path does not exist, add it directly
|
|
63
|
-
result[path] = pathItem;
|
|
64
|
-
continue;
|
|
65
|
-
}
|
|
66
|
-
// Find intersecting operation keys (e.g., "get", "post") for this path
|
|
67
|
-
const intersectingKeys = getSetIntersection(new Set(Object.keys(result[path])), new Set(Object.keys(pathItem)));
|
|
68
|
-
// If the path exists, merge the operations (get, post, etc.)
|
|
69
|
-
result[path] = { ...result[path], ...pathItem };
|
|
70
|
-
// Record conflicts for each intersecting operation key
|
|
71
|
-
intersectingKeys.forEach((key) => conflicts.push({ method: key, path }));
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
return { paths: result, conflicts };
|
|
75
|
-
};
|
|
76
|
-
/**
|
|
77
|
-
* Merges multiple arrays of OpenAPI TagObjects into a single array, ensuring uniqueness by tag name.
|
|
78
|
-
* - If a tag with the same name appears in multiple arrays, only the first occurrence is included in the result.
|
|
79
|
-
*
|
|
80
|
-
* @param inputs - Array of arrays of OpenAPI 3.1 TagObjects to merge
|
|
81
|
-
* @returns An array of unique TagObjects (by name)
|
|
82
|
-
*/
|
|
83
|
-
const mergeTags = (inputs) => {
|
|
84
|
-
const cache = new Set();
|
|
85
|
-
const result = [];
|
|
86
|
-
for (const tags of inputs) {
|
|
87
|
-
for (const tag of tags) {
|
|
88
|
-
if (!cache.has(tag.name)) {
|
|
89
|
-
result.push(tag);
|
|
90
|
-
}
|
|
91
|
-
cache.add(tag.name);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
return result;
|
|
95
|
-
};
|
|
96
|
-
/**
|
|
97
|
-
* Merges multiple arrays of OpenAPI ServerObjects into a single array, ensuring uniqueness by server URL.
|
|
98
|
-
* - If a server with the same URL appears in multiple arrays, only the first occurrence is included in the result.
|
|
99
|
-
*
|
|
100
|
-
* @param inputs - Array of arrays of OpenAPI 3.1 ServerObjects to merge
|
|
101
|
-
* @returns An array of unique ServerObjects (by url)
|
|
102
|
-
*/
|
|
103
|
-
const mergeServers = (inputs) => {
|
|
104
|
-
const cache = new Set();
|
|
105
|
-
const result = [];
|
|
106
|
-
for (const servers of inputs) {
|
|
107
|
-
for (const server of servers) {
|
|
108
|
-
if (!cache.has(server.url)) {
|
|
109
|
-
result.push(server);
|
|
110
|
-
}
|
|
111
|
-
cache.add(server.url);
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
return result;
|
|
115
|
-
};
|
|
116
|
-
/**
|
|
117
|
-
* Merges multiple OpenAPI ComponentsObject instances into a single components object.
|
|
118
|
-
* - If a component with the same type and name appears in multiple inputs, only the first occurrence is included.
|
|
119
|
-
* - Any conflicts (duplicate component names within the same type) are recorded in the `conflicts` array.
|
|
120
|
-
*
|
|
121
|
-
* @param inputs - Array of OpenAPI 3.1 ComponentsObjects to merge
|
|
122
|
-
* @returns An object containing the merged components and an array of conflicts
|
|
123
|
-
*/
|
|
124
|
-
const mergeComponents = (inputs) => {
|
|
125
|
-
const result = {};
|
|
126
|
-
const conflicts = [];
|
|
127
|
-
for (const components of inputs) {
|
|
128
|
-
if (typeof components !== 'object') {
|
|
129
|
-
continue;
|
|
130
|
-
}
|
|
131
|
-
// Merge each component type (schemas, responses, parameters, etc.)
|
|
132
|
-
for (const [key, value] of Object.entries(components)) {
|
|
133
|
-
if (isPollutionKey(key)) {
|
|
134
|
-
continue;
|
|
135
|
-
}
|
|
136
|
-
for (const [name, component] of Object.entries(value)) {
|
|
137
|
-
if (isPollutionKey(name)) {
|
|
138
|
-
continue;
|
|
139
|
-
}
|
|
140
|
-
if (!Object.hasOwn(result, key)) {
|
|
141
|
-
result[key] = {};
|
|
142
|
-
}
|
|
143
|
-
if (Object.hasOwn(result[key], name) && result[key][name]) {
|
|
144
|
-
// If the component already exists, record a conflict
|
|
145
|
-
conflicts.push({ componentType: key, name });
|
|
146
|
-
}
|
|
147
|
-
else {
|
|
148
|
-
// Otherwise, add the component
|
|
149
|
-
result[key][name] = component;
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
return { components: result, conflicts };
|
|
155
|
-
};
|
|
156
19
|
/**
|
|
157
20
|
* Prefixes component names and their references in multiple OpenAPI documents.
|
|
158
21
|
*
|
|
@@ -229,8 +92,8 @@ const asOpenApiDocumentV3_1 = (document) => {
|
|
|
229
92
|
*
|
|
230
93
|
* - Merges the "info" object, paths, webhooks, tags, and servers from all input documents.
|
|
231
94
|
* - If there are conflicting paths or webhooks (same path and method), returns a list of conflicts.
|
|
232
|
-
* -
|
|
233
|
-
* -
|
|
95
|
+
* - For tags (by name) and servers (by URL), the last input document wins.
|
|
96
|
+
* - Metadata uses the first input document when fields overlap.
|
|
234
97
|
*
|
|
235
98
|
* @param inputs - Array of OpenAPI documents (UnknownObject) to join
|
|
236
99
|
* @returns {JoinResult} - { ok: true, document } if successful, or { ok: false, conflicts } if there are conflicts
|
|
@@ -248,7 +111,7 @@ const asOpenApiDocumentV3_1 = (document) => {
|
|
|
248
111
|
* tags: [{ name: "bar" }],
|
|
249
112
|
* servers: [{ url: "https://api2.example.com" }]
|
|
250
113
|
* }
|
|
251
|
-
* const result = join([doc1, doc2])
|
|
114
|
+
* const result = await join([doc1, doc2])
|
|
252
115
|
* // result.ok === true
|
|
253
116
|
* // result.document.info.title === "API 1"
|
|
254
117
|
* // result.document.info.description === "Second API"
|
|
@@ -257,7 +120,7 @@ const asOpenApiDocumentV3_1 = (document) => {
|
|
|
257
120
|
* // result.document.servers contains both server URLs
|
|
258
121
|
*/
|
|
259
122
|
export const join = async (inputs, config) => {
|
|
260
|
-
//
|
|
123
|
+
// Keep OpenAPI version normalization separate from the format-independent join.
|
|
261
124
|
const upgraded = inputs.map((it) => upgrade(it).specification);
|
|
262
125
|
// Preprocess documents by prefixing components if specified
|
|
263
126
|
if (config?.prefixComponents) {
|
|
@@ -265,49 +128,69 @@ export const join = async (inputs, config) => {
|
|
|
265
128
|
}
|
|
266
129
|
// Reverse the upgraded documents to ensure the first document has the highest precedence
|
|
267
130
|
upgraded.reverse();
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
131
|
+
const documents = upgraded.map((document) => ({
|
|
132
|
+
...document,
|
|
133
|
+
info: isObject(document.info) ? document.info : {},
|
|
134
|
+
paths: document.paths ?? {},
|
|
135
|
+
webhooks: document.webhooks ?? {},
|
|
136
|
+
components: document.components ?? {},
|
|
137
|
+
tags: document.tags ?? [],
|
|
138
|
+
servers: document.servers ?? [],
|
|
139
|
+
}));
|
|
140
|
+
const result = joinDocuments(documents, {
|
|
141
|
+
strategy: ({ path, current }) => {
|
|
142
|
+
const [field] = path;
|
|
143
|
+
// Keep legacy filtering at merge boundaries, but preserve keys inside copied schemas and examples.
|
|
144
|
+
if (isPollutionKey(path.at(-1)) && (field === 'info' || field === 'components' || path.length === 2)) {
|
|
145
|
+
return 'skip';
|
|
146
|
+
}
|
|
147
|
+
if (field === 'info') {
|
|
148
|
+
return 'merge-by-index';
|
|
149
|
+
}
|
|
150
|
+
if (field === 'tags') {
|
|
151
|
+
return { uniqueBy: 'name' };
|
|
152
|
+
}
|
|
153
|
+
if (field === 'servers') {
|
|
154
|
+
return { uniqueBy: 'url' };
|
|
155
|
+
}
|
|
156
|
+
if (field === 'paths' || field === 'webhooks' || field === 'components') {
|
|
157
|
+
if (path.length < 3) {
|
|
158
|
+
return 'merge';
|
|
159
|
+
}
|
|
160
|
+
// Preserve the existing handling of falsy component definitions.
|
|
161
|
+
return field === 'components' && !current ? 'replace' : 'conflict';
|
|
162
|
+
}
|
|
163
|
+
return 'replace';
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
if (result.ok === false) {
|
|
167
|
+
const conflicts = [];
|
|
168
|
+
// Keep the public conflict shape and category order stable.
|
|
169
|
+
for (const field of ['paths', 'webhooks', 'components']) {
|
|
170
|
+
for (const { path } of result.conflicts) {
|
|
171
|
+
if (path[0] !== field) {
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
if (field === 'components') {
|
|
175
|
+
conflicts.push({ type: 'component', componentType: path[1], name: path[2] });
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
conflicts.push({ type: field === 'paths' ? 'path' : 'webhook', path: path[1], method: path[2] });
|
|
179
|
+
}
|
|
180
|
+
}
|
|
272
181
|
}
|
|
273
|
-
return
|
|
274
|
-
}, {});
|
|
275
|
-
// Merge paths from all documents, collecting conflicts
|
|
276
|
-
const { paths, conflicts: pathConflicts } = mergePaths(upgraded.map((it) => it.paths ?? {}));
|
|
277
|
-
// Merge webhooks from all documents, collecting conflicts
|
|
278
|
-
const { paths: webhooks, conflicts: webhookConflicts } = mergePaths(upgraded.map((it) => it.webhooks ?? {}));
|
|
279
|
-
// Merge tags, ensuring uniqueness by tag name
|
|
280
|
-
const tags = mergeTags(upgraded.map((it) => it.tags ?? []));
|
|
281
|
-
// Merge servers, ensuring uniqueness by server url
|
|
282
|
-
const servers = mergeServers(upgraded.map((it) => it.servers ?? []));
|
|
283
|
-
// Merge components, collecting conflicts
|
|
284
|
-
const { components, conflicts: componentConflicts } = mergeComponents(upgraded.map((it) => it.components ?? {}));
|
|
285
|
-
// Merge all documents in the upgraded array into a single object (shallow merge)
|
|
286
|
-
const result = upgraded.reduce((acc, curr) => ({ ...acc, ...curr }), {});
|
|
287
|
-
// Collect all conflicts (paths and webhooks)
|
|
288
|
-
const conflicts = [
|
|
289
|
-
...pathConflicts.map((it) => ({ type: 'path', ...it })),
|
|
290
|
-
...webhookConflicts.map((it) => ({ type: 'webhook', ...it })),
|
|
291
|
-
...componentConflicts.map((it) => ({ type: 'component', ...it })),
|
|
292
|
-
];
|
|
293
|
-
// If there are any conflicts, return them
|
|
294
|
-
if (conflicts.length) {
|
|
295
|
-
return {
|
|
296
|
-
ok: false,
|
|
297
|
-
conflicts,
|
|
298
|
-
};
|
|
182
|
+
return { ok: false, conflicts };
|
|
299
183
|
}
|
|
300
|
-
// Return the merged OpenAPI document
|
|
301
184
|
return {
|
|
302
185
|
ok: true,
|
|
303
186
|
document: asOpenApiDocumentV3_1({
|
|
304
|
-
...result,
|
|
305
|
-
info,
|
|
306
|
-
paths,
|
|
307
|
-
webhooks: withDefault(webhooks, undefined),
|
|
308
|
-
tags: withDefault(tags, undefined),
|
|
309
|
-
servers: withDefault(servers, undefined),
|
|
310
|
-
components: withDefault(components, undefined),
|
|
187
|
+
...result.document,
|
|
188
|
+
info: result.document.info ?? {},
|
|
189
|
+
paths: result.document.paths ?? {},
|
|
190
|
+
webhooks: withDefault(result.document.webhooks, undefined),
|
|
191
|
+
tags: withDefault(result.document.tags, undefined),
|
|
192
|
+
servers: withDefault(result.document.servers, undefined),
|
|
193
|
+
components: withDefault(result.document.components, undefined),
|
|
311
194
|
}),
|
|
312
195
|
};
|
|
313
196
|
};
|
package/package.json
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"parser",
|
|
18
18
|
"typescript"
|
|
19
19
|
],
|
|
20
|
-
"version": "0.29.
|
|
20
|
+
"version": "0.29.5",
|
|
21
21
|
"engines": {
|
|
22
22
|
"node": ">=22"
|
|
23
23
|
},
|
|
@@ -48,12 +48,12 @@
|
|
|
48
48
|
],
|
|
49
49
|
"sideEffects": false,
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"@scalar/helpers": "0.
|
|
52
|
-
"@scalar/json-magic": "0.
|
|
53
|
-
"@scalar/openapi-types": "0.9.
|
|
54
|
-
"@scalar/openapi-upgrader": "0.
|
|
55
|
-
"@scalar/openapi-validator": "0.1.
|
|
56
|
-
"@scalar/types": "0.
|
|
51
|
+
"@scalar/helpers": "0.13.0",
|
|
52
|
+
"@scalar/json-magic": "0.15.0",
|
|
53
|
+
"@scalar/openapi-types": "0.9.7",
|
|
54
|
+
"@scalar/openapi-upgrader": "0.3.0",
|
|
55
|
+
"@scalar/openapi-validator": "0.1.4",
|
|
56
|
+
"@scalar/types": "0.21.0",
|
|
57
57
|
"yaml": "^2.9.0"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Deep merges two objects, combining their properties recursively.
|
|
3
|
-
*
|
|
4
|
-
* ⚠️ Note: This operation assumes there are no key collisions between the objects.
|
|
5
|
-
* @param a - Target object to merge into
|
|
6
|
-
* @param b - Source object to merge from
|
|
7
|
-
* @returns The merged object (mutates and returns a)
|
|
8
|
-
*
|
|
9
|
-
* @example
|
|
10
|
-
* // Simple merge
|
|
11
|
-
* const a = { name: 'John' }
|
|
12
|
-
* const b = { age: 30 }
|
|
13
|
-
* mergeObjects(a, b) // { name: 'John', age: 30 }
|
|
14
|
-
*
|
|
15
|
-
* // Nested merge
|
|
16
|
-
* const a = { user: { name: 'John' } }
|
|
17
|
-
* const b = { user: { age: 30 } }
|
|
18
|
-
* mergeObjects(a, b) // { user: { name: 'John', age: 30 } }
|
|
19
|
-
*/
|
|
20
|
-
export declare const mergeObjects: <R>(a: Record<string, unknown>, b: Record<string, unknown>) => R;
|
|
21
|
-
//# sourceMappingURL=merge-objects.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"merge-objects.d.ts","sourceRoot":"","sources":["../../../src/utils/join/merge-objects.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,YAAY,GAAI,CAAC,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAG,CAwBxF,CAAA"}
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import { isObjectLike } from '@scalar/helpers/object/is-object';
|
|
2
|
-
import { isPollutionKey } from '@scalar/helpers/object/prevent-pollution';
|
|
3
|
-
/**
|
|
4
|
-
* Deep merges two objects, combining their properties recursively.
|
|
5
|
-
*
|
|
6
|
-
* ⚠️ Note: This operation assumes there are no key collisions between the objects.
|
|
7
|
-
* @param a - Target object to merge into
|
|
8
|
-
* @param b - Source object to merge from
|
|
9
|
-
* @returns The merged object (mutates and returns a)
|
|
10
|
-
*
|
|
11
|
-
* @example
|
|
12
|
-
* // Simple merge
|
|
13
|
-
* const a = { name: 'John' }
|
|
14
|
-
* const b = { age: 30 }
|
|
15
|
-
* mergeObjects(a, b) // { name: 'John', age: 30 }
|
|
16
|
-
*
|
|
17
|
-
* // Nested merge
|
|
18
|
-
* const a = { user: { name: 'John' } }
|
|
19
|
-
* const b = { user: { age: 30 } }
|
|
20
|
-
* mergeObjects(a, b) // { user: { name: 'John', age: 30 } }
|
|
21
|
-
*/
|
|
22
|
-
export const mergeObjects = (a, b) => {
|
|
23
|
-
for (const key in b) {
|
|
24
|
-
// Skip inherited keys and prototype pollution vectors such as an own `__proto__` key that
|
|
25
|
-
// survives JSON.parse. Without this guard the recursive merge below would walk into
|
|
26
|
-
// Object.prototype and pollute it for the whole process.
|
|
27
|
-
if (!Object.hasOwn(b, key) || isPollutionKey(key)) {
|
|
28
|
-
continue;
|
|
29
|
-
}
|
|
30
|
-
if (!(key in a)) {
|
|
31
|
-
a[key] = b[key];
|
|
32
|
-
}
|
|
33
|
-
else {
|
|
34
|
-
const aValue = a[key];
|
|
35
|
-
const bValue = b[key];
|
|
36
|
-
if (isObjectLike(aValue) && isObjectLike(bValue)) {
|
|
37
|
-
mergeObjects(aValue, bValue);
|
|
38
|
-
}
|
|
39
|
-
else {
|
|
40
|
-
a[key] = bValue; // Overwrite with b's value if not an object
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
return a;
|
|
45
|
-
};
|