@weborigami/language 0.6.10 → 0.6.12

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
@@ -13,11 +13,9 @@ export { default as projectRoot } from "./src/project/projectRoot.js";
13
13
  export { default as projectRootFromPath } from "./src/project/projectRootFromPath.js";
14
14
  export * as Protocols from "./src/protocols/protocols.js";
15
15
  export { formatError, highlightError, lineInfo } from "./src/runtime/errors.js";
16
+ export { default as evaluate } from "./src/runtime/evaluate.js";
16
17
  export { default as EventTargetMixin } from "./src/runtime/EventTargetMixin.js";
17
- export {
18
- default as evaluate,
19
- default as execute,
20
- } from "./src/runtime/execute.js";
18
+ export { default as execute } from "./src/runtime/execute.js";
21
19
  export * as expressionFunction from "./src/runtime/expressionFunction.js";
22
20
  export { default as expressionObject } from "./src/runtime/expressionObject.js";
23
21
  export * from "./src/runtime/handleExtension.js";
package/package.json CHANGED
@@ -1,20 +1,20 @@
1
1
  {
2
2
  "name": "@weborigami/language",
3
- "version": "0.6.10",
3
+ "version": "0.6.12",
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.10.1",
9
+ "@types/node": "25.3.2",
10
10
  "peggy": "5.0.6",
11
11
  "typescript": "5.9.3"
12
12
  },
13
13
  "dependencies": {
14
- "@weborigami/async-tree": "0.6.10",
14
+ "@weborigami/async-tree": "0.6.12",
15
15
  "exif-parser": "0.1.12",
16
16
  "watcher": "2.3.1",
17
- "yaml": "2.8.1"
17
+ "yaml": "2.8.2"
18
18
  },
19
19
  "scripts": {
20
20
  "build": "peggy --allowed-start-rules=\"*\" --format es src/compiler/origami.pegjs --output src/compiler/parse.js",
@@ -845,8 +845,10 @@ relationalExpression
845
845
  // We disallow a newline before the relational operator to support a newline
846
846
  // as a separator in an object literal that has an object shorthand property
847
847
  // with an angle bracket path. Otherwise the opening angle bracket would be
848
- // interpreted as a relational operator.
849
- = head:shiftExpression tail:(inlineSpace @relationalOperator __ @shiftExpression)* {
848
+ // interpreted as a relational operator. In shell mode we require a space to
849
+ // avoid ambiguity with an angle bracket literal as an argument in an implicit
850
+ // parentheses call.
851
+ = head:shiftExpression tail:(inlineSpace @relationalOperator whitespaceRequiredForShell @shiftExpression)* {
850
852
  return tail.reduce(makeBinaryOperation, head);
851
853
  }
852
854
 
@@ -1039,9 +1041,9 @@ uriKey
1039
1041
 
1040
1042
  // A single character in a URI key
1041
1043
  uriKeyChar
1042
- // Accept anything that doesn't end the URI key or path
1043
- // Reject whitespace; see notes for `whitespace` term
1044
- = char:[^/,\)\]\}] !&{ return /\s/.test(char); } { return char; }
1044
+ // Accept anything except whitespace (see notes for `whitespace` term),
1045
+ // brackets, comma, or slash
1046
+ = char:[^/,\(\[\{\)\]\}] !&{ return /\s/.test(char); } { return char; }
1045
1047
  / escapedChar
1046
1048
 
1047
1049
  // A slash-separated path of keys: `a/b/c`
@@ -1071,6 +1073,10 @@ whitespaceOptionalForProgram
1071
1073
  = programMode __
1072
1074
  / shellMode
1073
1075
 
1076
+ whitespaceRequiredForShell
1077
+ = shellMode whitespace
1078
+ / programMode __
1079
+
1074
1080
  whitespaceOrParenthesis
1075
1081
  = whitespace
1076
1082
  / "("
