@weborigami/language 0.5.8 → 0.6.0

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.
Files changed (47) hide show
  1. package/index.ts +3 -4
  2. package/main.js +1 -3
  3. package/package.json +4 -5
  4. package/src/handlers/csv_handler.js +13 -8
  5. package/src/handlers/tsv_handler.js +10 -1
  6. package/src/project/jsGlobals.js +5 -1
  7. package/src/project/projectConfig.js +4 -4
  8. package/src/project/projectRoot.js +8 -9
  9. package/src/protocols/constructSiteTree.js +4 -4
  10. package/src/protocols/explore.js +2 -3
  11. package/src/protocols/fetchAndHandleExtension.js +0 -1
  12. package/src/protocols/files.js +2 -3
  13. package/src/protocols/http.js +0 -1
  14. package/src/protocols/https.js +0 -1
  15. package/src/protocols/httpstree.js +2 -3
  16. package/src/protocols/httptree.js +2 -3
  17. package/src/runtime/HandleExtensionsTransform.js +32 -8
  18. package/src/runtime/ImportModulesMixin.js +2 -2
  19. package/src/runtime/{OrigamiFiles.js → OrigamiFileMap.d.ts} +3 -3
  20. package/src/runtime/{OrigamiFiles.d.ts → OrigamiFileMap.js} +3 -5
  21. package/src/runtime/expressionFunction.js +3 -3
  22. package/src/runtime/expressionObject.js +19 -8
  23. package/src/runtime/handleExtension.js +2 -6
  24. package/src/runtime/mergeTrees.js +4 -7
  25. package/src/runtime/ops.js +13 -13
  26. package/test/cases/logicalAndExpression.yaml +7 -8
  27. package/test/compiler/compile.test.js +1 -1
  28. package/test/compiler/optimize.test.js +2 -2
  29. package/test/generated/logicalAndExpression.test.js +4 -0
  30. package/test/handlers/csv_handler.test.js +5 -5
  31. package/test/handlers/js_handler.test.js +2 -2
  32. package/test/handlers/ori_handler.test.js +8 -8
  33. package/test/handlers/oridocument_handler.test.js +3 -3
  34. package/test/handlers/tsv_handler.test.js +5 -5
  35. package/test/handlers/wasm_handler.test.js +2 -2
  36. package/test/runtime/OrigamiFileMap.test.js +40 -0
  37. package/test/runtime/evaluate.test.js +3 -3
  38. package/test/runtime/expressionObject.test.js +14 -6
  39. package/test/runtime/handleExtension.test.js +2 -2
  40. package/test/runtime/mergeTrees.test.js +2 -2
  41. package/test/runtime/ops.test.js +5 -5
  42. package/src/runtime/InvokeFunctionsTransform.d.ts +0 -5
  43. package/src/runtime/InvokeFunctionsTransform.js +0 -25
  44. package/src/runtime/functionResultsMap.js +0 -17
  45. package/test/runtime/OrigamiFiles.test.js +0 -35
  46. package/test/runtime/fixtures/subgraph = this.js +0 -5
  47. package/test/runtime/functionResultsMap.test.js +0 -20
package/index.ts CHANGED
@@ -1,5 +1,4 @@
1
- import { UnpackFunction } from "@weborigami/async-tree";
2
- import { AsyncTree } from "@weborigami/types";
1
+ import { SyncOrAsyncMap, UnpackFunction } from "@weborigami/async-tree";
3
2
 
4
3
  export * from "./main.js";
5
4
 
