@weborigami/async-tree 0.5.7 → 0.6.0

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 (178) hide show
  1. package/browser.js +1 -1
  2. package/index.ts +31 -35
  3. package/main.js +1 -2
  4. package/package.json +4 -7
  5. package/shared.js +77 -12
  6. package/src/Tree.js +8 -2
  7. package/src/drivers/AsyncMap.js +210 -0
  8. package/src/drivers/{BrowserFileTree.js → BrowserFileMap.js} +36 -27
  9. package/src/drivers/{calendarTree.js → CalendarMap.js} +81 -62
  10. package/src/drivers/ConstantMap.js +30 -0
  11. package/src/drivers/DeepObjectMap.js +27 -0
  12. package/src/drivers/{ExplorableSiteTree.js → ExplorableSiteMap.js} +7 -7
  13. package/src/drivers/FileMap.js +245 -0
  14. package/src/drivers/{FunctionTree.js → FunctionMap.js} +19 -22
  15. package/src/drivers/ObjectMap.js +139 -0
  16. package/src/drivers/SetMap.js +13 -0
  17. package/src/drivers/{SiteTree.js → SiteMap.js} +16 -17
  18. package/src/drivers/SyncMap.js +245 -0
  19. package/src/jsonKeys.d.ts +2 -2
  20. package/src/jsonKeys.js +6 -5
  21. package/src/operations/addNextPrevious.js +35 -36
  22. package/src/operations/assign.js +30 -21
  23. package/src/operations/cache.js +29 -35
  24. package/src/operations/cachedKeyFunctions.js +1 -1
  25. package/src/operations/calendar.js +5 -0
  26. package/src/operations/clear.js +13 -12
  27. package/src/operations/constant.js +5 -0
  28. package/src/operations/deepEntries.js +23 -0
  29. package/src/operations/deepMap.js +9 -9
  30. package/src/operations/deepMerge.js +36 -25
  31. package/src/operations/deepReverse.js +23 -16
  32. package/src/operations/deepTake.js +7 -7
  33. package/src/operations/deepText.js +4 -4
  34. package/src/operations/deepValues.js +3 -6
  35. package/src/operations/deepValuesIterator.js +11 -11
  36. package/src/operations/delete.js +8 -12
  37. package/src/operations/entries.js +17 -10
  38. package/src/operations/filter.js +9 -7
  39. package/src/operations/first.js +12 -10
  40. package/src/operations/forEach.js +10 -13
  41. package/src/operations/from.js +31 -39
  42. package/src/operations/globKeys.js +22 -17
  43. package/src/operations/group.js +2 -2
  44. package/src/operations/groupBy.js +24 -22
  45. package/src/operations/has.js +7 -9
  46. package/src/operations/indent.js +2 -2
  47. package/src/operations/inners.js +19 -15
  48. package/src/operations/invokeFunctions.js +22 -10
  49. package/src/operations/isAsyncMutableTree.js +5 -12
  50. package/src/operations/isAsyncTree.js +5 -20
  51. package/src/operations/isMap.js +39 -0
  52. package/src/operations/isMaplike.js +34 -0
  53. package/src/operations/isReadOnlyMap.js +14 -0
  54. package/src/operations/isTraversable.js +3 -3
  55. package/src/operations/isTreelike.js +5 -30
  56. package/src/operations/json.js +4 -12
  57. package/src/operations/keys.js +17 -8
  58. package/src/operations/length.js +9 -8
  59. package/src/operations/map.js +27 -30
  60. package/src/operations/mapExtension.js +20 -16
  61. package/src/operations/mapReduce.js +22 -17
  62. package/src/operations/mask.js +31 -22
  63. package/src/operations/match.js +13 -9
  64. package/src/operations/merge.js +43 -35
  65. package/src/operations/paginate.js +26 -18
  66. package/src/operations/parent.js +7 -7
  67. package/src/operations/paths.js +8 -8
  68. package/src/operations/plain.js +6 -6
  69. package/src/operations/regExpKeys.js +21 -12
  70. package/src/operations/reverse.js +21 -15
  71. package/src/operations/root.js +6 -5
  72. package/src/operations/scope.js +31 -26
  73. package/src/operations/shuffle.js +23 -16
  74. package/src/operations/size.js +13 -0
  75. package/src/operations/sort.js +55 -40
  76. package/src/operations/sync.js +21 -0
  77. package/src/operations/take.js +23 -11
  78. package/src/operations/text.js +4 -4
  79. package/src/operations/toFunction.js +7 -7
  80. package/src/operations/traverse.js +4 -4
  81. package/src/operations/traverseOrThrow.js +13 -9
  82. package/src/operations/traversePath.js +2 -2
  83. package/src/operations/values.js +18 -9
  84. package/src/operations/withKeys.js +22 -16
  85. package/src/symbols.js +1 -0
  86. package/src/utilities/castArraylike.js +10 -2
  87. package/src/utilities/getMapArgument.js +38 -0
  88. package/src/utilities/getParent.js +2 -2
  89. package/src/utilities/isStringlike.js +7 -5
  90. package/src/utilities/setParent.js +7 -7
  91. package/src/utilities/toFunction.js +2 -2
  92. package/src/utilities/toPlainValue.js +22 -18
  93. package/test/SampleAsyncMap.js +34 -0
  94. package/test/browser/assert.js +20 -0
  95. package/test/browser/index.html +54 -21
  96. package/test/drivers/AsyncMap.test.js +119 -0
  97. package/test/drivers/{BrowserFileTree.test.js → BrowserFileMap.test.js} +42 -23
  98. package/test/drivers/{calendarTree.test.js → CalendarMap.test.js} +17 -19
  99. package/test/drivers/ConstantMap.test.js +15 -0
  100. package/test/drivers/DeepObjectMap.test.js +36 -0
  101. package/test/drivers/{ExplorableSiteTree.test.js → ExplorableSiteMap.test.js} +29 -14
  102. package/test/drivers/FileMap.test.js +185 -0
  103. package/test/drivers/FunctionMap.test.js +56 -0
  104. package/test/drivers/ObjectMap.test.js +166 -0
  105. package/test/drivers/SetMap.test.js +35 -0
  106. package/test/drivers/{SiteTree.test.js → SiteMap.test.js} +14 -10
  107. package/test/drivers/SyncMap.test.js +321 -0
  108. package/test/jsonKeys.test.js +2 -2
  109. package/test/operations/addNextPrevious.test.js +3 -2
  110. package/test/operations/assign.test.js +30 -35
  111. package/test/operations/cache.test.js +8 -6
  112. package/test/operations/cachedKeyFunctions.test.js +6 -5
  113. package/test/operations/clear.test.js +6 -27
  114. package/test/operations/deepEntries.test.js +32 -0
  115. package/test/operations/deepMerge.test.js +6 -5
  116. package/test/operations/deepReverse.test.js +2 -2
  117. package/test/operations/deepTake.test.js +2 -2
  118. package/test/operations/deepText.test.js +4 -4
  119. package/test/operations/deepValuesIterator.test.js +2 -2
  120. package/test/operations/delete.test.js +2 -2
  121. package/test/operations/extensionKeyFunctions.test.js +6 -5
  122. package/test/operations/filter.test.js +3 -3
  123. package/test/operations/from.test.js +23 -31
  124. package/test/operations/globKeys.test.js +9 -9
  125. package/test/operations/groupBy.test.js +6 -5
  126. package/test/operations/inners.test.js +4 -4
  127. package/test/operations/invokeFunctions.test.js +2 -2
  128. package/test/operations/isMap.test.js +15 -0
  129. package/test/operations/isMaplike.test.js +15 -0
  130. package/test/operations/json.test.js +2 -2
  131. package/test/operations/keys.test.js +16 -3
  132. package/test/operations/map.test.js +20 -18
  133. package/test/operations/mapExtension.test.js +6 -6
  134. package/test/operations/mapReduce.test.js +2 -2
  135. package/test/operations/mask.test.js +4 -3
  136. package/test/operations/match.test.js +2 -2
  137. package/test/operations/merge.test.js +15 -11
  138. package/test/operations/paginate.test.js +5 -5
  139. package/test/operations/parent.test.js +3 -3
  140. package/test/operations/paths.test.js +6 -6
  141. package/test/operations/plain.test.js +8 -8
  142. package/test/operations/regExpKeys.test.js +12 -11
  143. package/test/operations/reverse.test.js +4 -3
  144. package/test/operations/scope.test.js +6 -5
  145. package/test/operations/shuffle.test.js +3 -2
  146. package/test/operations/sort.test.js +7 -10
  147. package/test/operations/sync.test.js +43 -0
  148. package/test/operations/take.test.js +2 -2
  149. package/test/operations/toFunction.test.js +2 -2
  150. package/test/operations/traverse.test.js +4 -5
  151. package/test/operations/withKeys.test.js +2 -2
  152. package/test/utilities/setParent.test.js +6 -6
  153. package/test/utilities/toFunction.test.js +2 -2
  154. package/test/utilities/toPlainValue.test.js +51 -12
  155. package/src/drivers/DeepMapTree.js +0 -23
  156. package/src/drivers/DeepObjectTree.js +0 -18
  157. package/src/drivers/DeferredTree.js +0 -81
  158. package/src/drivers/FileTree.js +0 -276
  159. package/src/drivers/MapTree.js +0 -70
  160. package/src/drivers/ObjectTree.js +0 -158
  161. package/src/drivers/SetTree.js +0 -34
  162. package/src/drivers/constantTree.js +0 -19
  163. package/src/drivers/limitConcurrency.js +0 -63
  164. package/src/internal.js +0 -16
  165. package/src/utilities/getTreeArgument.js +0 -43
  166. package/test/drivers/DeepMapTree.test.js +0 -17
  167. package/test/drivers/DeepObjectTree.test.js +0 -35
  168. package/test/drivers/DeferredTree.test.js +0 -22
  169. package/test/drivers/FileTree.test.js +0 -192
  170. package/test/drivers/FunctionTree.test.js +0 -46
  171. package/test/drivers/MapTree.test.js +0 -59
  172. package/test/drivers/ObjectTree.test.js +0 -163
  173. package/test/drivers/SetTree.test.js +0 -44
  174. package/test/drivers/constantTree.test.js +0 -13
  175. package/test/drivers/limitConcurrency.test.js +0 -41
  176. package/test/operations/isAsyncMutableTree.test.js +0 -17
  177. package/test/operations/isAsyncTree.test.js +0 -26
  178. package/test/operations/isTreelike.test.js +0 -13
