rollup-plugin-concurrent-top-level-await 0.2.1 → 0.3.1

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
@@ -1,3 +1,6 @@
1
+ > [!Warning]
2
+ > This plugin uses Rollup-specific APIs and is therefore not compatible with Rolldown or Vite >= 8. For more information, see [this issue](https://github.com/zOadT/concurrent-top-level-await-plugins/issues/35).
3
+
1
4
  # rollup-plugin-concurrent-top-level-await
2
5
 
3
6
  Rollup (and therefore also Vite) will change the behavior of modules containing top level await (TLA):
@@ -8,6 +11,18 @@ This Vite-compatible plugin enables concurrent execution of TLA modules.
8
11
  Note that this plugin requires TLA support at runtime; it does _not_ provide a TLA polyfill.
9
12
  For that, check out [vite-plugin-top-level-await](https://www.npmjs.com/package/vite-plugin-top-level-await).
10
13
 
14
+ ### Evaluation Order
15
+
16
+ The evaluation order closely matches V8's behavior according to [tla-fuzzer](https://github.com/evanw/tla-fuzzer).
17
+ Minor deviations can still occur though.
18
+
19
+ | Variant | Rollup | Rollup with Plugin |
20
+ | ------------------------ | ------ | ------------------ |
21
+ | Simple | 80% | 99% |
22
+ | Trailing Promise | 10% | 99% |
23
+ | Cyclic | 69% | 99% |
24
+ | Cyclic, Trailing Promise | 15% | 99% |
25
+
11
26
  ## Installation
12
27
 
13
28
  Using npm:
@@ -41,7 +56,9 @@ export default {
41
56
 
42
57
  ### Which modules to include?
43
58
 
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:
59
+ 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.
60
+
61
+ Consider the following module structure as an example:
45
62
 
46
63
  ```mermaid
47
64
  flowchart LR
@@ -75,22 +92,11 @@ If the red modules contain top level awaits, these and their yellow ancestors sh
75
92
 
76
93
  ## Known Limitations
77
94
 
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% |
95
+ ### Evaluation Order
90
96
 
91
- Please do not rely on a specific execution order when using this plugin.
97
+ 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
98
 
93
- We might adapt Webpack's approach in the future to improve correctness.
99
+ 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
100
 
95
101
  ### Build Performance
96
102
 
package/dist/index.d.mts CHANGED
@@ -17,6 +17,8 @@ declare function concurrentTopLevelAwait(options?: {
17
17
  }): {
18
18
  name: string;
19
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;
20
22
  transform: {
21
23
  handler(this: TransformPluginContext, code: string, id: string, transformOptions: {
22
24
  ssr?: boolean | undefined;
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;
@@ -250,12 +238,100 @@ function resolveDeclarationSource(context, id, importerAttributes = {}, declarat
250
238
  custom: {}
251
239
  });
252
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
+ `;
253
322
  function concurrentTopLevelAwait(options = {}) {
254
323
  const filter = createFilter(options.include, options.exclude);
324
+ const registerModuleSource = `\0${options.generatedVariablePrefix ?? "__tla"}Register`;
255
325
  const asyncTracker = new AsyncModuleTracker();
256
326
  return {
257
327
  name: "rollup-plugin-concurrent-tla-plugin",
258
328
  apply: "build",
329
+ resolveId(source) {
330
+ if (source === registerModuleSource) return registerModuleSource;
331
+ },
332
+ load(id) {
333
+ if (id === registerModuleSource) return tlaModule;
334
+ },
259
335
  transform: { async handler(code, id, transformOptions) {
260
336
  if (!filter(id)) return;
261
337
  const ast = this.parse(code);
@@ -263,24 +339,23 @@ function concurrentTopLevelAwait(options = {}) {
263
339
  const hasAwait = hasTopLevelAwait(ast);
264
340
  asyncTracker.setEntryAsync(id, hasAwait);
265
341
  if (hasAwait) asyncTracker.setDependencies(id, []);
266
- else {
267
- const childrenIds = (await Promise.all(importDeclarations.map(async (declaration) => {
268
- const importId = await resolveDeclarationSource(this, id, transformOptions?.attributes, declaration);
269
- if (!importId || !filter(importId.id)) return null;
270
- return importId.id;
271
- }))).filter((a) => a != null);
272
- asyncTracker.setDependencies(id, childrenIds);
273
- }
274
- const asyncImports = (await Promise.all(importDeclarations.map(async (declaration) => {
342
+ let imports = (await Promise.all(importDeclarations.map(async (declaration) => {
275
343
  const importId = await resolveDeclarationSource(this, id, transformOptions?.attributes, declaration);
276
344
  if (!importId || !filter(importId.id)) return null;
277
- this.load(importId);
278
- 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;
279
354
  return declaration;
280
355
  }))).filter(Boolean);
281
356
  if (!(asyncImports.length > 0 || hasAwait)) return;
282
357
  const s = new MagicString(code);
283
- transform(s, ast, asyncImports, hasAwait, options.generatedVariablePrefix ?? "__tla");
358
+ transform(s, ast, registerModuleSource, asyncImports, hasAwait, options.generatedVariablePrefix ?? "__tla");
284
359
  return {
285
360
  code: s.toString(),
286
361
  map: options.sourceMap !== false ? s.generateMap({ hires: true }) : null
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rollup-plugin-concurrent-top-level-await",
3
- "version": "0.2.1",
3
+ "version": "0.3.1",
4
4
  "description": "Rollup (and Vite) plugin enabling concurrent execution of modules that contain top level await.",
5
5
  "keywords": [
6
6
  "rollup-plugin",
@@ -33,11 +33,22 @@
33
33
  },
34
34
  "homepage": "https://github.com/zOadT/concurrent-top-level-await-plugins/tree/main/packages/rollup-plugin#readme",
35
35
  "peerDependencies": {
36
- "rollup": "^4.0.0"
36
+ "rollup": "^4.0.0",
37
+ "vite": ">=5.0.0 <8.0.0"
37
38
  },
38
39
  "peerDependenciesMeta": {
39
40
  "rollup": {
40
41
  "optional": true
42
+ },
43
+ "vite": {
44
+ "optional": true
45
+ }
46
+ },
47
+ "compatiblePackages": {
48
+ "schemaVersion": 1,
49
+ "rolldown": {
50
+ "type": "incompatible",
51
+ "reason": "Uses Rollup-specific APIs"
41
52
  }
42
53
  },
43
54
  "dependencies": {