@weborigami/async-tree 0.0.35
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/ReadMe.md +1 -0
- package/browser.js +13 -0
- package/index.ts +44 -0
- package/main.js +23 -0
- package/package.json +20 -0
- package/src/BrowserFileTree.js +137 -0
- package/src/DeferredTree.js +72 -0
- package/src/FileTree.js +193 -0
- package/src/FunctionTree.js +47 -0
- package/src/MapTree.js +51 -0
- package/src/ObjectTree.js +103 -0
- package/src/SetTree.js +40 -0
- package/src/SiteTree.js +124 -0
- package/src/Tree.d.ts +22 -0
- package/src/Tree.js +404 -0
- package/src/keysJson.d.ts +4 -0
- package/src/keysJson.js +56 -0
- package/src/operations/cache.js +75 -0
- package/src/operations/merge.js +60 -0
- package/src/operations/mergeDeep.js +47 -0
- package/src/transforms/cachedKeyMaps.js +86 -0
- package/src/transforms/groupBy.js +40 -0
- package/src/transforms/keyMapsForExtensions.js +64 -0
- package/src/transforms/map.js +106 -0
- package/src/transforms/regExpKeys.js +65 -0
- package/src/transforms/sort.js +22 -0
- package/src/transforms/sortBy.js +29 -0
- package/src/transforms/sortNatural.js +10 -0
- package/src/utilities.d.ts +10 -0
- package/src/utilities.js +124 -0
- package/test/BrowserFileTree.test.js +119 -0
- package/test/DeferredTree.test.js +23 -0
- package/test/FileTree.test.js +134 -0
- package/test/FunctionTree.test.js +51 -0
- package/test/MapTree.test.js +37 -0
- package/test/ObjectTree.test.js +159 -0
- package/test/SetTree.test.js +32 -0
- package/test/SiteTree.test.js +110 -0
- package/test/Tree.test.js +347 -0
- package/test/browser/assert.js +45 -0
- package/test/browser/index.html +35 -0
- package/test/browser/testRunner.js +51 -0
- package/test/fixtures/markdown/Alice.md +1 -0
- package/test/fixtures/markdown/Bob.md +1 -0
- package/test/fixtures/markdown/Carol.md +1 -0
- package/test/operations/cache.test.js +57 -0
- package/test/operations/merge.test.js +39 -0
- package/test/operations/mergeDeep.test.js +38 -0
- package/test/transforms/cachedKeyMaps.test.js +41 -0
- package/test/transforms/groupBy.test.js +29 -0
- package/test/transforms/keyMapsForExtensions.test.js +70 -0
- package/test/transforms/map.test.js +174 -0
- package/test/transforms/regExpKeys.test.js +25 -0
- package/test/transforms/sort.test.js +30 -0
- package/test/transforms/sortBy.test.js +22 -0
- package/test/transforms/sortNatural.test.js +21 -0
- package/test/utilities.test.js +24 -0
package/ReadMe.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
This library contains definitions for asynchronous trees backed by standard JavaScript classes like `Object` and `Map` and standard browser APIs such as the [Origin Private File System](https://developer.mozilla.org/en-US/docs/Web/API/File_System_API/Origin_private_file_system). The library also includes collections of helpers for common tree operations.
|
package/browser.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Exports for browser
|
|
2
|
+
|
|
3
|
+
export { default as DeferredTree } from "./src/DeferredTree.js";
|
|
4
|
+
// Skip FileTree.js, which is Node.js only.
|
|
5
|
+
export { default as BrowserFileTree } from "./src/BrowserFileTree.js";
|
|
6
|
+
export { default as FunctionTree } from "./src/FunctionTree.js";
|
|
7
|
+
export { default as MapTree } from "./src/MapTree.js";
|
|
8
|
+
export { default as ObjectTree } from "./src/ObjectTree.js";
|
|
9
|
+
export { default as SetTree } from "./src/SetTree.js";
|
|
10
|
+
export { default as SiteTree } from "./src/SiteTree.js";
|
|
11
|
+
export * as Tree from "./src/Tree.js";
|
|
12
|
+
export * as keysJson from "./src/keysJson.js";
|
|
13
|
+
export * from "./src/utilities.js";
|
package/index.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { AsyncTree } from "@weborigami/types";
|
|
2
|
+
|
|
3
|
+
export * from "./main.js";
|
|
4
|
+
|
|
5
|
+
export type KeyFn = (key: any, innerTree: AsyncTree) => any;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* An object with a non-trivial `toString` method.
|
|
9
|
+
*
|
|
10
|
+
* TODO: We want to deliberately exclude the base `Object` class because its
|
|
11
|
+
* `toString` method return non-useful strings like `[object Object]`. How can
|
|
12
|
+
* we declare that in TypeScript?
|
|
13
|
+
*/
|
|
14
|
+
export type HasString = {
|
|
15
|
+
toString(): string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type PlainObject = {
|
|
19
|
+
[key: string]: any;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type ReduceFn = (values: any[], keys: any[]) => Promise<any>;
|
|
23
|
+
|
|
24
|
+
export type StringLike = string | HasString;
|
|
25
|
+
|
|
26
|
+
type NativeTreelike =
|
|
27
|
+
any[] |
|
|
28
|
+
AsyncTree |
|
|
29
|
+
Function |
|
|
30
|
+
Map<any, any> |
|
|
31
|
+
PlainObject |
|
|
32
|
+
Set<any>;
|
|
33
|
+
|
|
34
|
+
export type Treelike =
|
|
35
|
+
NativeTreelike |
|
|
36
|
+
Unpackable<NativeTreelike>;
|
|
37
|
+
|
|
38
|
+
export type TreeTransform = (tree: AsyncTree) => AsyncTree;
|
|
39
|
+
|
|
40
|
+
export type Unpackable<T> = {
|
|
41
|
+
unpack(): Promise<T>
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export type ValueKeyFn = (value: any, key: any, innerTree: AsyncTree) => any;
|
package/main.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Exports for Node.js
|
|
2
|
+
|
|
3
|
+
export { default as DeferredTree } from "./src/DeferredTree.js";
|
|
4
|
+
export { default as FileTree } from "./src/FileTree.js";
|
|
5
|
+
export { default as FunctionTree } from "./src/FunctionTree.js";
|
|
6
|
+
export { default as MapTree } from "./src/MapTree.js";
|
|
7
|
+
export { default as ObjectTree } from "./src/ObjectTree.js";
|
|
8
|
+
// Skip BrowserFileTree.js, which is browser-only.
|
|
9
|
+
export { default as SetTree } from "./src/SetTree.js";
|
|
10
|
+
export { default as SiteTree } from "./src/SiteTree.js";
|
|
11
|
+
export * as Tree from "./src/Tree.js";
|
|
12
|
+
export * as keysJson from "./src/keysJson.js";
|
|
13
|
+
export { default as cache } from "./src/operations/cache.js";
|
|
14
|
+
export { default as merge } from "./src/operations/merge.js";
|
|
15
|
+
export { default as mergeDeep } from "./src/operations/mergeDeep.js";
|
|
16
|
+
export { default as cachedKeyMaps } from "./src/transforms/cachedKeyMaps.js";
|
|
17
|
+
export { default as groupBy } from "./src/transforms/groupBy.js";
|
|
18
|
+
export { default as keyMapsForExtensions } from "./src/transforms/keyMapsForExtensions.js";
|
|
19
|
+
export { default as map } from "./src/transforms/map.js";
|
|
20
|
+
export { default as sort } from "./src/transforms/sort.js";
|
|
21
|
+
export { default as sortBy } from "./src/transforms/sortBy.js";
|
|
22
|
+
export { default as sortNatural } from "./src/transforms/sortNatural.js";
|
|
23
|
+
export * from "./src/utilities.js";
|
package/package.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@weborigami/async-tree",
|
|
3
|
+
"version": "0.0.35",
|
|
4
|
+
"description": "Asynchronous tree drivers based on standard JavaScript classes",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./main.js",
|
|
7
|
+
"browser": "./browser.js",
|
|
8
|
+
"types": "./index.ts",
|
|
9
|
+
"devDependencies": {
|
|
10
|
+
"@types/node": "20.8.10",
|
|
11
|
+
"typescript": "5.2.2"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@weborigami/types": "https://gitpkg.now.sh/GraphOrigami/origami/types?main"
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"test": "node --test",
|
|
18
|
+
"typecheck": "tsc"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import * as Tree from "./Tree.js";
|
|
2
|
+
import { hiddenFileNames, isStringLike, sortNatural } from "./utilities.js";
|
|
3
|
+
|
|
4
|
+
const TypedArray = Object.getPrototypeOf(Uint8Array);
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A tree of files backed by a browser-hosted file system such as the standard
|
|
8
|
+
* Origin Private File System or the (as of October 2023) experimental File
|
|
9
|
+
* System Access API.
|
|
10
|
+
*
|
|
11
|
+
* @typedef {import("@weborigami/types").AsyncMutableTree} AsyncMutableTree
|
|
12
|
+
* @implements {AsyncMutableTree}
|
|
13
|
+
*/
|
|
14
|
+
export default class BrowserFileTree {
|
|
15
|
+
/**
|
|
16
|
+
* Construct a tree of files backed by a browser-hosted file system.
|
|
17
|
+
*
|
|
18
|
+
* The directory handle can be obtained via any of the [methods that return a
|
|
19
|
+
* FileSystemDirectoryHandle](https://developer.mozilla.org/en-US/docs/Web/API/FileSystemDirectoryHandle).
|
|
20
|
+
* If no directory is supplied, the tree is rooted at the Origin Private File
|
|
21
|
+
* System for the current site.
|
|
22
|
+
*
|
|
23
|
+
* @param {FileSystemDirectoryHandle} [directoryHandle]
|
|
24
|
+
*/
|
|
25
|
+
constructor(directoryHandle) {
|
|
26
|
+
/** @type {FileSystemDirectoryHandle}
|
|
27
|
+
* @ts-ignore */
|
|
28
|
+
this.directory = directoryHandle;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async get(key) {
|
|
32
|
+
const directory = await this.getDirectory();
|
|
33
|
+
|
|
34
|
+
// Try the key as a subfolder name.
|
|
35
|
+
try {
|
|
36
|
+
const subfolderHandle = await directory.getDirectoryHandle(key);
|
|
37
|
+
return Reflect.construct(this.constructor, [subfolderHandle]);
|
|
38
|
+
} catch (error) {
|
|
39
|
+
if (
|
|
40
|
+
!(
|
|
41
|
+
error instanceof DOMException &&
|
|
42
|
+
(error.name === "NotFoundError" || error.name === "TypeMismatchError")
|
|
43
|
+
)
|
|
44
|
+
) {
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Try the key as a file name.
|
|
50
|
+
try {
|
|
51
|
+
const fileHandle = await directory.getFileHandle(key);
|
|
52
|
+
const file = await fileHandle.getFile();
|
|
53
|
+
return file.arrayBuffer();
|
|
54
|
+
} catch (error) {
|
|
55
|
+
if (!(error instanceof DOMException && error.name === "NotFoundError")) {
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async getDirectory() {
|
|
64
|
+
this.directory ??= await navigator.storage.getDirectory();
|
|
65
|
+
return this.directory;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async keys() {
|
|
69
|
+
const directory = await this.getDirectory();
|
|
70
|
+
let keys = [];
|
|
71
|
+
// @ts-ignore
|
|
72
|
+
for await (const key of directory.keys()) {
|
|
73
|
+
keys.push(key);
|
|
74
|
+
}
|
|
75
|
+
// Filter out unhelpful file names.
|
|
76
|
+
keys = keys.filter((key) => !hiddenFileNames.includes(key));
|
|
77
|
+
sortNatural(keys);
|
|
78
|
+
return keys;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async set(key, value) {
|
|
82
|
+
const directory = await this.getDirectory();
|
|
83
|
+
|
|
84
|
+
if (value === undefined) {
|
|
85
|
+
// Delete file.
|
|
86
|
+
try {
|
|
87
|
+
await directory.removeEntry(key);
|
|
88
|
+
} catch (error) {
|
|
89
|
+
// If the file didn't exist, ignore the error.
|
|
90
|
+
if (
|
|
91
|
+
!(error instanceof DOMException && error.name === "NotFoundError")
|
|
92
|
+
) {
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return this;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Treat null value as empty string; will create an empty file.
|
|
100
|
+
if (value === null) {
|
|
101
|
+
value = "";
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// True if fs.writeFile can directly write the value to a file.
|
|
105
|
+
let isWriteable =
|
|
106
|
+
value instanceof ArrayBuffer ||
|
|
107
|
+
value instanceof TypedArray ||
|
|
108
|
+
value instanceof DataView ||
|
|
109
|
+
value instanceof Blob;
|
|
110
|
+
|
|
111
|
+
if (!isWriteable && isStringLike(value)) {
|
|
112
|
+
// Value has a meaningful `toString` method, use that.
|
|
113
|
+
value = String(value);
|
|
114
|
+
isWriteable = true;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (isWriteable) {
|
|
118
|
+
// Write file.
|
|
119
|
+
const fileHandle = await directory.getFileHandle(key, { create: true });
|
|
120
|
+
const writable = await fileHandle.createWritable();
|
|
121
|
+
await writable.write(value);
|
|
122
|
+
await writable.close();
|
|
123
|
+
} else if (Tree.isTreelike(value)) {
|
|
124
|
+
// Treat value as a tree and write it out as a subdirectory.
|
|
125
|
+
const subdirectory = await directory.getDirectoryHandle(key, {
|
|
126
|
+
create: true,
|
|
127
|
+
});
|
|
128
|
+
const destTree = Reflect.construct(this.constructor, [subdirectory]);
|
|
129
|
+
await Tree.assign(destTree, value);
|
|
130
|
+
} else {
|
|
131
|
+
const typeName = value?.constructor?.name ?? "unknown";
|
|
132
|
+
throw new TypeError(`Cannot write a value of type ${typeName} as ${key}`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return this;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import * as Tree from "./Tree.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A tree that is loaded lazily.
|
|
5
|
+
*
|
|
6
|
+
* This is useful in situations that must return a tree synchronously. If
|
|
7
|
+
* constructing the tree requires an asynchronous operation, this class can be
|
|
8
|
+
* used as a wrapper that can be returned immediately. The tree will be loaded
|
|
9
|
+
* the first time the keys() or get() functions are called.
|
|
10
|
+
*
|
|
11
|
+
* @typedef {import("@weborigami/types").AsyncTree} AsyncTree
|
|
12
|
+
* @implements {AsyncTree}
|
|
13
|
+
*/
|
|
14
|
+
export default class DeferredTree {
|
|
15
|
+
/**
|
|
16
|
+
* @param {Function|Promise<any>} loader
|
|
17
|
+
*/
|
|
18
|
+
constructor(loader) {
|
|
19
|
+
this.loader = loader;
|
|
20
|
+
this.treePromise = null;
|
|
21
|
+
this._tree = null;
|
|
22
|
+
this._parent = null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async get(key) {
|
|
26
|
+
const tree = await this.tree();
|
|
27
|
+
return tree.get(key);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async loadResult() {
|
|
31
|
+
if (!(this.loader instanceof Promise)) {
|
|
32
|
+
this.loader = this.loader();
|
|
33
|
+
}
|
|
34
|
+
return this.loader;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async keys() {
|
|
38
|
+
const tree = await this.tree();
|
|
39
|
+
return tree.keys();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
get parent() {
|
|
43
|
+
return this._tree?.parent ?? this._parent;
|
|
44
|
+
}
|
|
45
|
+
set parent(parent) {
|
|
46
|
+
if (this._tree && !this._tree.parent) {
|
|
47
|
+
this._tree.parent = parent;
|
|
48
|
+
} else {
|
|
49
|
+
this._parent = parent;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async tree() {
|
|
54
|
+
if (this._tree) {
|
|
55
|
+
return this._tree;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Use a promise to ensure the treelike is only converted to a tree once.
|
|
59
|
+
this.treePromise ??= this.loadResult().then((treelike) => {
|
|
60
|
+
this._tree = Tree.from(treelike);
|
|
61
|
+
if (this._parent) {
|
|
62
|
+
if (!this._tree.parent) {
|
|
63
|
+
this._tree.parent = this._parent;
|
|
64
|
+
}
|
|
65
|
+
this._parent = null;
|
|
66
|
+
}
|
|
67
|
+
return this._tree;
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
return this.treePromise;
|
|
71
|
+
}
|
|
72
|
+
}
|
package/src/FileTree.js
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import * as Tree from "./Tree.js";
|
|
5
|
+
import {
|
|
6
|
+
getRealmObjectPrototype,
|
|
7
|
+
hiddenFileNames,
|
|
8
|
+
sortNatural,
|
|
9
|
+
} from "./utilities.js";
|
|
10
|
+
|
|
11
|
+
const TypedArray = Object.getPrototypeOf(Uint8Array);
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A file system tree via the Node file system API.
|
|
15
|
+
*
|
|
16
|
+
* @typedef {import("@weborigami/types").AsyncMutableTree} AsyncMutableTree
|
|
17
|
+
* @implements {AsyncMutableTree}
|
|
18
|
+
*/
|
|
19
|
+
export default class FileTree {
|
|
20
|
+
/**
|
|
21
|
+
* @param {string} location
|
|
22
|
+
*/
|
|
23
|
+
constructor(location) {
|
|
24
|
+
this.dirname = location.startsWith("file://")
|
|
25
|
+
? path.dirname(fileURLToPath(location))
|
|
26
|
+
: path.resolve(process.cwd(), location);
|
|
27
|
+
this.parent = null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async get(key) {
|
|
31
|
+
const filePath = path.resolve(this.dirname, key);
|
|
32
|
+
|
|
33
|
+
let stats;
|
|
34
|
+
try {
|
|
35
|
+
stats = await fs.stat(filePath);
|
|
36
|
+
} catch (/** @type {any} */ error) {
|
|
37
|
+
if (error.code === "ENOENT" /* File not found */) {
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
throw error;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (stats.isDirectory()) {
|
|
44
|
+
// Return subdirectory as a tree
|
|
45
|
+
const subtree = Reflect.construct(this.constructor, [filePath]);
|
|
46
|
+
subtree.parent = this;
|
|
47
|
+
return subtree;
|
|
48
|
+
} else {
|
|
49
|
+
// Return file contents
|
|
50
|
+
return fs.readFile(filePath);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async isKeyForSubtree(key) {
|
|
55
|
+
const filePath = path.join(this.dirname, key);
|
|
56
|
+
const stats = await stat(filePath);
|
|
57
|
+
return stats ? stats.isDirectory() : false;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Enumerate the names of the files/subdirectories in this directory.
|
|
62
|
+
*/
|
|
63
|
+
async keys() {
|
|
64
|
+
let entries;
|
|
65
|
+
try {
|
|
66
|
+
entries = await fs.readdir(this.dirname, { withFileTypes: true });
|
|
67
|
+
} catch (/** @type {any} */ error) {
|
|
68
|
+
if (error.code !== "ENOENT") {
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
|
71
|
+
entries = [];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
let names = entries.map((entry) => entry.name);
|
|
75
|
+
|
|
76
|
+
// Filter out unhelpful file names.
|
|
77
|
+
names = names.filter((name) => !hiddenFileNames.includes(name));
|
|
78
|
+
|
|
79
|
+
// Node fs.readdir sort order appears to be unreliable; see, e.g.,
|
|
80
|
+
// https://github.com/nodejs/node/issues/3232.
|
|
81
|
+
sortNatural(names);
|
|
82
|
+
return names;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
get path() {
|
|
86
|
+
return this.dirname;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async set(key, value) {
|
|
90
|
+
// Where are we going to write this value?
|
|
91
|
+
const stringKey = key ? String(key) : "";
|
|
92
|
+
const destPath = path.resolve(this.dirname, stringKey);
|
|
93
|
+
|
|
94
|
+
if (value === undefined) {
|
|
95
|
+
// Delete the file or directory.
|
|
96
|
+
let stats;
|
|
97
|
+
try {
|
|
98
|
+
stats = await stat(destPath);
|
|
99
|
+
} catch (/** @type {any} */ error) {
|
|
100
|
+
if (error.code === "ENOENT" /* File not found */) {
|
|
101
|
+
return this;
|
|
102
|
+
}
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (stats?.isDirectory()) {
|
|
107
|
+
// Delete directory.
|
|
108
|
+
await fs.rm(destPath, { recursive: true });
|
|
109
|
+
} else if (stats) {
|
|
110
|
+
// Delete file.
|
|
111
|
+
await fs.unlink(destPath);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return this;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Treat null value as empty string; will create an empty file.
|
|
118
|
+
if (value === null) {
|
|
119
|
+
value = "";
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (value instanceof ArrayBuffer) {
|
|
123
|
+
// Convert ArrayBuffer to Uint8Array, which Node.js can write directly.
|
|
124
|
+
value = new Uint8Array(value);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// True if fs.writeFile can directly write the value to a file.
|
|
128
|
+
let isWriteable =
|
|
129
|
+
value instanceof TypedArray ||
|
|
130
|
+
value instanceof DataView ||
|
|
131
|
+
(globalThis.ReadableStream && value instanceof ReadableStream);
|
|
132
|
+
|
|
133
|
+
if (!isWriteable && isStringLike(value)) {
|
|
134
|
+
// Value has a meaningful `toString` method, use that.
|
|
135
|
+
value = String(value);
|
|
136
|
+
isWriteable = true;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (isWriteable) {
|
|
140
|
+
// Ensure this directory exists.
|
|
141
|
+
await fs.mkdir(this.dirname, { recursive: true });
|
|
142
|
+
// Write out the value as the contents of a file.
|
|
143
|
+
await fs.writeFile(destPath, value);
|
|
144
|
+
} else if (Tree.isTreelike(value)) {
|
|
145
|
+
// Treat value as a tree and write it out as a subdirectory.
|
|
146
|
+
const destTree = Reflect.construct(this.constructor, [destPath]);
|
|
147
|
+
await Tree.assign(destTree, value);
|
|
148
|
+
} else {
|
|
149
|
+
const typeName = value?.constructor?.name ?? "unknown";
|
|
150
|
+
throw new TypeError(
|
|
151
|
+
`Cannot write a value of type ${typeName} as ${stringKey}`
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return this;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Return true if the object is a string or object with a non-trival `toString`
|
|
161
|
+
* method.
|
|
162
|
+
*
|
|
163
|
+
* @param {any} obj
|
|
164
|
+
*/
|
|
165
|
+
function isStringLike(obj) {
|
|
166
|
+
if (typeof obj === "string") {
|
|
167
|
+
return true;
|
|
168
|
+
} else if (obj?.toString === undefined) {
|
|
169
|
+
return false;
|
|
170
|
+
} else if (obj.toString === getRealmObjectPrototype(obj).toString) {
|
|
171
|
+
// The stupid Object.prototype.toString implementation always returns
|
|
172
|
+
// "[object Object]", so if that's the only toString method the object has,
|
|
173
|
+
// we return false.
|
|
174
|
+
return false;
|
|
175
|
+
} else {
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Return the file information for the file/folder at the given path.
|
|
181
|
+
// If it does not exist, return undefined.
|
|
182
|
+
async function stat(filePath) {
|
|
183
|
+
try {
|
|
184
|
+
// Await the result here so that, if the file doesn't exist, the catch block
|
|
185
|
+
// below will catch the exception.
|
|
186
|
+
return await fs.stat(filePath);
|
|
187
|
+
} catch (/** @type {any} */ error) {
|
|
188
|
+
if (error.code === "ENOENT" /* File not found */) {
|
|
189
|
+
return undefined;
|
|
190
|
+
}
|
|
191
|
+
throw error;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A tree defined by a function and an optional domain.
|
|
3
|
+
*
|
|
4
|
+
* @typedef {import("@weborigami/types").AsyncTree} AsyncTree
|
|
5
|
+
* @implements {AsyncTree}
|
|
6
|
+
*/
|
|
7
|
+
export default class FunctionTree {
|
|
8
|
+
/**
|
|
9
|
+
* @param {function} fn the key->value function
|
|
10
|
+
* @param {Iterable<any>} [domain] optional domain of the function
|
|
11
|
+
*/
|
|
12
|
+
constructor(fn, domain = []) {
|
|
13
|
+
this.fn = fn;
|
|
14
|
+
this.domain = domain;
|
|
15
|
+
this.parent = null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Return the application of the function to the given key.
|
|
20
|
+
*
|
|
21
|
+
* @param {any} key
|
|
22
|
+
*/
|
|
23
|
+
async get(key) {
|
|
24
|
+
const value =
|
|
25
|
+
this.fn.length <= 1
|
|
26
|
+
? // Function takes no arguments or only one argument: invoke
|
|
27
|
+
await this.fn.call(null, key)
|
|
28
|
+
: // Bind the key to the first parameter. Subsequent get calls will
|
|
29
|
+
// eventually bind all parameters until only one remains. At that point,
|
|
30
|
+
// the above condition will apply and the function will be invoked.
|
|
31
|
+
Reflect.construct(this.constructor, [this.fn.bind(null, key)]);
|
|
32
|
+
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Enumerates the function's domain (if defined) as the tree's keys. If no domain
|
|
38
|
+
* was defined, this returns an empty iterator.
|
|
39
|
+
*/
|
|
40
|
+
async keys() {
|
|
41
|
+
return this.domain;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async unpack() {
|
|
45
|
+
return this.fn;
|
|
46
|
+
}
|
|
47
|
+
}
|
package/src/MapTree.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import * as Tree from "./Tree.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A tree backed by a JavaScript `Map` object.
|
|
5
|
+
*
|
|
6
|
+
* Note: By design, the standard `Map` class already complies with the
|
|
7
|
+
* `AsyncTree` interface. This class adds some additional tree behavior, such as
|
|
8
|
+
* constructing subtree instances and setting their `parent` property. While
|
|
9
|
+
* we'd like to construct this by subclassing `Map`, that class appears
|
|
10
|
+
* puzzingly and deliberately implemented to break subclasses.
|
|
11
|
+
*
|
|
12
|
+
* @typedef {import("@weborigami/types").AsyncMutableTree} AsyncMutableTree
|
|
13
|
+
* @implements {AsyncMutableTree}
|
|
14
|
+
*/
|
|
15
|
+
export default class MapTree {
|
|
16
|
+
/**
|
|
17
|
+
* @param {Iterable} [iterable]
|
|
18
|
+
*/
|
|
19
|
+
constructor(iterable = []) {
|
|
20
|
+
this.map = new Map(iterable);
|
|
21
|
+
this.parent = null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async get(key) {
|
|
25
|
+
let value = this.map.get(key);
|
|
26
|
+
|
|
27
|
+
if (value instanceof Map) {
|
|
28
|
+
value = Reflect.construct(this.constructor, [value]);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (Tree.isAsyncTree(value) && !value.parent) {
|
|
32
|
+
value.parent = this;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return value;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async isKeyForSubtree(key) {
|
|
39
|
+
const value = this.map.get(key);
|
|
40
|
+
return value instanceof Map || Tree.isAsyncTree(value);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async keys() {
|
|
44
|
+
return this.map.keys();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async set(key, value) {
|
|
48
|
+
this.map.set(key, value);
|
|
49
|
+
return this;
|
|
50
|
+
}
|
|
51
|
+
}
|