@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.
- package/index.ts +1 -0
- package/main.js +7 -1
- package/package.json +7 -6
- package/src/compiler/compile.js +10 -3
- package/src/compiler/optimize.js +71 -40
- package/src/compiler/parse.js +1 -1
- package/src/compiler/parserHelpers.js +5 -3
- package/src/handlers/addExtensionKeyFn.js +18 -0
- package/src/handlers/epub_handler.js +54 -0
- package/src/handlers/getSource.js +11 -0
- package/src/handlers/handlers.js +2 -0
- package/src/handlers/htm_handler.js +1 -1
- package/src/handlers/js_handler.js +13 -4
- package/src/handlers/mediaTypeExtensions.json +15 -0
- package/src/handlers/ori_handler.js +8 -7
- package/src/handlers/oridocument_handler.js +19 -28
- package/src/handlers/processOriExport.js +17 -0
- package/src/handlers/tsv_handler.js +1 -1
- package/src/handlers/txt_handler.js +4 -2
- package/src/handlers/xhtml_handler.js +1 -1
- package/src/handlers/yaml_handler.js +6 -3
- package/src/handlers/zip_handler.js +112 -0
- package/src/project/activeProjectRoot.js +9 -0
- package/src/project/getGlobalsForTree.js +5 -0
- package/src/project/{projectGlobals.js → initializeGlobalsForTree.js} +8 -13
- package/src/project/jsGlobals.js +1 -0
- package/src/project/projectConfig.js +2 -2
- package/src/project/projectRootFromPath.js +2 -0
- package/src/protocols/constructHref.js +3 -3
- package/src/protocols/constructSiteTree.js +11 -2
- package/src/protocols/explore.js +1 -1
- package/src/protocols/explorehttp.js +1 -1
- package/src/protocols/fetchAndHandleExtension.js +23 -11
- package/src/protocols/files.js +1 -0
- package/src/protocols/http.js +4 -1
- package/src/protocols/https.js +4 -1
- package/src/protocols/httpstree.js +1 -1
- package/src/protocols/httptree.js +1 -1
- package/src/protocols/package.js +15 -3
- package/src/runtime/AsyncCacheTransform.d.ts +5 -0
- package/src/runtime/AsyncCacheTransform.js +134 -0
- package/src/runtime/HandleExtensionsTransform.d.ts +3 -1
- package/src/runtime/HandleExtensionsTransform.js +18 -2
- package/src/runtime/OrigamiFileMap.d.ts +5 -2
- package/src/runtime/OrigamiFileMap.js +27 -4
- package/src/runtime/ScopeMap.js +72 -0
- package/src/runtime/SyncCacheTransform.d.ts +8 -0
- package/src/runtime/SyncCacheTransform.js +133 -0
- package/src/runtime/SystemCacheMap.js +259 -0
- package/src/runtime/WatchFilesMixin.js +52 -19
- package/src/runtime/enableValueCaching.js +192 -0
- package/src/runtime/execute.js +2 -2
- package/src/runtime/executionContext.js +7 -0
- package/src/runtime/explainReferenceError.js +7 -2
- package/src/runtime/expressionObject.js +54 -46
- package/src/runtime/handleExtension.js +65 -34
- package/src/runtime/interop.js +2 -2
- package/src/runtime/mergeTrees.js +1 -1
- package/src/runtime/ops.js +28 -33
- package/src/runtime/symbols.js +3 -0
- package/src/runtime/systemCache.js +3 -0
- package/src/runtime/volatile.js +14 -0
- package/test/compiler/codeHelpers.js +2 -1
- package/test/compiler/optimize.test.js +62 -54
- package/test/handlers/epub_handler.test.js +27 -0
- package/test/handlers/fixtures/test.zip +0 -0
- package/test/handlers/ori_handler.test.js +22 -3
- package/test/handlers/oridocument_handler.test.js +1 -1
- package/test/handlers/zip_handler.test.js +45 -0
- package/test/protocols/https.test.js +19 -0
- package/test/protocols/package.test.js +7 -2
- package/test/runtime/AsyncCacheTransform.test.js +91 -0
- package/test/runtime/OrigamiFileMap.test.js +26 -23
- package/test/runtime/ScopeMap.test.js +49 -0
- package/test/runtime/SyncCacheTransform.test.js +93 -0
- package/test/runtime/SystemCacheMap.test.js +239 -0
- package/test/runtime/asyncCalcs.js +28 -0
- package/test/runtime/enableValueCaching.test.js +55 -0
- package/test/runtime/errors.test.js +53 -30
- package/test/runtime/evaluate.test.js +9 -4
- package/test/runtime/execute.test.js +6 -1
- package/test/runtime/expressionObject.test.js +55 -15
- package/test/runtime/fetchAndHandleExtension.test.js +24 -0
- package/test/runtime/fixtures/unpack/hello.json +1 -0
- package/test/runtime/handleExtension.test.js +12 -1
- package/test/runtime/ops.test.js +70 -65
- package/test/runtime/syncCalcs.js +27 -0
- package/test/runtime/systemCache.test.js +66 -0
- package/src/runtime/assignPropertyDescriptors.js +0 -23
- 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
|
+
}
|
package/src/runtime/execute.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isUnpackable, Tree } from "@weborigami/async-tree";
|
|
2
|
-
import
|
|
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
|
|
71
|
+
result = await executionContext.run(
|
|
72
72
|
context,
|
|
73
73
|
async () =>
|
|
74
74
|
fn instanceof Function
|
|
@@ -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
|
-
|
|
43
|
-
|
|
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:
|
|
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
|
-
//
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
132
|
-
|
|
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
|
|
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
|
|
23
|
-
if (
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
36
|
+
let extname = normalized.match(/\.ori\.\S+$/)
|
|
31
37
|
? ".oridocument"
|
|
32
|
-
: extension.extname(
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
if (
|
|
38
|
-
|
|
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
|
-
|
|
55
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
}
|
package/src/runtime/interop.js
CHANGED
|
@@ -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 =
|
|
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.
|