@weborigami/async-tree 0.0.66-beta.2 → 0.0.67-beta.1

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,9 +6,9 @@ export { default as FunctionTree } from "./src/FunctionTree.js";
6
6
  export { default as MapTree } from "./src/MapTree.js";
7
7
  // Skip BrowserFileTree.js, which is browser-only.
8
8
  export { default as DeepMapTree } from "./src/DeepMapTree.js";
9
+ export { default as ExplorableSiteTree } from "./src/ExplorableSiteTree.js";
9
10
  export { DeepObjectTree, ObjectTree, Tree } from "./src/internal.js";
10
11
  export * as jsonKeys from "./src/jsonKeys.js";
11
- export { default as OpenSiteTree } from "./src/OpenSiteTree.js";
12
12
  export { default as cache } from "./src/operations/cache.js";
13
13
  export { default as concat } from "./src/operations/concat.js";
14
14
  export { default as deepMerge } from "./src/operations/deepMerge.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@weborigami/async-tree",
3
- "version": "0.0.66-beta.2",
3
+ "version": "0.0.67-beta.1",
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.6.2"
12
12
  },
13
13
  "dependencies": {
14
- "@weborigami/types": "0.0.66-beta.2"
14
+ "@weborigami/types": "0.0.67-beta.1"
15
15
  },
