@weborigami/language 0.6.17 → 0.7.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/index.ts +1 -0
  2. package/main.js +7 -1
  3. package/package.json +7 -6
  4. package/src/compiler/compile.js +10 -3
  5. package/src/compiler/optimize.js +71 -40
  6. package/src/compiler/parse.js +1 -1
  7. package/src/compiler/parserHelpers.js +5 -3
  8. package/src/handlers/addExtensionKeyFn.js +18 -0
  9. package/src/handlers/epub_handler.js +54 -0
  10. package/src/handlers/getSource.js +11 -0
  11. package/src/handlers/handlers.js +2 -0
  12. package/src/handlers/htm_handler.js +1 -1
  13. package/src/handlers/js_handler.js +13 -4
  14. package/src/handlers/mediaTypeExtensions.json +15 -0
  15. package/src/handlers/ori_handler.js +8 -7
  16. package/src/handlers/oridocument_handler.js +19 -28
  17. package/src/handlers/processOriExport.js +17 -0
  18. package/src/handlers/tsv_handler.js +1 -1
  19. package/src/handlers/txt_handler.js +4 -2
  20. package/src/handlers/xhtml_handler.js +1 -1
  21. package/src/handlers/yaml_handler.js +6 -3
  22. package/src/handlers/zip_handler.js +112 -0
  23. package/src/project/activeProjectRoot.js +9 -0
  24. package/src/project/getGlobalsForTree.js +5 -0
  25. package/src/project/{projectGlobals.js → initializeGlobalsForTree.js} +8 -13
  26. package/src/project/jsGlobals.js +1 -0
  27. package/src/project/projectConfig.js +2 -2
  28. package/src/project/projectRootFromPath.js +2 -0
  29. package/src/protocols/constructHref.js +3 -3
  30. package/src/protocols/constructSiteTree.js +11 -2
  31. package/src/protocols/explore.js +1 -1
  32. package/src/protocols/explorehttp.js +1 -1
  33. package/src/protocols/fetchAndHandleExtension.js +23 -11
  34. package/src/protocols/files.js +1 -0
  35. package/src/protocols/http.js +4 -1
  36. package/src/protocols/https.js +4 -1
  37. package/src/protocols/httpstree.js +1 -1
  38. package/src/protocols/httptree.js +1 -1
  39. package/src/protocols/package.js +15 -3
  40. package/src/runtime/AsyncCacheTransform.d.ts +5 -0
  41. package/src/runtime/AsyncCacheTransform.js +134 -0
  42. package/src/runtime/HandleExtensionsTransform.d.ts +3 -1
  43. package/src/runtime/HandleExtensionsTransform.js +18 -2
  44. package/src/runtime/OrigamiFileMap.d.ts +5 -2
  45. package/src/runtime/OrigamiFileMap.js +27 -4
  46. package/src/runtime/ScopeMap.js +72 -0
  47. package/src/runtime/SyncCacheTransform.d.ts +8 -0
  48. package/src/runtime/SyncCacheTransform.js +133 -0
  49. package/src/runtime/SystemCacheMap.js +259 -0
  50. package/src/runtime/WatchFilesMixin.js +52 -19
  51. package/src/runtime/enableValueCaching.js +192 -0
  52. package/src/runtime/execute.js +2 -2
  53. package/src/runtime/executionContext.js +7 -0
  54. package/src/runtime/explainReferenceError.js +7 -2
  55. package/src/runtime/expressionObject.js +54 -46
  56. package/src/runtime/handleExtension.js +65 -34
  57. package/src/runtime/interop.js +2 -2
  58. package/src/runtime/mergeTrees.js +1 -1
  59. package/src/runtime/ops.js +28 -33
  60. package/src/runtime/symbols.js +3 -0
  61. package/src/runtime/systemCache.js +3 -0
  62. package/src/runtime/volatile.js +14 -0
  63. package/test/compiler/codeHelpers.js +2 -1
  64. package/test/compiler/optimize.test.js +62 -54
  65. package/test/handlers/epub_handler.test.js +27 -0
  66. package/test/handlers/fixtures/test.zip +0 -0
  67. package/test/handlers/ori_handler.test.js +22 -3
  68. package/test/handlers/oridocument_handler.test.js +1 -1
  69. package/test/handlers/zip_handler.test.js +45 -0
  70. package/test/protocols/https.test.js +19 -0
  71. package/test/protocols/package.test.js +7 -2
  72. package/test/runtime/AsyncCacheTransform.test.js +91 -0
  73. package/test/runtime/OrigamiFileMap.test.js +26 -23
  74. package/test/runtime/ScopeMap.test.js +49 -0
  75. package/test/runtime/SyncCacheTransform.test.js +93 -0
  76. package/test/runtime/SystemCacheMap.test.js +239 -0
  77. package/test/runtime/asyncCalcs.js +28 -0
  78. package/test/runtime/enableValueCaching.test.js +55 -0
  79. package/test/runtime/errors.test.js +53 -30
  80. package/test/runtime/evaluate.test.js +9 -4
  81. package/test/runtime/execute.test.js +6 -1
  82. package/test/runtime/expressionObject.test.js +55 -15
  83. package/test/runtime/fetchAndHandleExtension.test.js +24 -0
  84. package/test/runtime/fixtures/unpack/hello.json +1 -0
  85. package/test/runtime/handleExtension.test.js +12 -1
  86. package/test/runtime/ops.test.js +70 -65
  87. package/test/runtime/syncCalcs.js +27 -0
  88. package/test/runtime/systemCache.test.js +66 -0
  89. package/src/runtime/assignPropertyDescriptors.js +0 -23
  90. package/src/runtime/asyncStorage.js +0 -7
