@weborigami/async-tree 0.6.17 → 0.7.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/index.ts +1 -1
  2. package/package.json +4 -4
  3. package/shared.js +1 -58
  4. package/src/Tree.js +2 -5
  5. package/src/drivers/AsyncMap.js +1 -0
  6. package/src/drivers/ObjectMap.js +10 -2
  7. package/src/drivers/SetMap.js +11 -0
  8. package/src/drivers/SyncMap.js +2 -1
  9. package/src/operations/addNextPrevious.js +1 -3
  10. package/src/operations/cache.js +5 -0
  11. package/src/operations/combine.js +1 -1
  12. package/src/operations/deepEntriesIterator.js +1 -0
  13. package/src/operations/find.js +21 -0
  14. package/src/operations/findKey.js +21 -0
  15. package/src/operations/flat.js +1 -1
  16. package/src/operations/from.js +18 -6
  17. package/src/operations/inflatePaths.js +7 -6
  18. package/src/operations/invokeFunctions.js +15 -1
  19. package/src/operations/map.js +9 -41
  20. package/src/operations/mapExtension.js +38 -8
  21. package/src/operations/merge.js +2 -1
  22. package/src/operations/root.js +3 -3
  23. package/src/operations/traverseOrThrow.js +7 -2
  24. package/src/utilities/args.js +1 -1
  25. package/src/utilities/assignPropertyDescriptors.js +26 -0
  26. package/src/utilities/castArraylike.js +4 -1
  27. package/test/drivers/SyncMap.test.js +1 -0
  28. package/test/operations/find.test.js +10 -0
  29. package/test/operations/findKey.test.js +15 -0
  30. package/test/operations/from.test.js +18 -0
  31. package/test/operations/{length.test.js → size.test.js} +3 -3
  32. package/test/utilities/castArrayLike.test.js +32 -0
  33. package/src/operations/group.js +0 -6
  34. package/src/operations/isAsyncMutableTree.js +0 -8
  35. package/src/operations/isAsyncTree.js +0 -6
  36. package/src/operations/isTreelike.js +0 -8
  37. package/src/operations/length.js +0 -16
package/index.ts CHANGED
@@ -27,7 +27,6 @@ export type MapExtensionOptions = {
27
27
  deep?: boolean;
28
28
  description?: string;
29
29
  extension?: string;
30
- needsSourceValue?: boolean;
31
30
  value?: ValueKeyFn;
32
31
  };
33
32
 
@@ -46,6 +45,7 @@ export type MapOptions = {
46
45
  inverseKey?: KeyFn;
47
46
  key?: ValueKeyFn;
48
47
  keyNeedsSourceValue?: boolean;
48
+ needsSourceValue?: boolean;
49
49
  value?: ValueKeyFn;
50
50
  };
51
51
 
package/package.json CHANGED
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "name": "@weborigami/async-tree",
3
- "version": "0.6.17",
3
+ "version": "0.7.0-beta.2",
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": "25.3.2",
11
- "puppeteer": "24.37.5",
12
- "typescript": "5.9.3"
10
+ "@types/node": "25.9.1",
11
+ "puppeteer": "25.1.0",
12
+ "typescript": "6.0.3"
13
13
  },
