@weborigami/async-tree 0.0.66-beta.1 → 0.0.66-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 (45) hide show
  1. package/main.js +2 -0
  2. package/package.json +4 -4
  3. package/src/BrowserFileTree.js +28 -6
  4. package/src/DeepMapTree.js +2 -2
  5. package/src/DeepObjectTree.js +6 -9
  6. package/src/FileTree.js +14 -10
  7. package/src/MapTree.js +27 -4
  8. package/src/ObjectTree.js +53 -6
  9. package/src/OpenSiteTree.js +41 -0
  10. package/src/SetTree.js +0 -6
  11. package/src/SiteTree.js +24 -85
  12. package/src/Tree.d.ts +0 -1
  13. package/src/Tree.js +18 -38
  14. package/src/jsonKeys.js +4 -37
  15. package/src/operations/cache.js +0 -4
  16. package/src/operations/deepMerge.js +7 -10
  17. package/src/operations/merge.js +7 -10
  18. package/src/trailingSlash.js +54 -0
  19. package/src/transforms/cachedKeyFunctions.js +72 -34
  20. package/src/transforms/keyFunctionsForExtensions.js +24 -10
  21. package/src/transforms/mapFn.js +11 -17
  22. package/src/transforms/regExpKeys.js +17 -12
  23. package/src/utilities.js +34 -6
  24. package/test/BrowserFileTree.test.js +28 -5
  25. package/test/DeepMapTree.test.js +17 -0
  26. package/test/DeepObjectTree.test.js +17 -7
  27. package/test/FileTree.test.js +14 -7
  28. package/test/MapTree.test.js +21 -0
  29. package/test/ObjectTree.test.js +16 -12
  30. package/test/OpenSiteTree.test.js +113 -0
  31. package/test/SiteTree.test.js +14 -49
  32. package/test/Tree.test.js +19 -39
  33. package/test/browser/assert.js +9 -0
  34. package/test/browser/index.html +4 -4
  35. package/test/calendarTree.test.js +1 -1
  36. package/test/fixtures/markdown/subfolder/README.md +1 -0
  37. package/test/jsonKeys.test.js +0 -9
  38. package/test/operations/cache.test.js +1 -1
  39. package/test/operations/merge.test.js +20 -1
  40. package/test/trailingSlash.test.js +36 -0
  41. package/test/transforms/cachedKeyFunctions.test.js +90 -0
  42. package/test/transforms/keyFunctionsForExtensions.test.js +7 -3
  43. package/test/transforms/mapFn.test.js +29 -20
  44. package/test/utilities.test.js +6 -2
  45. package/test/transforms/cachedKeyMaps.test.js +0 -41
package/src/Tree.js CHANGED
@@ -3,6 +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 * as trailingSlash from "./trailingSlash.js";
6
7
  import mapTransform from "./transforms/mapFn.js";
7
8
  import * as utilities from "./utilities.js";
