@weborigami/language 0.5.7 → 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 (54) 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/handlers.js +2 -0
  6. package/src/handlers/sh_handler.js +65 -0
  7. package/src/handlers/tsv_handler.js +63 -0
  8. package/src/project/jsGlobals.js +5 -1
  9. package/src/project/projectConfig.js +4 -4
  10. package/src/project/projectRoot.js +8 -9
  11. package/src/protocols/constructSiteTree.js +4 -4
  12. package/src/protocols/explore.js +2 -3
  13. package/src/protocols/fetchAndHandleExtension.js +0 -1
  14. package/src/protocols/files.js +2 -3
  15. package/src/protocols/http.js +0 -1
  16. package/src/protocols/https.js +0 -1
  17. package/src/protocols/httpstree.js +2 -3
  18. package/src/protocols/httptree.js +2 -3
  19. package/src/runtime/HandleExtensionsTransform.js +32 -8
  20. package/src/runtime/ImportModulesMixin.js +2 -2
  21. package/src/runtime/{OrigamiFiles.js → OrigamiFileMap.d.ts} +3 -3
  22. package/src/runtime/{OrigamiFiles.d.ts → OrigamiFileMap.js} +3 -5
  23. package/src/runtime/expressionFunction.js +3 -3
  24. package/src/runtime/expressionObject.js +19 -8
  25. package/src/runtime/handleExtension.js +2 -6
  26. package/src/runtime/mergeTrees.js +4 -7
  27. package/src/runtime/ops.js +13 -13
  28. package/test/cases/logicalAndExpression.yaml +7 -8
  29. package/test/compiler/compile.test.js +1 -1
  30. package/test/compiler/optimize.test.js +2 -2
  31. package/test/generated/logicalAndExpression.test.js +4 -0
  32. package/test/handlers/{csv.handler.test.js → csv_handler.test.js} +5 -5
  33. package/test/handlers/{js.handler.test.js → js_handler.test.js} +2 -2
  34. package/test/handlers/{ori.handler.test.js → ori_handler.test.js} +8 -8
  35. package/test/handlers/{oridocument.handler.test.js → oridocument_handler.test.js} +3 -3
  36. package/test/handlers/sh_handler.test.js +14 -0
  37. package/test/handlers/tsv_handler.test.js +28 -0
  38. package/test/handlers/{wasm.handler.test.js → wasm_handler.test.js} +2 -2
  39. package/test/runtime/OrigamiFileMap.test.js +40 -0
  40. package/test/runtime/evaluate.test.js +3 -3
  41. package/test/runtime/expressionObject.test.js +14 -6
  42. package/test/runtime/handleExtension.test.js +2 -2
  43. package/test/runtime/mergeTrees.test.js +2 -2
  44. package/test/runtime/ops.test.js +5 -5
  45. package/src/runtime/InvokeFunctionsTransform.d.ts +0 -5
  46. package/src/runtime/InvokeFunctionsTransform.js +0 -25
  47. package/src/runtime/functionResultsMap.js +0 -17
  48. package/test/runtime/OrigamiFiles.test.js +0 -35
  49. package/test/runtime/fixtures/subgraph = this.js +0 -5
  50. package/test/runtime/functionResultsMap.test.js +0 -20
  51. /package/test/handlers/{jpeg.handler.test.js → jpeg_handler.test.js} +0 -0
  52. /package/test/handlers/{json.handler.test.js → json_handler.test.js} +0 -0
  53. /package/test/handlers/{txt.handler.test.js → txt_handler.test.js} +0 -0
  54. /package/test/handlers/{yaml.handler.test.js → yaml_handler.test.js} +0 -0
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.7",
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.7",
15
- "@weborigami/types": "0.5.7",
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 === ",") {
@@ -27,6 +27,8 @@ export { default as jpg_handler } from "./jpg_handler.js";
27
27
  export { default as json_handler } from "./json_handler.js";
28
28
  export { default as md_handler } from "./md_handler.js";
29
29
  export { default as mjs_handler } from "./mjs_handler.js";
30
+ export { default as sh_handler } from "./sh_handler.js";
31
+ export { default as tsv_handler } from "./tsv_handler.js";
30
32
  export { default as wasm_handler } from "./wasm_handler.js";
31
33
  export { default as xhtml_handler } from "./xhtml_handler.js";
32
34
  export { default as yaml_handler } from "./yaml_handler.js";
@@ -0,0 +1,65 @@
1
+ import { toString } from "@weborigami/async-tree";
2
+ import { spawn } from "node:child_process";
3
+
4
+ /**
5
+ * Shell script file extension handler
6
+ */
7
+ export default {
8
+ mediaType: "text/plain",
9
+
10
+ /** @type {import("@weborigami/async-tree").UnpackFunction} */
11
+ async unpack(packed) {
12
+ const scriptText = toString(packed);
13
+
14
+ if (scriptText === null) {
15
+ throw new Error(".sh handler: input isn't text");
16
+ }
17
+
18
+ return async (input) => {
19
+ return runShellScript(scriptText, input);
20
+ };
21
+ },
22
+ };
23
+
24
+ /**
25
+ * Run arbitrary shell script text in /bin/sh and feed it stdin.
26
+ * Supports multiple commands, pipelines, redirects, etc.
27
+ *
28
+ * @param {string} scriptText - Shell code (may contain newlines/side effects)
29
+ * @param {string} inputText - Text to pipe to the script's stdin
30
+ * @returns {Promise<string>}
31
+ */
32
+ function runShellScript(scriptText, inputText) {
33
+ return new Promise((resolve, reject) => {
34
+ // Use sh -c "<scriptText>" so stdin is free for inputText
35
+ const child = spawn("sh", ["-c", scriptText], {
36
+ env: { ...process.env },
37
+ stdio: ["pipe", "pipe", "pipe"],
38
+ });
39
+
40
+ let stdout = "";
41
+ let stderr = "";
42
+
43
+ child.stdout.on("data", (c) => (stdout += c));
44
+ child.stderr.on("data", (c) => (stderr += c));
45
+
46
+ child.on("error", reject);
47
+
48
+ child.on("close", (code) => {
49
+ if (code !== 0) {
50
+ /** @type {any} */
51
+ const err = new Error(
52
+ `Shell exited with code ${code}${stderr ? `: ${stderr}` : ""}`
53
+ );
54
+ err.code = code;
55
+ err.stdout = stdout;
56
+ err.stderr = stderr;
57
+ return reject(err);
58
+ }
59
+ resolve(stdout);
60
+ });
61
+
62
+ // Feed the input to the script's stdin and close it
63
+ child.stdin.end(inputText);
64
+ });
65
+ }
@@ -0,0 +1,63 @@
1
+ import { symbols, toString } from "@weborigami/async-tree";
2
+
3
+ export default {
4
+ mediaType: "text/csv",
5
+
6
+ unpack(packed, options = {}) {
7
+ const parent = options.parent ?? null;
8
+ const text = toString(packed);
9
+ if (text === null) {
10
+ throw new TypeError(".tsv handler can only unpack text");
11
+ }
12
+ const data = tsvParse(text);
13
+ // Define `parent` as non-enumerable property
14
+ Object.defineProperty(data, symbols.parent, {
15
+ configurable: true,
16
+ enumerable: false,
17
+ value: parent,
18
+ writable: true,
19
+ });
20
+ return data;
21
+ },
22
+ };
23
+
24
+ /**
25
+ * Parse text as tab-separated values (TSV) format into an array of objects.
26
+ *
27
+ * This assumes the presence of a header row, and accepts both CRLF and LF line
28
+ * endings.
29
+ *
30
+ * Blank lines are ignored.
31
+ *
32
+ * @param {string} text
33
+ * @returns {any[]}
34
+ */
35
+ function tsvParse(text) {
36
+ const lines = text.split(/\r?\n/).filter((line) => line.trim() !== "");
37
+ if (lines.length === 0) {
38
+ return [];
39
+ }
40
+
41
+ const headers = lines[0].split("\t");
42
+ const data = [];
43
+
44
+ for (let i = 1; i < lines.length; i++) {
45
+ const values = lines[i].split("\t");
46
+ const entry = {};
47
+ for (let j = 0; j < headers.length; 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 ?? "";
58
+ }
59
+ data.push(entry);
60
+ }
61
+
62
+ return data;
63
+ }
@@ -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