@weborigami/async-tree 0.0.58 → 0.0.60

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/main.js CHANGED
@@ -11,14 +11,17 @@ export { default as SiteTree } from "./src/SiteTree.js";
11
11
  export { DeepObjectTree, ObjectTree, Tree } from "./src/internal.js";
12
12
  export * as keysJson from "./src/keysJson.js";
13
13
  export { default as cache } from "./src/operations/cache.js";
14
+ export { default as concat } from "./src/operations/concat.js";
14
15
  export { default as deepMerge } from "./src/operations/deepMerge.js";
15
16
  export { default as deepTake } from "./src/operations/deepTake.js";
16
17
  export { default as deepTakeFn } from "./src/operations/deepTakeFn.js";
17
18
  export { default as deepValues } from "./src/operations/deepValues.js";
19
+ export { default as deepValuesIterator } from "./src/operations/deepValuesIterator.js";
18
20
  export { default as group } from "./src/operations/group.js";
19
21
  export { default as groupFn } from "./src/operations/groupFn.js";
20
22
  export { default as map } from "./src/operations/map.js";
21
23
  export { default as merge } from "./src/operations/merge.js";
24
+ export { default as scope } from "./src/operations/scope.js";
22
25
  export { default as sort } from "./src/operations/sort.js";
23
26
  export { default as take } from "./src/operations/take.js";
24
27
  export * as symbols from "./src/symbols.js";
