@weborigami/language 0.3.3-jse.2 → 0.3.3-jse.3

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.
@@ -15,8 +15,12 @@ const YAML = YAMLModule.default ?? YAMLModule.YAML;
15
15
  /** @typedef {import("../../index.ts").CodeLocation} CodeLocation */
16
16
  /** @typedef {import("../../index.ts").Code} Code */
17
17
 
18
- // Marker for a reference that may be a builtin or a scope reference
19
- export const undetermined = Symbol("undetermined");
18
+ // Markers in compiled output, will get optimized away
19
+ export const markers = {
20
+ global: Symbol("global"), // Global reference
21
+ traverse: Symbol("traverse"), // Continuation of path traversal
22
+ reference: Symbol("reference"), // Reference to local, scope, or global
23
+ };
20
24
 
21
25
  const builtinRegex = /^[A-Za-z][A-Za-z0-9]*$/;
22
26
 
@@ -37,7 +41,7 @@ export function annotate(code, location) {
37
41
  }
38
42
 
39
43
  /**
40
- * In the given code, replace all scope refernces to the given name with the
44
+ * In the given code, replace all function calls to the given name with the
41
45
  * given macro code.
42
46
  *
43
47
  * @param {AnnotatedCode} code
@@ -49,8 +53,16 @@ export function applyMacro(code, name, macro) {
49
53
  return code;
50
54
  }
51
55
 
52
- const [fn, ...args] = code;
53
- if (fn === ops.scope && args[0] === name) {
56
+ // We're looking for a function call with the given name.
57
+ // For `foo`, the call would be: [[reference, [ops.literal, "foo"]], undefined]
58
+ if (
59
+ code[0] &&
60
+ code[0][0] === markers.reference &&
61
+ code[0][1] instanceof Array &&
62
+ code[0][1][0] === ops.literal &&
63
+ code[0][1][1] === name &&
64
+ code[1] === undefined
65
+ ) {
54
66
  return macro;
55
67
  }
56
68
 
@@ -60,7 +72,7 @@ export function applyMacro(code, name, macro) {
60
72
 
61
73
  /**
62
74
  * The indicated code is being used to define a property named by the given key.
63
- * Rewrite any [ops.scope, key] calls to be [ops.inherited, key] to avoid
75
+ * Rewrite any [[ops.scope], key] calls to be [ops.inherited, key] to avoid
64
76
  * infinite recursion.
65
77
  *
66
78
  * @param {AnnotatedCode} code
@@ -73,11 +85,12 @@ function avoidRecursivePropertyCalls(code, key) {
73
85
  /** @type {Code} */
74
86
  let modified;