@@ -0,0 +1,192 @@
1
+ import { AsyncMap, isPlainObject, SyncMap, Tree } from "@weborigami/async-tree";
2
+ import AsyncCacheTransform from "./AsyncCacheTransform.js";
3
+ import { cachePathSymbol } from "./symbols.js";
4
+ import SyncCacheTransform from "./SyncCacheTransform.js";
5
+ import systemCache from "./systemCache.js";
6
+ import SystemCacheMap from "./SystemCacheMap.js";
7
+
8
+ // For detecting async functions
9
+ const AsyncFunction = async function () {}.constructor;
10
+
11
+ /**
12
+ * Given a maplike object whose values can be cached, enable caching. This may
13
+ * apply a caching transform, and sets a cache path on the object so that it can
14
+ * use as the prefix for cache paths.
15
+ *
16
+ * Note: this typically destructively modifies the given value.
17
+ *
18
+ * @param {any} value
19
+ * @param {string} cachePath
20
+ */
21
+ export default function enableValueCaching(value, cachePath) {
22
+ if (!(typeof value === "object" || typeof value === "function")) {
23
+ // Don't need to apply caching to primitive value
24
+ return value;
25
+ }
26
+
27
+ const cacheable =
28
+ value[cachePathSymbol] === undefined && Tree.isMaplike(value);
29
+ if (cacheable) {
30
+ if (isPlainObject(value)) {
31
+ // Expression objects do their own caching
32
+ // TODO: What if it's some other kind of plain object?
33
+ markCacheable(value, cachePath);
34
+ } else if (Array.isArray(value)) {
35
+ // Cache arrays
36
+ markCacheable(value, cachePath);
37
+ } else if (value instanceof Function) {
38
+ // Cache a function
39
+ value = cacheFunction(value, cachePath);
40
+ } else if (
41
+ isTransformApplied(SyncCacheTransform, value) ||
42
+ isTransformApplied(AsyncCacheTransform, value)
43
+ ) {
44
+ // Already has caching transform applied; just mark cacheable
45
+ markCacheable(value, cachePath);
46
+ } else {
47
+ // Other maplike; convert to a Map/AsyncMap
48
+ value = Tree.from(value);
49
+ if (value instanceof Map) {
50
+ if (!(value instanceof SyncMap)) {
51
+ // Convert regular Map to SyncMap so we can extend it
52
+ value = new (SyncCacheTransform(SyncMap))(value);
53
+ } else {
54
+ // Cache a SyncMap
55
+ value = transformObject(SyncCacheTransform, value);
56
+ }
57
+ } else if (value instanceof AsyncMap) {
58
+ // Cache an AsyncMap
59
+ value = transformObject(AsyncCacheTransform, value);
60
+ }
61
+ markCacheable(value, cachePath);
62
+ }
63
+ }
64
+
65
+ return value;
66
+ }
67
+
68
+ /**
69
+ * Cache a function with arity 1 or greater that takes string arguments
70
+ *
71
+ * @param {Function} fn
72
+ * @param {string} cachePath
73
+ */
74
+ export function cacheFunction(fn, cachePath) {
75
+ if (fn.length === 0) {
76
+ // Return as is
77
+ return fn;
78
+ }
79
+ let result;
80
+ if (fn instanceof AsyncFunction) {
81
+ // Return an async function that caches results for a unary argument
82
+ result = async (...args) => {
83
+ if (!allStringArguments(args)) {
84
+ // Run function in context of this cache path, but don't cache result
85
+ return systemCache.runInContextAsync(cachePath, () => fn(...args));
86
+ }
87
+ const keyCachePath = SystemCacheMap.joinPath(cachePath, args.join("/"));
88
+ let result = systemCache.getOrInsertComputedAsync(
89
+ keyCachePath,
90
+ async () => fn(...args),
91
+ );
92
+ result = enableValueCaching(result, keyCachePath);
93
+ return result;
94
+ };
95
+ } else {
96
+ // Return a sync function that caches results for a unary argument
97
+ result = (...args) => {
98
+ if (!allStringArguments(args)) {
99
+ // Run function in context of this cache path, but don't cache result
100
+ return systemCache.runInContext(cachePath, () => fn(...args));
101
+ }
102
+ const keyCachePath = SystemCacheMap.joinPath(cachePath, args.join("/"));
103
+ let result = systemCache.getOrInsertComputed(keyCachePath, () =>
104
+ fn(...args),
105
+ );
106
+ result = enableValueCaching(result, keyCachePath);
107
+ return result;
108
+ };
109
+ }
110
+ Object.defineProperty(result, "length", {
111
+ value: fn.length,
112
+ configurable: true,
113
+ });
114
+ markCacheable(result, cachePath);
115
+ return result;
116
+ }
117
+
118
+ // Non-string keys and non-empty strings can't be cached
119
+ function allStringArguments(args) {
120
+ return (
121
+ args.length > 0 &&
122
+ args.every((arg) => typeof arg === "string" && arg.length > 0)
123
+ );
124
+ }
125
+
126
+ export function isTransformApplied(Transform, obj) {
127
+ let transformName = Transform.name;
128
+ if (!transformName) {
129
+ throw `isTransformApplied was called on an unnamed transform function, but a name is required.`;
130
+ }
131
+ if (transformName.endsWith("Transform")) {
132
+ transformName = transformName.slice(0, -9);
133
+ }
134
+ // Walk up prototype chain looking for a constructor with the same name as the
135
+ // transform. This is not a great test.
136
+ for (let proto = obj; proto; proto = Object.getPrototypeOf(proto)) {
137
+ if (proto.constructor.name === transformName) {
138
+ return true;
139
+ }
140
+ }
141
+ return false;
142
+ }
143
+
144
+ function markCacheable(object, cachePath) {
145
+ Object.defineProperty(object, cachePathSymbol, {
146
+ configurable: true,
147
+ enumerable: false,
148
+ value: cachePath,
149
+ });
150
+ }
151
+ /**
152
+ * Apply a functional class mixin to an individual object instance.
153
+ *
154
+ * This works by create an intermediate class, creating an instance of that, and
155
+ * then setting the intermediate class's prototype to the given individual
156
+ * object. The resulting, extended object is then returned.
157
+ *
158
+ * This manipulation of the prototype chain is generally sound in JavaScript,
159
+ * with some caveats. In particular, the original object class cannot make
160
+ * direct use of private members; JavaScript will complain if the extended
161
+ * object does anything that requires access to those private members.
162
+ *
163
+ * @param {Function} Transform
164
+ * @param {any} obj
165
+ */
166
+ export function transformObject(Transform, obj) {
167
+ // Apply the mixin to Object and instantiate that. The Object base class here
168
+ // is going to be cut out of the prototype chain in a moment; we just use
169
+ // Object as a convenience because its constructor takes no arguments.
170
+ const mixed = new (Transform(Object))();
171
+
172
+ // Find the highest prototype in the chain that was added by the class mixin.
173
+ // The mixin may have added multiple prototypes to the chain. Walk up the
174
+ // prototype chain until we hit Object.
175
+ let mixinProto = Object.getPrototypeOf(mixed);
176
+ while (Object.getPrototypeOf(mixinProto) !== Object.prototype) {
177
+ mixinProto = Object.getPrototypeOf(mixinProto);
178
+ }
179
+
180
+ // Redirect the prototype chain above the mixin to point to the original
181
+ // object. The mixed object now extends the original object with the mixin.
182
+ Object.setPrototypeOf(mixinProto, obj);
183
+
184
+ // Create a new constructor for this mixed object that reflects its prototype
185
+ // chain. Because we've already got the instance we want, we won't use this
186
+ // constructor now, but this can be used later to instantiate other objects
187
+ // that look like the mixed one.
188
+ mixed.constructor = Transform(obj.constructor);
189
+
190
+ // Return the mixed object.
191
+ return mixed;
192
+ }
@@ -1,5 +1,5 @@
1
1
  import { isUnpackable, Tree } from "@weborigami/async-tree";
