rollup-plugin-concurrent-top-level-await 0.2.0 → 0.3.0
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/README.md +18 -15
- package/dist/index.d.mts +8 -1
- package/dist/index.mjs +117 -35
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -8,6 +8,18 @@ This Vite-compatible plugin enables concurrent execution of TLA modules.
|
|
|
8
8
|
Note that this plugin requires TLA support at runtime; it does _not_ provide a TLA polyfill.
|
|
9
9
|
For that, check out [vite-plugin-top-level-await](https://www.npmjs.com/package/vite-plugin-top-level-await).
|
|
10
10
|
|
|
11
|
+
### Evaluation Order
|
|
12
|
+
|
|
13
|
+
The evaluation order closely matches V8's behavior according to [tla-fuzzer](https://github.com/evanw/tla-fuzzer).
|
|
14
|
+
Minor deviations can still occur though.
|
|
15
|
+
|
|
16
|
+
| Variant | Rollup | Rollup with Plugin |
|
|
17
|
+
| ------------------------ | ------ | ------------------ |
|
|
18
|
+
| Simple | 80% | 99% |
|
|
19
|
+
| Trailing Promise | 10% | 99% |
|
|
20
|
+
| Cyclic | 69% | 99% |
|
|
21
|
+
| Cyclic, Trailing Promise | 15% | 99% |
|
|
22
|
+
|
|
11
23
|
## Installation
|
|
12
24
|
|
|
13
25
|
Using npm:
|
|
@@ -41,7 +53,9 @@ export default {
|
|
|
41
53
|
|
|
42
54
|
### Which modules to include?
|
|
43
55
|
|
|
44
|
-
The plugin needs to handle not only modules that directly contain a top-level `await`, but also their ancestor modules up to the lowest common ancestor. Ancestor modules must be transformed to handle the asynchronous completion of their children concurrently.
|
|
56
|
+
The plugin needs to handle not only modules that directly contain a top-level `await`, but also their ancestor modules up to the lowest common ancestor. Ancestor modules must be transformed to handle the asynchronous completion of their children concurrently. If an ancestor module is not transformed, its direct dependencies will become blocking and therefore alter the evaluation order.
|
|
57
|
+
|
|
58
|
+
Consider the following module structure as an example:
|
|
45
59
|
|
|
46
60
|
```mermaid
|
|
47
61
|
flowchart LR
|
|
@@ -75,22 +89,11 @@ If the red modules contain top level awaits, these and their yellow ancestors sh
|
|
|
75
89
|
|
|
76
90
|
## Known Limitations
|
|
77
91
|
|
|
78
|
-
###
|
|
79
|
-
|
|
80
|
-
We currently prioritize minimizing the required code transformations over complete compliance with the standard.
|
|
81
|
-
As a result, the execution order of TLA modules may differ from the standard behavior in certain cases, as can be seen
|
|
82
|
-
by the results for [tla-fuzzer](https://github.com/evanw/tla-fuzzer):
|
|
83
|
-
|
|
84
|
-
| Variant | Rollup | Rollup with Plugin |
|
|
85
|
-
| ------------------------ | ------ | ------------------ |
|
|
86
|
-
| Simple | 80% | 100% |
|
|
87
|
-
| Trailing Promise | 10% | 94% |
|
|
88
|
-
| Cyclic | 69% | 77% |
|
|
89
|
-
| Cyclic, Trailing Promise | 15% | 64% |
|
|
92
|
+
### Evaluation Order
|
|
90
93
|
|
|
91
|
-
|
|
94
|
+
As can be seen in the table above, the plugin does not guarantee 100% matching of V8's evaluation order, although the deviations should be pretty minor in practice. If you encounter significant deviations, please open an issue.
|
|
92
95
|
|
|
93
|
-
|
|
96
|
+
Additionally, some scenarios are known to cause deviations, e.g. when the [`include` option is not correctly configured](#which-modules-to-include) or when there is a dependency cycle that is split across multiple chunks.
|
|
94
97
|
|
|
95
98
|
### Build Performance
|
|
96
99
|
|
package/dist/index.d.mts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { FilterPattern } from "@rollup/pluginutils";
|
|
2
2
|
import * as magic_string0 from "magic-string";
|
|
3
3
|
import * as rollup0 from "rollup";
|
|
4
|
+
import { TransformPluginContext } from "rollup";
|
|
4
5
|
|
|
5
6
|
//#region src/index.d.ts
|
|
6
7
|
declare function concurrentTopLevelAwait(options?: {
|
|
@@ -16,8 +17,13 @@ declare function concurrentTopLevelAwait(options?: {
|
|
|
16
17
|
}): {
|
|
17
18
|
name: string;
|
|
18
19
|
apply: "build";
|
|
20
|
+
resolveId(this: rollup0.PluginContext, source: string): string | undefined;
|
|
21
|
+
load(this: rollup0.PluginContext, id: string): "export default function register(fn, evaluate_accesses) {\n let state = \"ready\";\n let whenDones = [];\n let whenErrors = [];\n let remaining = 1;\n let error = undefined;\n // evaluate eagerly\n evaluate();\n\n return evaluate;\n\n function evaluate(whenDone, onError) {\n if (state === \"done\") {\n if (whenDone)\n whenDone();\n return;\n }\n if (state === \"failed\") {\n if (onError)\n onError(error);\n return;\n }\n if (whenDone)\n whenDones.push(whenDone);\n if (onError)\n whenErrors.push(onError);\n if (state === \"busy\") {\n return;\n }\n state = \"busy\";\n let moduleDone = () => {\n state = \"done\";\n for (let x of whenDones)\n x();\n };\n let moduleError = (err) => {\n state = \"failed\";\n error = err;\n for (let x of whenErrors)\n x(err);\n };\n let importDone = () => {\n if (state !== \"busy\")\n return;\n if (--remaining !== 0)\n return;\n try {\n let result = fn();\n if (result) {\n result\n .then(moduleDone)\n .catch(moduleError);\n }\n else {\n moduleDone();\n }\n }\n catch (err) {\n moduleError(err);\n }\n };\n for (const access of evaluate_accesses) {\n let evaluate;\n try {\n // Environment-dependent behavior:\n // - throws on cyclic dependencies in V8\n // - returns undefined in some environments (e.g., vitest)\n evaluate = access();\n if (evaluate == null)\n continue;\n }\n catch (_a) {\n continue;\n }\n remaining++;\n evaluate(importDone, moduleError);\n }\n importDone();\n }\n}\n" | undefined;
|
|
19
22
|
transform: {
|
|
20
|
-
handler(this:
|
|
23
|
+
handler(this: TransformPluginContext, code: string, id: string, transformOptions: {
|
|
24
|
+
ssr?: boolean | undefined;
|
|
25
|
+
attributes?: Record<string, string>;
|
|
26
|
+
} | undefined): Promise<{
|
|
21
27
|
code: string;
|
|
22
28
|
map: magic_string0.SourceMap | null;
|
|
23
29
|
} | undefined>;
|
|
@@ -25,6 +31,7 @@ declare function concurrentTopLevelAwait(options?: {
|
|
|
25
31
|
resolveImportMeta(this: rollup0.PluginContext, property: string | null, {
|
|
26
32
|
moduleId
|
|
27
33
|
}: {
|
|
34
|
+
attributes: Record<string, string>;
|
|
28
35
|
chunkId: string;
|
|
29
36
|
format: rollup0.InternalModuleFormat;
|
|
30
37
|
moduleId: string;
|
package/dist/index.mjs
CHANGED
|
@@ -128,26 +128,14 @@ var AsyncModuleTracker = class {
|
|
|
128
128
|
|
|
129
129
|
//#endregion
|
|
130
130
|
//#region src/transform.ts
|
|
131
|
-
function transform(s, ast, asyncImports, hasAwait, variablePrefix) {
|
|
131
|
+
function transform(s, ast, registerModuleSource, asyncImports, hasAwait, variablePrefix) {
|
|
132
132
|
const declarationsEnd = transformAndMoveDeclarationsToModuleScope(s, ast, asyncImports, variablePrefix);
|
|
133
|
-
s.appendRight(declarationsEnd,
|
|
133
|
+
s.appendRight(declarationsEnd, `${hasAwait ? "async " : ""}function ${variablePrefix}_initModuleExports() {\n`);
|
|
134
134
|
s.append("\n}\n");
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
return result
|
|
140
|
-
}
|
|
141
|
-
return [a];
|
|
142
|
-
} catch {
|
|
143
|
-
return []; // happens for cyclic dependencies
|
|
144
|
-
}
|
|
145
|
-
})`;
|
|
146
|
-
const initModuleExportsAfterDeps = asyncImports.length === 0 ? `${variablePrefix}_initModuleExports()` : `Promise.all(${asyncDeps}.map(e => e())).then(() => ${variablePrefix}_initModuleExports())`;
|
|
147
|
-
if (hasAwait) s.append(`const ${variablePrefix} = ${initModuleExportsAfterDeps};\nconst ${variablePrefix}_initPromise = ${variablePrefix};\n`);
|
|
148
|
-
else s.append(`const ${variablePrefix} = ${asyncDeps};\nconst ${variablePrefix}_initPromise = ${initModuleExportsAfterDeps};\n`);
|
|
149
|
-
s.append(`if (import.meta.useTla) await ${variablePrefix}_initPromise;\n`);
|
|
150
|
-
s.append(`export function ${variablePrefix}_access() { return ${variablePrefix}; };\n`);
|
|
135
|
+
s.prepend(`import ${variablePrefix}_register from ${JSON.stringify(registerModuleSource)};\n`);
|
|
136
|
+
const asyncDeps = `[${asyncImports.map((_, i) => `() => ${variablePrefix}${i}`).join(", ")}]`;
|
|
137
|
+
s.append(`export const ${variablePrefix}_access = ${variablePrefix}_register(${variablePrefix}_initModuleExports, ${asyncDeps});\n`);
|
|
138
|
+
s.append(`if (import.meta.useTla) await new Promise(${variablePrefix}_access);\n`);
|
|
151
139
|
}
|
|
152
140
|
function transformAndMoveDeclarationsToModuleScope(s, ast, asyncImports, variablePrefix) {
|
|
153
141
|
let moduleScopeEnd = 0;
|
|
@@ -243,37 +231,131 @@ function getNames(pattern) {
|
|
|
243
231
|
|
|
244
232
|
//#endregion
|
|
245
233
|
//#region src/index.ts
|
|
234
|
+
function resolveDeclarationSource(context, id, importerAttributes = {}, declaration) {
|
|
235
|
+
return context.resolve(declaration.source.value, id, {
|
|
236
|
+
attributes: Object.fromEntries(declaration.attributes.map((attr) => [attr.key.type === "Identifier" ? attr.key.name : attr.key.value, attr.value.value])),
|
|
237
|
+
importerAttributes,
|
|
238
|
+
custom: {}
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
const tlaModule = `export default function register(fn, evaluate_accesses) {
|
|
242
|
+
let state = "ready";
|
|
243
|
+
let whenDones = [];
|
|
244
|
+
let whenErrors = [];
|
|
245
|
+
let remaining = 1;
|
|
246
|
+
let error = undefined;
|
|
247
|
+
// evaluate eagerly
|
|
248
|
+
evaluate();
|
|
249
|
+
|
|
250
|
+
return evaluate;
|
|
251
|
+
|
|
252
|
+
function evaluate(whenDone, onError) {
|
|
253
|
+
if (state === "done") {
|
|
254
|
+
if (whenDone)
|
|
255
|
+
whenDone();
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
if (state === "failed") {
|
|
259
|
+
if (onError)
|
|
260
|
+
onError(error);
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
if (whenDone)
|
|
264
|
+
whenDones.push(whenDone);
|
|
265
|
+
if (onError)
|
|
266
|
+
whenErrors.push(onError);
|
|
267
|
+
if (state === "busy") {
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
state = "busy";
|
|
271
|
+
let moduleDone = () => {
|
|
272
|
+
state = "done";
|
|
273
|
+
for (let x of whenDones)
|
|
274
|
+
x();
|
|
275
|
+
};
|
|
276
|
+
let moduleError = (err) => {
|
|
277
|
+
state = "failed";
|
|
278
|
+
error = err;
|
|
279
|
+
for (let x of whenErrors)
|
|
280
|
+
x(err);
|
|
281
|
+
};
|
|
282
|
+
let importDone = () => {
|
|
283
|
+
if (state !== "busy")
|
|
284
|
+
return;
|
|
285
|
+
if (--remaining !== 0)
|
|
286
|
+
return;
|
|
287
|
+
try {
|
|
288
|
+
let result = fn();
|
|
289
|
+
if (result) {
|
|
290
|
+
result
|
|
291
|
+
.then(moduleDone)
|
|
292
|
+
.catch(moduleError);
|
|
293
|
+
}
|
|
294
|
+
else {
|
|
295
|
+
moduleDone();
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
catch (err) {
|
|
299
|
+
moduleError(err);
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
for (const access of evaluate_accesses) {
|
|
303
|
+
let evaluate;
|
|
304
|
+
try {
|
|
305
|
+
// Environment-dependent behavior:
|
|
306
|
+
// - throws on cyclic dependencies in V8
|
|
307
|
+
// - returns undefined in some environments (e.g., vitest)
|
|
308
|
+
evaluate = access();
|
|
309
|
+
if (evaluate == null)
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
catch (_a) {
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
remaining++;
|
|
316
|
+
evaluate(importDone, moduleError);
|
|
317
|
+
}
|
|
318
|
+
importDone();
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
`;
|
|
246
322
|
function concurrentTopLevelAwait(options = {}) {
|
|
247
323
|
const filter = createFilter(options.include, options.exclude);
|
|
324
|
+
const registerModuleSource = `\0${options.generatedVariablePrefix ?? "__tla"}Register`;
|
|
248
325
|
const asyncTracker = new AsyncModuleTracker();
|
|
249
326
|
return {
|
|
250
327
|
name: "rollup-plugin-concurrent-tla-plugin",
|
|
251
328
|
apply: "build",
|
|
252
|
-
|
|
329
|
+
resolveId(source) {
|
|
330
|
+
if (source === registerModuleSource) return registerModuleSource;
|
|
331
|
+
},
|
|
332
|
+
load(id) {
|
|
333
|
+
if (id === registerModuleSource) return tlaModule;
|
|
334
|
+
},
|
|
335
|
+
transform: { async handler(code, id, transformOptions) {
|
|
253
336
|
if (!filter(id)) return;
|
|
254
337
|
const ast = this.parse(code);
|
|
255
338
|
const importDeclarations = ast.body.filter((a) => a.type === "ImportDeclaration");
|
|
256
339
|
const hasAwait = hasTopLevelAwait(ast);
|
|
257
340
|
asyncTracker.setEntryAsync(id, hasAwait);
|
|
258
341
|
if (hasAwait) asyncTracker.setDependencies(id, []);
|
|
259
|
-
|
|
260
|
-
const
|
|
261
|
-
const importId = await this.resolve(declaration.source.value, id);
|
|
262
|
-
if (!importId || !filter(importId.id)) return null;
|
|
263
|
-
return importId.id;
|
|
264
|
-
}))).filter((a) => a != null);
|
|
265
|
-
asyncTracker.setDependencies(id, childrenIds);
|
|
266
|
-
}
|
|
267
|
-
const asyncImports = (await Promise.all(importDeclarations.map(async (declaration) => {
|
|
268
|
-
const importId = await this.resolve(declaration.source.value, id);
|
|
342
|
+
let imports = (await Promise.all(importDeclarations.map(async (declaration) => {
|
|
343
|
+
const importId = await resolveDeclarationSource(this, id, transformOptions?.attributes, declaration);
|
|
269
344
|
if (!importId || !filter(importId.id)) return null;
|
|
270
|
-
|
|
271
|
-
|
|
345
|
+
return {
|
|
346
|
+
declaration,
|
|
347
|
+
id: importId.id
|
|
348
|
+
};
|
|
349
|
+
}))).filter(Boolean);
|
|
350
|
+
if (!hasAwait) asyncTracker.setDependencies(id, imports.map((x) => x.id));
|
|
351
|
+
const asyncImports = (await Promise.all(imports.map(async ({ declaration, id: id$1 }) => {
|
|
352
|
+
this.load({ id: id$1 });
|
|
353
|
+
if (!await asyncTracker.isAsync(id$1)) return null;
|
|
272
354
|
return declaration;
|
|
273
355
|
}))).filter(Boolean);
|
|
274
356
|
if (!(asyncImports.length > 0 || hasAwait)) return;
|
|
275
357
|
const s = new MagicString(code);
|
|
276
|
-
transform(s, ast, asyncImports, hasAwait, options.generatedVariablePrefix ?? "__tla");
|
|
358
|
+
transform(s, ast, registerModuleSource, asyncImports, hasAwait, options.generatedVariablePrefix ?? "__tla");
|
|
277
359
|
return {
|
|
278
360
|
code: s.toString(),
|
|
279
361
|
map: options.sourceMap !== false ? s.generateMap({ hires: true }) : null
|
|
@@ -282,9 +364,9 @@ function concurrentTopLevelAwait(options = {}) {
|
|
|
282
364
|
resolveImportMeta(property, { moduleId }) {
|
|
283
365
|
if (property !== "useTla") return;
|
|
284
366
|
const moduleInfo = this.getModuleInfo(moduleId);
|
|
285
|
-
|
|
286
|
-
if (moduleInfo?.
|
|
287
|
-
if (importers
|
|
367
|
+
if (moduleInfo?.isEntry) return "true";
|
|
368
|
+
if (moduleInfo?.dynamicImporters.length) return "true";
|
|
369
|
+
if ((moduleInfo?.importers)?.some((id) => !filter(id))) return "true";
|
|
288
370
|
return "false";
|
|
289
371
|
}
|
|
290
372
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rollup-plugin-concurrent-top-level-await",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Rollup (and Vite) plugin enabling concurrent execution of modules that contain top level await.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"rollup-plugin",
|
|
@@ -46,7 +46,8 @@
|
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"@types/estree": "^1.0.8",
|
|
49
|
-
"prettier": "3.7.4"
|
|
49
|
+
"prettier": "3.7.4",
|
|
50
|
+
"rollup": "^4.57.1"
|
|
50
51
|
},
|
|
51
52
|
"scripts": {
|
|
52
53
|
"build": "tsdown --dts"
|