rollup-plugin-iife-split 0.0.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 +119 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.js +564 -0
- package/package.json +40 -0
package/README.md
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# rollup-plugin-iife-split
|
|
2
|
+
|
|
3
|
+
A Rollup plugin that enables intelligent code-splitting for IIFE output.
|
|
4
|
+
|
|
5
|
+
## The Problem
|
|
6
|
+
|
|
7
|
+
Rollup's native IIFE output doesn't support code-splitting. With ESM, Rollup automatically creates shared chunks that are imported transparently. But with IIFE, you'd need to manually add `<script>` tags for each chunk, and those chunk names change between builds.
|
|
8
|
+
|
|
9
|
+
## The Solution
|
|
10
|
+
|
|
11
|
+
This plugin:
|
|
12
|
+
1. Uses Rollup's ESM code-splitting internally
|
|
13
|
+
2. Merges all shared code into a "primary" entry point
|
|
14
|
+
3. Converts everything to IIFE at the last moment
|
|
15
|
+
4. Satellite entries access shared code via a global variable
|
|
16
|
+
|
|
17
|
+
The result: You get code-splitting benefits with only one `<script>` tag per entry point.
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install rollup-plugin-iife-split
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Usage
|
|
26
|
+
|
|
27
|
+
```js
|
|
28
|
+
// rollup.config.js
|
|
29
|
+
import iifeSplit from 'rollup-plugin-iife-split';
|
|
30
|
+
|
|
31
|
+
export default {
|
|
32
|
+
input: {
|
|
33
|
+
main: 'src/main.js',
|
|
34
|
+
admin: 'src/admin.js',
|
|
35
|
+
widget: 'src/widget.js'
|
|
36
|
+
},
|
|
37
|
+
plugins: [
|
|
38
|
+
iifeSplit({
|
|
39
|
+
primary: 'main', // Which entry gets the shared code
|
|
40
|
+
primaryGlobal: 'MyLib', // Browser global: window.MyLib
|
|
41
|
+
secondaryProps: {
|
|
42
|
+
admin: 'Admin', // Browser global: window.MyLib.Admin
|
|
43
|
+
widget: 'Widget', // Browser global: window.MyLib.Widget
|
|
44
|
+
},
|
|
45
|
+
sharedProp: 'Shared', // Shared code at: window.MyLib.Shared
|
|
46
|
+
})
|
|
47
|
+
],
|
|
48
|
+
output: {
|
|
49
|
+
dir: 'dist'
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Output
|
|
55
|
+
|
|
56
|
+
**main.js** (primary entry):
|
|
57
|
+
```js
|
|
58
|
+
var MyLib = (function (exports) {
|
|
59
|
+
// Shared code from all common dependencies
|
|
60
|
+
function sharedUtil() { /* ... */ }
|
|
61
|
+
|
|
62
|
+
// Main entry code
|
|
63
|
+
function mainFeature() { /* ... */ }
|
|
64
|
+
|
|
65
|
+
exports.mainFeature = mainFeature;
|
|
66
|
+
exports.Shared = { sharedUtil };
|
|
67
|
+
return exports;
|
|
68
|
+
})({});
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**admin.js** (satellite entry):
|
|
72
|
+
```js
|
|
73
|
+
MyLib.Admin = (function (exports, shared) {
|
|
74
|
+
// Uses shared code via parameter
|
|
75
|
+
function adminFeature() {
|
|
76
|
+
return shared.sharedUtil();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
exports.adminFeature = adminFeature;
|
|
80
|
+
return exports;
|
|
81
|
+
})({}, MyLib.Shared);
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## HTML Usage
|
|
85
|
+
|
|
86
|
+
```html
|
|
87
|
+
<!-- Load primary first (contains shared code) -->
|
|
88
|
+
<script src="dist/main.js"></script>
|
|
89
|
+
|
|
90
|
+
<!-- Then load any satellite entries you need -->
|
|
91
|
+
<script src="dist/admin.js"></script>
|
|
92
|
+
|
|
93
|
+
<script>
|
|
94
|
+
MyLib.mainFeature();
|
|
95
|
+
MyLib.Admin.adminFeature();
|
|
96
|
+
</script>
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Options
|
|
100
|
+
|
|
101
|
+
| Option | Type | Required | Description |
|
|
102
|
+
|--------|------|----------|-------------|
|
|
103
|
+
| `primary` | `string` | Yes | Name of the primary entry (must match a key in Rollup's `input`). Shared code is merged into this entry. |
|
|
104
|
+
| `primaryGlobal` | `string` | Yes | Browser global variable name for the primary entry. Example: `'MyLib'` → `window.MyLib` |
|
|
105
|
+
| `secondaryProps` | `Record<string, string>` | Yes | Maps secondary entry names to their property name on the primary global. Example: `{ admin: 'Admin' }` → `window.MyLib.Admin` |
|
|
106
|
+
| `sharedProp` | `string` | Yes | Property name on the global where shared exports are attached. Example: `'Shared'` → `window.MyLib.Shared` |
|
|
107
|
+
| `debug` | `boolean` | No | Enable debug logging to see intermediate transformation steps. |
|
|
108
|
+
|
|
109
|
+
## How It Works
|
|
110
|
+
|
|
111
|
+
1. **Build phase**: Rollup builds with ESM format, using `manualChunks` to consolidate all shared modules into one chunk
|
|
112
|
+
2. **Transform phase**: In `generateBundle`, the plugin:
|
|
113
|
+
- Merges the shared chunk into the primary entry
|
|
114
|
+
- Converts each chunk from ESM to IIFE using a nested Rollup instance
|
|
115
|
+
- Deletes the shared chunk from output
|
|
116
|
+
|
|
117
|
+
## License
|
|
118
|
+
|
|
119
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Plugin } from 'rollup';
|
|
2
|
+
|
|
3
|
+
interface IifeSplitOptions {
|
|
4
|
+
/**
|
|
5
|
+
* The name of the primary entry point (must match a key in Rollup's input map).
|
|
6
|
+
* The shared chunk will be merged into this entry.
|
|
7
|
+
*/
|
|
8
|
+
primary: string;
|
|
9
|
+
/**
|
|
10
|
+
* The global variable name for the primary entry's exports.
|
|
11
|
+
* Example: 'MyLib' results in `window.MyLib = ...`
|
|
12
|
+
*/
|
|
13
|
+
primaryGlobal: string;
|
|
14
|
+
/**
|
|
15
|
+
* Maps secondary entry names to their property name on the primary global.
|
|
16
|
+
* Example: { admin: 'Admin', widget: 'Widget' } results in:
|
|
17
|
+
* - `window.MyLib.Admin` for the 'admin' entry
|
|
18
|
+
* - `window.MyLib.Widget` for the 'widget' entry
|
|
19
|
+
*/
|
|
20
|
+
secondaryProps: Record<string, string>;
|
|
21
|
+
/**
|
|
22
|
+
* The property name on the global where shared exports are attached.
|
|
23
|
+
* Example: 'Shared' results in `window.MyLib.Shared = { ... }`
|
|
24
|
+
*/
|
|
25
|
+
sharedProp: string;
|
|
26
|
+
/**
|
|
27
|
+
* Enable debug logging to see intermediate transformation steps.
|
|
28
|
+
*/
|
|
29
|
+
debug?: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
declare function iifeSplit(options: IifeSplitOptions): Plugin;
|
|
33
|
+
|
|
34
|
+
export { type IifeSplitOptions, iifeSplit as default };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,564 @@
|
|
|
1
|
+
// src/chunk-analyzer.ts
|
|
2
|
+
function isOutputChunk(item) {
|
|
3
|
+
return item.type === "chunk";
|
|
4
|
+
}
|
|
5
|
+
var SHARED_CHUNK_NAME = "__shared__";
|
|
6
|
+
function analyzeChunks(bundle, primaryEntryName) {
|
|
7
|
+
const chunks = Object.values(bundle).filter(isOutputChunk);
|
|
8
|
+
const sharedChunk = chunks.find(
|
|
9
|
+
(chunk) => !chunk.isEntry && chunk.name === SHARED_CHUNK_NAME
|
|
10
|
+
) || null;
|
|
11
|
+
const primaryChunk = chunks.find(
|
|
12
|
+
(chunk) => chunk.isEntry && chunk.name === primaryEntryName
|
|
13
|
+
);
|
|
14
|
+
if (!primaryChunk) {
|
|
15
|
+
const availableEntries = chunks.filter((c) => c.isEntry).map((c) => c.name).join(", ");
|
|
16
|
+
throw new Error(
|
|
17
|
+
`Primary entry "${primaryEntryName}" not found in bundle. Available entries: ${availableEntries}`
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
const satelliteChunks = chunks.filter(
|
|
21
|
+
(chunk) => chunk.isEntry && chunk.name !== primaryEntryName
|
|
22
|
+
);
|
|
23
|
+
return { sharedChunk, primaryChunk, satelliteChunks };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// src/esm-to-iife.ts
|
|
27
|
+
import { rollup } from "rollup";
|
|
28
|
+
import { walk } from "estree-walker";
|
|
29
|
+
import MagicString from "magic-string";
|
|
30
|
+
var VIRTUAL_ENTRY = "\0virtual:entry";
|
|
31
|
+
function createVirtualPlugin(code) {
|
|
32
|
+
return {
|
|
33
|
+
name: "virtual-entry",
|
|
34
|
+
resolveId(id) {
|
|
35
|
+
if (id === VIRTUAL_ENTRY) {
|
|
36
|
+
return id;
|
|
37
|
+
}
|
|
38
|
+
return { id, external: true };
|
|
39
|
+
},
|
|
40
|
+
load(id) {
|
|
41
|
+
if (id === VIRTUAL_ENTRY) {
|
|
42
|
+
return code;
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function extractSharedImportMappings(code, parse) {
|
|
49
|
+
const ast = parse(code);
|
|
50
|
+
const mappings = [];
|
|
51
|
+
walk(ast, {
|
|
52
|
+
enter(node) {
|
|
53
|
+
if (node.type === "ImportDeclaration") {
|
|
54
|
+
const importNode = node;
|
|
55
|
+
const source = importNode.source.value;
|
|
56
|
+
if (typeof source === "string" && source.includes(SHARED_CHUNK_NAME)) {
|
|
57
|
+
for (const spec of importNode.specifiers) {
|
|
58
|
+
if (spec.type === "ImportSpecifier" && spec.imported) {
|
|
59
|
+
mappings.push({
|
|
60
|
+
imported: spec.imported.name,
|
|
61
|
+
local: spec.local.name
|
|
62
|
+
});
|
|
63
|
+
} else if (spec.type === "ImportDefaultSpecifier") {
|
|
64
|
+
mappings.push({
|
|
65
|
+
imported: "default",
|
|
66
|
+
local: spec.local.name
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
return mappings;
|
|
75
|
+
}
|
|
76
|
+
function stripNamespaceGuards(code) {
|
|
77
|
+
let result = code.replace(/^this\.\w+\s*=\s*this\.\w+\s*\|\|\s*\{\};\n/gm, "");
|
|
78
|
+
result = result.replace(/^this\.(\w+\.\w+)\s*=/gm, "$1 =");
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
81
|
+
function destructureSharedParameter(code, mappings, parse) {
|
|
82
|
+
const ast = parse(code);
|
|
83
|
+
const ms = new MagicString(code);
|
|
84
|
+
let sharedParamStart = -1;
|
|
85
|
+
let sharedParamEnd = -1;
|
|
86
|
+
let sharedParamName = null;
|
|
87
|
+
walk(ast, {
|
|
88
|
+
enter(node) {
|
|
89
|
+
if (sharedParamName === null && node.type === "FunctionExpression") {
|
|
90
|
+
const fn = node;
|
|
91
|
+
const params = fn.params;
|
|
92
|
+
if (params.length > 0) {
|
|
93
|
+
const lastParam = params[params.length - 1];
|
|
94
|
+
if (lastParam.type === "Identifier") {
|
|
95
|
+
const acornParam = lastParam;
|
|
96
|
+
sharedParamStart = acornParam.start;
|
|
97
|
+
sharedParamEnd = acornParam.end;
|
|
98
|
+
sharedParamName = lastParam.name;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
if (sharedParamName === null) {
|
|
105
|
+
return code;
|
|
106
|
+
}
|
|
107
|
+
const propertyAccesses = [];
|
|
108
|
+
walk(ast, {
|
|
109
|
+
enter(node) {
|
|
110
|
+
if (node.type === "MemberExpression") {
|
|
111
|
+
const memberNode = node;
|
|
112
|
+
const obj = memberNode.object;
|
|
113
|
+
if (obj.type === "Identifier" && obj.name === sharedParamName && !memberNode.computed) {
|
|
114
|
+
const prop = memberNode.property;
|
|
115
|
+
propertyAccesses.push({
|
|
116
|
+
start: memberNode.start,
|
|
117
|
+
end: memberNode.end,
|
|
118
|
+
propName: prop.name
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
let effectiveMappings = mappings;
|
|
125
|
+
if (effectiveMappings.length === 0) {
|
|
126
|
+
const propNames = new Set(propertyAccesses.map((a) => a.propName));
|
|
127
|
+
effectiveMappings = Array.from(propNames).map((prop) => ({
|
|
128
|
+
imported: prop,
|
|
129
|
+
local: prop
|
|
130
|
+
}));
|
|
131
|
+
}
|
|
132
|
+
if (effectiveMappings.length === 0) {
|
|
133
|
+
return code;
|
|
134
|
+
}
|
|
135
|
+
const importToLocal = new Map(effectiveMappings.map((m) => [m.imported, m.local]));
|
|
136
|
+
const destructureEntries = effectiveMappings.map(
|
|
137
|
+
(m) => m.imported === m.local ? m.imported : `${m.imported}: ${m.local}`
|
|
138
|
+
);
|
|
139
|
+
const destructurePattern = `{ ${destructureEntries.join(", ")} }`;
|
|
140
|
+
ms.overwrite(sharedParamStart, sharedParamEnd, destructurePattern);
|
|
141
|
+
for (const { start, end, propName } of propertyAccesses) {
|
|
142
|
+
const localName = importToLocal.get(propName) ?? propName;
|
|
143
|
+
ms.overwrite(start, end, localName);
|
|
144
|
+
}
|
|
145
|
+
walk(ast, {
|
|
146
|
+
enter(node) {
|
|
147
|
+
if (node.type === "ExpressionStatement") {
|
|
148
|
+
const exprStmt = node;
|
|
149
|
+
if (exprStmt.expression.type === "Literal" && exprStmt.expression.value === "use strict") {
|
|
150
|
+
ms.remove(exprStmt.start, exprStmt.end);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
return ms.toString();
|
|
156
|
+
}
|
|
157
|
+
async function convertToIife(options) {
|
|
158
|
+
const { code, globalName, globals, sharedGlobalPath, sharedChunkFileName, parse, debug } = options;
|
|
159
|
+
const importMappings = sharedGlobalPath ? extractSharedImportMappings(code, parse) : [];
|
|
160
|
+
if (debug && sharedGlobalPath) {
|
|
161
|
+
console.log("\n=== DEBUG convertToIife ===");
|
|
162
|
+
console.log("globalName:", globalName);
|
|
163
|
+
console.log("sharedGlobalPath:", sharedGlobalPath);
|
|
164
|
+
console.log("sharedChunkFileName:", sharedChunkFileName);
|
|
165
|
+
console.log("--- ESM code (first 500 chars) ---");
|
|
166
|
+
console.log(code.slice(0, 500));
|
|
167
|
+
console.log("--- Import mappings ---");
|
|
168
|
+
console.log(importMappings);
|
|
169
|
+
}
|
|
170
|
+
const rollupGlobals = (id) => {
|
|
171
|
+
if (sharedGlobalPath) {
|
|
172
|
+
if (id.includes(SHARED_CHUNK_NAME)) {
|
|
173
|
+
return sharedGlobalPath;
|
|
174
|
+
}
|
|
175
|
+
if (sharedChunkFileName) {
|
|
176
|
+
const fileNameWithoutExt = sharedChunkFileName.replace(/\.js$/, "");
|
|
177
|
+
if (id.includes(fileNameWithoutExt)) {
|
|
178
|
+
return sharedGlobalPath;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return globals[id] ?? id;
|
|
183
|
+
};
|
|
184
|
+
const bundle = await rollup({
|
|
185
|
+
input: VIRTUAL_ENTRY,
|
|
186
|
+
plugins: [createVirtualPlugin(code)],
|
|
187
|
+
onwarn: () => {
|
|
188
|
+
}
|
|
189
|
+
// Suppress warnings
|
|
190
|
+
});
|
|
191
|
+
const { output } = await bundle.generate({
|
|
192
|
+
format: "iife",
|
|
193
|
+
name: globalName,
|
|
194
|
+
globals: rollupGlobals,
|
|
195
|
+
exports: "named"
|
|
196
|
+
});
|
|
197
|
+
await bundle.close();
|
|
198
|
+
let result = output[0].code;
|
|
199
|
+
if (debug && sharedGlobalPath) {
|
|
200
|
+
console.log("--- IIFE before destructuring (first 800 chars) ---");
|
|
201
|
+
console.log(result.slice(0, 800));
|
|
202
|
+
}
|
|
203
|
+
if (sharedGlobalPath) {
|
|
204
|
+
result = stripNamespaceGuards(result);
|
|
205
|
+
result = destructureSharedParameter(result, importMappings, parse);
|
|
206
|
+
if (debug) {
|
|
207
|
+
console.log("--- IIFE after destructuring (first 800 chars) ---");
|
|
208
|
+
console.log(result.slice(0, 800));
|
|
209
|
+
console.log("=== END DEBUG ===\n");
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return result;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// src/chunk-merger.ts
|
|
216
|
+
import MagicString2 from "magic-string";
|
|
217
|
+
import { walk as walk2 } from "estree-walker";
|
|
218
|
+
function extractTopLevelDeclarations(code, parse) {
|
|
219
|
+
const ast = parse(code);
|
|
220
|
+
const declarations = /* @__PURE__ */ new Set();
|
|
221
|
+
for (const node of ast.body) {
|
|
222
|
+
if (node.type === "VariableDeclaration") {
|
|
223
|
+
const varDecl = node;
|
|
224
|
+
for (const decl of varDecl.declarations) {
|
|
225
|
+
if (decl.id.type === "Identifier") {
|
|
226
|
+
declarations.add(decl.id.name);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
} else if (node.type === "FunctionDeclaration") {
|
|
230
|
+
const funcDecl = node;
|
|
231
|
+
if (funcDecl.id) {
|
|
232
|
+
declarations.add(funcDecl.id.name);
|
|
233
|
+
}
|
|
234
|
+
} else if (node.type === "ClassDeclaration") {
|
|
235
|
+
const classDecl = node;
|
|
236
|
+
if (classDecl.id) {
|
|
237
|
+
declarations.add(classDecl.id.name);
|
|
238
|
+
}
|
|
239
|
+
} else if (node.type === "ExportNamedDeclaration") {
|
|
240
|
+
const exportNode = node;
|
|
241
|
+
if (exportNode.declaration) {
|
|
242
|
+
if (exportNode.declaration.type === "VariableDeclaration") {
|
|
243
|
+
for (const decl of exportNode.declaration.declarations) {
|
|
244
|
+
if (decl.id.type === "Identifier") {
|
|
245
|
+
declarations.add(decl.id.name);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
} else if (exportNode.declaration.type === "FunctionDeclaration" && exportNode.declaration.id) {
|
|
249
|
+
declarations.add(exportNode.declaration.id.name);
|
|
250
|
+
} else if (exportNode.declaration.type === "ClassDeclaration" && exportNode.declaration.id) {
|
|
251
|
+
declarations.add(exportNode.declaration.id.name);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return declarations;
|
|
257
|
+
}
|
|
258
|
+
function renameIdentifiers(code, renameMap, parse) {
|
|
259
|
+
if (renameMap.size === 0) return code;
|
|
260
|
+
const ast = parse(code);
|
|
261
|
+
const s = new MagicString2(code);
|
|
262
|
+
walk2(ast, {
|
|
263
|
+
enter(node) {
|
|
264
|
+
if (node.type === "Identifier") {
|
|
265
|
+
const id = node;
|
|
266
|
+
const newName = renameMap.get(id.name);
|
|
267
|
+
if (newName) {
|
|
268
|
+
s.overwrite(id.start, id.end, newName);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
return s.toString();
|
|
274
|
+
}
|
|
275
|
+
function extractExports(code, parse) {
|
|
276
|
+
const ast = parse(code);
|
|
277
|
+
const exports = [];
|
|
278
|
+
let hasDefault = false;
|
|
279
|
+
walk2(ast, {
|
|
280
|
+
enter(node) {
|
|
281
|
+
if (node.type === "ExportNamedDeclaration") {
|
|
282
|
+
const exportNode = node;
|
|
283
|
+
if (exportNode.declaration) {
|
|
284
|
+
if (exportNode.declaration.type === "VariableDeclaration") {
|
|
285
|
+
for (const decl of exportNode.declaration.declarations) {
|
|
286
|
+
if (decl.id.type === "Identifier") {
|
|
287
|
+
exports.push({ exportedName: decl.id.name, localName: decl.id.name });
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
} else if (exportNode.declaration.type === "FunctionDeclaration" && exportNode.declaration.id) {
|
|
291
|
+
const name = exportNode.declaration.id.name;
|
|
292
|
+
exports.push({ exportedName: name, localName: name });
|
|
293
|
+
} else if (exportNode.declaration.type === "ClassDeclaration" && exportNode.declaration.id) {
|
|
294
|
+
const name = exportNode.declaration.id.name;
|
|
295
|
+
exports.push({ exportedName: name, localName: name });
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
if (exportNode.specifiers) {
|
|
299
|
+
for (const spec of exportNode.specifiers) {
|
|
300
|
+
const exported = spec.exported;
|
|
301
|
+
const local = spec.local;
|
|
302
|
+
exports.push({ exportedName: exported.name, localName: local.name });
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
if (node.type === "ExportDefaultDeclaration") {
|
|
307
|
+
hasDefault = true;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
return { exports, hasDefault };
|
|
312
|
+
}
|
|
313
|
+
function stripExports(code, parse) {
|
|
314
|
+
const ast = parse(code);
|
|
315
|
+
const s = new MagicString2(code);
|
|
316
|
+
walk2(ast, {
|
|
317
|
+
enter(node) {
|
|
318
|
+
const n = node;
|
|
319
|
+
if (node.type === "ExportNamedDeclaration") {
|
|
320
|
+
const exportNode = node;
|
|
321
|
+
if (exportNode.declaration) {
|
|
322
|
+
const declNode = exportNode.declaration;
|
|
323
|
+
s.remove(exportNode.start, declNode.start);
|
|
324
|
+
} else {
|
|
325
|
+
s.remove(n.start, n.end);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
if (node.type === "ExportDefaultDeclaration") {
|
|
329
|
+
const exportNode = node;
|
|
330
|
+
const declNode = exportNode.declaration;
|
|
331
|
+
s.overwrite(exportNode.start, declNode.start, "const __shared_default__ = ");
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
return s.toString();
|
|
336
|
+
}
|
|
337
|
+
function isSharedChunkSource(source, sharedChunkFileName) {
|
|
338
|
+
return source.includes(SHARED_CHUNK_NAME) || source.includes(sharedChunkFileName.replace(/\.js$/, ""));
|
|
339
|
+
}
|
|
340
|
+
function removeSharedImportsAndRewriteRefs(code, sharedChunkFileName, sharedExportToLocal, parse) {
|
|
341
|
+
const ast = parse(code);
|
|
342
|
+
const s = new MagicString2(code);
|
|
343
|
+
const namespaceNames = /* @__PURE__ */ new Set();
|
|
344
|
+
walk2(ast, {
|
|
345
|
+
enter(node) {
|
|
346
|
+
if (node.type === "ImportDeclaration") {
|
|
347
|
+
const importNode = node;
|
|
348
|
+
const source = importNode.source.value;
|
|
349
|
+
if (typeof source === "string" && isSharedChunkSource(source, sharedChunkFileName)) {
|
|
350
|
+
for (const spec of importNode.specifiers) {
|
|
351
|
+
if (spec.type === "ImportNamespaceSpecifier") {
|
|
352
|
+
namespaceNames.add(spec.local.name);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
walk2(ast, {
|
|
360
|
+
enter(node) {
|
|
361
|
+
if (node.type === "ImportDeclaration") {
|
|
362
|
+
const importNode = node;
|
|
363
|
+
const source = importNode.source.value;
|
|
364
|
+
if (typeof source === "string" && isSharedChunkSource(source, sharedChunkFileName)) {
|
|
365
|
+
s.remove(importNode.start, importNode.end);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
if (node.type === "ExportNamedDeclaration") {
|
|
369
|
+
const exportNode = node;
|
|
370
|
+
if (exportNode.source) {
|
|
371
|
+
const source = exportNode.source.value;
|
|
372
|
+
if (typeof source === "string" && isSharedChunkSource(source, sharedChunkFileName)) {
|
|
373
|
+
const exportParts = [];
|
|
374
|
+
for (const spec of exportNode.specifiers) {
|
|
375
|
+
const exportedName = spec.exported.name;
|
|
376
|
+
const importedName = spec.local.name;
|
|
377
|
+
const localName = sharedExportToLocal.get(importedName) ?? importedName;
|
|
378
|
+
if (localName === exportedName) {
|
|
379
|
+
exportParts.push(localName);
|
|
380
|
+
} else {
|
|
381
|
+
exportParts.push(`${localName} as ${exportedName}`);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
s.overwrite(exportNode.start, exportNode.end, `export { ${exportParts.join(", ")} };`);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
if (node.type === "MemberExpression") {
|
|
389
|
+
const memberNode = node;
|
|
390
|
+
if (memberNode.object.type === "Identifier" && memberNode.object.name && namespaceNames.has(memberNode.object.name) && memberNode.property.type === "Identifier" && memberNode.property.name && !memberNode.computed) {
|
|
391
|
+
const propertyName = memberNode.property.name;
|
|
392
|
+
const localName = sharedExportToLocal.get(propertyName) ?? propertyName;
|
|
393
|
+
s.overwrite(memberNode.start, memberNode.end, localName);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
return s.toString();
|
|
399
|
+
}
|
|
400
|
+
function extractSharedImports(code, sharedChunkFileName, parse) {
|
|
401
|
+
const ast = parse(code);
|
|
402
|
+
const imports = /* @__PURE__ */ new Set();
|
|
403
|
+
walk2(ast, {
|
|
404
|
+
enter(node) {
|
|
405
|
+
if (node.type === "ImportDeclaration") {
|
|
406
|
+
const importNode = node;
|
|
407
|
+
const source = importNode.source.value;
|
|
408
|
+
if (typeof source === "string" && isSharedChunkSource(source, sharedChunkFileName)) {
|
|
409
|
+
for (const spec of importNode.specifiers) {
|
|
410
|
+
if (spec.type === "ImportSpecifier" && spec.imported) {
|
|
411
|
+
imports.add(spec.imported.name);
|
|
412
|
+
} else if (spec.type === "ImportDefaultSpecifier") {
|
|
413
|
+
imports.add("default");
|
|
414
|
+
} else if (spec.type === "ImportNamespaceSpecifier") {
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
});
|
|
421
|
+
return imports;
|
|
422
|
+
}
|
|
423
|
+
function mergeSharedIntoPrimary(primaryChunk, sharedChunk, sharedProperty, neededExports, parse) {
|
|
424
|
+
const { exports: sharedExports, hasDefault } = extractExports(sharedChunk.code, parse);
|
|
425
|
+
const sharedDeclarations = extractTopLevelDeclarations(sharedChunk.code, parse);
|
|
426
|
+
const primaryDeclarations = extractTopLevelDeclarations(primaryChunk.code, parse);
|
|
427
|
+
const renameMap = /* @__PURE__ */ new Map();
|
|
428
|
+
for (const name of sharedDeclarations) {
|
|
429
|
+
if (primaryDeclarations.has(name)) {
|
|
430
|
+
renameMap.set(name, `__shared$${name}`);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
let processedSharedCode = sharedChunk.code;
|
|
434
|
+
if (renameMap.size > 0) {
|
|
435
|
+
processedSharedCode = renameIdentifiers(processedSharedCode, renameMap, parse);
|
|
436
|
+
}
|
|
437
|
+
const strippedSharedCode = stripExports(processedSharedCode, parse);
|
|
438
|
+
const sharedExportToLocal = /* @__PURE__ */ new Map();
|
|
439
|
+
for (const exp of sharedExports) {
|
|
440
|
+
const renamedLocal = renameMap.get(exp.localName) ?? exp.localName;
|
|
441
|
+
sharedExportToLocal.set(exp.exportedName, renamedLocal);
|
|
442
|
+
}
|
|
443
|
+
if (hasDefault) {
|
|
444
|
+
sharedExportToLocal.set("default", "__shared_default__");
|
|
445
|
+
}
|
|
446
|
+
const primaryWithoutSharedImports = removeSharedImportsAndRewriteRefs(
|
|
447
|
+
primaryChunk.code,
|
|
448
|
+
sharedChunk.fileName,
|
|
449
|
+
sharedExportToLocal,
|
|
450
|
+
parse
|
|
451
|
+
);
|
|
452
|
+
const sharedExportEntries = [
|
|
453
|
+
...sharedExports.filter((exp) => neededExports.has(exp.exportedName)).map((exp) => {
|
|
454
|
+
const renamedLocal = renameMap.get(exp.localName) ?? exp.localName;
|
|
455
|
+
return exp.exportedName === renamedLocal ? renamedLocal : `${exp.exportedName}: ${renamedLocal}`;
|
|
456
|
+
}),
|
|
457
|
+
...hasDefault && neededExports.has("default") ? ["default: __shared_default__"] : []
|
|
458
|
+
];
|
|
459
|
+
const sharedExportObject = `const ${sharedProperty} = { ${sharedExportEntries.join(", ")} };`;
|
|
460
|
+
primaryChunk.code = [
|
|
461
|
+
"// === Shared code (merged by rollup-plugin-iife-split) ===",
|
|
462
|
+
strippedSharedCode.trim(),
|
|
463
|
+
"",
|
|
464
|
+
"// === Primary entry code ===",
|
|
465
|
+
primaryWithoutSharedImports.trim(),
|
|
466
|
+
"",
|
|
467
|
+
"// === Shared exports object ===",
|
|
468
|
+
sharedExportObject,
|
|
469
|
+
`export { ${sharedProperty} };`
|
|
470
|
+
].join("\n");
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// src/index.ts
|
|
474
|
+
function iifeSplit(options) {
|
|
475
|
+
const { primary, primaryGlobal, secondaryProps, sharedProp, debug } = options;
|
|
476
|
+
let outputGlobals = {};
|
|
477
|
+
const manualChunks = (id, { getModuleInfo }) => {
|
|
478
|
+
const moduleInfo = getModuleInfo(id);
|
|
479
|
+
if (!moduleInfo) return void 0;
|
|
480
|
+
if (moduleInfo.isEntry) return void 0;
|
|
481
|
+
const importers = moduleInfo.importers || [];
|
|
482
|
+
if (importers.length > 1) {
|
|
483
|
+
return SHARED_CHUNK_NAME;
|
|
484
|
+
}
|
|
485
|
+
return void 0;
|
|
486
|
+
};
|
|
487
|
+
return {
|
|
488
|
+
name: "iife-split",
|
|
489
|
+
// Hook into outputOptions to capture globals and configure chunking
|
|
490
|
+
outputOptions(outputOptions) {
|
|
491
|
+
outputGlobals = outputOptions.globals ?? {};
|
|
492
|
+
return {
|
|
493
|
+
...outputOptions,
|
|
494
|
+
format: "es",
|
|
495
|
+
manualChunks
|
|
496
|
+
};
|
|
497
|
+
},
|
|
498
|
+
// Main transformation hook - convert ESM chunks to IIFE
|
|
499
|
+
async generateBundle(outputOptions, bundle) {
|
|
500
|
+
const parse = this.parse.bind(this);
|
|
501
|
+
const analysis = analyzeChunks(bundle, primary);
|
|
502
|
+
const sharedChunkFileName = analysis.sharedChunk?.fileName ?? null;
|
|
503
|
+
if (analysis.sharedChunk) {
|
|
504
|
+
const neededExports = /* @__PURE__ */ new Set();
|
|
505
|
+
for (const satellite of analysis.satelliteChunks) {
|
|
506
|
+
const imports = extractSharedImports(satellite.code, analysis.sharedChunk.fileName, parse);
|
|
507
|
+
for (const imp of imports) {
|
|
508
|
+
neededExports.add(imp);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
mergeSharedIntoPrimary(
|
|
512
|
+
analysis.primaryChunk,
|
|
513
|
+
analysis.sharedChunk,
|
|
514
|
+
sharedProp,
|
|
515
|
+
neededExports,
|
|
516
|
+
parse
|
|
517
|
+
);
|
|
518
|
+
delete bundle[analysis.sharedChunk.fileName];
|
|
519
|
+
}
|
|
520
|
+
const conversions = [];
|
|
521
|
+
conversions.push(
|
|
522
|
+
convertToIife({
|
|
523
|
+
code: analysis.primaryChunk.code,
|
|
524
|
+
globalName: primaryGlobal,
|
|
525
|
+
globals: outputGlobals,
|
|
526
|
+
sharedGlobalPath: null,
|
|
527
|
+
// Primary doesn't need to import shared
|
|
528
|
+
sharedChunkFileName: null,
|
|
529
|
+
parse,
|
|
530
|
+
debug
|
|
531
|
+
}).then((code) => {
|
|
532
|
+
analysis.primaryChunk.code = code;
|
|
533
|
+
})
|
|
534
|
+
);
|
|
535
|
+
for (const satellite of analysis.satelliteChunks) {
|
|
536
|
+
const satelliteProp = secondaryProps[satellite.name];
|
|
537
|
+
const hasExports = satellite.exports.length > 0;
|
|
538
|
+
if (!satelliteProp && hasExports) {
|
|
539
|
+
throw new Error(
|
|
540
|
+
`Secondary entry "${satellite.name}" not found in secondaryProps. Available entries: ${Object.keys(secondaryProps).join(", ") || "(none)"}`
|
|
541
|
+
);
|
|
542
|
+
}
|
|
543
|
+
const satelliteGlobalName = satelliteProp ? `${primaryGlobal}.${satelliteProp}` : void 0;
|
|
544
|
+
conversions.push(
|
|
545
|
+
convertToIife({
|
|
546
|
+
code: satellite.code,
|
|
547
|
+
globalName: satelliteGlobalName,
|
|
548
|
+
globals: outputGlobals,
|
|
549
|
+
sharedGlobalPath: `${primaryGlobal}.${sharedProp}`,
|
|
550
|
+
sharedChunkFileName,
|
|
551
|
+
parse,
|
|
552
|
+
debug
|
|
553
|
+
}).then((code) => {
|
|
554
|
+
satellite.code = code;
|
|
555
|
+
})
|
|
556
|
+
);
|
|
557
|
+
}
|
|
558
|
+
await Promise.all(conversions);
|
|
559
|
+
}
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
export {
|
|
563
|
+
iifeSplit as default
|
|
564
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "rollup-plugin-iife-split",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Rollup plugin for intelligent IIFE code-splitting",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist"
|
|
10
|
+
],
|
|
11
|
+
"peerDependencies": {
|
|
12
|
+
"rollup": "^3.0.0 || ^4.0.0"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"magic-string": "^0.30.0",
|
|
16
|
+
"estree-walker": "^3.0.0"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"rollup": "^4.0.0",
|
|
20
|
+
"typescript": "^5.0.0",
|
|
21
|
+
"tsup": "^8.0.0",
|
|
22
|
+
"vitest": "^1.0.0",
|
|
23
|
+
"@types/node": "^20.0.0"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"rollup",
|
|
27
|
+
"plugin",
|
|
28
|
+
"iife",
|
|
29
|
+
"code-splitting",
|
|
30
|
+
"bundle"
|
|
31
|
+
],
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"scripts": {
|
|
34
|
+
"clean": "rm -rf dist",
|
|
35
|
+
"build": "tsup src/index.ts --format esm --dts",
|
|
36
|
+
"typecheck": "tsc --noEmit",
|
|
37
|
+
"test": "vitest",
|
|
38
|
+
"test:run": "vitest run"
|
|
39
|
+
}
|
|
40
|
+
}
|