@@ -59,9 +58,9 @@ export type Position = {
59
58
 
60
59
  export type RuntimeState = {
61
60
  /** The container (e.g., file system) that holds the code */
62
- container?: AsyncTree | null;
61
+ container?: SyncOrAsyncMap | null;
63
62
  /** The object to which this code is attached */
64
- object?: AsyncTree | null;
63
+ object?: SyncOrAsyncMap | null;
65
64
  /** The current stack of function parameter assignments */
66
65
  stack?: Array<Record<string, any>>;
67
66
  }
package/main.js CHANGED
@@ -12,14 +12,12 @@ export { formatError } from "./src/runtime/errors.js";
12
12
  export { default as evaluate } from "./src/runtime/evaluate.js";
13
13
  export { default as EventTargetMixin } from "./src/runtime/EventTargetMixin.js";
14
14
  export * as expressionFunction from "./src/runtime/expressionFunction.js";
15
- export { default as functionResultsMap } from "./src/runtime/functionResultsMap.js";
16
15
  export * from "./src/runtime/handleExtension.js";
17
16
  export { default as handleExtension } from "./src/runtime/handleExtension.js";
18
17
  export { default as HandleExtensionsTransform } from "./src/runtime/HandleExtensionsTransform.js";
19
18
  export { default as ImportModulesMixin } from "./src/runtime/ImportModulesMixin.js";
20
- export { default as InvokeFunctionsTransform } from "./src/runtime/InvokeFunctionsTransform.js";
21
19
  export * as moduleCache from "./src/runtime/moduleCache.js";
22
- export { default as OrigamiFiles } from "./src/runtime/OrigamiFiles.js";
20
+ export { default as OrigamiFileMap } from "./src/runtime/OrigamiFileMap.js";
23
21
  export * as symbols from "./src/runtime/symbols.js";
24
22
  export { default as TreeEvent } from "./src/runtime/TreeEvent.js";
25
23
  export { default as WatchFilesMixin } from "./src/runtime/WatchFilesMixin.js";
package/package.json CHANGED
@@ -1,18 +1,17 @@
1
1
  {
2
2
  "name": "@weborigami/language",
3
- "version": "0.5.8",
3
+ "version": "0.6.0",
4
4
  "description": "Web Origami expression language compiler and runtime",
5
5
  "type": "module",
6
6
  "main": "./main.js",
7
7
  "types": "./index.ts",
8
8
  "devDependencies": {
9
- "@types/node": "24.3.0",
9
+ "@types/node": "24.10.1",
10
10
  "peggy": "5.0.6",
11
- "typescript": "5.9.2"
11
+ "typescript": "5.9.3"
12
12
  },
13
13
  "dependencies": {
14
- "@weborigami/async-tree": "0.5.8",
15
- "@weborigami/types": "0.5.8",
14
+ "@weborigami/async-tree": "0.6.0",
16
15
  "exif-parser": "0.1.12",
17
16
  "watcher": "2.3.1",
18
17
  "yaml": "2.8.1"
@@ -34,11 +34,21 @@ function csvParse(text) {
34
34
  const rows = [];
35
35
  let currentRow = [];
36
36
  let currentField = "";
37
+ let wasQuoted = false; // True if the current field was quoted
37
38
 
38
39
  const pushField = () => {
39
- // Push the completed field and reset for the next field.
40
- currentRow.push(currentField);
40
+ /** @type {string|number} */
41
+ let parsedField = currentField;
42
+ if (!wasQuoted) {
43
+ // Field wasn't quoted: if it's a valid number, convert it
44
+ const n = Number(currentField);
45
+ if (!isNaN(n)) {
46
+ parsedField = n;
47
+ }
48
+ }
49
+ currentRow.push(parsedField);
41
50
  currentField = "";
51
+ wasQuoted = false; // Reset the flag for the next field
42
52
  };
43
53
 
44
54
  const pushRow = () => {
@@ -54,29 +64,24 @@ function csvParse(text) {
54
64
  const char = text[i];
55
65
 
56
66
  if (inQuotes) {
57
- // In a quoted field
58
67
  if (char === '"') {
59
- // Check if next character is also a quote
60
68
  if (i + 1 < text.length && text[i + 1] === '"') {
61
- // Append a literal double quote and skip the next character
62
69
  currentField += '"';
63
70
  i += 2;
64
71
  continue;
65
72
  } else {
66
- // End of the quoted field
67
73
  inQuotes = false;
68
74
  i++;
69
75
  continue;
70
76
  }
71
77
  } else {
72
- // All other characters within quotes are taken literally.
73
78
  currentField += char;
74
79
  i++;
75
80
  continue;
76
81
  }
77
82
  } else if (char === '"') {
78
- // Start of a quoted field
79
83
  inQuotes = true;
84
+ wasQuoted = true; // Mark the field as quoted
80
85
  i++;
81
86
  continue;
82
87
  } else if (char === ",") {
@@ -45,7 +45,16 @@ function tsvParse(text) {
45
45
  const values = lines[i].split("\t");
46
46
  const entry = {};
47
47
  for (let j = 0; j < headers.length; j++) {
48
- entry[headers[j]] = values[j] ?? "";
48
+ /** @type {string|number} */
49
+ let value = values[j];
50
+ if (value !== undefined) {
51
+ // Attempt to convert to number if possible
52
+ const n = Number(value);
53
+ if (!isNaN(n)) {
54
+ value = n;
55
+ }
56
+ }
57
+ entry[headers[j]] = value ?? "";
49
58
  }
50
59
  data.push(entry);
51
60
  }
@@ -168,7 +168,11 @@ async function fetchWrapper(resource, options) {
168
168
  return response.ok ? await response.arrayBuffer() : undefined;
169
169
  }
170
170
 
171
- /** @this {import("@weborigami/types").AsyncTree|null|undefined} */
171
+ /**
172
+ * @typedef {import("@weborigami/async-tree").AsyncMap} AsyncMap
173
+ *
174
+ * @this {AsyncMap|null|undefined}
175
+ */
172
176
  async function importWrapper(modulePath) {
173
177
  // Walk up parent tree looking for a FileTree or other object with a `path`
174
178
  /** @type {any} */
@@ -1,4 +1,4 @@
1
- import { FileTree, toString } from "@weborigami/async-tree";
1
+ import { FileMap, toString } from "@weborigami/async-tree";
2
2
  import ori_handler from "../handlers/ori_handler.js";
3
3
  import coreGlobals from "./coreGlobals.js";
4
4
  import projectRoot from "./projectRoot.js";
@@ -14,9 +14,9 @@ export default async function config(dir = process.cwd()) {
14
14
  return cached;
15
15
  }
16
16
 
17
- // Use a plain FileTree to avoid loading extension handlers
18
- const rootFileTree = new FileTree(rootPath);
19
- const configBuffer = await rootFileTree.get("config.ori");
17
+ // Use a plain FileMap to avoid loading extension handlers
18
+ const rootFileMap = new FileMap(rootPath);
19
+ const configBuffer = await rootFileMap.get("config.ori");
20
20
  let configObject = {};
21
21
  if (configBuffer) {
22
22
  const configText = toString(configBuffer);
@@ -1,6 +1,6 @@
1
- import { FileTree } from "@weborigami/async-tree";
1
+ import { FileMap } from "@weborigami/async-tree";
2
2
  import path from "node:path";
3
- import OrigamiFiles from "../runtime/OrigamiFiles.js";
3
+ import OrigamiFileMap from "../runtime/OrigamiFileMap.js";
4
4
 
5
5
  const configFileName = "config.ori";
6
6
  const packageFileName = "package.json";
@@ -8,7 +8,7 @@ const packageFileName = "package.json";
8
8
  const mapPathToRoot = new Map();
9
9
 
10
10
  /**
11
- * Return an OrigamiFiles object for the current project.
11
+ * Return an OrigamiFileMap object for the current project.
12
12
  *
13
13
  * This searches the current directory and its ancestors for an Origami file
14
14
  * called `config.ori`. If an Origami configuration file is found, the
@@ -17,7 +17,6 @@ const mapPathToRoot = new Map();
17
17
  * Otherwise, this looks for a package.json file to determine the project root.
18
18
  * If no package.json is found, the current folder is used as the project root.
19
19
  *
20
- * @typedef {import("@weborigami/types").AsyncTree} AsyncTree
21
20
  *
22
21
  * @param {string} [dirname]
23
22
  */
@@ -29,19 +28,19 @@ export default async function projectRoot(dirname = process.cwd()) {
29
28
 
30
29
  let root;
31
30
  let value;
32
- // Use a plain FileTree to avoid loading extension handlers
33
- const currentTree = new FileTree(dirname);
31
+ // Use a plain FileMap to avoid loading extension handlers
32
+ const currentTree = new FileMap(dirname);
34
33
  // Try looking for config file
35
34
  value = await currentTree.get(configFileName);
36
35
  if (value) {
37
36
  // Found config file
38
- root = new OrigamiFiles(currentTree.path);
37
+ root = new OrigamiFileMap(currentTree.path);
39
38
  } else {
40
39
  // Try looking for package.json
41
40
  value = await currentTree.get(packageFileName);
42
41
  if (value) {
43
42
  // Found package.json
44
- root = new OrigamiFiles(currentTree.path);
43
+ root = new OrigamiFileMap(currentTree.path);
45
44
  } else {
46
45
  // Move up a folder and try again
47
46
  const parentPath = path.dirname(dirname);
@@ -49,7 +48,7 @@ export default async function projectRoot(dirname = process.cwd()) {
49
48
  root = await projectRoot(parentPath);
50
49
  } else {
51
50
  // At filesystem root, use current working directory
52
- root = new OrigamiFiles(process.cwd());
51
+ root = new OrigamiFileMap(process.cwd());
53
52
  }
54
53
  }
55
54
  }
@@ -5,14 +5,14 @@ import constructHref from "./constructHref.js";
5
5
  /**
6
6
  * Given a protocol, a host, and a list of keys, construct an href.
7
7
  *
8
- * @typedef {import("@weborigami/types").AsyncTree} AsyncTree
8
+ * @typedef {import("@weborigami/async-tree").SyncOrAsyncMap} SyncOrAsyncMap
9
9
  *
10
10
  * @param {string} protocol
11
- * @param {import("../../index.ts").Constructor<AsyncTree>} treeClass
11
+ * @param {import("../../index.ts").Constructor<SyncOrAsyncMap>} mapClass
12
12
  * @param {string} host
13
13
  * @param {string[]} keys
14
14
  */
15
- export default function constructSiteTree(protocol, treeClass, host, ...keys) {
15
+ export default function constructSiteTree(protocol, mapClass, host, ...keys) {
16
16
  // If the last key doesn't end in a slash, remove it for now.
17
17
  let lastKey;
18
18
  if (keys.length > 0 && keys.at(-1) && !trailingSlash.has(keys.at(-1))) {
@@ -20,7 +20,7 @@ export default function constructSiteTree(protocol, treeClass, host, ...keys) {
20
20
  }
21
21
 
22
22
  const href = constructHref(protocol, host, ...keys);
23
- let result = new (HandleExtensionsTransform(treeClass))(href);
23
+ let result = new (HandleExtensionsTransform(mapClass))(href);
24
24
 
25
25
  return lastKey ? result.get(lastKey) : result;
26
26
  }
@@ -1,14 +1,13 @@
1
- import { ExplorableSiteTree } from "@weborigami/async-tree";
1
+ import { ExplorableSiteMap } from "@weborigami/async-tree";
2
2
  import constructSiteTree from "./constructSiteTree.js";
3
3
 
4
4
  /**
5
5
  * A site tree with JSON Keys via HTTPS.
6
6
  *
7
- * @typedef {import("@weborigami/types").AsyncTree} AsyncTree
8
7
  *
9
8
  * @param {string} host
10
9
  * @param {...string} keys
11
10
  */
12
11
  export default function explore(host, ...keys) {
13
- return constructSiteTree("https:", ExplorableSiteTree, host, ...keys);
12
+ return constructSiteTree("https:", ExplorableSiteMap, host, ...keys);
14
13
  }
@@ -3,7 +3,6 @@ import handleExtension from "../runtime/handleExtension.js";
3
3
  /**
4
4
  * Fetch the resource at the given href.
5
5
  *
6
- * @typedef {import("@weborigami/types").AsyncTree} AsyncTree
7
6
  *
8
7
  * @param {string} href
9
8
  */
@@ -1,10 +1,9 @@
1
1
  import os from "node:os";
2
2
  import path from "node:path";
3
3
  import process from "node:process";
4
- import OrigamiFiles from "../runtime/OrigamiFiles.js";
4
+ import OrigamiFileMap from "../runtime/OrigamiFileMap.js";
5
5
 
6
6
  /**
7
- * @typedef {import("@weborigami/types").AsyncTree} AsyncTree
8
7
  *
9
8
  * @param {string[]} keys
10
9
  */
@@ -21,6 +20,6 @@ export default async function files(...keys) {
21
20
  }
22
21
  const resolved = path.resolve(basePath, relativePath);
23
22
 
24
- const result = new OrigamiFiles(resolved);
23
+ const result = new OrigamiFileMap(resolved);
25
24
  return result;
26
25
  }
@@ -4,7 +4,6 @@ import fetchAndHandleExtension from "./fetchAndHandleExtension.js";
4
4
  /**
5
5
  * Retrieve the indicated web resource via HTTP.
6
6
  *
7
- * @typedef {import("@weborigami/types").AsyncTree} AsyncTree
8
7
  *
9
8
  * @param {string} host
10
9
  * @param {...string} keys
@@ -4,7 +4,6 @@ import fetchAndHandleExtension from "./fetchAndHandleExtension.js";
4
4
  /**
5
5
  * Retrieve the indicated web resource via HTTPS.
6
6
  *
7
- * @typedef {import("@weborigami/types").AsyncTree} AsyncTree
8
7
  *
9
8
  * @param {string} host
10
9
  * @param {...string} keys
@@ -1,14 +1,13 @@
1
- import { SiteTree } from "@weborigami/async-tree";
1
+ import { SiteMap } from "@weborigami/async-tree";
2
2
  import constructSiteTree from "./constructSiteTree.js";
3
3
 
4
4
  /**
5
5
  * Return a website tree via HTTPS.
6
6
  *
7
- * @typedef {import("@weborigami/types").AsyncTree} AsyncTree
8
7
  *
9
8
  * @param {string} host
10
9
  * @param {...string} keys
11
10
  */
12
11
  export default function httpstree(host, ...keys) {
13
- return constructSiteTree("https:", SiteTree, host, ...keys);
12
+ return constructSiteTree("https:", SiteMap, host, ...keys);
14
13
  }
@@ -1,14 +1,13 @@
1
- import { SiteTree } from "@weborigami/async-tree";
1
+ import { SiteMap } from "@weborigami/async-tree";
2
2
  import constructSiteTree from "./constructSiteTree.js";
3
3
 
4
4
  /**
5
5
  * Return a website tree via HTTP.
6
6
  *
7
- * @typedef {import("@weborigami/types").AsyncTree} AsyncTree
8
7
  *
9
8
  * @param {string} host
10
9
  * @param {...string} keys
11
10
  */
12
11
  export default function httptree(host, ...keys) {
13
- return constructSiteTree("http:", SiteTree, host, ...keys);
12
+ return constructSiteTree("http:", SiteMap, host, ...keys);
14
13
  }
@@ -1,17 +1,41 @@
1
1
  import handleExtension from "./handleExtension.js";
2
2
 
3
3
  /**
4
- * @typedef {import("@weborigami/types").AsyncTree} AsyncTree
5
- * @typedef {import("../../index.ts").Constructor<AsyncTree>} AsyncTreeConstructor
4
+ * @typedef {import("../../index.ts").Constructor<Map>} MapConstructor
6
5
  * @typedef {import("@weborigami/async-tree").UnpackFunction} FileUnpackFunction
7
6
  *
8
- * @param {AsyncTreeConstructor} Base
7
+ * @param {MapConstructor} Base
9
8
  */
10
9
  export default function HandleExtensionsTransform(Base) {
11
- return class HandleExtensions extends Base {
12
- async get(key) {
13
- const value = await super.get(key);
14
- return handleExtension(value, key, this);
10
+ class HandleExtensions extends Base {
11
+ // Implement delete (and set) to keep the Map read-write
12
+ delete(key) {
13
+ return super.delete(key);
15
14
  }
16
- };
15
+
16
+ get(key) {
17
+ const value = super.get(key);
18
+ return value instanceof Promise
19
+ ? value.then((resolved) => handleExtension(resolved, key, this))
20
+ : handleExtension(value, key, this);
21
+ }
22
+
23
+ // See delete()
24
+ set(key, value) {
25
+ return super.set(key, value);
26
+ }
27
+ }
28
+
29
+ if (Base.prototype.readOnly) {
30
+ // Remove delete and set methods to keep the Map read-only. The base delete
31
+ // and set methods will exist (because it's a Map) but for our purposes the
32
+ // class is read-only.
33
+
34
+ // @ts-ignore
35
+ delete HandleExtensions.prototype.delete;
36
+ // @ts-ignore
37
+ delete HandleExtensions.prototype.set;
38
+ }
39
+
40
+ return HandleExtensions;
17
41
  }
@@ -5,8 +5,8 @@ import { maybeOrigamiSourceCode } from "./errors.js";
5
5
  import * as moduleCache from "./moduleCache.js";
6
6
 
7
7
  /**
8
- * @typedef {import("@weborigami/types").AsyncTree} AsyncTree
9
- * @typedef {import("../../index.ts").Constructor<AsyncTree & { dirname: string }>} BaseConstructor
8
+ * @typedef {import("@weborigami/async-tree").AsyncMap} AsyncMap
9
+ * @typedef {import("../../index.ts").Constructor<AsyncMap & { dirname: string }>} BaseConstructor
10
10
  * @param {BaseConstructor} Base
11
11
  */
12
12
  export default function ImportModulesMixin(Base) {
@@ -1,9 +1,9 @@
1
- import { FileTree } from "@weborigami/async-tree";
1
+ import { FileMap } from "@weborigami/async-tree";
2
2
  import EventTargetMixin from "./EventTargetMixin.js";
3
3
  import HandleExtensionsTransform from "./HandleExtensionsTransform.js";
4
4
  import ImportModulesMixin from "./ImportModulesMixin.js";
5
5
  import WatchFilesMixin from "./WatchFilesMixin.js";
6
6
 
7
- export default class OrigamiFiles extends HandleExtensionsTransform(
8
- ImportModulesMixin(WatchFilesMixin(EventTargetMixin(FileTree)))
7
+ export default class OrigamiFileMap extends HandleExtensionsTransform(
8
+ ImportModulesMixin(WatchFilesMixin(EventTargetMixin(FileMap)))
9
9
  ) {}
@@ -1,11 +1,9 @@
1
- import { FileTree } from "@weborigami/async-tree";
1
+ import { FileMap } from "@weborigami/async-tree";
2
2
  import EventTargetMixin from "./EventTargetMixin.js";
3
3
  import HandleExtensionsTransform from "./HandleExtensionsTransform.js";
4
4
  import ImportModulesMixin from "./ImportModulesMixin.js";
5
5
  import WatchFilesMixin from "./WatchFilesMixin.js";
6
6
 
7
- export default class OrigamiFiles extends HandleExtensionsTransform(
8
- (
9
- ImportModulesMixin(WatchFilesMixin(EventTargetMixin(FileTree)))
10
- )
7
+ export default class OrigamiFileMap extends HandleExtensionsTransform(
8
+ ImportModulesMixin(WatchFilesMixin(EventTargetMixin(FileMap)))
11
9
  ) {}
@@ -1,12 +1,12 @@
1
- /** @typedef {import("@weborigami/types").AsyncTree} AsyncTree */
2
-
3
1
  import { evaluate } from "./internal.js";
4
2
 
5
3
  /**
6
4
  * Given parsed Origami code, return a function that executes that code.
7
5
  *
6
+ * @typedef {import("@weborigami/async-tree").SyncOrAsyncMap} SyncOrAsyncMap
7
+ *
8
8
  * @param {import("../../index.js").AnnotatedCode} code - parsed Origami expression
9
- * @param {AsyncTree} parent - the parent tree in which the code is running
9
+ * @param {SyncOrAsyncMap} parent - the parent tree in which the code is running
10
10
  */
11
11
  export function createExpressionFunction(code, parent) {
12
12
  async function fn() {
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  extension,
3
- ObjectTree,
3
+ ObjectMap,
4
4
  setParent,
5
5
  symbols,
6
6
  trailingSlash,
@@ -30,14 +30,15 @@ export default async function expressionObject(entries, state = {}) {
30
30
  // Create the object and set its parent
31
31
  const object = {};
32
32
  const parent = state?.object ?? null;
33
- if (parent !== null && !Tree.isAsyncTree(parent)) {
34
- throw new TypeError(`Parent must be an AsyncTree or null`);
33
+ if (parent !== null && !Tree.isMap(parent)) {
34
+ throw new TypeError(`Parent must be a map or null`);
35
35
  }
36
36
  setParent(object, parent);
37
37
 
38
38
  let tree;
39
39
  const eagerProperties = [];
40
40
  const propertyIsEnumerable = {};
41
+ let hasLazyProperties = false;
41
42
  for (let [key, value] of entries) {
42
43
  // Determine if we need to define a getter or a regular property. If the key
43
44
  // has an extension, we need to define a getter. If the value is code (an
@@ -80,6 +81,7 @@ export default async function expressionObject(entries, state = {}) {
80
81
  // Property getter
81
82
  let code;
82
83
  if (value[0] === ops.getter) {
84
+ hasLazyProperties = true;
83
85
  code = value[1];
84
86
  } else {
85
87
  eagerProperties.push(key);
@@ -87,7 +89,7 @@ export default async function expressionObject(entries, state = {}) {
87
89
  }
88
90
 
89
91
  const get = async () => {
90
- tree ??= new ObjectTree(object);
92
+ tree ??= new ObjectMap(object);
91
93
  const newState = Object.assign({}, state, { object: tree });
92
94
  const result = await evaluate(code, newState);
93
95
  return extname ? handleExtension(result, key, tree) : result;
@@ -113,8 +115,7 @@ export default async function expressionObject(entries, state = {}) {
113
115
  // and overwrite the property getter with the actual value.
114
116
  for (const key of eagerProperties) {
115
117
  const value = await object[key];
116
- // @ts-ignore Unclear why TS thinks `object` might be undefined here
117
- const enumerable = Object.getOwnPropertyDescriptor(object, key).enumerable;
118
+ const enumerable = Object.getOwnPropertyDescriptor(object, key)?.enumerable;
118
119
  Object.defineProperty(object, key, {
119
120
  configurable: true,
120
121
  enumerable,
@@ -123,6 +124,16 @@ export default async function expressionObject(entries, state = {}) {
123
124
  });
124
125
  }
125
126
 
127
+ // If there are any getters, mark the object as async
128
+ if (hasLazyProperties) {
129
+ Object.defineProperty(object, symbols.async, {
130
+ configurable: true,
131
+ enumerable: false,
132
+ value: true,
133
+ writable: true,
134
+ });
135
+ }
136
+
126
137
  return object;
127
138
  }
128
139
 
@@ -141,8 +152,8 @@ export function entryKey(entry, object = null, eagerProperties = []) {
141
152
  return key;
142
153
  }
143
154
 
144
- // If eager property value is treelike, add slash to the key
145
- if (eagerProperties.includes(key) && Tree.isTreelike(object?.[key])) {
155
+ // If eager property value is maplike, add slash to the key
156
+ if (eagerProperties.includes(key) && Tree.isMaplike(object?.[key])) {
146
157
  return trailingSlash.add(key);
147
158
  }
148
159
 
@@ -18,7 +18,7 @@ let projectGlobals;
18
18
  *
19
19
  * @param {any} value
20
20
  * @param {any} key
21
- * @param {import("@weborigami/types").AsyncTree} [parent]
21
+ * @param {import("@weborigami/async-tree").SyncOrAsyncMap} [parent]
22
22
  */
23
23
  export default async function handleExtension(value, key, parent) {
24
24
  projectGlobals ??= await globals();
@@ -28,13 +28,9 @@ export default async function handleExtension(value, key, parent) {
28
28
  key = trailingSlash.remove(key);
29
29
  }
30
30
 
31
- // Special cases: `.ori.<ext>` extensions are Origami documents,
32
- // `.jse.<ext>` are JSE documents.
33
- // TODO: Remove .jse.<ext>
31
+ // Special cases: `.ori.<ext>` extensions are Origami documents
34
32
  const extname = key.match(/\.ori\.\S+$/)
35
33
  ? ".oridocument"
36
- : key.match(/\.jse\.\S+$/)
37
- ? ".jsedocument"
38
34
  : extension.extname(key);
39
35
  if (extname) {
40
36
  const handlerName = `${extname.slice(1)}_handler`;
@@ -3,14 +3,13 @@ import { isPlainObject, isUnpackable, Tree } from "@weborigami/async-tree";
3
3
  /**
4
4
  * Create a tree that's the result of merging the given trees.
5
5
  *
6
- * @typedef {import("@weborigami/types").AsyncTree} AsyncTree
7
- * @typedef {import("@weborigami/async-tree").Treelike} Treelike
6
+ * @typedef {import("@weborigami/async-tree").Maplike} Maplike
8
7
  *
9
- * @param {(Treelike|null)[]} trees
8
+ * @param {(Maplike|null)[]} trees
10
9
  */
11
10
  export default async function mergeTrees(...trees) {
12
11
  // Filter out null or undefined trees.
13
- /** @type {Treelike[]}
12
+ /** @type {Maplike[]}
14
13
  * @ts-ignore */
15
14
  const filtered = trees.filter((tree) => tree);
16
15
 
@@ -27,9 +26,7 @@ export default async function mergeTrees(...trees) {
27
26
  );
28
27
 
29
28
  // If all trees are plain objects, return a plain object.
30
- if (
31
- unpacked.every((tree) => isPlainObject(tree) && !Tree.isAsyncTree(tree))
32
- ) {
29
+ if (unpacked.every((tree) => isPlainObject(tree) && !Tree.isMap(tree))) {
33
30
  // If we do an Object.assign, we'd evaluate getters.
34
31
  // To avoid that, we'll merge property descriptors.
35
32
  const result = {};
@@ -1,17 +1,17 @@
1
1
  /**
2
2
  * @typedef {import("../../index.ts").AnnotatedCode} AnnotatedCode
3
- * @typedef {import("@weborigami/async-tree").PlainObject} PlainObject
4
- * @typedef {import("@weborigami/async-tree").Treelike} Treelike
5
- * @typedef {import("@weborigami/types").AsyncTree} AsyncTree
6
3
  * @typedef {import("../../index.ts").RuntimeState} RuntimeState
4
+ * @typedef {import("@weborigami/async-tree").Maplike} Maplike
5
+ * @typedef {import("@weborigami/async-tree").PlainObject} PlainObject
6
+ * @typedef {import("@weborigami/async-tree").SyncOrAsyncMap} SyncOrAsyncMap
7
7
  */
8
8
 
9
- import { isUnpackable, symbols, Tree } from "@weborigami/async-tree";
9
+ import { getParent, isUnpackable, Tree } from "@weborigami/async-tree";
10
10
  import os from "node:os";
11
11
  import expressionObject from "./expressionObject.js";
12
12
  import { evaluate } from "./internal.js";
13
13
  import mergeTrees from "./mergeTrees.js";
14
- import OrigamiFiles from "./OrigamiFiles.js";
14
+ import OrigamiFileMap from "./OrigamiFileMap.js";
15
15
  import { codeSymbol } from "./symbols.js";
16
16
 
17
17
  function addOpLabel(op, label) {
@@ -176,7 +176,7 @@ addOpLabel(greaterThanOrEqual, "«ops.greaterThanOrEqual»");
176
176
  * Files tree for the user's home directory.
177
177
  */
178
178
  export async function homeDirectory(...keys) {
179
- const tree = new OrigamiFiles(os.homedir());
179
+ const tree = new OrigamiFileMap(os.homedir());
180
180
  return keys.length > 0 ? Tree.traverse(tree, ...keys) : tree;
181
181
  }
182
182
  addOpLabel(homeDirectory, "«ops.homeDirectory»");
@@ -196,7 +196,7 @@ export async function inherited(depth, state) {
196
196
  `Origami internal error: Can't find context object`
197
197
  );
198
198
  }
199
- current = current.parent ?? current[symbols.parent];
199
+ current = getParent(current);
200
200
  }
201
201
  return current;
202
202
  }
@@ -406,11 +406,11 @@ export async function params(depth, state = {}) {
406
406
  addOpLabel(params, "«ops.params»");
407
407
  params.needsState = true;
408
408
 
409
- // export function optionalTraverse(treelike, key) {
410
- // if (!treelike) {
409
+ // export function optionalTraverse(maplike, key) {
410
+ // if (!maplike) {
411
411
  // return undefined;
412
412
  // }
413
- // return Tree.traverseOrThrow(treelike, key);
413
+ // return Tree.traverseOrThrow(maplike, key);
414
414
  // }
415
415
  // addOpLabel(optionalTraverse, "«ops.optionalTraverse");
416
416
 
@@ -436,7 +436,7 @@ export async function property(object, key) {
436
436
  if (key in object) {
437
437
  // Object defines the property, get it
438
438
  let value = object[key];
439
- // Is value an instance method? Copied from ObjectTree.
439
+ // Is value an instance method? Copied from ObjectMap.
440
440
  const isInstanceMethod =
441
441
  value instanceof Function && !Object.hasOwn(object, key);
442
442
  if (isInstanceMethod) {
@@ -460,7 +460,7 @@ addOpLabel(remainder, "«ops.remainder»");
460
460
  * Files tree for the filesystem root.
461
461
  */
462
462
  export async function rootDirectory(...keys) {
463
- const tree = new OrigamiFiles("/");
463
+ const tree = new OrigamiFileMap("/");
464
464
  return keys.length > 0 ? Tree.traverse(tree, ...keys) : tree;
465
465
  }
466
466
  addOpLabel(rootDirectory, "«ops.rootDirectory»");
@@ -468,7 +468,7 @@ addOpLabel(rootDirectory, "«ops.rootDirectory»");
468
468
  /**
469
469
  * Return the scope of the current tree
470
470
  *
471
- * @param {AsyncTree} parent
471
+ * @param {SyncOrAsyncMap} parent
472
472
  */
473
473
  export async function scope(parent) {
474
474
  if (!parent) {
@@ -108,14 +108,13 @@
108
108
  expected: false
109
109
  description: "Complex nesting with false at inner-most"
110
110
 
111
- # TODO: Uncomment when we can do math
112
- # - source: "true && (1 + 1 === 2)"
113
- # expected: true
114
- # description: "Combines logical AND with equality comparison"
115
-
116
- # - source: "false && (5 > 2)"
117
- # expected: false
118
- # description: "Logical AND with greater-than comparison"
111
+ - source: "true && (1 + 1 === 2)"
112
+ expected: true
113
+ description: "Combines logical AND with equality comparison"
114
+
115
+ - source: "false && (5 > 2)"
116
+ expected: false
117
+ description: "Logical AND with greater-than comparison"
119
118
 
120
119
  - source: "true && (3 || 0)"
121
120
  expected: 3
@@ -130,7 +130,7 @@ async function assertCompile(text, expected, options = {}) {
130
130
  const parent = options.target ?? null;
131
131
  const fn = compile.expression(text, { globals, mode, parent });
132
132
  let result = await fn();
133
- if (Tree.isTreelike(result)) {
133
+ if (Tree.isMaplike(result)) {
134
134
  result = await Tree.plain(result);
135
135
  }
136
136
  assert.deepEqual(result, expected);
@@ -1,4 +1,4 @@
1
- import { ObjectTree } from "@weborigami/async-tree";
1
+ import { SyncMap } from "@weborigami/async-tree";
2
2
  import { describe, test } from "node:test";
3
3
  import * as compile from "../../src/compiler/compile.js";
4
4
  import {
@@ -313,7 +313,7 @@ describe("optimize", () => {
313
313
  });
314
314
 
315
315
  function assertCompile(expression, expected, mode = "shell") {
316
- const parent = new ObjectTree({});
316
+ const parent = new SyncMap();
317
317
  const globals = {};
318
318
  const fn = compile.expression(expression, { globals, mode, parent });
319
319
  const actual = fn.code;
@@ -33,6 +33,8 @@ describe("logicalAndExpression - JavaScript", () => {
33
33
  assert.strictEqual((true && true) && true, true, "Nested logical ANDs with all true");
34
34
  assert.strictEqual(true && (true && false), false, "Nested logical ANDs with false in inner");
35
35
  assert.strictEqual((true && (false && true)), false, "Complex nesting with false at inner-most");
36
+ assert.strictEqual(true && (1 + 1 === 2), true, "Combines logical AND with equality comparison");
37
+ assert.strictEqual(false && (5 > 2), false, "Logical AND with greater-than comparison");
36
38
  assert.strictEqual(true && (3 || 0), 3, "Logical AND with logical OR");
37
39
  assert.strictEqual(true && (0 || 3), 3, "Logical AND with logical OR and falsy values");
38
40
  assert.strictEqual('' && false, "", "Falsy string and false");
@@ -70,6 +72,8 @@ describe("logicalAndExpression - Origami", async() => {
70
72
  assert.strictEqual(await oriEval("(true && true) && true"), true, "Nested logical ANDs with all true");
71
73
  assert.strictEqual(await oriEval("true && (true && false)"), false, "Nested logical ANDs with false in inner");
72
74
  assert.strictEqual(await oriEval("(true && (false && true))"), false, "Complex nesting with false at inner-most");
75
+ assert.strictEqual(await oriEval("true && (1 + 1 === 2)"), true, "Combines logical AND with equality comparison");
76
+ assert.strictEqual(await oriEval("false && (5 > 2)"), false, "Logical AND with greater-than comparison");
73
77
  assert.strictEqual(await oriEval("true && (3 || 0)"), 3, "Logical AND with logical OR");
74
78
  assert.strictEqual(await oriEval("true && (0 || 3)"), 3, "Logical AND with logical OR and falsy values");
75
79
  assert.strictEqual(await oriEval("'' && false"), "", "Falsy string and false");
@@ -10,17 +10,17 @@ Bob,25,Los Angeles
10
10
  "Carol ""CJ""",22,Chicago`;
11
11
  const result = csv_handler.unpack(csvText);
12
12
  assert.deepStrictEqual(result, [
13
- { name: "Alice", age: "30", city: "New York, NY" },
14
- { name: "Bob", age: "25", city: "Los Angeles" },
15
- { name: 'Carol "CJ"', age: "22", city: "Chicago" },
13
+ { name: "Alice", age: 30, city: "New York, NY" },
14
+ { name: "Bob", age: 25, city: "Los Angeles" },
15
+ { name: 'Carol "CJ"', age: 22, city: "Chicago" },
16
16
  ]);
17
17
  });
18
18
 
19
19
  test("handles CRLF line endings", () => {
20
20
  const textCRLF = `name,age,city\r\nAlice,30,"New York, NY"\r\nBob,25,Los Angeles\r\n`;
21
21
  const expected = [
22
- { name: "Alice", age: "30", city: "New York, NY" },
23
- { name: "Bob", age: "25", city: "Los Angeles" },
22
+ { name: "Alice", age: 30, city: "New York, NY" },
23
+ { name: "Bob", age: 25, city: "Los Angeles" },
24
24
  ];
25
25
  const result = csv_handler.unpack(textCRLF);
26
26
  assert.deepStrictEqual(result, expected);
@@ -1,11 +1,11 @@
1
- import { FileTree } from "@weborigami/async-tree";
1
+ import { FileMap } from "@weborigami/async-tree";
2
2
  import assert from "node:assert";
3
3
  import { describe, test } from "node:test";
4
4
  import js_handler from "../../src/handlers/js_handler.js";
5
5
  import ImportModulesMixin from "../../src/runtime/ImportModulesMixin.js";
6
6
 
7
7
  const fixturesUrl = new URL("fixtures", import.meta.url);
8
- const fixturesTree = new (ImportModulesMixin(FileTree))(fixturesUrl);
8
+ const fixturesTree = new (ImportModulesMixin(FileMap))(fixturesUrl);
9
9
 
10
10
  describe(".js handler", () => {
11
11
  test("loads .js file that exports a string", async () => {
@@ -1,11 +1,11 @@
1
- import { ObjectTree, Tree } from "@weborigami/async-tree";
1
+ import { ObjectMap, Tree } from "@weborigami/async-tree";
2
2
  import assert from "node:assert";
3
3
  import { describe, test } from "node:test";
4
4
  import ori_handler from "../../src/handlers/ori_handler.js";
5
- import OrigamiFiles from "../../src/runtime/OrigamiFiles.js";
5
+ import OrigamiFileMap from "../../src/runtime/OrigamiFileMap.js";
6
6
 
7
7
  const fixturesUrl = new URL("fixtures", import.meta.url);
8
- const fixtures = new OrigamiFiles(fixturesUrl);
8
+ const fixtures = new OrigamiFileMap(fixturesUrl);
9
9
 
10
10
  describe(".ori handler", async () => {
11
11
  test("loads a string expression", async () => {
@@ -15,7 +15,7 @@ describe(".ori handler", async () => {
15
15
  });
16
16
 
17
17
  test("loads a tree expression", async () => {
18
- const parent = new ObjectTree({
18
+ const parent = new ObjectMap({
19
19
  name: "world",
20
20
  });
21
21
  const source = `{
@@ -43,15 +43,15 @@ describe(".ori handler", async () => {
43
43
  });
44
44
 
45
45
  test("loads an object containing an object shorthand", async () => {
46
- const assets = new ObjectTree({});
47
- const parent = new ObjectTree({ assets });
46
+ const assets = new ObjectMap({});
47
+ const parent = new ObjectMap({ assets });
48
48
  const source = `{ assets }`;
49
49
  const object = await ori_handler.unpack(source, { parent });
50
50
  assert.equal(object.assets, assets);
51
51
  });
52
52
 
53
53
  test("loads a template literal", async () => {
54
- const scope = new ObjectTree({
54
+ const scope = new ObjectMap({
55
55
  name: "Alice",
56
56
  });
57
57
  const source = `\`Hello, \${name}!\``;
@@ -62,7 +62,7 @@ describe(".ori handler", async () => {
62
62
  });
63
63
 
64
64
  test("loads a template lambda that reads from parent scope", async () => {
65
- const parent = new ObjectTree({
65
+ const parent = new ObjectMap({
66
66
  name: "Alice",
67
67
  });
68
68
  const source = `() => \`Hello, \${name}!\``;
@@ -1,11 +1,11 @@
1
- import { ObjectTree } from "@weborigami/async-tree";
1
+ import { ObjectMap } from "@weborigami/async-tree";
2
2
  import assert from "node:assert";
3
3
  import { describe, test } from "node:test";
4
4
  import oridocument_handler from "../../src/handlers/oridocument_handler.js";
5
5
 
6
6
  describe("Origami document handler", () => {
7
7
  test("unpacks text with Origami expressions", async () => {
8
- const parent = new ObjectTree({
8
+ const parent = new ObjectMap({
9
9
  name: "world",
10
10
  });
11
11
  const text = "Hello, ${ name }!";
@@ -28,7 +28,7 @@ describe("Origami document handler", () => {
28
28
  });
29
29
 
30
30
  test("YAML front matter is returned with _body", async () => {
31
- const parent = new ObjectTree({
31
+ const parent = new ObjectMap({
32
32
  message: "Hello",
33
33
  });
34
34
  const text = `---
@@ -10,17 +10,17 @@ Bob\t25\tLos Angeles
10
10
  Carol\t22\tChicago`;
11
11
  const result = tsv_handler.unpack(TSVText);
12
12
  assert.deepStrictEqual(result, [
13
- { name: "Alice", age: "30", city: "New York, NY" },
14
- { name: "Bob", age: "25", city: "Los Angeles" },
15
- { name: "Carol", age: "22", city: "Chicago" },
13
+ { name: "Alice", age: 30, city: "New York, NY" },
14
+ { name: "Bob", age: 25, city: "Los Angeles" },
15
+ { name: "Carol", age: 22, city: "Chicago" },
16
16
  ]);
17
17
  });
18
18
 
19
19
  test("handles CRLF line endings", () => {
20
20
  const textCRLF = `name\tage\tcity\r\nAlice\t30\tNew York, NY\r\nBob\t25\tLos Angeles\r\n`;
21
21
  const expected = [
22
- { name: "Alice", age: "30", city: "New York, NY" },
23
- { name: "Bob", age: "25", city: "Los Angeles" },
22
+ { name: "Alice", age: 30, city: "New York, NY" },
23
+ { name: "Bob", age: 25, city: "Los Angeles" },
24
24
  ];
25
25
  const result = tsv_handler.unpack(textCRLF);
26
26
  assert.deepStrictEqual(result, expected);
@@ -1,11 +1,11 @@
1
- import { FileTree } from "@weborigami/async-tree";
1
+ import { FileMap } from "@weborigami/async-tree";
2
2
  import assert from "node:assert";
3
3
  import { describe, test } from "node:test";
4
4
  import wasm_handler from "../../src/handlers/wasm_handler.js";
5
5
  import ImportModulesMixin from "../../src/runtime/ImportModulesMixin.js";
6
6
 
7
7
  const fixturesUrl = new URL("fixtures", import.meta.url);
8
- const fixturesTree = new (ImportModulesMixin(FileTree))(fixturesUrl);
8
+ const fixturesTree = new (ImportModulesMixin(FileMap))(fixturesUrl);
9
9
 
10
10
  describe(".wasm handler", () => {
11
11
  test("loads .wasm file that exports a function", async () => {
@@ -0,0 +1,40 @@
1
+ import assert from "node:assert";
2
+ import * as fs from "node:fs";
3
+ import path from "node:path";
4
+ import { describe, test } from "node:test";
5
+ import { fileURLToPath } from "node:url";
6
+ import OrigamiFileMap from "../../src/runtime/OrigamiFileMap.js";
7
+
8
+ const dirname = path.dirname(fileURLToPath(import.meta.url));
9
+ const tempDirectory = path.join(dirname, "fixtures/temp/OrigamiFileMap");
10
+
11
+ describe("OrigamiFileMap", () => {
12
+ test.skip("can watch its folder for changes", { timeout: 2000 }, async () => {
13
+ createTempDirectory();
14
+
15
+ const tempFiles = new OrigamiFileMap(tempDirectory);
16
+ const changedFileName = await new Promise(async (resolve) => {
17
+ tempFiles.addEventListener("change", (event) => {
18
+ resolve(/** @type {any} */ (event).options.key);
19
+ });
20
+ tempFiles.set(
21
+ "foo.txt",
22
+ "This file is left over from testing and can be removed."
23
+ );
24
+ });
25
+ removeTempDirectory();
26
+ assert.equal(changedFileName, "foo.txt");
27
+ });
28
+ });
29
+
30
+ function createTempDirectory() {
31
+ // Remove any existing files or directories inside the temp directory so
32
+ // tests start from a clean slate. Use force so this is safe if the
33
+ // directory doesn't exist.
34
+ fs.rmSync(tempDirectory, { force: true, recursive: true });
35
+ fs.mkdirSync(tempDirectory, { recursive: true });
36
+ }
37
+
38
+ function removeTempDirectory() {
39
+ fs.rmSync(tempDirectory, { force: true, recursive: true });
40
+ }
@@ -1,7 +1,7 @@
1
1
  import assert from "node:assert";
2
2
  import { describe, test } from "node:test";
3
3
 
4
- import { ObjectTree } from "@weborigami/async-tree";
4
+ import { SyncMap } from "@weborigami/async-tree";
5
5
  import evaluate from "../../src/runtime/evaluate.js";
6
6
  import { createCode } from "../compiler/codeHelpers.js";
7
7
 
@@ -27,12 +27,12 @@ describe("evaluate", () => {
27
27
  });
28
28
 
29
29
  test("if function has containerAsTarget, it gets bound to state.container", async () => {
30
- /** @this {import("@weborigami/types").AsyncTree} */
30
+ /** @this {import("@weborigami/async-tree").SyncOrAsyncMap} */
31
31
  const fn = function () {
32
32
  return this;
33
33
  };
34
34
  fn.containerAsTarget = true;
35
- const container = new ObjectTree({});
35
+ const container = new SyncMap();
36
36
  const state = { container };
37
37
  const code = createCode([fn]);
38
38
  const result = await evaluate(code, state);
@@ -1,4 +1,4 @@
1
- import { ObjectTree, symbols, Tree } from "@weborigami/async-tree";
1
+ import { ObjectMap, symbols, SyncMap, Tree } from "@weborigami/async-tree";
2
2
  import assert from "node:assert";
3
3
  import { describe, test } from "node:test";
4
4
 
@@ -7,7 +7,7 @@ import { ops } from "../../src/runtime/internal.js";
7
7
 
8
8
  describe("expressionObject", () => {
9
9
  test("can instantiate an object", async () => {
10
- const container = new ObjectTree({
10
+ const container = new ObjectMap({
11
11
  upper: (s) => s.toUpperCase(),
12
12
  });
13
13
 
@@ -15,7 +15,7 @@ describe("expressionObject", () => {
15
15
  ["hello", [[[ops.scope, container], "upper"], "hello"]],
16
16
  ["world", [[[ops.scope, container], "upper"], "world"]],
17
17
  ];
18
- const context = new ObjectTree({});
18
+ const context = new SyncMap();
19
19
 
20
20
  const object = await expressionObject(entries, { object: context });
21
21
  assert.equal(await object.hello, "HELLO");
@@ -43,7 +43,7 @@ describe("expressionObject", () => {
43
43
  ["name", "world"],
44
44
  ["message", [ops.concat, "Hello, ", [[ops.inherited, 0], "name"], "!"]],
45
45
  ];
46
- const context = new ObjectTree({});
46
+ const context = new SyncMap();
47
47
  const object = await expressionObject(entries, { object: context });
48
48
  assert.deepEqual(await Tree.plain(object), {
49
49
  name: "world",
@@ -54,7 +54,7 @@ describe("expressionObject", () => {
54
54
 
55
55
  test("returned object values can be unpacked", async () => {
56
56
  const entries = [["data.json", `{ "a": 1 }`]];
57
- const context = new ObjectTree({});
57
+ const context = new SyncMap();
58
58
  const result = await expressionObject(entries, { object: context });
59
59
  const dataJson = await result["data.json"];
60
60
  const json = await dataJson.unpack();
@@ -77,7 +77,7 @@ describe("expressionObject", () => {
77
77
  ["getter", [ops.getter, [ops.object, ["b", [ops.literal, 2]]]]],
78
78
  ["hasSlash/", "This isn't really a tree but says it is"],
79
79
  ["message", "Hello"],
80
- // Immediate treelike value, should have a slash
80
+ // Immediate maplike value, should have a slash
81
81
  ["object", [ops.object, ["b", [ops.literal, 2]]]],
82
82
  ];
83
83
  const object = await expressionObject(entries);
@@ -88,4 +88,12 @@ describe("expressionObject", () => {
88
88
  "object/",
89
89
  ]);
90
90
  });
91
+
92
+ test("sets symbols.async on objects with getters", async () => {
93
+ const noGetter = await expressionObject([["eager", 1]]);
94
+ assert.equal(noGetter[symbols.async], undefined);
95
+
96
+ const hasGetter = await expressionObject([["lazy", [ops.getter, [2]]]]);
97
+ assert.equal(hasGetter[symbols.async], true);
98
+ });
91
99
  });
@@ -1,4 +1,4 @@
1
- import { ObjectTree } from "@weborigami/async-tree";
1
+ import { ObjectMap } from "@weborigami/async-tree";
2
2
  import assert from "node:assert";
3
3
  import { describe, test } from "node:test";
4
4
  import handleExtension from "../../src/runtime/handleExtension.js";
@@ -25,7 +25,7 @@ describe("handleExtension", () => {
25
25
  });
26
26
 
27
27
  function createFixture() {
28
- return new ObjectTree({
28
+ return new ObjectMap({
29
29
  foo: 1, // No extension, should be left alone
30
30
  "bar.json": `{ "bar": 2 }`,
31
31
  });
@@ -1,4 +1,4 @@
1
- import { ObjectTree, Tree } from "@weborigami/async-tree";
1
+ import { ObjectMap, Tree } from "@weborigami/async-tree";
2
2
  import assert from "node:assert";
3
3
  import { describe, test } from "node:test";
4
4
  import mergeTrees from "../../src/runtime/mergeTrees.js";
@@ -42,7 +42,7 @@ describe("mergeTrees", () => {
42
42
 
43
43
  test("merges heterogenous arguments as trees", async () => {
44
44
  const tree = await mergeTrees(
45
- new ObjectTree({
45
+ new ObjectMap({
46
46
  a: 1,
47
47
  b: 2,
48
48
  }),
@@ -1,4 +1,4 @@
1
- import { DeepObjectTree, ObjectTree, Tree } from "@weborigami/async-tree";
1
+ import { DeepObjectMap, ObjectMap, Tree } from "@weborigami/async-tree";
2
2
  import assert from "node:assert";
3
3
  import { describe, test } from "node:test";
4
4
 
@@ -128,12 +128,12 @@ describe("ops", () => {
128
128
  assert.deepEqual(await ops.flat(1, 2, [3]), [1, 2, 3]);
129
129
  });
130
130
 
131
- test("flattens treelike objects", async () => {
131
+ test("flattens maplike objects", async () => {
132
132
  const object = {
133
133
  a: 1,
134
134
  b: 2,
135
135
  };
136
- const tree = new ObjectTree({
136
+ const tree = new ObjectMap({
137
137
  c: 3,
138
138
  d: 4,
139
139
  });
@@ -156,7 +156,7 @@ describe("ops", () => {
156
156
  });
157
157
 
158
158
  test("ops.inherited walks up the object parent chain", async () => {
159
- const tree = new DeepObjectTree({
159
+ const tree = new DeepObjectMap({
160
160
  a: {
161
161
  b: {},
162
162
  },
@@ -368,7 +368,7 @@ describe("ops", () => {
368
368
 
369
369
  describe("ops.scope", () => {
370
370
  test("returns the scope of the given tree", async () => {
371
- const tree = new DeepObjectTree({
371
+ const tree = new DeepObjectMap({
372
372
  a: {
373
373
  b: {},
374
374
  },
@@ -1,5 +0,0 @@
1
- import { Mixin } from "../../index.ts";
2
-
3
- declare const InvokeFunctionsTransform: Mixin<{}>;
4
-
5
- export default InvokeFunctionsTransform;
@@ -1,25 +0,0 @@
1
- import { Tree } from "@weborigami/async-tree";
2
-
3
- /**
4
- * When using `get` to retrieve a value from a tree, if the value is a
5
- * function, invoke it and return the result.
6
- *
7
- * @typedef {import("@weborigami/types").AsyncTree} AsyncTree
8
- * @typedef {import("../../index.js").Constructor<AsyncTree>} AsyncTreeConstructor
9
- * @param {AsyncTreeConstructor} Base
10
- */
11
- export default function InvokeFunctionsTransform(Base) {
12
- return class InvokeFunctions extends Base {
13
- async get(key) {
14
- let value = await super.get(key);
15
- if (typeof value === "function") {
16
- value = await value();
17
-
18
- if (Tree.isAsyncTree(value) && !value.parent) {
19
- value.parent = this;
20
- }
21
- }
22
- return value;
23
- }
24
- };
25
- }
@@ -1,17 +0,0 @@
1
- import { Tree } from "@weborigami/async-tree";
2
-
3
- /**
4
- * When using `get` to retrieve a value from a tree, if the value is a
5
- * function, invoke it and return the result.
6
- */
7
- export default async function functionResultsMap(treelike) {
8
- return Tree.map(treelike, {
9
- description: "functionResultsMap",
10
-
11
- value: async (sourceValue, sourceKey, tree) => {
12
- const resultValue =
13
- typeof sourceValue === "function" ? await sourceValue() : sourceValue;
14
- return resultValue;
15
- },
16
- });
17
- }
@@ -1,35 +0,0 @@
1
- import assert from "node:assert";
2
- import * as fs from "node:fs/promises";
3
- import path from "node:path";
4
- import { describe, test } from "node:test";
5
- import { fileURLToPath } from "node:url";
6
- import OrigamiFiles from "../../src/runtime/OrigamiFiles.js";
7
-
8
- const dirname = path.dirname(fileURLToPath(import.meta.url));
9
- const tempDirectory = path.join(dirname, "fixtures/temp");
10
-
11
- describe("OrigamiFiles", () => {
12
- test("can watch its folder for changes", { timeout: 2000 }, async () => {
13
- await createTempDirectory();
14
- const tempFiles = new OrigamiFiles(tempDirectory);
15
- const changedFileName = await new Promise(async (resolve) => {
16
- tempFiles.addEventListener("change", (event) => {
17
- resolve(/** @type {any} */ (event).options.key);
18
- });
19
- await tempFiles.set(
20
- "foo.txt",
21
- "This file is left over from testing and can be removed."
22
- );
23
- });
24
- await removeTempDirectory();
25
- assert.equal(changedFileName, "foo.txt");
26
- });
27
- });
28
-
29
- async function createTempDirectory() {
30
- await fs.mkdir(tempDirectory, { recursive: true });
31
- }
32
-
33
- async function removeTempDirectory() {
34
- await fs.rm(tempDirectory, { recursive: true });
35
- }
@@ -1,5 +0,0 @@
1
- import { ObjectTree } from "@weborigami/async-tree";
2
- export default new ObjectTree({
3
- a: "Hello, a.",
4
- b: "Hello, b.",
5
- });
@@ -1,20 +0,0 @@
1
- import { ObjectTree, Tree } from "@weborigami/async-tree";
2
- import assert from "node:assert";
3
- import { describe, test } from "node:test";
4
- import functionResultsMap from "../../src/runtime/functionResultsMap.js";
5
-
6
- describe("functionResultsMap", () => {
7
- test("get() invokes functions, returns other values as is", async () => {
8
- const tree = new ObjectTree({
9
- fn: function () {
10
- return "Hello";
11
- },
12
- string: "string",
13
- });
14
- const fixture = await functionResultsMap(tree);
15
- assert.deepEqual(await Tree.plain(fixture), {
16
- fn: "Hello",
17
- string: "string",
18
- });
19
- });
20
- });