rollup-plugin-concurrent-top-level-await 0.0.5 → 0.0.7

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
@@ -24,12 +24,46 @@ import concurrentTopLevelAwait from "rollup-plugin-concurrent-top-level-await";
24
24
  export default {
25
25
  plugins: [
26
26
  concurrentTopLevelAwait({
27
- include: "**/*.ts",
27
+ include: "**/*.js",
28
28
  }),
29
29
  ],
30
30
  };
31
31
  ```
32
32
 
33
+ ### Which modules to include
34
+
35
+ 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:
36
+
37
+ ```mermaid
38
+ flowchart LR
39
+ app[app.js]
40
+ moduleA[moduleA.js]
41
+ moduleB[moduleB.js]
42
+ moduleC[moduleC.js]
43
+ tla1[tla1.js]
44
+ tla2[tla2.js]
45
+ tla3[tla3.js]
46
+ other[other.js]
47
+
48
+ app --> moduleA
49
+ moduleA --> moduleB
50
+ moduleA --> moduleC
51
+ moduleB --> tla1
52
+ moduleC --> tla2
53
+ moduleC --> tla3
54
+ app --> other
55
+
56
+ classDef tla fill:#ffe6e6,stroke:#ff0000,color:#660000
57
+ classDef ancestor fill:#fff4cc,stroke:#ffcc00,color:#663300
58
+ classDef unaffected fill:#e6ffe6,stroke:#00cc00,color:#006600
59
+
60
+ class tla1,tla2,tla3 tla
61
+ class moduleA,moduleB,moduleC ancestor
62
+ class app,other unaffected
63
+ ```
64
+
65
+ If the red modules contain top level awaits, these and their yellow ancestors should be included in the plugin's `include` option.
66
+
33
67
  ## Known Limitations
34
68
 
35
69
  ### Execution Order
package/dist/index.d.mts CHANGED
@@ -1,24 +1,23 @@
1
1
  import { FilterPattern } from "@rollup/pluginutils";
2
+ import * as magic_string0 from "magic-string";
2
3
  import * as rollup0 from "rollup";
3
4
 
4
5
  //#region src/index.d.ts
5
6
  declare function concurrentTopLevelAwait(options?: {
6
7
  include?: FilterPattern;
7
8
  exclude?: FilterPattern;
9
+ sourceMap?: boolean;
8
10
  }): {
9
11
  name: string;
10
12
  apply: "build";
11
13
  transform: {
12
14
  handler(this: rollup0.TransformPluginContext, code: string, id: string): Promise<{
13
15
  code: string;
14
- meta: {
15
- async: true;
16
- };
16
+ map: magic_string0.SourceMap | null;
17
17
  } | undefined>;
18
18
  };
19
19
  resolveImportMeta(this: rollup0.PluginContext, property: string | null, {
20
- moduleId,
21
- chunkId
20
+ moduleId
22
21
  }: {
23
22
  chunkId: string;
24
23
  format: rollup0.InternalModuleFormat;
package/dist/index.mjs CHANGED
@@ -31,69 +31,92 @@ function withResolvers() {
31
31
  }
32
32
 
33
33
  //#endregion
34
- //#region src/AsyncTlaTracker.ts
34
+ //#region src/AsyncModuleTracker.ts
35
35
  var AwaitableCache = class {
36
36
  #store = /* @__PURE__ */ new Map();
37
37
  #getPromise(key) {
38
38
  const entry = this.#store.get(key);
39
39
  if (entry) return entry;
40
- const { promise, resolve } = withResolvers();
41
- this.#store.set(key, [promise, resolve]);
42
- return [promise, resolve];
40
+ const { promise, resolve, reject } = withResolvers();
41
+ this.#store.set(key, [
42
+ promise,
43
+ resolve,
44
+ reject
45
+ ]);
46
+ return [
47
+ promise,
48
+ resolve,
49
+ reject
50
+ ];
43
51
  }
44
- set(key, value) {
52
+ resolve(key, value) {
45
53
  const [_, resolve] = this.#getPromise(key);
46
54
  resolve(value);
47
55
  }
56
+ reject(key) {
57
+ const [_, __, reject] = this.#getPromise(key);
58
+ reject();
59
+ }
48
60
  get(key) {
49
61
  const [promise] = this.#getPromise(key);
50
62
  return promise;
51
63
  }
52
64
  };
53
- var AsyncTlaTracker = class {
54
- #all = /* @__PURE__ */ new Set();
55
- #unseen = /* @__PURE__ */ new Set();
56
- #unresolved = /* @__PURE__ */ new Set();
57
- #resultCache = new AwaitableCache();
58
- get(key) {
59
- return this.#resultCache.get(key);
65
+ /**
66
+ * AsyncModuleTracker tracks whether modules must be treated as async due to
67
+ * containing top-level await or being an ancestor of an async module.
68
+ */
69
+ var AsyncModuleTracker = class {
70
+ #entryCache = new AwaitableCache();
71
+ #subtreeCache = new AwaitableCache();
72
+ #resultCache = /* @__PURE__ */ new Map();
73
+ #seen = /* @__PURE__ */ new Set();
74
+ #resolved = /* @__PURE__ */ new Set();
75
+ #childrenSeen = /* @__PURE__ */ new Set();
76
+ #cycleResolver;
77
+ #resolveCycles;
78
+ constructor() {
79
+ this.#createCycleResolver();
60
80
  }
61
- setMarked(key, value) {
62
- this.#all.add(key);
63
- this.#unseen.delete(key);
64
- this.#unresolved.delete(key);
65
- this.#resultCache.set(key, true);
66
- setTimeout(() => {
67
- if (this.#unseen.size == 0) {
68
- this.#unresolved.forEach((e) => this.#resultCache.set(e, false));
69
- this.#all.forEach((e) => this.#resultCache.set(e, false));
70
- this.#unresolved.clear();
71
- }
72
- }, 0);
81
+ #createCycleResolver() {
82
+ const { promise, resolve } = withResolvers();
83
+ this.#cycleResolver = promise;
84
+ this.#resolveCycles = () => resolve(false);
73
85
  }
74
- setChildren(key, children) {
75
- if (!this.#all.has(key)) {
76
- this.#all.add(key);
77
- this.#unresolved.add(key);
78
- }
79
- this.#unseen.delete(key);
80
- children.forEach((children$1) => {
81
- if (!this.#all.has(children$1)) {
82
- this.#all.add(children$1);
83
- this.#unresolved.add(key);
84
- this.#unseen.add(children$1);
85
- }
86
- this.#resultCache.get(children$1).then((value) => {
87
- if (value) this.setMarked(key, true);
86
+ #checkForCycles() {
87
+ if (this.#childrenSeen.size === this.#seen.size && this.#resolved.size === this.#seen.size) {
88
+ const resolveCycles = this.#resolveCycles;
89
+ this.#createCycleResolver();
90
+ setTimeout(() => {
91
+ resolveCycles();
88
92
  });
93
+ }
94
+ }
95
+ #bindResultCache(key) {
96
+ if (!this.#resultCache.has(key)) this.#resultCache.set(key, this.#subtreeCache.get(key).then(() => false).catch(() => true));
97
+ }
98
+ isAsync(key) {
99
+ this.#bindResultCache(key);
100
+ return this.#resultCache.get(key);
101
+ }
102
+ setEntryAsync(key, value) {
103
+ this.#seen.add(key);
104
+ this.#resolved.add(key);
105
+ if (value) {
106
+ this.#entryCache.reject(key);
107
+ this.#entryCache.get(key).catch(() => {});
108
+ } else this.#entryCache.resolve(key, void 0);
109
+ this.#checkForCycles();
110
+ }
111
+ setDependencies(key, children) {
112
+ this.#seen.add(key);
113
+ this.#childrenSeen.add(key);
114
+ children.forEach((child) => {
115
+ this.#seen.add(child);
89
116
  });
90
- setTimeout(() => {
91
- if (this.#unseen.size == 0) {
92
- this.#unresolved.forEach((e) => this.#resultCache.set(e, false));
93
- this.#all.forEach((e) => this.#resultCache.set(e, false));
94
- this.#unresolved.clear();
95
- }
96
- }, 0);
117
+ Promise.race([this.#cycleResolver, Promise.all([this.#entryCache.get(key), ...children.map((child) => this.#subtreeCache.get(child))])]).then(() => this.#subtreeCache.resolve(key, void 0)).catch(() => this.#subtreeCache.reject(key));
118
+ this.#bindResultCache(key);
119
+ this.#checkForCycles();
97
120
  }
98
121
  };
99
122
 
@@ -136,10 +159,11 @@ function tansformAndMoveDeclarationsToModuleScope(s, ast, asyncImports) {
136
159
  if (node.declaration?.type === "VariableDeclaration") {
137
160
  s = s.appendLeft(moduleScopeEnd, ";export ");
138
161
  s = s.remove(node.start, node.declaration.start);
139
- s = moveVarDeclarationToModuleScope(s, node.declaration, moduleScopeEnd);
162
+ s = moveVariableDeclarationToModuleScope(s, node.declaration, moduleScopeEnd);
140
163
  }
141
164
  }
142
- if (node.type === "VariableDeclaration") s = moveVarDeclarationToModuleScope(s, node, moduleScopeEnd);
165
+ if (node.type === "VariableDeclaration") if (node.kind.endsWith("using")) s = moveVariableDeclarationWithUsingToModuleScope(s, node, moduleScopeEnd);
166
+ else s = moveVariableDeclarationToModuleScope(s, node, moduleScopeEnd);
143
167
  if (isDeclaration(node.type) || node.type === "ImportDeclaration" || node.type === "ExportNamedDeclaration" && isDeclaration(node.declaration?.type) || node.type === "ExportNamedDeclaration" && node.declaration == null || node.type === "ExportDefaultDeclaration" || node.type === "ExportAllDeclaration") if (node.start > moduleScopeEnd) {
144
168
  s = s.appendRight(node.start, ";\n");
145
169
  s = s.move(node.start, node.end, moduleScopeEnd);
@@ -150,15 +174,26 @@ function tansformAndMoveDeclarationsToModuleScope(s, ast, asyncImports) {
150
174
  function isDeclaration(type) {
151
175
  return type === "ClassDeclaration" || type === "FunctionDeclaration";
152
176
  }
153
- function moveVarDeclarationToModuleScope(s, node, declarationsEnd) {
177
+ function moveVariableDeclarationToModuleScope(s, node, declarationsEnd) {
154
178
  const kind = replaceConstWithLet(node.kind);
155
179
  const names = node.declarations.flatMap((decl) => getNames(decl.id)).join(", ");
156
- s = s.appendRight(node.declarations[0].start, "(");
180
+ s = s.appendRight(node.declarations[0].start, ";(");
157
181
  s = s.appendLeft(node.declarations[node.declarations.length - 1].end, ")");
158
182
  s = s.appendLeft(declarationsEnd, `\n${kind} ${names};\n`);
159
183
  s = s.remove(node.start, node.declarations[0].start);
160
184
  return s;
161
185
  }
186
+ function moveVariableDeclarationWithUsingToModuleScope(s, node, declarationsEnd) {
187
+ node.declarations.forEach((declaration) => {
188
+ const id = declaration.id;
189
+ if (id.type !== "Identifier") throw new Error("'using' declarations may not have binding patterns.");
190
+ const name = id.name;
191
+ s = s.appendRight(id.start, `__tla_using_`);
192
+ s = s.appendLeft(node.end, `;\n${name} = __tla_using_${name};`);
193
+ s = s.appendLeft(declarationsEnd, `\nlet ${name};\n`);
194
+ });
195
+ return s;
196
+ }
162
197
  function replaceConstWithLet(value) {
163
198
  if (value === "const") return "let";
164
199
  return value;
@@ -183,41 +218,41 @@ function getNames(pattern) {
183
218
  //#region src/index.ts
184
219
  function concurrentTopLevelAwait(options = {}) {
185
220
  const filter = createFilter(options.include, options.exclude);
186
- const asyncTree = new AsyncTlaTracker();
221
+ const asyncTracker = new AsyncModuleTracker();
187
222
  return {
188
223
  name: "rollup-plugin-concurrent-tla-plugin",
189
224
  apply: "build",
190
225
  transform: { async handler(code, id) {
191
226
  if (!filter(id)) return;
192
- const ast = this.parse(code, { jsx: false });
227
+ const ast = this.parse(code);
193
228
  const importDeclarations = ast.body.filter((a) => a.type === "ImportDeclaration");
194
229
  const hasAwait = hasTopLevelAwait(ast);
195
- if (hasAwait) asyncTree.setMarked(id, true);
230
+ asyncTracker.setEntryAsync(id, hasAwait);
231
+ if (hasAwait) asyncTracker.setDependencies(id, []);
196
232
  else {
197
233
  const childrenIds = (await Promise.all(importDeclarations.map(async (declaration) => {
198
234
  const importId = await this.resolve(declaration.source.value, id);
199
235
  if (!importId || !filter(importId.id)) return null;
200
236
  return importId.id;
201
237
  }))).filter((a) => a != null);
202
- asyncTree.setChildren(id, childrenIds);
238
+ asyncTracker.setDependencies(id, childrenIds);
203
239
  }
204
240
  const asyncImports = (await Promise.all(importDeclarations.map(async (declaration) => {
205
241
  const importId = await this.resolve(declaration.source.value, id);
206
242
  if (!importId || !filter(importId.id)) return null;
207
243
  this.load(importId);
208
- if (!await asyncTree.get(importId.id)) return null;
244
+ if (!await asyncTracker.isAsync(importId.id)) return null;
209
245
  return declaration;
210
246
  }))).filter(Boolean);
211
- const isAsyncModule = asyncImports.length > 0 || hasAwait;
212
- if (!isAsyncModule) return;
247
+ if (!(asyncImports.length > 0 || hasAwait)) return;
213
248
  let s = new MagicString(code);
214
249
  s = transform(s, ast, asyncImports, hasAwait);
215
250
  return {
216
251
  code: s.toString(),
217
- meta: { async: isAsyncModule }
252
+ map: options.sourceMap !== false ? s.generateMap({ hires: true }) : null
218
253
  };
219
254
  } },
220
- resolveImportMeta(property, { moduleId, chunkId }) {
255
+ resolveImportMeta(property, { moduleId }) {
221
256
  if (property !== "useTla") return;
222
257
  const moduleInfo = this.getModuleInfo(moduleId);
223
258
  const importers = moduleInfo?.importers;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rollup-plugin-concurrent-top-level-await",
3
- "version": "0.0.5",
3
+ "version": "0.0.7",
4
4
  "description": "Rollup (and Vite) plugin enabling concurrent execution of modules that contain top level await.",
5
5
  "keywords": [
6
6
  "rollup-plugin",