@weborigami/async-tree 0.0.65 → 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 +1 -2
  13. package/src/Tree.js +40 -52
  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 {
@@ -105,35 +106,38 @@ export async function forEach(tree, callbackFn) {
105
106
  *
106
107
  * If the object is a plain object, it will be converted to an ObjectTree. The
107
108
  * optional `deep` option can be set to `true` to convert a plain object to a
108
- * DeepObjectTree.
109
+ * DeepObjectTree. The optional `parent` parameter will be used as the default
110
+ * parent of the new tree.
109
111
  *
110
112
  * @param {Treelike | Object} object
111
- * @param {{ deep?: true }} [options]
113
+ * @param {{ deep?: true, parent?: AsyncTree|null }} [options]
112
114
  * @returns {AsyncTree}
113
115
  */
114
116
  export function from(object, options = {}) {
117
+ let tree;
115
118
  if (isAsyncTree(object)) {
116
119
  // Argument already supports the tree interface.
117
120
  // @ts-ignore
118
121
  return object;
119
122
  } else if (typeof object === "function") {
120
- return new FunctionTree(object);
123
+ tree = new FunctionTree(object);
121
124
  } else if (object instanceof Map) {
122
- return new MapTree(object);
125
+ tree = new MapTree(object);
123
126
  } else if (object instanceof Set) {
124
- return new SetTree(object);
127
+ tree = new SetTree(object);
125
128
  } else if (isPlainObject(object) || object instanceof Array) {
126
- return options.deep ? new DeepObjectTree(object) : new ObjectTree(object);
129
+ tree = options.deep ? new DeepObjectTree(object) : new ObjectTree(object);
127
130
  } else if (isUnpackable(object)) {
128
131
  async function AsyncFunction() {} // Sample async function
129
- return object.unpack instanceof AsyncFunction.constructor
130
- ? // Async unpack: return a deferred tree.
131
- new DeferredTree(object.unpack)
132
- : // Synchronous unpack: cast the result of unpack() to a tree.
133
- from(object.unpack());
132
+ tree =
133
+ object.unpack instanceof AsyncFunction.constructor
134
+ ? // Async unpack: return a deferred tree.
135
+ new DeferredTree(object.unpack)
136
+ : // Synchronous unpack: cast the result of unpack() to a tree.
137
+ from(object.unpack());
134
138
  } else if (object && typeof object === "object") {
135
139
  // An instance of some class.
136
- return new ObjectTree(object);
140
+ tree = new ObjectTree(object);
137
141
  } else if (
138
142
  typeof object === "string" ||
139
143
  typeof object === "number" ||
@@ -141,10 +145,15 @@ export function from(object, options = {}) {
141
145
  ) {
142
146
  // A primitive value; box it into an object and construct a tree.
143
147
  const boxed = utilities.box(object);
144
- return new ObjectTree(boxed);
148
+ tree = new ObjectTree(boxed);
149
+ } else {
150
+ throw new TypeError("Couldn't convert argument to an async tree");
145
151
  }
146
152
 
147
- throw new TypeError("Couldn't convert argument to an async tree");
153
+ if (!tree.parent && options.parent) {
154
+ tree.parent = options.parent;
155
+ }
156
+ return tree;
148
157
  }
149
158
 
150
159
  /**
@@ -167,7 +176,8 @@ export async function has(tree, key) {
167
176
  */
168
177
  export function isAsyncTree(obj) {
169
178
  return (
170
- obj &&
179
+ obj !== null &&
180
+ typeof obj === "object" &&
171
181
  typeof obj.get === "function" &&
172
182
  typeof obj.keys === "function" &&
173
183
  // JavaScript Map look like trees but can't be extended the same way, so we
@@ -188,22 +198,6 @@ export function isAsyncMutableTree(obj) {
188
198
  );
189
199
  }
190
200
 
191
- /**
192
- * Return true if the indicated key produces or is expected to produce an
193
- * async tree.
194
- *
195
- * This defers to the tree's own isKeyForSubtree method. If not found, this
196
- * gets the value of that key and returns true if the value is an async
197
- * tree.
198
- */
199
- export async function isKeyForSubtree(tree, key) {
200
- if (tree.isKeyForSubtree) {
201
- return tree.isKeyForSubtree(key);
202
- }
203
- const value = await tree.get(key);
204
- return isAsyncTree(value);
205
- }
206
-
207
201
  /**
208
202
  * Return true if the object can be traversed via the `traverse()` method. The
209
203
  * object must be either treelike or a packed object with an `unpack()` method.
@@ -307,7 +301,8 @@ export async function paths(treelike, base = "") {
307
301
  const tree = from(treelike);
308
302
  const result = [];
309
303
  for (const key of await tree.keys()) {
310
- const valuePath = base ? `${base}/${key}` : key;
304
+ const separator = trailingSlash.has(base) ? "" : "/";
305
+ const valuePath = base ? `${base}${separator}${key}` : key;
311
306
  const value = await tree.get(key);
312
307
  if (await isAsyncTree(value)) {
313
308
  const subPaths = await paths(value, valuePath);
@@ -336,7 +331,10 @@ export async function plain(treelike) {
336
331
  }
337
332
  const object = {};
338
333
  for (let i = 0; i < keys.length; i++) {
339
- 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;
340
338
  }
341
339
  return castArrayLike(object);
342
340
  });
@@ -428,15 +426,6 @@ export async function traverseOrThrow(treelike, ...keys) {
428
426
  value = await value.unpack();
429
427
  }
430
428
 
431
- if (!isTreelike(value)) {
432
- // Value isn't treelike, so can't be traversed except for a special case:
433
- // if there's only one key left and it's an empty string, return the
434
- // non-treelike value.
435
- if (remainingKeys.length === 1 && remainingKeys[0] === "") {
436
- return value;
437
- }
438
- }
439
-
440
429
  if (value instanceof Function) {
441
430
  // Value is a function: call it with the remaining keys.
442
431
  const fn = value;
@@ -445,21 +434,20 @@ export async function traverseOrThrow(treelike, ...keys) {
445
434
  const args = remainingKeys.splice(0, fnKeyCount);
446
435
  key = null;
447
436
  value = await fn.call(target, ...args);
448
- } else {
449
- const originalValue = value;
450
-
437
+ } else if (isTraversable(value) || typeof value === "object") {
451
438
  // Value is some other treelike object: cast it to a tree.
452
439
  const tree = from(value);
453
440
  // Get the next key.
454
441
  key = remainingKeys.shift();
455
442
  // Get the value for the key.
456
443
  value = await tree.get(key);
457
-
458
- // The empty key as the final key is a special case: if the tree doesn't
459
- // have a value for the empty key, use the original value.
460
- if (value === undefined && remainingKeys.length === 0 && key === "") {
461
- value = originalValue;
462
- }
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
+ });
463
451
  }
464
452
 
465
453
  position++;
@@ -469,7 +457,7 @@ export async function traverseOrThrow(treelike, ...keys) {
469
457
  }
470
458
 
471
459
  /**
472
- * 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
473
461
  * "bar" and return the resulting value.
474
462
  *
475
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);