@weborigami/async-tree 0.0.48 → 0.0.50

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.
Files changed (35) hide show
  1. package/main.js +12 -6
  2. package/package.json +4 -4
  3. package/src/BrowserFileTree.js +3 -2
  4. package/src/FileTree.js +10 -4
  5. package/src/Tree.js +19 -6
  6. package/src/operations/{mergeDeep.js → deepMerge.js} +21 -4
  7. package/src/operations/deepTake.js +15 -0
  8. package/src/operations/deepTakeFn.js +37 -0
  9. package/src/operations/deepValues.js +10 -0
  10. package/src/operations/group.js +12 -0
  11. package/src/{transforms/groupBy.js → operations/groupFn.js} +2 -2
  12. package/src/operations/map.js +14 -0
  13. package/src/operations/merge.js +9 -4
  14. package/src/operations/sort.js +18 -0
  15. package/src/operations/take.js +11 -0
  16. package/src/transforms/{map.js → mapFn.js} +8 -2
  17. package/src/transforms/sortFn.js +67 -0
  18. package/src/transforms/takeFn.js +25 -0
  19. package/src/utilities.d.ts +1 -2
  20. package/src/utilities.js +10 -18
  21. package/test/operations/{mergeDeep.test.js → deepMerge.test.js} +1 -1
  22. package/test/operations/deepTakeFn.test.js +21 -0
  23. package/test/operations/deepValues.test.js +22 -0
  24. package/test/transforms/{groupBy.test.js → groupFn.test.js} +3 -3
  25. package/test/transforms/keyMapsForExtensions.test.js +1 -1
  26. package/test/transforms/{map.test.js → mapFn.test.js} +13 -13
  27. package/test/transforms/sortFn.test.js +49 -0
  28. package/test/transforms/takeFn.test.js +20 -0
  29. package/test/utilities.test.js +6 -0
  30. package/src/transforms/sort.js +0 -25
  31. package/src/transforms/sortBy.js +0 -33
  32. package/src/transforms/sortNatural.js +0 -10
  33. package/test/transforms/sort.test.js +0 -30
  34. package/test/transforms/sortBy.test.js +0 -22
  35. package/test/transforms/sortNatural.test.js +0 -21
package/main.js CHANGED
@@ -11,14 +11,20 @@ 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 deepMerge } from "./src/operations/deepMerge.js";
15
+ export { default as deepTake } from "./src/operations/deepTake.js";
16
+ export { default as deepTakeFn } from "./src/operations/deepTakeFn.js";
17
+ export { default as deepValues } from "./src/operations/deepValues.js";
18
+ export { default as group } from "./src/operations/group.js";
19
+ export { default as groupFn } from "./src/operations/groupFn.js";
20
+ export { default as map } from "./src/operations/map.js";
14
21
  export { default as merge } from "./src/operations/merge.js";
15
- export { default as mergeDeep } from "./src/operations/mergeDeep.js";
22
+ export { default as sort } from "./src/operations/sort.js";
23
+ export { default as take } from "./src/operations/take.js";
16
24
  export * as symbols from "./src/symbols.js";
17
25
  export { default as cachedKeyFunctions } from "./src/transforms/cachedKeyFunctions.js";
18
- export { default as groupBy } from "./src/transforms/groupBy.js";
19
26
  export { default as keyFunctionsForExtensions } from "./src/transforms/keyFunctionsForExtensions.js";
20
- export { default as map } from "./src/transforms/map.js";
21
- export { default as sort } from "./src/transforms/sort.js";
22
- export { default as sortBy } from "./src/transforms/sortBy.js";
23
- export { default as sortNatural } from "./src/transforms/sortNatural.js";
27
+ export { default as mapFn } from "./src/transforms/mapFn.js";
28
+ export { default as sortFn } from "./src/transforms/sortFn.js";
29
+ export { default as takeFn } from "./src/transforms/takeFn.js";
24
30
  export * from "./src/utilities.js";
package/package.json CHANGED
@@ -1,17 +1,17 @@
1
1
  {
2
2
  "name": "@weborigami/async-tree",
3
- "version": "0.0.48",
3
+ "version": "0.0.50",
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.11.27",
11
- "typescript": "5.4.2"
10
+ "@types/node": "20.12.8",
11
+ "typescript": "5.4.5"
12
12
  },
13
13
  "dependencies": {
14
- "@weborigami/types": "0.0.48"
14
+ "@weborigami/types": "0.0.50"
15
15
  },
16
16
  "scripts": {
17
17
  "test": "node --test --test-reporter=spec",
@@ -1,5 +1,5 @@
1
1
  import { Tree } from "./internal.js";
2
- import { hiddenFileNames, isStringLike, sortNatural } from "./utilities.js";
2
+ import { hiddenFileNames, isStringLike, naturalOrder } from "./utilities.js";
3
3
 
4
4
  const TypedArray = Object.getPrototypeOf(Uint8Array);
5
5
 
@@ -74,7 +74,8 @@ export default class BrowserFileTree {
74
74
  }
75
75
  // Filter out unhelpful file names.
76
76
  keys = keys.filter((key) => !hiddenFileNames.includes(key));
77
- sortNatural(keys);
77
+ keys.sort(naturalOrder);
78
+
78
79
  return keys;
79
80
  }
80
81
 
package/src/FileTree.js CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  getRealmObjectPrototype,
7
7
  hiddenFileNames,
