@weborigami/async-tree 0.5.8 → 0.6.1
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.
- package/browser.js +1 -1
- package/index.ts +43 -35
- package/main.js +1 -2
- package/package.json +4 -7
- package/shared.js +74 -12
- package/src/Tree.js +11 -2
- package/src/drivers/AsyncMap.js +237 -0
- package/src/drivers/{BrowserFileTree.js → BrowserFileMap.js} +54 -38
- package/src/drivers/{calendarTree.js → CalendarMap.js} +80 -63
- package/src/drivers/ConstantMap.js +28 -0
- package/src/drivers/{ExplorableSiteTree.js → ExplorableSiteMap.js} +7 -7
- package/src/drivers/FileMap.js +238 -0
- package/src/drivers/{FunctionTree.js → FunctionMap.js} +19 -22
- package/src/drivers/ObjectMap.js +151 -0
- package/src/drivers/SetMap.js +13 -0
- package/src/drivers/{SiteTree.js → SiteMap.js} +17 -20
- package/src/drivers/SyncMap.js +260 -0
- package/src/jsonKeys.d.ts +2 -2
- package/src/jsonKeys.js +20 -5
- package/src/operations/addNextPrevious.js +35 -36
- package/src/operations/assign.js +27 -23
- package/src/operations/cache.js +30 -36
- package/src/operations/cachedKeyFunctions.js +1 -1
- package/src/operations/calendar.js +5 -0
- package/src/operations/child.js +35 -0
- package/src/operations/clear.js +13 -12
- package/src/operations/constant.js +5 -0
- package/src/operations/deepEntries.js +23 -0
- package/src/operations/deepMap.js +9 -9
- package/src/operations/deepMerge.js +36 -25
- package/src/operations/deepReverse.js +23 -16
- package/src/operations/deepTake.js +7 -7
- package/src/operations/deepText.js +4 -4
- package/src/operations/deepValues.js +3 -6
- package/src/operations/deepValuesIterator.js +11 -11
- package/src/operations/delete.js +8 -12
- package/src/operations/entries.js +17 -10
- package/src/operations/filter.js +9 -7
- package/src/operations/first.js +12 -10
- package/src/operations/forEach.js +10 -13
- package/src/operations/from.js +30 -39
- package/src/operations/globKeys.js +22 -17
- package/src/operations/group.js +2 -2
- package/src/operations/groupBy.js +24 -22
- package/src/operations/has.js +7 -9
- package/src/operations/indent.js +2 -2
- package/src/operations/inners.js +19 -15
- package/src/operations/invokeFunctions.js +22 -10
- package/src/operations/isAsyncMutableTree.js +5 -12
- package/src/operations/isAsyncTree.js +5 -20
- package/src/operations/isMap.js +39 -0
- package/src/operations/isMaplike.js +34 -0
- package/src/operations/isReadOnlyMap.js +14 -0
- package/src/operations/isTraversable.js +3 -3
- package/src/operations/isTreelike.js +5 -30
- package/src/operations/json.js +4 -12
- package/src/operations/keys.js +17 -8
- package/src/operations/length.js +9 -8
- package/src/operations/map.js +28 -30
- package/src/operations/mapExtension.js +20 -16
- package/src/operations/mapReduce.js +38 -24
- package/src/operations/mask.js +54 -29
- package/src/operations/match.js +13 -9
- package/src/operations/merge.js +43 -35
- package/src/operations/paginate.js +26 -18
- package/src/operations/parent.js +7 -7
- package/src/operations/paths.js +20 -22
- package/src/operations/plain.js +6 -6
- package/src/operations/reduce.js +16 -0
- package/src/operations/regExpKeys.js +21 -12
- package/src/operations/reverse.js +21 -15
- package/src/operations/root.js +6 -5
- package/src/operations/scope.js +31 -26
- package/src/operations/set.js +20 -0
- package/src/operations/shuffle.js +23 -16
- package/src/operations/size.js +13 -0
- package/src/operations/sort.js +55 -40
- package/src/operations/sync.js +14 -0
- package/src/operations/take.js +23 -11
- package/src/operations/text.js +4 -4
- package/src/operations/toFunction.js +7 -7
- package/src/operations/traverse.js +4 -4
- package/src/operations/traverseOrThrow.js +18 -9
- package/src/operations/traversePath.js +2 -2
- package/src/operations/values.js +18 -9
- package/src/operations/withKeys.js +22 -16
- package/src/symbols.js +1 -0
- package/src/utilities/castArraylike.js +24 -13
- package/src/utilities/getMapArgument.js +38 -0
- package/src/utilities/getParent.js +2 -2
- package/src/utilities/isStringlike.js +7 -5
- package/src/utilities/setParent.js +7 -7
- package/src/utilities/toFunction.js +2 -2
- package/src/utilities/toPlainValue.js +21 -19
- package/test/SampleAsyncMap.js +34 -0
- package/test/browser/assert.js +20 -0
- package/test/browser/index.html +53 -21
- package/test/drivers/AsyncMap.test.js +119 -0
- package/test/drivers/{BrowserFileTree.test.js → BrowserFileMap.test.js} +50 -33
- package/test/drivers/{calendarTree.test.js → CalendarMap.test.js} +17 -19
- package/test/drivers/ConstantMap.test.js +15 -0
- package/test/drivers/{ExplorableSiteTree.test.js → ExplorableSiteMap.test.js} +29 -14
- package/test/drivers/FileMap.test.js +156 -0
- package/test/drivers/FunctionMap.test.js +56 -0
- package/test/drivers/ObjectMap.test.js +194 -0
- package/test/drivers/SetMap.test.js +35 -0
- package/test/drivers/{SiteTree.test.js → SiteMap.test.js} +14 -10
- package/test/drivers/SyncMap.test.js +335 -0
- package/test/jsonKeys.test.js +18 -6
- package/test/operations/addNextPrevious.test.js +3 -2
- package/test/operations/assign.test.js +30 -35
- package/test/operations/cache.test.js +17 -12
- package/test/operations/cachedKeyFunctions.test.js +12 -9
- package/test/operations/child.test.js +34 -0
- package/test/operations/clear.test.js +6 -27
- package/test/operations/deepEntries.test.js +32 -0
- package/test/operations/deepMerge.test.js +23 -16
- package/test/operations/deepReverse.test.js +2 -2
- package/test/operations/deepTake.test.js +2 -2
- package/test/operations/deepText.test.js +4 -4
- package/test/operations/deepValuesIterator.test.js +2 -2
- package/test/operations/delete.test.js +2 -2
- package/test/operations/extensionKeyFunctions.test.js +6 -5
- package/test/operations/filter.test.js +3 -3
- package/test/operations/from.test.js +25 -31
- package/test/operations/globKeys.test.js +9 -9
- package/test/operations/groupBy.test.js +6 -5
- package/test/operations/inners.test.js +17 -14
- package/test/operations/invokeFunctions.test.js +2 -2
- package/test/operations/isMap.test.js +15 -0
- package/test/operations/isMaplike.test.js +15 -0
- package/test/operations/json.test.js +2 -2
- package/test/operations/keys.test.js +16 -3
- package/test/operations/map.test.js +40 -30
- package/test/operations/mapExtension.test.js +6 -6
- package/test/operations/mapReduce.test.js +14 -12
- package/test/operations/mask.test.js +16 -3
- package/test/operations/match.test.js +2 -2
- package/test/operations/merge.test.js +20 -14
- package/test/operations/paginate.test.js +5 -5
- package/test/operations/parent.test.js +3 -3
- package/test/operations/paths.test.js +20 -27
- package/test/operations/plain.test.js +8 -8
- package/test/operations/regExpKeys.test.js +22 -18
- package/test/operations/reverse.test.js +4 -3
- package/test/operations/scope.test.js +6 -5
- package/test/operations/set.test.js +11 -0
- package/test/operations/shuffle.test.js +3 -2
- package/test/operations/sort.test.js +7 -10
- package/test/operations/sync.test.js +43 -0
- package/test/operations/take.test.js +2 -2
- package/test/operations/toFunction.test.js +2 -2
- package/test/operations/traverse.test.js +17 -5
- package/test/operations/withKeys.test.js +2 -2
- package/test/utilities/castArrayLike.test.js +53 -0
- package/test/utilities/setParent.test.js +6 -6
- package/test/utilities/toFunction.test.js +2 -2
- package/test/utilities/toPlainValue.test.js +51 -12
- package/src/drivers/DeepMapTree.js +0 -23
- package/src/drivers/DeepObjectTree.js +0 -18
- package/src/drivers/DeferredTree.js +0 -81
- package/src/drivers/FileTree.js +0 -276
- package/src/drivers/MapTree.js +0 -70
- package/src/drivers/ObjectTree.js +0 -158
- package/src/drivers/SetTree.js +0 -34
- package/src/drivers/constantTree.js +0 -19
- package/src/drivers/limitConcurrency.js +0 -63
- package/src/internal.js +0 -16
- package/src/utilities/getTreeArgument.js +0 -43
- package/test/drivers/DeepMapTree.test.js +0 -17
- package/test/drivers/DeepObjectTree.test.js +0 -35
- package/test/drivers/DeferredTree.test.js +0 -22
- package/test/drivers/FileTree.test.js +0 -192
- package/test/drivers/FunctionTree.test.js +0 -46
- package/test/drivers/MapTree.test.js +0 -59
- package/test/drivers/ObjectTree.test.js +0 -163
- package/test/drivers/SetTree.test.js +0 -44
- package/test/drivers/constantTree.test.js +0 -13
- package/test/drivers/limitConcurrency.test.js +0 -41
- package/test/operations/isAsyncMutableTree.test.js +0 -17
- package/test/operations/isAsyncTree.test.js +0 -26
- package/test/operations/isTreelike.test.js +0 -13
|
@@ -1,24 +1,21 @@
|
|
|
1
1
|
import { hiddenFileNames } from "../constants.js";
|
|
2
|
-
import
|
|
3
|
-
import isTreelike from "../operations/isTreelike.js";
|
|
2
|
+
import isMap from "../operations/isMap.js";
|
|
4
3
|
import * as trailingSlash from "../trailingSlash.js";
|
|
5
4
|
import isStringlike from "../utilities/isStringlike.js";
|
|
6
5
|
import naturalOrder from "../utilities/naturalOrder.js";
|
|
7
6
|
import setParent from "../utilities/setParent.js";
|
|
7
|
+
import AsyncMap from "./AsyncMap.js";
|
|
8
8
|
|
|
9
9
|
const TypedArray = Object.getPrototypeOf(Uint8Array);
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
|
-
* A
|
|
12
|
+
* A map of files backed by a browser-hosted file system such as the standard
|
|
13
13
|
* Origin Private File System or the (as of October 2023) experimental File
|
|
14
14
|
* System Access API.
|
|
15
|
-
*
|
|
16
|
-
* @typedef {import("@weborigami/types").AsyncMutableTree} AsyncMutableTree
|
|
17
|
-
* @implements {AsyncMutableTree}
|
|
18
15
|
*/
|
|
19
|
-
export default class
|
|
16
|
+
export default class BrowserFileMap extends AsyncMap {
|
|
20
17
|
/**
|
|
21
|
-
* Construct a
|
|
18
|
+
* Construct a map of files backed by a browser-hosted file system.
|
|
22
19
|
*
|
|
23
20
|
* The directory handle can be obtained via any of the [methods that return a
|
|
24
21
|
* FileSystemDirectoryHandle](https://developer.mozilla.org/en-US/docs/Web/API/FileSystemDirectoryHandle).
|
|
@@ -28,11 +25,50 @@ export default class BrowserFileTree {
|
|
|
28
25
|
* @param {FileSystemDirectoryHandle} [directoryHandle]
|
|
29
26
|
*/
|
|
30
27
|
constructor(directoryHandle) {
|
|
31
|
-
|
|
32
|
-
* @ts-ignore */
|
|
28
|
+
super();
|
|
33
29
|
this.directory = directoryHandle;
|
|
34
30
|
}
|
|
35
31
|
|
|
32
|
+
async child(key) {
|
|
33
|
+
const normalized = trailingSlash.remove(key);
|
|
34
|
+
let result = await this.get(normalized);
|
|
35
|
+
|
|
36
|
+
// If child is already a map we can use it as is
|
|
37
|
+
if (!isMap(result)) {
|
|
38
|
+
// Create subfolder
|
|
39
|
+
const directory = await this.getDirectory();
|
|
40
|
+
if (result) {
|
|
41
|
+
// Delete existing file with same name
|
|
42
|
+
await directory.removeEntry(normalized);
|
|
43
|
+
}
|
|
44
|
+
const subfolderHandle = await directory.getDirectoryHandle(normalized, {
|
|
45
|
+
create: true,
|
|
46
|
+
});
|
|
47
|
+
result = Reflect.construct(this.constructor, [subfolderHandle]);
|
|
48
|
+
setParent(result, this);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async delete(key) {
|
|
55
|
+
const normalized = trailingSlash.remove(key);
|
|
56
|
+
const directory = await this.getDirectory();
|
|
57
|
+
|
|
58
|
+
// Delete file.
|
|
59
|
+
try {
|
|
60
|
+
await directory.removeEntry(normalized);
|
|
61
|
+
} catch (error) {
|
|
62
|
+
// If the file didn't exist, ignore the error.
|
|
63
|
+
if (error instanceof DOMException && error.name === "NotFoundError") {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
|
|
36
72
|
async get(key) {
|
|
37
73
|
if (key == null) {
|
|
38
74
|
// Reject nullish key.
|
|
@@ -90,15 +126,15 @@ export default class BrowserFileTree {
|
|
|
90
126
|
return this.directory;
|
|
91
127
|
}
|
|
92
128
|
|
|
93
|
-
async keys() {
|
|
129
|
+
async *keys() {
|
|
94
130
|
const directory = await this.getDirectory();
|
|
95
131
|
let keys = [];
|
|
96
132
|
// @ts-ignore
|
|
97
133
|
for await (const entryKey of directory.keys()) {
|
|
98
134
|
// Check if the entry is a subfolder
|
|
99
|
-
const
|
|
135
|
+
const normalized = trailingSlash.remove(entryKey);
|
|
100
136
|
const subfolderHandle = await directory
|
|
101
|
-
.getDirectoryHandle(
|
|
137
|
+
.getDirectoryHandle(normalized)
|
|
102
138
|
.catch(() => null);
|
|
103
139
|
const isSubfolder = subfolderHandle !== null;
|
|
104
140
|
|
|
@@ -110,28 +146,13 @@ export default class BrowserFileTree {
|
|
|
110
146
|
keys = keys.filter((key) => !hiddenFileNames.includes(key));
|
|
111
147
|
keys.sort(naturalOrder);
|
|
112
148
|
|
|
113
|
-
|
|
149
|
+
yield* keys;
|
|
114
150
|
}
|
|
115
151
|
|
|
116
152
|
async set(key, value) {
|
|
117
|
-
const
|
|
153
|
+
const normalized = trailingSlash.remove(key);
|
|
118
154
|
const directory = await this.getDirectory();
|
|
119
155
|
|
|
120
|
-
if (value === undefined) {
|
|
121
|
-
// Delete file.
|
|
122
|
-
try {
|
|
123
|
-
await directory.removeEntry(baseKey);
|
|
124
|
-
} catch (error) {
|
|
125
|
-
// If the file didn't exist, ignore the error.
|
|
126
|
-
if (
|
|
127
|
-
!(error instanceof DOMException && error.name === "NotFoundError")
|
|
128
|
-
) {
|
|
129
|
-
throw error;
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
return this;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
156
|
// Treat null value as empty string; will create an empty file.
|
|
136
157
|
if (value === null) {
|
|
137
158
|
value = "";
|
|
@@ -152,19 +173,12 @@ export default class BrowserFileTree {
|
|
|
152
173
|
|
|
153
174
|
if (isWriteable) {
|
|
154
175
|
// Write file.
|
|
155
|
-
const fileHandle = await directory.getFileHandle(
|
|
176
|
+
const fileHandle = await directory.getFileHandle(normalized, {
|
|
156
177
|
create: true,
|
|
157
178
|
});
|
|
158
179
|
const writable = await fileHandle.createWritable();
|
|
159
180
|
await writable.write(value);
|
|
160
181
|
await writable.close();
|
|
161
|
-
} else if (isTreelike(value)) {
|
|
162
|
-
// Treat value as a tree and write it out as a subdirectory.
|
|
163
|
-
const subdirectory = await directory.getDirectoryHandle(baseKey, {
|
|
164
|
-
create: true,
|
|
165
|
-
});
|
|
166
|
-
const destTree = Reflect.construct(this.constructor, [subdirectory]);
|
|
167
|
-
await assign(destTree, value);
|
|
168
182
|
} else {
|
|
169
183
|
const typeName = value?.constructor?.name ?? "unknown";
|
|
170
184
|
throw new TypeError(`Cannot write a value of type ${typeName} as ${key}`);
|
|
@@ -172,4 +186,6 @@ export default class BrowserFileTree {
|
|
|
172
186
|
|
|
173
187
|
return this;
|
|
174
188
|
}
|
|
189
|
+
|
|
190
|
+
trailingSlashKeys = true;
|
|
175
191
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as trailingSlash from "../trailingSlash.js";
|
|
2
|
+
import SyncMap from "./SyncMap.js";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Return a tree of years, months, and days from a start date to an end date.
|
|
@@ -14,41 +15,71 @@ import * as trailingSlash from "../trailingSlash.js";
|
|
|
14
15
|
*
|
|
15
16
|
* @typedef {string|undefined} CalendarOptionsDate
|
|
16
17
|
* @typedef {( year: string, month: string, day: string ) => any} CalendarOptionsFn
|
|
18
|
+
* @typedef {{ year: number, month: number, day: number}} CalendarDateParts}
|
|
19
|
+
*
|
|
17
20
|
* @param {{ end?: CalendarOptionsDate, start?: CalendarOptionsDate, value: CalendarOptionsFn }} options
|
|
18
21
|
*/
|
|
19
|
-
export default
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
start.
|
|
22
|
+
export default class CalendarMap extends SyncMap {
|
|
23
|
+
constructor(options = {}) {
|
|
24
|
+
super();
|
|
25
|
+
|
|
26
|
+
/** @type {CalendarDateParts} */
|
|
27
|
+
// @ts-ignore
|
|
28
|
+
const start = dateParts(options.start);
|
|
29
|
+
/** @type {CalendarDateParts} */
|
|
30
|
+
// @ts-ignore
|
|
31
|
+
const end = dateParts(options.end);
|
|
32
|
+
const valueFn = options.value;
|
|
33
|
+
|
|
34
|
+
// Fill in the missing parts of the start and end dates.
|
|
35
|
+
const today = new Date();
|
|
36
|
+
|
|
37
|
+
if (start.day === undefined) {
|
|
38
|
+
start.day = start.year ? 1 : today.getDate();
|
|
39
|
+
}
|
|
40
|
+
if (start.month === undefined) {
|
|
41
|
+
start.month = start.year ? 1 : today.getMonth() + 1;
|
|
42
|
+
}
|
|
43
|
+
if (start.year === undefined) {
|
|
44
|
+
start.year = today.getFullYear();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (end.day === undefined) {
|
|
48
|
+
end.day = end.month
|
|
49
|
+
? daysInMonth(end.year, end.month)
|
|
50
|
+
: end.year
|
|
51
|
+
? 31 // Last day of December
|
|
52
|
+
: today.getDate();
|
|
53
|
+
}
|
|
54
|
+
if (end.month === undefined) {
|
|
55
|
+
end.month = end.year ? 12 : today.getMonth() + 1;
|
|
56
|
+
}
|
|
57
|
+
if (end.year === undefined) {
|
|
58
|
+
end.year = today.getFullYear();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
this.start = start;
|
|
62
|
+
this.end = end;
|
|
63
|
+
this.valueFn = valueFn ?? defaultValueFn;
|
|
35
64
|
}
|
|
36
65
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
: today.getDate();
|
|
43
|
-
}
|
|
44
|
-
if (end.month === undefined) {
|
|
45
|
-
end.month = end.year ? 12 : today.getMonth() + 1;
|
|
66
|
+
get(year) {
|
|
67
|
+
year = parseInt(trailingSlash.remove(year));
|
|
68
|
+
return this.inRange(year)
|
|
69
|
+
? monthsForYearMap(year, this.start, this.end, this.valueFn)
|
|
70
|
+
: undefined;
|
|
46
71
|
}
|
|
47
|
-
|
|
48
|
-
|
|
72
|
+
|
|
73
|
+
inRange(year) {
|
|
74
|
+
return year >= this.start.year && year <= this.end.year;
|
|
49
75
|
}
|
|
50
76
|
|
|
51
|
-
|
|
77
|
+
keys() {
|
|
78
|
+
return Array.from(
|
|
79
|
+
{ length: this.end.year - this.start.year + 1 },
|
|
80
|
+
(_, i) => this.start.year + i
|
|
81
|
+
)[Symbol.iterator]();
|
|
82
|
+
}
|
|
52
83
|
}
|
|
53
84
|
|
|
54
85
|
function dateParts(date) {
|
|
@@ -64,9 +95,9 @@ function dateParts(date) {
|
|
|
64
95
|
return { year, month, day };
|
|
65
96
|
}
|
|
66
97
|
|
|
67
|
-
function
|
|
68
|
-
return {
|
|
69
|
-
|
|
98
|
+
function daysForMonthMap(year, month, start, end, valueFn) {
|
|
99
|
+
return Object.assign(new SyncMap(), {
|
|
100
|
+
get(day) {
|
|
70
101
|
day = parseInt(trailingSlash.remove(day));
|
|
71
102
|
return this.inRange(day)
|
|
72
103
|
? valueFn(year.toString(), twoDigits(month), twoDigits(day))
|
|
@@ -101,28 +132,34 @@ function daysForMonthTree(year, month, start, end, valueFn) {
|
|
|
101
132
|
}
|
|
102
133
|
},
|
|
103
134
|
|
|
104
|
-
|
|
135
|
+
*keys() {
|
|
105
136
|
const days = Array.from(
|
|
106
137
|
{ length: daysInMonth(year, month) },
|
|
107
138
|
(_, i) => i + 1
|
|
108
139
|
);
|
|
109
|
-
|
|
140
|
+
yield* days
|
|
110
141
|
.filter((day) => this.inRange(day))
|
|
111
142
|
.map((day) => twoDigits(day));
|
|
112
143
|
},
|
|
113
|
-
|
|
144
|
+
|
|
145
|
+
trailingSlashKeys: false,
|
|
146
|
+
});
|
|
114
147
|
}
|
|
115
148
|
|
|
116
149
|
function daysInMonth(year, month) {
|
|
117
150
|
return new Date(year, month, 0).getDate();
|
|
118
151
|
}
|
|
119
152
|
|
|
120
|
-
function
|
|
121
|
-
return {
|
|
122
|
-
|
|
153
|
+
function defaultValueFn(year, month, day) {
|
|
154
|
+
return `${year}-${month}-${day}`;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function monthsForYearMap(year, start, end, valueFn) {
|
|
158
|
+
return Object.assign(new SyncMap(), {
|
|
159
|
+
get(month) {
|
|
123
160
|
month = parseInt(trailingSlash.remove(month));
|
|
124
161
|
return this.inRange(month)
|
|
125
|
-
?
|
|
162
|
+
? daysForMonthMap(year, month, start, end, valueFn)
|
|
126
163
|
: undefined;
|
|
127
164
|
},
|
|
128
165
|
|
|
@@ -138,37 +175,17 @@ function monthsForYearTree(year, start, end, valueFn) {
|
|
|
138
175
|
}
|
|
139
176
|
},
|
|
140
177
|
|
|
141
|
-
|
|
178
|
+
*keys() {
|
|
142
179
|
const months = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
|
|
143
|
-
|
|
180
|
+
yield* months
|
|
144
181
|
.filter((month) => this.inRange(month))
|
|
145
182
|
.map((month) => twoDigits(month));
|
|
146
183
|
},
|
|
147
|
-
|
|
184
|
+
|
|
185
|
+
trailingSlashKeys: false,
|
|
186
|
+
});
|
|
148
187
|
}
|
|
149
188
|
|
|
150
189
|
function twoDigits(number) {
|
|
151
190
|
return number.toString().padStart(2, "0");
|
|
152
191
|
}
|
|
153
|
-
|
|
154
|
-
function yearsTree(start, end, valueFn) {
|
|
155
|
-
return {
|
|
156
|
-
async get(year) {
|
|
157
|
-
year = parseInt(trailingSlash.remove(year));
|
|
158
|
-
return this.inRange(year)
|
|
159
|
-
? monthsForYearTree(year, start, end, valueFn)
|
|
160
|
-
: undefined;
|
|
161
|
-
},
|
|
162
|
-
|
|
163
|
-
inRange(year) {
|
|
164
|
-
return year >= start.year && year <= end.year;
|
|
165
|
-
},
|
|
166
|
-
|
|
167
|
-
async keys() {
|
|
168
|
-
return Array.from(
|
|
169
|
-
{ length: end.year - start.year + 1 },
|
|
170
|
-
(_, i) => start.year + i
|
|
171
|
-
);
|
|
172
|
-
},
|
|
173
|
-
};
|
|
174
|
-
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import * as trailingSlash from "../trailingSlash.js";
|
|
2
|
+
import SyncMap from "./SyncMap.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A tree that returns a constant value for any key. If the key ends with a
|
|
6
|
+
* slash, then the same type of subtree is returned.
|
|
7
|
+
*
|
|
8
|
+
* @param {any} constant
|
|
9
|
+
* @returns {SyncMap}
|
|
10
|
+
*/
|
|
11
|
+
export default class ConstantTree extends SyncMap {
|
|
12
|
+
constructor(constant) {
|
|
13
|
+
super();
|
|
14
|
+
this.constant = constant;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
get(key) {
|
|
18
|
+
return trailingSlash.has(key)
|
|
19
|
+
? new ConstantTree(this.constant)
|
|
20
|
+
: this.constant;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
keys() {
|
|
24
|
+
return [][Symbol.iterator]();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
trailingSlashKeys = true;
|
|
28
|
+
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import
|
|
1
|
+
import SiteMap from "./SiteMap.js";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* A [
|
|
4
|
+
* A [SiteMap](SiteMap.html) that implements the [JSON Keys](jsonKeys.html)
|
|
5
5
|
* protocol. This enables a `keys()` method that can return the keys of a site
|
|
6
6
|
* route even though such a mechanism is not built into the HTTP protocol.
|
|
7
7
|
*/
|
|
8
|
-
export default class
|
|
8
|
+
export default class ExplorableSiteMap extends SiteMap {
|
|
9
9
|
/**
|
|
10
10
|
* @param {string} href
|
|
11
11
|
*/
|
|
@@ -24,12 +24,12 @@ export default class ExplorableSiteTree extends SiteTree {
|
|
|
24
24
|
.then((response) => (response.ok ? response.text() : null))
|
|
25
25
|
.then((text) => {
|
|
26
26
|
try {
|
|
27
|
-
return text ? JSON.parse(text) :
|
|
27
|
+
return text ? JSON.parse(text) : [];
|
|
28
28
|
} catch (error) {
|
|
29
29
|
// Got a response, but it's not JSON. Most likely the site doesn't
|
|
30
30
|
// actually have a .keys.json file, and is returning a Not Found page,
|
|
31
31
|
// but hasn't set the correct 404 status code.
|
|
32
|
-
return
|
|
32
|
+
return [];
|
|
33
33
|
}
|
|
34
34
|
});
|
|
35
35
|
return this.serverKeysPromise;
|
|
@@ -39,9 +39,9 @@ export default class ExplorableSiteTree extends SiteTree {
|
|
|
39
39
|
* Returns the keys of the site route. For this to work, the route must have a
|
|
40
40
|
* `.keys.json` file that contains a JSON array of string keys.
|
|
41
41
|
*/
|
|
42
|
-
async keys() {
|
|
42
|
+
async *keys() {
|
|
43
43
|
const serverKeys = await this.getServerKeys();
|
|
44
|
-
|
|
44
|
+
yield* serverKeys;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
processResponse(response) {
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { hiddenFileNames } from "../constants.js";
|
|
5
|
+
import * as trailingSlash from "../trailingSlash.js";
|
|
6
|
+
import isPacked from "../utilities/isPacked.js";
|
|
7
|
+
import isStringlike from "../utilities/isStringlike.js";
|
|
8
|
+
import naturalOrder from "../utilities/naturalOrder.js";
|
|
9
|
+
import setParent from "../utilities/setParent.js";
|
|
10
|
+
import SyncMap from "./SyncMap.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A file system folder as a Map.
|
|
14
|
+
*
|
|
15
|
+
* File values are returned as Uint8Array instances. The underlying Node fs API
|
|
16
|
+
* returns file contents as instances of the Node-specific Buffer class, but
|
|
17
|
+
* that class has some incompatible method implementations; see
|
|
18
|
+
* https://nodejs.org/api/buffer.html#buffers-and-typedarrays. For greater
|
|
19
|
+
* compatibility, files are returned as standard Uint8Array instances instead.
|
|
20
|
+
*/
|
|
21
|
+
export default class FileMap extends SyncMap {
|
|
22
|
+
constructor(location) {
|
|
23
|
+
if (location instanceof URL) {
|
|
24
|
+
location = location.href;
|
|
25
|
+
} else if (
|
|
26
|
+
!(
|
|
27
|
+
typeof location === "string" ||
|
|
28
|
+
/** @type {any} */ (location) instanceof String
|
|
29
|
+
)
|
|
30
|
+
) {
|
|
31
|
+
throw new TypeError("FileMap constructor needs a string or URL");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
super();
|
|
35
|
+
this.dirname = location.startsWith("file://")
|
|
36
|
+
? fileURLToPath(location)
|
|
37
|
+
: path.resolve(process.cwd(), location);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Return the (possibly new) subdirectory with the given key.
|
|
41
|
+
child(key) {
|
|
42
|
+
const stringKey = key != null ? String(key) : "";
|
|
43
|
+
const baseKey = trailingSlash.remove(stringKey);
|
|
44
|
+
const destPath = path.resolve(this.dirname, baseKey);
|
|
45
|
+
const destTree = Reflect.construct(this.constructor, [destPath]);
|
|
46
|
+
|
|
47
|
+
const stats = getStats(destPath);
|
|
48
|
+
if (stats === null || !stats.isDirectory()) {
|
|
49
|
+
if (stats !== null) {
|
|
50
|
+
// File with the same name exists; delete it.
|
|
51
|
+
fs.rmSync(destPath);
|
|
52
|
+
}
|
|
53
|
+
// Ensure the directory exists.
|
|
54
|
+
fs.mkdirSync(destPath, { recursive: true });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return destTree;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
delete(key) {
|
|
61
|
+
if (key === "" || key == null) {
|
|
62
|
+
// Can't have a file with no name or a nullish name
|
|
63
|
+
throw new Error("delete: key was empty or nullish");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// What file or directory are we going to delete?
|
|
67
|
+
const stringKey = key != null ? String(key) : "";
|
|
68
|
+
const baseKey = trailingSlash.remove(stringKey);
|
|
69
|
+
const destPath = path.resolve(this.dirname, baseKey);
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
fs.rmSync(destPath, { recursive: true });
|
|
73
|
+
return true;
|
|
74
|
+
} catch (/** @type {any} */ error) {
|
|
75
|
+
if (error.code === "ENOENT") {
|
|
76
|
+
return false; // File or directory didn't exist
|
|
77
|
+
}
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
get(key) {
|
|
83
|
+
if (key == null) {
|
|
84
|
+
// Reject nullish key
|
|
85
|
+
throw new ReferenceError(
|
|
86
|
+
`${this.constructor.name}: Cannot get a null or undefined key.`
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
if (key === "") {
|
|
90
|
+
// Can't have a file with no name
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
key = trailingSlash.remove(key); // normalize key
|
|
95
|
+
const filePath = path.resolve(this.dirname, key);
|
|
96
|
+
|
|
97
|
+
const stats = getStats(filePath);
|
|
98
|
+
if (stats === null) {
|
|
99
|
+
return undefined; // File or directory doesn't exist
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
let value;
|
|
103
|
+
if (stats.isDirectory()) {
|
|
104
|
+
// Return subdirectory as an instance of this class
|
|
105
|
+
value = Reflect.construct(this.constructor, [filePath]);
|
|
106
|
+
} else {
|
|
107
|
+
// Return file contents as a standard Uint8Array
|
|
108
|
+
const buffer = fs.readFileSync(filePath);
|
|
109
|
+
value = Uint8Array.from(buffer);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
value.parent =
|
|
113
|
+
key === ".."
|
|
114
|
+
? // Special case: ".." parent is the grandparent (if it exists)
|
|
115
|
+
this.parent?.parent
|
|
116
|
+
: this;
|
|
117
|
+
setParent(value, this);
|
|
118
|
+
|
|
119
|
+
return value;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
keys() {
|
|
123
|
+
let dirEntries;
|
|
124
|
+
try {
|
|
125
|
+
dirEntries = fs.readdirSync(this.dirname, { withFileTypes: true });
|
|
126
|
+
} catch (/** @type {any} */ error) {
|
|
127
|
+
if (error.code !== "ENOENT") {
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
// Directory doesn't exist yet; treat as empty
|
|
131
|
+
dirEntries = [];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Add slashes to directory names.
|
|
135
|
+
let names = dirEntries.map((dirEntry) =>
|
|
136
|
+
trailingSlash.toggle(dirEntry.name, dirEntry.isDirectory())
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
// Filter out unhelpful file names.
|
|
140
|
+
names = names.filter((name) => !hiddenFileNames.includes(name));
|
|
141
|
+
|
|
142
|
+
// Node fs.readdir sort order appears to be unreliable; see, e.g.,
|
|
143
|
+
// https://github.com/nodejs/node/issues/3232.
|
|
144
|
+
names.sort(naturalOrder);
|
|
145
|
+
|
|
146
|
+
return names[Symbol.iterator]();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
get path() {
|
|
150
|
+
return this.dirname;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
set(key, value) {
|
|
154
|
+
// Where are we going to write this value?
|
|
155
|
+
const stringKey = key != null ? String(key) : "";
|
|
156
|
+
const normalized = trailingSlash.remove(stringKey);
|
|
157
|
+
const destPath = path.resolve(this.dirname, normalized);
|
|
158
|
+
|
|
159
|
+
// Ensure this directory exists.
|
|
160
|
+
const dirname = path.dirname(destPath);
|
|
161
|
+
fs.mkdirSync(dirname, { recursive: true });
|
|
162
|
+
|
|
163
|
+
if (value === undefined) {
|
|
164
|
+
// Special case: undefined value is equivalent to delete()
|
|
165
|
+
this.delete(normalized);
|
|
166
|
+
return this;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (typeof value === "function") {
|
|
170
|
+
// Invoke function; write out the result.
|
|
171
|
+
value = value();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
let packed = false;
|
|
175
|
+
if (value === null) {
|
|
176
|
+
// Treat null value as empty string; will create an empty file.
|
|
177
|
+
value = "";
|
|
178
|
+
packed = true;
|
|
179
|
+
} else if (value instanceof ArrayBuffer) {
|
|
180
|
+
// Convert ArrayBuffer to Uint8Array, which Node.js can write directly.
|
|
181
|
+
value = new Uint8Array(value);
|
|
182
|
+
packed = true;
|
|
183
|
+
} else if (!(value instanceof String) && isPacked(value)) {
|
|
184
|
+
// As of Node 22, fs.writeFile is incredibly slow for large String
|
|
185
|
+
// instances. Instead of treating a String instance as a Packed value, we
|
|
186
|
+
// want to consider it as a stringlike below. That will convert it to a
|
|
187
|
+
// primitive string before writing — which is orders of magnitude faster.
|
|
188
|
+
packed = true;
|
|
189
|
+
} else if (typeof value.pack === "function") {
|
|
190
|
+
// Pack the value for writing.
|
|
191
|
+
value = value.pack();
|
|
192
|
+
packed = true;
|
|
193
|
+
} else if (isStringlike(value)) {
|
|
194
|
+
// Value has a meaningful `toString` method, use that.
|
|
195
|
+
value = String(value);
|
|
196
|
+
packed = true;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (packed) {
|
|
200
|
+
writeFile(value, destPath);
|
|
201
|
+
} else {
|
|
202
|
+
const typeName = value?.constructor?.name ?? "unknown";
|
|
203
|
+
throw new TypeError(
|
|
204
|
+
`Cannot write a value of type ${typeName} as ${stringKey}`
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return this;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
trailingSlashKeys = true;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Return stats for the path, or null if it doesn't exist.
|
|
215
|
+
function getStats(filePath) {
|
|
216
|
+
let stats;
|
|
217
|
+
try {
|
|
218
|
+
stats = fs.statSync(filePath);
|
|
219
|
+
} catch (/** @type {any} */ error) {
|
|
220
|
+
if (error.code === "ENOENT" /* File not found */) {
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
throw error;
|
|
224
|
+
}
|
|
225
|
+
return stats;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Write a value to a file.
|
|
229
|
+
function writeFile(value, destPath) {
|
|
230
|
+
// If path exists and it's a directory, delete the directory first.
|
|
231
|
+
const stats = getStats(destPath);
|
|
232
|
+
if (stats !== null && stats.isDirectory()) {
|
|
233
|
+
fs.rmSync(destPath, { recursive: true });
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Write out the value as the contents of a file.
|
|
237
|
+
fs.writeFileSync(destPath, value);
|
|
238
|
+
}
|