@weborigami/async-tree 0.2.4 → 0.2.6
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/LICENSE +1 -1
- package/package.json +2 -2
- package/shared.js +2 -0
- package/src/Tree.js +3 -10
- package/src/drivers/constantTree.js +19 -0
- package/src/operations/cache.js +18 -36
- package/src/operations/cachedKeyFunctions.js +5 -7
- package/src/operations/concat.js +4 -2
- package/src/operations/deepMerge.js +6 -3
- package/src/operations/filter.js +51 -0
- package/src/operations/globKeys.js +87 -0
- package/src/operations/map.js +31 -14
- package/src/operations/regExpKeys.js +2 -9
- package/src/utilities.js +6 -2
- package/test/drivers/constantTree.test.js +13 -0
- package/test/operations/cache.test.js +1 -25
- package/test/operations/filter.test.js +32 -0
- package/test/operations/globKeys.test.js +53 -0
- package/test/operations/regExpKeys.test.js +8 -4
- package/test/utilities.test.js +1 -0
package/LICENSE
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
MIT License
|
|
2
2
|
|
|
3
|
-
Copyright (c)
|
|
3
|
+
Copyright (c) 2025 Jan Miksovsky and other contributors
|
|
4
4
|
|
|
5
5
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
6
|
of this software and associated documentation files (the "Software"), to deal
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@weborigami/async-tree",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.6",
|
|
4
4
|
"description": "Asynchronous tree drivers based on standard JavaScript classes",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./main.js",
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"typescript": "5.7.2"
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
|
-
"@weborigami/types": "0.2.
|
|
14
|
+
"@weborigami/types": "0.2.6"
|
|
15
15
|
},
|
|
16
16
|
"scripts": {
|
|
17
17
|
"test": "node --test --test-reporter=spec",
|
package/shared.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Exports for both Node.js and browser
|
|
2
2
|
|
|
3
3
|
export { default as calendarTree } from "./src/drivers/calendarTree.js";
|
|
4
|
+
export { default as constantTree } from "./src/drivers/constantTree.js";
|
|
4
5
|
export { default as DeepMapTree } from "./src/drivers/DeepMapTree.js";
|
|
5
6
|
export { default as DeferredTree } from "./src/drivers/DeferredTree.js";
|
|
6
7
|
export { default as ExplorableSiteTree } from "./src/drivers/ExplorableSiteTree.js";
|
|
@@ -18,6 +19,7 @@ export { default as deepReverse } from "./src/operations/deepReverse.js";
|
|
|
18
19
|
export { default as deepTake } from "./src/operations/deepTake.js";
|
|
19
20
|
export { default as deepValues } from "./src/operations/deepValues.js";
|
|
20
21
|
export { default as deepValuesIterator } from "./src/operations/deepValuesIterator.js";
|
|
22
|
+
export { default as filter } from "./src/operations/filter.js";
|
|
21
23
|
export { default as group } from "./src/operations/group.js";
|
|
22
24
|
export { default as invokeFunctions } from "./src/operations/invokeFunctions.js";
|
|
23
25
|
export { default as keyFunctionsForExtensions } from "./src/operations/keyFunctionsForExtensions.js";
|
package/src/Tree.js
CHANGED
|
@@ -422,7 +422,7 @@ export async function traverseOrThrow(treelike, ...keys) {
|
|
|
422
422
|
const remainingKeys = keys.slice();
|
|
423
423
|
let key;
|
|
424
424
|
while (remainingKeys.length > 0) {
|
|
425
|
-
if (value
|
|
425
|
+
if (value == null) {
|
|
426
426
|
throw new TraverseError("A null or undefined value can't be traversed", {
|
|
427
427
|
tree: treelike,
|
|
428
428
|
keys,
|
|
@@ -443,20 +443,13 @@ export async function traverseOrThrow(treelike, ...keys) {
|
|
|
443
443
|
const args = remainingKeys.splice(0, fnKeyCount);
|
|
444
444
|
key = null;
|
|
445
445
|
value = await fn.call(target, ...args);
|
|
446
|
-
} else
|
|
447
|
-
//
|
|
446
|
+
} else {
|
|
447
|
+
// Cast value to a tree.
|
|
448
448
|
const tree = from(value);
|
|
449
449
|
// Get the next key.
|
|
450
450
|
key = remainingKeys.shift();
|
|
451
451
|
// Get the value for the key.
|
|
452
452
|
value = await tree.get(key);
|
|
453
|
-
} else {
|
|
454
|
-
// Value can't be traversed
|
|
455
|
-
throw new TraverseError("Tried to traverse a value that's not treelike", {
|
|
456
|
-
tree: treelike,
|
|
457
|
-
keys,
|
|
458
|
-
position,
|
|
459
|
-
});
|
|
460
453
|
}
|
|
461
454
|
|
|
462
455
|
position++;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { trailingSlash } from "../../main.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A tree that returns a constant value for any key. If the key ends with a
|
|
5
|
+
* slash, then the same type of subtree is returned.
|
|
6
|
+
*
|
|
7
|
+
* @param {any} constant
|
|
8
|
+
*/
|
|
9
|
+
export default function constantTree(constant) {
|
|
10
|
+
return {
|
|
11
|
+
async get(key) {
|
|
12
|
+
return trailingSlash.has(key) ? constantTree(constant) : constant;
|
|
13
|
+
},
|
|
14
|
+
|
|
15
|
+
async keys() {
|
|
16
|
+
return [];
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
}
|
package/src/operations/cache.js
CHANGED
|
@@ -6,23 +6,15 @@ import { ObjectTree, Tree } from "../internal.js";
|
|
|
6
6
|
*
|
|
7
7
|
* If no second tree is supplied, an in-memory value cache is used.
|
|
8
8
|
*
|
|
9
|
-
* An optional third filter tree can be supplied. If a filter tree is supplied,
|
|
10
|
-
* only values for keys that match the filter will be cached.
|
|
11
|
-
*
|
|
12
9
|
* @typedef {import("@weborigami/types").AsyncTree} AsyncTree
|
|
13
10
|
* @typedef {import("@weborigami/types").AsyncMutableTree} AsyncMutableTree
|
|
14
11
|
* @typedef {import("../../index.ts").Treelike} Treelike
|
|
15
12
|
*
|
|
16
13
|
* @param {Treelike} sourceTreelike
|
|
17
14
|
* @param {AsyncMutableTree} [cacheTreelike]
|
|
18
|
-
* @param {Treelike} [filterTreelike]
|
|
19
15
|
* @returns {AsyncTree & { description: string }}
|
|
20
16
|
*/
|
|
21
|
-
export default function treeCache(
|
|
22
|
-
sourceTreelike,
|
|
23
|
-
cacheTreelike,
|
|
24
|
-
filterTreelike
|
|
25
|
-
) {
|
|
17
|
+
export default function treeCache(sourceTreelike, cacheTreelike) {
|
|
26
18
|
if (!sourceTreelike) {
|
|
27
19
|
const error = new TypeError(`cache: The source tree isn't defined.`);
|
|
28
20
|
/** @type {any} */ (error).position = 0;
|
|
@@ -30,7 +22,6 @@ export default function treeCache(
|
|
|
30
22
|
}
|
|
31
23
|
|
|
32
24
|
const source = Tree.from(sourceTreelike);
|
|
33
|
-
const filter = filterTreelike ? Tree.from(filterTreelike) : undefined;
|
|
34
25
|
|
|
35
26
|
/** @type {AsyncMutableTree} */
|
|
36
27
|
let cache;
|
|
@@ -58,36 +49,27 @@ export default function treeCache(
|
|
|
58
49
|
|
|
59
50
|
// Cache miss or interior node cache hit.
|
|
60
51
|
let value = await source.get(key);
|
|
61
|
-
if (value
|
|
62
|
-
//
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
// Coerce to tree and then save it back to the cache. This is
|
|
74
|
-
// necessary, e.g., if cache is an ObjectTree; we want the
|
|
75
|
-
// subtree to also be an ObjectTree, not a plain object.
|
|
76
|
-
cacheValue = Tree.from(cacheValue);
|
|
77
|
-
await cache.set(key, cacheValue);
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
value = treeCache(value, cacheValue, filterValue);
|
|
81
|
-
} else {
|
|
82
|
-
// Save in cache before returning.
|
|
83
|
-
await cache.set(key, value);
|
|
52
|
+
if (Tree.isAsyncTree(value)) {
|
|
53
|
+
// Construct merged tree for a tree result.
|
|
54
|
+
if (cacheValue === undefined) {
|
|
55
|
+
// Construct new empty container in cache
|
|
56
|
+
await cache.set(key, {});
|
|
57
|
+
cacheValue = await cache.get(key);
|
|
58
|
+
if (!Tree.isAsyncTree(cacheValue)) {
|
|
59
|
+
// Coerce to tree and then save it back to the cache. This is
|
|
60
|
+
// necessary, e.g., if cache is an ObjectTree; we want the
|
|
61
|
+
// subtree to also be an ObjectTree, not a plain object.
|
|
62
|
+
cacheValue = Tree.from(cacheValue);
|
|
63
|
+
await cache.set(key, cacheValue);
|
|
84
64
|
}
|
|
85
65
|
}
|
|
86
|
-
|
|
87
|
-
|
|
66
|
+
value = treeCache(value, cacheValue);
|
|
67
|
+
} else if (value !== undefined) {
|
|
68
|
+
// Save in cache before returning.
|
|
69
|
+
await cache.set(key, value);
|
|
88
70
|
}
|
|
89
71
|
|
|
90
|
-
return
|
|
72
|
+
return value;
|
|
91
73
|
},
|
|
92
74
|
|
|
93
75
|
async keys() {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as trailingSlash from "../trailingSlash.js";
|
|
2
2
|
|
|
3
|
-
const treeToCaches = new
|
|
3
|
+
const treeToCaches = new Map();
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Given a key function, return a new key function and inverse key function that
|
|
@@ -111,12 +111,10 @@ function searchKeyMap(keyMap, key) {
|
|
|
111
111
|
let match;
|
|
112
112
|
if (keyMap.has(key)) {
|
|
113
113
|
match = keyMap.get(key);
|
|
114
|
-
} else
|
|
115
|
-
// Check
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
match = keyMap.get(withSlash);
|
|
119
|
-
}
|
|
114
|
+
} else {
|
|
115
|
+
// Check alternative with/without slash
|
|
116
|
+
const alternativeKey = trailingSlash.toggle(key, !trailingSlash.has(key));
|
|
117
|
+
match = keyMap.get(alternativeKey);
|
|
120
118
|
}
|
|
121
119
|
return match
|
|
122
120
|
? trailingSlash.toggle(match, trailingSlash.has(key))
|
package/src/operations/concat.js
CHANGED
|
@@ -20,14 +20,16 @@ export default async function concatTreeValues(treelike) {
|
|
|
20
20
|
for await (const value of deepValuesIterator(treelike, { expand: true })) {
|
|
21
21
|
let string;
|
|
22
22
|
if (value === null) {
|
|
23
|
-
console.warn("Warning: Origami template encountered a null value");
|
|
24
23
|
string = "null";
|
|
25
24
|
} else if (value === undefined) {
|
|
26
|
-
console.warn("Warning: Origami template encountered an undefined value");
|
|
27
25
|
string = "undefined";
|
|
28
26
|
} else {
|
|
29
27
|
string = toString(value);
|
|
30
28
|
}
|
|
29
|
+
if (value === null || value === undefined) {
|
|
30
|
+
const message = `Warning: Origami template encountered a ${string} value. To locate where this happened, build your project and search your build output for the text "${string}".`;
|
|
31
|
+
console.warn(message);
|
|
32
|
+
}
|
|
31
33
|
strings.push(string);
|
|
32
34
|
}
|
|
33
35
|
return strings.join("");
|
|
@@ -21,10 +21,13 @@ export default function deepMerge(...sources) {
|
|
|
21
21
|
for (let index = trees.length - 1; index >= 0; index--) {
|
|
22
22
|
const tree = trees[index];
|
|
23
23
|
const value = await tree.get(key);
|
|
24
|
-
if (
|
|
25
|
-
|
|
24
|
+
if (
|
|
25
|
+
Tree.isAsyncTree(value) ||
|
|
26
|
+
(Tree.isTreelike(value) && trailingSlash.has(key))
|
|
27
|
+
) {
|
|
28
|
+
if (/** @type {any} */ (value).parent === tree) {
|
|
26
29
|
// Merged tree acts as parent instead of the source tree.
|
|
27
|
-
value.parent = this;
|
|
30
|
+
/** @type {any} */ (value).parent = this;
|
|
28
31
|
}
|
|
29
32
|
subtrees.unshift(value);
|
|
30
33
|
} else if (value !== undefined) {
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { trailingSlash, Tree } from "@weborigami/async-tree";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Given trees `a` and `b`, return a filtered version of `a` where only the keys
|
|
5
|
+
* that exist in `b` and have truthy values are kept. The filter operation is
|
|
6
|
+
* deep: if a value from `a` is a subtree, it will be filtered recursively.
|
|
7
|
+
*
|
|
8
|
+
* @typedef {import("@weborigami/types").AsyncTree} AsyncTree
|
|
9
|
+
* @typedef {import("../../index.ts").Treelike} Treelike
|
|
10
|
+
*
|
|
11
|
+
* @param {Treelike} a
|
|
12
|
+
* @param {Treelike} b
|
|
13
|
+
* @returns {AsyncTree}
|
|
14
|
+
*/
|
|
15
|
+
export default function filter(a, b) {
|
|
16
|
+
a = Tree.from(a);
|
|
17
|
+
b = Tree.from(b, { deep: true });
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
async get(key) {
|
|
21
|
+
// The key must exist in b and return a truthy value
|
|
22
|
+
const bValue = await b.get(key);
|
|
23
|
+
if (!bValue) {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
let aValue = await a.get(key);
|
|
27
|
+
if (Tree.isTreelike(aValue)) {
|
|
28
|
+
// Filter the subtree
|
|
29
|
+
return filter(aValue, bValue);
|
|
30
|
+
} else {
|
|
31
|
+
return aValue;
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
async keys() {
|
|
36
|
+
// Use a's keys as the basis
|
|
37
|
+
const aKeys = [...(await a.keys())];
|
|
38
|
+
const bValues = await Promise.all(aKeys.map((key) => b.get(key)));
|
|
39
|
+
// An async tree value in b implies that the a key should have a slash
|
|
40
|
+
const aKeySlashes = aKeys.map((key, index) =>
|
|
41
|
+
trailingSlash.toggle(
|
|
42
|
+
key,
|
|
43
|
+
trailingSlash.has(key) || Tree.isAsyncTree(bValues[index])
|
|
44
|
+
)
|
|
45
|
+
);
|
|
46
|
+
// Remove keys that don't have values in b
|
|
47
|
+
const keys = aKeySlashes.filter((key, index) => bValues[index] ?? false);
|
|
48
|
+
return keys;
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { ObjectTree, Tree, merge, trailingSlash } from "@weborigami/async-tree";
|
|
2
|
+
|
|
3
|
+
const globstar = "**";
|
|
4
|
+
const globstarSlash = `${globstar}/`;
|
|
5
|
+
|
|
6
|
+
export default function globKeys(treelike) {
|
|
7
|
+
const globs = Tree.from(treelike, { deep: true });
|
|
8
|
+
return {
|
|
9
|
+
async get(key) {
|
|
10
|
+
if (typeof key !== "string") {
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let value = await matchGlobs(globs, key);
|
|
15
|
+
if (Tree.isAsyncTree(value)) {
|
|
16
|
+
value = globKeys(value);
|
|
17
|
+
}
|
|
18
|
+
return value;
|
|
19
|
+
},
|
|
20
|
+
|
|
21
|
+
async keys() {
|
|
22
|
+
return globs.keys();
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Convert the glob to a regular expression
|
|
28
|
+
function matchGlob(glob, key) {
|
|
29
|
+
const regexText = glob
|
|
30
|
+
// Escape special regex characters
|
|
31
|
+
.replace(/[+?^${}()|\.\[\]\\]/g, "\\$&")
|
|
32
|
+
// Replace the glob wildcards with regex wildcards
|
|
33
|
+
.replace(/\*/g, ".*")
|
|
34
|
+
.replace(/\?/g, ".");
|
|
35
|
+
const regex = new RegExp(`^${regexText}$`);
|
|
36
|
+
return regex.test(key);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function matchGlobs(globs, key) {
|
|
40
|
+
let globstarGlobs;
|
|
41
|
+
|
|
42
|
+
// Collect all matches
|
|
43
|
+
let matches = [];
|
|
44
|
+
for (let glob of await globs.keys()) {
|
|
45
|
+
if (glob === globstarSlash) {
|
|
46
|
+
// Remember for later
|
|
47
|
+
globstarGlobs = await globs.get(glob);
|
|
48
|
+
if (trailingSlash.has(key)) {
|
|
49
|
+
// A key for a subtree matches the globstar
|
|
50
|
+
matches.push(new ObjectTree({ [globstar]: globstarGlobs }));
|
|
51
|
+
}
|
|
52
|
+
} else if (matchGlob(glob, key)) {
|
|
53
|
+
// Text matches glob, get value
|
|
54
|
+
const globValue = await globs.get(glob);
|
|
55
|
+
if (globValue !== undefined) {
|
|
56
|
+
if (!Tree.isAsyncTree(globValue)) {
|
|
57
|
+
// Found a non-tree match, return immediately
|
|
58
|
+
return globValue;
|
|
59
|
+
}
|
|
60
|
+
// Add to matches
|
|
61
|
+
matches.push(globValue);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// If we don't have a match yet, try globstar
|
|
67
|
+
if (matches.length === 0) {
|
|
68
|
+
if (!globstarGlobs) {
|
|
69
|
+
// No matches
|
|
70
|
+
return undefined;
|
|
71
|
+
} else {
|
|
72
|
+
// Try globstar
|
|
73
|
+
const globstarValue = await matchGlobs(globstarGlobs, key);
|
|
74
|
+
if (!Tree.isAsyncTree(globstarValue)) {
|
|
75
|
+
// Found a non-tree match, return immediately
|
|
76
|
+
return globstarValue;
|
|
77
|
+
} else if (trailingSlash.has(key)) {
|
|
78
|
+
// No match but key is for subtree, return globstar tree
|
|
79
|
+
return new ObjectTree({ [globstar]: globstarGlobs });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Merge all matches
|
|
85
|
+
const value = matches.length === 1 ? matches[0] : merge(...matches);
|
|
86
|
+
return value;
|
|
87
|
+
}
|
package/src/operations/map.js
CHANGED
|
@@ -65,37 +65,54 @@ export default function map(treelike, options = {}) {
|
|
|
65
65
|
|
|
66
66
|
if (keyFn || valueFn) {
|
|
67
67
|
transformed.get = async (resultKey) => {
|
|
68
|
-
|
|
69
|
-
|
|
68
|
+
if (resultKey === undefined) {
|
|
69
|
+
throw new ReferenceError(`map: Cannot get an undefined key.`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Step 1: Map the result key to the source key
|
|
73
|
+
let sourceKey = await inverseKeyFn?.(resultKey, tree);
|
|
70
74
|
|
|
71
75
|
if (sourceKey === undefined) {
|
|
72
|
-
|
|
73
|
-
|
|
76
|
+
if (deep && trailingSlash.has(resultKey)) {
|
|
77
|
+
// Special case: deep tree and value is expected to be a subtree
|
|
78
|
+
const sourceValue = await tree.get(resultKey);
|
|
79
|
+
// If we did get a subtree, apply the map to it
|
|
80
|
+
const resultValue = Tree.isAsyncTree(sourceValue)
|
|
81
|
+
? mapFn(sourceValue)
|
|
82
|
+
: undefined;
|
|
83
|
+
return resultValue;
|
|
84
|
+
} else {
|
|
85
|
+
// No inverseKeyFn, or it returned undefined; use resultKey
|
|
86
|
+
sourceKey = resultKey;
|
|
87
|
+
}
|
|
74
88
|
}
|
|
75
89
|
|
|
76
|
-
//
|
|
90
|
+
// Regular path: map a single value
|
|
91
|
+
|
|
92
|
+
// Step 2: Get the source value
|
|
77
93
|
let sourceValue;
|
|
78
94
|
if (needsSourceValue) {
|
|
79
|
-
// Normal case: get the value from the source tree
|
|
95
|
+
// Normal case: get the value from the source tree
|
|
80
96
|
sourceValue = await tree.get(sourceKey);
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
97
|
+
if (deep && sourceValue === undefined) {
|
|
98
|
+
// Key might be for a subtree, see if original key exists
|
|
99
|
+
sourceValue = await tree.get(resultKey);
|
|
100
|
+
}
|
|
84
101
|
}
|
|
85
102
|
|
|
86
|
-
// Step 3: Map the source value to the result value
|
|
103
|
+
// Step 3: Map the source value to the result value
|
|
87
104
|
let resultValue;
|
|
88
105
|
if (needsSourceValue && sourceValue === undefined) {
|
|
89
|
-
// No source value means no result value
|
|
106
|
+
// No source value means no result value
|
|
90
107
|
resultValue = undefined;
|
|
91
108
|
} else if (deep && Tree.isAsyncTree(sourceValue)) {
|
|
92
|
-
//
|
|
109
|
+
// We weren't expecting a subtree but got one; map it
|
|
93
110
|
resultValue = mapFn(sourceValue);
|
|
94
111
|
} else if (valueFn) {
|
|
95
|
-
// Map a single value
|
|
112
|
+
// Map a single value
|
|
96
113
|
resultValue = await valueFn(sourceValue, sourceKey, tree);
|
|
97
114
|
} else {
|
|
98
|
-
// Return source value as is
|
|
115
|
+
// Return source value as is
|
|
99
116
|
resultValue = sourceValue;
|
|
100
117
|
}
|
|
101
118
|
|
|
@@ -33,7 +33,7 @@ export default async function regExpKeys(treelike) {
|
|
|
33
33
|
if (key == null) {
|
|
34
34
|
// Reject nullish key.
|
|
35
35
|
throw new ReferenceError(
|
|
36
|
-
|
|
36
|
+
`regExpKeys: Cannot get a null or undefined key.`
|
|
37
37
|
);
|
|
38
38
|
}
|
|
39
39
|
|
|
@@ -72,14 +72,7 @@ export default async function regExpKeys(treelike) {
|
|
|
72
72
|
}
|
|
73
73
|
} else {
|
|
74
74
|
// Construct regular expression.
|
|
75
|
-
|
|
76
|
-
if (!text.startsWith("^")) {
|
|
77
|
-
text = "^" + text;
|
|
78
|
-
}
|
|
79
|
-
if (!text.endsWith("$")) {
|
|
80
|
-
text = text + "$";
|
|
81
|
-
}
|
|
82
|
-
regExp = new RegExp(text);
|
|
75
|
+
regExp = new RegExp(key);
|
|
83
76
|
}
|
|
84
77
|
map.set(regExp, value);
|
|
85
78
|
}
|
package/src/utilities.js
CHANGED
|
@@ -219,8 +219,12 @@ export const naturalOrder = new Intl.Collator(undefined, {
|
|
|
219
219
|
* @param {string[]} keys
|
|
220
220
|
*/
|
|
221
221
|
export function pathFromKeys(keys) {
|
|
222
|
-
|
|
223
|
-
|
|
222
|
+
// Ensure there's a slash between all keys. If the last key has a trailing
|
|
223
|
+
// slash, leave it there.
|
|
224
|
+
const normalized = keys.map((key, index) =>
|
|
225
|
+
index < keys.length - 1 ? trailingSlash.add(key) : key
|
|
226
|
+
);
|
|
227
|
+
return normalized.join("");
|
|
224
228
|
}
|
|
225
229
|
|
|
226
230
|
/**
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Tree } from "@weborigami/async-tree";
|
|
2
|
+
import assert from "node:assert";
|
|
3
|
+
import { describe, test } from "node:test";
|
|
4
|
+
import constantTree from "../../src/drivers/constantTree.js";
|
|
5
|
+
|
|
6
|
+
describe("constantTree", () => {
|
|
7
|
+
test("returns a deep tree that returns constant for all keys", async () => {
|
|
8
|
+
const fixture = constantTree(1);
|
|
9
|
+
assert.equal(await fixture.get("a"), 1);
|
|
10
|
+
assert.equal(await fixture.get("b"), 1);
|
|
11
|
+
assert.equal(await Tree.traverse(fixture, "c/", "d/", "e"), 1);
|
|
12
|
+
});
|
|
13
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import assert from "node:assert";
|
|
2
2
|
import { describe, test } from "node:test";
|
|
3
|
-
import { DeepObjectTree, ObjectTree
|
|
3
|
+
import { DeepObjectTree, ObjectTree } from "../../src/internal.js";
|
|
4
4
|
import cache from "../../src/operations/cache.js";
|
|
5
5
|
|
|
6
6
|
describe("cache", () => {
|
|
@@ -36,28 +36,4 @@ describe("cache", () => {
|
|
|
36
36
|
const moreCache = await objectCache.get("more");
|
|
37
37
|
assert.equal(await moreCache.get("d"), 4);
|
|
38
38
|
});
|
|
39
|
-
|
|
40
|
-
test("if a cache filter is supplied, it only caches values whose keys match the filter", async () => {
|
|
41
|
-
const objectCache = new ObjectTree({});
|
|
42
|
-
const fixture = cache(
|
|
43
|
-
Tree.from({
|
|
44
|
-
"a.txt": "a",
|
|
45
|
-
"b.txt": "b",
|
|
46
|
-
}),
|
|
47
|
-
objectCache,
|
|
48
|
-
Tree.from({
|
|
49
|
-
"a.txt": true,
|
|
50
|
-
})
|
|
51
|
-
);
|
|
52
|
-
|
|
53
|
-
// Access some values to populate the cache.
|
|
54
|
-
assert.equal(await fixture.get("a.txt"), "a");
|
|
55
|
-
assert.equal(await fixture.get("b.txt"), "b");
|
|
56
|
-
|
|
57
|
-
// The a.txt value should be cached because it matches the filter.
|
|
58
|
-
assert.equal(await objectCache.get("a.txt"), "a");
|
|
59
|
-
|
|
60
|
-
// The b.txt value should not be cached because it does not match the filter.
|
|
61
|
-
assert.equal(await objectCache.get("b.txt"), undefined);
|
|
62
|
-
});
|
|
63
39
|
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import assert from "node:assert";
|
|
2
|
+
import { describe, test } from "node:test";
|
|
3
|
+
import { Tree } from "../../src/internal.js";
|
|
4
|
+
import filter from "../../src/operations/filter.js";
|
|
5
|
+
|
|
6
|
+
describe("filter", () => {
|
|
7
|
+
test("removes keys and values whose filter values are falsy", async () => {
|
|
8
|
+
const result = filter(
|
|
9
|
+
{
|
|
10
|
+
a: 1,
|
|
11
|
+
b: 2,
|
|
12
|
+
c: {
|
|
13
|
+
d: 3,
|
|
14
|
+
e: 4,
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
a: true,
|
|
19
|
+
c: {
|
|
20
|
+
d: true,
|
|
21
|
+
},
|
|
22
|
+
}
|
|
23
|
+
);
|
|
24
|
+
assert.deepEqual(await result.keys(), ["a", "c/"]);
|
|
25
|
+
assert.deepEqual(await Tree.plain(result), {
|
|
26
|
+
a: 1,
|
|
27
|
+
c: {
|
|
28
|
+
d: 3,
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { Tree } from "@weborigami/async-tree";
|
|
2
|
+
import assert from "node:assert";
|
|
3
|
+
import { describe, test } from "node:test";
|
|
4
|
+
import globKeys from "../../src/operations/globKeys.js";
|
|
5
|
+
|
|
6
|
+
describe("globKeys", () => {
|
|
7
|
+
test("matches globs", async () => {
|
|
8
|
+
const globTree = globKeys({
|
|
9
|
+
"*.txt": true,
|
|
10
|
+
"*.js": false,
|
|
11
|
+
"*.jsx": true,
|
|
12
|
+
"foo*baz": true,
|
|
13
|
+
});
|
|
14
|
+
assert(await globTree.get("file.txt"));
|
|
15
|
+
assert(!(await globTree.get("script.js")));
|
|
16
|
+
assert(await globTree.get("component.jsx"));
|
|
17
|
+
assert(await globTree.get("foobarbaz"));
|
|
18
|
+
assert(await globTree.get("foobaz"));
|
|
19
|
+
assert(!(await globTree.get("foo")));
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("matches nested globs", async () => {
|
|
23
|
+
const globTree = globKeys({
|
|
24
|
+
sub: {
|
|
25
|
+
foo: "bar",
|
|
26
|
+
"*": "default",
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
assert.equal(await Tree.traverse(globTree, "sub/", "file"), "default");
|
|
30
|
+
assert.equal(await Tree.traverse(globTree, "sub/", "foo"), "bar");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("supports deep matches with globstar", async () => {
|
|
34
|
+
const globTree = globKeys({
|
|
35
|
+
"**": {
|
|
36
|
+
"*.txt": true, // More specific glob pattern must come
|
|
37
|
+
"*": false,
|
|
38
|
+
},
|
|
39
|
+
sub: {
|
|
40
|
+
"*.md": true,
|
|
41
|
+
},
|
|
42
|
+
"s*": {
|
|
43
|
+
"*.html": true,
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
assert.equal(await Tree.traverse(globTree, "a/", "b/", "foo.txt"), true);
|
|
47
|
+
assert.equal(await Tree.traverse(globTree, "c/", "foo"), false);
|
|
48
|
+
assert.equal(await Tree.traverse(globTree, "sub/", "file.md"), true);
|
|
49
|
+
assert.equal(await Tree.traverse(globTree, "sub/", "file.txt"), true);
|
|
50
|
+
assert.equal(await Tree.traverse(globTree, "sub/", "file.html"), true);
|
|
51
|
+
assert.equal(await Tree.traverse(globTree, "sub/", "file"), false);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -7,19 +7,23 @@ describe("regExpKeys", () => {
|
|
|
7
7
|
test("matches keys using regular expressions", async () => {
|
|
8
8
|
const fixture = await regExpKeys(
|
|
9
9
|
new DeepObjectTree({
|
|
10
|
-
a: true,
|
|
11
|
-
"b.*": true,
|
|
10
|
+
"^a$": true,
|
|
11
|
+
"^b.*": true,
|
|
12
12
|
c: {
|
|
13
13
|
d: true,
|
|
14
14
|
"e*": true,
|
|
15
15
|
},
|
|
16
|
+
f: true,
|
|
16
17
|
})
|
|
17
18
|
);
|
|
18
19
|
assert(await Tree.traverse(fixture, "a"));
|
|
19
20
|
assert(!(await Tree.traverse(fixture, "alice")));
|
|
20
21
|
assert(await Tree.traverse(fixture, "bob"));
|
|
21
22
|
assert(await Tree.traverse(fixture, "brenda"));
|
|
22
|
-
assert(await Tree.traverse(fixture, "c", "d"));
|
|
23
|
-
assert(await Tree.traverse(fixture, "c", "eee"));
|
|
23
|
+
assert(await Tree.traverse(fixture, "c/", "d"));
|
|
24
|
+
assert(await Tree.traverse(fixture, "c/", "eee"));
|
|
25
|
+
assert(await Tree.traverse(fixture, "f"));
|
|
26
|
+
assert(await Tree.traverse(fixture, "stef")); // contains "f"
|
|
27
|
+
assert(!(await Tree.traverse(fixture, "gail")));
|
|
24
28
|
});
|
|
25
29
|
});
|
package/test/utilities.test.js
CHANGED
|
@@ -59,6 +59,7 @@ describe("utilities", () => {
|
|
|
59
59
|
assert.equal(utilities.pathFromKeys([]), "");
|
|
60
60
|
assert.equal(utilities.pathFromKeys(["a", "b", "c"]), "a/b/c");
|
|
61
61
|
assert.equal(utilities.pathFromKeys(["a/", "b/", "c"]), "a/b/c");
|
|
62
|
+
assert.equal(utilities.pathFromKeys(["a/", "b/", "c/"]), "a/b/c/");
|
|
62
63
|
});
|
|
63
64
|
|
|
64
65
|
test("pipeline applies a series of functions to a value", async () => {
|