@weborigami/async-tree 0.6.0 → 0.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.ts +13 -1
- package/package.json +1 -1
- package/shared.js +13 -16
- package/src/Tree.js +3 -0
- package/src/drivers/AsyncMap.js +30 -3
- package/src/drivers/BrowserFileMap.js +30 -23
- package/src/drivers/CalendarMap.js +0 -2
- package/src/drivers/ConstantMap.js +1 -3
- package/src/drivers/ExplorableSiteMap.js +2 -0
- package/src/drivers/FileMap.js +50 -57
- package/src/drivers/ObjectMap.js +22 -10
- package/src/drivers/SiteMap.js +4 -6
- package/src/drivers/SyncMap.js +22 -7
- package/src/jsonKeys.js +15 -1
- package/src/operations/assign.js +7 -12
- package/src/operations/cache.js +2 -2
- package/src/operations/child.js +35 -0
- package/src/operations/from.js +5 -6
- package/src/operations/globKeys.js +2 -3
- package/src/operations/isMaplike.js +1 -1
- package/src/operations/map.js +22 -3
- package/src/operations/mapReduce.js +28 -19
- package/src/operations/mask.js +34 -18
- package/src/operations/paths.js +14 -16
- package/src/operations/reduce.js +16 -0
- package/src/operations/root.js +2 -2
- package/src/operations/set.js +20 -0
- package/src/operations/sync.js +2 -9
- package/src/operations/traverseOrThrow.js +5 -0
- package/src/utilities/castArraylike.js +23 -20
- package/src/utilities/toPlainValue.js +6 -8
- package/test/browser/index.html +0 -1
- package/test/drivers/BrowserFileMap.test.js +21 -23
- package/test/drivers/FileMap.test.js +2 -31
- package/test/drivers/ObjectMap.test.js +28 -0
- package/test/drivers/SyncMap.test.js +19 -5
- package/test/jsonKeys.test.js +18 -6
- package/test/operations/cache.test.js +11 -8
- package/test/operations/cachedKeyFunctions.test.js +8 -6
- package/test/operations/child.test.js +34 -0
- package/test/operations/deepMerge.test.js +20 -14
- package/test/operations/from.test.js +6 -4
- package/test/operations/inners.test.js +15 -12
- package/test/operations/map.test.js +24 -16
- package/test/operations/mapReduce.test.js +14 -12
- package/test/operations/mask.test.js +12 -0
- package/test/operations/merge.test.js +7 -5
- package/test/operations/paths.test.js +20 -27
- package/test/operations/regExpKeys.test.js +12 -9
- package/test/operations/root.test.js +23 -0
- package/test/operations/set.test.js +11 -0
- package/test/operations/traverse.test.js +13 -0
- package/test/utilities/castArrayLike.test.js +53 -0
- package/src/drivers/DeepObjectMap.js +0 -27
- package/test/drivers/DeepObjectMap.test.js +0 -36
package/index.ts
CHANGED
|
@@ -43,6 +43,18 @@ export type MapOptions = {
|
|
|
43
43
|
value?: ValueKeyFn;
|
|
44
44
|
};
|
|
45
45
|
|
|
46
|
+
export interface SyncTree<MapType> {
|
|
47
|
+
child(key: any): MapType;
|
|
48
|
+
parent: MapType | null;
|
|
49
|
+
trailingSlashKeys: boolean;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface AsyncTree<MapType> {
|
|
53
|
+
child(key: any): Promise<MapType>;
|
|
54
|
+
parent: MapType | null;
|
|
55
|
+
trailingSlashKeys: boolean;
|
|
56
|
+
}
|
|
57
|
+
|
|
46
58
|
/**
|
|
47
59
|
* A packed value is one that can be written to a file via fs.writeFile or into
|
|
48
60
|
* an HTTP response via response.write, or readily converted to such a form.
|
|
@@ -56,7 +68,7 @@ export type PlainObject = {
|
|
|
56
68
|
[key: string]: any;
|
|
57
69
|
};
|
|
58
70
|
|
|
59
|
-
export type ReduceFn = (
|
|
71
|
+
export type ReduceFn = (mapped: Map<any, any>, source: SyncOrAsyncMap) => any | Promise<any>;
|
|
60
72
|
|
|
61
73
|
export type Stringlike = string | HasString;
|
|
62
74
|
|
package/package.json
CHANGED
package/shared.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
// Exports for both Node.js and browser
|
|
2
2
|
|
|
3
|
-
import { default as DeepObjectMap } from "./src/drivers/DeepObjectMap.js";
|
|
4
3
|
import { default as ExplorableSiteMap } from "./src/drivers/ExplorableSiteMap.js";
|
|
5
4
|
import { default as FileMap } from "./src/drivers/FileMap.js";
|
|
6
5
|
import { default as FunctionMap } from "./src/drivers/FunctionMap.js";
|
|
@@ -14,6 +13,7 @@ export { default as ConstantMap } from "./src/drivers/ConstantMap.js";
|
|
|
14
13
|
export { default as SyncMap } from "./src/drivers/SyncMap.js";
|
|
15
14
|
export * as extension from "./src/extension.js";
|
|
16
15
|
export * as jsonKeys from "./src/jsonKeys.js";
|
|
16
|
+
export { default as reduce } from "./src/operations/reduce.js";
|
|
17
17
|
export { default as scope } from "./src/operations/scope.js";
|
|
18
18
|
export * as symbols from "./src/symbols.js";
|
|
19
19
|
export * as trailingSlash from "./src/trailingSlash.js";
|
|
@@ -35,15 +35,14 @@ export { default as setParent } from "./src/utilities/setParent.js";
|
|
|
35
35
|
export { default as toPlainValue } from "./src/utilities/toPlainValue.js";
|
|
36
36
|
export { default as toString } from "./src/utilities/toString.js";
|
|
37
37
|
|
|
38
|
-
export {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
};
|
|
38
|
+
export { ExplorableSiteMap, FileMap, FunctionMap, ObjectMap, SetMap, SiteMap };
|
|
39
|
+
|
|
40
|
+
export class DeepObjectMap extends ObjectMap {
|
|
41
|
+
constructor(object) {
|
|
42
|
+
super(object, { deep: true });
|
|
43
|
+
console.warn("DeepObjectMap is deprecated. Please use ObjectMap instead.");
|
|
44
|
+
}
|
|
45
|
+
}
|
|
47
46
|
|
|
48
47
|
export class ObjectTree extends ObjectMap {
|
|
49
48
|
constructor(...args) {
|
|
@@ -52,12 +51,10 @@ export class ObjectTree extends ObjectMap {
|
|
|
52
51
|
}
|
|
53
52
|
}
|
|
54
53
|
|
|
55
|
-
export class DeepObjectTree extends
|
|
56
|
-
constructor(
|
|
57
|
-
super(
|
|
58
|
-
console.warn(
|
|
59
|
-
"DeepObjectTree is deprecated. Please use DeepObjectMap instead."
|
|
60
|
-
);
|
|
54
|
+
export class DeepObjectTree extends ObjectMap {
|
|
55
|
+
constructor(object) {
|
|
56
|
+
super(object, { deep: true });
|
|
57
|
+
console.warn("DeepObjectTree is deprecated. Please use ObjectMap instead.");
|
|
61
58
|
}
|
|
62
59
|
}
|
|
63
60
|
|
package/src/Tree.js
CHANGED
|
@@ -6,6 +6,7 @@ export { default as addNextPrevious } from "./operations/addNextPrevious.js";
|
|
|
6
6
|
export { default as assign } from "./operations/assign.js";
|
|
7
7
|
export { default as cache } from "./operations/cache.js";
|
|
8
8
|
export { default as calendar } from "./operations/calendar.js";
|
|
9
|
+
export { default as child } from "./operations/child.js";
|
|
9
10
|
export { default as clear } from "./operations/clear.js";
|
|
10
11
|
export { default as constant } from "./operations/constant.js";
|
|
11
12
|
export { default as deepEntries } from "./operations/deepEntries.js";
|
|
@@ -49,10 +50,12 @@ export { default as paginate } from "./operations/paginate.js";
|
|
|
49
50
|
export { default as parent } from "./operations/parent.js";
|
|
50
51
|
export { default as paths } from "./operations/paths.js";
|
|
51
52
|
export { default as plain } from "./operations/plain.js";
|
|
53
|
+
export { default as reduce } from "./operations/reduce.js";
|
|
52
54
|
export { default as regExpKeys } from "./operations/regExpKeys.js";
|
|
53
55
|
export { default as reverse } from "./operations/reverse.js";
|
|
54
56
|
export { default as root } from "./operations/root.js";
|
|
55
57
|
export { default as scope } from "./operations/scope.js";
|
|
58
|
+
export { default as set } from "./operations/set.js";
|
|
56
59
|
export { default as shuffle } from "./operations/shuffle.js";
|
|
57
60
|
export { default as size } from "./operations/size.js";
|
|
58
61
|
export { default as sort } from "./operations/sort.js";
|
package/src/drivers/AsyncMap.js
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
|
+
import isMap from "../operations/isMap.js";
|
|
1
2
|
import * as trailingSlash from "../trailingSlash.js";
|
|
2
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Base class for asynchronous maps. These have the same interface as Map but the methods
|
|
6
|
+
* are asynchronous.
|
|
7
|
+
*
|
|
8
|
+
* @typedef {import("../../index.ts").AsyncTree<AsyncMap>} AsyncTree
|
|
9
|
+
* @implements {AsyncTree}
|
|
10
|
+
*/
|
|
3
11
|
export default class AsyncMap {
|
|
4
12
|
/** @type {AsyncMap|null} */
|
|
5
13
|
_parent = null;
|
|
@@ -10,15 +18,34 @@ export default class AsyncMap {
|
|
|
10
18
|
return this.entries();
|
|
11
19
|
}
|
|
12
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Return the child node for the given key, creating it if necessary.
|
|
23
|
+
*/
|
|
24
|
+
async child(key) {
|
|
25
|
+
let result = await this.get(key);
|
|
26
|
+
|
|
27
|
+
// If child is already a map we can use it as is
|
|
28
|
+
if (!isMap(result)) {
|
|
29
|
+
// Create new child node using no-arg constructor
|
|
30
|
+
result = new /** @type {any} */ (this.constructor)();
|
|
31
|
+
await this.set(key, result);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
result.parent = this;
|
|
35
|
+
return result;
|
|
36
|
+
}
|
|
37
|
+
|
|
13
38
|
/**
|
|
14
39
|
* Remove all key/value entries from the map.
|
|
15
40
|
*
|
|
16
41
|
* This method invokes the `keys()` and `delete()` methods.
|
|
17
42
|
*/
|
|
18
43
|
async clear() {
|
|
44
|
+
const promises = [];
|
|
19
45
|
for await (const key of this.keys()) {
|
|
20
|
-
|
|
46
|
+
promises.push(this.delete(key));
|
|
21
47
|
}
|
|
48
|
+
await Promise.all(promises);
|
|
22
49
|
}
|
|
23
50
|
|
|
24
51
|
/**
|
|
@@ -32,8 +59,6 @@ export default class AsyncMap {
|
|
|
32
59
|
throw new Error("delete() not implemented");
|
|
33
60
|
}
|
|
34
61
|
|
|
35
|
-
static EMPTY = Symbol("EMPTY");
|
|
36
|
-
|
|
37
62
|
/**
|
|
38
63
|
* Returns a new `AsyncIterator` object that contains a two-member array of
|
|
39
64
|
* [key, value] for each element in the map in insertion order.
|
|
@@ -191,6 +216,8 @@ export default class AsyncMap {
|
|
|
191
216
|
})();
|
|
192
217
|
}
|
|
193
218
|
|
|
219
|
+
trailingSlashKeys = false;
|
|
220
|
+
|
|
194
221
|
/**
|
|
195
222
|
* Returns a new `AsyncIterator` object that contains the values for each
|
|
196
223
|
* element in the map in insertion order.
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { hiddenFileNames } from "../constants.js";
|
|
2
|
-
import
|
|
3
|
-
import isMaplike from "../operations/isMaplike.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";
|
|
@@ -30,13 +29,35 @@ export default class BrowserFileMap extends AsyncMap {
|
|
|
30
29
|
this.directory = directoryHandle;
|
|
31
30
|
}
|
|
32
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
|
+
|
|
33
54
|
async delete(key) {
|
|
34
|
-
const
|
|
55
|
+
const normalized = trailingSlash.remove(key);
|
|
35
56
|
const directory = await this.getDirectory();
|
|
36
57
|
|
|
37
58
|
// Delete file.
|
|
38
59
|
try {
|
|
39
|
-
await directory.removeEntry(
|
|
60
|
+
await directory.removeEntry(normalized);
|
|
40
61
|
} catch (error) {
|
|
41
62
|
// If the file didn't exist, ignore the error.
|
|
42
63
|
if (error instanceof DOMException && error.name === "NotFoundError") {
|
|
@@ -111,9 +132,9 @@ export default class BrowserFileMap extends AsyncMap {
|
|
|
111
132
|
// @ts-ignore
|
|
112
133
|
for await (const entryKey of directory.keys()) {
|
|
113
134
|
// Check if the entry is a subfolder
|
|
114
|
-
const
|
|
135
|
+
const normalized = trailingSlash.remove(entryKey);
|
|
115
136
|
const subfolderHandle = await directory
|
|
116
|
-
.getDirectoryHandle(
|
|
137
|
+
.getDirectoryHandle(normalized)
|
|
117
138
|
.catch(() => null);
|
|
118
139
|
const isSubfolder = subfolderHandle !== null;
|
|
119
140
|
|
|
@@ -129,7 +150,7 @@ export default class BrowserFileMap extends AsyncMap {
|
|
|
129
150
|
}
|
|
130
151
|
|
|
131
152
|
async set(key, value) {
|
|
132
|
-
const
|
|
153
|
+
const normalized = trailingSlash.remove(key);
|
|
133
154
|
const directory = await this.getDirectory();
|
|
134
155
|
|
|
135
156
|
// Treat null value as empty string; will create an empty file.
|
|
@@ -152,24 +173,12 @@ export default class BrowserFileMap extends AsyncMap {
|
|
|
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 (value === /** @type {any} */ (this.constructor).EMPTY) {
|
|
162
|
-
// Create empty subtree.
|
|
163
|
-
await directory.getDirectoryHandle(baseKey, {
|
|
164
|
-
create: true,
|
|
165
|
-
});
|
|
166
|
-
} else if (isMaplike(value)) {
|
|
167
|
-
// Treat value as a tree and write it out as a subdirectory.
|
|
168
|
-
const subdirectory = await directory.getDirectoryHandle(baseKey, {
|
|
169
|
-
create: true,
|
|
170
|
-
});
|
|
171
|
-
const destTree = Reflect.construct(this.constructor, [subdirectory]);
|
|
172
|
-
await assign(destTree, value);
|
|
173
182
|
} else {
|
|
174
183
|
const typeName = value?.constructor?.name ?? "unknown";
|
|
175
184
|
throw new TypeError(`Cannot write a value of type ${typeName} as ${key}`);
|
|
@@ -178,7 +187,5 @@ export default class BrowserFileMap extends AsyncMap {
|
|
|
178
187
|
return this;
|
|
179
188
|
}
|
|
180
189
|
|
|
181
|
-
|
|
182
|
-
return true;
|
|
183
|
-
}
|
|
190
|
+
trailingSlashKeys = true;
|
|
184
191
|
}
|
package/src/drivers/FileMap.js
CHANGED
|
@@ -2,8 +2,6 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import { hiddenFileNames } from "../constants.js";
|
|
5
|
-
import from from "../operations/from.js";
|
|
6
|
-
import isMaplike from "../operations/isMaplike.js";
|
|
7
5
|
import * as trailingSlash from "../trailingSlash.js";
|
|
8
6
|
import isPacked from "../utilities/isPacked.js";
|
|
9
7
|
import isStringlike from "../utilities/isStringlike.js";
|
|
@@ -39,6 +37,26 @@ export default class FileMap extends SyncMap {
|
|
|
39
37
|
: path.resolve(process.cwd(), location);
|
|
40
38
|
}
|
|
41
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
|
+
|
|
42
60
|
delete(key) {
|
|
43
61
|
if (key === "" || key == null) {
|
|
44
62
|
// Can't have a file with no name or a nullish name
|
|
@@ -76,14 +94,9 @@ export default class FileMap extends SyncMap {
|
|
|
76
94
|
key = trailingSlash.remove(key); // normalize key
|
|
77
95
|
const filePath = path.resolve(this.dirname, key);
|
|
78
96
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
} catch (/** @type {any} */ error) {
|
|
83
|
-
if (error.code === "ENOENT" /* File not found */) {
|
|
84
|
-
return undefined;
|
|
85
|
-
}
|
|
86
|
-
throw error;
|
|
97
|
+
const stats = getStats(filePath);
|
|
98
|
+
if (stats === null) {
|
|
99
|
+
return undefined; // File or directory doesn't exist
|
|
87
100
|
}
|
|
88
101
|
|
|
89
102
|
let value;
|
|
@@ -140,13 +153,19 @@ export default class FileMap extends SyncMap {
|
|
|
140
153
|
set(key, value) {
|
|
141
154
|
// Where are we going to write this value?
|
|
142
155
|
const stringKey = key != null ? String(key) : "";
|
|
143
|
-
const
|
|
144
|
-
const destPath = path.resolve(this.dirname,
|
|
156
|
+
const normalized = trailingSlash.remove(stringKey);
|
|
157
|
+
const destPath = path.resolve(this.dirname, normalized);
|
|
145
158
|
|
|
146
159
|
// Ensure this directory exists.
|
|
147
160
|
const dirname = path.dirname(destPath);
|
|
148
161
|
fs.mkdirSync(dirname, { recursive: true });
|
|
149
162
|
|
|
163
|
+
if (value === undefined) {
|
|
164
|
+
// Special case: undefined value is equivalent to delete()
|
|
165
|
+
this.delete(normalized);
|
|
166
|
+
return this;
|
|
167
|
+
}
|
|
168
|
+
|
|
150
169
|
if (typeof value === "function") {
|
|
151
170
|
// Invoke function; write out the result.
|
|
152
171
|
value = value();
|
|
@@ -179,10 +198,6 @@ export default class FileMap extends SyncMap {
|
|
|
179
198
|
|
|
180
199
|
if (packed) {
|
|
181
200
|
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
201
|
} else {
|
|
187
202
|
const typeName = value?.constructor?.name ?? "unknown";
|
|
188
203
|
throw new TypeError(
|
|
@@ -193,53 +208,31 @@ export default class FileMap extends SyncMap {
|
|
|
193
208
|
return this;
|
|
194
209
|
}
|
|
195
210
|
|
|
196
|
-
|
|
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;
|
|
211
|
+
trailingSlashKeys = true;
|
|
212
212
|
}
|
|
213
213
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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);
|
|
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;
|
|
230
224
|
}
|
|
225
|
+
return stats;
|
|
231
226
|
}
|
|
232
227
|
|
|
233
228
|
// Write a value to a file.
|
|
234
229
|
function writeFile(value, destPath) {
|
|
235
|
-
//
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
-
}
|
|
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 });
|
|
244
234
|
}
|
|
235
|
+
|
|
236
|
+
// Write out the value as the contents of a file.
|
|
237
|
+
fs.writeFileSync(destPath, value);
|
|
245
238
|
}
|
package/src/drivers/ObjectMap.js
CHANGED
|
@@ -1,10 +1,20 @@
|
|
|
1
1
|
import * as symbols from "../symbols.js";
|
|
2
2
|
import * as trailingSlash from "../trailingSlash.js";
|
|
3
|
+
import isPlainObject from "../utilities/isPlainObject.js";
|
|
3
4
|
import setParent from "../utilities/setParent.js";
|
|
4
5
|
import SyncMap from "./SyncMap.js";
|
|
5
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Map wrapper for a JavaScript object or array
|
|
9
|
+
*/
|
|
6
10
|
export default class ObjectMap extends SyncMap {
|
|
7
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Wrap the given object in a `Map`.
|
|
13
|
+
*
|
|
14
|
+
* @param {any} object
|
|
15
|
+
* @param {{ deep?: boolean }} [options]
|
|
16
|
+
*/
|
|
17
|
+
constructor(object = {}, options = {}) {
|
|
8
18
|
super();
|
|
9
19
|
// Note: we use `typeof` here instead of `instanceof Object` to allow for
|
|
10
20
|
// objects such as Node's `Module` class for representing an ES module.
|
|
@@ -15,6 +25,7 @@ export default class ObjectMap extends SyncMap {
|
|
|
15
25
|
}
|
|
16
26
|
this.object = object;
|
|
17
27
|
this.parent = object[symbols.parent] ?? null;
|
|
28
|
+
this.deep = options.deep === true;
|
|
18
29
|
}
|
|
19
30
|
|
|
20
31
|
delete(key) {
|
|
@@ -51,12 +62,20 @@ export default class ObjectMap extends SyncMap {
|
|
|
51
62
|
value = value.bind(this.object);
|
|
52
63
|
}
|
|
53
64
|
|
|
65
|
+
if (this.deep && (value instanceof Array || isPlainObject(value))) {
|
|
66
|
+
// Construct submap for sub-objects and sub-arrays
|
|
67
|
+
value = Reflect.construct(this.constructor, [value, { deep: true }]);
|
|
68
|
+
}
|
|
69
|
+
|
|
54
70
|
return value;
|
|
55
71
|
}
|
|
56
72
|
|
|
57
73
|
/** @returns {boolean} */
|
|
58
74
|
isSubtree(value) {
|
|
59
|
-
|
|
75
|
+
if (value instanceof Map) {
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
return this.deep && (value instanceof Array || isPlainObject(value));
|
|
60
79
|
}
|
|
61
80
|
|
|
62
81
|
keys() {
|
|
@@ -109,20 +128,13 @@ export default class ObjectMap extends SyncMap {
|
|
|
109
128
|
delete this.object[existingKey];
|
|
110
129
|
}
|
|
111
130
|
|
|
112
|
-
if (value === /** @type {any} */ (this.constructor).EMPTY) {
|
|
113
|
-
// Create empty subtree
|
|
114
|
-
value = Reflect.construct(this.constructor, []);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
131
|
// Set the value for the key.
|
|
118
132
|
this.object[key] = value;
|
|
119
133
|
|
|
120
134
|
return this;
|
|
121
135
|
}
|
|
122
136
|
|
|
123
|
-
|
|
124
|
-
return true;
|
|
125
|
-
}
|
|
137
|
+
trailingSlashKeys = true;
|
|
126
138
|
}
|
|
127
139
|
|
|
128
140
|
function findExistingKey(object, key) {
|
package/src/drivers/SiteMap.js
CHANGED
|
@@ -34,9 +34,9 @@ export default class SiteMap extends AsyncMap {
|
|
|
34
34
|
);
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
// A key with a trailing slash
|
|
38
|
-
//
|
|
39
|
-
if (trailingSlash.has(key)
|
|
37
|
+
// A key with a trailing slash is for a folder; return a subtree without
|
|
38
|
+
// making a network request.
|
|
39
|
+
if (trailingSlash.has(key)) {
|
|
40
40
|
const href = new URL(key, this.href).href;
|
|
41
41
|
const value = Reflect.construct(this.constructor, [href]);
|
|
42
42
|
setParent(value, this);
|
|
@@ -112,9 +112,7 @@ export default class SiteMap extends AsyncMap {
|
|
|
112
112
|
}
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
-
|
|
116
|
-
return true;
|
|
117
|
-
}
|
|
115
|
+
trailingSlashKeys = true;
|
|
118
116
|
|
|
119
117
|
get url() {
|
|
120
118
|
return new URL(this.href);
|
package/src/drivers/SyncMap.js
CHANGED
|
@@ -17,6 +17,9 @@ const previewSymbol = Symbol("preview");
|
|
|
17
17
|
* also indicate children subtrees using the trailing slash convention: a key
|
|
18
18
|
* for a subtree may optionally end with a slash. The get() and has() methods
|
|
19
19
|
* support optional trailing slashes on keys.
|
|
20
|
+
*
|
|
21
|
+
* @typedef {import("../../index.ts").SyncTree<SyncMap>} SyncTree
|
|
22
|
+
* @implements {SyncTree}
|
|
20
23
|
*/
|
|
21
24
|
export default class SyncMap extends Map {
|
|
22
25
|
_initialized = false;
|
|
@@ -36,6 +39,23 @@ export default class SyncMap extends Map {
|
|
|
36
39
|
this._self = this;
|
|
37
40
|
}
|
|
38
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Return the child node for the given key, creating it if necessary.
|
|
44
|
+
*/
|
|
45
|
+
child(key) {
|
|
46
|
+
let result = this.get(key);
|
|
47
|
+
|
|
48
|
+
// If child is already a map we can use it as is
|
|
49
|
+
if (!(result instanceof Map)) {
|
|
50
|
+
// Create new child node using no-arg constructor
|
|
51
|
+
result = new /** @type {any} */ (this.constructor)();
|
|
52
|
+
this.set(key, result);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
result.parent = this;
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
|
|
39
59
|
/**
|
|
40
60
|
* Removes all key/value entries from the map.
|
|
41
61
|
*
|
|
@@ -68,8 +88,6 @@ export default class SyncMap extends Map {
|
|
|
68
88
|
return super.delete.call(this._self, key);
|
|
69
89
|
}
|
|
70
90
|
|
|
71
|
-
static EMPTY = Symbol("EMPTY");
|
|
72
|
-
|
|
73
91
|
/**
|
|
74
92
|
* Returns a new `Iterator` object that contains a two-member array of [key,
|
|
75
93
|
* value] for each element in the map in insertion order.
|
|
@@ -191,11 +209,6 @@ export default class SyncMap extends Map {
|
|
|
191
209
|
// necessary to let the constructor call `super()`.
|
|
192
210
|
const target = this._self ?? this;
|
|
193
211
|
|
|
194
|
-
// Support EMPTY symbol to create empty subtrees
|
|
195
|
-
if (value === /** @type {any} */ (this.constructor).EMPTY) {
|
|
196
|
-
value = Reflect.construct(this.constructor, []);
|
|
197
|
-
}
|
|
198
|
-
|
|
199
212
|
return super.set.call(target, key, value);
|
|
200
213
|
}
|
|
201
214
|
|
|
@@ -219,6 +232,8 @@ export default class SyncMap extends Map {
|
|
|
219
232
|
return this.entries();
|
|
220
233
|
}
|
|
221
234
|
|
|
235
|
+
trailingSlashKeys = false;
|
|
236
|
+
|
|
222
237
|
/**
|
|
223
238
|
* Returns a new `Iterator` object that contains the values for each element
|
|
224
239
|
* in the map in insertion order.
|
package/src/jsonKeys.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import entries from "./operations/entries.js";
|
|
1
2
|
import from from "./operations/from.js";
|
|
3
|
+
import isMap from "./operations/isMap.js";
|
|
2
4
|
import keys from "./operations/keys.js";
|
|
5
|
+
import * as trailingSlash from "./trailingSlash.js";
|
|
3
6
|
|
|
4
7
|
/**
|
|
5
8
|
* Given a tree node, return a JSON string that can be written to a .keys.json
|
|
@@ -14,7 +17,18 @@ import keys from "./operations/keys.js";
|
|
|
14
17
|
*/
|
|
15
18
|
export async function stringify(maplike) {
|
|
16
19
|
const tree = from(maplike);
|
|
17
|
-
|
|
20
|
+
|
|
21
|
+
let treeKeys;
|
|
22
|
+
if (/** @type {any} */ (tree).trailingSlashKeys) {
|
|
23
|
+
treeKeys = await keys(tree);
|
|
24
|
+
} else {
|
|
25
|
+
// Use entries() to determine which keys are subtrees.
|
|
26
|
+
const treeEntries = await entries(tree);
|
|
27
|
+
treeKeys = treeEntries.map(([key, value]) =>
|
|
28
|
+
trailingSlash.toggle(key, isMap(value))
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
18
32
|
// Skip the key `.keys.json` if present.
|
|
19
33
|
treeKeys = treeKeys.filter((key) => key !== ".keys.json");
|
|
20
34
|
const json = JSON.stringify(treeKeys);
|