@@ -357,6 +357,7 @@ function peg$parse(input, options) {
357
357
  whitespace: peg$parsewhitespace,
358
358
  whitespaceChar: peg$parsewhitespaceChar,
359
359
  whitespaceOptionalForProgram: peg$parsewhitespaceOptionalForProgram,
360
+ whitespaceRequiredForShell: peg$parsewhitespaceRequiredForShell,
360
361
  whitespaceOrParenthesis: peg$parsewhitespaceOrParenthesis,
361
362
  whitespaceWithNewLine: peg$parsewhitespaceWithNewLine,
362
363
  };
@@ -459,7 +460,7 @@ function peg$parse(input, options) {
459
460
  const peg$r19 = /^[^\n\r]/;
460
461
  const peg$r20 = /^[!+]/;
461
462
  const peg$r21 = /^[\/)\]}]/;
462
- const peg$r22 = /^[^\/,)\]}]/;
463
+ const peg$r22 = /^[^\/,([{)\]}]/;
463
464
  const peg$r23 = /^[a-z]/;
464
465
  const peg$r24 = /^[a-z0-9+-.]/;
465
466
  const peg$r25 = /^[:]/;
@@ -589,7 +590,7 @@ function peg$parse(input, options) {
589
590
  const peg$e122 = peg$literalExpectation("await", false);
590
591
  const peg$e123 = peg$literalExpectation("typeof", false);
591
592
  const peg$e124 = peg$literalExpectation("void", false);
592
- const peg$e125 = peg$classExpectation(["/", ",", ")", "]", "}"], true, false, false);
593
+ const peg$e125 = peg$classExpectation(["/", ",", "(", "[", "{", ")", "]", "}"], true, false, false);
593
594
  const peg$e126 = peg$otherExpectation("slash-separated path");
594
595
  const peg$e127 = peg$classExpectation([["a", "z"]], false, false, false);
595
596
  const peg$e128 = peg$classExpectation([["a", "z"], ["0", "9"], ["+", "."]], false, false, false);
@@ -6313,10 +6314,15 @@ function peg$parse(input, options) {
6313
6314
  if (s4 !== peg$FAILED) {
6314
6315
  s5 = peg$parserelationalOperator();
6315
6316
  if (s5 !== peg$FAILED) {
6316
- s6 = peg$parse__();
6317
- s7 = peg$parseshiftExpression();
6318
- if (s7 !== peg$FAILED) {
6319
- s3 = [ s5, s7 ];
6317
+ s6 = peg$parsewhitespaceRequiredForShell();
6318
+ if (s6 !== peg$FAILED) {
6319
+ s7 = peg$parseshiftExpression();
6320
+ if (s7 !== peg$FAILED) {
6321
+ s3 = [ s5, s7 ];
6322
+ } else {
6323
+ peg$currPos = s3;
6324
+ s3 = peg$FAILED;
6325
+ }
6320
6326
  } else {
6321
6327
  peg$currPos = s3;
6322
6328
  s3 = peg$FAILED;
@@ -6336,10 +6342,15 @@ function peg$parse(input, options) {
6336
6342
  if (s4 !== peg$FAILED) {
6337
6343
  s5 = peg$parserelationalOperator();
6338
6344
  if (s5 !== peg$FAILED) {
6339
- s6 = peg$parse__();
6340
- s7 = peg$parseshiftExpression();
6341
- if (s7 !== peg$FAILED) {
6342
- s3 = [ s5, s7 ];
6345
+ s6 = peg$parsewhitespaceRequiredForShell();
6346
+ if (s6 !== peg$FAILED) {
6347
+ s7 = peg$parseshiftExpression();
6348
+ if (s7 !== peg$FAILED) {
6349
+ s3 = [ s5, s7 ];
6350
+ } else {
6351
+ peg$currPos = s3;
6352
+ s3 = peg$FAILED;
6353
+ }
6343
6354
  } else {
6344
6355
  peg$currPos = s3;
6345
6356
  s3 = peg$FAILED;
@@ -7834,6 +7845,40 @@ function peg$parse(input, options) {
7834
7845
  return s0;
7835
7846
  }
7836
7847
 
7848
+ function peg$parsewhitespaceRequiredForShell() {
7849
+ let s0, s1, s2;
7850
+
7851
+ s0 = peg$currPos;
7852
+ s1 = peg$parseshellMode();
7853
+ if (s1 !== peg$FAILED) {
7854
+ s2 = peg$parsewhitespace();
7855
+ if (s2 !== peg$FAILED) {
7856
+ s1 = [s1, s2];
7857
+ s0 = s1;
7858
+ } else {
7859
+ peg$currPos = s0;
7860
+ s0 = peg$FAILED;
7861
+ }
7862
+ } else {
7863
+ peg$currPos = s0;
7864
+ s0 = peg$FAILED;
7865
+ }
7866
+ if (s0 === peg$FAILED) {
7867
+ s0 = peg$currPos;
7868
+ s1 = peg$parseprogramMode();
7869
+ if (s1 !== peg$FAILED) {
7870
+ s2 = peg$parse__();
7871
+ s1 = [s1, s2];
7872
+ s0 = s1;
7873
+ } else {
7874
+ peg$currPos = s0;
7875
+ s0 = peg$FAILED;
7876
+ }
7877
+ }
7878
+
7879
+ return s0;
7880
+ }
7881
+
7837
7882
  function peg$parsewhitespaceOrParenthesis() {
7838
7883
  let s0;
7839
7884
 
@@ -8075,6 +8120,7 @@ const peg$allowedStartRules = [
8075
8120
  "whitespace",
8076
8121
  "whitespaceChar",
8077
8122
  "whitespaceOptionalForProgram",
8123
+ "whitespaceRequiredForShell",
8078
8124
  "whitespaceOrParenthesis",
8079
8125
  "whitespaceWithNewLine"
8080
8126
  ];
@@ -634,8 +634,7 @@ function makePossibleSpreadCall(target, args, location) {
634
634
  return [target, ...args];
635
635
  }
636
636
 
637
- // Get function's apply method
638
- const applyMethod = annotate([ops.property, target, "apply"], location);
637
+ // We'll need to use ops.apply and ops.flat to handle the spreads.
639
638
  const wrappedArgs = args.map((arg) => {
640
639
  if (arg[0] === markers.spread) {
641
640
  return arg[1];
@@ -643,8 +642,8 @@ function makePossibleSpreadCall(target, args, location) {
643
642
  return annotate([ops.array, arg], arg.location);
644
643
  }
645
644
  });
646
- const flatCall = annotate([ops.flat, ...wrappedArgs], location);
647
- return [applyMethod, null, flatCall];
645
+ const flattened = annotate([ops.flat, ...wrappedArgs], location);
646
+ return annotate([ops.apply, target, flattened], location);
648
647
  }
649
648
 
650
649
  /**
@@ -26,10 +26,15 @@ export default {
26
26
  * Supports multiple commands, pipelines, redirects, etc.
27
27
  *
28
28
  * @param {string} scriptText - Shell code (may contain newlines/side effects)
29
- * @param {string} inputText - Text to pipe to the script's stdin
29
+ * @param {import("@weborigami/async-tree").Stringlike} inputText - Text to pipe to the script's stdin
30
30
  * @returns {Promise<string>}
31
31
  */
32
32
  function runShellScript(scriptText, inputText) {
33
+ if (inputText instanceof Function) {
34
+ throw new Error(
35
+ "A .sh file expects text input but got a function instead. Did you mean to invoke the function?",
36
+ );
37
+ }
33
38
  return new Promise((resolve, reject) => {
34
39
  // Use sh -c "<scriptText>" so stdin is free for inputText
35
40
  const child = spawn("sh", ["-c", scriptText], {
@@ -49,7 +54,7 @@ function runShellScript(scriptText, inputText) {
49
54
  if (code !== 0) {
50
55
  /** @type {any} */
51
56
  const err = new Error(
52
- `Shell exited with code ${code}${stderr ? `: ${stderr}` : ""}`
57
+ `Shell exited with code ${code}${stderr ? `: ${stderr}` : ""}`,
53
58
  );
54
59
  err.code = code;
55
60
  err.stdout = stdout;
@@ -1,4 +1,5 @@
1
1
  import {
2
+ castArraylike,
2
3
  getParent,
3
4
  isUnpackable,
4
5
  symbols,
@@ -82,8 +83,13 @@ export default {
82
83
  }
83
84
 
84
85
  if (hasOriTags) {
85
- // Resolve any promises in the data.
86
- data = await Tree.plain(data);
86
+ // Invoke any functions and resolve any promises in the deep data.
87
+ const tree = Tree.from(data, { deep: true });
88
+ data = await Tree.mapReduce(
89
+ tree,
90
+ async (value) => (value instanceof Function ? await value() : value),
91
+ (mapped) => castArraylike(mapped),
92
+ );
87
93
  }
88
94
 
89
95
  if (data && typeof data === "object" && Object.isExtensible(data)) {
@@ -128,7 +128,7 @@ const globals = {
128
128
  encodeURIComponent,
129
129
  escape,
130
130
  eval,
131
- // fetch -- special case, see below
131
+ fetch,
132
132
  globalThis,
133
133
  isFinite,
134
134
  isNaN,
@@ -150,7 +150,6 @@ const globals = {
150
150
  true: true,
151
151
 
152
152
  // Special cases
153
- fetch: fetchWrapper,
154
153
  import: importWrapper,
155
154
  };
156
155
 
@@ -160,14 +159,6 @@ Object.defineProperty(globals, "globalThis", {
160
159
  value: globals,
161
160
  });
162
161
 
163
- async function fetchWrapper(resource, options) {
164
- console.warn(
165
- "Warning: A plain `fetch` reference will eventually call the standard JavaScript fetch() function. For Origami's fetch behavior, update your code to call Origami.fetch().",
166
- );
167
- const response = await fetch(resource, options);
168
- return response.ok ? await response.arrayBuffer() : undefined;
169
- }
170
-
171
162
  /**
172
163
  * @typedef {import("@weborigami/async-tree").AsyncMap} AsyncMap
173
164
  *
@@ -1,4 +1,4 @@
1
- import { Tree, keysFromPath } from "@weborigami/async-tree";
1
+ import { Tree, keysFromPath, pathFromKeys } from "@weborigami/async-tree";
2
2
  import projectRoot from "../project/projectRoot.js";
3
3
 
4
4
  /**
@@ -11,27 +11,36 @@ export default async function packageProtocol(...args) {
11
11
  const root = await projectRoot(state);
12
12
 
13
13
  // Identify the path to the package root
14
- const packageRootPath = ["node_modules"];
15
- const name = args.shift();
16
- packageRootPath.push(name);
14
+ const packageRootKeys = ["node_modules"];
15
+ let name = args.shift();
16
+ packageRootKeys.push(name);
17
17
  if (name.startsWith("@")) {
18
18
  // First key is an npm organization, add next key as name
19
- packageRootPath.push(args.shift());
19
+ const nameArg = args.shift();
20
+ name += nameArg;
21
+ packageRootKeys.push(nameArg);
20
22
  }
23
+ const packageRootPath = pathFromKeys(packageRootKeys);
21
24
 
22
25
  // Get the package root (top level folder of the package)
23
- const packageRoot = await Tree.traverse(root, ...packageRootPath);
26
+ let packageRoot = await Tree.traverse(root, ...packageRootKeys);
24
27
  if (!packageRoot) {
25
- throw new Error(`Can't find ${packageRootPath.join("/")}`);
28
+ // Can't find package -- are we *in* the package?
29
+ const packageJson = await Tree.traverse(root, "package.json");
30
+ const packageData = await packageJson?.unpack();
31
+ if (packageData?.name === name) {
32
+ // Yes, we're in the package itself
33
+ packageRoot = root;
34
+ } else {
35
+ throw new Error(`Can't find ${packageRootPath}`);
36
+ }
26
37
  }
27
38
 
28
39
  // Identify the main entry point
29
40
  const mainPath = await Tree.traverse(packageRoot, "package.json", "main");
30
41
  if (mainPath === undefined) {
31
42
  throw new Error(
32
- `${packageRootPath.join(
33
- "/",
34
- )} doesn't contain a package.json with a "main" entry.`,
43
+ `${packageRootPath} doesn't contain a package.json with a "main" entry.`,
35
44
  );
36
45
  }
37
46
 
@@ -1,5 +1,7 @@
1
1
  import { Mixin } from "../../index.ts";
2
2
 
3
- declare const WatchFilesMixin: Mixin<{}>;
3
+ declare const WatchFilesMixin: Mixin<{
4
+ watch(): Promise<void>;
5
+ }>;
4
6
 
5
7
  export default WatchFilesMixin;
@@ -1,4 +1,5 @@
1
1
  import * as fs from "node:fs";
2
+ import path from "node:path";
2
3
  import Watcher from "watcher";
3
4
  import TreeEvent from "./TreeEvent.js";
4
5
 
@@ -17,6 +18,12 @@ export default function WatchFilesMixin(Base) {
17
18
  onChange(filePath) {
18
19
  // Reset cached values.
19
20
  this.subfoldersMap = new Map();
21
+
22
+ // Special case: ignore events in .git folder
23
+ if (filePath.includes(`${path.sep}.git${path.sep}`)) {
24
+ return;
25
+ }
26
+
20
27
  this.dispatchEvent(new TreeEvent("change", { filePath }));
21
28
  }
22
29
 
@@ -20,6 +20,11 @@ const origamiSourceSignals = [
20
20
  * @param {Error} error
21
21
  */
22
22
  export async function formatError(error) {
23
+ // We want to display information for the root cause
24
+ while (error.cause instanceof Error) {
25
+ error = error.cause;
26
+ }
27
+
23
28
  // Get the original error message
24
29
  let originalMessage;
25
30
  // If the first line of the stack is just the error message, use that as the message
@@ -19,23 +19,13 @@ export default async function execute(code, state = {}) {
19
19
  return code;
20
20
  }
21
21
 
22
- let evaluated;
23
- if (code[0]?.unevaluatedArgs) {
24
- // Don't evaluate instructions, use as is.
25
- evaluated = code;
26
- } else {
27
- // Evaluate each instruction in the code.
28
- evaluated = await Promise.all(
29
- code.map((instruction) => execute(instruction, state)),
30
- );
31
- }
32
-
33
22
  // Add the code to the runtime state
34
23
  /** @type {import("../../index.ts").CodeContext} */
35
24
  const context = { state, code };
36
25
 
37
- // The head of the array is a function or a map; the rest are args or keys.
38
- let [fn, ...args] = evaluated;
26
+ // Start by evaluating the head of the instruction
27
+ const [head, ...tail] = code;
28
+ let fn = await execute(head, state);
39
29
 
40
30
  if (!fn) {
41
31
  // The code wants to invoke something that's couldn't be found in scope.
@@ -53,9 +43,23 @@ export default async function execute(code, state = {}) {
53
43
  fn = await fn.unpack();
54
44
  }
55
45
 
46
+ let args;
47
+ if (fn?.unevaluatedArgs) {
48
+ // Don't evaluate instructions, use as is.
49
+ args = tail;
50
+ } else {
51
+ // Evaluate each instruction in the code.
52
+ args = await Promise.all(
53
+ tail.map((instruction) => execute(instruction, state)),
54
+ );
55
+ }
56
+
56
57
  if (fn.needsState) {
57
58
  // The function is an op that wants the runtime state
58
59
  args.push(state);
60
+ } else if (fn.needsContext) {
61
+ // The function is an op that wants the code context
62
+ args.push(context);
59
63
  } else if (fn.parentAsTarget && state.parent) {
60
64
  // The function wants the code's parent as the `this` target
61
65
  fn = fn.bind(state.parent);
@@ -26,6 +26,28 @@ export function addition(a, b) {
26
26
  }
27
27
  addOpLabel(addition, "«ops.addition»");
28
28
 
29
+ /**
30
+ * Flatten the arguments and then apply the function.
31
+ * This is used to handle spreads in function calls.
32
+ */
33
+ export async function apply(fn, args, state) {
34
+ // TODO: This is starting to recapitulate much of execute()
35
+ if (isUnpackable(fn)) {
36
+ fn = await fn.unpack();
37
+ }
38
+ if (fn.needsState) {
39
+ // The function is an op that wants the runtime state
40
+ args.push(state);
41
+ }
42
+ const result =
43
+ fn instanceof Function
44
+ ? await fn(...args) // Invoke the function
45
+ : await Tree.traverseOrThrow(fn, ...args); // Traverse the map.
46
+ return result;
47
+ }
48
+ addOpLabel(apply, "«ops.apply»");
49
+ apply.needsState = true;
50
+
29
51
  /**
30
52
  * Construct an array.
31
53
  *
@@ -157,11 +179,14 @@ addOpLabel(exponentiation, "«ops.exponentiation»");
157
179
  */
158
180
  export async function flat(...args) {
159
181
  const arrays = await Promise.all(
160
- args.map(async (arg) =>
161
- arg instanceof Array || typeof arg !== "object"
182
+ args.map(async (arg) => {
183
+ if (isUnpackable(arg)) {
184
+ arg = await arg.unpack();
185
+ }
186
+ return arg instanceof Array || typeof arg !== "object"
162
187
  ? arg
163
- : await Tree.values(arg),
164
- ),
188
+ : await Tree.values(arg);
189
+ }),
165
190
  );
166
191
 
167
192
  return arrays.flat();
@@ -569,8 +569,8 @@ describe("Origami parser", () => {
569
569
 
570
570
  test("parentheses arguments with spreads", () => {
571
571
  assertParse("callExpression", "fn(a, ...b, ...c)", [
572
- [ops.property, [markers.traverse, [markers.reference, "fn"]], "apply"],
573
- null,
572
+ ops.apply,
573
+ [markers.traverse, [markers.reference, "fn"]],
574
574
  [
575
575
  ops.flat,
576
576
  [ops.array, [markers.traverse, [markers.reference, "a"]]],
@@ -1126,12 +1126,8 @@ Body`,
1126
1126
  "implicitParenthesesCallExpression",
1127
1127
  "concat a.json, ...b.json, c.json",
1128
1128
  [
1129
- [
1130
- ops.property,
1131
- [markers.traverse, [markers.reference, "concat"]],
1132
- "apply",
1133
- ],
1134
- null,
1129
+ ops.apply,
1130
+ [markers.traverse, [markers.reference, "concat"]],
1135
1131
  [
1136
1132
  ops.flat,
1137
1133
  [ops.array, [markers.traverse, [markers.reference, "a.json"]]],
@@ -20,6 +20,12 @@ describe("ops", () => {
20
20
  );
21
21
  });
22
22
 
23
+ test("ops.apply applies a function to arguments", async () => {
24
+ const code = createCode([ops.apply, ops.addition, [ops.array, 1, 2]]);
25
+ const result = await execute(code);
26
+ assert.strictEqual(result, 3);
27
+ });
28
+
23
29
  test("ops.array creates an array", async () => {
24
30
  const code = createCode([ops.array, 1, 2, 3]);
25
31
  const result = await execute(code);