@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.
- package/browser.js +1 -1
- package/index.ts +31 -35
- package/main.js +1 -2
- package/package.json +4 -7
- package/shared.js +77 -12
- package/src/Tree.js +8 -2
- package/src/drivers/AsyncMap.js +210 -0
- package/src/drivers/{BrowserFileTree.js → BrowserFileMap.js} +36 -27
- package/src/drivers/{calendarTree.js → CalendarMap.js} +81 -62
- package/src/drivers/ConstantMap.js +30 -0
- package/src/drivers/DeepObjectMap.js +27 -0
- package/src/drivers/{ExplorableSiteTree.js → ExplorableSiteMap.js} +7 -7
- package/src/drivers/FileMap.js +245 -0
- package/src/drivers/{FunctionTree.js → FunctionMap.js} +19 -22
- package/src/drivers/ObjectMap.js +139 -0
- package/src/drivers/SetMap.js +13 -0
- package/src/drivers/{SiteTree.js → SiteMap.js} +16 -17
- package/src/drivers/SyncMap.js +245 -0
- package/src/jsonKeys.d.ts +2 -2
- package/src/jsonKeys.js +6 -5
- package/src/operations/addNextPrevious.js +35 -36
- package/src/operations/assign.js +30 -21
- package/src/operations/cache.js +29 -35
- package/src/operations/cachedKeyFunctions.js +1 -1
- package/src/operations/calendar.js +5 -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 +31 -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 +27 -30
- package/src/operations/mapExtension.js +20 -16
- package/src/operations/mapReduce.js +22 -17
- package/src/operations/mask.js +31 -22
- 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 +8 -8
- package/src/operations/plain.js +6 -6
- 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/shuffle.js +23 -16
- package/src/operations/size.js +13 -0
- package/src/operations/sort.js +55 -40
- package/src/operations/sync.js +21 -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 +13 -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 +10 -2
- 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 +22 -18
- package/test/SampleAsyncMap.js +34 -0
- package/test/browser/assert.js +20 -0
- package/test/browser/index.html +54 -21
- package/test/drivers/AsyncMap.test.js +119 -0
- package/test/drivers/{BrowserFileTree.test.js → BrowserFileMap.test.js} +42 -23
- package/test/drivers/{calendarTree.test.js → CalendarMap.test.js} +17 -19
- package/test/drivers/ConstantMap.test.js +15 -0
- package/test/drivers/DeepObjectMap.test.js +36 -0
- package/test/drivers/{ExplorableSiteTree.test.js → ExplorableSiteMap.test.js} +29 -14
- package/test/drivers/FileMap.test.js +185 -0
- package/test/drivers/FunctionMap.test.js +56 -0
- package/test/drivers/ObjectMap.test.js +166 -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 +321 -0
- package/test/jsonKeys.test.js +2 -2
- package/test/operations/addNextPrevious.test.js +3 -2
- package/test/operations/assign.test.js +30 -35
- package/test/operations/cache.test.js +8 -6
- package/test/operations/cachedKeyFunctions.test.js +6 -5
- package/test/operations/clear.test.js +6 -27
- package/test/operations/deepEntries.test.js +32 -0
- package/test/operations/deepMerge.test.js +6 -5
- 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 +23 -31
- package/test/operations/globKeys.test.js +9 -9
- package/test/operations/groupBy.test.js +6 -5
- package/test/operations/inners.test.js +4 -4
- 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 +20 -18
- package/test/operations/mapExtension.test.js +6 -6
- package/test/operations/mapReduce.test.js +2 -2
- package/test/operations/mask.test.js +4 -3
- package/test/operations/match.test.js +2 -2
- package/test/operations/merge.test.js +15 -11
- package/test/operations/paginate.test.js +5 -5
- package/test/operations/parent.test.js +3 -3
- package/test/operations/paths.test.js +6 -6
- package/test/operations/plain.test.js +8 -8
- package/test/operations/regExpKeys.test.js +12 -11
- package/test/operations/reverse.test.js +4 -3
- package/test/operations/scope.test.js +6 -5
- 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 +4 -5
- package/test/operations/withKeys.test.js +2 -2
- 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,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,73 @@ 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();
|
|
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;
|
|
43
71
|
}
|
|
44
|
-
|
|
45
|
-
|
|
72
|
+
|
|
73
|
+
inRange(year) {
|
|
74
|
+
return year >= this.start.year && year <= this.end.year;
|
|
46
75
|
}
|
|
47
|
-
|
|
48
|
-
|
|
76
|
+
|
|
77
|
+
keys() {
|
|
78
|
+
return Array.from(
|
|
79
|
+
{ length: this.end.year - this.start.year + 1 },
|
|
80
|
+
(_, i) => this.start.year + i
|
|
81
|
+
)[Symbol.iterator]();
|
|
49
82
|
}
|
|
50
83
|
|
|
51
|
-
|
|
84
|
+
trailingSlashKeys = false;
|
|
52
85
|
}
|
|
53
86
|
|
|
54
87
|
function dateParts(date) {
|
|
@@ -64,9 +97,9 @@ function dateParts(date) {
|
|
|
64
97
|
return { year, month, day };
|
|
65
98
|
}
|
|
66
99
|
|
|
67
|
-
function
|
|
68
|
-
return {
|
|
69
|
-
|
|
100
|
+
function daysForMonthMap(year, month, start, end, valueFn) {
|
|
101
|
+
return Object.assign(new SyncMap(), {
|
|
102
|
+
get(day) {
|
|
70
103
|
day = parseInt(trailingSlash.remove(day));
|
|
71
104
|
return this.inRange(day)
|
|
72
105
|
? valueFn(year.toString(), twoDigits(month), twoDigits(day))
|
|
@@ -101,28 +134,34 @@ function daysForMonthTree(year, month, start, end, valueFn) {
|
|
|
101
134
|
}
|
|
102
135
|
},
|
|
103
136
|
|
|
104
|
-
|
|
137
|
+
*keys() {
|
|
105
138
|
const days = Array.from(
|
|
106
139
|
{ length: daysInMonth(year, month) },
|
|
107
140
|
(_, i) => i + 1
|
|
108
141
|
);
|
|
109
|
-
|
|
142
|
+
yield* days
|
|
110
143
|
.filter((day) => this.inRange(day))
|
|
111
144
|
.map((day) => twoDigits(day));
|
|
112
145
|
},
|
|
113
|
-
|
|
146
|
+
|
|
147
|
+
trailingSlashKeys: false,
|
|
148
|
+
});
|
|
114
149
|
}
|
|
115
150
|
|
|
116
151
|
function daysInMonth(year, month) {
|
|
117
152
|
return new Date(year, month, 0).getDate();
|
|
118
153
|
}
|
|
119
154
|
|
|
120
|
-
function
|
|
121
|
-
return {
|
|
122
|
-
|
|
155
|
+
function defaultValueFn(year, month, day) {
|
|
156
|
+
return `${year}-${month}-${day}`;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function monthsForYearMap(year, start, end, valueFn) {
|
|
160
|
+
return Object.assign(new SyncMap(), {
|
|
161
|
+
get(month) {
|
|
123
162
|
month = parseInt(trailingSlash.remove(month));
|
|
124
163
|
return this.inRange(month)
|
|
125
|
-
?
|
|
164
|
+
? daysForMonthMap(year, month, start, end, valueFn)
|
|
126
165
|
: undefined;
|
|
127
166
|
},
|
|
128
167
|
|
|
@@ -138,37 +177,17 @@ function monthsForYearTree(year, start, end, valueFn) {
|
|
|
138
177
|
}
|
|
139
178
|
},
|
|
140
179
|
|
|
141
|
-
|
|
180
|
+
*keys() {
|
|
142
181
|
const months = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
|
|
143
|
-
|
|
182
|
+
yield* months
|
|
144
183
|
.filter((month) => this.inRange(month))
|
|
145
184
|
.map((month) => twoDigits(month));
|
|
146
185
|
},
|
|
147
|
-
|
|
186
|
+
|
|
187
|
+
trailingSlashKeys: false,
|
|
188
|
+
});
|
|
148
189
|
}
|
|
149
190
|
|
|
150
191
|
function twoDigits(number) {
|
|
151
192
|
return number.toString().padStart(2, "0");
|
|
152
193
|
}
|
|
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,30 @@
|
|
|
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
|
+
get trailingSlashKeys() {
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import isMap from "../operations/isMap.js";
|
|
2
|
+
import isPlainObject from "../utilities/isPlainObject.js";
|
|
3
|
+
import ObjectMap from "./ObjectMap.js";
|
|
4
|
+
|
|
5
|
+
export default class DeepObjectMap extends ObjectMap {
|
|
6
|
+
// Implement delete (and set) to keep the Map read-write
|
|
7
|
+
delete(key) {
|
|
8
|
+
return super.delete(key);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
get(key) {
|
|
12
|
+
let value = super.get(key);
|
|
13
|
+
if (value instanceof Array || isPlainObject(value)) {
|
|
14
|
+
value = Reflect.construct(this.constructor, [value]);
|
|
15
|
+
}
|
|
16
|
+
return value;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
isSubtree(value) {
|
|
20
|
+
return value instanceof Array || isPlainObject(value) || isMap(value);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// See delete()
|
|
24
|
+
set(key, value) {
|
|
25
|
+
return super.set(key, value);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -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,245 @@
|
|
|
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 from from "../operations/from.js";
|
|
6
|
+
import isMaplike from "../operations/isMaplike.js";
|
|
7
|
+
import * as trailingSlash from "../trailingSlash.js";
|
|
8
|
+
import isPacked from "../utilities/isPacked.js";
|
|
9
|
+
import isStringlike from "../utilities/isStringlike.js";
|
|
10
|
+
import naturalOrder from "../utilities/naturalOrder.js";
|
|
11
|
+
import setParent from "../utilities/setParent.js";
|
|
12
|
+
import SyncMap from "./SyncMap.js";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A file system folder as a Map.
|
|
16
|
+
*
|
|
17
|
+
* File values are returned as Uint8Array instances. The underlying Node fs API
|
|
18
|
+
* returns file contents as instances of the Node-specific Buffer class, but
|
|
19
|
+
* that class has some incompatible method implementations; see
|
|
20
|
+
* https://nodejs.org/api/buffer.html#buffers-and-typedarrays. For greater
|
|
21
|
+
* compatibility, files are returned as standard Uint8Array instances instead.
|
|
22
|
+
*/
|
|
23
|
+
export default class FileMap extends SyncMap {
|
|
24
|
+
constructor(location) {
|
|
25
|
+
if (location instanceof URL) {
|
|
26
|
+
location = location.href;
|
|
27
|
+
} else if (
|
|
28
|
+
!(
|
|
29
|
+
typeof location === "string" ||
|
|
30
|
+
/** @type {any} */ (location) instanceof String
|
|
31
|
+
)
|
|
32
|
+
) {
|
|
33
|
+
throw new TypeError("FileMap constructor needs a string or URL");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
super();
|
|
37
|
+
this.dirname = location.startsWith("file://")
|
|
38
|
+
? fileURLToPath(location)
|
|
39
|
+
: path.resolve(process.cwd(), location);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
delete(key) {
|
|
43
|
+
if (key === "" || key == null) {
|
|
44
|
+
// Can't have a file with no name or a nullish name
|
|
45
|
+
throw new Error("delete: key was empty or nullish");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// What file or directory are we going to delete?
|
|
49
|
+
const stringKey = key != null ? String(key) : "";
|
|
50
|
+
const baseKey = trailingSlash.remove(stringKey);
|
|
51
|
+
const destPath = path.resolve(this.dirname, baseKey);
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
fs.rmSync(destPath, { recursive: true });
|
|
55
|
+
return true;
|
|
56
|
+
} catch (/** @type {any} */ error) {
|
|
57
|
+
if (error.code === "ENOENT") {
|
|
58
|
+
return false; // File or directory didn't exist
|
|
59
|
+
}
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
get(key) {
|
|
65
|
+
if (key == null) {
|
|
66
|
+
// Reject nullish key
|
|
67
|
+
throw new ReferenceError(
|
|
68
|
+
`${this.constructor.name}: Cannot get a null or undefined key.`
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
if (key === "") {
|
|
72
|
+
// Can't have a file with no name
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
key = trailingSlash.remove(key); // normalize key
|
|
77
|
+
const filePath = path.resolve(this.dirname, key);
|
|
78
|
+
|
|
79
|
+
let stats;
|
|
80
|
+
try {
|
|
81
|
+
stats = fs.statSync(filePath);
|
|
82
|
+
} catch (/** @type {any} */ error) {
|
|
83
|
+
if (error.code === "ENOENT" /* File not found */) {
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
throw error;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
let value;
|
|
90
|
+
if (stats.isDirectory()) {
|
|
91
|
+
// Return subdirectory as an instance of this class
|
|
92
|
+
value = Reflect.construct(this.constructor, [filePath]);
|
|
93
|
+
} else {
|
|
94
|
+
// Return file contents as a standard Uint8Array
|
|
95
|
+
const buffer = fs.readFileSync(filePath);
|
|
96
|
+
value = Uint8Array.from(buffer);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
value.parent =
|
|
100
|
+
key === ".."
|
|
101
|
+
? // Special case: ".." parent is the grandparent (if it exists)
|
|
102
|
+
this.parent?.parent
|
|
103
|
+
: this;
|
|
104
|
+
setParent(value, this);
|
|
105
|
+
|
|
106
|
+
return value;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
keys() {
|
|
110
|
+
let dirEntries;
|
|
111
|
+
try {
|
|
112
|
+
dirEntries = fs.readdirSync(this.dirname, { withFileTypes: true });
|
|
113
|
+
} catch (/** @type {any} */ error) {
|
|
114
|
+
if (error.code !== "ENOENT") {
|
|
115
|
+
throw error;
|
|
116
|
+
}
|
|
117
|
+
// Directory doesn't exist yet; treat as empty
|
|
118
|
+
dirEntries = [];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Add slashes to directory names.
|
|
122
|
+
let names = dirEntries.map((dirEntry) =>
|
|
123
|
+
trailingSlash.toggle(dirEntry.name, dirEntry.isDirectory())
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
// Filter out unhelpful file names.
|
|
127
|
+
names = names.filter((name) => !hiddenFileNames.includes(name));
|
|
128
|
+
|
|
129
|
+
// Node fs.readdir sort order appears to be unreliable; see, e.g.,
|
|
130
|
+
// https://github.com/nodejs/node/issues/3232.
|
|
131
|
+
names.sort(naturalOrder);
|
|
132
|
+
|
|
133
|
+
return names[Symbol.iterator]();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
get path() {
|
|
137
|
+
return this.dirname;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
set(key, value) {
|
|
141
|
+
// Where are we going to write this value?
|
|
142
|
+
const stringKey = key != null ? String(key) : "";
|
|
143
|
+
const baseKey = trailingSlash.remove(stringKey);
|
|
144
|
+
const destPath = path.resolve(this.dirname, baseKey);
|
|
145
|
+
|
|
146
|
+
// Ensure this directory exists.
|
|
147
|
+
const dirname = path.dirname(destPath);
|
|
148
|
+
fs.mkdirSync(dirname, { recursive: true });
|
|
149
|
+
|
|
150
|
+
if (typeof value === "function") {
|
|
151
|
+
// Invoke function; write out the result.
|
|
152
|
+
value = value();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
let packed = false;
|
|
156
|
+
if (value === null) {
|
|
157
|
+
// Treat null value as empty string; will create an empty file.
|
|
158
|
+
value = "";
|
|
159
|
+
packed = true;
|
|
160
|
+
} else if (value instanceof ArrayBuffer) {
|
|
161
|
+
// Convert ArrayBuffer to Uint8Array, which Node.js can write directly.
|
|
162
|
+
value = new Uint8Array(value);
|
|
163
|
+
packed = true;
|
|
164
|
+
} else if (!(value instanceof String) && isPacked(value)) {
|
|
165
|
+
// As of Node 22, fs.writeFile is incredibly slow for large String
|
|
166
|
+
// instances. Instead of treating a String instance as a Packed value, we
|
|
167
|
+
// want to consider it as a stringlike below. That will convert it to a
|
|
168
|
+
// primitive string before writing — which is orders of magnitude faster.
|
|
169
|
+
packed = true;
|
|
170
|
+
} else if (typeof value.pack === "function") {
|
|
171
|
+
// Pack the value for writing.
|
|
172
|
+
value = value.pack();
|
|
173
|
+
packed = true;
|
|
174
|
+
} else if (isStringlike(value)) {
|
|
175
|
+
// Value has a meaningful `toString` method, use that.
|
|
176
|
+
value = String(value);
|
|
177
|
+
packed = true;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (packed) {
|
|
181
|
+
writeFile(value, destPath);
|
|
182
|
+
} else if (value === /** @type {any} */ (this.constructor).EMPTY) {
|
|
183
|
+
clearDirectory(destPath, this);
|
|
184
|
+
} else if (isMaplike(value)) {
|
|
185
|
+
writeDirectory(value, destPath, this);
|
|
186
|
+
} else {
|
|
187
|
+
const typeName = value?.constructor?.name ?? "unknown";
|
|
188
|
+
throw new TypeError(
|
|
189
|
+
`Cannot write a value of type ${typeName} as ${stringKey}`
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return this;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
get trailingSlashKeys() {
|
|
197
|
+
return true;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Create the indicated directory.
|
|
202
|
+
function clearDirectory(destPath, parent) {
|
|
203
|
+
const destTree = Reflect.construct(parent.constructor, [destPath]);
|
|
204
|
+
|
|
205
|
+
// Ensure the directory exists.
|
|
206
|
+
fs.mkdirSync(destPath, { recursive: true });
|
|
207
|
+
|
|
208
|
+
// Clear any existing files
|
|
209
|
+
destTree.clear();
|
|
210
|
+
|
|
211
|
+
return destTree;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Treat value as a subtree and write it out as a subdirectory.
|
|
216
|
+
*
|
|
217
|
+
* @param {import("../../index.ts").Maplike} value
|
|
218
|
+
*/
|
|
219
|
+
function writeDirectory(value, destPath, parent) {
|
|
220
|
+
// Since value is Maplike, result will be a Map
|
|
221
|
+
/** @type {Map} */
|
|
222
|
+
// @ts-ignore
|
|
223
|
+
const valueMap = from(value);
|
|
224
|
+
const destTree = clearDirectory(destPath, parent);
|
|
225
|
+
|
|
226
|
+
// Write out the subtree.
|
|
227
|
+
for (const key of valueMap.keys()) {
|
|
228
|
+
const childValue = valueMap.get(key);
|
|
229
|
+
destTree.set(key, childValue);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Write a value to a file.
|
|
234
|
+
function writeFile(value, destPath) {
|
|
235
|
+
// Write out the value as the contents of a file.
|
|
236
|
+
try {
|
|
237
|
+
fs.writeFileSync(destPath, value);
|
|
238
|
+
} catch (/** @type {any} */ error) {
|
|
239
|
+
if (error.code === "EISDIR" /* Is a directory */) {
|
|
240
|
+
throw new Error(
|
|
241
|
+
`Tried to overwrite a directory with a single file: ${destPath}`
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
@@ -1,20 +1,14 @@
|
|
|
1
1
|
import setParent from "../utilities/setParent.js";
|
|
2
|
+
import SyncMap from "./SyncMap.js";
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
* A tree defined by a function and an optional domain.
|
|
5
|
-
*
|
|
6
|
-
* @typedef {import("@weborigami/types").AsyncTree} AsyncTree
|
|
7
|
-
* @implements {AsyncTree}
|
|
8
|
-
*/
|
|
9
|
-
export default class FunctionTree {
|
|
10
|
-
/**
|
|
11
|
-
* @param {function} fn the key->value function
|
|
12
|
-
* @param {Iterable<any>} [domain] optional domain of the function
|
|
13
|
-
*/
|
|
4
|
+
export default class FunctionMap extends SyncMap {
|
|
14
5
|
constructor(fn, domain = []) {
|
|
6
|
+
if (typeof fn !== "function") {
|
|
7
|
+
throw new TypeError("FunctionMap: first argument must be a function");
|
|
8
|
+
}
|
|
9
|
+
super();
|
|
15
10
|
this.fn = fn;
|
|
16
11
|
this.domain = domain;
|
|
17
|
-
this.parent = null;
|
|
18
12
|
}
|
|
19
13
|
|
|
20
14
|
/**
|
|
@@ -22,25 +16,28 @@ export default class FunctionTree {
|
|
|
22
16
|
*
|
|
23
17
|
* @param {any} key
|
|
24
18
|
*/
|
|
25
|
-
|
|
26
|
-
|
|
19
|
+
get(key) {
|
|
20
|
+
let value =
|
|
27
21
|
this.fn.length <= 1
|
|
28
22
|
? // Function takes no arguments, one argument, or a variable number of
|
|
29
23
|
// arguments: invoke it.
|
|
30
|
-
|
|
24
|
+
this.fn(key)
|
|
31
25
|
: // Bind the key to the first parameter. Subsequent get calls will
|
|
32
26
|
// eventually bind all parameters until only one remains. At that point,
|
|
33
27
|
// the above condition will apply and the function will be invoked.
|
|
34
28
|
Reflect.construct(this.constructor, [this.fn.bind(null, key)]);
|
|
35
|
-
|
|
29
|
+
if (value instanceof Promise) {
|
|
30
|
+
value = value.then((v) => {
|
|
31
|
+
setParent(v, this);
|
|
32
|
+
return v;
|
|
33
|
+
});
|
|
34
|
+
} else {
|
|
35
|
+
setParent(value, this);
|
|
36
|
+
}
|
|
36
37
|
return value;
|
|
37
38
|
}
|
|
38
39
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
* was defined, this returns an empty iterator.
|
|
42
|
-
*/
|
|
43
|
-
async keys() {
|
|
44
|
-
return this.domain;
|
|
40
|
+
keys() {
|
|
41
|
+
return this.domain[Symbol.iterator]();
|
|
45
42
|
}
|
|
46
43
|
}
|