@weborigami/async-tree 0.0.41 → 0.0.43

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/main.js CHANGED
@@ -6,6 +6,7 @@ export { default as FunctionTree } from "./src/FunctionTree.js";
6
6
  export { default as MapTree } from "./src/MapTree.js";
7
7
  export { default as ObjectTree } from "./src/ObjectTree.js";
8
8
  // Skip BrowserFileTree.js, which is browser-only.
9
+ export { default as DeepMapTree } from "./src/DeepMapTree.js";
9
10
  export { default as DeepObjectTree } from "./src/DeepObjectTree.js";
10
11
  export { default as SetTree } from "./src/SetTree.js";
11
12
  export { default as SiteTree } from "./src/SiteTree.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@weborigami/async-tree",
3
- "version": "0.0.41",
3
+ "version": "0.0.43",
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.3.3"
12
12
  },
13
13
  "dependencies": {
14
- "@weborigami/types": "0.0.41"
14
+ "@weborigami/types": "0.0.43"
15
15
  },
16
16
  "scripts": {
17
17
  "test": "node --test --test-reporter=spec",
@@ -0,0 +1,23 @@
1
+ import MapTree from "./MapTree.js";
2
+ import * as Tree from "./Tree.js";
3
+
4
+ export default class DeepMapTree extends MapTree {
5
+ async get(key) {
6
+ let value = await super.get(key);
7
+
8
+ if (value instanceof Map) {
9
+ value = Reflect.construct(this.constructor, [value]);
10
+ }
11
+
12
+ if (Tree.isAsyncTree(value) && !value.parent) {
13
+ value.parent = this;
14
+ }
15
+
16
+ return value;
17
+ }
18
+
19
+ async isKeyForSubtree(key) {
20
+ const value = this.map.get[key];
21
+ return value instanceof Map || Tree.isAsyncTree(value);
22
+ }
23
+ }
@@ -50,6 +50,23 @@ export default class DeferredTree {
50
50
  }
51
51
  }
52
52
 
