@weborigami/language 0.6.4 → 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.
@@ -16,8 +16,13 @@ const YAML = YAMLModule.default ?? YAMLModule.YAML;
16
16
 
17
17
  // Markers in compiled output, will get optimized away
18
18
  export const markers = {
19
- global: Symbol("global"), // Global reference
20
19
  external: Symbol("external"), // External reference
20
+ global: Symbol("global"), // Global reference
21
+ paramArray: Symbol("paramArray"), // Parameter array destructuring
22
+ paramInitializer: Symbol("paramInitializer"), // Parameter default value
23
+ paramName: Symbol("paramName"), // Parameter name
24
+ paramObject: Symbol("paramObject"), // Parameter object destructuring
25
+ paramRest: Symbol("paramRest"), // Rest operator in parameters
21
26
  property: Symbol("property"), // Property access
22
27
  reference: Symbol("reference"), // Reference to local, scope, or global
23
28
  spread: Symbol("spread"), // Spread operator
@@ -69,6 +74,21 @@ export function applyMacro(code, name, macro) {
69
74
  return annotate(applied, code.location);
70
75
  }
71
76
 
77
+ function checkDuplicateParamNames(flat) {
78
+ const names = new Set();
79
+ for (const binding of flat) {
80
+ const paramName = binding[0];
81
+ if (names.has(paramName)) {
82
+ const error = new SyntaxError(`Duplicate parameter name "${paramName}"`);
83
+ /** @type {any} */ (error).location = /** @type {any} */ (
84
+ binding
85
+ ).location;
86
+ throw error;
87
+ }
88
+ names.add(paramName);
89
+ }
90
+ }
91
+
72
92
  /**
73
93
  * Create an array
74
94
  *
@@ -236,10 +256,8 @@ export function makeDeferredArguments(args) {
236
256
  if (arg instanceof Array && arg[0] === ops.literal) {
237
257
  return arg;
238
258
  }
239
- const lambdaParameters = annotate([], arg.location);
240
- /** @type {AnnotatedCodeItem} */
241
- const fn = [ops.lambda, lambdaParameters, arg];
242
- return annotate(fn, arg.location);
259
+ const params = annotate([], arg.location);
260
+ return makeLambda(params, arg, arg.location);
243
261
  });
244
262
  }
245
263
 
@@ -257,6 +275,40 @@ export function makeDocument(front, body, location) {
257
275
  return annotate([ops.object, ...entries], location);
258
276
  }
259
277
 