75
87
  if (
76
- code[0] === ops.scope &&
77
- trailingSlash.remove(code[1]) === trailingSlash.remove(key)
88
+ code[0] instanceof Array &&
89
+ code[0][0] === ops.scope &&
90
+ trailingSlash.remove(code[1][1]) === trailingSlash.remove(key)
78
91
  ) {
79
92
  // Rewrite to avoid recursion
80
- modified = [ops.inherited, code[1]];
93
+ modified = [ops.inherited, code[1][1]];
81
94
  } else if (
82
95
  code[0] === ops.lambda &&
83
96
  code[1].some((param) => param[1] === key)
@@ -92,13 +105,13 @@ function avoidRecursivePropertyCalls(code, key) {
92
105
  }
93
106
 
94
107
  /**
95
- * Downgrade a potential builtin reference to a scope reference.
108
+ * Downgrade a potential global reference to a reference.
96
109
  *
97
110
  * @param {AnnotatedCode} code
98
111
  */
99
112
  export function downgradeReference(code) {
100
- if (code && code.length === 2 && code[0] === undetermined) {
101
- return annotate([ops.scope, code[1]], code.location);
113
+ if (code && code.length === 2 && code[0] === markers.reference) {
114
+ return annotate([markers.reference, code[1]], code.location);
102
115
  } else {
103
116
  return code;
104
117
  }
@@ -139,7 +152,7 @@ export function makeArray(entries, location) {
139
152
 
140
153
  let result;
141
154
  if (spreads.length > 1) {
142
- result = [ops.merge, ...spreads];
155
+ result = [ops.flat, ...spreads];
143
156
  } else if (spreads.length === 1) {
144
157
  result = spreads[0];
145
158
  } else {
@@ -196,7 +209,7 @@ export function makeBinaryOperation(left, [operatorToken, right]) {
196
209
  * @param {AnnotatedCode} target
197
210
  * @param {any[]} args
198
211
  */
199
- export function makeCall(target, args) {
212
+ export function makeCall(target, args, mode) {
200
213
  if (!(target instanceof Array)) {
201
214
  const error = new SyntaxError(`Can't call this like a function: ${target}`);
202
215
  /** @type {any} */ (error).location = /** @type {any} */ (target).location;
@@ -205,38 +218,55 @@ export function makeCall(target, args) {
205
218
 
206
219
  let fnCall;
207
220
  const op = args[0];
208
- if (op === ops.traverse || op === ops.optionalTraverse) {
221
+ if (op === markers.traverse || op === ops.optionalTraverse) {
209
222
  let tree = target;
210
223
 
211
- if (tree[0] === undetermined) {
212
- // In a traversal, downgrade ops.builtin references to ops.scope
213
- tree = downgradeReference(tree);
214
- if (tree[0] === ops.scope && !trailingSlash.has(tree[1])) {
215
- // Target didn't parse with a trailing slash; add one
216
- tree[1] = trailingSlash.add(tree[1]);
224
+ if (tree[0] === markers.reference && !trailingSlash.has(tree[1][1])) {
225
+ // Target didn't parse with a trailing slash; add one
226
+ tree[1][1] = trailingSlash.add(tree[1][1]);
227
+ }
228
+
229
+ // Is the target an existing traversal that can be extended? It should be a
230
+ // reference or global where all the args are literals.
231
+ const extend =
232
+ (tree[0] === markers.reference ||
233
+ (tree[0] instanceof Array && tree[0][0] === markers.global)) &&
234
+ !tree
235
+ .slice(1)
236
+ .some((arg) => !(arg instanceof Array && arg[0] === ops.literal));
237
+ if (extend) {
238
+ fnCall = tree;
239
+ // If last key doesn't end with slash, add one
240
+ const last = tree.at(-1);
241
+ if (last instanceof Array && last[0] === ops.literal) {
242
+ last[1] = trailingSlash.add(last[1]);
217
243
  }
244
+ } else {
245
+ fnCall = [tree];
218
246
  }
219
247
 
220
248
  if (args.length > 1) {
221
249
  // Regular traverse
222
250
  const keys = args.slice(1);
223
- fnCall = [op, tree, ...keys];
224
- } else {
251
+ fnCall.push(...keys);
252
+ } else if (tree[0] !== ops.rootDirectory) {
225
253
  // Traverse without arguments equates to unpack
226
254
  fnCall = [ops.unpack, tree];
255
+ } else {
256
+ fnCall = tree;
227
257
  }
228
258
  } else if (op === ops.templateStandard || op === ops.templateTree) {
229
259
  // Tagged template
230
260
  const strings = args[1];
231
261
  const values = args.slice(2);
232
262
  fnCall = makeTaggedTemplateCall(
233
- upgradeReference(target),
263
+ upgradeReference(target, mode),
234
264
  strings,
235
265
  ...values
236
266
  );
237
267
  } else {
238
268
  // Function call with explicit or implicit parentheses
239
- fnCall = [upgradeReference(target), ...args];
269
+ fnCall = [upgradeReference(target, mode), ...args];
240
270
  }
241
271
 
242
272
  // Create a location spanning the newly-constructed function call.
@@ -276,6 +306,21 @@ export function makeDeferredArguments(args) {
276
306
  });
277
307
  }
278
308
 
309
+ export function makeDocument(mode, front, body, location) {
310
+ // In order for template expressions to see the front matter properties,
311
+ // we translate the top-level front properties to object entries.
312
+ const entries = Object.entries(front).map(([key, value]) =>
313
+ annotate([key, annotate([ops.literal, value], location)], location)
314
+ );
315
+
316
+ // Add an entry for the body
317
+ const bodyKey = mode === "jse" ? "_body" : "@text";
318
+ entries.push(annotate([bodyKey, body], location));
319
+
320
+ // Return the code for the document object
321
+ return annotate([ops.object, ...entries], location);
322
+ }
323
+
279
324
  export function makeJsPropertyAccess(expression, property) {
280
325
  const location = {
281
326
  source: expression.location.source,
@@ -285,6 +330,70 @@ export function makeJsPropertyAccess(expression, property) {
285
330
  return annotate([expression, property], location);
286
331
  }
287
332
 
333
+ /**
334
+ * From the given spreads within an object spread, return the merge.
335
+ *
336
+ * Example:
337
+ *
338
+ * {
339
+ * x = { a: 1 }
340
+ * …x
341
+ * y = x
342
+ * }
343
+ *
344
+ * will be treated as:
345
+ *
346
+ * {
347
+ * x = { a: 1 }
348
+ * y = x
349
+ * _result: {
350
+ * x
351
+ * …x
352
+ * y
353
+ * }
354
+ * }.result
355
+ *
356
+ * @param {*} spreads
357
+ * @param {CodeLocation} location
358
+ */
359
+ function makeMerge(spreads, location) {
360
+ const topEntries = [];
361
+ const resultEntries = [];
362
+ for (const spread of spreads) {
363
+ if (spread[0] === ops.object) {
364
+ topEntries.push(...spread.slice(1));
365
+ // Also add an object to the result with indirect references
366
+ const indirectEntries = spread.slice(1).map((entry) => {
367
+ const [key] = entry;
368
+ const context = annotate([ops.context, 1], entry.location);
369
+ const reference = annotate([context, key], entry.location);
370
+ const getter = annotate([ops.getter, reference], entry.location);
371
+ return annotate([key, getter], entry.location);
372
+ });
373
+ const indirectObject = annotate(
374
+ [ops.object, ...indirectEntries],
375
+ location
376
+ );
377
+ resultEntries.push(indirectObject);
378
+ } else {
379
+ resultEntries.push(spread);
380
+ }
381
+ }
382
+
383
+ // Merge to create result
384
+ const result = annotate([ops.merge, ...resultEntries], location);
385
+
386
+ // Add the result to the top-level object as _result
387
+ topEntries.push(annotate(["_result", result], location));
388
+
389
+ // Construct the top-level object
390
+ const topObject = annotate([ops.object, ...topEntries], location);
391
+
392
+ // Get the _result property
393
+ const code = annotate([topObject, "_result"], location);
394
+ return code;
395
+ }
396
+
288
397
  /**
289
398
  * Make an object.
290
399
  *
@@ -341,7 +450,7 @@ export function makeObject(entries, location) {
341
450
  let code;
342
451
  if (spreads.length > 1) {
343
452
  // Merge multiple spreads
344
- code = [ops.merge, ...spreads];
453
+ code = makeMerge(spreads, location);
345
454
  } else if (spreads.length === 1) {
346
455
  // A single spread can just be the object
347
456
  code = spreads[0];
@@ -358,10 +467,11 @@ export function makeObject(entries, location) {
358
467
  *
359
468
  * @param {AnnotatedCode} arg
360
469
  * @param {AnnotatedCode} fn
470
+ * @param {string} mode
361
471
  */
362
- export function makePipeline(arg, fn) {
363
- const upgraded = upgradeReference(fn);
364
- const result = makeCall(upgraded, [arg]);
472
+ export function makePipeline(arg, fn, mode) {
473
+ const upgraded = upgradeReference(fn, mode);
474
+ const result = makeCall(upgraded, [arg], mode);
365
475
  const source = fn.location.source;
366
476
  let start = arg.location.start;
367
477
  let end = fn.location.end;
@@ -374,21 +484,6 @@ export function makeProperty(key, value) {
374
484
  return [key, modified];
375
485
  }
376
486
 
377
- export function makeReference(identifier) {
378
- // We can't know for sure that an identifier is a builtin reference until we
379
- // see whether it's being called as a function.
380
- let op;
381
- if (builtinRegex.test(identifier)) {
382
- op = identifier.endsWith(":")
383
- ? // Namespace is always a builtin reference
384
- ops.builtin
385
- : undetermined;
386
- } else {
387
- op = ops.scope;
388
- }
389
- return [op, identifier];
390
- }
391
-
392
487
  /**
393
488
  * Make a tagged template call
394
489
  *
@@ -496,7 +591,7 @@ export function makeYamlObject(text, location) {
496
591
  throw error;
497
592
  }
498
593
 
499
- return annotate([ops.literal, parsed], location);
594
+ return parsed;
500
595
  }
501
596
 
502
597
  /**
@@ -504,9 +599,14 @@ export function makeYamlObject(text, location) {
504
599
  *
505
600
  * @param {AnnotatedCode} code
506
601
  */
507
- export function upgradeReference(code) {
508
- if (code.length === 2 && code[0] === undetermined) {
509
- const result = [ops.builtin, code[1]];
602
+ export function upgradeReference(code, mode) {
603
+ if (
604
+ mode === "shell" &&
605
+ code.length === 2 &&
606
+ code[0] === markers.reference &&
607
+ builtinRegex.exec(code[1][1])
608
+ ) {
609
+ const result = [markers.global, code[1][1]];
510
610
  return annotate(result, code.location);
511
611
  } else {
512
612
  return code;
@@ -1,3 +1,4 @@
1
+ import getHandlers from "./getHandlers.js";
1
2
  import { handleExtension } from "./handlers.js";
2
3
 
3
4
  /**
@@ -9,9 +10,17 @@ import { handleExtension } from "./handlers.js";
9
10
  */
10
11
  export default function HandleExtensionsTransform(Base) {
11
12
  return class FileLoaders extends Base {
13
+ constructor(...args) {
14
+ super(...args);
15
+
16
+ // Callers should set this to the set of supported extension handlers
17
+ this.handlers = null;
18
+ }
19
+
12
20
  async get(key) {
13
21
  const value = await super.get(key);
14
- return handleExtension(this, value, key);
22
+ const handlers = getHandlers(this);
23
+ return handleExtension(this, value, key, handlers);
15
24
  }
16
25
  };
17
26
  }
@@ -1,6 +1,5 @@
1
- import { Tree, isUnpackable, scope } from "@weborigami/async-tree";
1
+ import { Tree, isUnpackable } from "@weborigami/async-tree";
2
2
  import codeFragment from "./codeFragment.js";
3
- import { codeSymbol, scopeSymbol, sourceSymbol } from "./symbols.js";
4
3
 
5
4
  /**
6
5
  * Evaluate the given code and return the result.
@@ -52,7 +51,7 @@ export default async function evaluate(code) {
52
51
  result =
53
52
  fn instanceof Function
54
53
  ? await fn.call(tree, ...args) // Invoke the function
55
- : await Tree.traverseOrThrow(fn, ...args); // Traverse the tree.
54
+ : await Tree.traverseOrThrow.call(tree, fn, ...args); // Traverse the tree.
56
55
  } catch (/** @type {any} */ error) {
57
56
  if (!error.location) {
58
57
  // Attach the location of the code we tried to evaluate.
@@ -67,39 +66,33 @@ export default async function evaluate(code) {
67
66
  throw error;
68
67
  }
69
68
 
70
- // If the result is a tree, then the default parent of the tree is the current
71
- // tree.
72
- if (Tree.isAsyncTree(result) && !result.parent) {
73
- result.parent = tree;
74
- }
75
-
76
69
  // To aid debugging, add the code to the result.
77
- if (Object.isExtensible(result)) {
78
- try {
79
- if (code.location && !result[sourceSymbol]) {
80
- Object.defineProperty(result, sourceSymbol, {
81
- value: codeFragment(code.location),
82
- enumerable: false,
83
- });
84
- }
85
- if (!result[codeSymbol]) {
86
- Object.defineProperty(result, codeSymbol, {
87
- value: code,
88
- enumerable: false,
89
- });
90
- }
91
- if (!result[scopeSymbol]) {
92
- Object.defineProperty(result, scopeSymbol, {
93
- get() {
94
- return scope(this).trees;
95
- },
96
- enumerable: false,
97
- });
98
- }
99
- } catch (/** @type {any} */ error) {
100
- // Ignore errors.
101
- }
102
- }
70
+ // if (Object.isExtensible(result)) {
71
+ // try {
72
+ // if (code.location && !result[sourceSymbol]) {
73
+ // Object.defineProperty(result, sourceSymbol, {
74
+ // value: codeFragment(code.location),
75
+ // enumerable: false,
76
+ // });
77
+ // }
78
+ // if (!result[codeSymbol]) {
79
+ // Object.defineProperty(result, codeSymbol, {
80
+ // value: code,
81
+ // enumerable: false,
82
+ // });
83
+ // }
84
+ // if (!result[scopeSymbol]) {
85
+ // Object.defineProperty(result, scopeSymbol, {
86
+ // get() {
87
+ // return scope(this).trees;
88
+ // },
89
+ // enumerable: false,
90
+ // });
91
+ // }
92
+ // } catch (/** @type {any} */ error) {
93
+ // // Ignore errors.
94
+ // }
95
+ // }
103
96
 
104
97
  return result;
105
98
  }
@@ -6,6 +6,7 @@ import {
6
6
  trailingSlash,
7
7
  Tree,
8
8
  } from "@weborigami/async-tree";
9
+ import getHandlers from "./getHandlers.js";
9
10
  import { handleExtension } from "./handlers.js";
10
11
  import { evaluate, ops } from "./internal.js";
11
12
 
@@ -32,6 +33,7 @@ export default async function expressionObject(entries, parent) {
32
33
  if (parent !== null && !Tree.isAsyncTree(parent)) {
33
34
  throw new TypeError(`Parent must be an AsyncTree or null`);
34
35
  }
36
+ setParent(object, parent);
35
37
 
36
38
  let tree;
37
39
  const eagerProperties = [];
@@ -90,7 +92,8 @@ export default async function expressionObject(entries, parent) {
90
92
  get = async () => {
91
93
  tree ??= new ObjectTree(object);
92
94
  const result = await evaluate.call(tree, code);
93
- return handleExtension(tree, result, key);
95
+ const handlers = getHandlers(tree);
96
+ return handleExtension(tree, result, key, handlers);
94
97
  };
95
98
  } else {
96
99
  // No extension, so getter just invokes code.
@@ -116,9 +119,6 @@ export default async function expressionObject(entries, parent) {
116
119
  writable: true,
117
120
  });
118
121
 
119
- // Attach the parent
120
- setParent(object, parent);
121
-
122
122
  // Evaluate any properties that were declared as immediate: get their value
123
123
  // and overwrite the property getter with the actual value.
124
124
  for (const key of eagerProperties) {
@@ -0,0 +1,10 @@
1
+ import { Tree } from "@weborigami/async-tree";
2
+
3
+ // Return the extension handlers for the given tree
4
+ export default function getHandlers(tree) {
5
+ if (!tree) {
6
+ return null;
7
+ }
8
+ const root = Tree.root(tree);
9
+ return root.handlers;
10
+ }
@@ -4,60 +4,10 @@ import {
4
4
  isPacked,
5
5
  isStringLike,
6
6
  isUnpackable,
7
- scope,
8
7
  setParent,
9
8
  trailingSlash,
10
9
  } from "@weborigami/async-tree";
11
10
 
12
- /** @typedef {import("../../index.ts").ExtensionHandler} ExtensionHandler */
13
-
14
- // Track extensions handlers for a given containing tree.
15
- const handlersForContainer = new Map();
16
-
17
- /**
18
- * Find an extension handler for a file in the given container.
19
- *
20
- * @typedef {import("@weborigami/types").AsyncTree} AsyncTree
21
- *
22
- * @param {AsyncTree} parent
23
- * @param {string} extension
24
- */
25
- export async function getExtensionHandler(parent, extension) {
26
- let handlers = handlersForContainer.get(parent);
27
- if (handlers) {
28
- if (handlers[extension]) {
29
- return handlers[extension];
30
- }
31
- } else {
32
- handlers = {};
33
- handlersForContainer.set(parent, handlers);
34
- }
35
-
36
- const handlerName = `${extension.slice(1)}.handler`;
37
- const parentScope = scope(parent);
38
-
39
- /** @type {Promise<ExtensionHandler>} */
40
- let handlerPromise = parentScope
41
- ?.get(handlerName)
42
- .then(async (extensionHandler) => {
43
- if (isUnpackable(extensionHandler)) {
44
- // The extension handler itself needs to be unpacked. E.g., if it's a
45
- // buffer containing JavaScript file, we need to unpack it to get its
46
- // default export.
47
- // @ts-ignore
48
- extensionHandler = await extensionHandler.unpack();
49
- }
50
- // Update cache with actual handler
51
- handlers[extension] = extensionHandler;
52
- return extensionHandler;
53
- });
54
-
55
- // Cache handler even if it's undefined so we don't look it up again
56
- handlers[extension] = handlerPromise;
57
-
58
- return handlerPromise;
59
- }
60
-
61
11
  /**
62
12
  * If the given value is packed (e.g., buffer) and the key is a string-like path
63
13
  * that ends in an extension, search for a handler for that extension and, if
@@ -67,8 +17,13 @@ export async function getExtensionHandler(parent, extension) {
67
17
  * @param {any} value
68
18
  * @param {any} key
69
19
  */
70
- export async function handleExtension(parent, value, key) {
71
- if (isPacked(value) && isStringLike(key) && value.unpack === undefined) {
20
+ export async function handleExtension(parent, value, key, handlers) {
21
+ if (
22
+ handlers &&
23
+ isPacked(value) &&
24
+ isStringLike(key) &&
25
+ value.unpack === undefined
26
+ ) {
72
27
  const hasSlash = trailingSlash.has(key);
73
28
  if (hasSlash) {
74
29
  key = trailingSlash.remove(key);
@@ -82,8 +37,14 @@ export async function handleExtension(parent, value, key) {
82
37
  ? ".jsedocument"
83
38
  : extension.extname(key);
84
39
  if (extname) {
85
- const handler = await getExtensionHandler(parent, extname);
40
+ const handlerName = `${extname.slice(1)}.handler`;
41
+ let handler = await handlers.get(handlerName);
86
42
  if (handler) {
43
+ if (isUnpackable(handler)) {
44
+ // The extension handler itself needs to be unpacked
45
+ handler = await handler.unpack();
46
+ }
47
+
87
48
  if (hasSlash && handler.unpack) {
88
49
  // Key like `data.json/` ends in slash -- unpack immediately
89
50
  return handler.unpack(value, { key, parent });
@@ -0,0 +1,99 @@
1
+ import path from "node:path";
2
+
3
+ /**
4
+ * The complete set of support JavaScript globals and global-like values.
5
+ *
6
+ * See
7
+ * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects.
8
+ * That page lists some things like `TypedArrays` which are not globals so are
9
+ * omitted here.
10
+ */
11
+ export default {
12
+ AggregateError,
13
+ Array,
14
+ ArrayBuffer,
15
+ Atomics,
16
+ BigInt,
17
+ BigInt64Array,
18
+ BigUint64Array,
19
+ Boolean,
20
+ DataView,
21
+ Date,
22
+ Error,
23
+ EvalError,
24
+ FinalizationRegistry,
25
+ Float32Array,
26
+ Float64Array,
27
+ Function,
28
+ Infinity,
29
+ Int16Array,
30
+ Int32Array,
31
+ Int8Array,
32
+ Intl,
33
+ // @ts-ignore Iterator does exist despite what TypeScript thinks
34
+ Iterator,
35
+ JSON,
36
+ Map,
37
+ Math,
38
+ NaN,
39
+ Number,
40
+ Object,
41
+ Promise,
42
+ Proxy,
43
+ RangeError,
44
+ ReferenceError,
45
+ Reflect,
46
+ RegExp,
47
+ Set,
48
+ SharedArrayBuffer,
49
+ String,
50
+ Symbol,
51
+ SyntaxError,
52
+ TypeError,
53
+ URIError,
54
+ Uint16Array,
55
+ Uint32Array,
56
+ Uint8Array,
57
+ Uint8ClampedArray,
58
+ WeakMap,
59
+ WeakRef,
60
+ WeakSet,
61
+ decodeURI,
62
+ decodeURIComponent,
63
+ encodeURI,
64
+ encodeURIComponent,
65
+ eval,
66
+ false: false, // treat like a global
67
+ fetch: fetchWrapper, // special case
68
+ globalThis,
69
+ import: importWrapper, // not a function in JS but acts like one
70
+ isFinite,
71
+ isNaN,
72
+ null: null, // treat like a global
73
+ parseFloat,
74
+ parseInt,
75
+ true: true, // treat like a global
76
+ undefined,
77
+ };
78
+
79
+ async function fetchWrapper(resource, options) {
80
+ const response = await fetch(resource, options);
81
+ return response.ok ? await response.arrayBuffer() : undefined;
82
+ }
83
+
84
+ /** @this {import("@weborigami/types").AsyncTree|null|undefined} */
85
+ async function importWrapper(modulePath) {
86
+ // Walk up parent tree looking for a FileTree or other object with a `path`
87
+ /** @type {any} */
88
+ let current = this;
89
+ while (current && !("path" in current)) {
90
+ current = current.parent;
91
+ }
92
+ if (!current) {
93
+ throw new TypeError(
94
+ "Modules can only be imported from a folder or other object with a path property."
95
+ );
96
+ }
97
+ const filePath = path.resolve(current.path, modulePath);
98
+ return import(filePath);
99
+ }
@@ -53,11 +53,6 @@ export default async function mergeTrees(...trees) {
53
53
  return result;
54
54
  }
55
55
 
56
- // If all trees are arrays, return an array.
57
- if (unpacked.every((tree) => Array.isArray(tree))) {
58
- return unpacked.flat();
59
- }
60
-
61
56
  // Merge the trees.
62
57
  const result = merge(...unpacked);
63
58
  setParent(result, this);