@weborigami/language 0.6.5 → 0.6.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/main.js CHANGED
@@ -4,9 +4,12 @@ export * as compile from "./src/compiler/compile.js";
4
4
  export { default as isOrigamiFrontMatter } from "./src/compiler/isOrigamiFrontMatter.js";
5
5
  export * as Handlers from "./src/handlers/handlers.js";
6
6
  export { default as builtins } from "./src/project/builtins.js";
7
+ export { default as coreGlobals } from "./src/project/coreGlobals.js";
7
8
  export { default as jsGlobals } from "./src/project/jsGlobals.js";
9
+ export { default as projectConfig } from "./src/project/projectConfig.js";
8
10
  export { default as projectGlobals } from "./src/project/projectGlobals.js";
9
11
  export { default as projectRoot } from "./src/project/projectRoot.js";
12
+ export { default as projectRootFromPath } from "./src/project/projectRootFromPath.js";
10
13
  export * as Protocols from "./src/protocols/protocols.js";
11
14
  export { formatError, highlightError } from "./src/runtime/errors.js";
12
15
  export { default as evaluate } from "./src/runtime/evaluate.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@weborigami/language",
3
- "version": "0.6.5",
3
+ "version": "0.6.6",
4
4
  "description": "Web Origami expression language compiler and runtime",
5
5
  "type": "module",
6
6
  "main": "./main.js",
@@ -11,7 +11,7 @@
11
11
  "typescript": "5.9.3"
12
12
  },