278
+ /**
279
+ * Create a lambda function with the given parameters.
280
+ *
281
+ * @param {AnnotatedCode} params
282
+ * @param {AnnotatedCode} body
283
+ * @param {CodeLocation} location
284
+ */
285
+ export function makeLambda(params, body, location) {
286
+ // Create a reference that at runtime resolves to parameters array. All
287
+ // parameter references will use this as their basis.
288
+ const reference = annotate([ops.params, 0], location);
289
+
290
+ // Shared state for parameter processing
291
+ const state = { tempVariableCount: 0 };
292
+
293
+ const bindings = makeParamArray(params, reference, state);
294
+ const annotatedBindings = annotate(bindings, params.location);
295
+
296
+ // Calculate function "length" (number of expected arguments). The length
297
+ // can't be easily calculated at runtime because default values and rest
298
+ // parameters affect it.
299
+ let length = 0;
300
+ for (const param of params) {
301
+ const [op] = param;
302
+ if (op === markers.paramInitializer || op === markers.paramRest) {
303
+ // Default value or rest parameter: stop counting
304
+ break;
305
+ }
306
+ length++;
307
+ }
308
+
309
+ return annotate([ops.lambda, length, annotatedBindings, body], location);
310
+ }
311
+
260
312
  /**
261
313
  * From the given spreads within an object spread, return the merge.
262
314
  *
@@ -413,19 +465,129 @@ function makeOptionalCall(target, chain, location) {
413
465
  location
414
466
  );
415
467
 
416
- // Create the call to be made if the target is not null/undefined
417
- const call = makeCallChain(optionalTraverse, chain, location);
468
+ // Create the call body to be made if the target is not null/undefined
469
+ const body = makeCallChain(optionalTraverse, chain, location);
418
470
 
419
471
  // Create a function that takes __optional__ and makes the call
420
- const optionalLiteral = annotate([ops.literal, optionalKey], location);
421
- const lambdaParameters = annotate([optionalLiteral], location);
422
- const lambda = annotate([ops.lambda, lambdaParameters, call], location);
472
+ const optionalParam = annotate([markers.paramName, optionalKey], location);
473
+ const params = annotate([optionalParam], location);
474
+ const lambda = makeLambda(params, body, location);
423
475
 
424
476
  // Create the call to ops.optional
425
477
  const optionalCall = annotate([ops.optional, target, lambda], location);
426
478
  return optionalCall;
427
479
  }
428
480
 
481
+ // Return bindings for the given parameter
482
+ function makeParam(parameter, reference, state) {
483
+ const [marker, ...args] = parameter;
484
+ switch (marker) {
485
+ case markers.paramArray:
486
+ return makeParamArray(args, reference, state);
487
+
488
+ case markers.paramInitializer:
489
+ return makeParamInitializer(parameter, reference, state);
490
+
491
+ case markers.paramName:
492
+ return makeParamName(parameter, reference, state);
493
+
494
+ case markers.paramObject:
495
+ return makeParamObject(args, reference, state);
496
+
497
+ default:
498
+ throw new Error(`Unknown parameter type: ${parameter[0]}`);
499
+ }
500
+ }
501
+
502
+ // Return bindings for the array destructuring parameter
503
+ function makeParamArray(entries, reference, state) {
504
+ const bindings = entries.map((entry, index) => {
505
+ if (entry === undefined) {
506
+ return []; // Skip missing entry
507
+ } else if (entry[0] === markers.paramRest) {
508
+ // Rest parameter
509
+ const sliceFunction = annotate([reference, "slice"], entry.location);
510
+ const sliceCall = annotate([sliceFunction, index], entry.location);
511
+ return makeParam(entry[1], sliceCall, state);
512
+ }
513
+ // Other type of parameter
514
+ const indexReference = annotate([reference, index], entry.location);
515
+ return makeParam(entry, indexReference, state);
516
+ });
517
+
518
+ const flat = bindings.flat();
519
+ checkDuplicateParamNames(flat);
520
+ return flat;
521
+ }
522
+
523
+ // Return binding for a parameter with a default value
524
+ function makeParamInitializer(parameter, reference, state) {
525
+ const [baseParam, defaultValue] = parameter.slice(1);
526
+
527
+ if (defaultValue[0] === ops.literal) {
528
+ // Literal default value can be inlined
529
+ const defaultReference = annotate(
530
+ [ops.defaultValue, reference, defaultValue],
531
+ parameter.location
532
+ );
533
+ return makeParam(baseParam, defaultReference, state);
534
+ }
535
+
536
+ // Need to introduce a temporary variable so that the default value, if it's
537
+ // an expression, is only evaluated once.
538
+ const deferred = makeDeferredArguments([defaultValue])[0];
539
+ const defaultReference = annotate(
540
+ [ops.defaultValue, reference, deferred],
541
+ parameter.location
542
+ );
543
+ const tempVariableName = `__temp${state.tempVariableCount++}__`;
544
+ const tempBinding = annotate(
545
+ [tempVariableName, defaultReference],
546
+ parameter.location
547
+ );
548
+
549
+ const selfReference = annotate([ops.inherited, 0], parameter.location);
550
+ const tempReference = annotate(
551
+ [selfReference, tempVariableName],
552
+ parameter.location
553
+ );
554
+
555
+ const paramBindings = makeParam(baseParam, tempReference, state);
556
+ return [tempBinding, ...paramBindings];
557
+ }
558
+
559
+ // Return binding for a single parameter name
560
+ function makeParamName(parameter, reference, state) {
561
+ const paramName = parameter[1];
562
+ // Return as an array with one entry
563
+ const bindings = [annotate([paramName, reference], parameter.location)];
564
+ return bindings;
565
+ }
566
+
567
+ // Return bindings for an object destructuring parameter
568
+ function makeParamObject(entries, reference, state) {
569
+ const keys = [];
570
+ const bindings = entries.map((entry) => {
571
+ if (entry[0] === markers.paramRest) {
572
+ // Rest parameter; exclude keys we've seen so far
573
+ const annotatedKeys = annotate([ops.array, ...keys], entry.location);
574
+ const objectRest = annotate(
575
+ [ops.objectRest, reference, annotatedKeys],
576
+ entry.location
577
+ );
578
+ return makeParam(entry[1], objectRest, state);
579
+ }
580
+ const [key, binding] = entry;
581
+ keys.push(key);
582
+ const propertyValue = annotate([reference, key], entry.location);
583
+ return makeParam(binding, propertyValue, state);
584
+ });
585
+
586
+ const flat = bindings.flat();
587
+ checkDuplicateParamNames(flat);
588
+ return flat;
589
+ }
590
+
429
591
  /**
430
592
  * Handle a path with one or more segments separated by slashes.
431
593
  *
@@ -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;