8
8
  isPacked,
9
- sortNatural,
9
+ naturalOrder,
10
10
  } from "./utilities.js";
11
11
 
12
12
  /**
@@ -41,6 +41,11 @@ export default class FileTree {
41
41
  }
42
42
 
43
43
  async get(key) {
44
+ if (!key) {
45
+ // Undefined key or empty string key is invalid.
46
+ return undefined;
47
+ }
48
+
44
49
  const filePath = path.resolve(this.dirname, key);
45
50
 
46
51
  let stats;
@@ -91,7 +96,8 @@ export default class FileTree {
91
96
 
92
97
  // Node fs.readdir sort order appears to be unreliable; see, e.g.,
93
98
  // https://github.com/nodejs/node/issues/3232.
94
- sortNatural(names);
99
+ names.sort(naturalOrder);
100
+
95
101
  return names;
96
102
  }
97
103
 
@@ -101,7 +107,7 @@ export default class FileTree {
101
107
 
102
108
  async set(key, value) {
103
109
  // Where are we going to write this value?
104
- const stringKey = key ? String(key) : "";
110
+ const stringKey = key != null ? String(key) : "";
105
111
  const destPath = path.resolve(this.dirname, stringKey);
106
112
 
107
113
  if (value === undefined) {
@@ -189,7 +195,7 @@ function isStringLike(obj) {
189
195
  return true;
190
196
  } else if (obj?.toString === undefined) {
191
197
  return false;
192
- } else if (obj.toString === getRealmObjectPrototype(obj).toString) {
198
+ } else if (obj.toString === getRealmObjectPrototype(obj)?.toString) {
193
199
  // The stupid Object.prototype.toString implementation always returns
194
200
  // "[object Object]", so if that's the only toString method the object has,
195
201
  // we return false.
package/src/Tree.js CHANGED
@@ -3,7 +3,7 @@ import FunctionTree from "./FunctionTree.js";
3
3
  import MapTree from "./MapTree.js";
4
4
  import SetTree from "./SetTree.js";
5
5
  import { DeepObjectTree, ObjectTree } from "./internal.js";
6
- import mapTransform from "./transforms/map.js";
6
+ import mapTransform from "./transforms/mapFn.js";
7
7
  import * as utilities from "./utilities.js";
8
8
  import {
9
9
  castArrayLike,
@@ -388,11 +388,16 @@ export async function traverseOrThrow(treelike, ...keys) {
388
388
  value = await value.unpack();
389
389
  }
390
390
 
391
- // Peek ahead: if there's only one key left and it's an empty string, return
392
- // the value itself.
393
- if (remainingKeys.length === 1 && remainingKeys[0] === "") {
394
- return value;
395
- } else if (value instanceof Function) {
391
+ if (!isTreelike(value)) {
392
+ // Value isn't treelike, so can't be traversed except for a special case:
393
+ // if there's only one key left and it's an empty string, return the
394
+ // non-treelike value.
395
+ if (remainingKeys.length === 1 && remainingKeys[0] === "") {
396
+ return value;
397
+ }
398
+ }
399
+
400
+ if (value instanceof Function) {
396
401
  // Value is a function: call it with the remaining keys.
397
402
  const fn = value;
398
403
  // We'll take as many keys as the function's length, but at least one.
@@ -401,12 +406,20 @@ export async function traverseOrThrow(treelike, ...keys) {
401
406
  key = null;
402
407
  value = await fn.call(target, ...args);
403
408
  } else {
409
+ const originalValue = value;
410
+
404
411
  // Value is some other treelike object: cast it to a tree.
405
412
  const tree = from(value);
406
413
  // Get the next key.
407
414
  key = remainingKeys.shift();
408
415
  // Get the value for the key.
409
416
  value = await tree.get(key);
417
+
418
+ // The empty key as the final key is a special case: if the tree doesn't
419
+ // have a value for the empty key, use the original value.
420
+ if (value === undefined && remainingKeys.length === 0 && key === "") {
421
+ value = originalValue;
422
+ }
410
423
  }
411
424
  }
412
425
 
@@ -4,11 +4,14 @@ import { Tree } from "../internal.js";
4
4
  * Return a tree that performs a deep merge of the given trees.
5
5
  *
6
6
  * @typedef {import("@weborigami/types").AsyncTree} AsyncTree
7
+ * @param {import("../../index.ts").Treelike[]} sources
7
8
  * @returns {AsyncTree & { description: string }}
8
9
  */
