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 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. As an example, consider the following module structure:
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
- ### Execution Order
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
- Please do not rely on a specific execution order when using this plugin.
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
- We might adapt Webpack's approach in the future to improve correctness.
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: rollup0.TransformPluginContext, code: string, id: string): Promise<{
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, `async function ${variablePrefix}_initModuleExports() {\n`);
133
+ s.appendRight(declarationsEnd, `${hasAwait ? "async " : ""}function ${variablePrefix}_initModuleExports() {\n`);
134
134
  s.append("\n}\n");
135
- const asyncDeps = `[${asyncImports.map((_, i) => `${variablePrefix}${i}`).join()}].flatMap(a => {
136
- try {
137
- const result = a();
138
- if (Array.isArray(result)) {
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
- transform: { async handler(code, id) {
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
- else {
260
- const childrenIds = (await Promise.all(importDeclarations.map(async (declaration) => {
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
- this.load(importId);
271
- if (!await asyncTracker.isAsync(importId.id)) return null;
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
- const importers = moduleInfo?.importers;
286
- if (moduleInfo?.isEntry || !importers?.length) return "true";
287
- if (importers.some((id) => !filter(id))) return "true";
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.2.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"