@@ -0,0 +1,119 @@
1
+ import assert from "node:assert";
2
+ import { describe, test } from "node:test";
3
+ import AsyncMap from "../../src/drivers/AsyncMap.js";
4
+ import SampleAsyncMap from "../SampleAsyncMap.js";
5
+
6
+ describe("AsyncMap", () => {
7
+ test("clear", async () => {
8
+ const map = new SampleAsyncMap([
9
+ ["a", 1],
10
+ ["b", 2],
11
+ ]);
12
+ assert.strictEqual(await map.size, 2);
13
+ await map.clear();
14
+ assert.strictEqual(await map.size, 0);
15
+ });
16
+
17
+ test("entries invokes keys() and get()", async () => {
18
+ const map = new SampleAsyncMap([
19
+ ["a", 1],
20
+ ["b", 2],
21
+ ]);
22
+ const entries = [];
23
+ for await (const entry of map.entries()) {
24
+ entries.push(entry);
25
+ }
26
+ assert.deepStrictEqual(entries, [
27
+ ["a", 1],
28
+ ["b", 2],
29
+ ]);
30
+ });
31
+
32
+ test("forEach", async () => {
33
+ const map = new SampleAsyncMap([
34
+ ["a", 1],
35
+ ["b", 2],
36
+ ]);
37
+ const calls = [];
38
+ await map.forEach(async (value, key, theMap) => {
39
+ calls.push([key, value, theMap]);
40
+ });
41
+ assert.deepStrictEqual(calls, [
42
+ ["a", 1, map],
43
+ ["b", 2, map],
44
+ ]);
45
+ });
46
+
47
+ test("static groupBy", async () => {
48
+ const items = [
49
+ { name: "apple", type: "fruit" },
50
+ { name: "beet", type: "vegetable" },
51
+ { name: "cherry", type: "fruit" },
52
+ ];
53
+ const map = await AsyncMap.groupBy(
54
+ items,
55
+ async (element, index) => element.type
56
+ );
57
+ assert.deepStrictEqual(Array.from(map.entries()), [
58
+ [
59
+ "fruit",
60
+ [
61
+ { name: "apple", type: "fruit" },
62
+ { name: "cherry", type: "fruit" },
63
+ ],
64
+ ],
65
+ ["vegetable", [{ name: "beet", type: "vegetable" }]],
66
+ ]);
67
+ });
68
+
69
+ test("has returns true if key exists in keys()", async () => {
70
+ const map = new AsyncMap();
71
+ map.keys = async function* () {
72
+ yield* ["a", "b/"];
73
+ };
74
+ assert.strictEqual(await map.has("a"), true);
75
+ assert.strictEqual(await map.has("b"), true); // trailing slash optional
76
+ assert.strictEqual(await map.has("b/"), true);
77
+ assert.strictEqual(await map.has("c"), false);
78
+ });
79
+
80
+ test("readOnly if get() is overridden but not delete() and set()", () => {
81
+ const map1 = new AsyncMap();
82
+ assert.strictEqual(map1.readOnly, false);
83
+
84
+ const map2 = new AsyncMap();
85
+ map2.get = async (key) => null;
86
+ assert.strictEqual(map2.readOnly, true);
87
+
88
+ const map3 = new AsyncMap();
89
+ map3.delete = async (key) => false;
90
+ map3.get = async (key) => null;
91
+ map3.set = async (key, value) => map3;
92
+ assert.strictEqual(map3.readOnly, false);
93
+ });
94
+
95
+ test("size", async () => {
96
+ const map = new SampleAsyncMap();
97
+ assert.strictEqual(await map.size, 0);
98
+ await map.set("a", 1);
99
+ assert.strictEqual(await map.size, 1);
100
+ await map.set("b", 2);
101
+ assert.strictEqual(await map.size, 2);
102
+ await map.delete("a");
103
+ assert.strictEqual(await map.size, 1);
104
+ await map.clear();
105
+ assert.strictEqual(await map.size, 0);
106
+ });
107
+
108
+ test("values", async () => {
109
+ const map = new SampleAsyncMap([
110
+ ["a", 1],
111
+ ["b", 2],
112
+ ]);
113
+ const values = [];
114
+ for await (const value of map.values()) {
115
+ values.push(value);
116
+ }
117
+ assert.deepStrictEqual(values, [1, 2]);
118
+ });
119
+ });
@@ -1,15 +1,17 @@
1
1
  import assert from "node:assert";