9
- export default function mergeDeep(...trees) {
10
+ export default function deepMerge(...sources) {
11
+ let trees = sources.map((treelike) => Tree.from(treelike));
12
+ let mergeParent;
10
13
  return {
11
- description: "mergeDeep",
14
+ description: "deepMerge",
12
15
 
13
16
  async get(key) {
14
17
  const subtrees = [];
@@ -22,12 +25,12 @@ export default function mergeDeep(...trees) {
22
25
  }
23
26
  }
24
27
 
25
- return subtrees.length > 0 ? mergeDeep(...subtrees) : undefined;
28
+ return subtrees.length > 0 ? deepMerge(...subtrees) : undefined;
26
29
  },
27
30
 
28
31
  async isKeyForSubtree(key) {
29
32
  for (const tree of trees) {
30
- if (await tree.isKeyForSubtree(key)) {
33
+ if (await Tree.isKeyForSubtree(tree, key)) {
31
34
  return true;
32
35
  }
33
36
  }
@@ -43,5 +46,19 @@ export default function mergeDeep(...trees) {
43
46
  }
44
47
  return keys;
45
48
  },
49
+
50
+ get parent() {
51
+ return mergeParent;
52
+ },
53
+ set parent(parent) {
54
+ mergeParent = parent;
55
+ trees = sources.map((treelike) => {
56
+ const tree = Tree.isAsyncTree(treelike)
57
+ ? Object.create(treelike)
58
+ : Tree.from(treelike);
59
+ tree.parent = parent;
60
+ return tree;
61
+ });
62
+ },
46
63
  };
47
64
  }
@@ -0,0 +1,15 @@
1
+ import deepTakeFn from "./deepTakeFn.js";
2
+
3
+ /**
4
+ * Returns a function that traverses a tree deeply and returns the values of the
5
+ * first `count` keys.
6
+ *
7
+ * This is similar to `deepValues`, but it is more efficient for large trees as
8
+ * stops after `count` values.
9
+ *
10
+ * @param {import("../../index.ts").Treelike} treelike
11
+ * @param {number} count
12
+ */
13
+ export default function deepTake(treelike, count) {
14
+ return deepTakeFn(count)(treelike);
15
+ }
@@ -0,0 +1,37 @@
1
+ import { Tree } from "../internal.js";
2
+
3
+ /**
4
+ * Returns a function that traverses a tree deeply and returns the values of the
5
+ * first `count` keys.
6
+ *
7
+ * @param {number} count
8
+ */
9
+ export default function deepTakeFn(count) {
10
+ /**
11
+ * @param {import("../../index.ts").Treelike} treelike
12
+ */
13
+ return async function deepTakeFn(treelike) {
14
+ const tree = await Tree.from(treelike);
15
+ const { values } = await traverse(tree, count);
16
+ return values;
17
+ };
18
+ }
19
+
20
+ async function traverse(tree, count) {
21
+ const values = [];
22
+ for (const key of await tree.keys()) {
23
+ if (count <= 0) {
24
+ break;
25
+ }
26
+ let value = await tree.get(key);
27
+ if (Tree.isAsyncTree(value)) {
28
+ const traversed = await traverse(value, count);
29
+ values.push(...traversed.values);
30
+ count = traversed.count;
31
+ } else {
32
+ values.push(value);
33
+ count--;
34
+ }
35
+ }
36
+ return { count, values };
37
+ }
@@ -0,0 +1,10 @@
1
+ import { Tree } from "../internal.js";
2
+
3
+ /**
4
+ * Return the in-order exterior values of a tree as a flat array.
5
+ *
6
+ * @param {import("../../index.ts").Treelike} treelike
7
+ */
8
+ export default async function deepValues(treelike) {
9
+ return Tree.mapReduce(treelike, null, async (values) => values.flat());
10
+ }
@@ -0,0 +1,12 @@
1
+ import groupFn from "./groupFn.js";
2
+
3
+ /**
4
+ * Given a function that returns a grouping key for a value, returns a transform
5
+ * that applies that grouping function to a tree.
6
+ *
7
+ * @param {import("../../index.ts").Treelike} treelike
8
+ * @param {import("../../index.ts").ValueKeyFn} groupKeyFn
9
+ */
10
+ export default function group(treelike, groupKeyFn) {
11
+ return groupFn(groupKeyFn)(treelike);
12
+ }
@@ -1,4 +1,4 @@
1
- import { DeepObjectTree, Tree } from "../internal.js";
1
+ import { ObjectTree, Tree } from "../internal.js";
2
2
 
3
3
  /**
4
4
  * Given a function that returns a grouping key for a value, returns a transform
@@ -36,6 +36,6 @@ export default function createGroupByTransform(groupKeyFn) {
36
36
  }
37
37
  }
38
38
 
39
- return new DeepObjectTree(result);
39
+ return new ObjectTree(result);
40
40
  };
41
41
  }
@@ -0,0 +1,14 @@
1
+ import mapFn from "../transforms/mapFn.js";
2
+
3
+ /**
4
+ * Transform the keys and/or values of a tree.
5
+ *
6
+ * @typedef {import("../../index.ts").KeyFn} KeyFn
7
+ * @typedef {import("../../index.ts").ValueKeyFn} ValueKeyFn
8
+ *
9
+ * @param {import("../../index.ts").Treelike} treelike
10
+ * @param {ValueKeyFn|{ deep?: boolean, description?: string, needsSourceValue?: boolean, inverseKey?: KeyFn, key?: KeyFn, value?: ValueKeyFn }} options
11
+ */
12
+ export default function map(treelike, options = {}) {
13
+ return mapFn(options)(treelike);
14
+ }
@@ -1,3 +1,5 @@
1
+ import { Tree } from "../internal.js";
2
+
1
3
  /**
2
4
  * Return a tree that performs a shallow merge of the given trees.
3
5
  *
@@ -8,10 +10,11 @@
8
10
  * return a defined value, the `get` method returns undefined.
9
11
  *
10
12
  * @typedef {import("@weborigami/types").AsyncTree} AsyncTree
13
+ * @param {import("../../index.ts").Treelike[]} sources
11
14
  * @returns {AsyncTree & { description: string }}
12
15
  */
13
16
  export default function merge(...sources) {
14
- let trees = sources;
17
+ let trees = sources.map((treelike) => Tree.from(treelike));
15
18
  let mergeParent;
16
19
  return {
17
20
  description: "merge",
@@ -28,7 +31,7 @@ export default function merge(...sources) {
28
31
 
29
32
  async isKeyForSubtree(key) {
30
33
  for (const tree of trees) {
31
- if (await tree.isKeyForSubtree(key)) {
34
+ if (await Tree.isKeyForSubtree(tree, key)) {
32
35
  return true;
33
36
  }
34
37
  }
@@ -50,8 +53,10 @@ export default function merge(...sources) {
50
53
  },
51
54
  set parent(parent) {
52
55
  mergeParent = parent;
53
- trees = sources.map((source) => {
54
- const tree = Object.create(source);
56
+ trees = sources.map((treelike) => {
57
+ const tree = Tree.isAsyncTree(treelike)
58
+ ? Object.create(treelike)
59
+ : Tree.from(treelike);
55
60
  tree.parent = parent;
56
61
  return tree;
57
62
  });
@@ -0,0 +1,18 @@
1
+ import sortFn from "../transforms/sortFn.js";
2
+
3
+ /**
4
+ * Return a new tree with the original's keys sorted. A comparison function can
5
+ * be provided; by default the keys will be sorted in [natural sort
6
+ * order](https://en.wikipedia.org/wiki/Natural_sort_order).
7
+ *
8
+ * @typedef {import("@weborigami/types").AsyncTree} AsyncTree
9
+ * @typedef {(key: any, tree: AsyncTree) => any} SortKeyFn
10
+ * @typedef {{ compare?: (a: any, b: any) => number, sortKey?: SortKeyFn }}
11
+ * SortOptions
12
+ *
13
+ * @param {import("../../index.ts").Treelike} treelike
14
+ * @param {SortOptions} [options]
15
+ */
16
+ export default function sort(treelike, options) {
17
+ return sortFn(options)(treelike);
18
+ }
@@ -0,0 +1,11 @@
1
+ import takeFn from "../transforms/takeFn.js";
2
+
3
+ /**
4
+ * Returns a new tree with the number of keys limited to the indicated count.
5
+ *
6
+ * @param {import("../../index.ts").Treelike} treelike
7
+ * @param {number} count
8
+ */
9
+ export default function take(treelike, count) {
10
+ return takeFn(count)(treelike);
11
+ }
@@ -1,4 +1,5 @@
1
- import { Tree } from "../internal.js";
1
+ import { ObjectTree, Tree } from "../internal.js";
2
+ import { isPlainObject } from "../utilities.js";
2
3
 
3
4
  /**
4
5
  * Return a transform function that maps the keys and/or values of a tree.
@@ -7,6 +8,7 @@ import { Tree } from "../internal.js";
7
8
  * @typedef {import("../../index.ts").ValueKeyFn} ValueKeyFn
8
9
  *
9
10
  * @param {ValueKeyFn|{ deep?: boolean, description?: string, needsSourceValue?: boolean, inverseKey?: KeyFn, key?: KeyFn, value?: ValueKeyFn }} options
11
+ * @returns {import("../../index.ts").TreeTransform}
10
12
  */
11
13
  export default function createMapTransform(options = {}) {
12
14
  let deep;
@@ -45,7 +47,11 @@ export default function createMapTransform(options = {}) {
45
47
  * @type {import("../../index.ts").TreeTransform}
46
48
  */
47
49
  return function map(treelike) {
48
- const tree = Tree.from(treelike);
50
+ const tree =
51
+ !deep && isPlainObject(treelike) && !Tree.isAsyncTree(treelike)
52
+ ? new ObjectTree(treelike)
53
+ : Tree.from(treelike);
54
+
49
55
  // The transformed tree is actually an extension of the original tree's
50
56
  // prototype chain. This allows the transformed tree to inherit any
51
57
  // properties/methods. For example, the `parent` of the transformed tree is
@@ -0,0 +1,67 @@
1
+ import { Tree } from "../internal.js";
2
+
3
+ /**
4
+ * Return a transform function that sorts a tree's keys using a comparison
5
+ * function.
6
+ *
7
+ * If the `options` include a `sortKey` function, that will be invoked for each
8
+ * key in the tree to produce a sort key. If no `sortKey` function is provided,
9
+ * the original keys will be used as sort keys.
10
+ *
11
+ * If the `options` include a `compare` function, that will be used to compare
12
+ * sort keys.
13
+ *
14
+ * @typedef {import("@weborigami/types").AsyncTree} AsyncTree
15
+ * @typedef {(key: any, tree: AsyncTree) => any} SortKeyFn
16
+ * @typedef {{ compare?: (a: any, b: any) => number, sortKey?: SortKeyFn }}
17
+ * SortOptions
18
+ *
19
+ * @param {SortOptions} [options]
20
+ */
21
+ export default function sortFn(options) {
22
+ const sortKey = options?.sortKey;
23
+ let compare = options?.compare;
24
+
25
+ /**
26
+ * @type {import("../../index.ts").TreeTransform}
27
+ */
28
+ return function sort(treelike) {
29
+ const tree = Tree.from(treelike);
30
+ const transformed = Object.create(tree);
31
+ transformed.keys = async () => {
32
+ const keys = Array.from(await tree.keys());
33
+
34
+ if (sortKey) {
35
+ // Invoke the async sortKey function to get sort keys.
36
+ // Create { key, sortKey } tuples.
37
+ const tuples = await Promise.all(
38
+ keys.map(async (key) => {
39
+ const sort = await sortKey(key, tree);
40
+ if (sort === undefined) {
41
+ throw new Error(
42
+ `sortKey function returned undefined for key ${key}`
43
+ );
44
+ }
45
+ return { key, sort };
46
+ })
47
+ );
48
+
49
+ // Wrap the comparison function so it applies to sort keys.
50
+ const defaultCompare = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
51
+ const originalCompare = compare ?? defaultCompare;
52
+ // Sort by the sort key.
53
+ const sortedTuples = tuples.toSorted((a, b) =>
54
+ originalCompare(a.sort, b.sort)
55
+ );
56
+ // Map back to the original keys.
57
+ const sorted = sortedTuples.map((pair) => pair.key);
58
+ return sorted;
59
+ } else {
60
+ // Use original keys as sort keys.
61
+ // If compare is undefined, this uses default sort order.
62
+ return keys.toSorted(compare);
63
+ }
64
+ };
65
+ return transformed;
66
+ };
67
+ }
@@ -0,0 +1,25 @@
1
+ import { Tree } from "../internal.js";
2
+
3
+ /**
4
+ * Limit the number of keys to the indicated count.
5
+ *
6
+ * @param {number} count
7
+ */
8
+ export default function take(count) {
9
+ /**
10
+ * @type {import("../../index.ts").TreeTransform}
11
+ */
12
+ return (treelike) => {
13
+ const tree = Tree.from(treelike);
14
+ return {
15
+ async keys() {
16
+ const keys = Array.from(await tree.keys());
17
+ return keys.slice(0, count);
18
+ },
19
+
20
+ async get(key) {
21
+ return tree.get(key);
22
+ },
23
+ };
24
+ };
25
+ }
@@ -8,5 +8,4 @@ export function isPlainObject(object: any): object is PlainObject;
8
8
  export function isUnpackable(object): object is { unpack: () => any };
9
9
  export function isStringLike(obj: any): obj is StringLike;
10
10
  export function keysFromPath(path: string): string[];
11
- export const naturalSortCompareFn: (a: string, b: string) => number;
12
- export function sortNatural(array: string[]): void;
11
+ export const naturalOrder: (a: string, b: string) => number;
package/src/utilities.js CHANGED
@@ -30,6 +30,10 @@ export function castArrayLike(object) {
30
30
  * @param {any} object
31
31
  */
32
32
  export function getRealmObjectPrototype(object) {
33
+ if (Object.getPrototypeOf(object) === null) {
34
+ // The object has no prototype.
35
+ return null;
36
+ }
33
37
  let proto = object;
34
38
  while (Object.getPrototypeOf(proto) !== null) {
35
39
  proto = Object.getPrototypeOf(proto);
@@ -96,7 +100,7 @@ export function isStringLike(object) {
96
100
  return true;
97
101
  } else if (object?.toString === undefined) {
98
102
  return false;
99
- } else if (object.toString === getRealmObjectPrototype(object).toString) {
103
+ } else if (object.toString === getRealmObjectPrototype(object)?.toString) {
100
104
  // The stupid Object.prototype.toString implementation always returns
101
105
  // "[object Object]", so if that's the only toString method the object has,
102
106
  // we return false.
@@ -131,22 +135,10 @@ export function keysFromPath(pathname) {
131
135
  return keys;
132
136
  }
133
137
 
134
- // Used for natural sort order
135
- export const naturalSortCompareFn = new Intl.Collator(undefined, {
136
- numeric: true,
137
- }).compare;
138
-
139
138
  /**
140
- * Sort the given array using [natural sort
141
- * order](https://en.wikipedia.org/wiki/Natural_sort_order). Like the native
142
- * `Array` `sort` function, this operation destructively modifies the array in
143
- * place.
144
- *
145
- * The default sort order for some sources like operating system files can be
146
- * unpredictable. Since it's quite common for file names to include numbers, it
147
- * can helpful to use natural sort order instead: ["file1", "file9", "file10"]
148
- * instead of ["file1", "file10", "file9"].
139
+ * Compare two strings using [natural sort
140
+ * order](https://en.wikipedia.org/wiki/Natural_sort_order).
149
141
  */
150
- export function sortNatural(array) {
151
- array.sort(naturalSortCompareFn);
152
- }
142
+ export const naturalOrder = new Intl.Collator(undefined, {
143
+ numeric: true,
144
+ }).compare;
@@ -1,7 +1,7 @@
1
1
  import assert from "node:assert";
2
2
  import { describe, test } from "node:test";
3
3
  import { DeepObjectTree, Tree } from "../../src/internal.js";
4
- import mergeDeep from "../../src/operations/mergeDeep.js";
4
+ import mergeDeep from "../../src/operations/deepMerge.js";
5
5
 
6
6
  describe("mergeDeep", () => {
7
7
  test("can merge deep", async () => {
@@ -0,0 +1,21 @@
1
+ import assert from "node:assert";
2
+ import { describe, test } from "node:test";
3
+ import deepTakeFn from "../../src/operations/deepTakeFn.js";
4
+
5
+ describe("deepTakeFn", () => {
6
+ test("traverses deeply and returns a limited number of items", async () => {
7
+ const tree = {
8
+ a: 1,
9
+ b: {
10
+ c: 2,
11
+ d: {
12
+ e: 3,
13
+ },
14
+ f: 4,
15
+ },
16
+ g: 5,
17
+ };
18
+ const result = await deepTakeFn(4)(tree);
19
+ assert.deepEqual(result, [1, 2, 3, 4]);
20
+ });
21
+ });
@@ -0,0 +1,22 @@
1
+ import assert from "node:assert";
2
+ import { describe, test } from "node:test";
3
+ import deepValues from "../../src/operations/deepValues.js";
4
+
5
+ describe("deepValues", () => {
6
+ test("returns in-order array of a tree's values", async () => {
7
+ const tree = {
8
+ a: 1,
9
+ b: {
10
+ c: 2,
11
+ d: {
12
+ e: 3,
13
+ },
14
+ },
15
+ f: {
16
+ g: 4,
17
+ },
18
+ };
19
+ const values = await deepValues(tree);
20
+ assert.deepEqual(values, [1, 2, 3, 4]);
21
+ });
22
+ });
@@ -1,9 +1,9 @@
1
1
  import assert from "node:assert";
2
2
  import { describe, test } from "node:test";
3
3
  import { Tree } from "../../src/internal.js";
4
- import groupBy from "../../src/transforms/groupBy.js";
4
+ import groupFn from "../../src/operations/groupFn.js";
5
5
 
6
- describe("groupBy transform", () => {
6
+ describe("groupFn transform", () => {
7
7
  test("groups using a group key function", async () => {
8
8
  const fonts = [
9
9
  { name: "Aboreto", tags: ["Sans Serif"] },
@@ -12,7 +12,7 @@ describe("groupBy transform", () => {
12
12
  { name: "Work Sans", tags: ["Grotesque", "Sans Serif"] },
13
13
  ];
14
14
  const tree = Tree.from(fonts);
15
- const grouped = await groupBy((value, key, tree) => value.tags)(tree);
15
+ const grouped = await groupFn((value, key, tree) => value.tags)(tree);
16
16
  assert.deepEqual(await Tree.plain(grouped), {
17
17
  Geometric: [{ name: "Albert Sans", tags: ["Geometric", "Sans Serif"] }],
18
18
  Grotesque: [{ name: "Work Sans", tags: ["Grotesque", "Sans Serif"] }],
@@ -2,7 +2,7 @@ import assert from "node:assert";
2
2
  import { describe, test } from "node:test";
3
3
  import { ObjectTree, Tree } from "../../src/internal.js";
4
4
  import keyFunctionsForExtensions from "../../src/transforms/keyFunctionsForExtensions.js";
5
- import map from "../../src/transforms/map.js";
5
+ import map from "../../src/transforms/mapFn.js";
6
6
 
7
7
  describe("keyMapsForExtensions", () => {
8
8
  test("returns key functions that pass a matching key through", async () => {
@@ -2,15 +2,15 @@ import assert from "node:assert";
2
2
  import { describe, test } from "node:test";
3
3
  import FunctionTree from "../../src/FunctionTree.js";
4
4
  import { DeepObjectTree, ObjectTree, Tree } from "../../src/internal.js";
5
- import map from "../../src/transforms/map.js";
5
+ import mapFn from "../../src/transforms/mapFn.js";
6
6
 
7
- describe("map", () => {
7
+ describe("mapFn", () => {
8
8
  test("returns identity graph if no key or value", async () => {
9
9
  const tree = {
10
10
  a: "letter a",
11
11
  b: "letter b",
12
12
  };
13
- const mapped = map()(tree);
13
+ const mapped = mapFn()(tree);
14
14
  assert.deepEqual(await Tree.plain(mapped), {
15
15
  a: "letter a",
16
16
  b: "letter b",
@@ -23,7 +23,7 @@ describe("map", () => {
23
23
  b: "letter b",
24
24
  c: undefined, // Won't be mapped
25
25
  });
26
- const uppercaseValues = map({
26
+ const uppercaseValues = mapFn({
27
27
  value: (sourceValue, sourceKey, innerTree) => {
28
28
  assert(sourceKey === "a" || sourceKey === "b");
29
29
  assert.equal(innerTree, tree);
@@ -43,7 +43,7 @@ describe("map", () => {
43
43
  a: "letter a",
44
44
  b: "letter b",
45
45
  };
46
- const uppercaseValues = map((sourceValue, sourceKey, tree) => {
46
+ const uppercaseValues = mapFn((sourceValue, sourceKey, tree) => {
47
47
  assert(sourceKey === "a" || sourceKey === "b");
48
48
  return sourceValue.toUpperCase();
49
49
  });
@@ -59,7 +59,7 @@ describe("map", () => {
59
59
  a: "letter a",
60
60
  b: "letter b",
61
61
  };
62
- const doubleKeys = map({
62
+ const doubleKeys = mapFn({
63
63
  key: async (sourceKey, tree) => `_${sourceKey}`,
64
64
  inverseKey: async (resultKey, tree) => resultKey.slice(1),
65
65
  });
@@ -75,7 +75,7 @@ describe("map", () => {
75
75
  a: "letter a",
76
76
  b: "letter b",
77
77
  };
78
- const doubleKeysUppercaseValues = map({
78
+ const doubleKeysUppercaseValues = mapFn({
79
79
  key: async (sourceKey, tree) => `_${sourceKey}`,
80
80
  inverseKey: async (resultKey, tree) => resultKey.slice(1),
81
81
  value: async (sourceValue, sourceKey, tree) => sourceValue.toUpperCase(),
@@ -94,7 +94,7 @@ describe("map", () => {
94
94
  b: "letter b",
95
95
  },
96
96
  };
97
- const doubleKeys = map({
97
+ const doubleKeys = mapFn({
98
98
  key: async (sourceKey, tree) => `_${sourceKey}`,
99
99
  inverseKey: async (resultKey, tree) => resultKey.slice(1),
100
100
  value: async (sourceValue, sourceKey, tree) => sourceKey,
@@ -114,7 +114,7 @@ describe("map", () => {
114
114
  a: "letter a",
115
115
  b: "letter b",
116
116
  };
117
- const mapped = map(uppercase)(tree);
117
+ const mapped = mapFn(uppercase)(tree);
118
118
  assert.deepEqual(await Tree.plain(mapped), {
119
119
  _a: "LETTER A",
120
120
  _b: "LETTER B",
@@ -128,7 +128,7 @@ describe("map", () => {
128
128
  b: "letter b",
129
129
  },
130
130
  });
131
- const uppercaseValues = map({
131
+ const uppercaseValues = mapFn({
132
132
  deep: true,
133
133
  value: (sourceValue, sourceKey, tree) => sourceValue.toUpperCase(),
134
134
  });
@@ -148,7 +148,7 @@ describe("map", () => {
148
148
  b: "letter b",
149
149
  },
150
150
  });
151
- const doubleKeys = map({
151
+ const doubleKeys = mapFn({
152
152
  deep: true,
153
153
  key: async (sourceKey, tree) => `_${sourceKey}`,
154
154
  inverseKey: async (resultKey, tree) => resultKey.slice(1),
@@ -169,7 +169,7 @@ describe("map", () => {
169
169
  b: "letter b",
170
170
  },
171
171
  });
172
- const doubleKeysUppercaseValues = map({
172
+ const doubleKeysUppercaseValues = mapFn({
173
173
  deep: true,
174
174
  key: async (sourceKey, tree) => `_${sourceKey}`,
175
175
  inverseKey: async (resultKey, tree) => resultKey.slice(1),
@@ -189,7 +189,7 @@ describe("map", () => {
189
189
  const tree = new FunctionTree(() => {
190
190
  flag = true;
191
191
  }, ["a", "b", "c"]);
192
- const mapped = map({
192
+ const mapped = mapFn({
193
193
  needsSourceValue: false,
194
194
  value: () => "X",
195
195
  })(tree);
@@ -0,0 +1,49 @@
1
+ import assert from "node:assert";
2
+ import { describe, test } from "node:test";
3
+ import { Tree } from "../../src/internal.js";
4
+ import sortFn from "../../src/transforms/sortFn.js";
5
+
6
+ describe("sortFn", () => {
7
+ test("sorts keys using default sort order", async () => {
8
+ const tree = Tree.from({
9
+ file10: null,
10
+ file1: null,
11
+ file9: null,
12
+ });
13
+ const sorted = sortFn()(tree);
14
+ assert.deepEqual(Array.from(await sorted.keys()), [
15
+ "file1",
16
+ "file10",
17
+ "file9",
18
+ ]);
19
+ });
20
+
21
+ test("invokes a comparison function", async () => {
22
+ const tree = Tree.from({
23
+ b: 2,
24
+ c: 3,
25
+ a: 1,
26
+ });
27
+ // Reverse order
28
+ const compare = (a, b) => (a > b ? -1 : a < b ? 1 : 0);
29
+ const sorted = sortFn({ compare })(tree);
30
+ assert.deepEqual(Array.from(await sorted.keys()), ["c", "b", "a"]);
31
+ });
32
+
33
+ test("invokes a sortKey function", async () => {
34
+ const tree = {
35
+ Alice: { age: 48 },
36
+ Bob: { age: 36 },
37
+ Carol: { age: 42 },
38
+ };
39
+ const transform = await sortFn({
40
+ sortKey: async (key, tree) => Tree.traverse(tree, key, "age"),
41
+ });
42
+ const result = transform(tree);
43
+ assert.deepEqual(Array.from(await result.keys()), [
44
+ "Bob",
45
+ "Carol",
46
+ "Alice",
47
+ ]);
48
+ });
49
+ });
@@ -0,0 +1,20 @@
1
+ import assert from "node:assert";
2
+ import { describe, test } from "node:test";
3
+ import { Tree } from "../../src/internal.js";
4
+ import takeFn from "../../src/transforms/takeFn.js";
5
+
6
+ describe("takeFn", () => {
7
+ test("limits the number of keys to the indicated count", async () => {
8
+ const tree = {
9
+ a: 1,
10
+ b: 2,
11
+ c: 3,
12
+ d: 4,
13
+ };
14
+ const result = await takeFn(2)(tree);
15
+ assert.deepEqual(await Tree.plain(result), {
16
+ a: 1,
17
+ b: 2,
18
+ });
19
+ });
20
+ });
@@ -21,4 +21,10 @@ describe("utilities", () => {
21
21
  assert.deepEqual(utilities.keysFromPath("a/b/c"), ["a", "b", "c"]);
22
22
  assert.deepEqual(utilities.keysFromPath("foo/"), ["foo", ""]);
23
23
  });
24
+
25
+ test("naturalOrder compares strings in natural order", () => {
26
+ const strings = ["file10", "file1", "file9"];
27
+ strings.sort(utilities.naturalOrder);
28
+ assert.deepEqual(strings, ["file1", "file9", "file10"]);
29
+ });
24
30
  });
@@ -1,25 +0,0 @@
1
- import { Tree } from "../internal.js";
2
-
3
- /**
4
- * Return a transform function that sorts a tree's keys.
5
- *
6
- * For sorting, the keys are converted to strings, then sorted according to each
7
- * character's Unicode code point value.
8
- *
9
- * @param {(a: any, b: any) => number} [compareFn]
10
- */
11
- export default function createSortTransform(compareFn) {
12
- /**
13
- * @type {import("../../index.ts").TreeTransform}
14
- */
15
- return function sortTransform(treelike) {
16
- const tree = Tree.from(treelike);
17
- const transform = Object.create(tree);
18
- transform.keys = async () => {
19
- const keys = Array.from(await tree.keys());
20
- keys.sort(compareFn);
21
- return keys;
22
- };
23
- return transform;
24
- };
25
- }
@@ -1,33 +0,0 @@
1
- import { Tree } from "../internal.js";
2
-
3
- /**
4
- * Return a transform function that sorts a tree's keys.
5
- *
6
- * The given `sortKeyFn` is invoked for each key/value pair in the tree, and
7
- * should produce a sort key for that pair. The sort keys are then compared to
8
- * determine the sort order for the tree's keys.
9
- *
10
- * @param {import("./map.js").KeyFn} sortKeyFn
11
- */
12
- export default function createSortByTransform(sortKeyFn) {
13
- /**
14
- * @type {import("../../index.ts").TreeTransform}
15
- */
16
- return function sortByTransform(treelike) {
17
- const tree = Tree.from(treelike);
18
- const transform = Object.create(tree);
19
- transform.keys = async () => {
20
- const keysAndSortKeys = [];
21
- for (const key of await tree.keys()) {
22
- const sortKey = await sortKeyFn(key, tree);
23
- keysAndSortKeys.push({ key, sortKey });
24
- }
25
- keysAndSortKeys.sort((a, b) =>
26
- a.sortKey < b.sortKey ? -1 : a.sortKey > b.sortKey ? 1 : 0
27
- );
28
- const sortedKeys = keysAndSortKeys.map(({ key }) => key);
29
- return sortedKeys;
30
- };
31
- return transform;
32
- };
33
- }
@@ -1,10 +0,0 @@
1
- import { naturalSortCompareFn } from "../utilities.js";
2
- import sort from "./sort.js";
3
-
4
- /**
5
- * Return a transform function that sorts a tree's keys using [natural sort
6
- * order](https://en.wikipedia.org/wiki/Natural_sort_order).
7
- */
8
- export default function sortNaturalTransform() {
9
- return sort(naturalSortCompareFn);
10
- }
@@ -1,30 +0,0 @@
1
- import assert from "node:assert";
2
- import { describe, test } from "node:test";
3
- import { Tree } from "../../src/internal.js";
4
- import sort from "../../src/transforms/sort.js";
5
-
6
- describe("sort transform", () => {
7
- test("sorts keys", async () => {
8
- const tree = Tree.from({
9
- b: 2,
10
- c: 3,
11
- a: 1,
12
- });
13
- const sortTransform = sort();
14
- const sorted = await sortTransform(tree);
15
- assert.deepEqual(Array.from(await sorted.keys()), ["a", "b", "c"]);
16
- });
17
-
18
- test("sorts keys using a comparison function", async () => {
19
- const tree = Tree.from({
20
- b: 2,
21
- c: 3,
22
- a: 1,
23
- });
24
- // Reverse order
25
- const compareFn = (a, b) => (a > b ? -1 : a < b ? 1 : 0);
26
- const sortTransform = sort(compareFn);
27
- const sorted = await sortTransform(tree);
28
- assert.deepEqual(Array.from(await sorted.keys()), ["c", "b", "a"]);
29
- });
30
- });
@@ -1,22 +0,0 @@
1
- import assert from "node:assert";
2
- import { describe, test } from "node:test";
3
- import { Tree } from "../../src/internal.js";
4
- import sortBy from "../../src/transforms/sortBy.js";
5
-
6
- describe("sortBy transform", () => {
7
- test("sorts keys using a provided sort key function", async () => {
8
- const tree = Tree.from({
9
- Alice: { age: 48 },
10
- Bob: { age: 36 },
11
- Carol: { age: 42 },
12
- });
13
- const sorted = await sortBy((key, tree) => Tree.traverse(tree, key, "age"))(
14
- tree
15
- );
16
- assert.deepEqual(Array.from(await sorted.keys()), [
17
- "Bob",
18
- "Carol",
19
- "Alice",
20
- ]);
21
- });
22
- });
@@ -1,21 +0,0 @@
1
- import assert from "node:assert";
2
- import { describe, test } from "node:test";
3
- import { Tree } from "../../src/internal.js";
4
- import sortNatural from "../../src/transforms/sortNatural.js";
5
-
6
- describe("sortNatural transform", () => {
7
- test("sorts keys", async () => {
8
- const tree = Tree.from({
9
- file10: null,
10
- file1: null,
11
- file9: null,
12
- });
13
- const sortTransform = sortNatural();
14
- const sorted = await sortTransform(tree);
15
- assert.deepEqual(Array.from(await sorted.keys()), [
16
- "file1",
17
- "file9",
18
- "file10",
19
- ]);
20
- });
21
- });