@weborigami/async-tree 0.0.61 → 0.0.63
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/index.ts +1 -1
- package/package.json +2 -2
- package/src/BrowserFileTree.js +12 -3
- package/src/DeepObjectTree.js +1 -5
- package/src/FileTree.js +7 -4
- package/src/FunctionTree.js +3 -1
- package/src/MapTree.js +3 -6
- package/src/ObjectTree.js +6 -8
- package/src/SetTree.js +3 -6
- package/src/SiteTree.js +11 -4
- package/src/Tree.d.ts +3 -2
- package/src/Tree.js +67 -35
- package/src/operations/concat.js +1 -1
- package/src/operations/deepMerge.js +1 -1
- package/src/operations/deepTakeFn.js +1 -1
- package/src/operations/deepValuesIterator.js +1 -1
- package/src/operations/merge.js +16 -7
- package/src/operations/scope.js +2 -2
- package/src/transforms/mapFn.js +2 -6
- package/src/transforms/sortFn.js +3 -5
- package/src/utilities.d.ts +8 -5
- package/src/utilities.js +72 -22
- package/test/ObjectTree.test.js +9 -0
- package/test/Tree.test.js +38 -0
- package/test/operations/deepValues.test.js +6 -0
- package/test/operations/merge.test.js +3 -2
- package/test/utilities.test.js +50 -0
package/index.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@weborigami/async-tree",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.63",
|
|
4
4
|
"description": "Asynchronous tree drivers based on standard JavaScript classes",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./main.js",
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"typescript": "5.5.3"
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
|
-
"@weborigami/types": "0.0.
|
|
14
|
+
"@weborigami/types": "0.0.63"
|
|
15
15
|
},
|
|
16
16
|
"scripts": {
|
|
17
17
|
"test": "node --test --test-reporter=spec",
|
package/src/BrowserFileTree.js
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { Tree } from "./internal.js";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
hiddenFileNames,
|
|
4
|
+
isStringLike,
|
|
5
|
+
naturalOrder,
|
|
6
|
+
setParent,
|
|
7
|
+
} from "./utilities.js";
|
|
3
8
|
|
|
4
9
|
const TypedArray = Object.getPrototypeOf(Uint8Array);
|
|
5
10
|
|
|
@@ -34,7 +39,9 @@ export default class BrowserFileTree {
|
|
|
34
39
|
// Try the key as a subfolder name.
|
|
35
40
|
try {
|
|
36
41
|
const subfolderHandle = await directory.getDirectoryHandle(key);
|
|
37
|
-
|
|
42
|
+
const value = Reflect.construct(this.constructor, [subfolderHandle]);
|
|
43
|
+
setParent(value, this);
|
|
44
|
+
return value;
|
|
38
45
|
} catch (error) {
|
|
39
46
|
if (
|
|
40
47
|
!(
|
|
@@ -50,7 +57,9 @@ export default class BrowserFileTree {
|
|
|
50
57
|
try {
|
|
51
58
|
const fileHandle = await directory.getFileHandle(key);
|
|
52
59
|
const file = await fileHandle.getFile();
|
|
53
|
-
|
|
60
|
+
const buffer = file.arrayBuffer();
|
|
61
|
+
setParent(buffer, this);
|
|
62
|
+
return buffer;
|
|
54
63
|
} catch (error) {
|
|
55
64
|
if (!(error instanceof DOMException && error.name === "NotFoundError")) {
|
|
56
65
|
throw error;
|
package/src/DeepObjectTree.js
CHANGED
|
@@ -12,15 +12,11 @@ export default class DeepObjectTree extends ObjectTree {
|
|
|
12
12
|
value = Reflect.construct(this.constructor, [value]);
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
if (Tree.isAsyncTree(value) && !value.parent) {
|
|
16
|
-
value.parent = this;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
15
|
return value;
|
|
20
16
|
}
|
|
21
17
|
|
|
22
18
|
async isKeyForSubtree(key) {
|
|
23
|
-
const value = this.object[key];
|
|
19
|
+
const value = await this.object[key];
|
|
24
20
|
return isPlainObject(value) || Tree.isAsyncTree(value);
|
|
25
21
|
}
|
|
26
22
|
}
|
package/src/FileTree.js
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
isPacked,
|
|
9
9
|
isPlainObject,
|
|
10
10
|
naturalOrder,
|
|
11
|
+
setParent,
|
|
11
12
|
} from "./utilities.js";
|
|
12
13
|
|
|
13
14
|
/**
|
|
@@ -65,16 +66,18 @@ export default class FileTree {
|
|
|
65
66
|
throw error;
|
|
66
67
|
}
|
|
67
68
|
|
|
69
|
+
let value;
|
|
68
70
|
if (stats.isDirectory()) {
|
|
69
71
|
// Return subdirectory as a tree
|
|
70
|
-
|
|
71
|
-
subtree.parent = this;
|
|
72
|
-
return subtree;
|
|
72
|
+
value = Reflect.construct(this.constructor, [filePath]);
|
|
73
73
|
} else {
|
|
74
74
|
// Return file contents as a standard Uint8Array.
|
|
75
75
|
const buffer = await fs.readFile(filePath);
|
|
76
|
-
|
|
76
|
+
value = Uint8Array.from(buffer);
|
|
77
77
|
}
|
|
78
|
+
|
|
79
|
+
setParent(value, this);
|
|
80
|
+
return value;
|
|
78
81
|
}
|
|
79
82
|
|
|
80
83
|
async isKeyForSubtree(key) {
|
package/src/FunctionTree.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { setParent } from "./utilities.js";
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* A tree defined by a function and an optional domain.
|
|
3
5
|
*
|
|
@@ -30,7 +32,7 @@ export default class FunctionTree {
|
|
|
30
32
|
// eventually bind all parameters until only one remains. At that point,
|
|
31
33
|
// the above condition will apply and the function will be invoked.
|
|
32
34
|
Reflect.construct(this.constructor, [this.fn.bind(null, key)]);
|
|
33
|
-
|
|
35
|
+
setParent(value, this);
|
|
34
36
|
return value;
|
|
35
37
|
}
|
|
36
38
|
|
package/src/MapTree.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Tree } from "./internal.js";
|
|
2
|
+
import { setParent } from "./utilities.js";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* A tree backed by a JavaScript `Map` object.
|
|
@@ -22,12 +23,8 @@ export default class MapTree {
|
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
async get(key) {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
if (Tree.isAsyncTree(value) && !value.parent) {
|
|
28
|
-
value.parent = this;
|
|
29
|
-
}
|
|
30
|
-
|
|
26
|
+
const value = this.map.get(key);
|
|
27
|
+
setParent(value, this);
|
|
31
28
|
return value;
|
|
32
29
|
}
|
|
33
30
|
|
package/src/ObjectTree.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Tree } from "./internal.js";
|
|
2
|
-
import
|
|
2
|
+
import * as symbols from "./symbols.js";
|
|
3
|
+
import { getRealmObjectPrototype, setParent } from "./utilities.js";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* A tree defined by a plain object or array.
|
|
@@ -15,7 +16,7 @@ export default class ObjectTree {
|
|
|
15
16
|
*/
|
|
16
17
|
constructor(object) {
|
|
17
18
|
this.object = object;
|
|
18
|
-
this.parent = null;
|
|
19
|
+
this.parent = object[symbols.parent] ?? null;
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
/**
|
|
@@ -35,11 +36,8 @@ export default class ObjectTree {
|
|
|
35
36
|
return undefined;
|
|
36
37
|
}
|
|
37
38
|
|
|
38
|
-
let value = this.object[key];
|
|
39
|
-
|
|
40
|
-
if (Tree.isAsyncTree(value) && !value.parent) {
|
|
41
|
-
value.parent = this;
|
|
42
|
-
}
|
|
39
|
+
let value = await this.object[key];
|
|
40
|
+
setParent(value, this);
|
|
43
41
|
|
|
44
42
|
if (typeof value === "function" && !Object.hasOwn(this.object, key)) {
|
|
45
43
|
// Value is an inherited method; bind it to the object.
|
|
@@ -50,7 +48,7 @@ export default class ObjectTree {
|
|
|
50
48
|
}
|
|
51
49
|
|
|
52
50
|
async isKeyForSubtree(key) {
|
|
53
|
-
const value = this.object[key];
|
|
51
|
+
const value = await this.object[key];
|
|
54
52
|
return Tree.isAsyncTree(value);
|
|
55
53
|
}
|
|
56
54
|
|
package/src/SetTree.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Tree } from "./internal.js";
|
|
2
|
+
import { setParent } from "./utilities.js";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* A tree of Set objects.
|
|
@@ -16,12 +17,8 @@ export default class SetTree {
|
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
async get(key) {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
if (Tree.isAsyncTree(value) && !value.parent) {
|
|
22
|
-
value.parent = this;
|
|
23
|
-
}
|
|
24
|
-
|
|
20
|
+
const value = this.values[key];
|
|
21
|
+
setParent(value, this);
|
|
25
22
|
return value;
|
|
26
23
|
}
|
|
27
24
|
|
package/src/SiteTree.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Tree } from "./internal.js";
|
|
2
2
|
import * as keysJson from "./keysJson.js";
|
|
3
|
+
import { setParent } from "./utilities.js";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* A tree of values obtained via HTTP/HTTPS calls. These values will be strings
|
|
@@ -44,7 +45,9 @@ export default class SiteTree {
|
|
|
44
45
|
// If the (possibly adjusted) route ends with a slash and the site is an
|
|
45
46
|
// explorable site, we return a tree for the indicated route.
|
|
46
47
|
if (href.endsWith("/") && (await this.hasKeysJson())) {
|
|
47
|
-
|
|
48
|
+
const value = Reflect.construct(this.constructor, [href]);
|
|
49
|
+
setParent(value, this);
|
|
50
|
+
return value;
|
|
48
51
|
}
|
|
49
52
|
|
|
50
53
|
// Fetch the data at the given route.
|
|
@@ -67,9 +70,13 @@ export default class SiteTree {
|
|
|
67
70
|
}
|
|
68
71
|
|
|
69
72
|
const mediaType = response.headers?.get("Content-Type");
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
+
if (SiteTree.mediaTypeIsText(mediaType)) {
|
|
74
|
+
return response.text();
|
|
75
|
+
} else {
|
|
76
|
+
const buffer = response.arrayBuffer();
|
|
77
|
+
setParent(buffer, this);
|
|
78
|
+
return buffer;
|
|
79
|
+
}
|
|
73
80
|
}
|
|
74
81
|
|
|
75
82
|
async getKeyDictionary() {
|
package/src/Tree.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ export function assign(target: Treelike, source: Treelike): Promise<AsyncTree>;
|
|
|
5
5
|
export function clear(AsyncTree: AsyncMutableTree): Promise<void>;
|
|
6
6
|
export function entries(AsyncTree: AsyncTree): Promise<IterableIterator<any>>;
|
|
7
7
|
export function forEach(AsyncTree: AsyncTree, callbackfn: (value: any, key: any) => Promise<void>): Promise<void>;
|
|
8
|
-
export function from(obj: any): AsyncTree;
|
|
8
|
+
export function from(obj: any, options?: { deep: boolean }): AsyncTree;
|
|
9
9
|
export function has(AsyncTree: AsyncTree, key: any): Promise<boolean>;
|
|
10
10
|
export function isAsyncMutableTree(obj: any): obj is AsyncMutableTree;
|
|
11
11
|
export function isAsyncTree(obj: any): obj is AsyncTree;
|
|
@@ -13,7 +13,8 @@ export function isKeyForSubtree(tree: AsyncTree, obj: any): Promise<boolean>;
|
|
|
13
13
|
export function isTraversable(obj: any): boolean;
|
|
14
14
|
export function isTreelike(obj: any): obj is Treelike;
|
|
15
15
|
export function map(tree: Treelike, valueFn: ValueKeyFn): AsyncTree;
|
|
16
|
-
export function mapReduce(tree: Treelike, mapFn: ValueKeyFn|null, reduceFn: ReduceFn): Promise<any>;
|
|
16
|
+
export function mapReduce(tree: Treelike, mapFn: ValueKeyFn | null, reduceFn: ReduceFn): Promise<any>;
|
|
17
|
+
export function paths(tree: Treelike, base: string): string[];
|
|
17
18
|
export function plain(tree: Treelike): Promise<PlainObject>;
|
|
18
19
|
export function remove(AsyncTree: AsyncMutableTree, key: any): Promise<boolean>;
|
|
19
20
|
export function toFunction(tree: Treelike): Function;
|
package/src/Tree.js
CHANGED
|
@@ -103,32 +103,45 @@ export async function forEach(tree, callbackFn) {
|
|
|
103
103
|
/**
|
|
104
104
|
* Attempts to cast the indicated object to an async tree.
|
|
105
105
|
*
|
|
106
|
-
*
|
|
106
|
+
* If the object is a plain object, it will be converted to an ObjectTree. The
|
|
107
|
+
* optional `deep` option can be set to `true` to convert a plain object to a
|
|
108
|
+
* DeepObjectTree.
|
|
109
|
+
*
|
|
110
|
+
* @param {Treelike | Object} object
|
|
111
|
+
* @param {{ deep?: true }} [options]
|
|
107
112
|
* @returns {AsyncTree}
|
|
108
113
|
*/
|
|
109
|
-
export function from(
|
|
110
|
-
if (isAsyncTree(
|
|
114
|
+
export function from(object, options = {}) {
|
|
115
|
+
if (isAsyncTree(object)) {
|
|
111
116
|
// Argument already supports the tree interface.
|
|
112
117
|
// @ts-ignore
|
|
113
|
-
return
|
|
114
|
-
} else if (typeof
|
|
115
|
-
return new FunctionTree(
|
|
116
|
-
} else if (
|
|
117
|
-
return new MapTree(
|
|
118
|
-
} else if (
|
|
119
|
-
return new SetTree(
|
|
120
|
-
} else if (isPlainObject(
|
|
121
|
-
return new DeepObjectTree(
|
|
122
|
-
} else if (isUnpackable(
|
|
118
|
+
return object;
|
|
119
|
+
} else if (typeof object === "function") {
|
|
120
|
+
return new FunctionTree(object);
|
|
121
|
+
} else if (object instanceof Map) {
|
|
122
|
+
return new MapTree(object);
|
|
123
|
+
} else if (object instanceof Set) {
|
|
124
|
+
return new SetTree(object);
|
|
125
|
+
} else if (isPlainObject(object) || object instanceof Array) {
|
|
126
|
+
return options.deep ? new DeepObjectTree(object) : new ObjectTree(object);
|
|
127
|
+
} else if (isUnpackable(object)) {
|
|
123
128
|
async function AsyncFunction() {} // Sample async function
|
|
124
|
-
return
|
|
129
|
+
return object.unpack instanceof AsyncFunction.constructor
|
|
125
130
|
? // Async unpack: return a deferred tree.
|
|
126
|
-
new DeferredTree(
|
|
131
|
+
new DeferredTree(object.unpack)
|
|
127
132
|
: // Synchronous unpack: cast the result of unpack() to a tree.
|
|
128
|
-
from(
|
|
129
|
-
} else if (
|
|
133
|
+
from(object.unpack());
|
|
134
|
+
} else if (object && typeof object === "object") {
|
|
130
135
|
// An instance of some class.
|
|
131
|
-
return new ObjectTree(
|
|
136
|
+
return new ObjectTree(object);
|
|
137
|
+
} else if (
|
|
138
|
+
typeof object === "string" ||
|
|
139
|
+
typeof object === "number" ||
|
|
140
|
+
typeof object === "boolean"
|
|
141
|
+
) {
|
|
142
|
+
// A primitive value; box it into an object and construct a tree.
|
|
143
|
+
const boxed = utilities.box(object);
|
|
144
|
+
return new ObjectTree(boxed);
|
|
132
145
|
}
|
|
133
146
|
|
|
134
147
|
throw new TypeError("Couldn't convert argument to an async tree");
|
|
@@ -149,25 +162,23 @@ export async function has(tree, key) {
|
|
|
149
162
|
/**
|
|
150
163
|
* Return true if the indicated object is an async tree.
|
|
151
164
|
*
|
|
152
|
-
* @param {any}
|
|
165
|
+
* @param {any} obj
|
|
153
166
|
* @returns {obj is AsyncTree}
|
|
154
167
|
*/
|
|
155
|
-
export function isAsyncTree(
|
|
156
|
-
return
|
|
157
|
-
object &&
|
|
158
|
-
typeof object.get === "function" &&
|
|
159
|
-
typeof object.keys === "function"
|
|
160
|
-
);
|
|
168
|
+
export function isAsyncTree(obj) {
|
|
169
|
+
return obj && typeof obj.get === "function" && typeof obj.keys === "function";
|
|
161
170
|
}
|
|
162
171
|
|
|
163
172
|
/**
|
|
164
173
|
* Return true if the indicated object is an async mutable tree.
|
|
165
174
|
*
|
|
166
|
-
* @param {any}
|
|
175
|
+
* @param {any} obj
|
|
167
176
|
* @returns {obj is AsyncMutableTree}
|
|
168
177
|
*/
|
|
169
|
-
export function isAsyncMutableTree(
|
|
170
|
-
return
|
|
178
|
+
export function isAsyncMutableTree(obj) {
|
|
179
|
+
return (
|
|
180
|
+
isAsyncTree(obj) && typeof (/** @type {any} */ (obj).set) === "function"
|
|
181
|
+
);
|
|
171
182
|
}
|
|
172
183
|
|
|
173
184
|
/**
|
|
@@ -214,16 +225,16 @@ export function isTraversable(object) {
|
|
|
214
225
|
* Note: the `from()` method accepts any JavaScript object, but `isTreelike`
|
|
215
226
|
* returns `false` for an object that isn't one of the above types.
|
|
216
227
|
*
|
|
217
|
-
* @param {any}
|
|
228
|
+
* @param {any} obj
|
|
218
229
|
* @returns {obj is Treelike}
|
|
219
230
|
*/
|
|
220
|
-
export function isTreelike(
|
|
231
|
+
export function isTreelike(obj) {
|
|
221
232
|
return (
|
|
222
|
-
isAsyncTree(
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
isPlainObject(
|
|
233
|
+
isAsyncTree(obj) ||
|
|
234
|
+
obj instanceof Function ||
|
|
235
|
+
obj instanceof Array ||
|
|
236
|
+
obj instanceof Set ||
|
|
237
|
+
isPlainObject(obj)
|
|
227
238
|
);
|
|
228
239
|
}
|
|
229
240
|
|
|
@@ -278,6 +289,27 @@ export async function mapReduce(treelike, valueFn, reduceFn) {
|
|
|
278
289
|
return reduceFn(values, keys);
|
|
279
290
|
}
|
|
280
291
|
|
|
292
|
+
/**
|
|
293
|
+
* Returns slash-separated paths for all values in the tree.
|
|
294
|
+
*
|
|
295
|
+
* @param {Treelike} treelike
|
|
296
|
+
* @param {string} base
|
|
297
|
+
*/
|
|
298
|
+
export async function paths(treelike, base = "") {
|
|
299
|
+
const tree = from(treelike);
|
|
300
|
+
const result = [];
|
|
301
|
+
for (const key of await tree.keys()) {
|
|
302
|
+
const valuePath = base ? `${base}/${key}` : key;
|
|
303
|
+
const value = await tree.get(key);
|
|
304
|
+
if (await isAsyncTree(value)) {
|
|
305
|
+
const subPaths = await paths(value, valuePath);
|
|
306
|
+
result.push(...subPaths);
|
|
307
|
+
} else {
|
|
308
|
+
result.push(valuePath);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
return result;
|
|
312
|
+
}
|
|
281
313
|
/**
|
|
282
314
|
* Converts an asynchronous tree into a synchronous plain JavaScript object.
|
|
283
315
|
*
|
package/src/operations/concat.js
CHANGED
|
@@ -7,7 +7,7 @@ import deepValuesIterator from "./deepValuesIterator.js";
|
|
|
7
7
|
* @typedef {import("@weborigami/types").AsyncTree} AsyncTree
|
|
8
8
|
*
|
|
9
9
|
* @this {AsyncTree|null}
|
|
10
|
-
* @param {import("
|
|
10
|
+
* @param {import("../../index.ts").Treelike} treelike
|
|
11
11
|
*/
|
|
12
12
|
export default async function concatTreeValues(treelike) {
|
|
13
13
|
const strings = [];
|
|
@@ -8,7 +8,7 @@ import { Tree } from "../internal.js";
|
|
|
8
8
|
* @returns {AsyncTree & { description: string }}
|
|
9
9
|
*/
|
|
10
10
|
export default function deepMerge(...sources) {
|
|
11
|
-
let trees = sources.map((treelike) => Tree.from(treelike));
|
|
11
|
+
let trees = sources.map((treelike) => Tree.from(treelike, { deep: true }));
|
|
12
12
|
let mergeParent;
|
|
13
13
|
return {
|
|
14
14
|
description: "deepMerge",
|
|
@@ -11,7 +11,7 @@ export default function deepTakeFn(count) {
|
|
|
11
11
|
* @param {import("../../index.ts").Treelike} treelike
|
|
12
12
|
*/
|
|
13
13
|
return async function deepTakeFn(treelike) {
|
|
14
|
-
const tree = await Tree.from(treelike);
|
|
14
|
+
const tree = await Tree.from(treelike, { deep: true });
|
|
15
15
|
const { values } = await traverse(tree, count);
|
|
16
16
|
return Tree.from(values);
|
|
17
17
|
};
|
|
@@ -14,7 +14,7 @@ export default async function* deepValuesIterator(
|
|
|
14
14
|
treelike,
|
|
15
15
|
options = { expand: false }
|
|
16
16
|
) {
|
|
17
|
-
const tree = Tree.from(treelike);
|
|
17
|
+
const tree = Tree.from(treelike, { deep: true });
|
|
18
18
|
for (const key of await tree.keys()) {
|
|
19
19
|
let value = await tree.get(key);
|
|
20
20
|
|
package/src/operations/merge.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Tree } from "../internal.js";
|
|
2
|
+
import * as symbols from "../symbols.js";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Return a tree that performs a shallow merge of the given trees.
|
|
@@ -14,18 +15,24 @@ import { Tree } from "../internal.js";
|
|
|
14
15
|
* @returns {AsyncTree & { description: string, trees: AsyncTree[]}}
|
|
15
16
|
*/
|
|
16
17
|
export default function merge(...sources) {
|
|
18
|
+
const trees = sources.map((treelike) => Tree.from(treelike));
|
|
17
19
|
return {
|
|
18
20
|
description: "merge",
|
|
19
21
|
|
|
20
22
|
async get(key) {
|
|
21
23
|
// Check trees for the indicated key in reverse order.
|
|
22
|
-
for (let index =
|
|
23
|
-
const tree =
|
|
24
|
+
for (let index = trees.length - 1; index >= 0; index--) {
|
|
25
|
+
const tree = trees[index];
|
|
24
26
|
const value = await tree.get(key);
|
|
25
27
|
if (value !== undefined) {
|
|
28
|
+
// Merged tree acts as parent instead of the source tree.
|
|
26
29
|
if (Tree.isAsyncTree(value) && value.parent === tree) {
|
|
27
|
-
// Merged tree acts as parent instead of the source tree.
|
|
28
30
|
value.parent = this;
|
|
31
|
+
} else if (
|
|
32
|
+
typeof value === "object" &&
|
|
33
|
+
value?.[symbols.parent] === tree
|
|
34
|
+
) {
|
|
35
|
+
value[symbols.parent] = this;
|
|
29
36
|
}
|
|
30
37
|
return value;
|
|
31
38
|
}
|
|
@@ -35,8 +42,8 @@ export default function merge(...sources) {
|
|
|
35
42
|
|
|
36
43
|
async isKeyForSubtree(key) {
|
|
37
44
|
// Check trees for the indicated key in reverse order.
|
|
38
|
-
for (let index =
|
|
39
|
-
if (await Tree.isKeyForSubtree(
|
|
45
|
+
for (let index = trees.length - 1; index >= 0; index--) {
|
|
46
|
+
if (await Tree.isKeyForSubtree(trees[index], key)) {
|
|
40
47
|
return true;
|
|
41
48
|
}
|
|
42
49
|
}
|
|
@@ -46,7 +53,7 @@ export default function merge(...sources) {
|
|
|
46
53
|
async keys() {
|
|
47
54
|
const keys = new Set();
|
|
48
55
|
// Collect keys in the order the trees were provided.
|
|
49
|
-
for (const tree of
|
|
56
|
+
for (const tree of trees) {
|
|
50
57
|
for (const key of await tree.keys()) {
|
|
51
58
|
keys.add(key);
|
|
52
59
|
}
|
|
@@ -54,6 +61,8 @@ export default function merge(...sources) {
|
|
|
54
61
|
return keys;
|
|
55
62
|
},
|
|
56
63
|
|
|
57
|
-
trees
|
|
64
|
+
get trees() {
|
|
65
|
+
return trees;
|
|
66
|
+
},
|
|
58
67
|
};
|
|
59
68
|
}
|
package/src/operations/scope.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { Tree } from "
|
|
1
|
+
import { Tree } from "../internal.js";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* A tree's "scope" is the collection of everything in that tree and all of its
|
|
5
5
|
* ancestors.
|
|
6
6
|
*
|
|
7
7
|
* @typedef {import("@weborigami/types").AsyncTree} AsyncTree
|
|
8
|
-
* @typedef {import("
|
|
8
|
+
* @typedef {import("../../index.ts").Treelike} Treelike
|
|
9
9
|
*
|
|
10
10
|
* @param {Treelike} treelike
|
|
11
11
|
* @returns {AsyncTree & {trees: AsyncTree[]}}
|
package/src/transforms/mapFn.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { isPlainObject } from "../utilities.js";
|
|
1
|
+
import { Tree } from "../internal.js";
|
|
3
2
|
|
|
4
3
|
/**
|
|
5
4
|
* Return a transform function that maps the keys and/or values of a tree.
|
|
@@ -47,10 +46,7 @@ export default function createMapTransform(options = {}) {
|
|
|
47
46
|
* @type {import("../../index.ts").TreeTransform}
|
|
48
47
|
*/
|
|
49
48
|
return function map(treelike) {
|
|
50
|
-
const tree =
|
|
51
|
-
!deep && isPlainObject(treelike) && !Tree.isAsyncTree(treelike)
|
|
52
|
-
? new ObjectTree(treelike)
|
|
53
|
-
: Tree.from(treelike);
|
|
49
|
+
const tree = Tree.from(treelike, { deep });
|
|
54
50
|
|
|
55
51
|
// The transformed tree is actually an extension of the original tree's
|
|
56
52
|
// prototype chain. This allows the transformed tree to inherit any
|
package/src/transforms/sortFn.js
CHANGED
|
@@ -50,16 +50,14 @@ export default function sortFn(options) {
|
|
|
50
50
|
const defaultCompare = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
|
|
51
51
|
const originalCompare = compare ?? defaultCompare;
|
|
52
52
|
// Sort by the sort key.
|
|
53
|
-
|
|
54
|
-
originalCompare(a.sort, b.sort)
|
|
55
|
-
);
|
|
53
|
+
tuples.sort((a, b) => originalCompare(a.sort, b.sort));
|
|
56
54
|
// Map back to the original keys.
|
|
57
|
-
const sorted =
|
|
55
|
+
const sorted = tuples.map((pair) => pair.key);
|
|
58
56
|
return sorted;
|
|
59
57
|
} else {
|
|
60
58
|
// Use original keys as sort keys.
|
|
61
59
|
// If compare is undefined, this uses default sort order.
|
|
62
|
-
return keys.
|
|
60
|
+
return keys.slice().sort(compare);
|
|
63
61
|
}
|
|
64
62
|
};
|
|
65
63
|
return transformed;
|
package/src/utilities.d.ts
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
|
+
import { AsyncTree } from "@weborigami/types";
|
|
1
2
|
import { Packed, PlainObject, StringLike } from "../index.ts";
|
|
2
3
|
|
|
4
|
+
export function box(value: any): any;
|
|
3
5
|
export function castArrayLike(object: any): any;
|
|
4
6
|
export function getRealmObjectPrototype(object: any): any;
|
|
5
7
|
export const hiddenFileNames: string[];
|
|
6
|
-
export function isPacked(
|
|
7
|
-
export function isPlainObject(
|
|
8
|
-
export function
|
|
9
|
-
export function
|
|
8
|
+
export function isPacked(obj: any): obj is Packed;
|
|
9
|
+
export function isPlainObject(obj: any): obj is PlainObject;
|
|
10
|
+
export function isStringLike(obj: any): obj is StringLike;
|
|
11
|
+
export function isUnpackable(obj): obj is { unpack: () => any };
|
|
10
12
|
export function keysFromPath(path: string): string[];
|
|
11
13
|
export const naturalOrder: (a: string, b: string) => number;
|
|
12
14
|
export function pipeline(start: any, ...functions: Function[]): Promise<any>;
|
|
15
|
+
export function setParent(child: any, parent: AsyncTree): void;
|
|
13
16
|
export function toPlainValue(object: any): Promise<any>;
|
|
14
|
-
export function toString(object: any): string;
|
|
17
|
+
export function toString(object: any): string;
|
package/src/utilities.js
CHANGED
|
@@ -1,8 +1,30 @@
|
|
|
1
1
|
import { Tree } from "./internal.js";
|
|
2
|
+
import * as symbols from "./symbols.js";
|
|
2
3
|
|
|
3
4
|
const textDecoder = new TextDecoder();
|
|
4
5
|
const TypedArray = Object.getPrototypeOf(Uint8Array);
|
|
5
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Return the value as an object. If the value is already an object it will be
|
|
9
|
+
* returned as is. If the value is a primitive, it will be wrapped in an object:
|
|
10
|
+
* a string will be wrapped in a String object, a number will be wrapped in a
|
|
11
|
+
* Number object, and a boolean will be wrapped in a Boolean object.
|
|
12
|
+
*
|
|
13
|
+
* @param {any} value
|
|
14
|
+
*/
|
|
15
|
+
export function box(value) {
|
|
16
|
+
switch (typeof value) {
|
|
17
|
+
case "string":
|
|
18
|
+
return new String(value);
|
|
19
|
+
case "number":
|
|
20
|
+
return new Number(value);
|
|
21
|
+
case "boolean":
|
|
22
|
+
return new Boolean(value);
|
|
23
|
+
default:
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
6
28
|
/**
|
|
7
29
|
* If the given plain object has only sequential integer keys, return it as an
|
|
8
30
|
* array. Otherwise return it as is.
|
|
@@ -51,16 +73,16 @@ export const hiddenFileNames = [".DS_Store"];
|
|
|
51
73
|
* Return true if the object is in a packed form (or can be readily packed into
|
|
52
74
|
* a form) that can be given to fs.writeFile or response.write().
|
|
53
75
|
*
|
|
54
|
-
* @param {any}
|
|
55
|
-
* @returns {
|
|
76
|
+
* @param {any} obj
|
|
77
|
+
* @returns {obj is import("../index.ts").Packed}
|
|
56
78
|
*/
|
|
57
|
-
export function isPacked(
|
|
79
|
+
export function isPacked(obj) {
|
|
58
80
|
return (
|
|
59
|
-
typeof
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
81
|
+
typeof obj === "string" ||
|
|
82
|
+
obj instanceof ArrayBuffer ||
|
|
83
|
+
obj instanceof ReadableStream ||
|
|
84
|
+
obj instanceof String ||
|
|
85
|
+
obj instanceof TypedArray
|
|
64
86
|
);
|
|
65
87
|
}
|
|
66
88
|
|
|
@@ -71,23 +93,23 @@ export function isPacked(object) {
|
|
|
71
93
|
* This function also considers object-like things with no prototype (like a
|
|
72
94
|
* `Module`) as plain objects.
|
|
73
95
|
*
|
|
74
|
-
* @param {any}
|
|
75
|
-
* @returns {
|
|
96
|
+
* @param {any} obj
|
|
97
|
+
* @returns {obj is import("../index.ts").PlainObject}
|
|
76
98
|
*/
|
|
77
|
-
export function isPlainObject(
|
|
99
|
+
export function isPlainObject(obj) {
|
|
78
100
|
// From https://stackoverflow.com/q/51722354/76472
|
|
79
|
-
if (typeof
|
|
101
|
+
if (typeof obj !== "object" || obj === null) {
|
|
80
102
|
return false;
|
|
81
103
|
}
|
|
82
104
|
|
|
83
105
|
// We treat object-like things with no prototype (like a Module) as plain
|
|
84
106
|
// objects.
|
|
85
|
-
if (Object.getPrototypeOf(
|
|
107
|
+
if (Object.getPrototypeOf(obj) === null) {
|
|
86
108
|
return true;
|
|
87
109
|
}
|
|
88
110
|
|
|
89
111
|
// Do we inherit directly from Object in this realm?
|
|
90
|
-
return Object.getPrototypeOf(
|
|
112
|
+
return Object.getPrototypeOf(obj) === getRealmObjectPrototype(obj);
|
|
91
113
|
}
|
|
92
114
|
|
|
93
115
|
/**
|
|
@@ -108,15 +130,15 @@ export function isPrimitive(value) {
|
|
|
108
130
|
* Return true if the object is a string or object with a non-trival `toString`
|
|
109
131
|
* method.
|
|
110
132
|
*
|
|
111
|
-
* @param {any}
|
|
133
|
+
* @param {any} obj
|
|
112
134
|
* @returns {obj is import("../index.ts").StringLike}
|
|
113
135
|
*/
|
|
114
|
-
export function isStringLike(
|
|
115
|
-
if (typeof
|
|
136
|
+
export function isStringLike(obj) {
|
|
137
|
+
if (typeof obj === "string") {
|
|
116
138
|
return true;
|
|
117
|
-
} else if (
|
|
139
|
+
} else if (obj?.toString === undefined) {
|
|
118
140
|
return false;
|
|
119
|
-
} else if (
|
|
141
|
+
} else if (obj.toString === getRealmObjectPrototype(obj)?.toString) {
|
|
120
142
|
// The stupid Object.prototype.toString implementation always returns
|
|
121
143
|
// "[object Object]", so if that's the only toString method the object has,
|
|
122
144
|
// we return false.
|
|
@@ -126,10 +148,9 @@ export function isStringLike(object) {
|
|
|
126
148
|
}
|
|
127
149
|
}
|
|
128
150
|
|
|
129
|
-
export function isUnpackable(
|
|
151
|
+
export function isUnpackable(obj) {
|
|
130
152
|
return (
|
|
131
|
-
isPacked(
|
|
132
|
-
typeof (/** @type {any} */ (object).unpack) === "function"
|
|
153
|
+
isPacked(obj) && typeof (/** @type {any} */ (obj).unpack) === "function"
|
|
133
154
|
);
|
|
134
155
|
}
|
|
135
156
|
|
|
@@ -170,6 +191,35 @@ export async function pipeline(start, ...fns) {
|
|
|
170
191
|
return fns.reduce(async (acc, fn) => fn(await acc), start);
|
|
171
192
|
}
|
|
172
193
|
|
|
194
|
+
/**
|
|
195
|
+
* If the child object doesn't have a parent yet, set it to the indicated
|
|
196
|
+
* parent. If the child is an AsyncTree, set the `parent` property. Otherwise,
|
|
197
|
+
* set the `symbols.parent` property.
|
|
198
|
+
*
|
|
199
|
+
* @param {*} child
|
|
200
|
+
* @param {*} parent
|
|
201
|
+
*/
|
|
202
|
+
export function setParent(child, parent) {
|
|
203
|
+
if (Tree.isAsyncTree(child)) {
|
|
204
|
+
// Value is a subtree; set its parent to this tree.
|
|
205
|
+
if (!child.parent) {
|
|
206
|
+
child.parent = parent;
|
|
207
|
+
}
|
|
208
|
+
} else if (Object.isExtensible(child) && !child[symbols.parent]) {
|
|
209
|
+
// Add parent reference as a symbol to avoid polluting the object. This
|
|
210
|
+
// reference will be used if the object is later used as a tree. We set
|
|
211
|
+
// `enumerable` to false even thought this makes no practical difference
|
|
212
|
+
// (symbols are never enumerated) because it can provide a hint in the
|
|
213
|
+
// debugger that the property is for internal use.
|
|
214
|
+
Object.defineProperty(child, symbols.parent, {
|
|
215
|
+
configurable: true,
|
|
216
|
+
enumerable: false,
|
|
217
|
+
value: parent,
|
|
218
|
+
writable: true,
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
173
223
|
/**
|
|
174
224
|
* Convert the given input to the plainest possible JavaScript value. This
|
|
175
225
|
* helper is intended for functions that want to accept an argument from the ori
|
package/test/ObjectTree.test.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import assert from "node:assert";
|
|
2
2
|
import { describe, test } from "node:test";
|
|
3
3
|
import { ObjectTree, Tree } from "../src/internal.js";
|
|
4
|
+
import * as symbols from "../src/symbols.js";
|
|
4
5
|
|
|
5
6
|
describe("ObjectTree", () => {
|
|
6
7
|
test("can get the keys of the tree", async () => {
|
|
@@ -76,6 +77,14 @@ describe("ObjectTree", () => {
|
|
|
76
77
|
assert.equal(await fixture.get("prop"), "Goodbye");
|
|
77
78
|
});
|
|
78
79
|
|
|
80
|
+
test("sets parent symbol on subobjects", async () => {
|
|
81
|
+
const fixture = new ObjectTree({
|
|
82
|
+
sub: {},
|
|
83
|
+
});
|
|
84
|
+
const sub = await fixture.get("sub");
|
|
85
|
+
assert.equal(sub[symbols.parent], fixture);
|
|
86
|
+
});
|
|
87
|
+
|
|
79
88
|
test("sets parent on subtrees", async () => {
|
|
80
89
|
const fixture = new ObjectTree({
|
|
81
90
|
a: 1,
|
package/test/Tree.test.js
CHANGED
|
@@ -97,6 +97,16 @@ describe("Tree", () => {
|
|
|
97
97
|
});
|
|
98
98
|
});
|
|
99
99
|
|
|
100
|
+
test("from returns a deep object tree if deep option is true", async () => {
|
|
101
|
+
const obj = {
|
|
102
|
+
sub: {
|
|
103
|
+
a: 1,
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
const tree = Tree.from(obj, { deep: true });
|
|
107
|
+
assert(tree instanceof DeepObjectTree);
|
|
108
|
+
});
|
|
109
|
+
|
|
100
110
|
test("from() creates a deferred tree if unpack() returns a promise", async () => {
|
|
101
111
|
const obj = new String();
|
|
102
112
|
/** @type {any} */ (obj).unpack = async () => ({
|
|
@@ -108,6 +118,13 @@ describe("Tree", () => {
|
|
|
108
118
|
});
|
|
109
119
|
});
|
|
110
120
|
|
|
121
|
+
test("from() autoboxes primitive values", async () => {
|
|
122
|
+
const tree = Tree.from("Hello, world.");
|
|
123
|
+
const slice = await tree.get("slice");
|
|
124
|
+
const result = await slice(0, 5);
|
|
125
|
+
assert.equal(result, "Hello");
|
|
126
|
+
});
|
|
127
|
+
|
|
111
128
|
test("has returns true if the key exists", async () => {
|
|
112
129
|
const fixture = createFixture();
|
|
113
130
|
assert.equal(await Tree.has(fixture, "Alice.md"), true);
|
|
@@ -198,6 +215,18 @@ describe("Tree", () => {
|
|
|
198
215
|
assert.deepEqual(reduced, "1234");
|
|
199
216
|
});
|
|
200
217
|
|
|
218
|
+
test("paths returns an array of paths to the values in the tree", async () => {
|
|
219
|
+
const tree = new DeepObjectTree({
|
|
220
|
+
a: 1,
|
|
221
|
+
b: 2,
|
|
222
|
+
c: {
|
|
223
|
+
d: 3,
|
|
224
|
+
e: 4,
|
|
225
|
+
},
|
|
226
|
+
});
|
|
227
|
+
assert.deepEqual(await Tree.paths(tree), ["a", "b", "c/d", "c/e"]);
|
|
228
|
+
});
|
|
229
|
+
|
|
201
230
|
test("plain() produces a plain object version of a tree", async () => {
|
|
202
231
|
const original = {
|
|
203
232
|
a: 1,
|
|
@@ -232,6 +261,15 @@ describe("Tree", () => {
|
|
|
232
261
|
assert.deepEqual(plain, original);
|
|
233
262
|
});
|
|
234
263
|
|
|
264
|
+
test("plain() awaits async properties", async () => {
|
|
265
|
+
const object = {
|
|
266
|
+
get name() {
|
|
267
|
+
return Promise.resolve("Alice");
|
|
268
|
+
},
|
|
269
|
+
};
|
|
270
|
+
assert.deepEqual(await Tree.plain(object), { name: "Alice" });
|
|
271
|
+
});
|
|
272
|
+
|
|
235
273
|
test("remove method removes a value", async () => {
|
|
236
274
|
const fixture = createFixture();
|
|
237
275
|
await Tree.remove(fixture, "Alice.md");
|
|
@@ -19,4 +19,10 @@ describe("deepValues", () => {
|
|
|
19
19
|
const values = await deepValues(tree);
|
|
20
20
|
assert.deepEqual(values, [1, 2, 3, 4]);
|
|
21
21
|
});
|
|
22
|
+
|
|
23
|
+
test("returns in-order array of values in nested arrays", async () => {
|
|
24
|
+
const tree = [1, [2, 3], 4];
|
|
25
|
+
const values = await deepValues(tree);
|
|
26
|
+
assert.deepEqual(values, [1, 2, 3, 4]);
|
|
27
|
+
});
|
|
22
28
|
});
|
|
@@ -2,6 +2,7 @@ import assert from "node:assert";
|
|
|
2
2
|
import { describe, test } from "node:test";
|
|
3
3
|
import { Tree } from "../../src/internal.js";
|
|
4
4
|
import merge from "../../src/operations/merge.js";
|
|
5
|
+
import * as symbols from "../../src/symbols.js";
|
|
5
6
|
|
|
6
7
|
describe("merge", () => {
|
|
7
8
|
test("performs a shallow merge", async () => {
|
|
@@ -38,7 +39,7 @@ describe("merge", () => {
|
|
|
38
39
|
assert.equal(c, undefined);
|
|
39
40
|
|
|
40
41
|
// Parent of a subvalue is the merged tree
|
|
41
|
-
const b = await
|
|
42
|
-
assert.equal(b.parent, fixture);
|
|
42
|
+
const b = await fixture.get("b");
|
|
43
|
+
assert.equal(b[symbols.parent], fixture);
|
|
43
44
|
});
|
|
44
45
|
});
|
package/test/utilities.test.js
CHANGED
|
@@ -1,8 +1,31 @@
|
|
|
1
1
|
import assert from "node:assert";
|
|
2
2
|
import { describe, test } from "node:test";
|
|
3
|
+
import { ObjectTree } from "../src/internal.js";
|
|
4
|
+
import * as symbols from "../src/symbols.js";
|
|
3
5
|
import * as utilities from "../src/utilities.js";
|
|
4
6
|
|
|
5
7
|
describe("utilities", () => {
|
|
8
|
+
test("box returns a boxed value", () => {
|
|
9
|
+
const string = "string";
|
|
10
|
+
const stringObject = utilities.box(string);
|
|
11
|
+
assert(stringObject instanceof String);
|
|
12
|
+
assert.equal(stringObject, string);
|
|
13
|
+
|
|
14
|
+
const number = 1;
|
|
15
|
+
const numberObject = utilities.box(number);
|
|
16
|
+
assert(numberObject instanceof Number);
|
|
17
|
+
assert.equal(numberObject, number);
|
|
18
|
+
|
|
19
|
+
const boolean = true;
|
|
20
|
+
const booleanObject = utilities.box(boolean);
|
|
21
|
+
assert(booleanObject instanceof Boolean);
|
|
22
|
+
assert.equal(booleanObject, boolean);
|
|
23
|
+
|
|
24
|
+
const object = {};
|
|
25
|
+
const boxedObject = utilities.box(object);
|
|
26
|
+
assert.equal(boxedObject, object);
|
|
27
|
+
});
|
|
28
|
+
|
|
6
29
|
test("getRealmObjectPrototype returns the object's root prototype", () => {
|
|
7
30
|
const object = {};
|
|
8
31
|
const proto = utilities.getRealmObjectPrototype(object);
|
|
@@ -36,6 +59,33 @@ describe("utilities", () => {
|
|
|
36
59
|
assert.equal(result, 16);
|
|
37
60
|
});
|
|
38
61
|
|
|
62
|
+
test("setParent sets a child's parent", () => {
|
|
63
|
+
const parent = new ObjectTree({});
|
|
64
|
+
|
|
65
|
+
// Set [symbols.parent] on a plain object.
|
|
66
|
+
const object = {};
|
|
67
|
+
utilities.setParent(object, parent);
|
|
68
|
+
assert.equal(object[symbols.parent], parent);
|
|
69
|
+
|
|
70
|
+
// Leave [symbols.parent] alone if it's already set.
|
|
71
|
+
const childWithParent = {
|
|
72
|
+
[symbols.parent]: "parent",
|
|
73
|
+
};
|
|
74
|
+
utilities.setParent(childWithParent, parent);
|
|
75
|
+
assert.equal(childWithParent[symbols.parent], "parent");
|
|
76
|
+
|
|
77
|
+
// Set `parent` on a tree.
|
|
78
|
+
const tree = new ObjectTree({});
|
|
79
|
+
utilities.setParent(tree, parent);
|
|
80
|
+
assert.equal(tree.parent, parent);
|
|
81
|
+
|
|
82
|
+
// Leave `parent` alone if it's already set.
|
|
83
|
+
const treeWithParent = new ObjectTree({});
|
|
84
|
+
treeWithParent.parent = "parent";
|
|
85
|
+
utilities.setParent(treeWithParent, parent);
|
|
86
|
+
assert.equal(treeWithParent.parent, "parent");
|
|
87
|
+
});
|
|
88
|
+
|
|
39
89
|
test("toPlainValue returns the plainest representation of an object", async () => {
|
|
40
90
|
class User {
|
|
41
91
|
constructor(name) {
|