package/package.json CHANGED
@@ -1,17 +1,17 @@
1
1
  {
2
2
  "name": "@weborigami/async-tree",
3
- "version": "0.0.58",
3
+ "version": "0.0.60",
4
4
  "description": "Asynchronous tree drivers based on standard JavaScript classes",
5
5
  "type": "module",
6
6
  "main": "./main.js",
7
7
  "browser": "./browser.js",
8
8
  "types": "./index.ts",
9
9
  "devDependencies": {
10
- "@types/node": "20.12.8",
11
- "typescript": "5.4.5"
10
+ "@types/node": "20.14.9",
11
+ "typescript": "5.5.3"
12
12
  },
13
13
  "dependencies": {
14
- "@weborigami/types": "0.0.58"
14
+ "@weborigami/types": "0.0.60"
15
15
  },
16
16
  "scripts": {
17
17
  "test": "node --test --test-reporter=spec",
@@ -20,7 +20,6 @@ export default class DeferredTree {
20
20
  this.treePromise = null;
21
21
  this._tree = null;
22
22
  this._parentUntilLoaded = null;
23
- this._scopeUntilLoaded = null;
24
23
  }
25
24
 
26
25
  async get(key) {
@@ -54,25 +53,6 @@ export default class DeferredTree {
54
53
  }
55
54
  }
56
55
 
57
- // HACK: The concept of scope is defined in Origami, not at the AsyncTree
58
- // level. If a DeferredTree is used to wrap an OrigamiTree, the inner
59
- // OrigamiTree will have a `scope` but not a `parent`. If someone asks the
60
- // outer deferrred tree for a scope, they'd otherwise get `undefined`, which
61
- // is incorrect. As a workaround, we introduce a `scope` getter here that
62
- // defers to the inner tree, but we need to find a way to avoid having to
63
- // introduce the concept of scope here.
64
- get scope() {
65
- return /** @type {any} */ (this._tree)?.scope;
66
- }
67
- set scope(scope) {
68
- // As with `parent`, we can defer setting of scope.
69
- if (this._tree && !(/** @type {any} */ (this._tree).scope)) {
70
- /** @type {any} */ (this._tree).scope = scope;
71
- } else {
72
- this._scopeUntilLoaded = scope;
73
- }
74
- }
75
-
76
56
  async tree() {
77
57
  if (this._tree) {
78
58
  return this._tree;
@@ -82,18 +62,13 @@ export default class DeferredTree {
82
62
  this.treePromise ??= this.loadResult().then((treelike) => {
83
63
  this._tree = Tree.from(treelike);
84
64
  if (this._parentUntilLoaded) {
85
- // Now that the tree has been loaded, we can set its parent.
65
+ // Now that the tree has been loaded, we can set its parent if it hasn't
66
+ // already been set.
86
67
  if (!this._tree.parent) {
87
68
  this._tree.parent = this._parentUntilLoaded;
88
69
  }
89
70
  this._parentUntilLoaded = null;
90
71
  }
91
- if (this._scopeUntilLoaded) {
92
- if (!(/** @type {any} */ (this._tree).scope)) {
93
- /** @type {any} */ (this._tree).scope = this._scopeUntilLoaded;
94
- }
95
- this._scopeUntilLoaded = null;
96
- }
97
72
  return this._tree;
98
73
  });
99
74
 
package/src/FileTree.js CHANGED
@@ -13,6 +13,12 @@ import {
13
13
  /**
14
14
  * A file system tree via the Node file system API.
15
15
  *
16
+ * File values are returned as Uint8Array instances. The underlying Node fs API
17
+ * returns file contents as instances of the node-specific Buffer class, but
18
+ * that class has some incompatible method implementations; see
19
+ * https://nodejs.org/api/buffer.html#buffers-and-typedarrays. For greater
20
+ * compatibility, files are returned as standard Uint8Array instances instead.
21
+ *
16
22
  * @typedef {import("@weborigami/types").AsyncMutableTree} AsyncMutableTree
17
23
  * @implements {AsyncMutableTree}
18
24
  */
@@ -65,8 +71,9 @@ export default class FileTree {
65
71
  subtree.parent = this;
66
72
  return subtree;
67
73
  } else {
68
- // Return file contents
69
- return fs.readFile(filePath);
74
+ // Return file contents as a standard Uint8Array.
75
+ const buffer = await fs.readFile(filePath);
76
+ return Uint8Array.from(buffer);
70
77
  }
71
78
  }
72
79
 
@@ -68,7 +68,7 @@ export default function treeCache(sourceTreelike, cacheTree, filterTreelike) {
68
68
  },
69
69
 
70
70
  async keys() {
71
- keys ??= source.keys();
71
+ keys ??= await source.keys();
72
72
  return keys;
73
73
  },
74
74
  };
@@ -0,0 +1,20 @@
1
+ import { toString } from "../utilities.js";
2
+ import deepValuesIterator from "./deepValuesIterator.js";
3
+
4
+ /**
5
+ * Concatenate the deep text values in a tree.
6
+ *
7
+ * @typedef {import("@weborigami/types").AsyncTree} AsyncTree
8
+ *
9
+ * @this {AsyncTree|null}
10
+ * @param {import("@weborigami/async-tree").Treelike} treelike
11
+ */
12
+ export default async function concatTreeValues(treelike) {
13
+ const strings = [];
14
+ for await (const value of deepValuesIterator(treelike, { expand: true })) {
15
+ if (value) {
16
+ strings.push(toString(value));
17
+ }
18
+ }
19
+ return strings.join("");
20
+ }
@@ -18,15 +18,28 @@ export default function deepMerge(...sources) {
18
18
 
19
19
  // Check trees for the indicated key in reverse order.
20
20
  for (let index = trees.length - 1; index >= 0; index--) {
21
- const value = await trees[index].get(key);
21
+ const tree = trees[index];
22
+ const value = await tree.get(key);
22
23
  if (Tree.isAsyncTree(value)) {
24
+ if (value.parent === tree) {
25
+ // Merged tree acts as parent instead of the source tree.
26
+ value.parent = this;
27
+ }
23
28
  subtrees.unshift(value);
24
29
  } else if (value !== undefined) {
25
30
  return value;
26
31
  }
27
32
  }
28
33
 
29
- return subtrees.length > 0 ? deepMerge(...subtrees) : undefined;
34
+ if (subtrees.length > 1) {
35
+ const merged = deepMerge(...subtrees);
36
+ merged.parent = this;
37
+ return merged;
38
+ } else if (subtrees.length === 1) {
39
+ return subtrees[0];
40
+ } else {
41
+ return undefined;
42
+ }
30
43
  },
31
44
 
32
45
  async isKeyForSubtree(key) {
@@ -13,7 +13,7 @@ export default function deepTakeFn(count) {
13
13
  return async function deepTakeFn(treelike) {
14
14
  const tree = await Tree.from(treelike);
15
15
  const { values } = await traverse(tree, count);
16
- return values;
16
+ return Tree.from(values);
17
17
  };
18
18
  }
19
19
 
@@ -1,10 +1,19 @@
1
- import { Tree } from "../internal.js";
1
+ import deepValuesIterator from "./deepValuesIterator.js";
2
2
 
3
3
  /**
4
4
  * Return the in-order exterior values of a tree as a flat array.
5
5
  *
6
6
  * @param {import("../../index.ts").Treelike} treelike
7
+ * @param {{ expand?: boolean }} [options]
7
8
  */
8
- export default async function deepValues(treelike) {
9
- return Tree.mapReduce(treelike, null, async (values) => values.flat());
9
+ export default async function deepValues(
10
+ treelike,
11
+ options = { expand: false }
12
+ ) {
13
+ const iterator = deepValuesIterator(treelike, options);
14
+ const values = [];
15
+ for await (const value of iterator) {
16
+ values.push(value);
17
+ }
18
+ return values;
10
19
  }
@@ -0,0 +1,31 @@
1
+ import { Tree } from "../internal.js";
2
+
3
+ /**
4
+ * Return an iterator that yields all values in a tree, including nested trees.
5
+ *
6
+ * If the `expand` option is true, treelike values (but not functions) will be
7
+ * expanded into nested trees and their values will be yielded.
8
+ *
9
+ * @param {import("../../index.ts").Treelike} treelike
10
+ * @param {{ expand?: boolean }} [options]
11
+ * @returns {AsyncGenerator<any, void, undefined>}
12
+ */
13
+ export default async function* deepValuesIterator(
14
+ treelike,
15
+ options = { expand: false }
16
+ ) {
17
+ const tree = Tree.from(treelike);
18
+ for (const key of await tree.keys()) {
19
+ let value = await tree.get(key);
20
+
21
+ // Recurse into child trees, but don't expand functions.
22
+ const recurse =
23
+ Tree.isAsyncTree(value) ||
24
+ (options.expand && typeof value !== "function" && Tree.isTreelike(value));
25
+ if (recurse) {
26
+ yield* deepValuesIterator(value, options);
27
+ } else {
28
+ yield value;
29
+ }
30
+ }
31
+ }
@@ -11,19 +11,22 @@ import { Tree } from "../internal.js";
11
11
  *
12
12
  * @typedef {import("@weborigami/types").AsyncTree} AsyncTree
13
13
  * @param {import("../../index.ts").Treelike[]} sources
14
- * @returns {AsyncTree & { description: string }}
14
+ * @returns {AsyncTree & { description: string, trees: AsyncTree[]}}
15
15
  */
16
16
  export default function merge(...sources) {
17
- let trees = sources.map((treelike) => Tree.from(treelike));
18
- let mergeParent;
19
17
  return {
20
18
  description: "merge",
21
19
 
22
20
  async get(key) {
23
21
  // Check trees for the indicated key in reverse order.
24
- for (let index = trees.length - 1; index >= 0; index--) {
25
- const value = await trees[index].get(key);
22
+ for (let index = this.trees.length - 1; index >= 0; index--) {
23
+ const tree = this.trees[index];
24
+ const value = await tree.get(key);
26
25
  if (value !== undefined) {
26
+ if (Tree.isAsyncTree(value) && value.parent === tree) {
27
+ // Merged tree acts as parent instead of the source tree.
28
+ value.parent = this;
29
+ }
27
30
  return value;
28
31
  }
29
32
  }
@@ -32,8 +35,8 @@ export default function merge(...sources) {
32
35
 
33
36
  async isKeyForSubtree(key) {
34
37
  // Check trees for the indicated key in reverse order.
35
- for (let index = trees.length - 1; index >= 0; index--) {
36
- if (await Tree.isKeyForSubtree(trees[index], key)) {
38
+ for (let index = this.trees.length - 1; index >= 0; index--) {
39
+ if (await Tree.isKeyForSubtree(this.trees[index], key)) {
37
40
  return true;
38
41
  }
39
42
  }
@@ -43,7 +46,7 @@ export default function merge(...sources) {
43
46
  async keys() {
44
47
  const keys = new Set();
45
48
  // Collect keys in the order the trees were provided.
46
- for (const tree of trees) {
49
+ for (const tree of this.trees) {
47
50
  for (const key of await tree.keys()) {
48
51
  keys.add(key);
49
52
  }
@@ -51,18 +54,6 @@ export default function merge(...sources) {
51
54
  return keys;
52
55
  },
53
56
 
54
- get parent() {
55
- return mergeParent;
56
- },
57
- set parent(parent) {
58
- mergeParent = parent;
59
- trees = sources.map((treelike) => {
60
- const tree = Tree.isAsyncTree(treelike)
61
- ? Object.create(treelike)
62
- : Tree.from(treelike);
63
- tree.parent = parent;
64
- return tree;
65
- });
66
- },
57
+ trees: sources.map((treelike) => Tree.from(treelike)),
67
58
  };
68
59
  }
@@ -0,0 +1,65 @@
1
+ import { Tree } from "@weborigami/async-tree";
2
+
3
+ /**
4
+ * A tree's "scope" is the collection of everything in that tree and all of its
5
+ * ancestors.
6
+ *
7
+ * @typedef {import("@weborigami/types").AsyncTree} AsyncTree
8
+ * @typedef {import("@weborigami/async-tree").Treelike} Treelike
9
+ *
10
+ * @param {Treelike} treelike
11
+ * @returns {AsyncTree & {trees: AsyncTree[]}}
12
+ */
13
+ export default function scope(treelike) {
14
+ const tree = Tree.from(treelike);
15
+
16
+ return {
17
+ // Starting with this tree, search up the parent hierarchy.
18
+ async get(key) {
19
+ /** @type {AsyncTree|null|undefined} */
20
+ let current = tree;
21
+ let value;
22
+ while (current) {
23
+ value = await current.get(key);
24
+ if (value !== undefined) {
25
+ break;
26
+ }
27
+ current = current.parent;
28
+ }
29
+ return value;
30
+ },
31
+
32
+ // Collect all keys for this tree and all parents
33
+ async keys() {
34
+ const keys = new Set();
35
+
36
+ /** @type {AsyncTree|null|undefined} */
37
+ let current = tree;
38
+ while (current) {
39
+ for (const key of await current.keys()) {
40
+ keys.add(key);
41
+ }
42
+ current = current.parent;
43
+ }
44
+
45
+ return keys;
46
+ },
47
+
48
+ // Collect all keys for this tree and all parents.
49
+ //
50
+ // This method exists for debugging purposes, as it's helpful to be able to
51
+ // quickly flatten and view the entire scope chain.
52
+ get trees() {
53
+ const result = [];
54
+
55
+ /** @type {AsyncTree|null|undefined} */
56
+ let current = tree;
57
+ while (current) {
58
+ result.push(current);
59
+ current = current.parent;
60
+ }
61
+
62
+ return result;
63
+ },
64
+ };
65
+ }
@@ -56,7 +56,9 @@ export default async function regExpKeys(treelike) {
56
56
  let value = await tree.get(key);
57
57
  if (Tree.isAsyncTree(value)) {
58
58
  value = regExpKeys(value);
59
- value.parent = result;
59
+ if (!value.parent) {
60
+ value.parent = result;
61
+ }
60
62
  }
61
63
 
62
64
  map.set(regExp, value);
@@ -10,4 +10,5 @@ export function isStringLike(object: any): object is StringLike;
10
10
  export function keysFromPath(path: string): string[];
11
11
  export const naturalOrder: (a: string, b: string) => number;
12
12
  export function pipeline(start: any, ...functions: Function[]): Promise<any>;
13
- export function toString(object: any): string;
13
+ export function toPlainValue(object: any): Promise<any>;
14
+ export function toString(object: any): string;
package/src/utilities.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { Tree } from "./internal.js";
2
+
1
3
  const textDecoder = new TextDecoder();
2
4
  const TypedArray = Object.getPrototypeOf(Uint8Array);
3
5
 
@@ -56,7 +58,6 @@ export function isPacked(object) {
56
58
  return (
57
59
  typeof object === "string" ||
58
60
  object instanceof ArrayBuffer ||
59
- object instanceof Buffer ||
60
61
  object instanceof ReadableStream ||
61
62
  object instanceof String ||
62
63
  object instanceof TypedArray
@@ -89,6 +90,20 @@ export function isPlainObject(object) {
89
90
  return Object.getPrototypeOf(object) === getRealmObjectPrototype(object);
90
91
  }
91
92
 
93
+ /**
94
+ * Return true if the value is a primitive JavaScript value.
95
+ *
96
+ * @param {any} value
97
+ */
98
+ export function isPrimitive(value) {
99
+ // Check for null first, since typeof null === "object".
100
+ if (value === null) {
101
+ return true;
102
+ }
103
+ const type = typeof value;
104
+ return type !== "object" && type !== "function";
105
+ }
106
+
92
107
  /**
93
108
  * Return true if the object is a string or object with a non-trival `toString`
94
109
  * method.
@@ -155,6 +170,72 @@ export async function pipeline(start, ...fns) {
155
170
  return fns.reduce(async (acc, fn) => fn(await acc), start);
156
171
  }
157
172
 
173
+ /**
174
+ * Convert the given input to the plainest possible JavaScript value. This
175
+ * helper is intended for functions that want to accept an argument from the ori
176
+ * CLI, which could a string, a stream of data, or some other kind of JavaScript
177
+ * object.
178
+ *
179
+ * If the input is a function, it will be invoked and its result will be
180
+ * processed.
181
+ *
182
+ * If the input is a promise, it will be resolved and its result will be
183
+ * processed.
184
+ *
185
+ * If the input is treelike, it will be converted to a plain JavaScript object,
186
+ * recursively traversing the tree and converting all values to plain types.
187
+ *
188
+ * If the input is stringlike, its text will be returned.
189
+ *
190
+ * If the input is a ArrayBuffer or typed array, it will be interpreted as UTF-8
191
+ * text if it does not contain unprintable characters. If it does, it will be
192
+ * returned as a base64-encoded string.
193
+ *
194
+ * If the input has a custom class instance, its public properties will be
195
+ * returned as a plain object.
196
+ *
197
+ * @param {any} input
198
+ * @returns {Promise<any>}
199
+ */
200
+ export async function toPlainValue(input) {
201
+ if (input instanceof Function) {
202
+ // Invoke function
203
+ input = input();
204
+ }
205
+ if (input instanceof Promise) {
206
+ // Resolve promise
207
+ input = await input;
208
+ }
209
+
210
+ if (isPrimitive(input) || input instanceof Date) {
211
+ return input;
212
+ } else if (Tree.isTreelike(input)) {
213
+ const mapped = await Tree.map(input, (value) => toPlainValue(value));
214
+ return Tree.plain(mapped);
215
+ } else if (isStringLike(input)) {
216
+ return toString(input);
217
+ } else if (input instanceof ArrayBuffer || input instanceof TypedArray) {
218
+ // Try to interpret the buffer as UTF-8 text, otherwise use base64.
219
+ const text = toString(input);
220
+ if (text !== null) {
221
+ return text;
222
+ } else {
223
+ return toBase64(input);
224
+ }
225
+ } else {
226
+ // Some other kind of class instance; return its public properties.
227
+ const plain = {};
228
+ for (const [key, value] of Object.entries(input)) {
229
+ plain[key] = await toPlainValue(value);
230
+ }
231
+ return plain;
232
+ }
233
+ }
234
+
235
+ function toBase64(object) {
236
+ return Buffer.from(object).toString("base64");
237
+ }
238
+
158
239
  /**
159
240
  * Return a string form of the object, handling cases not generally handled by
160
241
  * the standard JavaScript `toString()` method:
@@ -164,6 +245,10 @@ export async function pipeline(start, ...fns) {
164
245
  * default toString() method, return null instead of "[object Object]". In
165
246
  * practice, it's generally more useful to have this method fail than to
166
247
  * return a useless string.
248
+ * 3. If the object is a defined primitive value, return the result of
249
+ * String(object).
250
+ *
251
+ * Otherwise return null.
167
252
  *
168
253
  * @param {any} object
169
254
  * @returns {string|null}
@@ -176,7 +261,7 @@ export function toString(object) {
176
261
  // https://stackoverflow.com/a/1677660/76472
177
262
  const hasNonPrintableCharacters = /[\x00-\x08\x0E-\x1F]/.test(decoded);
178
263
  return hasNonPrintableCharacters ? null : decoded;
179
- } else if (isStringLike(object)) {
264
+ } else if (isStringLike(object) || (object !== null && isPrimitive(object))) {
180
265
  return String(object);
181
266
  } else {
182
267
  return null;
@@ -15,9 +15,11 @@ describe("DeepObjectTree", () => {
15
15
  const object = await tree.get("object");
16
16
  assert.equal(object instanceof DeepObjectTree, true);
17
17
  assert.deepEqual(await Tree.plain(object), { b: 2 });
18
+ assert.equal(object.parent, tree);
18
19
 
19
20
  const array = await tree.get("array");
20
21
  assert.equal(array instanceof DeepObjectTree, true);
21
22
  assert.deepEqual(await Tree.plain(array), [3]);
23
+ assert.equal(array.parent, tree);
22
24
  });
23
25
  });
@@ -9,6 +9,8 @@ import { Tree } from "../src/internal.js";
9
9
  const dirname = path.dirname(fileURLToPath(import.meta.url));
10
10
  const tempDirectory = path.join(dirname, "fixtures/temp");
11
11
 
12
+ const textDecoder = new TextDecoder();
13
+
12
14
  describe.only("FileTree", async () => {
13
15
  test("can get the keys of the tree", async () => {
14
16
  const fixture = createFixture("fixtures/markdown");
@@ -21,8 +23,9 @@ describe.only("FileTree", async () => {
21
23
 
22
24
  test("can get the value for a key", async () => {
23
25
  const fixture = createFixture("fixtures/markdown");
24
- const alice = await fixture.get("Alice.md");
25
- assert.equal(alice, "Hello, **Alice**.");
26
+ const buffer = await fixture.get("Alice.md");
27
+ const text = textDecoder.decode(buffer);
28
+ assert.equal(text, "Hello, **Alice**.");
26
29
  });
27
30
 
28
31
  test("getting an unsupported key returns undefined", async () => {
@@ -95,7 +98,9 @@ describe.only("FileTree", async () => {
95
98
 
96
99
  // Read them back in.
97
100
  const actualFiles = await tempFiles.get("folder");
98
- const strings = Tree.map(actualFiles, (buffer) => String(buffer));
101
+ const strings = Tree.map(actualFiles, (buffer) =>
102
+ textDecoder.decode(buffer)
103
+ );
99
104
  const plain = await Tree.plain(strings);
100
105
  assert.deepEqual(plain, obj);
101
106
 
@@ -3,6 +3,9 @@ import { beforeEach, describe, mock, test } from "node:test";
3
3
  import SiteTree from "../src/SiteTree.js";
4
4
  import { Tree } from "../src/internal.js";
5
5
 
6
+ const textDecoder = new TextDecoder();
7
+ const textEncoder = new TextEncoder();
8
+
6
9
  const mockHost = "https://mock";
7
10
 
8
11
  const mockResponses = {
@@ -53,8 +56,9 @@ describe("SiteTree", () => {
53
56
  test("can get the value for a key", async () => {
54
57
  const fixture = new SiteTree(mockHost);
55
58
  const about = fixture.resolve("about");
56
- const alice = await about.get("Alice.html");
57
- assert.equal(alice, "Hello, Alice!");
59
+ const arrayBuffer = await about.get("Alice.html");
60
+ const text = textDecoder.decode(arrayBuffer);
61
+ assert.equal(text, "Hello, Alice!");
58
62
  });
59
63
 
60
64
  test("getting an unsupported key returns undefined", async () => {
@@ -78,7 +82,7 @@ describe("SiteTree", () => {
78
82
  test("can convert a SiteGraph to a plain object", async () => {
79
83
  const fixture = new SiteTree(mockHost);
80
84
  // Convert buffers to strings.
81
- const strings = Tree.map(fixture, (value) => value.toString());
85
+ const strings = Tree.map(fixture, (value) => textDecoder.decode(value));
82
86
  assert.deepEqual(await Tree.plain(strings), {
83
87
  about: {
84
88
  "Alice.html": "Hello, Alice!",
@@ -98,8 +102,7 @@ async function mockFetch(href) {
98
102
  if (mockedResponse) {
99
103
  return Object.assign(
100
104
  {
101
- // Returns a Buffer, not an ArrayBuffer
102
- arrayBuffer: () => Buffer.from(mockedResponse.data),
105
+ arrayBuffer: () => textEncoder.encode(mockedResponse.data).buffer,
103
106
  ok: true,
104
107
  status: 200,
105
108
  text: () => mockedResponse.data,
@@ -0,0 +1,34 @@
1
+ import assert from "node:assert";
2
+ import { describe, test } from "node:test";
3
+ import FunctionTree from "../../src/FunctionTree.js";
4
+ import { Tree } from "../../src/internal.js";
5
+ import concat from "../../src/operations/concat.js";
6
+
7
+ describe("concat", () => {
8
+ test("concatenates deep tree values", async () => {
9
+ const tree = Tree.from({
10
+ a: "A",
11
+ b: "B",
12
+ c: "C",
13
+ more: {
14
+ d: "D",
15
+ e: "E",
16
+ },
17
+ });
18
+ const result = await concat.call(null, tree);
19
+ assert.equal(result, "ABCDE");
20
+ });
21
+
22
+ test("concatenates deep tree-like values", async () => {
23
+ const letters = ["a", "b", "c"];
24
+ const specimens = new FunctionTree(
25
+ (letter) => ({
26
+ lowercase: letter,
27
+ uppercase: letter.toUpperCase(),
28
+ }),
29
+ letters
30
+ );
31
+ const result = await concat.call(null, specimens);
32
+ assert.equal(result, "aAbBcC");
33
+ });
34
+ });
@@ -34,5 +34,9 @@ describe("mergeDeep", () => {
34
34
  f: 4,
35
35
  },
36
36
  });
37
+
38
+ // Parent of a subvalue is the merged tree
39
+ const a = await fixture.get("a");
40
+ assert.equal(a.parent, fixture);
37
41
  });
38
42
  });
@@ -1,5 +1,6 @@
1
1
  import assert from "node:assert";
2
2
  import { describe, test } from "node:test";
3
+ import { Tree } from "../../src/internal.js";
3
4
  import deepTakeFn from "../../src/operations/deepTakeFn.js";
4
5
 
5
6
  describe("deepTakeFn", () => {
@@ -16,6 +17,6 @@ describe("deepTakeFn", () => {
16
17
  g: 5,
17
18
  };
18
19
  const result = await deepTakeFn(4)(tree);
19
- assert.deepEqual(result, [1, 2, 3, 4]);
20
+ assert.deepEqual(await Tree.plain(result), [1, 2, 3, 4]);
20
21
  });
21
22
  });
@@ -0,0 +1,23 @@
1
+ import assert from "node:assert";
2
+ import { describe, test } from "node:test";
3
+ import { ObjectTree } from "../../src/internal.js";
4
+ import deepValuesIterator from "../../src/operations/deepValuesIterator.js";
5
+
6
+ describe("deepValuesIterator", () => {
7
+ test("returns an iterator of a tree's deep values", async () => {
8
+ const tree = new ObjectTree({
9
+ a: 1,
10
+ b: 2,
11
+ more: {
12
+ c: 3,
13
+ d: 4,
14
+ },
15
+ });
16
+ const values = [];
17
+ // The tree will be shallow, but we'll ask to expand the values.
18
+ for await (const value of deepValuesIterator(tree, { expand: true })) {
19
+ values.push(value);
20
+ }
21
+ assert.deepEqual(values, [1, 2, 3, 4]);
22
+ });
23
+ });
@@ -33,7 +33,12 @@ describe("merge", () => {
33
33
  },
34
34
  });
35
35
 
36
+ // Merge is shallow, and last tree wins, so `b/c` doesn't exist
36
37
  const c = await Tree.traverse(fixture, "b", "c");
37
38
  assert.equal(c, undefined);
39
+
40
+ // Parent of a subvalue is the merged tree
41
+ const b = await Tree.traverse(fixture, "b");
42
+ assert.equal(b.parent, fixture);
38
43
  });
39
44
  });
@@ -0,0 +1,25 @@
1
+ import assert from "node:assert";
2
+ import { describe, test } from "node:test";
3
+ import { ObjectTree } from "../../src/internal.js";
4
+ import scope from "../../src/operations/scope.js";
5
+
6
+ describe("scope", () => {
7
+ test("gets the first defined value from the scope trees", async () => {
8
+ const outer = new ObjectTree({
9
+ a: 1,
10
+ b: 2,
11
+ });
12
+ const inner = new ObjectTree({
13
+ a: 3,
14
+ });
15
+ inner.parent = outer;
16
+ const innerScope = scope(inner);
17
+ assert.deepEqual([...(await innerScope.keys())], ["a", "b"]);
18
+ // Inner tree has precedence
19
+ assert.equal(await innerScope.get("a"), 3);
20
+ // If tree doesn't have value, finds value from parent
21
+ assert.equal(await innerScope.get("b"), 2);
22
+ assert.equal(await innerScope.get("c"), undefined);
23
+ assert.deepEqual(innerScope.trees, [inner, outer]);
24
+ });
25
+ });
@@ -1,6 +1,6 @@
1
- import { Tree } from "@weborigami/async-tree";
2
1
  import assert from "node:assert";
3
2
  import { describe, test } from "node:test";
3
+ import { Tree } from "../../src/internal.js";
4
4
  import deepReverse from "../../src/transforms/deepReverse.js";
5
5
 
6
6
  describe("deepReverse", () => {
@@ -1,6 +1,6 @@
1
- import { Tree } from "@weborigami/async-tree";
2
1
  import assert from "node:assert";
3
2
  import { describe, test } from "node:test";
3
+ import { Tree } from "../../src/internal.js";
4
4
  import reverse from "../../src/transforms/reverse.js";
5
5
 
6
6
  describe("reverse", () => {
@@ -36,6 +36,31 @@ describe("utilities", () => {
36
36
  assert.equal(result, 16);
37
37
  });
38
38
 
39
+ test("toPlainValue returns the plainest representation of an object", async () => {
40
+ class User {
41
+ constructor(name) {
42
+ this.name = name;
43
+ }
44
+ }
45
+
46
+ assert.equal(await utilities.toPlainValue(1), 1);
47
+ assert.equal(await utilities.toPlainValue("string"), "string");
48
+ assert.deepEqual(await utilities.toPlainValue({ a: 1 }), { a: 1 });
49
+ assert.equal(
50
+ await utilities.toPlainValue(new TextEncoder().encode("bytes")),
51
+ "bytes"
52
+ );
53
+ // ArrayBuffer with non-printable characters should be returned as base64
54
+ assert.equal(
55
+ await utilities.toPlainValue(new Uint8Array([1, 2, 3]).buffer),
56
+ "AQID"
57
+ );
58
+ assert.equal(await utilities.toPlainValue(async () => "result"), "result");
59
+ assert.deepEqual(await utilities.toPlainValue(new User("Alice")), {
60
+ name: "Alice",
61
+ });
62
+ });
63
+
39
64
  test("toString returns the value of an object's `toString` method", () => {
40
65
  const object = {
41
66
  toString: () => "text",
@@ -49,7 +74,7 @@ describe("utilities", () => {
49
74
  });
50
75
 
51
76
  test("toString decodes an ArrayBuffer as UTF-8", () => {
52
- const buffer = Buffer.from("text", "utf8");
53
- assert.equal(utilities.toString(buffer), "text");
77
+ const arrayBuffer = new TextEncoder().encode("text").buffer;
78
+ assert.equal(utilities.toString(arrayBuffer), "text");
54
79
  });
55
80
  });