2
2
  import { describe, test } from "node:test";
3
- import BrowserFileTree from "../../src/drivers/BrowserFileTree.js";
4
- import { Tree } from "../../src/internal.js";
3
+ import BrowserFileMap from "../../src/drivers/BrowserFileMap.js";
4
+ import keys from "../../src/operations/keys.js";
5
+ import map from "../../src/operations/map.js";
6
+ import plain from "../../src/operations/plain.js";
5
7
 
6
8
  // Skip these tests if we're not in a browser.
7
9
  const isBrowser = typeof window !== "undefined";
8
10
  if (isBrowser) {
9
- describe("BrowserFileTree", async () => {
11
+ describe("BrowserFileMap", async () => {
10
12
  test("can get the keys of the tree", async () => {
11
13
  const fixture = await createFixture();
12
- assert.deepEqual(Array.from(await fixture.keys()), [
14
+ assert.deepEqual(await keys(fixture), [
13
15
  "Alice.md",
14
16
  "Bob.md",
15
17
  "Carol.md",
@@ -57,6 +59,24 @@ if (isBrowser) {
57
59
  assert(await fixture.get("subfolder/"));
58
60
  });
59
61
 
62
+ test("can delete a value", async () => {
63
+ const fixture = await createFixture();
64
+
65
+ // Delete existing key.
66
+ const deleted = await fixture.delete("Bob.md");
67
+ assert.equal(deleted, true);
68
+
69
+ // Delete non-existing key.
70
+ const deletedAgain = await fixture.delete("Bob.md");
71
+ assert.equal(deletedAgain, false);
72
+
73
+ assert.deepEqual(await strings(fixture), {
74
+ "Alice.md": "Hello, **Alice**.",
75
+ "Carol.md": "Hello, **Carol**.",
76
+ subfolder: {},
77
+ });
78
+ });
79
+
60
80
  test("can set a value", async () => {
61
81
  const fixture = await createFixture();
62
82
 
@@ -66,32 +86,31 @@ if (isBrowser) {
66
86
  // New key.
67
87
  await fixture.set("David.md", "Hello, **David**.");
68
88
 
69
- // Delete key.
70
- await fixture.set("Bob.md", undefined);
71
-
72
- // Delete non-existent key.
73
- await fixture.set("xyz", undefined);
74
-
75
89
  assert.deepEqual(await strings(fixture), {
76
90
  "Alice.md": "Goodbye, **Alice**.",
91
+ "Bob.md": "Hello, **Bob**.",
77
92
  "Carol.md": "Hello, **Carol**.",
78
93
  "David.md": "Hello, **David**.",
79
94
  subfolder: {},
80
95
  });
81
96
  });
82
97
 
98
+ test("can create an empty subfolder", async () => {
99
+ const fixture = await createFixture();
100
+ await fixture.set("emptyFolder", BrowserFileMap.EMPTY);
101
+ assert.deepEqual(await strings(fixture), {
102
+ "Alice.md": "Hello, **Alice**.",
103
+ "Bob.md": "Hello, **Bob**.",
104
+ "Carol.md": "Hello, **Carol**.",
105
+ emptyFolder: {},
106
+ subfolder: {},
107
+ });
108
+ });
109
+
83
110
  test("can create a subfolder via set", async () => {
84
111
  const fixture = await createFixture();
85
- const tree = {
86
- async get(key) {
87
- const name = key.replace(/\.md$/, "");
88
- return `Hello, **${name}**.`;
89
- },
90
- async keys() {
91
- return ["Ellen.md"];
92
- },
93
- };
94
- await fixture.set("more", tree);
112
+ const more = new Map([["Ellen.md", "Hello, **Ellen**."]]);
113
+ await fixture.set("more", more);
95
114
  assert.deepEqual(await strings(fixture), {
96
115
  "Alice.md": "Hello, **Alice**.",
97
116
  "Bob.md": "Hello, **Bob**.",
@@ -141,12 +160,12 @@ async function createFixture() {
141
160
  create: true,
142
161
  });
143
162
 
144
- return new BrowserFileTree(subdirectory);
163
+ return new BrowserFileMap(subdirectory);
145
164
  }
146
165
 
147
166
  async function strings(tree) {
148
- return Tree.plain(
149
- Tree.map(tree, {
167
+ return plain(
168
+ await map(tree, {
150
169
  deep: true,
151
170
  value: (value) => text(value),
152
171
  })
@@ -1,14 +1,12 @@
1
1
  import assert from "node:assert";
2
2
  import { describe, test } from "node:test";
3
- import calendar from "../../src/drivers/calendarTree.js";
3
+ import CalendarMap from "../../src/drivers/CalendarMap.js";
4
4
  import toPlainValue from "../../src/utilities/toPlainValue.js";
5
5
 
6
- describe("calendarTree", () => {
7
- test("without a start or end, returns a tree for today", async () => {
8
- const tree = calendar({
9
- value: (year, month, day) => `${year}-${month}-${day}`,
10
- });
11
- const plain = await toPlainValue(tree);
6
+ describe("CalendarMap", () => {
7
+ test("without a start or end, returns a map for today", async () => {
8
+ const map = new CalendarMap();
9
+ const plain = await toPlainValue(map);
12
10
  const today = new Date();
13
11
  const year = today.getFullYear();
14
12
  const month = (today.getMonth() + 1).toString().padStart(2, "0");
@@ -22,13 +20,12 @@ describe("calendarTree", () => {
22
20
  });
23
21
  });
24
22
 
25
- test("returns a tree for a month range", async () => {
26
- const tree = calendar({
23
+ test("returns a map for a month range", async () => {
24
+ const map = new CalendarMap({
27
25
  start: "2025-01",
28
26
  end: "2025-02",
29
- value: (year, month, day) => `${year}-${month}-${day}`,
30
27
  });
31
- const plain = await toPlainValue(tree);
28
+ const plain = await toPlainValue(map);
32
29
  assert.deepEqual(plain, {
33
30
  2025: {
34
31
  "01": {
@@ -98,22 +95,23 @@ describe("calendarTree", () => {
98
95
  });
99
96
  });
100
97
 
101
- test("returns a tree for a day range", async () => {
102
- const tree = calendar({
98
+ test("returns a map for a day range", async () => {
99
+ const map = new CalendarMap({
103
100
  start: "2025-02-27",
104
101
  end: "2025-03-02",
105
- value: (year, month, day) => `${year}-${month}-${day}`,
102
+ // Exercise custom value function
103
+ value: (year, month, day) => `${year}.${month}.${day}`,
106
104
  });
107
- const plain = await toPlainValue(tree);
105
+ const plain = await toPlainValue(map);
108
106
  assert.deepEqual(plain, {
109
107
  2025: {
110
108
  "02": {
111
- 27: "2025-02-27",
112
- 28: "2025-02-28",
109
+ 27: "2025.02.27",
110
+ 28: "2025.02.28",
113
111
  },
114
112
  "03": {
115
- "01": "2025-03-01",
116
- "02": "2025-03-02",
113
+ "01": "2025.03.01",
114
+ "02": "2025.03.02",
117
115
  },
118
116
  },
119
117
  });
@@ -0,0 +1,15 @@
1
+ import assert from "node:assert";
2
+ import { describe, test } from "node:test";
3
+ import ConstantMap from "../../src/drivers/ConstantMap.js";
4
+ import keys from "../../src/operations/keys.js";
5
+ import traverse from "../../src/operations/traverse.js";
6
+
7
+ describe("ConstantMap", () => {
8
+ test("returns a deep tree that returns constant for all keys", async () => {
9
+ const fixture = new ConstantMap(1);
10
+ assert.deepEqual(Array.from(await keys(fixture)), []);
11
+ assert.equal(fixture.get("a"), 1);
12
+ assert.equal(fixture.get("b"), 1);
13
+ assert.equal(await traverse(fixture, "c/", "d/", "e"), 1);
14
+ });
15
+ });
@@ -0,0 +1,36 @@
1
+ import assert from "node:assert";
2
+ import { describe, test } from "node:test";
3
+ import DeepObjectMap from "../../src/drivers/DeepObjectMap.js";
4
+ import plain from "../../src/operations/plain.js";
5
+
6
+ describe("DeepObjectMap", () => {
7
+ test("returns a map for a value that's a plain sub-object or sub-array", async () => {
8
+ const map = createFixture();
9
+
10
+ const object = await map.get("object");
11
+ assert.equal(object instanceof DeepObjectMap, true);
12
+ assert.deepEqual(await plain(object), { b: 2 });
13
+ assert.equal(object.parent, map);
14
+
15
+ const array = await map.get("array");
16
+ assert.equal(array instanceof DeepObjectMap, true);
17
+ assert.deepEqual(await plain(array), [3]);
18
+ assert.equal(array.parent, map);
19
+ });
20
+
21
+ test("adds trailing slashes to keys for submaps including plain objects or arrays", async () => {
22
+ const map = createFixture();
23
+ const keys = Array.from(await map.keys());
24
+ assert.deepEqual(keys, ["a", "object/", "array/"]);
25
+ });
26
+ });
27
+
28
+ function createFixture() {
29
+ return new DeepObjectMap({
30
+ a: 1,
31
+ object: {
32
+ b: 2,
33
+ },
34
+ array: [3],
35
+ });
36
+ }
@@ -1,7 +1,9 @@
1
1
  import assert from "node:assert";
2
2
  import { beforeEach, describe, mock, test } from "node:test";
3
- import ExplorableSiteTree from "../../src/drivers/ExplorableSiteTree.js";
4
- import { Tree } from "../../src/internal.js";
3
+ import ExplorableSiteMap from "../../src/drivers/ExplorableSiteMap.js";
4
+ import isMap from "../../src/operations/isMap.js";
5
+ import map from "../../src/operations/map.js";
6
+ import * as trailingSlash from "../../src/trailingSlash.js";
5
7
 
6
8
  const textDecoder = new TextDecoder();
7
9
  const textEncoder = new TextEncoder();
@@ -34,31 +36,35 @@ const mockResponses = {
34
36
  },
35
37
  };
36
38
 
37
- describe("ExplorableSiteTree", () => {
39
+ describe("ExplorableSiteMap", () => {
38
40
  beforeEach(() => {
39
41
  mock.method(global, "fetch", mockFetch);
40
42
  });
41
43
 
42
44
  test("can get the keys of a tree", async () => {
43
- const fixture = new ExplorableSiteTree(mockHost);
44
- const keys = await fixture.keys();
45
- assert.deepEqual(Array.from(keys), ["about/", "index.html"]);
45
+ const fixture = new ExplorableSiteMap(mockHost);
46
+ const keys = [];
47
+ for await (const key of fixture.keys()) {
48
+ keys.push(key);
49
+ }
50
+ assert.deepEqual(keys, ["about/", "index.html"]);
46
51
  });
47
52
 
48
53
  test("can get a plain value for a key", async () => {
49
- const fixture = new ExplorableSiteTree(mockHost);
54
+ const fixture = new ExplorableSiteMap(mockHost);
55
+ /** @type {any} */
50
56
  const arrayBuffer = await fixture.get("index.html");
51
57
  const text = textDecoder.decode(arrayBuffer);
52
58
  assert.equal(text, "Home page");
53
59
  });
54
60
 
55
61
  test("getting an unsupported key returns undefined", async () => {
56
- const fixture = new ExplorableSiteTree(mockHost);
62
+ const fixture = new ExplorableSiteMap(mockHost);
57
63
  assert.equal(await fixture.get("xyz"), undefined);
58
64
  });
59
65
 
60
66
  test("getting a null/undefined key throws an exception", async () => {
61
- const fixture = new ExplorableSiteTree(mockHost);
67
+ const fixture = new ExplorableSiteMap(mockHost);
62
68
  await assert.rejects(async () => {
63
69
  await fixture.get(null);
64
70
  });
@@ -68,20 +74,20 @@ describe("ExplorableSiteTree", () => {
68
74
  });
69
75
 
70
76
  test("can return a new tree for a key that redirects", async () => {
71
- const fixture = new ExplorableSiteTree(mockHost);
77
+ const fixture = new ExplorableSiteMap(mockHost);
72
78
  const about = await fixture.get("about");
73
- assert(about instanceof ExplorableSiteTree);
79
+ assert(about instanceof ExplorableSiteMap);
74
80
  assert.equal(about.href, "https://mock/about/");
75
81
  });
76
82
 
77
83
  test("can convert a site to a plain object", async () => {
78
- const fixture = new ExplorableSiteTree(mockHost);
84
+ const fixture = new ExplorableSiteMap(mockHost);
79
85
  // Convert buffers to strings.
80
- const strings = await Tree.map(fixture, {
86
+ const strings = await map(fixture, {
81
87
  deep: true,
82
88
  value: (value) => textDecoder.decode(value),
83
89
  });
84
- assert.deepEqual(await Tree.plain(strings), {
90
+ assert.deepEqual(await plain(strings), {
85
91
  about: {
86
92
  "Alice.html": "Hello, Alice!",
87
93
  "Bob.html": "Hello, Bob!",
@@ -114,3 +120,12 @@ async function mockFetch(href) {
114
120
  status: 404,
115
121
  };
116
122
  }
123
+
124
+ async function plain(tree) {
125
+ const result = {};
126
+ for await (const [key, value] of tree.entries()) {
127
+ const normalized = trailingSlash.remove(key);
128
+ result[normalized] = isMap(value) ? await plain(value) : value;
129
+ }
130
+ return result;
131
+ }
@@ -0,0 +1,185 @@
1
+ import assert from "node:assert";
2
+ import * as fs from "node:fs";
3
+ import path from "node:path";
4
+ import { describe, test } from "node:test";
5
+ import { fileURLToPath } from "node:url";
6
+ import FileMap from "../../src/drivers/FileMap.js";
7
+ import map from "../../src/operations/map.js";
8
+ import plain from "../../src/operations/plain.js";
9
+
10
+ const dirname = path.dirname(fileURLToPath(import.meta.url));
11
+ const tempDirectory = path.join(dirname, "fixtures/temp");
12
+
13
+ const textDecoder = new TextDecoder();
14
+
15
+ describe("FileMap", () => {
16
+ test("can get the keys of the tree", () => {
17
+ const fixture = createFixture("fixtures/markdown");
18
+ assert.deepEqual(Array.from(fixture.keys()), [
19
+ "Alice.md",
20
+ "Bob.md",
21
+ "Carol.md",
22
+ "subfolder/",
23
+ ]);
24
+ });
25
+
26
+ test("can get the value for a key", () => {
27
+ const fixture = createFixture("fixtures/markdown");
28
+ const buffer = fixture.get("Alice.md");
29
+ const text = textDecoder.decode(buffer);
30
+ assert.equal(text, "Hello, **Alice**.");
31
+ });
32
+
33
+ test("getting an unsupported key returns undefined", () => {
34
+ const fixture = createFixture("fixtures/markdown");
35
+ assert.equal(fixture.get("xyz"), undefined);
36
+ });
37
+
38
+ test("getting empty key returns undefined", () => {
39
+ const fixture = createFixture("fixtures/markdown");
40
+ assert.equal(fixture.get(""), undefined);
41
+ });
42
+
43
+ test("getting a null/undefined key throws an exception", async () => {
44
+ const fixture = createFixture("fixtures/markdown");
45
+ await assert.rejects(async () => {
46
+ await fixture.get(null);
47
+ });
48
+ await assert.rejects(async () => {
49
+ await fixture.get(undefined);
50
+ });
51
+ });
52
+
53
+ test("can retrieve values with optional trailing slash", () => {
54
+ const fixture = createFixture("fixtures/markdown");
55
+ assert(fixture.get("Alice.md"));
56
+ assert(fixture.get("Alice.md/"));
57
+ assert(fixture.get("subfolder"));
58
+ assert(fixture.get("subfolder/"));
59
+ });
60
+
61
+ test("sets parent on subtrees", () => {
62
+ const fixture = createFixture("fixtures");
63
+ const markdown = fixture.get("markdown");
64
+ assert.deepEqual(markdown.parent, fixture);
65
+ });
66
+
67
+ test("can write out a file via set()", () => {
68
+ createTempDirectory();
69
+
70
+ // Write out a file.
71
+ const fileName = "file1";
72
+ const fileText = "This is the first file.";
73
+ const tempFiles = new FileMap(tempDirectory);
74
+ tempFiles.set(fileName, fileText);
75
+
76
+ // Read it back in.
77
+ const filePath = path.join(tempDirectory, fileName);
78
+ const actualText = String(fs.readFileSync(filePath));
79
+
80
+ assert.equal(fileText, actualText);
81
+
82
+ removeTempDirectory();
83
+ });
84
+
85
+ test("create subfolder via set() with EMPTY", () => {
86
+ createTempDirectory();
87
+
88
+ // Write out new, empty folder called "empty".
89
+ const tempFiles = new FileMap(tempDirectory);
90
+ tempFiles.set("empty", FileMap.EMPTY);
91
+
92
+ // Verify folder exists and has no contents.
93
+ const folderPath = path.join(tempDirectory, "empty");
94
+ const stats = fs.statSync(folderPath);
95
+ assert(stats.isDirectory());
96
+ const files = fs.readdirSync(folderPath);
97
+ assert.deepEqual(files, []);
98
+
99
+ removeTempDirectory();
100
+ });
101
+
102
+ test("can write out subfolder via set()", async () => {
103
+ createTempDirectory();
104
+
105
+ // Create a tiny set of "files".
106
+ // @ts-ignore
107
+ const files = new Map([
108
+ ["file1", "This is the first file."],
109
+ ["subfolder", new Map([["file2", "This is the second file."]])],
110
+ ]);
111
+ const object = await plain(files);
112
+
113
+ // Write out files as a new folder called "folder".
114
+ const tempFiles = new FileMap(tempDirectory);
115
+ tempFiles.set("folder", files);
116
+
117
+ // Read them back in.
118
+ const actualFiles = tempFiles.get("folder");
119
+ const strings = await map(actualFiles, {
120
+ deep: true,
121
+ value: (buffer) => textDecoder.decode(buffer),
122
+ });
123
+ const result = await plain(strings);
124
+ assert.deepEqual(result, object);
125
+
126
+ removeTempDirectory();
127
+ });
128
+
129
+ test("can delete a file", () => {
130
+ createTempDirectory();
131
+ const tempFile = path.join(tempDirectory, "file");
132
+ fs.writeFileSync(tempFile, "");
133
+ const tempFiles = new FileMap(tempDirectory);
134
+ const deleted = tempFiles.delete("file");
135
+ assert(deleted);
136
+ let stats;
137
+ try {
138
+ stats = fs.statSync(tempFile);
139
+ } catch (/** @type {any} */ error) {
140
+ if (error.code !== "ENOENT") {
141
+ throw error;
142
+ }
143
+ }
144
+ assert(stats === undefined);
145
+ removeTempDirectory();
146
+ });
147
+
148
+ test("delete returns false if file does not exist", () => {
149
+ createTempDirectory();
150
+ const tempFiles = new FileMap(tempDirectory);
151
+ const deleted = tempFiles.delete("nonexistent-file");
152
+ assert(!deleted);
153
+ removeTempDirectory();
154
+ });
155
+
156
+ test("can delete a folder", () => {
157
+ createTempDirectory();
158
+ const folder = path.join(tempDirectory, "folder");
159
+ fs.mkdirSync(folder);
160
+ const tempFiles = new FileMap(tempDirectory);
161
+ tempFiles.delete("folder");
162
+ let stats;
163
+ try {
164
+ stats = fs.statSync(folder);
165
+ } catch (/** @type {any} */ error) {
166
+ if (error.code !== "ENOENT") {
167
+ throw error;
168
+ }
169
+ }
170
+ assert(stats === undefined);
171
+ removeTempDirectory();
172
+ });
173
+ });
174
+
175
+ function createFixture(fixturePath) {
176
+ return new FileMap(path.join(dirname, fixturePath));
177
+ }
178
+
179
+ function createTempDirectory() {
180
+ fs.mkdirSync(tempDirectory, { recursive: true });
181
+ }
182
+
183
+ function removeTempDirectory() {
184
+ fs.rmSync(tempDirectory, { force: true, recursive: true });
185
+ }
@@ -0,0 +1,56 @@
1
+ import assert from "node:assert";
2
+ import { describe, test } from "node:test";
3
+ import FunctionMap from "../../src/drivers/FunctionMap.js";
4
+ import * as symbols from "../../src/symbols.js";
5
+
6
+ describe("FunctionMap", () => {
7
+ test("keys uses supplied domain", () => {
8
+ const map = createFixture();
9
+ assert.deepEqual(Array.from(map.keys()), [
10
+ "Alice.md",
11
+ "Bob.md",
12
+ "Carol.md",
13
+ ]);
14
+ });
15
+
16
+ test("can get the value for a key", () => {
17
+ const map = createFixture();
18
+ const alice = map.get("Alice.md");
19
+ assert.equal(alice, "Hello, **Alice**.");
20
+ });
21
+
22
+ test("can get the value for an async function", async () => {
23
+ const map = new FunctionMap(async (s) => ({ s }));
24
+ const result = await map.get("test");
25
+ assert.deepEqual(result, {
26
+ s: "test",
27
+ });
28
+ assert.equal(result[symbols.parent], map);
29
+ });
30
+
31
+ test("getting a value from function with multiple arguments curries the function", () => {
32
+ const map = new FunctionMap((a, b, c) => a + b + c);
33
+ const fnA = map.get(1);
34
+ const fnAB = fnA.get(2);
35
+ const result = fnAB.get(3);
36
+ assert.equal(result, 6);
37
+ });
38
+
39
+ test("getting an unsupported key returns undefined", () => {
40
+ const fixture = createFixture();
41
+ assert.equal(fixture.get("xyz"), undefined);
42
+ });
43
+ });
44
+
45
+ function createFixture() {
46
+ return new FunctionMap(
47
+ (key) => {
48
+ if (key?.endsWith?.(".md")) {
49
+ const name = key.slice(0, -3);
50
+ return `Hello, **${name}**.`;
51
+ }
52
+ return undefined;
53
+ },
54
+ ["Alice.md", "Bob.md", "Carol.md"]
55
+ );
56
+ }