@weborigami/async-tree 0.0.58 → 0.0.59

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,10 +11,12 @@ 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";
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.59",
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.59"
15
15
  },
16
16
  "scripts": {
17
17
  "test": "node --test --test-reporter=spec",
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
 
@@ -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
+ }
@@ -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
+ }
@@ -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,59 @@ 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.
192
+ *
193
+ * If the input has a custom class instance, its public properties will be
194
+ * returned as a plain object.
195
+ *
196
+ * @param {any} input
197
+ * @returns {Promise<any>}
198
+ */
199
+ export async function toPlainValue(input) {
200
+ if (input instanceof Function) {
201
+ // Invoke function
202
+ input = input();
203
+ }
204
+ if (input instanceof Promise) {
205
+ // Resolve promise
206
+ input = await input;
207
+ }
208
+
209
+ if (isPrimitive(input) || input instanceof Date) {
210
+ return input;
211
+ } else if (Tree.isTreelike(input)) {
212
+ const mapped = await Tree.map(input, (value) => toPlainValue(value));
213
+ return Tree.plain(mapped);
214
+ } else if (isStringLike(input)) {
215
+ return toString(input);
216
+ } else {
217
+ // Some other kind of class instance; return its public properties.
218
+ const plain = {};
219
+ for (const [key, value] of Object.entries(input)) {
220
+ plain[key] = await toPlainValue(value);
221
+ }
222
+ return plain;
223
+ }
224
+ }
225
+
158
226
  /**
159
227
  * Return a string form of the object, handling cases not generally handled by
160
228
  * the standard JavaScript `toString()` method:
@@ -164,6 +232,10 @@ export async function pipeline(start, ...fns) {
164
232
  * default toString() method, return null instead of "[object Object]". In
165
233
  * practice, it's generally more useful to have this method fail than to
166
234
  * return a useless string.
235
+ * 3. If the object is a defined primitive value, return the result of
236
+ * String(object).
237
+ *
238
+ * Otherwise return null.
167
239
  *
168
240
  * @param {any} object
169
241
  * @returns {string|null}
@@ -176,7 +248,7 @@ export function toString(object) {
176
248
  // https://stackoverflow.com/a/1677660/76472
177
249
  const hasNonPrintableCharacters = /[\x00-\x08\x0E-\x1F]/.test(decoded);
178
250
  return hasNonPrintableCharacters ? null : decoded;
179
- } else if (isStringLike(object)) {
251
+ } else if (isStringLike(object) || (object !== null && isPrimitive(object))) {
180
252
  return String(object);
181
253
  } else {
182
254
  return null;
@@ -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
+ });
@@ -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
+ });
@@ -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,26 @@ 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
+ assert.equal(await utilities.toPlainValue(async () => "result"), "result");
54
+ assert.deepEqual(await utilities.toPlainValue(new User("Alice")), {
55
+ name: "Alice",
56
+ });
57
+ });
58
+
39
59
  test("toString returns the value of an object's `toString` method", () => {
40
60
  const object = {
41
61
  toString: () => "text",
@@ -49,7 +69,7 @@ describe("utilities", () => {
49
69
  });
50
70
 
51
71
  test("toString decodes an ArrayBuffer as UTF-8", () => {
52
- const buffer = Buffer.from("text", "utf8");
53
- assert.equal(utilities.toString(buffer), "text");
72
+ const arrayBuffer = new TextEncoder().encode("text").buffer;
73
+ assert.equal(utilities.toString(arrayBuffer), "text");
54
74
  });
55
75
  });