rollup-plugin-iife-split 0.0.3 → 0.0.5

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
@@ -105,7 +105,8 @@ MyLib.Admin = (function (exports, shared) {
105
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
106
  | `sharedProp` | `string` | Yes | Property name on the global where shared exports are attached. Example: `'Shared'` → `window.MyLib.Shared` |
107
107
  | `unshared` | `(id: string) => boolean` | No | Function that returns `true` for modules that should be duplicated instead of shared. See [Excluding Modules from Sharing](#excluding-modules-from-sharing). |
108
- | `debug` | `boolean` | No | Enable debug logging to see intermediate transformation steps. |
108
+ | `debugDir` | `string` | No | Directory to write intermediate files for debugging. If set, writes ESM files before IIFE conversion to help diagnose issues. Example: `'./debug-output'` |
109
+ | `skipRequireGlobals` | `boolean` | No | If `true`, don't error when an external module is missing a `globals` mapping. Instead, let Rollup auto-generate a sanitized global name. Default: `false` |
109
110
 
110
111
  ## Excluding Modules from Sharing
111
112
 
@@ -135,6 +136,27 @@ With this configuration:
135
136
  - They are **not** merged into the primary/shared chunk
136
137
  - Each satellite entry is self-contained with its own copy of the locale data
137
138
 
139
+ ## External Dependencies
140
+
141
+ When using external dependencies with IIFE output, you must specify `globals` in your Rollup output options to map module IDs to global variable names:
142
+
143
+ ```js
144
+ export default {
145
+ input: { /* ... */ },
146
+ external: ['lodash', '@fullcalendar/core'],
147
+ plugins: [iifeSplit({ /* ... */ })],
148
+ output: {
149
+ dir: 'dist',
150
+ globals: {
151
+ 'lodash': '_',
152
+ '@fullcalendar/core': 'FullCalendar'
153
+ }
154
+ }
155
+ };
156
+ ```
157
+
158
+ By default, the plugin will error if an external is missing from `globals`—this prevents invalid JavaScript output. If you want Rollup to auto-generate global names instead, set `skipRequireGlobals: true`.
159
+
138
160
  ## How It Works
139
161
 
140
162
  1. **Build phase**: Rollup builds with ESM format, using `manualChunks` to consolidate all shared modules into one chunk
package/dist/index.d.ts CHANGED
@@ -44,6 +44,12 @@ interface IifeSplitOptions {
44
44
  * Example: './debug-output'
45
45
  */
46
46
  debugDir?: string;
47
+ /**
48
+ * If true, don't error when an external module is missing a global mapping.
49
+ * Instead, let Rollup generate a sanitized global name automatically.
50
+ * Default: false (errors on missing globals)
51
+ */
52
+ skipRequireGlobals?: boolean;
47
53
  }
48
54
 
49
55
  declare function iifeSplit(options: IifeSplitOptions): Plugin;
package/dist/index.js CHANGED
@@ -158,9 +158,12 @@ function destructureSharedParameter(code, mappings, parse) {
158
158
  return ms.toString();
159
159
  }
160
160
  async function convertToIife(options) {
161
- const { code, globalName, globals, sharedGlobalPath, sharedChunkFileName, parse } = options;
161
+ const { code, globalName, globals, sharedGlobalPath, sharedChunkFileName, parse, skipRequireGlobals } = options;
162
162
  const importMappings = sharedGlobalPath ? extractSharedImportMappings(code, parse) : [];
163
163
  const rollupGlobals = (id) => {
164
+ if (globalName && (id === globalName || globalName.startsWith(id + "."))) {
165
+ return id;
166
+ }
164
167
  if (sharedGlobalPath) {
165
168
  if (id.includes(SHARED_CHUNK_NAME)) {
166
169
  return sharedGlobalPath;
@@ -172,7 +175,16 @@ async function convertToIife(options) {
172
175
  }
173
176
  }
174
177
  }
175
- return globals[id] ?? id;
178
+ const global = globals[id];
179
+ if (global === void 0) {
180
+ if (skipRequireGlobals) {
181
+ return void 0;
182
+ }
183
+ throw new Error(
184
+ `[iife-split] Missing global for external "${id}". IIFE builds require all externals to have a global mapping. Add it to output.globals in your Rollup config, e.g.: globals: { '${id}': 'SomeGlobalName' }`
185
+ );
186
+ }
187
+ return global;
176
188
  };
177
189
  const bundle = await rollup({
178
190
  input: VIRTUAL_ENTRY,
@@ -184,6 +196,7 @@ async function convertToIife(options) {
184
196
  const { output } = await bundle.generate({
185
197
  format: "iife",
186
198
  name: globalName,
199
+ // Cast needed: Rollup's types say string, but it handles undefined by using default name generation
187
200
  globals: rollupGlobals,
188
201
  exports: "named"
189
202
  });
@@ -242,6 +255,22 @@ function extractTopLevelDeclarations(code, parse) {
242
255
  }
243
256
  return declarations;
244
257
  }
258
+ function extractExternalImportBindings(code, parse) {
259
+ const ast = parse(code);
260
+ const bindings = /* @__PURE__ */ new Set();
261
+ for (const node of ast.body) {
262
+ if (node.type === "ImportDeclaration") {
263
+ const importNode = node;
264
+ const source = importNode.source.value;
265
+ if (typeof source === "string" && !source.startsWith(".") && !source.startsWith("/")) {
266
+ for (const spec of importNode.specifiers) {
267
+ bindings.add(spec.local.name);
268
+ }
269
+ }
270
+ }
271
+ }
272
+ return bindings;
273
+ }
245
274
  function renameIdentifiers(code, renameMap, parse) {
246
275
  if (renameMap.size === 0) return code;
247
276
  const ast = parse(code);
@@ -321,6 +350,117 @@ function stripExports(code, parse) {
321
350
  });
322
351
  return s.toString();
323
352
  }
353
+ function extractExternalImports(code, parse) {
354
+ const ast = parse(code);
355
+ const imports = [];
356
+ walk2(ast, {
357
+ enter(node) {
358
+ if (node.type === "ImportDeclaration") {
359
+ const importNode = node;
360
+ const source = importNode.source.value;
361
+ if (typeof source === "string" && !source.startsWith(".") && !source.startsWith("/")) {
362
+ const specifiers = [];
363
+ for (const spec of importNode.specifiers) {
364
+ if (spec.type === "ImportDefaultSpecifier") {
365
+ specifiers.push({ type: "default", localName: spec.local.name });
366
+ } else if (spec.type === "ImportNamespaceSpecifier") {
367
+ specifiers.push({ type: "namespace", localName: spec.local.name });
368
+ } else if (spec.type === "ImportSpecifier" && spec.imported) {
369
+ specifiers.push({
370
+ type: "named",
371
+ localName: spec.local.name,
372
+ importedName: spec.imported.name
373
+ });
374
+ }
375
+ }
376
+ imports.push({
377
+ source,
378
+ specifiers,
379
+ fullStatement: code.slice(importNode.start, importNode.end),
380
+ start: importNode.start,
381
+ end: importNode.end
382
+ });
383
+ }
384
+ }
385
+ }
386
+ });
387
+ return imports;
388
+ }
389
+ function removeOrRewriteDuplicateExternalImports(sharedCode, primaryImports, parse) {
390
+ const sharedImports = extractExternalImports(sharedCode, parse);
391
+ const s = new MagicString2(sharedCode);
392
+ const renameMap = /* @__PURE__ */ new Map();
393
+ const primaryBySource = /* @__PURE__ */ new Map();
394
+ for (const imp of primaryImports) {
395
+ const existing = primaryBySource.get(imp.source) || [];
396
+ existing.push(imp);
397
+ primaryBySource.set(imp.source, existing);
398
+ }
399
+ for (const sharedImp of sharedImports) {
400
+ const primaryImpsForSource = primaryBySource.get(sharedImp.source);
401
+ if (!primaryImpsForSource) continue;
402
+ const specifiersToKeep = [];
403
+ for (const sharedSpec of sharedImp.specifiers) {
404
+ let foundInPrimary = false;
405
+ for (const primaryImp of primaryImpsForSource) {
406
+ for (const primarySpec of primaryImp.specifiers) {
407
+ if (sharedSpec.type === primarySpec.type) {
408
+ if (sharedSpec.type === "named" && primarySpec.type === "named") {
409
+ if (sharedSpec.importedName === primarySpec.importedName) {
410
+ foundInPrimary = true;
411
+ if (sharedSpec.localName !== primarySpec.localName) {
412
+ renameMap.set(sharedSpec.localName, primarySpec.localName);
413
+ }
414
+ break;
415
+ }
416
+ } else if (sharedSpec.type === "default" && primarySpec.type === "default") {
417
+ foundInPrimary = true;
418
+ if (sharedSpec.localName !== primarySpec.localName) {
419
+ renameMap.set(sharedSpec.localName, primarySpec.localName);
420
+ }
421
+ break;
422
+ } else if (sharedSpec.type === "namespace" && primarySpec.type === "namespace") {
423
+ foundInPrimary = true;
424
+ if (sharedSpec.localName !== primarySpec.localName) {
425
+ renameMap.set(sharedSpec.localName, primarySpec.localName);
426
+ }
427
+ break;
428
+ }
429
+ }
430
+ }
431
+ if (foundInPrimary) break;
432
+ }
433
+ if (!foundInPrimary) {
434
+ specifiersToKeep.push(sharedSpec);
435
+ }
436
+ }
437
+ if (specifiersToKeep.length === 0) {
438
+ s.remove(sharedImp.start, sharedImp.end);
439
+ } else if (specifiersToKeep.length < sharedImp.specifiers.length) {
440
+ const parts = [];
441
+ const namedParts = [];
442
+ for (const spec of specifiersToKeep) {
443
+ if (spec.type === "default") {
444
+ parts.unshift(spec.localName);
445
+ } else if (spec.type === "namespace") {
446
+ parts.push(`* as ${spec.localName}`);
447
+ } else if (spec.type === "named") {
448
+ if (spec.importedName === spec.localName) {
449
+ namedParts.push(spec.localName);
450
+ } else {
451
+ namedParts.push(`${spec.importedName} as ${spec.localName}`);
452
+ }
453
+ }
454
+ }
455
+ if (namedParts.length > 0) {
456
+ parts.push(`{ ${namedParts.join(", ")} }`);
457
+ }
458
+ const newImport = `import ${parts.join(", ")} from '${sharedImp.source}';`;
459
+ s.overwrite(sharedImp.start, sharedImp.end, newImport);
460
+ }
461
+ }
462
+ return { code: s.toString(), renameMap };
463
+ }
324
464
  function isSharedChunkSource(source, sharedChunkFileName) {
325
465
  return source.includes(SHARED_CHUNK_NAME) || source.includes(sharedChunkFileName.replace(/\.js$/, ""));
326
466
  }
@@ -546,56 +686,63 @@ ${entryWithoutImports.trim()}`;
546
686
  }
547
687
  function mergeSharedIntoPrimary(primaryChunk, sharedChunk, sharedProperty, neededExports, parse) {
548
688
  const { exports: sharedExports, hasDefault } = extractExports(sharedChunk.code, parse);
689
+ const sharedExternalImports = extractExternalImports(sharedChunk.code, parse);
690
+ const { code: primaryCodeDeduped, renameMap: externalRenameMap } = removeOrRewriteDuplicateExternalImports(primaryChunk.code, sharedExternalImports, parse);
549
691
  const sharedDeclarations = extractTopLevelDeclarations(sharedChunk.code, parse);
550
- const primaryDeclarations = extractTopLevelDeclarations(primaryChunk.code, parse);
551
- const renameMap = /* @__PURE__ */ new Map();
692
+ const primaryDeclarations = extractTopLevelDeclarations(primaryCodeDeduped, parse);
693
+ const primaryExternalBindings = extractExternalImportBindings(primaryCodeDeduped, parse);
694
+ const collisionRenameMap = /* @__PURE__ */ new Map();
552
695
  for (const name of sharedDeclarations) {
553
- if (primaryDeclarations.has(name)) {
554
- renameMap.set(name, `__shared$${name}`);
696
+ if (primaryDeclarations.has(name) || primaryExternalBindings.has(name)) {
697
+ collisionRenameMap.set(name, `__shared$${name}`);
555
698
  }
556
699
  }
557
700
  let processedSharedCode = sharedChunk.code;
558
- if (renameMap.size > 0) {
559
- processedSharedCode = renameIdentifiers(processedSharedCode, renameMap, parse);
701
+ if (collisionRenameMap.size > 0) {
702
+ processedSharedCode = renameIdentifiers(processedSharedCode, collisionRenameMap, parse);
560
703
  }
561
704
  const strippedSharedCode = stripExports(processedSharedCode, parse);
562
705
  const sharedExportToLocal = /* @__PURE__ */ new Map();
563
706
  for (const exp of sharedExports) {
564
- const renamedLocal = renameMap.get(exp.localName) ?? exp.localName;
707
+ const renamedLocal = collisionRenameMap.get(exp.localName) ?? exp.localName;
565
708
  sharedExportToLocal.set(exp.exportedName, renamedLocal);
566
709
  }
567
710
  if (hasDefault) {
568
711
  sharedExportToLocal.set("default", "__shared_default__");
569
712
  }
570
- const primaryWithoutSharedImports = removeSharedImportsAndRewriteRefs(
571
- primaryChunk.code,
713
+ let primaryWithoutSharedImports = removeSharedImportsAndRewriteRefs(
714
+ primaryCodeDeduped,
572
715
  sharedChunk.fileName,
573
716
  sharedExportToLocal,
574
717
  parse
575
718
  );
719
+ if (externalRenameMap.size > 0) {
720
+ primaryWithoutSharedImports = renameIdentifiers(primaryWithoutSharedImports, externalRenameMap, parse);
721
+ }
576
722
  const sharedExportEntries = [
577
723
  ...sharedExports.filter((exp) => neededExports.has(exp.exportedName)).map((exp) => {
578
- const renamedLocal = renameMap.get(exp.localName) ?? exp.localName;
724
+ const renamedLocal = collisionRenameMap.get(exp.localName) ?? exp.localName;
579
725
  return exp.exportedName === renamedLocal ? renamedLocal : `${exp.exportedName}: ${renamedLocal}`;
580
726
  }),
581
727
  ...hasDefault && neededExports.has("default") ? ["default: __shared_default__"] : []
582
728
  ];
583
- const sharedExportObject = `const ${sharedProperty} = { ${sharedExportEntries.join(", ")} };`;
584
- primaryChunk.code = [
729
+ const parts = [
585
730
  strippedSharedCode.trim(),
586
731
  "",
587
- primaryWithoutSharedImports.trim(),
588
- "",
589
- sharedExportObject,
590
- `export { ${sharedProperty} };`
591
- ].join("\n");
732
+ primaryWithoutSharedImports.trim()
733
+ ];
734
+ if (sharedExportEntries.length > 0) {
735
+ const sharedExportObject = `const ${sharedProperty} = { ${sharedExportEntries.join(", ")} };`;
736
+ parts.push("", sharedExportObject, `export { ${sharedProperty} };`);
737
+ }
738
+ primaryChunk.code = parts.join("\n");
592
739
  }
593
740
 
594
741
  // src/index.ts
595
742
  import { writeFileSync, mkdirSync } from "fs";
596
743
  import { join } from "path";
597
744
  function iifeSplit(options) {
598
- const { primary, primaryGlobal, secondaryProps, sharedProp, unshared, debugDir } = options;
745
+ const { primary, primaryGlobal, secondaryProps, sharedProp, unshared, debugDir, skipRequireGlobals } = options;
599
746
  const sanitizeName = (name) => name.replace(/[/\\]/g, "-");
600
747
  const writeDebugFile = (filename, content) => {
601
748
  if (!debugDir) return;
@@ -607,6 +754,8 @@ function iifeSplit(options) {
607
754
  }
608
755
  };
609
756
  let outputGlobals = {};
757
+ let outputBanner;
758
+ let outputFooter;
610
759
  const manualChunks = (id, { getModuleInfo }) => {
611
760
  const moduleInfo = getModuleInfo(id);
612
761
  if (!moduleInfo) return void 0;
@@ -622,13 +771,18 @@ function iifeSplit(options) {
622
771
  };
623
772
  return {
624
773
  name: "iife-split",
625
- // Hook into outputOptions to capture globals and configure chunking
774
+ // Hook into outputOptions to capture globals/banner/footer and configure chunking
626
775
  outputOptions(outputOptions) {
627
776
  outputGlobals = outputOptions.globals ?? {};
777
+ outputBanner = outputOptions.banner;
778
+ outputFooter = outputOptions.footer;
628
779
  return {
629
780
  ...outputOptions,
630
781
  format: "es",
631
- manualChunks
782
+ manualChunks,
783
+ // Remove banner/footer so Rollup doesn't embed them in ESM
784
+ banner: void 0,
785
+ footer: void 0
632
786
  };
633
787
  },
634
788
  // Main transformation hook - convert ESM chunks to IIFE
@@ -691,7 +845,8 @@ function iifeSplit(options) {
691
845
  sharedGlobalPath: null,
692
846
  // Primary doesn't need to import shared
693
847
  sharedChunkFileName: null,
694
- parse
848
+ parse,
849
+ skipRequireGlobals
695
850
  }).then((code) => {
696
851
  analysis.primaryChunk.code = code;
697
852
  })
@@ -712,13 +867,35 @@ function iifeSplit(options) {
712
867
  globals: outputGlobals,
713
868
  sharedGlobalPath: `${primaryGlobal}.${sharedProp}`,
714
869
  sharedChunkFileName,
715
- parse
870
+ parse,
871
+ skipRequireGlobals
716
872
  }).then((code) => {
717
873
  satellite.code = code;
718
874
  })
719
875
  );
720
876
  }
721
877
  await Promise.all(conversions);
878
+ if (outputBanner || outputFooter) {
879
+ for (const chunk of Object.values(bundle)) {
880
+ if (chunk.type === "chunk") {
881
+ const resolveBannerFooter = async (value) => {
882
+ if (value === void 0) return "";
883
+ if (typeof value === "function") {
884
+ return await value(chunk);
885
+ }
886
+ return value;
887
+ };
888
+ const banner = await resolveBannerFooter(outputBanner);
889
+ const footer = await resolveBannerFooter(outputFooter);
890
+ if (banner) {
891
+ chunk.code = banner + "\n" + chunk.code;
892
+ }
893
+ if (footer) {
894
+ chunk.code = chunk.code + "\n" + footer;
895
+ }
896
+ }
897
+ }
898
+ }
722
899
  }
723
900
  };
724
901
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rollup-plugin-iife-split",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
4
4
  "type": "module",
5
5
  "description": "Rollup plugin for intelligent IIFE code-splitting",
6
6
  "main": "dist/index.js",
@@ -12,15 +12,16 @@
12
12
  "rollup": "^3.0.0 || ^4.0.0"
13
13
  },
14
14
  "dependencies": {
15
- "magic-string": "^0.30.0",
16
- "estree-walker": "^3.0.0"
15
+ "estree-walker": "^3.0.0",
16
+ "magic-string": "^0.30.0"
17
17
  },
18
18
  "devDependencies": {
19
+ "@types/node": "^20.0.0",
20
+ "acorn": "^8.15.0",
19
21
  "rollup": "^4.0.0",
20
- "typescript": "^5.0.0",
21
22
  "tsup": "^8.0.0",
22
- "vitest": "^1.0.0",
23
- "@types/node": "^20.0.0"
23
+ "typescript": "^5.0.0",
24
+ "vitest": "^1.0.0"
24
25
  },
25
26
  "keywords": [
26
27
  "rollup",