13
13
  "dependencies": {
14
- "@weborigami/async-tree": "0.6.5",
14
+ "@weborigami/async-tree": "0.6.6",
15
15
  "exif-parser": "0.1.12",
16
16
  "watcher": "2.3.1",
17
17
  "yaml": "2.8.1"
@@ -1,6 +1,10 @@
1
1
  import { pathFromKeys, trailingSlash } from "@weborigami/async-tree";
2
2
  import jsGlobals from "../project/jsGlobals.js";
3
- import { entryKey } from "../runtime/expressionObject.js";
3
+ import {
4
+ KEY_TYPE,
5
+ normalizeKey,
6
+ propertyInfo,
7
+ } from "../runtime/expressionObject.js";
4
8
  import { ops } from "../runtime/internal.js";
5
9
  import { annotate, markers, spanLocations } from "./parserHelpers.js";
6
10
 
@@ -62,10 +66,7 @@ export default function optimize(code, options = {}) {
62
66
 
63
67
  case ops.object:
64
68
  const entries = args;
65
- // Filter out computed property keys when determining local variables
66
- const propertyNames = entries
67
- .map((entry) => entryKey(entry))
68
- .filter((key) => key !== null);
69
+ const propertyNames = getPropertyNames(entries);
69
70
  locals.push({
70
71
  type: REFERENCE_INHERITED,
71
72
  names: propertyNames,
@@ -207,6 +208,14 @@ function findLocalDetails(key, locals) {
207
208
  return null;
208
209
  }
209
210
 
211
+ function getPropertyNames(entries) {
212
+ const infos = entries.map(([key, value]) => propertyInfo(key, value));
213
+ // Filter out computed property keys when determining local variables
214
+ return infos
215
+ .filter((info) => info.keyType !== KEY_TYPE.COMPUTED)
216
+ .map((info) => normalizeKey(info));
217
+ }
218
+
210
219
  function globalReference(key, globals) {
211
220
  const normalized = trailingSlash.remove(key);
212
221
  return globals[normalized];
@@ -163,16 +163,18 @@ comment "comment"
163
163
  / singleLineComment
164
164
 
165
165
  computedPropertyAccess
166
- = computedPropertySpace "[" expression:expectExpression expectClosingBracket {
166
+ = computedPropertySpace? "[" expression:expectExpression expectClosingBracket {
167
167
  return annotate([markers.property, expression], location());
168
168
  }
169
169
 
170
- // A space before a computed property access. This is allowed when not in shell
171
- // mode. In shell mode `foo [bar]` should parse as a function call with a single
172
- // argument of an array, not as a property access.
170
+ // An inline space before a computed property access. This is allowed when not
171
+ // in shell mode. In shell mode `foo [bar]` should parse as a function call with
172
+ // a single argument of an array, not as a property access. In program made, we
173
+ // allow an inline space per JavaScript. JavaScript also allows newlines, but we
174
+ // disallow those to avoid confusion with array/list/object entry separators.
173
175
  computedPropertySpace
174
176
  = shellMode
175
- / !shellMode __
177
+ / !shellMode inlineSpace
176
178
 
177
179
  conditionalExpression
178
180
  = condition:logicalOrExpression tail:(__
@@ -750,7 +752,7 @@ parenthesesArgumentList "list"
750
752
  // Function arguments in parentheses
751
753
  parenthesesArguments "function arguments in parentheses"
752
754
  = inlineSpace* "(" __ list:parenthesesArgumentList? __ expectClosingParenthesis {
753
- return annotate(list ?? [undefined], location());
755
+ return annotate(list ?? [], location());
754
756
  }
755
757
 
756
758
  // A slash-separated path of keys that follows a call target, such as the path
@@ -931,7 +931,7 @@ function peg$parse(input, options) {
931
931
  return annotate(args, location());
932
932
  }
933
933
  function peg$f98(list) {
934
- return annotate(list ?? [undefined], location());
934
+ return annotate(list ?? [], location());
935
935
  }
936
936
  function peg$f99(keys) {
937
937
  const args = keys ?? [];
@@ -2192,25 +2192,23 @@ function peg$parse(input, options) {
2192
2192
 
2193
2193
  s0 = peg$currPos;
2194
2194
  s1 = peg$parsecomputedPropertySpace();
2195
- if (s1 !== peg$FAILED) {
2196
- if (input.charCodeAt(peg$currPos) === 91) {
2197
- s2 = peg$c6;
2198
- peg$currPos++;
2199
- } else {
2200
- s2 = peg$FAILED;
2201
- if (peg$silentFails === 0) { peg$fail(peg$e9); }
2202
- }
2203
- if (s2 !== peg$FAILED) {
2204
- s3 = peg$parseexpectExpression();
2205
- if (s3 !== peg$FAILED) {
2206
- s4 = peg$parseexpectClosingBracket();
2207
- if (s4 !== peg$FAILED) {
2208
- peg$savedPos = s0;
2209
- s0 = peg$f16(s3);
2210
- } else {
2211
- peg$currPos = s0;
2212
- s0 = peg$FAILED;
2213
- }
2195
+ if (s1 === peg$FAILED) {
2196
+ s1 = null;
2197
+ }
2198
+ if (input.charCodeAt(peg$currPos) === 91) {
2199
+ s2 = peg$c6;
2200
+ peg$currPos++;
2201
+ } else {
2202
+ s2 = peg$FAILED;
2203
+ if (peg$silentFails === 0) { peg$fail(peg$e9); }
2204
+ }
2205
+ if (s2 !== peg$FAILED) {
2206
+ s3 = peg$parseexpectExpression();
2207
+ if (s3 !== peg$FAILED) {
2208
+ s4 = peg$parseexpectClosingBracket();
2209
+ if (s4 !== peg$FAILED) {
2210
+ peg$savedPos = s0;
2211
+ s0 = peg$f16(s3);
2214
2212
  } else {
2215
2213
  peg$currPos = s0;
2216
2214
  s0 = peg$FAILED;
@@ -2244,9 +2242,14 @@ function peg$parse(input, options) {
2244
2242
  s1 = peg$FAILED;
2245
2243
  }
2246
2244
  if (s1 !== peg$FAILED) {
2247
- s2 = peg$parse__();
2248
- s1 = [s1, s2];
2249
- s0 = s1;
2245
+ s2 = peg$parseinlineSpace();
2246
+ if (s2 !== peg$FAILED) {
2247
+ s1 = [s1, s2];
2248
+ s0 = s1;
2249
+ } else {
2250
+ peg$currPos = s0;
2251
+ s0 = peg$FAILED;
2252
+ }
2250
2253
  } else {
2251
2254
  peg$currPos = s0;
2252
2255
  s0 = peg$FAILED;
@@ -1,12 +1,12 @@
1
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
- import projectRoot from "./projectRoot.js";
4
+ import projectRootFromPath from "./projectRootFromPath.js";
5
5
 
6
6
  const mapPathToConfig = new Map();
7
7
 
8
8
  export default async function config(dir = process.cwd()) {
9
- const root = await projectRoot(dir);
9
+ const root = await projectRootFromPath(dir);
10
10
 
11
11
  const rootPath = root.path;
12
12
  const cached = mapPathToConfig.get(rootPath);
@@ -4,14 +4,14 @@ import projectConfig from "./projectConfig.js";
4
4
  let globals;
5
5
 
6
6
  // Core globals plus project config
7
- export default async function projectGlobals() {
7
+ export default async function projectGlobals(dir = process.cwd()) {
8
8
  if (!globals) {
9
9
  // Start with core globals
10
10
  globals = await coreGlobals();
11
11
  // Now get config. The config.ori file may require access to globals,
12
12
  // which will obtain the core globals set above. Once we've got the
13
13
  // config, we add it to the globals.
14
- const config = await projectConfig();
14
+ const config = await projectConfig(dir);
15
15
  Object.assign(globals, config);
16
16
  }
17
17
 
@@ -1,58 +1,9 @@
1
- import { FileMap } from "@weborigami/async-tree";
2
- import path from "node:path";
3
- import OrigamiFileMap from "../runtime/OrigamiFileMap.js";
4
-
5
- const configFileName = "config.ori";
6
- const packageFileName = "package.json";
7
-
8
- const mapPathToRoot = new Map();
1
+ import { Tree } from "@weborigami/async-tree";
9
2
 
10
3
  /**
11
- * Return an OrigamiFileMap object for the current project.
12
- *
13
- * This searches the current directory and its ancestors for an Origami file
14
- * called `config.ori`. If an Origami configuration file is found, the
15
- * containing folder is considered to be the project root.
16
- *
17
- * Otherwise, this looks for a package.json file to determine the project root.
18
- * If no package.json is found, the current folder is used as the project root.
19
- *
20
- *
21
- * @param {string} [dirname]
4
+ * Return an OrigamiFileMap object for the current code context.
22
5
  */
23
- export default async function projectRoot(dirname = process.cwd()) {
24
- const cached = mapPathToRoot.get(dirname);
25
- if (cached) {
26
- return cached;
27
- }
28
-
29
- let root;
30
- let value;
31
- // Use a plain FileMap to avoid loading extension handlers
32
- const currentTree = new FileMap(dirname);
33
- // Try looking for config file
34
- value = await currentTree.get(configFileName);
35
- if (value) {
36
- // Found config file
37
- root = new OrigamiFileMap(currentTree.path);
38
- } else {
39
- // Try looking for package.json
40
- value = await currentTree.get(packageFileName);
41
- if (value) {
42
- // Found package.json
43
- root = new OrigamiFileMap(currentTree.path);
44
- } else {
45
- // Move up a folder and try again
46
- const parentPath = path.dirname(dirname);
47
- if (parentPath !== dirname) {
48
- root = await projectRoot(parentPath);
49
- } else {
50
- // At filesystem root, use current working directory
51
- root = new OrigamiFileMap(process.cwd());
52
- }
53
- }
54
- }
55
-
56
- mapPathToRoot.set(dirname, root);
57
- return root;
6
+ export default async function projectRoot(state) {
7
+ return Tree.root(state.container);
58
8
  }
9
+ projectRoot.needsState = true;
@@ -0,0 +1,58 @@
1
+ import { FileMap } from "@weborigami/async-tree";
2
+ import path from "node:path";
3
+ import OrigamiFileMap from "../runtime/OrigamiFileMap.js";
4
+
5
+ const configFileName = "config.ori";
6
+ const packageFileName = "package.json";
7
+
8
+ const mapPathToRoot = new Map();
9
+
10
+ /**
11
+ * Return an OrigamiFileMap object for the current project root.
12
+ *
13
+ * This searches the current directory and its ancestors for an Origami file
14
+ * called `config.ori`. If an Origami configuration file is found, the
15
+ * containing folder is considered to be the project root.
16
+ *
17
+ * Otherwise, this looks for a package.json file to determine the project root.
18
+ * If no package.json is found, the current folder is used as the project root.
19
+ *
20
+ *
21
+ * @param {string} [dirname]
22
+ */
23
+ export default async function projectRootFromPath(dirname = process.cwd()) {
24
+ const cached = mapPathToRoot.get(dirname);
25
+ if (cached) {
26
+ return cached;
27
+ }
28
+
29
+ let root;
30
+ let value;
31
+ // Use a plain FileMap to avoid loading extension handlers
32
+ const currentTree = new FileMap(dirname);
33
+ // Try looking for config file
34
+ value = await currentTree.get(configFileName);
35
+ if (value) {
36
+ // Found config file
37
+ root = new OrigamiFileMap(currentTree.path);
38
+ } else {
39
+ // Try looking for package.json
40
+ value = await currentTree.get(packageFileName);
41
+ if (value) {
42
+ // Found package.json
43
+ root = new OrigamiFileMap(currentTree.path);
44
+ } else {
45
+ // Move up a folder and try again
46
+ const parentPath = path.dirname(dirname);
47
+ if (parentPath !== dirname) {
48
+ root = await projectRootFromPath(parentPath);
49
+ } else {
50
+ // At filesystem root, use current working directory
51
+ root = new OrigamiFileMap(process.cwd());
52
+ }
53
+ }
54
+ }
55
+
56
+ mapPathToRoot.set(dirname, root);
57
+ return root;
58
+ }
@@ -13,6 +13,11 @@ export default async function fetchAndHandleExtension(href) {
13
13
  }
14
14
  let buffer = await response.arrayBuffer();
15
15
 
16
+ const mediaType = response.headers.get("Content-Type");
17
+ if (mediaType) {
18
+ /** @type {any} */ (buffer).mediaType = mediaType;
19
+ }
20
+
16
21
  // Attach any loader defined for the file type.
17
22
  const url = new URL(href);
18
23
  const filename = url.pathname.split("/").pop();
@@ -2,66 +2,53 @@ import { Tree, keysFromPath } from "@weborigami/async-tree";
2
2
  import projectRoot from "../project/projectRoot.js";
3
3
 
4
4
  /**
5
- * @param {string[]} keys
5
+ * The package: protocol handler
6
+ *
7
+ * @param {any[]} args
6
8
  */
7
- export default async function packageNamespace(...keys) {
8
- const parent = await projectRoot();
9
-
10
- let name = keys.shift();
11
- let organization;
12
- if (name?.startsWith("@")) {
13
- // First key is an npm organization
14
- organization = name;
15
- if (keys.length === 0) {
16
- // Return a function that will process the next key
17
- return async (name, ...keys) =>
18
- getPackage(parent, organization, name, keys);
19
- }
20
- name = keys.shift();
21
- }
22
-
23
- return getPackage(parent, organization, name, keys);
24
- }
25
-
26
- async function getPackage(parent, organization, name, keys) {
27
- const packagePath = ["node_modules"];
28
- if (organization) {
29
- packagePath.push(organization);
9
+ export default async function packageProtocol(...args) {
10
+ const state = args.pop(); // Remaining args are the path
11
+ const root = await projectRoot(state);
12
+
13
+ // Identify the path to the package root
14
+ const packageRootPath = ["node_modules"];
15
+ const name = args.shift();
16
+ packageRootPath.push(name);
17
+ if (name.startsWith("@")) {
18
+ // First key is an npm organization, add next key as name
19
+ packageRootPath.push(args.shift());
30
20
  }
31
- packagePath.push(name);
32
-
33
- const parentScope = await Tree.scope(parent);
34
- const packageRoot = await Tree.traverse(
35
- // @ts-ignore
36
- parentScope,
37
- ...packagePath
38
- );
39
21
 
22
+ // Get the package root (top level folder of the package)
23
+ const packageRoot = await Tree.traverse(root, ...packageRootPath);
40
24
  if (!packageRoot) {
41
- throw new Error(`Can't find ${packagePath.join("/")}`);
25
+ throw new Error(`Can't find ${packageRootPath.join("/")}`);
42
26
  }
43
27
 
28
+ // Identify the main entry point
44
29
  const mainPath = await Tree.traverse(packageRoot, "package.json", "main");
45
30
  if (!mainPath) {
46
31
  throw new Error(
47
- `node_modules/${keys.join(
32
+ `${packageRootPath.join(
48
33
  "/"
49
34
  )} doesn't contain a package.json with a "main" entry.`
50
35
  );
51
36
  }
52
37
 
38
+ // Identify the folder containing the main entry point
53
39
  const mainKeys = keysFromPath(mainPath);
54
- const mainContainerKeys = mainKeys.slice(0, -1);
55
- const mainFileName = mainKeys[mainKeys.length - 1];
56
- const mainContainer = await Tree.traverse(packageRoot, ...mainContainerKeys);
40
+ const mainFileName = mainKeys.pop();
41
+ const mainContainer = await Tree.traverse(packageRoot, ...mainKeys);
57
42
  const packageExports = await mainContainer.import(mainFileName);
58
43
 
59
44
  let result =
60
45
  "default" in packageExports ? packageExports.default : packageExports;
61
46
 
62
- if (keys.length > 0) {
63
- result = await Tree.traverse(result, ...keys);
47
+ // If there are remaining args, traverse into the package exports
48
+ if (args.length > 0) {
49
+ result = await Tree.traverse(result, ...args);
64
50
  }
65
51
 
66
52
  return result;
67
53
  }
54
+ packageProtocol.needsState = true;
@@ -9,20 +9,22 @@ import {
9
9
  import handleExtension from "./handleExtension.js";
10
10
  import { evaluate, ops } from "./internal.js";
11
11
 
12
+ export const KEY_TYPE = {
13
+ STRING: 0, // Simple string key: `a: 1`
14
+ COMPUTED: 1, // Computed key: `[code]: 1`
15
+ };
16
+
17
+ const VALUE_TYPE = {
18
+ PRIMITIVE: 0, // Primitive value: `a: 1`
19
+ EAGER: 1, // Calculated immediately: `a: 1 + 1`
20
+ GETTER: 2, // Calculated on demand: `a = fn()`
21
+ };
22
+
12
23
  /**
13
24
  * Given an array of entries with string keys and Origami code values (arrays of
14
25
  * ops and operands), return an object with the same keys defining properties
15
26
  * whose getters evaluate the code.
16
- *
17
- * The value can take three forms:
18
- *
19
- * 1. A primitive value (string, etc.). This will be defined directly as an
20
- * object property.
21
- * 1. An eager (as opposed to lazy) code entry. This will be evaluated during
22
- * this call and its result defined as an object property.
23
- * 1. A code entry that starts with ops.getter. This will be defined as a
24
- * property getter on the object.
25
- *
27
+
26
28
  * @param {*} entries
27
29
  * @param {import("../../index.ts").RuntimeState} [state]
28
30
  */
@@ -35,191 +37,201 @@ export default async function expressionObject(entries, state = {}) {
35
37
  }
36
38
  setParent(object, parent);
37
39
 
38
- // Get the keys, which might included computed keys
39
- const computedKeys = await Promise.all(
40
- entries.map(async ([key]) =>
41
- key instanceof Array ? await evaluate(key, state) : key
42
- )
43
- );
44
-
45
- let tree;
46
- const eagerProperties = [];
47
- const propertyIsEnumerable = {};
48
- let hasLazyProperties = false;
49
- for (let i = 0; i < entries.length; i++) {
50
- let key = computedKeys[i];
51
- let value = entries[i][1];
52
-
53
- // Determine if we need to define a getter or a regular property. If the key
54
- // has an extension, we need to define a getter. If the value is code (an
55
- // array), we need to define a getter -- but if that code takes the form
56
- // [ops.getter, <primitive>] or [ops.literal, <value>], we can define a
57
- // regular property.
58
- let defineProperty;
59
- const extname = extension.extname(key);
60
- if (extname) {
61
- defineProperty = false;
62
- } else if (!(value instanceof Array)) {
63
- defineProperty = true;
64
- } else if (value[0] === ops.getter && !(value[1] instanceof Array)) {
65
- defineProperty = true;
66
- value = value[1];
67
- } else if (value[0] === ops.literal) {
68
- defineProperty = true;
69
- value = value[1];
70
- } else {
71
- defineProperty = false;
40
+ // The object in Map form for use on the stack
41
+ const map = new ObjectMap(object);
42
+
43
+ // Preparation: gather information about all properties
44
+ const infos = entries.map(([key, value]) => propertyInfo(key, value));
45
+
46
+ // First pass: define all properties with plain string keys
47
+ for (const info of infos) {
48
+ if (info.keyType === KEY_TYPE.STRING) {
49
+ defineProperty(object, info, state, map);
72
50
  }
51
+ }
73
52
 
74
- // If the key is wrapped in parentheses, it is not enumerable.
75
- let enumerable = true;
76
- if (key[0] === "(" && key[key.length - 1] === ")") {
77
- key = key.slice(1, -1);
78
- enumerable = false;
53
+ // Second pass: redefine eager string-keyed properties with actual values.
54
+ for (const info of infos) {
55
+ if (
56
+ info.keyType === KEY_TYPE.STRING &&
57
+ info.valueType === VALUE_TYPE.EAGER
58
+ ) {
59
+ await redefineProperty(object, info);
79
60
  }
80
- propertyIsEnumerable[key] = enumerable;
81
-
82
- if (defineProperty) {
83
- // Define simple property
84
- Object.defineProperty(object, key, {
85
- configurable: true,
86
- enumerable,
87
- value,
88
- writable: true,
89
- });
90
- } else {
91
- // Property getter
92
- let code;
93
- if (value[0] === ops.getter) {
94
- hasLazyProperties = true;
95
- code = value[1];
96
- } else {
97
- eagerProperties.push(key);
98
- code = value;
99
- }
100
-
101
- const get = async () => {
102
- tree ??= new ObjectMap(object);
103
- const newState = Object.assign({}, state, { object: tree });
104
- const result = await evaluate(code, newState);
105
- return extname ? handleExtension(result, key, tree) : result;
106
- };
107
-
108
- Object.defineProperty(object, key, {
109
- configurable: true,
110
- enumerable,
111
- get,
112
- });
61
+ }
62
+
63
+ // Third pass: define all computed properties. These may refer to the
64
+ // properties we just defined.
65
+ for (const info of infos) {
66
+ if (info.keyType === KEY_TYPE.COMPUTED) {
67
+ const newState = Object.assign({}, state, { object: map });
68
+ const key = await evaluate(/** @type {any} */ (info.key), newState);
69
+ // Destructively update the property info with the computed key
70
+ info.key = key;
71
+ defineProperty(object, info, state, map);
113
72
  }
114
73
  }
115
74
 
116
- // Attach a keys method
75
+ // Fourth pass: redefine eager computed-keyed properties with actual values.
76
+ for (const info of infos) {
77
+ if (
78
+ info.keyType === KEY_TYPE.COMPUTED &&
79
+ info.valueType === VALUE_TYPE.EAGER
80
+ ) {
81
+ await redefineProperty(object, info);
82
+ }
83
+ }
84
+
85
+ // Attach a keys method, where keys for primitive/eager properties with
86
+ // maplike values get a trailing slash.
117
87
  Object.defineProperty(object, symbols.keys, {
118
88
  configurable: true,
119
89
  enumerable: false,
120
90
  value: () =>
121
- objectKeys(
122
- object,
123
- computedKeys,
124
- eagerProperties,
125
- propertyIsEnumerable,
126
- entries
127
- ),
91
+ infos
92
+ .filter((info) => info.enumerable)
93
+ .map((info) => normalizeKey(info, object)),
128
94
  writable: true,
129
95
  });
130
96
 
131
- // Evaluate any properties that were declared as immediate: get their value
132
- // and overwrite the property getter with the actual value.
133
- for (const key of eagerProperties) {
134
- const value = await object[key];
135
- const enumerable = Object.getOwnPropertyDescriptor(object, key)?.enumerable;
97
+ // TODO: If there are any getters, mark the object as async. Note: this code
98
+ // was added so that Tree.from() could know whether to return an ObjectMap or
99
+ // a hypothetical AsyncObjectMap, which in turn would let a map operation know
100
+ // whether to expect async property values. const hasGetters =
101
+ // infos.some((info) => info.valueType === VALUE_TYPE.GETTER); if (hasGetters)
102
+ // { Object.defineProperty(object, symbols.async, { configurable: true,
103
+ // enumerable: false, value: true, writable: true,
104
+ // });
105
+ // }
106
+
107
+ return object;
108
+ }
109
+
110
+ /**
111
+ * Define a single property on the object
112
+ */
113
+ function defineProperty(object, propertyInfo, state, map) {
114
+ let { enumerable, hasExtension, key, value, valueType } = propertyInfo;
115
+ if (valueType == VALUE_TYPE.PRIMITIVE) {
116
+ // Define simple property
136
117
  Object.defineProperty(object, key, {
137
118
  configurable: true,
138
119
  enumerable,
139
120
  value,
140
121
  writable: true,
141
122
  });
142
- }
143
-
144
- // If there are any getters, mark the object as async
145
- if (hasLazyProperties) {
146
- Object.defineProperty(object, symbols.async, {
123
+ } else {
124
+ // Eager or getter; will evaluate eager property later
125
+ Object.defineProperty(object, key, {
147
126
  configurable: true,
148
- enumerable: false,
149
- value: true,
150
- writable: true,
127
+ enumerable,
128
+ get: async () => {
129
+ const newState = Object.assign({}, state, { object: map });
130
+ const result = await evaluate(value, newState);
131
+ return hasExtension ? handleExtension(result, key, map) : result;
132
+ },
151
133
  });
152
134
  }
153
-
154
- return object;
155
135
  }
156
136
 
157
- export function entryKey(entry, object = null, eagerProperties = []) {
158
- let [key, value] = entry;
159
-
160
- if (typeof key !== "string") {
161
- // Computed property key
162
- return null;
163
- }
164
-
165
- if (key[0] === "(" && key[key.length - 1] === ")") {
166
- // Non-enumerable property, remove parentheses. This doesn't come up in the
167
- // constructor, but can happen in situations encountered by the compiler's
168
- // optimizer.
169
- key = key.slice(1, -1);
170
- }
137
+ /**
138
+ * Return a normalized version of the property key for use in the keys() method.
139
+ * Among other things, this adds trailing slashes to keys that correspond to
140
+ * maplike values.
141
+ */
142
+ export function normalizeKey(propertyInfo, object = null) {
143
+ const { key, value, valueType } = propertyInfo;
171
144
 
172
145
  if (trailingSlash.has(key)) {
173
146
  // Explicit trailing slash, return as is
174
147
  return key;
175
148
  }
176
149
 
177
- // If eager property value is maplike, add slash to the key
178
- if (eagerProperties.includes(key) && Tree.isMaplike(object?.[key])) {
150
+ // If actual property value is maplike, add slash
151
+ if (
152
+ (valueType === VALUE_TYPE.EAGER || valueType === VALUE_TYPE.PRIMITIVE) &&
153
+ Tree.isMaplike(object?.[key])
154
+ ) {
179
155
  return trailingSlash.add(key);
180
156
  }
181
157
 
158
+ // Look at value code to see if it will produce a maplike value
182
159
  if (!(value instanceof Array)) {
183
160
  // Can't be a subtree
184
161
  return trailingSlash.remove(key);
185
162
  }
186
-
187
- // If we're dealing with a getter, work with what that gets
188
- if (value[0] === ops.getter) {
189
- value = value[1];
190
- }
191
-
192
- // If entry will definitely create a subtree, add a trailing slash
193
163
  if (value[0] === ops.object) {
194
- // Subtree
164
+ // Creates an object; maplike
195
165
  return trailingSlash.add(key);
196
166
  }
197
-
198
- // See if it looks a merged object
199
167
  if (value[1] === "_result" && value[0][0] === ops.object) {
200
- // Merge
168
+ // Merges an object; maplike
201
169
  return trailingSlash.add(key);
202
170
  }
203
171
 
172
+ // Return as is
204
173
  return key;
205
174
  }
206
175
 
207
- function objectKeys(
208
- object,
209
- computedKeys,
210
- eagerProperties,
211
- propertyIsEnumerable,
212
- entries
213
- ) {
214
- // If the key is a simple string key and it's enumerable, get the friendly
215
- // version of it; if it's a computed key used that.
216
- const keys = entries.map((entry, index) =>
217
- typeof entry[0] !== "string"
218
- ? computedKeys[index]
219
- : propertyIsEnumerable[entry[0]]
220
- ? entryKey(entry, object, eagerProperties)
221
- : null
222
- );
223
- // Return the enumerable keys
224
- return keys.filter((key) => key !== null);
176
+ /**
177
+ * Given a key and the code for its value, determine some basic aspects of the
178
+ * property. This may return an updated key and/or value as well.
179
+ */
180
+ export function propertyInfo(key, value) {
181
+ // If the key is wrapped in parentheses, it is not enumerable.
182
+ let enumerable = true;
183
+ if (
184
+ typeof key === "string" &&
185
+ key[0] === "(" &&
186
+ key[key.length - 1] === ")"
187
+ ) {
188
+ key = key.slice(1, -1);
189
+ enumerable = false;
190
+ }
191
+
192
+ const keyType = key instanceof Array ? KEY_TYPE.COMPUTED : KEY_TYPE.STRING;
193
+
194
+ let valueType;
195
+ if (!(value instanceof Array)) {
196
+ // Primitive, no code to evaluate
197
+ valueType = VALUE_TYPE.PRIMITIVE;
198
+ } else if (value[0] !== ops.getter) {
199
+ // Code will be eagerly evaluated when object is constructed
200
+ valueType = VALUE_TYPE.EAGER;
201
+ } else {
202
+ // Defined as a getter
203
+ value = value[1]; // The actual code
204
+ if (!(value instanceof Array)) {
205
+ // Getter returns a primitive value; treat as regular property
206
+ valueType = VALUE_TYPE.PRIMITIVE;
207
+ } else if (value[0] === ops.literal) {
208
+ // Getter returns a literal value; treat as eager property
209
+ valueType = VALUE_TYPE.EAGER;
210
+ } else {
211
+ valueType = VALUE_TYPE.GETTER;
212
+ }
213
+ }
214
+
215
+ // Special case: a key with an extension has to be a getter
216
+ const hasExtension =
217
+ typeof key === "string" && extension.extname(key).length > 0;
218
+ if (hasExtension) {
219
+ valueType = VALUE_TYPE.GETTER;
220
+ }
221
+
222
+ return { enumerable, hasExtension, key, keyType, value, valueType };
223
+ }
224
+
225
+ /**
226
+ * Get the value of the indicated eager property and overwrite the property
227
+ * definition with the actual value.
228
+ */
229
+ async function redefineProperty(object, info) {
230
+ const value = await object[info.key];
231
+ Object.defineProperty(object, info.key, {
232
+ configurable: true,
233
+ enumerable: info.enumerable,
234
+ value,
235
+ writable: true,
236
+ });
225
237
  }
@@ -21,7 +21,6 @@ let projectGlobals;
21
21
  * @param {import("@weborigami/async-tree").SyncOrAsyncMap} [parent]
22
22
  */
23
23
  export default async function handleExtension(value, key, parent) {
24
- projectGlobals ??= await globals();
25
24
  if (isPacked(value) && isStringlike(key) && value.unpack === undefined) {
26
25
  const hasSlash = trailingSlash.has(key);
27
26
  if (hasSlash) {
@@ -34,7 +33,8 @@ export default async function handleExtension(value, key, parent) {
34
33
  : extension.extname(key);
35
34
  if (extname) {
36
35
  const handlerName = `${extname.slice(1)}_handler`;
37
- let handler = await projectGlobals[handlerName];
36
+ const handlers = await getHandlers(parent);
37
+ let handler = await handlers[handlerName];
38
38
  if (handler) {
39
39
  if (isUnpackable(handler)) {
40
40
  // The extension handler itself needs to be unpacked
@@ -66,3 +66,19 @@ export default async function handleExtension(value, key, parent) {
66
66
  }
67
67
  return value;
68
68
  }
69
+
70
+ async function getHandlers(parent) {
71
+ // Walk up the parent chain to find first `handlers` property
72
+ let current = parent;
73
+
74
+ while (current) {
75
+ if (current.handlers) {
76
+ return current.handlers;
77
+ }
78
+ current = current.parent;
79
+ }
80
+
81
+ // Fall back to project globals
82
+ projectGlobals ??= await globals();
83
+ return projectGlobals;
84
+ }
@@ -172,7 +172,7 @@ describe("Origami parser", () => {
172
172
  ops.lambda,
173
173
  0,
174
174
  [],
175
- [[markers.traverse, [markers.reference, "fn"]], undefined],
175
+ [[markers.traverse, [markers.reference, "fn"]]],
176
176
  ],
177
177
  ],
178
178
  ],
@@ -371,7 +371,7 @@ describe("Origami parser", () => {
371
371
  describe("callExpression", () => {
372
372
  test("call chains", () => {
373
373
  assertParse("callExpression", "(foo.js())('arg')", [
374
- [[markers.traverse, [markers.reference, "foo.js"]], undefined],
374
+ [[markers.traverse, [markers.reference, "foo.js"]]],
375
375
  [ops.literal, "arg"],
376
376
  ]);
377
377
  assertParse("callExpression", "fn('a')('b')", [
@@ -382,7 +382,7 @@ describe("Origami parser", () => {
382
382
  [ops.literal, "b"],
383
383
  ]);
384
384
  assertParse("callExpression", "(foo.js())(a, b)", [
385
- [[markers.traverse, [markers.reference, "foo.js"]], undefined],
385
+ [[markers.traverse, [markers.reference, "foo.js"]]],
386
386
  [markers.traverse, [markers.reference, "a"]],
387
387
  [markers.traverse, [markers.reference, "b"]],
388
388
  ]);
@@ -546,7 +546,6 @@ describe("Origami parser", () => {
546
546
  test("parentheses arguments", () => {
547
547
  assertParse("callExpression", "fn()", [
548
548
  [markers.traverse, [markers.reference, "fn"]],
549
- undefined,
550
549
  ]);
551
550
  assertParse("callExpression", "foo.js(arg)", [
552
551
  [markers.traverse, [markers.reference, "foo.js"]],
@@ -563,7 +562,7 @@ describe("Origami parser", () => {
563
562
  [markers.traverse, [markers.reference, "b"]],
564
563
  ]);
565
564
  assertParse("callExpression", "fn()(arg)", [
566
- [[markers.traverse, [markers.reference, "fn"]], undefined],
565
+ [[markers.traverse, [markers.reference, "fn"]]],
567
566
  [markers.traverse, [markers.reference, "arg"]],
568
567
  ]);
569
568
  });
@@ -617,19 +616,14 @@ describe("Origami parser", () => {
617
616
 
618
617
  test("path and parentheses chains", () => {
619
618
  assertParse("callExpression", "foo.js()/key", [
620
- [[markers.traverse, [markers.reference, "foo.js"]], undefined],
619
+ [[markers.traverse, [markers.reference, "foo.js"]]],
621
620
  [ops.literal, "key"],
622
621
  ]);
623
622
  assertParse("callExpression", "tree/key()", [
624
623
  [markers.traverse, [markers.reference, "tree/"], [ops.literal, "key"]],
625
- undefined,
626
624
  ]);
627
625
  assertParse("callExpression", "fn()/key()", [
628
- [
629
- [[markers.traverse, [markers.reference, "fn"]], undefined],
630
- [ops.literal, "key"],
631
- ],
632
- undefined,
626
+ [[[markers.traverse, [markers.reference, "fn"]]], [ops.literal, "key"]],
633
627
  ]);
634
628
  assertParse("callExpression", "package:@weborigami/dropbox/auth(creds)", [
635
629
  [
@@ -1035,7 +1029,7 @@ Body`,
1035
1029
  ops.lambda,
1036
1030
  1,
1037
1031
  [["name", [[ops.params, 0], 0]]],
1038
- [[markers.traverse, [markers.reference, "_template"]], undefined],
1032
+ [[markers.traverse, [markers.reference, "_template"]]],
1039
1033
  ],
1040
1034
  "program",
1041
1035
  false
@@ -1053,7 +1047,6 @@ Body`,
1053
1047
  ]);
1054
1048
  assertParse("group", "(fn())", [
1055
1049
  [markers.traverse, [markers.reference, "fn"]],
1056
- undefined,
1057
1050
  ]);
1058
1051
  assertParse("group", "(a -> b)", [
1059
1052
  [markers.traverse, [markers.reference, "b"]],
@@ -1105,7 +1098,7 @@ Body`,
1105
1098
  [markers.traverse, [markers.reference, "c"]],
1106
1099
  ]);
1107
1100
  assertParse("implicitParenthesesCallExpression", "(fn()) 'arg'", [
1108
- [[markers.traverse, [markers.reference, "fn"]], undefined],
1101
+ [[markers.traverse, [markers.reference, "fn"]]],
1109
1102
  [ops.literal, "arg"],
1110
1103
  ]);
1111
1104
  assertParse(
@@ -1311,10 +1304,7 @@ Body`,
1311
1304
  ops.object,
1312
1305
  [
1313
1306
  "b",
1314
- [
1315
- ops.getter,
1316
- [[markers.traverse, [markers.reference, "fn"]], undefined],
1317
- ],
1307
+ [ops.getter, [[markers.traverse, [markers.reference, "fn"]]]],
1318
1308
  ],
1319
1309
  ],
1320
1310
  ],
@@ -1506,7 +1496,7 @@ Body`,
1506
1496
  });
1507
1497
 
1508
1498
  test("parenthesesArguments", () => {
1509
- assertParse("parenthesesArguments", "()", [undefined]);
1499
+ assertParse("parenthesesArguments", "()", []);
1510
1500
  assertParse("parenthesesArguments", "(a, b, c)", [
1511
1501
  [markers.traverse, [markers.reference, "a"]],
1512
1502
  [markers.traverse, [markers.reference, "b"]],
@@ -2,9 +2,9 @@ import assert from "node:assert";
2
2
  import path from "node:path";
3
3
  import { describe, test } from "node:test";
4
4
  import { fileURLToPath } from "node:url";
5
- import projectRoot from "../../src/project/projectRoot.js";
5
+ import projectRootFromPath from "../../src/project/projectRootFromPath.js";
6
6
 
7
- describe("projectRoot", () => {
7
+ describe("projectRootFromPath", () => {
8
8
  test("finds Origami configuration file", async () => {
9
9
  // Find the folder that represents the project root.
10
10
  const projectUrl = new URL("fixtures/withConfig/", import.meta.url);
@@ -12,7 +12,7 @@ describe("projectRoot", () => {
12
12
  const subfolderUrl = new URL("./subfolder/", projectUrl);
13
13
  const subfolderPath = fileURLToPath(subfolderUrl);
14
14
 
15
- const root = await projectRoot(subfolderPath);
15
+ const root = await projectRootFromPath(subfolderPath);
16
16
 
17
17
  // Get result path, it'll need a trailing slash to compare.
18
18
  const resultPath = root.path + path.sep;
@@ -26,7 +26,7 @@ describe("projectRoot", () => {
26
26
  const subfolderUrl = new URL("./subfolder/", projectUrl);
27
27
  const subfolderPath = fileURLToPath(subfolderUrl);
28
28
 
29
- const root = await projectRoot(subfolderPath);
29
+ const root = await projectRootFromPath(subfolderPath);
30
30
 
31
31
  // Get result path, it'll need a trailing slash to compare.
32
32
  const resultPath = root.path + path.sep;
@@ -34,7 +34,7 @@ describe("projectRoot", () => {
34
34
  });
35
35
 
36
36
  test("defaults to current working directory", async () => {
37
- const root = await projectRoot("/");
37
+ const root = await projectRootFromPath("/");
38
38
  assert.equal(root.path, process.cwd());
39
39
  });
40
40
  });
@@ -1,10 +1,15 @@
1
1
  import assert from "node:assert";
2
2
  import { describe, test } from "node:test";
3
+ import projectRootFromPath from "../../src/project/projectRootFromPath.js";
3
4
  import packageProtocol from "../../src/protocols/package.js";
4
5
 
5
6
  describe("package: protocol", () => {
6
7
  test("returns a package's main export(s)", async () => {
7
- const result = await packageProtocol("@weborigami", "async-tree");
8
+ // Reproduce the type of evaluation context object the runtime would create
9
+ const projectRoot = await projectRootFromPath();
10
+ const context = { container: projectRoot };
11
+
12
+ const result = await packageProtocol("@weborigami", "async-tree", context);
8
13
  const { toString } = result;
9
14
  assert.equal(toString(123), "123");
10
15
  });
@@ -53,11 +53,26 @@ describe("expressionObject", () => {
53
53
  });
54
54
 
55
55
  test("can compute a property key", async () => {
56
- const entries = [[[ops.concat, "data", ".json"], 1]];
56
+ const entries = [
57
+ [
58
+ [
59
+ ops.concat,
60
+ [
61
+ [ops.inherited, 0],
62
+ [ops.literal, "name"], // references `name` on same object
63
+ ],
64
+ ".json",
65
+ ],
66
+ 1,
67
+ ],
68
+ ["name", "data"],
69
+ ];
57
70
  const context = new SyncMap();
58
71
  const object = await expressionObject(entries, { object: context });
59
- assert.equal(await object["data.json"], 1);
60
- assert.deepEqual(object[symbols.keys](), ["data.json"]);
72
+ assert.deepEqual(await Tree.plain(object), {
73
+ "data.json": 1,
74
+ name: "data",
75
+ });
61
76
  });
62
77
 
63
78
  test("returned object values can be unpacked", async () => {
@@ -79,7 +94,7 @@ describe("expressionObject", () => {
79
94
  assert.equal(object["hidden"], "shh");
80
95
  });
81
96
 
82
- test("provides a symbols.keys method", async () => {
97
+ test("provides a symbols.keys method returning normalized keys", async () => {
83
98
  const entries = [
84
99
  // Will return a tree, should have a slash
85
100
  ["getter", [ops.getter, [ops.object, ["b", [ops.literal, 2]]]]],
@@ -100,11 +115,11 @@ describe("expressionObject", () => {
100
115
  ]);
101
116
  });
102
117
 
103
- test("sets symbols.async on objects with getters", async () => {
104
- const noGetter = await expressionObject([["eager", 1]]);
105
- assert.equal(noGetter[symbols.async], undefined);
118
+ // test("sets symbols.async on objects with getters", async () => {
119
+ // const noGetter = await expressionObject([["eager", 1]]);
120
+ // assert.equal(noGetter[symbols.async], undefined);
106
121
 
107
- const hasGetter = await expressionObject([["lazy", [ops.getter, [2]]]]);
108
- assert.equal(hasGetter[symbols.async], true);
109
- });
122
+ // const hasGetter = await expressionObject([["lazy", [ops.getter, [2]]]]);
123
+ // assert.equal(hasGetter[symbols.async], true);
124
+ // });
110
125
  });