16
16
  "scripts": {
17
17
  "test": "node --test --test-reporter=spec",
@@ -14,12 +14,14 @@ import { Tree } from "./internal.js";
14
14
  export default class DeferredTree {
15
15
  /**
16
16
  * @param {Function|Promise<any>} loader
17
+ * @param {{ deep?: boolean }} [options]
17
18
  */
18
- constructor(loader) {
19
+ constructor(loader, options) {
19
20
  this.loader = loader;
20
21
  this.treePromise = null;
21
22
  this._tree = null;
22
23
  this._parentUntilLoaded = null;
24
+ this._deep = options?.deep ?? false;
23
25
  }
24
26
 
25
27
  async get(key) {
@@ -60,7 +62,7 @@ export default class DeferredTree {
60
62
 
61
63
  // Use a promise to ensure the treelike is only converted to a tree once.
62
64
  this.treePromise ??= this.loadResult().then((treelike) => {
63
- this._tree = Tree.from(treelike);
65
+ this._tree = Tree.from(treelike, { deep: this._deep });
64
66
  if (this._parentUntilLoaded) {
65
67
  // Now that the tree has been loaded, we can set its parent if it hasn't
66
68
  // already been set.
@@ -1,6 +1,11 @@
1
1
  import SiteTree from "./SiteTree.js";
2
2
 
3
- export default class OpenSiteTree extends SiteTree {
3
+ /**
4
+ * A [SiteTree](SiteTree.html) that implements the [JSON Keys](jsonKeys.html)
5
+ * protocol. This enables a `keys()` method that can return the keys of a site
6
+ * route even though such a mechanism is not built into the HTTP protocol.
7
+ */
8
+ export default class ExplorableSiteTree extends SiteTree {
4
9
  constructor(...args) {
5
10
  super(...args);
6
11
  this.serverKeysPromise = undefined;
@@ -24,6 +29,12 @@ export default class OpenSiteTree extends SiteTree {
24
29
  return this.serverKeysPromise;
25
30
  }
26
31
 
32
+ /**
33
+ * Returns the keys of the site route. For this to work, the route must have a
34
+ * `.keys.json` file that contains a JSON array of string keys.
35
+ *
36
+ * @returns {Promise<Iterable<string>>}
37
+ */
27
38
  async keys() {
28
39
  const serverKeys = await this.getServerKeys();
29
40
  return serverKeys ?? [];
package/src/FileTree.js CHANGED
@@ -190,8 +190,11 @@ export default class FileTree {
190
190
  // Special case: empty object means create an empty directory.
191
191
  await fs.mkdir(destPath, { recursive: true });
192
192
  } else if (Tree.isTreelike(value)) {
193
- // Treat value as a tree and write it out as a subdirectory.
193
+ // Treat value as a subtree and write it out as a subdirectory.
194
194
  const destTree = Reflect.construct(this.constructor, [destPath]);
195
+ // Create the directory here, even if the subtree is empty.
196
+ await fs.mkdir(destPath, { recursive: true });
197
+ // Write out the subtree.
195
198
  await Tree.assign(destTree, value);
196
199
  } else {
197
200
  const typeName = value?.constructor?.name ?? "unknown";
package/src/SiteTree.js CHANGED
@@ -35,14 +35,20 @@ export default class SiteTree {
35
35
  );
36
36
  }
37
37
 
38
- // If the keys ends with a slash, return a tree for the indicated route.
39
- if (trailingSlash.has(key)) {
38
+ // A key with a trailing slash and no extension is for a folder; return a
39
+ // subtree without making a network request.
40
+ if (trailingSlash.has(key) && !key.includes(".")) {
40
41
  const href = new URL(key, this.href).href;
41
42
  const value = Reflect.construct(this.constructor, [href]);
42
43
  setParent(value, this);
43
44
  return value;
44
45
  }
45
46
 
47
+ // HACK: For now we don't allow lookup of Origami extension handlers.
48
+ if (key.endsWith("_handler")) {
49
+ return undefined;
50
+ }
51
+
46
52
  const href = new URL(key, this.href).href;
47
53
 
48
54
  // Fetch the data at the given route.
@@ -56,6 +62,14 @@ export default class SiteTree {
56
62
  return this.processResponse(response);
57
63
  }
58
64
 
65
+ /**
66
+ * Returns an empty set of keys.
67
+ *
68
+ * For a variation of `SiteTree` that can return the keys for a site route,
69
+ * see [ExplorableSiteTree](ExplorableSiteTree.html).
70
+ *
71
+ * @returns {Promise<Iterable<string>>}
72
+ */
59
73
  async keys() {
60
74
  return [];
61
75
  }
package/src/Tree.js CHANGED
@@ -110,10 +110,11 @@ export async function forEach(tree, callbackFn) {
110
110
  * parent of the new tree.
111
111
  *
112
112
  * @param {Treelike | Object} object
113
- * @param {{ deep?: true, parent?: AsyncTree|null }} [options]
113
+ * @param {{ deep?: boolean, parent?: AsyncTree|null }} [options]
114
114
  * @returns {AsyncTree}
115
115
  */
116
116
  export function from(object, options = {}) {
117
+ const deep = options.deep ?? false;
117
118
  let tree;
118
119
  if (isAsyncTree(object)) {
119
120
  // Argument already supports the tree interface.
@@ -126,13 +127,13 @@ export function from(object, options = {}) {
126
127
  } else if (object instanceof Set) {
127
128
  tree = new SetTree(object);
128
129
  } else if (isPlainObject(object) || object instanceof Array) {
129
- tree = options.deep ? new DeepObjectTree(object) : new ObjectTree(object);
130
+ tree = deep ? new DeepObjectTree(object) : new ObjectTree(object);
130
131
  } else if (isUnpackable(object)) {
131
132
  async function AsyncFunction() {} // Sample async function
132
133
  tree =
133
134
  object.unpack instanceof AsyncFunction.constructor
134
135
  ? // Async unpack: return a deferred tree.
135
- new DeferredTree(object.unpack)
136
+ new DeferredTree(object.unpack, { deep })
136
137
  : // Synchronous unpack: cast the result of unpack() to a tree.
137
138
  from(object.unpack());
138
139
  } else if (object && typeof object === "object") {
@@ -320,6 +321,8 @@ export async function paths(treelike, base = "") {
320
321
  * The result's keys will be the tree's keys cast to strings. Any tree value
321
322
  * that is itself a tree will be similarly converted to a plain object.
322
323
  *
324
+ * Any trailing slashes in keys will be removed.
325
+ *
323
326
  * @param {Treelike} treelike
324
327
  * @returns {Promise<PlainObject|Array>}
325
328
  */
@@ -60,16 +60,22 @@ export default function treeCache(
60
60
  let value = await source.get(key);
61
61
  if (value !== undefined) {
62
62
  // If a filter is defined, does the key match the filter?
63
- const filterValue = await filter?.get(key);
63
+ const filterValue = filter ? await filter.get(key) : undefined;
64
64
  const filterMatch = !filter || filterValue !== undefined;
65
65
  if (filterMatch) {
66
66
  if (Tree.isAsyncTree(value)) {
67
67
  // Construct merged tree for a tree result.
68
68
  if (cacheValue === undefined) {
69
- // Construct new container in cache
70
- cacheValue = new ObjectTree({});
71
- cacheValue.parent = this;
72
- await cache.set(key, cacheValue);
69
+ // Construct new empty container in cache
70
+ await cache.set(key, {});
71
+ cacheValue = await cache.get(key);
72
+ if (!Tree.isAsyncTree(cacheValue)) {
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
+ }
73
79
  }
74
80
  value = treeCache(value, cacheValue, filterValue);
75
81
  } else {
@@ -1,6 +1,6 @@
1
1
  import assert from "node:assert";
2
2
  import { beforeEach, describe, mock, test } from "node:test";
3
- import OpenSiteTree from "../src/OpenSiteTree.js";
3
+ import ExplorableSiteTree from "../src/ExplorableSiteTree.js";
4
4
  import { Tree } from "../src/internal.js";
5
5
 
6
6
  const textDecoder = new TextDecoder();
@@ -34,31 +34,31 @@ const mockResponses = {
34
34
  },
35
35
  };
36
36
 
37
- describe("OpenSiteTree", () => {
37
+ describe("ExplorableSiteTree", () => {
38
38
  beforeEach(() => {
39
39
  mock.method(global, "fetch", mockFetch);
40
40
  });
41
41
 
42
42
  test("can get the keys of a tree", async () => {
43
- const fixture = new OpenSiteTree(mockHost);
43
+ const fixture = new ExplorableSiteTree(mockHost);
44
44
  const keys = await fixture.keys();
45
45
  assert.deepEqual(Array.from(keys), ["about/", "index.html"]);
46
46
  });
47
47
 
48
48
  test("can get a plain value for a key", async () => {
49
- const fixture = new OpenSiteTree(mockHost);
49
+ const fixture = new ExplorableSiteTree(mockHost);
50
50
  const arrayBuffer = await fixture.get("index.html");
51
51
  const text = textDecoder.decode(arrayBuffer);
52
52
  assert.equal(text, "Home page");
53
53
  });
54
54
 
55
55
  test("getting an unsupported key returns undefined", async () => {
56
- const fixture = new OpenSiteTree(mockHost);
56
+ const fixture = new ExplorableSiteTree(mockHost);
57
57
  assert.equal(await fixture.get("xyz"), undefined);
58
58
  });
59
59
 
60
60
  test("getting a null/undefined key throws an exception", async () => {
61
- const fixture = new OpenSiteTree(mockHost);
61
+ const fixture = new ExplorableSiteTree(mockHost);
62
62
  await assert.rejects(async () => {
63
63
  await fixture.get(null);
64
64
  });
@@ -68,14 +68,14 @@ describe("OpenSiteTree", () => {
68
68
  });
69
69
 
70
70
  test("can return a new tree for a key that redirects", async () => {
71
- const fixture = new OpenSiteTree(mockHost);
71
+ const fixture = new ExplorableSiteTree(mockHost);
72
72
  const about = await fixture.get("about");
73
- assert(about instanceof OpenSiteTree);
73
+ assert(about instanceof ExplorableSiteTree);
74
74
  assert.equal(about.href, "https://mock/about/");
75
75
  });
76
76
 
77
77
  test("can convert a site to a plain object", async () => {
78
- const fixture = new OpenSiteTree(mockHost);
78
+ const fixture = new ExplorableSiteTree(mockHost);
79
79
  // Convert buffers to strings.
80
80
  const strings = Tree.map(fixture, (value) => textDecoder.decode(value));
81
81
  assert.deepEqual(await Tree.plain(strings), {
@@ -4,7 +4,7 @@ import path from "node:path";
4
4
  import { describe, test } from "node:test";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import FileTree from "../src/FileTree.js";
7
- import { Tree } from "../src/internal.js";
7
+ import { ObjectTree, Tree } from "../src/internal.js";
8
8
 
9
9
  const dirname = path.dirname(fileURLToPath(import.meta.url));
10
10
  const tempDirectory = path.join(dirname, "fixtures/temp");
@@ -81,7 +81,7 @@ describe("FileTree", async () => {
81
81
  await removeTempDirectory();
82
82
  });
83
83
 
84
- test("can create empty subfolder via set()", async () => {
84
+ test("create subfolder via set() with empty object value", async () => {
85
85
  await createTempDirectory();
86
86
 
87
87
  // Write out new, empty folder called "empty".
@@ -98,6 +98,23 @@ describe("FileTree", async () => {
98
98
  await removeTempDirectory();
99
99
  });
100
100
 
101
+ test("create subfolder via set() with empty tree value", async () => {
102
+ await createTempDirectory();
103
+
104
+ // Write out new, empty folder called "empty".
105
+ const tempFiles = new FileTree(tempDirectory);
106
+ await tempFiles.set("empty", new ObjectTree({}));
107
+
108
+ // Verify folder exists and has no contents.
109
+ const folderPath = path.join(tempDirectory, "empty");
110
+ const stats = await fs.stat(folderPath);
111
+ assert(stats.isDirectory());
112
+ const files = await fs.readdir(folderPath);
113
+ assert.deepEqual(files, []);
114
+
115
+ await removeTempDirectory();
116
+ });
117
+
101
118
  test("can write out subfolder via set()", async () => {
102
119
  await createTempDirectory();
103
120