8
9
  import {
@@ -175,7 +176,8 @@ export async function has(tree, key) {
175
176
  */
176
177
  export function isAsyncTree(obj) {
177
178
  return (
178
- obj &&
179
+ obj !== null &&
180
+ typeof obj === "object" &&
179
181
  typeof obj.get === "function" &&
180
182
  typeof obj.keys === "function" &&
181
183
  // JavaScript Map look like trees but can't be extended the same way, so we
@@ -196,22 +198,6 @@ export function isAsyncMutableTree(obj) {
196
198
  );
197
199
  }
198
200
 
199
- /**
200
- * Return true if the indicated key produces or is expected to produce an
201
- * async tree.
202
- *
203
- * This defers to the tree's own isKeyForSubtree method. If not found, this
204
- * gets the value of that key and returns true if the value is an async
205
- * tree.
206
- */
207
- export async function isKeyForSubtree(tree, key) {
208
- if (tree.isKeyForSubtree) {
209
- return tree.isKeyForSubtree(key);
210
- }
211
- const value = await tree.get(key);
212
- return isAsyncTree(value);
213
- }
214
-
215
201
  /**
216
202
  * Return true if the object can be traversed via the `traverse()` method. The
217
203
  * object must be either treelike or a packed object with an `unpack()` method.
@@ -315,7 +301,8 @@ export async function paths(treelike, base = "") {
315
301
  const tree = from(treelike);
316
302
  const result = [];
317
303
  for (const key of await tree.keys()) {
318
- const valuePath = base ? `${base}/${key}` : key;
304
+ const separator = trailingSlash.has(base) ? "" : "/";
305
+ const valuePath = base ? `${base}${separator}${key}` : key;
319
306
  const value = await tree.get(key);
320
307
  if (await isAsyncTree(value)) {
321
308
  const subPaths = await paths(value, valuePath);
@@ -344,7 +331,10 @@ export async function plain(treelike) {
344
331
  }
345
332
  const object = {};
346
333
  for (let i = 0; i < keys.length; i++) {
347
- object[keys[i]] = values[i];
334
+ // Normalize slashes in keys.
335
+ const key = trailingSlash.remove(keys[i]);
336
+ const value = values[i];
337
+ object[key] = value;
348
338
  }
349
339
  return castArrayLike(object);
350
340
  });
@@ -436,15 +426,6 @@ export async function traverseOrThrow(treelike, ...keys) {
436
426
  value = await value.unpack();
437
427
  }
438
428
 
439
- if (!isTreelike(value)) {
440
- // Value isn't treelike, so can't be traversed except for a special case:
441
- // if there's only one key left and it's an empty string, return the
442
- // non-treelike value.
443
- if (remainingKeys.length === 1 && remainingKeys[0] === "") {
444
- return value;
445
- }
446
- }
447
-
448
429
  if (value instanceof Function) {
449
430
  // Value is a function: call it with the remaining keys.
450
431
  const fn = value;
@@ -453,21 +434,20 @@ export async function traverseOrThrow(treelike, ...keys) {
453
434
  const args = remainingKeys.splice(0, fnKeyCount);
454
435
  key = null;
455
436
  value = await fn.call(target, ...args);
456
- } else {
457
- const originalValue = value;
458
-
437
+ } else if (isTraversable(value) || typeof value === "object") {
459
438
  // Value is some other treelike object: cast it to a tree.
460
439
  const tree = from(value);
461
440
  // Get the next key.
462
441
  key = remainingKeys.shift();
463
442
  // Get the value for the key.
464
443
  value = await tree.get(key);
465
-
466
- // The empty key as the final key is a special case: if the tree doesn't
467
- // have a value for the empty key, use the original value.
468
- if (value === undefined && remainingKeys.length === 0 && key === "") {
469
- value = originalValue;
470
- }
444
+ } else {
445
+ // Value can't be traversed
446
+ throw new TraverseError("Tried to traverse a value that's not treelike", {
447
+ tree: treelike,
448
+ keys,
449
+ position,
450
+ });
471
451
  }
472
452
 
473
453
  position++;
@@ -477,7 +457,7 @@ export async function traverseOrThrow(treelike, ...keys) {
477
457
  }
478
458
 
479
459
  /**
480
- * Given a slash-separated path like "foo/bar", traverse the keys "foo" and
460
+ * Given a slash-separated path like "foo/bar", traverse the keys "foo/" and
481
461
  * "bar" and return the resulting value.
482
462
  *
483
463
  * @param {Treelike} tree
package/src/jsonKeys.js CHANGED
@@ -9,48 +9,15 @@ import { Tree } from "./internal.js";
9
9
  * a trailing slash like "about/" for a subtree of that node.
10
10
  */
11
11
 
12
- /**
13
- * Parse the JSON in a .keys.json file.
14
- *
15
- * This returns a flat dictionary of flags which are true for subtrees and
16
- * false otherwise.
17
- *
18
- * Example: the JSON `["index.html","about/"]` parses as:
19
- *
20
- * {
21
- * "index.html": false,
22
- * about: true,
23
- * }
24
- */
25
- export function parse(json) {
26
- const descriptors = JSON.parse(json);
27
- const result = {};
28
- for (const descriptor of descriptors) {
29
- if (descriptor.endsWith("/")) {
30
- result[descriptor.slice(0, -1)] = true;
31
- } else {
32
- result[descriptor] = false;
33
- }
34
- }
35
- return result;
36
- }
37
-
38
12
  /**
39
13
  * Given a tree node, return a JSON string that can be written to a .keys.json
40
14
  * file.
41
15
  */
42
16
  export async function stringify(treelike) {
43
17
  const tree = Tree.from(treelike);
44
- const keyDescriptors = [];
45
- for (const key of await tree.keys()) {
46
- // Skip the key `.keys.json` if present.
47
- if (key === ".keys.json") {
48
- continue;
49
- }
50
- const isKeyForSubtree = await Tree.isKeyForSubtree(tree, key);
51
- const keyDescriptor = isKeyForSubtree ? `${key}/` : key;
52
- keyDescriptors.push(keyDescriptor);
53
- }
54
- const json = JSON.stringify(keyDescriptors);
18
+ let keys = Array.from(await tree.keys());
19
+ // Skip the key `.keys.json` if present.
20
+ keys = keys.filter((key) => key !== ".keys.json");
21
+ const json = JSON.stringify(keys);
55
22
  return json;
56
23
  }
@@ -84,10 +84,6 @@ export default function treeCache(
84
84
  return undefined;
85
85
  },
86
86
 
87
- async isKeyForSubtree(key) {
88
- return Tree.isKeyForSubtree(source, key);
89
- },
90
-
91
87
  async keys() {
92
88
  keys ??= await source.keys();
93
89
  return keys;
@@ -1,4 +1,5 @@
1
1
  import { Tree } from "../internal.js";
2
+ import * as trailingSlash from "../trailingSlash.js";
2
3
 
3
4
  /**
4
5
  * Return a tree that performs a deep merge of the given trees.
@@ -42,21 +43,17 @@ export default function deepMerge(...sources) {
42
43
  }
43
44
  },
44
45
 
45
- async isKeyForSubtree(key) {
46
- // Check trees for the indicated key in reverse order.
47
- for (let index = trees.length - 1; index >= 0; index--) {
48
- if (await Tree.isKeyForSubtree(trees[index], key)) {
49
- return true;
50
- }
51
- }
52
- return false;
53
- },
54
-
55
46
  async keys() {
56
47
  const keys = new Set();
57
48
  // Collect keys in the order the trees were provided.
58
49
  for (const tree of trees) {
59
50
  for (const key of await tree.keys()) {
51
+ // Remove the alternate form of the key (if it exists)
52
+ const alternateKey = trailingSlash.toggle(key);
53
+ if (alternateKey !== key) {
54
+ keys.delete(alternateKey);
55
+ }
56
+
60
57
  keys.add(key);
61
58
  }
62
59
  }
@@ -1,5 +1,6 @@
1
1
  import { Tree } from "../internal.js";
2
2
  import * as symbols from "../symbols.js";
3
+ import * as trailingSlash from "../trailingSlash.js";
3
4
 
4
5
  /**
5
6
  * Return a tree that performs a shallow merge of the given trees.
@@ -40,21 +41,17 @@ export default function merge(...sources) {
40
41
  return undefined;
41
42
  },
42
43
 
43
- async isKeyForSubtree(key) {
44
- // Check trees for the indicated key in reverse order.
45
- for (let index = trees.length - 1; index >= 0; index--) {
46
- if (await Tree.isKeyForSubtree(trees[index], key)) {
47
- return true;
48
- }
49
- }
50
- return false;
51
- },
52
-
53
44
  async keys() {
54
45
  const keys = new Set();
55
46
  // Collect keys in the order the trees were provided.
56
47
  for (const tree of trees) {
57
48
  for (const key of await tree.keys()) {
49
+ // Remove the alternate form of the key (if it exists)
50
+ const alternateKey = trailingSlash.toggle(key);
51
+ if (alternateKey !== key) {
52
+ keys.delete(alternateKey);
53
+ }
54
+
58
55
  keys.add(key);
59
56
  }
60
57
  }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Add a trailing slash to a string key if the value is truthy. If the key
3
+ * is not a string, it will be returned as is.
4
+ *
5
+ * @param {any} key
6
+ */
7
+ export function add(key) {
8
+ if (key == null) {
9
+ throw new ReferenceError("trailingSlash: key was undefined");
10
+ }
11
+ return typeof key === "string" && !key.endsWith("/") ? `${key}/` : key;
12
+ }
13
+
14
+ /**
15
+ * Return true if the indicated key is a string with a trailing slash,
16
+ * false otherwise.
17
+ *
18
+ * @param {any} key
19
+ */
20
+ export function has(key) {
21
+ if (key == null) {
22
+ throw new ReferenceError("trailingSlash: key was undefined");
23
+ }
24
+ return typeof key === "string" && key.endsWith("/");
25
+ }
26
+
27
+ /**
28
+ * Remove a trailing slash from a string key.
29
+ *
30
+ * @param {any} key
31
+ */
32
+ export function remove(key) {
33
+ if (key == null) {
34
+ throw new ReferenceError("trailingSlash: key was undefined");
35
+ }
36
+ return typeof key === "string" ? key.replace(/\/$/, "") : key;
37
+ }
38
+
39
+ /**
40
+ * If the key has a trailing slash, remove it; otherwise add it.
41
+ *
42
+ * @param {any} key
43
+ * @param {boolean} [force]
44
+ */
45
+ export function toggle(key, force = undefined) {
46
+ if (key == null) {
47
+ throw new ReferenceError("trailingSlash: key was undefined");
48
+ }
49
+ if (typeof key !== "string") {
50
+ return key;
51
+ }
52
+ const addSlash = force ?? !has(key);
53
+ return addSlash ? add(key) : remove(key);
54
+ }
@@ -1,4 +1,4 @@
1
- import { Tree } from "../internal.js";
1
+ import * as trailingSlash from "../trailingSlash.js";
2
2
 
3
3
  const treeToCaches = new WeakMap();
4
4
 
@@ -6,46 +6,50 @@ const treeToCaches = new WeakMap();
6
6
  * Given a key function, return a new key function and inverse key function that
7
7
  * cache the results of the original.
8
8
  *
9
+ * If `skipSubtrees` is true, the inverse key function will skip any source keys
10
+ * that are keys for subtrees, returning the source key unmodified.
11
+ *
9
12
  * @typedef {import("../../index.ts").KeyFn} KeyFn
10
13
  *
11
14
  * @param {KeyFn} keyFn
12
- * @param {boolean} [deep]
15
+ * @param {boolean?} skipSubtrees
13
16
  * @returns {{ key: KeyFn, inverseKey: KeyFn }}
14
17
  */
15
- export default function createCachedKeysTransform(keyFn, deep = false) {
18
+ export default function cachedKeyFunctions(keyFn, skipSubtrees = false) {
16
19
  return {
17
20
  async inverseKey(resultKey, tree) {
18
- const caches = getCachesForTree(tree);
21
+ const { resultKeyToSourceKey, sourceKeyToResultKey } =
22
+ getKeyMapsForTree(tree);
19
23
 
20
- // First check to see if we've already computed an source key for this
21
- // result key. Again, we have to use `has()` for this check.
22
- if (caches.resultKeyToSourceKey.has(resultKey)) {
23
- return caches.resultKeyToSourceKey.get(resultKey);
24
+ const cachedSourceKey = searchKeyMap(resultKeyToSourceKey, resultKey);
25
+ if (cachedSourceKey !== undefined) {
26
+ return cachedSourceKey;
24
27
  }
25
28
 
26
29
  // Iterate through the tree's keys, calculating source keys as we go,
27
30
  // until we find a match. Cache all the intermediate results and the
28
31
  // final match. This is O(n), but we stop as soon as we find a match,
29
32
  // and subsequent calls will benefit from the intermediate results.
33
+ const resultKeyWithoutSlash = trailingSlash.remove(resultKey);
30
34
  for (const sourceKey of await tree.keys()) {
31
35
  // Skip any source keys we already know about.
32
- if (caches.sourceKeyToResultKey.has(sourceKey)) {
36
+ if (sourceKeyToResultKey.has(sourceKey)) {
33
37
  continue;
34
38
  }
35
39
 
36
- let computedResultKey;
37
- if (deep && (await Tree.isKeyForSubtree(tree, sourceKey))) {
38
- computedResultKey = sourceKey;
39
- } else {
40
- computedResultKey = await keyFn(sourceKey, tree);
41
- }
42
-
43
- caches.sourceKeyToResultKey.set(sourceKey, computedResultKey);
44
- caches.resultKeyToSourceKey.set(computedResultKey, sourceKey);
40
+ const computedResultKey = await computeAndCacheResultKey(
41
+ tree,
42
+ keyFn,
43
+ skipSubtrees,
44
+ sourceKey
45
+ );
45
46
 
46
- if (computedResultKey === resultKey) {
47
- // Match found.
48
- return sourceKey;
47
+ if (
48
+ computedResultKey &&
49
+ trailingSlash.remove(computedResultKey) === resultKeyWithoutSlash
50
+ ) {
51
+ // Match found, match trailing slash and return
52
+ return trailingSlash.toggle(sourceKey, trailingSlash.has(resultKey));
49
53
  }
50
54
  }
51
55
 
@@ -53,27 +57,42 @@ export default function createCachedKeysTransform(keyFn, deep = false) {
53
57
  },
54
58
 
55
59
  async key(sourceKey, tree) {
56
- const keyMaps = getCachesForTree(tree);
60
+ const { sourceKeyToResultKey } = getKeyMapsForTree(tree);
57
61
 
58
- // First check to see if we've already computed an result key for this
59
- // source key. The cached result key may be undefined, so we have to use
60
- // `has()` instead of calling `get()` and checking for undefined.
61
- if (keyMaps.sourceKeyToResultKey.has(sourceKey)) {
62
- return keyMaps.sourceKeyToResultKey.get(sourceKey);
62
+ const cachedResultKey = searchKeyMap(sourceKeyToResultKey, sourceKey);
63
+ if (cachedResultKey !== undefined) {
64
+ return cachedResultKey;
63
65
  }
64
66
 
65
- const resultKey = await keyFn(sourceKey, tree);
66
-
67
- // Cache the mappings from source key <-> result key for next time.
68
- keyMaps.sourceKeyToResultKey.set(sourceKey, resultKey);
69
- keyMaps.resultKeyToSourceKey.set(resultKey, sourceKey);
70
-
67
+ const resultKey = await computeAndCacheResultKey(
68
+ tree,
69
+ keyFn,
70
+ skipSubtrees,
71
+ sourceKey
72
+ );
71
73
  return resultKey;
72
74
  },
73
75
  };
74
76
  }
75
77
 
76
- function getCachesForTree(tree) {
78
+ async function computeAndCacheResultKey(tree, keyFn, skipSubtrees, sourceKey) {
79
+ const { resultKeyToSourceKey, sourceKeyToResultKey } =
80
+ getKeyMapsForTree(tree);
81
+
82
+ const resultKey =
83
+ skipSubtrees && trailingSlash.has(sourceKey)
84
+ ? sourceKey
85
+ : await keyFn(sourceKey, tree);
86
+
87
+ sourceKeyToResultKey.set(sourceKey, resultKey);
88
+ resultKeyToSourceKey.set(resultKey, sourceKey);
89
+
90
+ return resultKey;
91
+ }
92
+
93
+ // Maintain key->inverseKey and inverseKey->key mappings for each tree. These
94
+ // store subtree keys in either direction with a trailing slash.
95
+ function getKeyMapsForTree(tree) {
77
96
  let keyMaps = treeToCaches.get(tree);
78
97
  if (!keyMaps) {
79
98
  keyMaps = {
@@ -84,3 +103,22 @@ function getCachesForTree(tree) {
84
103
  }
85
104
  return keyMaps;
86
105
  }
106
+
107
+ // Search the given key map for the key. Ignore trailing slashes in the search,
108
+ // but preserve them in the result.
109
+ function searchKeyMap(keyMap, key) {
110
+ // Check key as is
111
+ let match;
112
+ if (keyMap.has(key)) {
113
+ match = keyMap.get(key);
114
+ } else if (!trailingSlash.has(key)) {
115
+ // Check key without trailing slash
116
+ const withSlash = trailingSlash.add(key);
117
+ if (keyMap.has(withSlash)) {
118
+ match = keyMap.get(withSlash);
119
+ }
120
+ }
121
+ return match
122
+ ? trailingSlash.toggle(match, trailingSlash.has(key))
123
+ : undefined;
124
+ }
@@ -1,3 +1,5 @@
1
+ import * as trailingSlash from "../trailingSlash.js";
2
+
1
3
  /**
2
4
  * Given a source resultExtension and a result resultExtension, return a pair of key
3
5
  * functions that map between them.
@@ -19,13 +21,23 @@ export default function keyFunctionsForExtensions({
19
21
 
20
22
  return {
21
23
  async inverseKey(resultKey, tree) {
22
- const basename = matchExtension(resultKey, resultExtension);
24
+ // Remove trailing slash so that mapFn won't inadvertently unpack files.
25
+ const baseKey = trailingSlash.remove(resultKey);
26
+ const basename = matchExtension(baseKey, resultExtension);
23
27
  return basename ? `${basename}${dotPrefix(sourceExtension)}` : undefined;
24
28
  },
25
29
 
26
30
  async key(sourceKey, tree) {
27
- const basename = matchExtension(sourceKey, sourceExtension);
28
- return basename ? `${basename}${dotPrefix(resultExtension)}` : undefined;
31
+ const hasSlash = trailingSlash.has(sourceKey);
32
+ const baseKey = trailingSlash.remove(sourceKey);
33
+ const basename = matchExtension(baseKey, sourceExtension);
34
+ return basename
35
+ ? // Preserve trailing slash
36
+ trailingSlash.toggle(
37
+ `${basename}${dotPrefix(resultExtension)}`,
38
+ hasSlash
39
+ )
40
+ : undefined;
29
41
  },
30
42
  };
31
43
  }
@@ -35,15 +47,17 @@ function dotPrefix(resultExtension) {
35
47
  }
36
48
 
37
49
  /**
38
- * See if the key ends with the given resultExtension. If it does, return the base
39
- * name without the resultExtension; if it doesn't return null.
50
+ * See if the key ends with the given resultExtension. If it does, return the
51
+ * base name without the resultExtension; if it doesn't return null.
40
52
  *
41
- * An empty/null resultExtension means: match any key that does *not* contain a
42
- * period.
53
+ * A trailing slash in the key is ignored for purposes of comparison to comply
54
+ * with the way Origami can unpack files. Example: the keys "data.json" and
55
+ * "data.json/" are treated equally.
43
56
  *
44
- * This uses a different, more general interpretation of "resultExtension" to mean any
45
- * suffix, rather than Node's interpretation `path.extname`. In particular, this
46
- * will match an "resultExtension" like ".foo.bar" that contains more than one dot.
57
+ * This uses a different, more general interpretation of "resultExtension" to
58
+ * mean any suffix, rather than Node's interpretation `path.extname`. In
59
+ * particular, this will match an "resultExtension" like ".foo.bar" that
60
+ * contains more than one dot.
47
61
  */
48
62
  function matchExtension(key, resultExtension) {
49
63
  if (resultExtension) {
@@ -1,4 +1,5 @@
1
1
  import { Tree } from "../internal.js";
2
+ import * as trailingSlash from "../trailingSlash.js";
2
3
 
3
4
  /**
4
5
  * Return a transform function that maps the keys and/or values of a tree.
@@ -65,13 +66,9 @@ export default function createMapTransform(options = {}) {
65
66
  if (keyFn || valueFn) {
66
67
  transformed.get = async (resultKey) => {
67
68
  // Step 1: Map the result key to the source key.
68
- const isSubtree = deep && (await Tree.isKeyForSubtree(tree, resultKey));
69
- const sourceKey =
70
- !isSubtree && inverseKeyFn
71
- ? await inverseKeyFn(resultKey, tree)
72
- : resultKey;
69
+ const sourceKey = (await inverseKeyFn?.(resultKey, tree)) ?? resultKey;
73
70
 
74
- if (sourceKey == null) {
71
+ if (sourceKey === undefined) {
75
72
  // No source key means no value.
76
73
  return undefined;
77
74
  }
@@ -81,8 +78,8 @@ export default function createMapTransform(options = {}) {
81
78
  if (needsSourceValue) {
82
79
  // Normal case: get the value from the source tree.
83
80
  sourceValue = await tree.get(sourceKey);
84
- } else if (deep && (await Tree.isKeyForSubtree(tree, sourceKey))) {
85
- // Only get the source value if it's a subtree.
81
+ } else if (deep && trailingSlash.has(sourceKey)) {
82
+ // Only get the source value if it's expected to be a subtree.
86
83
  sourceValue = tree;
87
84
  }
88
85
 
@@ -111,15 +108,12 @@ export default function createMapTransform(options = {}) {
111
108
  // Apply the keyFn to source keys for leaf values (not subtrees).
112
109
  const sourceKeys = Array.from(await tree.keys());
113
110
  const mapped = await Promise.all(
114
- sourceKeys.map(async (sourceKey) => {
115
- let resultKey;
116
- if (deep && (await Tree.isKeyForSubtree(tree, sourceKey))) {
117
- resultKey = sourceKey;
118
- } else {
119
- resultKey = await keyFn(sourceKey, tree);
120
- }
121
- return resultKey;
122
- })
111
+ sourceKeys.map(async (sourceKey) =>
112
+ // Deep maps leave source keys for subtrees alone
113
+ deep && trailingSlash.has(sourceKey)
114
+ ? sourceKey
115
+ : await keyFn(sourceKey, tree)
116
+ )
123
117
  );
124
118
  // Filter out any cases where the keyFn returned undefined.
125
119
  const resultKeys = mapped.filter((key) => key !== undefined);
@@ -1,4 +1,5 @@
1
1
  import { Tree } from "../internal.js";
2
+ import * as trailingSlash from "../trailingSlash.js";
2
3
 
3
4
  /**
4
5
  * A tree whose keys are strings interpreted as regular expressions.
@@ -57,25 +58,29 @@ export default async function regExpKeys(treelike) {
57
58
  continue;
58
59
  }
59
60
 
60
- // Construct regular expression.
61
- let text = key;
62
- if (!text.startsWith("^")) {
63
- text = "^" + text;
64
- }
65
- if (!text.endsWith("$")) {
66
- text = text + "$";
67
- }
68
- const regExp = new RegExp(text);
69
-
70
61
  // Get value.
71
62
  let value = await tree.get(key);
72
- if (Tree.isAsyncTree(value)) {
63
+
64
+ let regExp;
65
+ if (trailingSlash.has(key) || Tree.isAsyncTree(value)) {
66
+ const baseKey = trailingSlash.remove(key);
67
+ regExp = new RegExp("^" + baseKey + "/?$");
68
+ // Subtree
73
69
  value = regExpKeys(value);
74
70
  if (!value.parent) {
75
71
  value.parent = result;
76
72
  }
73
+ } else {
74
+ // Construct regular expression.
75
+ let text = key;
76
+ if (!text.startsWith("^")) {
77
+ text = "^" + text;
78
+ }
79
+ if (!text.endsWith("$")) {
80
+ text = text + "$";
81
+ }
82
+ regExp = new RegExp(text);
77
83
  }
78
-
79
84
  map.set(regExp, value);
80
85
  }
81
86