14
14
  "scripts": {
15
15
  "headlessTest": "node scripts/headlessTest.js",
package/shared.js CHANGED
@@ -20,6 +20,7 @@ export * as trailingSlash from "./src/trailingSlash.js";
20
20
  export { default as TraverseError } from "./src/TraverseError.js";
21
21
  export * as Tree from "./src/Tree.js";
22
22
  export * as args from "./src/utilities/args.js";
23
+ export { default as assignPropertyDescriptors } from "./src/utilities/assignPropertyDescriptors.js";
23
24
  export { default as box } from "./src/utilities/box.js";
24
25
  export { default as castArraylike } from "./src/utilities/castArraylike.js";
25
26
  export { default as getParent } from "./src/utilities/getParent.js";
@@ -38,61 +39,3 @@ export { default as toPlainValue } from "./src/utilities/toPlainValue.js";
38
39
  export { default as toString } from "./src/utilities/toString.js";
39
40
 
40
41
  export { ExplorableSiteMap, FileMap, FunctionMap, ObjectMap, SetMap, SiteMap };
41
-
42
- export class DeepObjectMap extends ObjectMap {
43
- constructor(object) {
44
- super(object, { deep: true });
45
- console.warn("DeepObjectMap is deprecated. Please use ObjectMap instead.");
46
- }
47
- }
48
-
49
- export class ObjectTree extends ObjectMap {
50
- constructor(...args) {
51
- super(...args);
52
- console.warn("ObjectTree is deprecated. Please use ObjectMap instead.");
53
- }
54
- }
55
-
56
- export class DeepObjectTree extends ObjectMap {
57
- constructor(object) {
58
- super(object, { deep: true });
59
- console.warn("DeepObjectTree is deprecated. Please use ObjectMap instead.");
60
- }
61
- }
62
-
63
- export class ExplorableSiteTree extends ExplorableSiteMap {
64
- constructor(href) {
65
- super(href);
66
- console.warn(
67
- "ExplorableSiteTree is deprecated. Please use ExplorableSiteMap instead.",
68
- );
69
- }
70
- }
71
-
72
- export class FileTree extends FileMap {
73
- constructor(...args) {
74
- super(...args);
75
- console.warn("FileTree is deprecated. Please use FileMap instead.");
76
- }
77
- }
78
-
79
- export class FunctionTree extends FunctionMap {
80
- constructor(...args) {
81
- super(...args);
82
- console.warn("FunctionTree is deprecated. Please use FunctionMap instead.");
83
- }
84
- }
85
-
86
- export class SetTree extends SetMap {
87
- constructor(set) {
88
- super(set);
89
- console.warn("SetTree is deprecated. Please use SetMap instead.");
90
- }
91
- }
92
-
93
- export class SiteTree extends SiteMap {
94
- constructor(...args) {
95
- super(...args);
96
- console.warn("SiteTree is deprecated. Please use SiteMap instead.");
97
- }
98
- }
package/src/Tree.js CHANGED
@@ -25,28 +25,25 @@ export { default as deflatePaths } from "./operations/deflatePaths.js";
25
25
  export { default as delete } from "./operations/delete.js";
26
26
  export { default as entries } from "./operations/entries.js";
27
27
  export { default as filter } from "./operations/filter.js";
28
+ export { default as find } from "./operations/find.js";
29
+ export { default as findKey } from "./operations/findKey.js";
28
30
  export { default as first } from "./operations/first.js";
29
31
  export { default as flat } from "./operations/flat.js";
30
32
  export { default as forEach } from "./operations/forEach.js";
31
33
  export { default as from } from "./operations/from.js";
32
34
  export { default as globKeys } from "./operations/globKeys.js";
33
- export { default as group } from "./operations/group.js";
34
35
  export { default as groupBy } from "./operations/groupBy.js";
35
36
  export { default as has } from "./operations/has.js";
36
37
  export { default as indent } from "./operations/indent.js";
37
38
  export { default as inflatePaths } from "./operations/inflatePaths.js";
38
39
  export { default as inners } from "./operations/inners.js";
39
40
  export { default as invokeFunctions } from "./operations/invokeFunctions.js";
40
- export { default as isAsyncMutableTree } from "./operations/isAsyncMutableTree.js";
41
- export { default as isAsyncTree } from "./operations/isAsyncTree.js";
42
41
  export { default as isMap } from "./operations/isMap.js";
43
42
  export { default as isMaplike } from "./operations/isMaplike.js";
44
43
  export { default as isReadOnlyMap } from "./operations/isReadOnlyMap.js";
45
44
  export { default as isTraversable } from "./operations/isTraversable.js";
46
- export { default as isTreelike } from "./operations/isTreelike.js";
47
45
  export { default as json } from "./operations/json.js";
48
46
  export { default as keys } from "./operations/keys.js";
49
- export { default as length } from "./operations/length.js";
50
47
  export { default as map } from "./operations/map.js";
51
48
  export { default as mapExtension } from "./operations/mapExtension.js";
52
49
  export { default as mapReduce } from "./operations/mapReduce.js";
@@ -28,6 +28,7 @@ export default class AsyncMap {
28
28
  if (!isMap(result)) {
29
29
  // Create new child node using no-arg constructor
30
30
  result = new /** @type {any} */ (this.constructor)();
31
+ result.trailingSlashKeys = this.trailingSlashKeys;
31
32
  await this.set(key, result);
32
33
  }
33
34
 
@@ -20,7 +20,7 @@ export default class ObjectMap extends SyncMap {
20
20
  // objects such as Node's `Module` class for representing an ES module.
21
21
  if (typeof object !== "object" || object === null) {
22
22
  throw new TypeError(
23
- `${this.constructor.name}: Expected an object or array.`
23
+ `${this.constructor.name}: Expected an object or array.`,
24
24
  );
25
25
  }
26
26
  this.object = object;
@@ -38,6 +38,13 @@ export default class ObjectMap extends SyncMap {
38
38
  }
39
39
 
40
40
  get(key) {
41
+ if (key == null) {
42
+ // Reject nullish key
43
+ throw new ReferenceError(
44
+ `${this.constructor.name}: Cannot get a null or undefined key.`,
45
+ );
46
+ }
47
+
41
48
  // Does the object have the key with or without a trailing slash?
42
49
  const existingKey = findExistingKey(this.object, key);
43
50
  if (existingKey === null) {
@@ -111,7 +118,8 @@ export default class ObjectMap extends SyncMap {
111
118
  ? name
112
119
  : trailingSlash.toggle(
113
120
  name,
114
- descriptor.value !== undefined && this.isSubtree(descriptor.value)
121
+ descriptor.value !== undefined &&
122
+ this.isSubtree(descriptor.value),
115
123
  );
116
124
  result.add(key);
117
125
  }
@@ -13,4 +13,15 @@ export default class SetMap extends SyncMap {
13
13
  const entries = Array.from(set).map((value, index) => [index, value]);
14
14
  super(entries);
15
15
  }
16
+
17
+ get(key) {
18
+ if (key == null) {
19
+ // Reject nullish key
20
+ throw new ReferenceError(
21
+ `${this.constructor.name}: Cannot get a null or undefined key.`,
22
+ );
23
+ }
24
+
25
+ return super.get(key);
26
+ }
16
27
  }
@@ -49,6 +49,7 @@ export default class SyncMap extends Map {
49
49
  if (!(result instanceof Map)) {
50
50
  // Create new child node using no-arg constructor
51
51
  result = new /** @type {any} */ (this.constructor)();
52
+ result.trailingSlashKeys = this.trailingSlashKeys;
52
53
  this.set(key, result);
53
54
  }
54
55
 
@@ -128,7 +129,7 @@ export default class SyncMap extends Map {
128
129
  */
129
130
  get(key) {
130
131
  let value = super.get.call(this._self, key);
131
- if (value === undefined) {
132
+ if (value === undefined && this.trailingSlashKeys) {
132
133
  // Try alternate key with trailing slash added or removed
133
134
  value = super.get.call(this._self, trailingSlash.toggle(key));
134
135
  }
@@ -10,7 +10,7 @@ import keys from "./keys.js";
10
10
  */
11
11
  export default async function addNextPrevious(maplike) {
12
12
  const source = await args.map(maplike, "Tree.addNextPrevious");
13
- let sourceKeys;
13
+ const sourceKeys = await keys(source);
14
14
 
15
15
  return Object.assign(new AsyncMap(), {
16
16
  async get(key) {
@@ -29,7 +29,6 @@ export default async function addNextPrevious(maplike) {
29
29
  }
30
30
 
31
31
  // Find the index of the current key
32
- sourceKeys ??= await keys(source);
33
32
  const index = sourceKeys.indexOf(key);
34
33
  if (index >= 0) {
35
34
  // Extend result with nextKey/previousKey properties.
@@ -47,7 +46,6 @@ export default async function addNextPrevious(maplike) {
47
46
  },
48
47
 
49
48
  async *keys() {
50
- sourceKeys ??= await keys(source);
51
49
  yield* sourceKeys;
52
50
  },
53
51
 
@@ -67,6 +67,11 @@ export default async function treeCache(sourceMaplike, cacheMaplike) {
67
67
  },
68
68
 
69
69
  async *keys() {
70
+ // REVIEW: Saving our own copy of the source keys can create issues when
71
+ // this operation is applied in an Origami site. Because this keys() call
72
+ // happens outside of the language package's system cache, the system
73
+ // cache may not detect a dependency. If the underlying keys change, the
74
+ // keys obtained here won't be invalidated.
70
75
  sourceKeys ??= await keys(source);
71
76
  yield* sourceKeys;
72
77
  },
@@ -12,7 +12,7 @@ import keys from "./keys.js";
12
12
  * that were not `undefined`. If all results were `undefined`, the overall
13
13
  * result is itself `undefined`.
14
14
  *
15
- * @typedef {import("@weborigami/async-tree").Maplike} Maplike
15
+ * @typedef {import("../../index.ts").Maplike} Maplike
16
16
  *
17
17
  * @param {Maplike} maplike1
18
18
  * @param {Maplike} maplike2
@@ -2,6 +2,7 @@ import * as args from "../utilities/args.js";
2
2
  import isUnpackable from "../utilities/isUnpackable.js";
3
3
  import isMap from "./isMap.js";
4
4
  import isMaplike from "./isMaplike.js";
5
+
5
6
  /**
6
7
  * Return an iterator that yields all entries in a tree, including nested trees.
7
8
  *
@@ -0,0 +1,21 @@
1
+ import * as args from "../utilities/args.js";
2
+
3
+ /**
4
+ * Return the first value in the map that satisfies the given predicate
5
+ * function.
6
+ *
7
+ * @typedef {import("../../index.ts").Maplike} Maplike
8
+ *
9
+ * @param {Maplike} maplike
10
+ * @param {Function} predicate
11
+ */
12
+ export default async function find(maplike, predicate) {
13
+ const map = await args.map(maplike, "Tree.find");
14
+ for await (const key of map.keys()) {
15
+ const value = await map.get(key);
16
+ if (await predicate(value, key, map)) {
17
+ return value;
18
+ }
19
+ }
20
+ return undefined;
21
+ }
@@ -0,0 +1,21 @@
1
+ import * as args from "../utilities/args.js";
2
+
3
+ /**
4
+ * Return the first key in the map that satisfies the given predicate
5
+ * function.
6
+ *
7
+ * @typedef {import("../../index.ts").Maplike} Maplike
8
+ *
9
+ * @param {Maplike} maplike
10
+ * @param {Function} predicate
11
+ */
12
+ export default async function findKey(maplike, predicate) {
13
+ const map = await args.map(maplike, "Tree.findKey");
14
+ for await (const key of map.keys()) {
15
+ const value = await map.get(key);
16
+ if (await predicate(value, key, map)) {
17
+ return key;
18
+ }
19
+ }
20
+ return undefined;
21
+ }
@@ -17,7 +17,7 @@ import deepEntriesIterator from "./deepEntriesIterator.js";
17
17
  * @param {number} [depth] The maximum depth to flatten
18
18
  */
19
19
  export default async function flat(maplike, depth = 1) {
20
- const map = await args.map(maplike, "Tree.flat", { deep: true });
20
+ const map = await args.map(maplike, "Tree.flat");
21
21
 
22
22
  let index = 0;
23
23
  let onlyNumericKeys = true;
@@ -2,13 +2,16 @@ import AsyncMap from "../drivers/AsyncMap.js";
2
2
  import FunctionMap from "../drivers/FunctionMap.js";
3
3
  import ObjectMap from "../drivers/ObjectMap.js";
4
4
  import SetMap from "../drivers/SetMap.js";
5
+ import SyncMap from "../drivers/SyncMap.js";
5
6
  import * as symbols from "../symbols.js";
6
7
  import box from "../utilities/box.js";
7
- import isPlainObject from "../utilities/isPlainObject.js";
8
8
  import setParent from "../utilities/setParent.js";
9
9
  import TypedArray from "../utilities/TypedArray.js";
10
10
  import isMap from "./isMap.js";
11
11
 
12
+ // Base class for async functions
13
+ const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor;
14
+
12
15
  /**
13
16
  * Attempts to cast the indicated object to a map.
14
17
  *
@@ -42,15 +45,17 @@ export default function from(object, options = {}) {
42
45
  map = new SetMap(object);
43
46
  } else if (object instanceof ArrayBuffer || object instanceof TypedArray) {
44
47
  throw new TypeError("Can't treat binary file data as a map.");
45
- } else if (isPlainObject(object) || object instanceof Array) {
46
- map = new ObjectMap(object, { deep });
47
48
  // @ts-ignore
48
49
  } else if (globalThis.Iterator && object instanceof Iterator) {
49
50
  const array = Array.from(object);
50
51
  map = new ObjectMap(array, { deep });
51
- } else if (object && typeof object === "object") {
52
- // An instance of some class.
53
- map = new ObjectMap(object, { deep });
52
+ } else if (typeof object === "object") {
53
+ if (typeof object.get === "function" && typeof object.keys === "function") {
54
+ map = upgradeToMap(object);
55
+ } else {
56
+ // A plain object, array, or instance of some class
57
+ map = new ObjectMap(object, { deep });
58
+ }
54
59
  } else if (
55
60
  typeof object === "string" ||
56
61
  typeof object === "number" ||
@@ -69,3 +74,10 @@ export default function from(object, options = {}) {
69
74
 
70
75
  return map;
71
76
  }
77
+
78
+ function upgradeToMap(object) {
79
+ const isAsync =
80
+ object.get instanceof AsyncFunction || object.keys instanceof AsyncFunction;
81
+ const baseClass = isAsync ? AsyncMap : SyncMap;
82
+ return Object.assign(new baseClass(), object);
83
+ }
@@ -5,23 +5,24 @@ import keysFromPath from "../utilities/keysFromPath.js";
5
5
  /**
6
6
  * Given a mapping of string paths to values, return the described tree.
7
7
  */
8
- export default async function inflatePaths(maplike) {
9
- const map = await args.map(maplike, "Tree.inflatePaths", { deep: true });
8
+ export default async function inflatePaths(maplike, options = {}) {
9
+ const map = await args.map(maplike, "Tree.inflatePaths");
10
10
 
11
- const result = new SyncMap();
11
+ const classFn = options.classFn ?? SyncMap;
12
+ const result = new classFn();
12
13
  for await (const [path, value] of map) {
13
14
  const keys = keysFromPath(path);
14
- setValue(result, keys, value);
15
+ setValue(result, keys, value, classFn);
15
16
  }
16
17
  return result;
17
18
  }
18
19
 
19
20
  // Add the value to the tree at the given path of keys
20
- function setValue(map, keys, value) {
21
+ function setValue(map, keys, value, classFn) {
21
22
  let node = map;
22
23
  for (const key of keys.slice(0, -1)) {
23
24
  // Create a new node if one doesn't exist yet
24
- node = node.getOrInsertComputed(key, () => new SyncMap());
25
+ node = node.getOrInsertComputed(key, () => new classFn());
25
26
  }
26
27
  node.set(keys[keys.length - 1], value);
27
28
  }
@@ -7,7 +7,7 @@ export default async function invokeFunctions(maplike) {
7
7
  deep: true,
8
8
  });
9
9
 
10
- return Object.assign(new AsyncMap(), {
10
+ const result = Object.assign(new AsyncMap(), {
11
11
  description: "invokeFunctions",
12
12
 
13
13
  async get(key) {
@@ -30,4 +30,18 @@ export default async function invokeFunctions(maplike) {
30
30
 
31
31
  trailingSlashKeys: /** @type {any} */ (source).trailingSlashKeys,
32
32
  });
33
+
34
+ if (!(/** @type {any} */ (source).readOnly)) {
35
+ Object.assign(result, {
36
+ delete(key) {
37
+ return source.delete(key);
38
+ },
39
+
40
+ set(key, value) {
41
+ return source.set(key, value);
42
+ },
43
+ });
44
+ }
45
+
46
+ return result;
33
47
  }
@@ -5,10 +5,8 @@ import isPlainObject from "../utilities/isPlainObject.js";
5
5
  import isUnpackable from "../utilities/isUnpackable.js";
6
6
  import toFunction from "../utilities/toFunction.js";
7
7
  import cachedKeyFunctions from "./cachedKeyFunctions.js";
8
- import extensionKeyFunctions from "./extensionKeyFunctions.js";
9
8
  import isMap from "./isMap.js";
10
9
  import keys from "./keys.js";
11
- import parseExtensions from "./parseExtensions.js";
12
10
 
13
11
  /**
14
12
  * Transform the keys and/or values of a tree.
@@ -147,7 +145,6 @@ function validateOption(options, key) {
147
145
  function validateOptions(options) {
148
146
  let deep;
149
147
  let description;
150
- let extension;
151
148
  let inverseKeyFn;
152
149
  let keyFn;
153
150
  let keyNeedsSourceValue;
@@ -162,7 +159,6 @@ function validateOptions(options) {
162
159
 
163
160
  // Validate individual options
164
161
  deep = validateOption(options, "deep");
165
- extension = validateOption(options, "extension");
166
162
  inverseKeyFn = validateOption(options, "inverseKey");
167
163
  keyFn = validateOption(options, "key");
168
164
  keyNeedsSourceValue = validateOption(options, "keyNeedsSourceValue");
@@ -196,49 +192,21 @@ function validateOptions(options) {
196
192
  throw error;
197
193
  }
198
194
 
199
- if (extension && !options._noExtensionWarning) {
200
- console.warn(
201
- `Tree.map: The 'extension' option for Tree.map() is deprecated and will be removed in a future release. Use Tree.mapExtension() instead.`,
202
- );
203
- }
204
- if (extension && (keyFn || inverseKeyFn)) {
205
- throw new TypeError(
206
- `Tree.map: You can't specify extensions and also a key or inverseKey function`,
207
- );
208
- }
209
- if (extension && keyNeedsSourceValue === true) {
195
+ // If key or inverseKey weren't specified, look for sidecar functions
196
+ inverseKeyFn ??= valueFn?.inverseKey;
197
+ keyFn ??= valueFn?.key;
198
+
199
+ if (!keyFn && inverseKeyFn) {
210
200
  throw new TypeError(
211
- `Tree.map: using extensions sets keyNeedsSourceValue to be false`,
201
+ `Tree.map: You can't specify an inverseKey function without a key function`,
212
202
  );
213
203
  }
214
204
 
215
- if (extension) {
216
- // Use the extension mapping to generate key and inverseKey functions
217
- const parsed = parseExtensions(extension);
218
- const keyFns = extensionKeyFunctions(
219
- parsed.sourceExtension,
220
- parsed.resultExtension,
221
- );
205
+ if (keyFn && !inverseKeyFn) {
206
+ // Only keyFn was provided, so we need to generate the inverseKeyFn
207
+ const keyFns = cachedKeyFunctions(keyFn, deep);
222
208
  keyFn = keyFns.key;
223
209
  inverseKeyFn = keyFns.inverseKey;
224
- keyNeedsSourceValue = false;
225
- } else {
226
- // If key or inverseKey weren't specified, look for sidecar functions
227
- inverseKeyFn ??= valueFn?.inverseKey;
228
- keyFn ??= valueFn?.key;
229
-
230
- if (!keyFn && inverseKeyFn) {
231
- throw new TypeError(
232
- `Tree.map: You can't specify an inverseKey function without a key function`,
233
- );
234
- }
235
-
236
- if (keyFn && !inverseKeyFn) {
237
- // Only keyFn was provided, so we need to generate the inverseKeyFn
238
- const keyFns = cachedKeyFunctions(keyFn, deep);
239
- keyFn = keyFns.key;
240
- inverseKeyFn = keyFns.inverseKey;
241
- }
242
210
  }
243
211
 
244
212
  if (!valueFn && !keyFn) {
@@ -1,6 +1,8 @@
1
1
  import isPlainObject from "../utilities/isPlainObject.js";
2
2
  import isUnpackable from "../utilities/isUnpackable.js";
3
+ import extensionKeyFunctions from "./extensionKeyFunctions.js";
3
4
  import map from "./map.js";
5
+ import parseExtensions from "./parseExtensions.js";
4
6
 
5
7
  /**
6
8
  * @typedef {import("../../index.ts").AsyncMap} AsyncMap
@@ -44,14 +46,17 @@ import map from "./map.js";
44
46
  * @returns {Promise<AsyncMap>}
45
47
  */
46
48
  export default async function mapExtension(maplike, arg2, arg3) {
47
- /** @type {MapExtensionOptions} */
48
- // @ts-ignore
49
- let options = { _noExtensionWarning: true };
49
+ let extension;
50
+
51
+ /** @type {import("../../index.ts").MapOptions} */
52
+ let options = { keyNeedsSourceValue: false };
53
+ let optionsArg;
50
54
  if (arg3 === undefined) {
51
55
  if (typeof arg2 === "string") {
52
- options.extension = arg2;
56
+ extension = arg2;
53
57
  } else if (isPlainObject(arg2)) {
54
- Object.assign(options, arg2);
58
+ extension = arg2.extension;
59
+ optionsArg = arg2;
55
60
  } else {
56
61
  throw new TypeError(
57
62
  "Tree.mapExtension: Expected a string or options object for the second argument.",
@@ -63,14 +68,14 @@ export default async function mapExtension(maplike, arg2, arg3) {
63
68
  "Tree.mapExtension: Expected a string for the second argument.",
64
69
  );
65
70
  }
66
- options.extension = arg2;
71
+ extension = arg2;
67
72
  if (isUnpackable(arg3)) {
68
73
  arg3 = await arg3.unpack();
69
74
  }
70
75
  if (typeof arg3 === "function") {
71
76
  options.value = arg3;
72
77
  } else if (isPlainObject(arg3)) {
73
- Object.assign(options, arg3);
78
+ optionsArg = arg3;
74
79
  } else {
75
80
  throw new TypeError(
76
81
  "Tree.mapExtension: Expected a function or options object for the third argument.",
@@ -78,9 +83,34 @@ export default async function mapExtension(maplike, arg2, arg3) {
78
83
  }
79
84
  }
80
85
 
86
+ if (!extension) {
87
+ throw new TypeError(
88
+ "Tree.mapExtension: An extension mapping string is required.",
89
+ );
90
+ }
91
+
92
+ if (optionsArg?.deep !== undefined) {
93
+ options.deep = optionsArg.deep;
94
+ }
95
+ if (optionsArg?.description !== undefined) {
96
+ options.description = optionsArg.description;
97
+ }
98
+ if (optionsArg?.value !== undefined) {
99
+ options.value = optionsArg.value;
100
+ }
101
+
81
102
  if (!options.description) {
82
- options.description = `mapExtension ${options.extension}`;
103
+ options.description = `mapExtension ${extension}`;
83
104
  }
84
105
 
106
+ // Use the extension mapping to generate key and inverseKey functions
107
+ const parsed = parseExtensions(extension);
108
+ const keyFns = extensionKeyFunctions(
109
+ parsed.sourceExtension,
110
+ parsed.resultExtension,
111
+ );
112
+ options.key = keyFns.key;
113
+ options.inverseKey = keyFns.inverseKey;
114
+
85
115
  return map(maplike, options);
86
116
  }
@@ -1,5 +1,6 @@
1
1
  import AsyncMap from "../drivers/AsyncMap.js";
2
2
  import * as trailingSlash from "../trailingSlash.js";
3
+ import assignPropertyDescriptors from "../utilities/assignPropertyDescriptors.js";
3
4
  import isPlainObject from "../utilities/isPlainObject.js";
4
5
  import isUnpackable from "../utilities/isUnpackable.js";
5
6
  import from from "./from.js";
@@ -42,7 +43,7 @@ export default async function merge(...treelikes) {
42
43
 
43
44
  // If all arguments are plain objects, return a plain object.
44
45
  if (unpacked.every((source) => !isMap(source) && isPlainObject(source))) {
45
- return unpacked.reduce((acc, obj) => ({ ...acc, ...obj }), {});
46
+ return assignPropertyDescriptors({}, ...unpacked);
46
47
  }
47
48
 
48
49
  const sources = unpacked.map((maplike) => from(maplike));
@@ -1,5 +1,5 @@
1
1
  import * as symbols from "../symbols.js";
2
- import * as args from "../utilities/args.js";
2
+ import from from "./from.js";
3
3
 
4
4
  /**
5
5
  * Walk up the `parent` chain to find the root of the tree.
@@ -8,9 +8,9 @@ import * as args from "../utilities/args.js";
8
8
  *
9
9
  * @param {Maplike} maplike
10
10
  */
11
- export default async function root(maplike) {
11
+ export default function root(maplike) {
12
12
  /** @type {any} */
13
- let current = await args.map(maplike, "Tree.root");
13
+ let current = from(maplike);
14
14
  while (current.parent || current[symbols.parent]) {
15
15
  current = current.parent || current[symbols.parent];
16
16
  }
@@ -42,8 +42,12 @@ export default async function traverseOrThrow(maplike, ...keys) {
42
42
  if (typeof (/** @type {any} */ (value).unpack) === "function") {
43
43
  value = await value.unpack();
44
44
  } else {
45
+ const type =
46
+ typeof value === "string" || value instanceof String
47
+ ? "string"
48
+ : "binary";
45
49
  throw new TraverseError(
46
- "A path hit binary file data that can't be unpacked.",
50
+ `A path hit ${type} data that can't be unpacked.`,
47
51
  {
48
52
  head: maplike,
49
53
  lastValue,
@@ -60,8 +64,9 @@ export default async function traverseOrThrow(maplike, ...keys) {
60
64
  // We'll take as many keys as the function's length, but at least one.
61
65
  let fnKeyCount = Math.max(fn.length, 1);
62
66
  const args = remainingKeys.splice(0, fnKeyCount);
67
+ const normalized = args.map((key) => trailingSlash.remove(key));
63
68
  key = null;
64
- value = await fn(...args);
69
+ value = await fn(...normalized);
65
70
  } else {
66
71
  // Cast value to a map.
67
72
  const map = from(value);
@@ -69,7 +69,7 @@ export async function map(arg, operation, options = {}) {
69
69
  } catch (/** @type {any} */ error) {
70
70
  let message = error.message ?? error;
71
71
  message = `${operation}: ${message}`;
72
- const newError = new error.constructor(message);
72
+ const newError = new TypeError(message);
73
73
  /** @type {any} */ (newError).position = position;
74
74
  throw newError;
75
75
  }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * This is an analogue of Object.assign that destructively copies properties to
3
+ * a target object -- but avoids invoking property getters. Instead, it copies
4
+ * property descriptors over to the target object.
5
+ *
6
+ * @param {any} target
7
+ * @param {...any} sources
8
+ */
9
+ export default function assignPropertyDescriptors(target, ...sources) {
10
+ for (const source of sources) {
11
+ if (!source) {
12
+ continue;
13
+ }
14
+ const descriptors = Object.getOwnPropertyDescriptors(source);
15
+ for (const [key, descriptor] of Object.entries(descriptors)) {
16
+ if (descriptor.value !== undefined) {
17
+ // Simple value, copy it
18
+ target[key] = descriptor.value;
19
+ } else {
20
+ // Getter and/or setter, copy the descriptor
21
+ Object.defineProperty(target, key, descriptor);
22
+ }
23
+ }
24
+ }
25
+ return target;
26
+ }
@@ -41,7 +41,10 @@ export default function castArraylike(map, createFn = Object.fromEntries) {
41
41
  // result. By default this will create a plain object from the entries.
42
42
  const normalizedMap = new Map();
43
43
  for (const [key, value] of map.entries()) {
44
- const normalized = trailingSlash.remove(key);
44
+ // Normalize the key by stripping trailing slashes, but only if there
45
+ // aren't multiple keys that only differ by trailing slashes.
46
+ const normalize = !map.has(trailingSlash.toggle(key));
47
+ const normalized = normalize ? trailingSlash.remove(key) : key;
45
48
  normalizedMap.set(normalized, value);
46
49
  }
47
50
  return createFn(normalizedMap);
@@ -121,6 +121,7 @@ describe("SyncMap", () => {
121
121
  ["a", 1],
122
122
  ["b/", subMap],
123
123
  ]);
124
+ map.trailingSlashKeys = true;
124
125
  assert.strictEqual(map.get("a"), 1);
125
126
 
126
127
  const b = map.get("b/");
@@ -0,0 +1,10 @@
1
+ import assert from "node:assert";
2
+ import { describe, test } from "node:test";
3
+ import find from "../../src/operations/find.js";
4
+
5
+ describe("find", () => {
6
+ test("returns the first value that satisfies the predicate", async () => {
7
+ const result = await find([5, 12, 8], (e) => e > 10);
8
+ assert.strictEqual(result, 12);
9
+ });
10
+ });
@@ -0,0 +1,15 @@
1
+ import assert from "node:assert";
2
+ import { describe, test } from "node:test";
3
+ import findKey from "../../src/operations/findKey.js";
4
+
5
+ describe("findKey", () => {
6
+ test("returns the first key that satisfies the predicate", async () => {
7
+ const object = {
8
+ a: 5,
9
+ b: 12,
10
+ c: 8,
11
+ };
12
+ const result = await findKey(object, (e) => e > 10);
13
+ assert.strictEqual(result, "b");
14
+ });
15
+ });
@@ -58,4 +58,22 @@ describe("from", () => {
58
58
  const result = await slice(0, 5);
59
59
  assert.equal(result, "Hello");
60
60
  });
61
+
62
+ test("upgrades object with get/keys to a Map", () => {
63
+ const object = {
64
+ get(key) {
65
+ return key.toUpperCase();
66
+ },
67
+
68
+ *keys() {
69
+ yield "a";
70
+ yield "b";
71
+ },
72
+ };
73
+ const tree = from(object);
74
+ // @ts-ignore
75
+ assert.deepEqual([...tree.keys()], ["a", "b"]);
76
+ assert.equal(tree.get("a"), "A");
77
+ assert.equal(tree.get("b"), "B");
78
+ });
61
79
  });
@@ -1,15 +1,15 @@
1
1
  import assert from "node:assert";
2
2
  import { describe, test } from "node:test";
3
- import length from "../../src/operations/length.js";
3
+ import size from "../../src/operations/size.js";
4
4
 
5
- describe("length", () => {
5
+ describe("size", () => {
6
6
  test("returns the number of keys in the tree", async () => {
7
7
  const obj = {
8
8
  a: 1,
9
9
  b: 2,
10
10
  c: 3,
11
11
  };
12
- const result = await length(obj);
12
+ const result = await size(obj);
13
13
  assert.equal(result, 3);
14
14
  });
15
15
  });
@@ -50,4 +50,36 @@ describe("castArraylike", () => {
50
50
  3: "c",
51
51
  });
52
52
  });
53
+
54
+ test("strips trailing slashes if map only has one form of the key", () => {
55
+ const map = new /** @type {any} */ (Map)([
56
+ ["a/", 1],
57
+ ["b", 2],
58
+ ["c/", 3],
59
+ ]);
60
+ const result = castArraylike(map);
61
+ assert.deepEqual(result, {
62
+ a: 1,
63
+ b: 2,
64
+ c: 3,
65
+ });
66
+ });
67
+
68
+ test.only("preserves trailing slashes if map has both forms of the key", () => {
69
+ const map = new /** @type {any} */ (Map)([
70
+ ["a/", 1],
71
+ ["a", 2],
72
+ ["b", 3],
73
+ ["c/", 4],
74
+ ["c", 5],
75
+ ]);
76
+ const result = castArraylike(map);
77
+ assert.deepEqual(result, {
78
+ "a/": 1,
79
+ a: 2,
80
+ b: 3,
81
+ "c/": 4,
82
+ c: 5,
83
+ });
84
+ });
53
85
  });
@@ -1,6 +0,0 @@
1
- import groupBy from "./groupBy.js";
2
-
3
- export default async function group(maplike, groupKeyFn) {
4
- console.warn("Tree.group() is deprecated. Use Tree.groupBy() instead.");
5
- return groupBy(maplike, groupKeyFn);
6
- }
@@ -1,8 +0,0 @@
1
- import isReadOnlyMap from "./isReadOnlyMap.js";
2
-
3
- export default function isAsyncMutableTree(treelike) {
4
- console.warn(
5
- "Tree.isAsyncMutableTree() is deprecated, use Tree.isReadOnlyMap() instead, which returns the inverse."
6
- );
7
- return !isReadOnlyMap(treelike);
8
- }
@@ -1,6 +0,0 @@
1
- import isMap from "./isMap.js";
2
-
3
- export default function isAsyncTree(treelike) {
4
- console.warn("Tree.isAsyncTree() is deprecated, use Tree.isMap() instead.");
5
- return isMap(treelike);
6
- }
@@ -1,8 +0,0 @@
1
- import isMaplike from "./isMaplike.js";
2
-
3
- export default function isTreelike(treelike) {
4
- console.warn(
5
- "Tree.isTreelike() is deprecated, use Tree.isMaplike() instead."
6
- );
7
- return isMaplike(treelike);
8
- }
@@ -1,16 +0,0 @@
1
- import * as args from "../utilities/args.js";
2
- import keys from "./keys.js";
3
-
4
- /**
5
- * Return the number of keys in the tree.
6
- *
7
- * @typedef {import("../../index.ts").Maplike} Maplike
8
- *
9
- * @param {Maplike} maplike
10
- */
11
- export default async function length(maplike) {
12
- console.warn("Tree.length() is deprecated. Use Tree.size() instead.");
13
- const tree = await args.map(maplike, "Tree.length");
14
- const treeKeys = await keys(tree);
15
- return treeKeys.length;
16
- }