@scalar/json-magic 0.12.20 → 0.13.2
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 +53 -0
- package/README.md +392 -173
- package/dist/bundle/index.d.ts +1 -1
- package/dist/bundle/index.d.ts.map +1 -1
- package/dist/bundle/index.js +1 -1
- package/dist/diff/apply.d.ts +18 -2
- package/dist/diff/apply.d.ts.map +1 -1
- package/dist/diff/apply.js +37 -2
- package/dist/diff/diff.d.ts +10 -0
- package/dist/diff/diff.d.ts.map +1 -1
- package/dist/diff/diff.js +36 -2
- package/dist/diff/merge.d.ts +8 -1
- package/dist/diff/merge.d.ts.map +1 -1
- package/dist/diff/merge.js +14 -3
- package/dist/diff/trie.d.ts +7 -1
- package/dist/diff/trie.d.ts.map +1 -1
- package/dist/diff/trie.js +10 -5
- package/dist/diff/utils.d.ts +11 -2
- package/dist/diff/utils.d.ts.map +1 -1
- package/dist/diff/utils.js +32 -2
- package/package.json +23 -3
package/dist/bundle/index.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
export type { LifecyclePlugin, LoaderPlugin, Plugin, ResolveResult } from './bundle.js';
|
|
2
|
-
export { bundle, resolveAndCopyReferences } from './bundle.js';
|
|
2
|
+
export { bundle, extensions, isLocalRef, prefixInternalRef, prefixInternalRefRecursive, resolveAndCopyReferences, } from './bundle.js';
|
|
3
3
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/bundle/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AACpF,OAAO,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/bundle/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AACpF,OAAO,EACL,MAAM,EACN,UAAU,EACV,UAAU,EACV,iBAAiB,EACjB,0BAA0B,EAC1B,wBAAwB,GACzB,MAAM,UAAU,CAAA"}
|
package/dist/bundle/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { bundle, resolveAndCopyReferences } from './bundle.js';
|
|
1
|
+
export { bundle, extensions, isLocalRef, prefixInternalRef, prefixInternalRefRecursive, resolveAndCopyReferences, } from './bundle.js';
|
package/dist/diff/apply.d.ts
CHANGED
|
@@ -7,9 +7,25 @@ export declare class InvalidChangesDetectedError extends Error {
|
|
|
7
7
|
* The function traverses the document structure following the paths specified in the differences
|
|
8
8
|
* and applies the corresponding changes (add, update, or delete) at each location.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
10
|
+
* Paths that reach the prototype chain (`__proto__`, `constructor` or `prototype`) are rejected
|
|
11
|
+
* before anything is written, so a hostile changeset cannot poison `Object.prototype`.
|
|
12
|
+
*
|
|
13
|
+
* A change with an empty path asks to replace the document itself, which is not supported: the
|
|
14
|
+
* function writes through the parent container of each path and the root has no parent. `diff`
|
|
15
|
+
* emits such a change whenever the two documents differ at the root, which covers a different
|
|
16
|
+
* `typeof`, `null` against an object, and an array on one side against a plain object on the
|
|
17
|
+
* other. Those changesets have to be handled by the caller instead of being applied.
|
|
18
|
+
*
|
|
19
|
+
* ⚠️ `document` is mutated in place and the result shares structure with the document the diff was
|
|
20
|
+
* built from: every `add` and `update` writes the change into the document by reference, and those
|
|
21
|
+
* changes are live references into the target document `diff` compared (see `diff`). A later write
|
|
22
|
+
* into the result can therefore be seen through that document, and the other way around. Callers
|
|
23
|
+
* that need an isolated result have to deep clone the document and the changes first.
|
|
24
|
+
*
|
|
25
|
+
* @param document - The original document to apply changes to, mutated in place
|
|
11
26
|
* @param diff - Array of differences to apply, each containing a path and change type
|
|
12
|
-
* @returns The modified document with all changes applied
|
|
27
|
+
* @returns The modified document with all changes applied, structurally shared with the changes
|
|
28
|
+
* @throws {InvalidChangesDetectedError} When a path is unusable, empty or reaches the prototype chain
|
|
13
29
|
*
|
|
14
30
|
* @example
|
|
15
31
|
* const original = {
|
package/dist/diff/apply.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"apply.d.ts","sourceRoot":"","sources":["../../src/diff/apply.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"apply.d.ts","sourceRoot":"","sources":["../../src/diff/apply.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAE7C,qBAAa,2BAA4B,SAAQ,KAAK;gBACxC,OAAO,EAAE,MAAM;CAI5B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AACH,eAAO,MAAM,KAAK,GAAI,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACrD,UAAU,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACjC,MAAM,UAAU,CAAC,CAAC,CAAC,EAAE,KACpB,CAkEF,CAAA"}
|
package/dist/diff/apply.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isPollutionKey } from '@scalar/helpers/object/prevent-pollution';
|
|
1
2
|
export class InvalidChangesDetectedError extends Error {
|
|
2
3
|
constructor(message) {
|
|
3
4
|
super(message);
|
|
@@ -9,9 +10,25 @@ export class InvalidChangesDetectedError extends Error {
|
|
|
9
10
|
* The function traverses the document structure following the paths specified in the differences
|
|
10
11
|
* and applies the corresponding changes (add, update, or delete) at each location.
|
|
11
12
|
*
|
|
12
|
-
*
|
|
13
|
+
* Paths that reach the prototype chain (`__proto__`, `constructor` or `prototype`) are rejected
|
|
14
|
+
* before anything is written, so a hostile changeset cannot poison `Object.prototype`.
|
|
15
|
+
*
|
|
16
|
+
* A change with an empty path asks to replace the document itself, which is not supported: the
|
|
17
|
+
* function writes through the parent container of each path and the root has no parent. `diff`
|
|
18
|
+
* emits such a change whenever the two documents differ at the root, which covers a different
|
|
19
|
+
* `typeof`, `null` against an object, and an array on one side against a plain object on the
|
|
20
|
+
* other. Those changesets have to be handled by the caller instead of being applied.
|
|
21
|
+
*
|
|
22
|
+
* ⚠️ `document` is mutated in place and the result shares structure with the document the diff was
|
|
23
|
+
* built from: every `add` and `update` writes the change into the document by reference, and those
|
|
24
|
+
* changes are live references into the target document `diff` compared (see `diff`). A later write
|
|
25
|
+
* into the result can therefore be seen through that document, and the other way around. Callers
|
|
26
|
+
* that need an isolated result have to deep clone the document and the changes first.
|
|
27
|
+
*
|
|
28
|
+
* @param document - The original document to apply changes to, mutated in place
|
|
13
29
|
* @param diff - Array of differences to apply, each containing a path and change type
|
|
14
|
-
* @returns The modified document with all changes applied
|
|
30
|
+
* @returns The modified document with all changes applied, structurally shared with the changes
|
|
31
|
+
* @throws {InvalidChangesDetectedError} When a path is unusable, empty or reaches the prototype chain
|
|
15
32
|
*
|
|
16
33
|
* @example
|
|
17
34
|
* const original = {
|
|
@@ -64,6 +81,24 @@ export const apply = (document, diff) => {
|
|
|
64
81
|
}
|
|
65
82
|
applyChange(current[path[depth]], path, d, depth + 1);
|
|
66
83
|
};
|
|
84
|
+
// Reject the two kinds of unusable entry we can spot without walking the document - a root level
|
|
85
|
+
// change and a prototype reaching path - before any entry touches it. A path that does not exist
|
|
86
|
+
// is only found while traversing, so that one can still leave the document half updated.
|
|
87
|
+
for (const d of diff) {
|
|
88
|
+
// An empty path targets the document itself. We only ever write through the parent container of
|
|
89
|
+
// a path, so there is nothing to write into for the root, and the caller has to swap the
|
|
90
|
+
// document out on its own.
|
|
91
|
+
if (d.path.length === 0) {
|
|
92
|
+
throw new InvalidChangesDetectedError('Process aborted. Root-level replacement is not supported, the change targets the document itself instead of a property inside it');
|
|
93
|
+
}
|
|
94
|
+
// A path segment such as `__proto__` would make the traversal walk onto `Object.prototype` and
|
|
95
|
+
// write there, poisoning every object in the runtime. `diff` never emits these segments, so
|
|
96
|
+
// only a hand-crafted changeset reaches this guard.
|
|
97
|
+
const unsafeSegment = d.path.find(isPollutionKey);
|
|
98
|
+
if (unsafeSegment !== undefined) {
|
|
99
|
+
throw new InvalidChangesDetectedError(`Process aborted. Path ${d.path.join('.')} contains the unsafe segment "${unsafeSegment}", which can modify the prototype chain`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
67
102
|
for (const d of diff) {
|
|
68
103
|
applyChange(document, d.path, d);
|
|
69
104
|
}
|
package/dist/diff/diff.d.ts
CHANGED
|
@@ -22,6 +22,16 @@ export type Difference<_T> = {
|
|
|
22
22
|
* This function performs a breadth-first comparison between two objects and returns
|
|
23
23
|
* a list of operations needed to transform the first object into the second.
|
|
24
24
|
*
|
|
25
|
+
* Keys that reach the prototype chain (`__proto__`, `constructor` and `prototype`) are skipped, so
|
|
26
|
+
* an untrusted document cannot produce a diff that poisons `Object.prototype` once applied.
|
|
27
|
+
*
|
|
28
|
+
* ⚠️ The returned `changes` are live references into the documents, not clones. An `add` or an
|
|
29
|
+
* `update` carries the very subtree `doc2` holds, and a `delete` carries the subtree from `doc1`,
|
|
30
|
+
* so writing into a change writes into the document it came from. This matters downstream:
|
|
31
|
+
* `merge` merges values into these objects, and `apply` writes them into its target document,
|
|
32
|
+
* which leaves the result structurally shared with `doc2`. Callers that need isolation have to
|
|
33
|
+
* deep clone the documents before diffing them, or the changes afterwards.
|
|
34
|
+
*
|
|
25
35
|
* @param doc1 - The source object to compare from
|
|
26
36
|
* @param doc2 - The target object to compare to
|
|
27
37
|
* @returns A list of operations (add/update/delete) with their paths and changes
|
package/dist/diff/diff.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"diff.d.ts","sourceRoot":"","sources":["../../src/diff/diff.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"diff.d.ts","sourceRoot":"","sources":["../../src/diff/diff.ts"],"names":[],"mappings":"AAEA;;;;;GAKG;AACH,KAAK,UAAU,GAAG,KAAK,GAAG,QAAQ,GAAG,QAAQ,CAAA;AAE7C;;;;;GAKG;AACH,MAAM,MAAM,UAAU,CAAC,EAAE,IAAI;IAAE,IAAI,EAAE,MAAM,EAAE,CAAC;IAAC,OAAO,EAAE,GAAG,CAAC;IAAC,IAAI,EAAE,UAAU,CAAA;CAAE,CAAA;AAE/E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AACH,eAAO,MAAM,IAAI,GAAI,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,oBAkE7F,CAAA"}
|
package/dist/diff/diff.js
CHANGED
|
@@ -1,9 +1,20 @@
|
|
|
1
|
+
import { isPollutionKey } from '@scalar/helpers/object/prevent-pollution';
|
|
1
2
|
/**
|
|
2
3
|
* Get the difference between two objects.
|
|
3
4
|
*
|
|
4
5
|
* This function performs a breadth-first comparison between two objects and returns
|
|
5
6
|
* a list of operations needed to transform the first object into the second.
|
|
6
7
|
*
|
|
8
|
+
* Keys that reach the prototype chain (`__proto__`, `constructor` and `prototype`) are skipped, so
|
|
9
|
+
* an untrusted document cannot produce a diff that poisons `Object.prototype` once applied.
|
|
10
|
+
*
|
|
11
|
+
* ⚠️ The returned `changes` are live references into the documents, not clones. An `add` or an
|
|
12
|
+
* `update` carries the very subtree `doc2` holds, and a `delete` carries the subtree from `doc1`,
|
|
13
|
+
* so writing into a change writes into the document it came from. This matters downstream:
|
|
14
|
+
* `merge` merges values into these objects, and `apply` writes them into its target document,
|
|
15
|
+
* which leaves the result structurally shared with `doc2`. Callers that need isolation have to
|
|
16
|
+
* deep clone the documents before diffing them, or the changes afterwards.
|
|
17
|
+
*
|
|
7
18
|
* @param doc1 - The source object to compare from
|
|
8
19
|
* @param doc2 - The target object to compare to
|
|
9
20
|
* @returns A list of operations (add/update/delete) with their paths and changes
|
|
@@ -52,8 +63,31 @@ export const diff = (doc1, doc2) => {
|
|
|
52
63
|
// We now can assume that el1 and el2 are of the same type
|
|
53
64
|
// For nested objects, we need to recursively check the properties
|
|
54
65
|
if (typeof el1 === 'object' && typeof el2 === 'object' && el1 !== null && el2 !== null) {
|
|
55
|
-
|
|
56
|
-
|
|
66
|
+
// A container type change (array to plain object or vice versa) is a single update.
|
|
67
|
+
// Recursing would treat array indices as object keys and produce per-key
|
|
68
|
+
// differences that corrupt the container when applied.
|
|
69
|
+
if (Array.isArray(el1) !== Array.isArray(el2)) {
|
|
70
|
+
diff.push({ path: prefix, changes: el2, type: 'update' });
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
// Keys that reach `Object.prototype` are dropped before we recurse. `JSON.parse` turns
|
|
74
|
+
// `__proto__` into a real own property that `Object.keys` reports, so an untrusted document
|
|
75
|
+
// would otherwise make us walk the prototype chain and emit a diff that poisons every object
|
|
76
|
+
// in the runtime once applied. `apply` rejects the same segments, so nothing emitted here can
|
|
77
|
+
// be turned away later. Note that this only covers the keys compared position by position: a
|
|
78
|
+
// brand new subtree is emitted as a single value and carries its own keys along untouched.
|
|
79
|
+
const keys = [...new Set([...Object.keys(el1), ...Object.keys(el2)])].filter((key) => !isPollutionKey(key));
|
|
80
|
+
// Removed array elements are applied with `splice` (see `apply`), which re-indexes every
|
|
81
|
+
// element after the removed one. Emitting the highest index first keeps the remaining indices
|
|
82
|
+
// valid, so an array that loses more than one element still applies correctly. Please keep
|
|
83
|
+
// this ordering in place, applying the same deletes in ascending order corrupts the array.
|
|
84
|
+
// Only an array that shrinks can lose elements, so an array that grows keeps the natural
|
|
85
|
+
// ascending order. Equal length arrays cannot lose elements either, they take the reversed
|
|
86
|
+
// branch to keep the guard simple. Deletes nested inside elements are object keys rather than
|
|
87
|
+
// array indices, so they never reach `splice` and are unaffected by the order.
|
|
88
|
+
// `keys` is a fresh array, so reversing it in place is safe.
|
|
89
|
+
const orderedKeys = Array.isArray(el1) && Array.isArray(el2) && el1.length >= el2.length ? keys.reverse() : keys;
|
|
90
|
+
for (const key of orderedKeys) {
|
|
57
91
|
bfs(el1[key], el2[key], [...prefix, key]);
|
|
58
92
|
}
|
|
59
93
|
return;
|
package/dist/diff/merge.d.ts
CHANGED
|
@@ -5,8 +5,15 @@ import type { Difference } from '../diff/diff.js';
|
|
|
5
5
|
* that arise when both diffs modify the same paths. It uses a trie data structure for
|
|
6
6
|
* efficient path matching and conflict detection.
|
|
7
7
|
*
|
|
8
|
+
* ⚠️ This function mutates the entries of `diff2`. When two changes on the same path can be folded
|
|
9
|
+
* together without a collision, the value from `diff1` is merged into the `changes` of the `diff2`
|
|
10
|
+
* entry, in place. Diffs built by `diff` carry live references into the documents they came from
|
|
11
|
+
* (see `diff`), so folding two changes together also writes into the document behind `diff2`.
|
|
12
|
+
* Callers that need the source documents to stay untouched have to deep clone them before diffing,
|
|
13
|
+
* or clone the changes afterwards.
|
|
14
|
+
*
|
|
8
15
|
* @param diff1 - First list of differences
|
|
9
|
-
* @param diff2 - Second list of differences
|
|
16
|
+
* @param diff2 - Second list of differences, whose entries are mutated, see the note above
|
|
10
17
|
* @returns Object containing:
|
|
11
18
|
* - diffs: Combined list of non-conflicting differences
|
|
12
19
|
* - conflicts: Array of conflicting difference pairs that need manual resolution
|
package/dist/diff/merge.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"merge.d.ts","sourceRoot":"","sources":["../../src/diff/merge.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAI7C
|
|
1
|
+
{"version":3,"file":"merge.d.ts","sourceRoot":"","sources":["../../src/diff/merge.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAI7C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AACH,eAAO,MAAM,KAAK,GAAI,CAAC,EAAE,OAAO,UAAU,CAAC,CAAC,CAAC,EAAE,EAAE,OAAO,UAAU,CAAC,CAAC,CAAC,EAAE;;;CAkGtE,CAAA"}
|
package/dist/diff/merge.js
CHANGED
|
@@ -6,8 +6,15 @@ import { isArrayEqual, isKeyCollisions, mergeObjects } from '../diff/utils.js';
|
|
|
6
6
|
* that arise when both diffs modify the same paths. It uses a trie data structure for
|
|
7
7
|
* efficient path matching and conflict detection.
|
|
8
8
|
*
|
|
9
|
+
* ⚠️ This function mutates the entries of `diff2`. When two changes on the same path can be folded
|
|
10
|
+
* together without a collision, the value from `diff1` is merged into the `changes` of the `diff2`
|
|
11
|
+
* entry, in place. Diffs built by `diff` carry live references into the documents they came from
|
|
12
|
+
* (see `diff`), so folding two changes together also writes into the document behind `diff2`.
|
|
13
|
+
* Callers that need the source documents to stay untouched have to deep clone them before diffing,
|
|
14
|
+
* or clone the changes afterwards.
|
|
15
|
+
*
|
|
9
16
|
* @param diff1 - First list of differences
|
|
10
|
-
* @param diff2 - Second list of differences
|
|
17
|
+
* @param diff2 - Second list of differences, whose entries are mutated, see the note above
|
|
11
18
|
* @returns Object containing:
|
|
12
19
|
* - diffs: Combined list of non-conflicting differences
|
|
13
20
|
* - conflicts: Array of conflicting difference pairs that need manual resolution
|
|
@@ -65,12 +72,16 @@ export const merge = (diff1, diff2) => {
|
|
|
65
72
|
trie.findMatch(diff.path, (value) => {
|
|
66
73
|
if (diff.type === 'delete') {
|
|
67
74
|
if (value.changes.type === 'delete') {
|
|
68
|
-
// Keep the
|
|
75
|
+
// Keep the shallowest delete operation and skip the other, since deleting an
|
|
76
|
+
// ancestor already removes everything the deeper delete would have removed.
|
|
77
|
+
// On equal paths the first list keeps the entry and the second one is skipped.
|
|
78
|
+
// Note the two sets are indexed differently: `value.index` points into `diff1`,
|
|
79
|
+
// while `index` points into `diff2`.
|
|
69
80
|
if (value.changes.path.length > diff.path.length) {
|
|
70
81
|
skipDiff1.add(value.index);
|
|
71
82
|
}
|
|
72
83
|
else {
|
|
73
|
-
skipDiff2.add(
|
|
84
|
+
skipDiff2.add(index);
|
|
74
85
|
}
|
|
75
86
|
}
|
|
76
87
|
else {
|
package/dist/diff/trie.d.ts
CHANGED
|
@@ -11,8 +11,14 @@
|
|
|
11
11
|
*/
|
|
12
12
|
export declare class TrieNode<Value> {
|
|
13
13
|
value: Value | null;
|
|
14
|
+
/**
|
|
15
|
+
* Children are keyed by path segments taken from untrusted documents, so the map has a null
|
|
16
|
+
* prototype. A plain object would resolve `children['__proto__']` to `Object.prototype`, and
|
|
17
|
+
* `addPath` would then write onto the prototype of every object in the runtime. The map is built
|
|
18
|
+
* here rather than taken as an argument, so a caller cannot hand back a polluting one.
|
|
19
|
+
*/
|
|
14
20
|
children: Record<string, TrieNode<Value>>;
|
|
15
|
-
constructor(value: Value | null
|
|
21
|
+
constructor(value: Value | null);
|
|
16
22
|
}
|
|
17
23
|
/**
|
|
18
24
|
* A trie (prefix tree) data structure implementation.
|
package/dist/diff/trie.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"trie.d.ts","sourceRoot":"","sources":["../../src/diff/trie.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH;;;;;GAKG;AACH,qBAAa,QAAQ,CAAC,KAAK;
|
|
1
|
+
{"version":3,"file":"trie.d.ts","sourceRoot":"","sources":["../../src/diff/trie.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH;;;;;GAKG;AACH,qBAAa,QAAQ,CAAC,KAAK;IASN,KAAK,EAAE,KAAK,GAAG,IAAI;IARtC;;;;;OAKG;IACI,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAsB;gBAEnD,KAAK,EAAE,KAAK,GAAG,IAAI;CACvC;AAED;;;;;;;;;;;GAWG;AACH,qBAAa,IAAI,CAAC,KAAK;IACrB,OAAO,CAAC,IAAI,CAAiB;;IAK7B;;;;;;;;;;OAUG;IACH,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,KAAK;IAcpC;;;;;;;;;;;;;;;;;OAiBG;IACH,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI;CAgC3D"}
|
package/dist/diff/trie.js
CHANGED
|
@@ -11,10 +11,15 @@
|
|
|
11
11
|
*/
|
|
12
12
|
export class TrieNode {
|
|
13
13
|
value;
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
/**
|
|
15
|
+
* Children are keyed by path segments taken from untrusted documents, so the map has a null
|
|
16
|
+
* prototype. A plain object would resolve `children['__proto__']` to `Object.prototype`, and
|
|
17
|
+
* `addPath` would then write onto the prototype of every object in the runtime. The map is built
|
|
18
|
+
* here rather than taken as an argument, so a caller cannot hand back a polluting one.
|
|
19
|
+
*/
|
|
20
|
+
children = Object.create(null);
|
|
21
|
+
constructor(value) {
|
|
16
22
|
this.value = value;
|
|
17
|
-
this.children = children;
|
|
18
23
|
}
|
|
19
24
|
}
|
|
20
25
|
/**
|
|
@@ -32,7 +37,7 @@ export class TrieNode {
|
|
|
32
37
|
export class Trie {
|
|
33
38
|
root;
|
|
34
39
|
constructor() {
|
|
35
|
-
this.root = new TrieNode(null
|
|
40
|
+
this.root = new TrieNode(null);
|
|
36
41
|
}
|
|
37
42
|
/**
|
|
38
43
|
* Adds a value to the trie at the specified path.
|
|
@@ -52,7 +57,7 @@ export class Trie {
|
|
|
52
57
|
current = current.children[dir];
|
|
53
58
|
}
|
|
54
59
|
else {
|
|
55
|
-
current.children[dir] = new TrieNode(null
|
|
60
|
+
current.children[dir] = new TrieNode(null);
|
|
56
61
|
current = current.children[dir];
|
|
57
62
|
}
|
|
58
63
|
}
|
package/dist/diff/utils.d.ts
CHANGED
|
@@ -18,6 +18,9 @@
|
|
|
18
18
|
*
|
|
19
19
|
* // Nested objects with collision
|
|
20
20
|
* isKeyCollisions({ a: { b: 1 } }, { a: { b: 2 } }) // true
|
|
21
|
+
*
|
|
22
|
+
* // An array against a plain object
|
|
23
|
+
* isKeyCollisions([1, 2], { 0: 1, 1: 2 }) // true
|
|
21
24
|
*/
|
|
22
25
|
export declare const isKeyCollisions: (a: unknown, b: unknown) => boolean;
|
|
23
26
|
/**
|
|
@@ -26,8 +29,14 @@ export declare const isKeyCollisions: (a: unknown, b: unknown) => boolean;
|
|
|
26
29
|
* ⚠️ Note: This operation assumes there are no key collisions between the objects.
|
|
27
30
|
* Use isKeyCollisions() to check for collisions before merging.
|
|
28
31
|
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
32
|
+
* ⚠️ Note: `a` is mutated in place and the subtrees `b` contributes are attached by reference, not
|
|
33
|
+
* cloned. Those subtrees stay shared with `b`, so a later write into one of them is seen through
|
|
34
|
+
* `b` as well. A key both objects already hold keeps the subtree of `a` and merges into it, so
|
|
35
|
+
* only what `b` brings along is shared. `merge` relies on this to fold two changes into one, which
|
|
36
|
+
* is how a merge ends up writing into the documents its diffs were built from.
|
|
37
|
+
*
|
|
38
|
+
* @param a - Target object to merge into, mutated in place
|
|
39
|
+
* @param b - Source object to merge from, whose subtrees are shared with the result
|
|
31
40
|
* @returns The merged object (mutates and returns a)
|
|
32
41
|
*
|
|
33
42
|
* @example
|
package/dist/diff/utils.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/diff/utils.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/diff/utils.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,eAAO,MAAM,eAAe,GAAI,GAAG,OAAO,EAAE,GAAG,OAAO,KAAG,OAoCxD,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,eAAO,MAAM,YAAY,GAAI,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAsB3G,CAAA;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,YAAY,GAAI,CAAC,EAAE,GAAG,CAAC,EAAE,EAAE,GAAG,CAAC,EAAE,YAY7C,CAAA"}
|
package/dist/diff/utils.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isPollutionKey } from '@scalar/helpers/object/prevent-pollution';
|
|
1
2
|
/**
|
|
2
3
|
* Deep check for objects for collisions
|
|
3
4
|
* Check primitives if their values are different
|
|
@@ -18,14 +19,31 @@
|
|
|
18
19
|
*
|
|
19
20
|
* // Nested objects with collision
|
|
20
21
|
* isKeyCollisions({ a: { b: 1 } }, { a: { b: 2 } }) // true
|
|
22
|
+
*
|
|
23
|
+
* // An array against a plain object
|
|
24
|
+
* isKeyCollisions([1, 2], { 0: 1, 1: 2 }) // true
|
|
21
25
|
*/
|
|
22
26
|
export const isKeyCollisions = (a, b) => {
|
|
23
27
|
if (typeof a !== typeof b) {
|
|
24
28
|
return true;
|
|
25
29
|
}
|
|
26
30
|
if (typeof a === 'object' && typeof b === 'object' && a !== null && b !== null) {
|
|
31
|
+
// An array on one side and a plain object on the other is always a collision. Comparing them
|
|
32
|
+
// key by key matches array indices against object keys, so two containers that hold the same
|
|
33
|
+
// values look mergeable, and `mergeObjects` then absorbs one into the other and drops its type.
|
|
34
|
+
// The same guard lives in `diff`, which reports a container type change as a single update.
|
|
35
|
+
if (Array.isArray(a) !== Array.isArray(b)) {
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
27
38
|
const keys = new Set([...Object.keys(a), ...Object.keys(b)]);
|
|
28
39
|
for (const key of keys) {
|
|
40
|
+
// Skip the keys that reach `Object.prototype`, so this stays in step with `mergeObjects`,
|
|
41
|
+
// which drops them. Without the skip, an own `__proto__` on one side is compared against the
|
|
42
|
+
// inherited prototype of the other and reports a collision that is not really there, turning
|
|
43
|
+
// an otherwise auto-mergeable change into a manual conflict.
|
|
44
|
+
if (isPollutionKey(key)) {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
29
47
|
if (a[key] !== undefined && b[key] !== undefined) {
|
|
30
48
|
if (isKeyCollisions(a[key], b[key])) {
|
|
31
49
|
return true;
|
|
@@ -43,8 +61,14 @@ export const isKeyCollisions = (a, b) => {
|
|
|
43
61
|
* ⚠️ Note: This operation assumes there are no key collisions between the objects.
|
|
44
62
|
* Use isKeyCollisions() to check for collisions before merging.
|
|
45
63
|
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
64
|
+
* ⚠️ Note: `a` is mutated in place and the subtrees `b` contributes are attached by reference, not
|
|
65
|
+
* cloned. Those subtrees stay shared with `b`, so a later write into one of them is seen through
|
|
66
|
+
* `b` as well. A key both objects already hold keeps the subtree of `a` and merges into it, so
|
|
67
|
+
* only what `b` brings along is shared. `merge` relies on this to fold two changes into one, which
|
|
68
|
+
* is how a merge ends up writing into the documents its diffs were built from.
|
|
69
|
+
*
|
|
70
|
+
* @param a - Target object to merge into, mutated in place
|
|
71
|
+
* @param b - Source object to merge from, whose subtrees are shared with the result
|
|
48
72
|
* @returns The merged object (mutates and returns a)
|
|
49
73
|
*
|
|
50
74
|
* @example
|
|
@@ -60,6 +84,12 @@ export const isKeyCollisions = (a, b) => {
|
|
|
60
84
|
*/
|
|
61
85
|
export const mergeObjects = (a, b) => {
|
|
62
86
|
for (const key in b) {
|
|
87
|
+
// Merging into a prototype-reaching key writes straight onto the prototype of every object in
|
|
88
|
+
// the runtime, so these keys are dropped rather than merged. `diff` skips them as well, which
|
|
89
|
+
// keeps both sides of a merge consistent.
|
|
90
|
+
if (isPollutionKey(key)) {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
63
93
|
if (!(key in a)) {
|
|
64
94
|
a[key] = b[key];
|
|
65
95
|
}
|
package/package.json
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"url": "git+https://github.com/scalar/scalar.git",
|
|
11
11
|
"directory": "packages/json-magic"
|
|
12
12
|
},
|
|
13
|
-
"version": "0.
|
|
13
|
+
"version": "0.13.2",
|
|
14
14
|
"engines": {
|
|
15
15
|
"node": ">=22"
|
|
16
16
|
},
|
|
@@ -56,6 +56,11 @@
|
|
|
56
56
|
"types": "./dist/helpers/get-segments-from-path.d.ts",
|
|
57
57
|
"default": "./dist/helpers/get-segments-from-path.js"
|
|
58
58
|
},
|
|
59
|
+
"./helpers/get-value-by-path": {
|
|
60
|
+
"import": "./dist/helpers/get-value-by-path.js",
|
|
61
|
+
"types": "./dist/helpers/get-value-by-path.d.ts",
|
|
62
|
+
"default": "./dist/helpers/get-value-by-path.js"
|
|
63
|
+
},
|
|
59
64
|
"./helpers/is-file-path": {
|
|
60
65
|
"import": "./dist/helpers/is-file-path.js",
|
|
61
66
|
"types": "./dist/helpers/is-file-path.d.ts",
|
|
@@ -66,11 +71,26 @@
|
|
|
66
71
|
"types": "./dist/helpers/is-http-url.d.ts",
|
|
67
72
|
"default": "./dist/helpers/is-http-url.js"
|
|
68
73
|
},
|
|
74
|
+
"./helpers/is-json-object": {
|
|
75
|
+
"import": "./dist/helpers/is-json-object.js",
|
|
76
|
+
"types": "./dist/helpers/is-json-object.d.ts",
|
|
77
|
+
"default": "./dist/helpers/is-json-object.js"
|
|
78
|
+
},
|
|
79
|
+
"./helpers/is-yaml": {
|
|
80
|
+
"import": "./dist/helpers/is-yaml.js",
|
|
81
|
+
"types": "./dist/helpers/is-yaml.d.ts",
|
|
82
|
+
"default": "./dist/helpers/is-yaml.js"
|
|
83
|
+
},
|
|
69
84
|
"./helpers/normalize": {
|
|
70
85
|
"import": "./dist/helpers/normalize.js",
|
|
71
86
|
"types": "./dist/helpers/normalize.d.ts",
|
|
72
87
|
"default": "./dist/helpers/normalize.js"
|
|
73
88
|
},
|
|
89
|
+
"./helpers/set-value-at-path": {
|
|
90
|
+
"import": "./dist/helpers/set-value-at-path.js",
|
|
91
|
+
"types": "./dist/helpers/set-value-at-path.d.ts",
|
|
92
|
+
"default": "./dist/helpers/set-value-at-path.js"
|
|
93
|
+
},
|
|
74
94
|
"./helpers/unescape-json-pointer": {
|
|
75
95
|
"import": "./dist/helpers/unescape-json-pointer.js",
|
|
76
96
|
"types": "./dist/helpers/unescape-json-pointer.d.ts",
|
|
@@ -89,10 +109,10 @@
|
|
|
89
109
|
"dependencies": {
|
|
90
110
|
"pathe": "^2.0.3",
|
|
91
111
|
"yaml": "^2.9.0",
|
|
92
|
-
"@scalar/helpers": "0.
|
|
112
|
+
"@scalar/helpers": "0.11.1"
|
|
93
113
|
},
|
|
94
114
|
"devDependencies": {
|
|
95
|
-
"fastify": "^5.
|
|
115
|
+
"fastify": "^5.11.2",
|
|
96
116
|
"vite": "8.1.5"
|
|
97
117
|
},
|
|
98
118
|
"scripts": {
|