53
+ // HACK: The concept of scope is defined in Origami, not at the AsyncTree
54
+ // level. If a DeferredTree is used to wrap an OrigamiTree, the inner
55
+ // OrigamiTree will have a `scope` but not a `parent`. If someone asks the
56
+ // outer deferrred tree for a scope, they'd otherwise get `undefined`, which
57
+ // is incorrect. As a workaround, we introduce a `scope` getter here that
58
+ // defers to the inner tree, but we need to find a way to avoid having to
59
+ // introduce the concept of scope here.
60
+ get scope() {
61
+ return /** @type {any} */ (this._tree)?.scope;
62
+ }
63
+ set scope(scope) {
64
+ // If tree hasn't been loaded yet, setting scope has no effect.
65
+ if (this._tree) {
66
+ /** @type {any} */ (this._tree).scope = scope;
67
+ }
68
+ }
69
+
53
70
  async tree() {
54
71
  if (this._tree) {
55
72
  return this._tree;
package/src/FileTree.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as fs from "node:fs/promises";
2
2
  import path from "node:path";
3
- import { fileURLToPath } from "node:url";
3
+ import { fileURLToPath, pathToFileURL } from "node:url";
4
4
  import * as Tree from "./Tree.js";
5
5
  import {
6
6
  getRealmObjectPrototype,
@@ -18,11 +18,25 @@ const TypedArray = Object.getPrototypeOf(Uint8Array);
18
18
  */
19
19
  export default class FileTree {
20
20
  /**
21
- * @param {string} location
21
+ * @param {string|URL} location
22
22
  */
23
23
  constructor(location) {
24
+ if (location instanceof URL) {
25
+ location = location.href;
26
+ } else if (
27
+ !(
28
+ typeof location === "string" ||
29
+ /** @type {any} */ (location) instanceof String
30
+ )
31
+ ) {
32
+ throw new TypeError(
33
+ `FileTree constructor needs a string or URL, received an instance of ${
34
+ /** @type {any} */ (location)?.constructor?.name
35
+ }`
36
+ );
37
+ }
24
38
  this.dirname = location.startsWith("file://")
25
- ? path.dirname(fileURLToPath(location))
39
+ ? fileURLToPath(location)
26
40
  : path.resolve(process.cwd(), location);
27
41
  this.parent = null;
28
42
  }
@@ -154,6 +168,10 @@ export default class FileTree {
154
168
 
155
169
  return this;
156
170
  }
171
+
172
+ get url() {
173
+ return pathToFileURL(this.dirname);
174
+ }
157
175
  }
158
176
 
159
177
  /**
package/src/SiteTree.js CHANGED
@@ -2,7 +2,9 @@ import * as Tree from "./Tree.js";
2
2
  import * as keysJson from "./keysJson.js";
3
3
 
4
4
  /**
5
- * An HTTP/HTTPS site as a tree of ArrayBuffers.
5
+ * A tree of values obtained via HTTP/HTTPS calls. These values will be strings
6
+ * for HTTP responses with a MIME text type; otherwise they will be ArrayBuffer
7
+ * instances.
6
8
  *
7
9
  * @typedef {import("@weborigami/types").AsyncTree} AsyncTree
8
10
  * @implements {AsyncTree}
@@ -64,7 +66,10 @@ export default class SiteTree {
64
66
  }
65
67
  }
66
68
 
67
- return response.arrayBuffer();
69
+ const mediaType = response.headers?.get("Content-Type");
70
+ return SiteTree.mediaTypeIsText(mediaType)
71
+ ? response.text()
72
+ : response.arrayBuffer();
68
73
  }
69
74
 
70
75
  async getKeyDictionary() {
@@ -106,6 +111,36 @@ export default class SiteTree {
106
111
  return keyDictionary ? Object.keys(keyDictionary) : [];
107
112
  }
108
113
 
114
+ // Return true if the given media type is a standard text type.
115
+ static mediaTypeIsText(mediaType) {
116
+ if (!mediaType) {
117
+ return false;
118
+ }
119
+ const regex = /^(?<type>[^/]+)\/(?<subtype>[^;]+)/;
120
+ const match = mediaType.match(regex);
121
+ if (!match) {
122
+ return false;
123
+ }
124
+ const { type, subtype } = match.groups;
125
+ if (type === "text") {
126
+ return true;
127
+ } else if (type === "application") {
128
+ return (
129
+ subtype === "json" ||
130
+ subtype.endsWith("+json") ||
131
+ subtype.endsWith(".json") ||
132
+ subtype === "xml" ||
133
+ subtype.endsWith("+xml") ||
134
+ subtype.endsWith(".xml")
135
+ );
136
+ }
137
+ return false;
138
+ }
139
+
140
+ get path() {
141
+ return this.href;
142
+ }
143
+
109
144
  /**
110
145
  * Returns a new `SiteTree` for the given relative route.
111
146
  *
@@ -121,4 +156,8 @@ export default class SiteTree {
121
156
  const response = await fetch(this.href);
122
157
  return response.ok ? response.arrayBuffer() : undefined;
123
158
  }
159
+
160
+ get url() {
161
+ return new URL(this.href);
162
+ }
124
163
  }
package/src/Tree.js CHANGED
@@ -338,6 +338,10 @@ export async function traverse(treelike, ...keys) {
338
338
  * @param {...any} keys
339
339
  */
340
340
  export async function traverseOrThrow(treelike, ...keys) {
341
+ if (!treelike) {
342
+ throw new TraverseError("Tried to traverse a null or undefined value");
343
+ }
344
+
341
345
  // Start our traversal at the root of the tree.
342
346
  /** @type {any} */
343
347
  let value = treelike;
@@ -348,15 +352,16 @@ export async function traverseOrThrow(treelike, ...keys) {
348
352
 
349
353
  // Process all the keys.
350
354
  const remainingKeys = keys.slice();
355
+ let key;
351
356
  while (remainingKeys.length > 0) {
352
357
  if (value === undefined) {
353
358
  // Attempted to traverse an undefined value
354
- const keyStrings = keys.map((key) => String(key));
355
- throw new TraverseError(
356
- `Couldn't traverse the path: ${keyStrings.join("/")}`,
357
- value,
358
- keys
359
- );
359
+ const message = key
360
+ ? `${key} does not exist`
361
+ : `Couldn't traverse the path: ${keys
362
+ .map((key) => String(key))
363
+ .join("/")}`;
364
+ throw new TraverseError(message, treelike, keys);
360
365
  }
361
366
 
362
367
  // Special case: one key left that's an empty string
@@ -382,12 +387,13 @@ export async function traverseOrThrow(treelike, ...keys) {
382
387
  // We'll take as many keys as the function's length, but at least one.
383
388
  let fnKeyCount = Math.max(fn.length, 1);
384
389
  const args = remainingKeys.splice(0, fnKeyCount);
390
+ key = null;
385
391
  value = await fn.call(target, ...args);
386
392
  } else {
387
393
  // Value is some other treelike object: cast it to a tree.
388
394
  const tree = from(value);
389
395
  // Get the next key.
390
- const key = remainingKeys.shift();
396
+ key = remainingKeys.shift();
391
397
  // Get the value for the key.
392
398
  value = await tree.get(key);
393
399
  }