2
- import asyncStorage from "./asyncStorage.js";
2
+ import executionContext from "./executionContext.js";
3
3
  import "./interop.js";
4
4
 
5
5
  /**
@@ -68,7 +68,7 @@ export default async function execute(code, state = {}) {
68
68
  // Execute the function or traverse the map.
69
69
  let result;
70
70
  try {
71
- result = await asyncStorage.run(
71
+ result = await executionContext.run(
72
72
  context,
73
73
  async () =>
74
74
  fn instanceof Function
@@ -0,0 +1,7 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+
3
+ /**
4
+ * The execute() function's context made available to async functions called
5
+ * during evaluation.
6
+ */
7
+ export default new AsyncLocalStorage();
@@ -39,8 +39,13 @@ export default async function explainReferenceError(code, state) {
39
39
  let key;
40
40
  if (code[0] === ops.cache) {
41
41
  // External scope reference
42
- const scopeCall = code[3].slice(1); // drop the ops.scope
43
- const keys = scopeCall.map((part) => part[1]);
42
+ let refCall = code[2];
43
+ if (refCall[0] === ops.unpack) {
44
+ // Unwrap implied unpack
45
+ refCall = refCall[1];
46
+ }
47
+ const scopeArgs = refCall.slice(1); // get the scope reference arguments
48
+ const keys = scopeArgs.map((part) => part[1]);
44
49
  const path = pathFromKeys(keys);
45
50
 
46
51
  if (keys.length > 1) {
@@ -6,9 +6,13 @@ import {
6
6
  trailingSlash,
7
7
  Tree,
8
8
  } from "@weborigami/async-tree";
9
+ import enableValueCaching from "./enableValueCaching.js";
9
10
  import execute from "./execute.js";
10
11
  import handleExtension from "./handleExtension.js";
11
12
  import { ops } from "./internal.js";
13
+ import { cachePathSymbol } from "./symbols.js";
14
+ import systemCache from "./systemCache.js";
15
+ import SystemCacheMap from "./SystemCacheMap.js";
12
16
 
13
17
  export const KEY_TYPE = {
14
18
  STRING: 0, // Simple string key: `a: 1`
@@ -36,6 +40,7 @@ export default async function expressionObject(entries, state = {}) {
36
40
  if (parent !== null && !Tree.isMap(parent)) {
37
41
  throw new TypeError(`Parent must be a map or null`);
38
42
  }
43
+
39
44
  setParent(object, parent);
40
45
 
41
46
  // The object in Map form for use on the stack
@@ -51,17 +56,7 @@ export default async function expressionObject(entries, state = {}) {
51
56
  }
52
57
  }
53
58
 
54
- // Second pass: redefine eager string-keyed properties with actual values.
55
- for (const info of infos) {
56
- if (
57
- info.keyType === KEY_TYPE.STRING &&
58
- info.valueType === VALUE_TYPE.EAGER
59
- ) {
60
- await redefineProperty(object, info);
61
- }
62
- }
63
-
64
- // Third pass: define all computed properties. These may refer to the
59
+ // Second pass: define all computed properties. These may refer to the
65
60
  // properties we just defined.
66
61
  for (const info of infos) {
67
62
  if (info.keyType === KEY_TYPE.COMPUTED) {
@@ -73,15 +68,11 @@ export default async function expressionObject(entries, state = {}) {
73
68
  }
74
69
  }
75
70
 
76
- // Fourth pass: redefine eager computed-keyed properties with actual values.
77
- for (const info of infos) {
78
- if (
79
- info.keyType === KEY_TYPE.COMPUTED &&
80
- info.valueType === VALUE_TYPE.EAGER
81
- ) {
82
- await redefineProperty(object, info);
83
- }
84
- }
71
+ // Third pass: retrieve eager properties, memoizing them on the object
72
+ const eagerKeys = infos
73
+ .filter((info) => info.valueType === VALUE_TYPE.EAGER)
74
+ .map((info) => info.key);
75
+ await Promise.all(eagerKeys.map((key) => object[key]));
85
76
 
86
77
  // Attach a keys method, where keys for primitive/eager properties with
87
78
  // maplike values get a trailing slash.
@@ -95,16 +86,6 @@ export default async function expressionObject(entries, state = {}) {
95
86
  writable: true,
96
87
  });
97
88
 
98
- // TODO: If there are any getters, mark the object as async. Note: this code
99
- // was added so that Tree.from() could know whether to return an ObjectMap or
100
- // a hypothetical AsyncObjectMap, which in turn would let a map operation know
101
- // whether to expect async property values. const hasGetters =
102
- // infos.some((info) => info.valueType === VALUE_TYPE.GETTER); if (hasGetters)
103
- // { Object.defineProperty(object, symbols.async, { configurable: true,
104
- // enumerable: false, value: true, writable: true,
105
- // });
106
- // }
107
-
108
89
  return object;
109
90
  }
110
91
 
@@ -112,6 +93,7 @@ export default async function expressionObject(entries, state = {}) {
112
93
  * Define a single property on the object
113
94
  */
114
95
  function defineProperty(object, propertyInfo, state, map) {
96
+ const { globals } = state;
115
97
  let { enumerable, hasExtension, key, value, valueType } = propertyInfo;
116
98
  if (valueType == VALUE_TYPE.PRIMITIVE) {
117
99
  // Define simple property
@@ -127,14 +109,54 @@ function defineProperty(object, propertyInfo, state, map) {
127
109
  configurable: true,
128
110
  enumerable,
129
111
  get: async () => {
112
+ // Execute the code to get the value of the property
113
+ const propertyCachePath = getPropertyCachePath(object, key);
114
+
130
115
  const newState = Object.assign({}, state, { object: map });
131
- const result = await execute(value, newState);
132
- return hasExtension ? handleExtension(result, key, map) : result;
116
+ let result = propertyCachePath
117
+ ? await systemCache.getOrInsertComputedAsync(propertyCachePath, () =>
118
+ execute(value, newState),
119
+ )
120
+ : await execute(value, newState);
121
+
122
+ if (hasExtension) {
123
+ // Handle extension
124
+ result = handleExtension(result, key, globals, map);
125
+ }
126
+
127
+ if (valueType === VALUE_TYPE.EAGER) {
128
+ // Memoize result on the object itself
129
+ Object.defineProperty(object, key, {
130
+ configurable: true,
131
+ enumerable,
132
+ value: result,
133
+ writable: true,
134
+ });
135
+ } else if (propertyCachePath) {
136
+ result = enableValueCaching(result, propertyCachePath);
137
+ }
138
+
139
+ return result;
133
140
  },
134
141
  });
135
142
  }
136
143
  }
137
144
 
145
+ function getPropertyCachePath(object, key) {
146
+ // Follow parent chain looking for a parent that has caching enabled
147
+ let current = object;
148
+ while (current[cachePathSymbol] === undefined) {
149
+ current = current[symbols.parent];
150
+ if (!current) {
151
+ // Caching isn't enabled on this object tree
152
+ return null;
153
+ }
154
+ }
155
+
156
+ const cachePath = SystemCacheMap.joinPath(current[cachePathSymbol], key);
157
+ return cachePath;
158
+ }
159
+
138
160
  /**
139
161
  * Return a normalized version of the property key for use in the keys() method.
140
162
  * Among other things, this adds trailing slashes to keys that correspond to
@@ -227,17 +249,3 @@ export function propertyInfo(key, value) {
227
249
 
228
250
  return { enumerable, hasExtension, key, keyType, value, valueType };
229
251
  }
230
-
231
- /**
232
- * Get the value of the indicated eager property and overwrite the property
233
- * definition with the actual value.
234
- */
235
- async function redefineProperty(object, info) {
236
- const value = await object[info.key];
237
- Object.defineProperty(object, info.key, {
238
- configurable: true,
239
- enumerable: info.enumerable,
240
- value,
241
- writable: true,
242
- });
243
- }
@@ -8,7 +8,10 @@ import {
8
8
  trailingSlash,
9
9
  } from "@weborigami/async-tree";
10
10
  import getPackedPath from "../handlers/getPackedPath.js";
11
- import projectGlobals from "../project/projectGlobals.js";
11
+ import mediaTypeExtensions from "../handlers/mediaTypeExtensions.json" with { type: "json" };
12
+ import { cachePathSymbol } from "./symbols.js";
13
+ import systemCache from "./systemCache.js";
14
+ import SystemCacheMap from "./SystemCacheMap.js";
12
15
 
13
16
  /**
14
17
  * If the given value is packed (e.g., buffer) and the key is a string-like path
@@ -17,28 +20,33 @@ import projectGlobals from "../project/projectGlobals.js";
17
20
  *
18
21
  * @param {any} value
19
22
  * @param {any} key
23
+ * @param {any} handlers
20
24
  * @param {import("@weborigami/async-tree").SyncOrAsyncMap|null} [parent]
21
25
  */
22
- export default async function handleExtension(value, key, parent = null) {
23
- if (isPacked(value) && isStringlike(key) && value.unpack === undefined) {
24
- const hasSlash = trailingSlash.has(key);
25
- if (hasSlash) {
26
- key = trailingSlash.remove(key);
27
- }
26
+ export default function handleExtension(value, key, handlers, parent = null) {
27
+ if (
28
+ isPacked(value) &&
29
+ isStringlike(key) &&
30
+ value.unpack === undefined &&
31
+ handlers
32
+ ) {
33
+ const normalized = trailingSlash.remove(key);
28
34
 
29
35
  // Special cases: `.ori.<ext>` extensions are Origami documents
30
- const extname = key.match(/\.ori\.\S+$/)
36
+ let extname = normalized.match(/\.ori\.\S+$/)
31
37
  ? ".oridocument"
32
- : extension.extname(key);
38
+ : extension.extname(normalized);
39
+
40
+ if (!extname && /** @type {any} */ (value)?.mediaType) {
41
+ extname = extensionFromMediaType(/** @type {any} */ (value).mediaType);
42
+ }
43
+
33
44
  if (extname) {
34
45
  const handlerName = `${extname.slice(1)}_handler`;
35
- const handlers = await projectGlobals(parent);
36
- let handler = await handlers[handlerName];
37
- if (handler) {
38
- if (isUnpackable(handler)) {
39
- // The extension handler itself needs to be unpacked
40
- handler = await handler.unpack();
41
- }
46
+ // Use `in` to look for handle so that, if the handler is a promise, we
47
+ // can still find it without awaiting it here.
48
+ if (handlerName in handlers) {
49
+ let handler = handlers[handlerName];
42
50
 
43
51
  // If the value is a primitive, box it so we can attach data to it.
44
52
  value = box(value);
@@ -51,9 +59,45 @@ export default async function handleExtension(value, key, parent = null) {
51
59
  setParent(value, parent);
52
60
  }
53
61
 
54
- if (handler.unpack) {
55
- value.unpack = wrapUnpack(handler.unpack, value, key, parent);
62
+ // Wrap the unpack function so it caches the unpacked value, and so we
63
+ // can add the file path to any errors the unpack function throws.
64
+ const filePath = getPackedPath(value, { key: normalized, parent });
65
+ let fileCachePath;
66
+ if (parent?.[cachePathSymbol]) {
67
+ fileCachePath = SystemCacheMap.joinPath(
68
+ parent[cachePathSymbol],
69
+ normalized,
70
+ );
71
+ } else {
72
+ fileCachePath = filePath;
56
73
  }
74
+ const unpackCachePath = trailingSlash.add(fileCachePath);
75
+ value.unpack = async () =>
76
+ systemCache.getOrInsertComputedAsync(unpackCachePath, async () => {
77
+ if (handler instanceof Promise) {
78
+ handler = await handler;
79
+ }
80
+ if (isUnpackable(handler)) {
81
+ // The extension handler itself needs to be unpacked
82
+ handler = await handler.unpack();
83
+ }
84
+
85
+ const unpacked = await handler.unpack(value, {
86
+ key: normalized,
87
+ parent,
88
+ });
89
+
90
+ // Now that we know the file was unpacked, we cache the file value
91
+ // itself so that subsequent requests for the file can be fulfilled
92
+ // from the cache. Doing this sort of manipulation outside of the
93
+ // cache doesn't feel great, but works.
94
+ const fileCacheEntry = systemCache.get(fileCachePath);
95
+ if (fileCacheEntry) {
96
+ fileCacheEntry.value = value;
97
+ }
98
+
99
+ return unpacked;
100
+ });
57
101
  }
58
102
  }
59
103
  }
@@ -61,20 +105,7 @@ export default async function handleExtension(value, key, parent = null) {
61
105
  return value;
62
106
  }
63
107
 
64
- // Wrap the unpack function so it's only called once per value, and so we can
65
- // add the file path to any errors it throws.
66
- function wrapUnpack(unpack, value, key, parent) {
67
- let result;
68
- return async () => {
69
- if (!result) {
70
- try {
71
- result = await unpack(value, { key, parent });
72
- } catch (/** @type {any} */ error) {
73
- const filePath = getPackedPath(value, { key, parent });
74
- const message = `Can't unpack ${filePath}\n${error.message}`;
75
- throw new error.constructor(message, { cause: error });
76
- }
77
- }
78
- return result;
79
- };
108
+ function extensionFromMediaType(mediaType) {
109
+ const essence = mediaType.split(";")[0].trim();
110
+ return mediaTypeExtensions[essence];
80
111
  }
@@ -1,13 +1,13 @@
1
1
  import { interop } from "@weborigami/async-tree";
2
- import asyncStorage from "./asyncStorage.js";
3
2
  import { lineInfo } from "./errors.js";
3
+ import executionContext from "./executionContext.js";
4
4
 
5
5
  /**
6
6
  * Inject our warning function into async-tree calls
7
7
  */
8
8
  interop.warn = function warn(...args) {
9
9
  console.warn(...args);
10
- const context = asyncStorage.getStore();
10
+ const context = executionContext.getStore();
11
11
  const location = context?.code?.location;
12
12
  if (location) {
13
13
  console.warn(lineInfo(location));
@@ -1,11 +1,11 @@
1
1
  import {
2
+ assignPropertyDescriptors,
2
3
  isPlainObject,
3
4
  isUnpackable,
4
5
  symbols,
5
6
  trailingSlash,
6
7
  Tree,
7
8
  } from "@weborigami/async-tree";
8
- import assignPropertyDescriptors from "./assignPropertyDescriptors.js";
9
9
 
10
10
  /**
11
11
  * Create a tree that's the result of merging the given trees.