Skip to content

Commit 1705f2a

Browse files
authored
fix: preserve polyfill module ids in production (#1357)
1 parent 60e6a17 commit 1705f2a

File tree

5 files changed

+332
-26
lines changed

5 files changed

+332
-26
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@callstack/repack": patch
3+
---
4+
5+
Fix production bundles so `NativeEntryPlugin` keeps polyfills on module-id based `__webpack_require__(id)` startup.

packages/repack/src/plugins/NativeEntryPlugin/NativeEntryPlugin.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,11 @@ export class NativeEntryPlugin {
7878
compiler.hooks.compilation.tap('RepackNativeEntryPlugin', (compilation) => {
7979
compilation.hooks.additionalTreeRuntimeRequirements.tap(
8080
'RepackNativeEntryPlugin',
81-
(chunk) => {
81+
(chunk, runtimeRequirements) => {
82+
runtimeRequirements.add(
83+
compiler.webpack.RuntimeGlobals.moduleFactories
84+
);
85+
runtimeRequirements.add(compiler.webpack.RuntimeGlobals.require);
8286
compilation.addRuntimeModule(
8387
chunk,
8488
makePolyfillsRuntimeModule(compiler, { polyfillPaths })

tests/integration/src/plugins/NativeEntryPlugin.test.ts

Lines changed: 173 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,17 +23,41 @@ ObjectMiddleware.register = (...args: unknown[]) => {
2323
const __dirname = path.dirname(fileURLToPath(import.meta.url));
2424
const REACT_NATIVE_PATH = path.join(__dirname, '__fixtures__', 'react-native');
2525
const REPO_ROOT = path.resolve(__dirname, '..', '..', '..', '..');
26+
const APP_ENTRY_VIRTUAL_MODULES = {
27+
'./index.js': 'globalThis.__APP_ENTRY__ = true;',
28+
};
29+
30+
type CompileBundleOverrides = Pick<
31+
Configuration,
32+
'mode' | 'optimization' | 'output'
33+
>;
34+
35+
const PRODUCTION_OPTIMIZATION: NonNullable<Configuration['optimization']> = {
36+
moduleIds: 'deterministic',
37+
concatenateModules: true,
38+
mangleExports: true,
39+
innerGraph: true,
40+
usedExports: true,
41+
sideEffects: true,
42+
// Keep runtime/startup sections readable in snapshots and assertion failures.
43+
minimize: false,
44+
// NativeEntryPlugin test fixture intentionally leaves unresolved modules.
45+
// We still need emitted output to validate runtime/startup code shape.
46+
emitOnErrors: true,
47+
};
2648

2749
/**
2850
* Normalizes bundle code for deterministic snapshots by replacing
2951
* machine-specific absolute paths and non-deterministic hashes.
3052
*/
3153
function normalizeBundle(code: string): string {
3254
// Webpack mangles absolute paths into variable names with underscores
33-
const mangledRoot = REPO_ROOT.replaceAll('/', '_').replaceAll('-', '_');
55+
const mangledRoot = REPO_ROOT.replaceAll(/[^a-zA-Z0-9]/g, '_');
56+
const compactMangledRoot = mangledRoot.replaceAll(/_+/g, '_');
3457
return code
3558
.replaceAll(REPO_ROOT, '<rootDir>')
3659
.replaceAll(mangledRoot, '_rootDir_')
60+
.replaceAll(compactMangledRoot, '_rootDir_')
3761
.replace(
3862
/\.federation\/entry\.[a-f0-9]+\.js/g,
3963
'.federation/entry.HASH.js'
@@ -48,18 +72,21 @@ function normalizeBundle(code: string): string {
4872
async function compileBundle(
4973
virtualModules: Record<string, string>,
5074
extraPlugins: Array<{ apply(compiler: any): void }> = [],
51-
externals?: Configuration['externals']
75+
externals?: Configuration['externals'],
76+
overrides: CompileBundleOverrides = {}
5277
) {
5378
const virtualPlugin = await createVirtualModulePlugin(virtualModules);
5479

5580
const compiler = await createCompiler({
5681
context: __dirname,
57-
mode: 'development',
82+
mode: overrides.mode ?? 'development',
5883
devtool: false,
5984
entry: './index.js',
6085
output: {
6186
path: '/out',
87+
...(overrides.output ?? {}),
6288
},
89+
optimization: overrides.optimization,
6390
resolve: {
6491
alias: {
6592
'react-native': REACT_NATIVE_PATH,
@@ -109,12 +136,83 @@ function expectBundleOrder(code: string, markers: string[]) {
109136
}
110137
}
111138

139+
function normalizeModuleId(moduleId: string): string {
140+
return moduleId
141+
.trim()
142+
.replace(/,$/, '')
143+
.replace(/^["']|["']$/g, '');
144+
}
145+
146+
function extractModuleIdByMarker(code: string, marker: string): string {
147+
const webpackModuleRegex =
148+
/\/\*\*\*\/\s*([^\n]+)\n(?:\/\*![\s\S]*?\*\/\n)?\([^)]*\)\s*\{([\s\S]*?)\n\/\*\*\*\/\s*\},/g;
149+
for (const match of code.matchAll(webpackModuleRegex)) {
150+
const moduleId = normalizeModuleId(match[1]);
151+
const moduleBody = match[2];
152+
if (moduleBody.includes(marker)) return moduleId;
153+
}
154+
155+
const rspackModuleRegex =
156+
/\n([^\s:\n]+):\s*\(function\s*\([^)]*\)\s*\{([\s\S]*?)\n\}\),/g;
157+
for (const match of code.matchAll(rspackModuleRegex)) {
158+
const moduleId = normalizeModuleId(match[1]);
159+
const moduleBody = match[2];
160+
if (moduleBody.includes(marker)) return moduleId;
161+
}
162+
163+
throw new Error(`Could not find module id for marker "${marker}" in bundle`);
164+
}
165+
166+
function extractRuntimePolyfillRequireIds(code: string): string[] {
167+
const runtimeStart = code.indexOf('runtime/repack/polyfills');
168+
expect(runtimeStart).toBeGreaterThan(-1);
169+
const startupStart = code.indexOf('// startup', runtimeStart);
170+
expect(startupStart).toBeGreaterThan(runtimeStart);
171+
172+
const runtimeSection = code.slice(runtimeStart, startupStart);
173+
return [...runtimeSection.matchAll(/__webpack_require__\(([^)]+)\);/g)].map(
174+
(match) => normalizeModuleId(match[1])
175+
);
176+
}
177+
178+
function getStartupSection(code: string): string {
179+
const startupStart = code.indexOf('// startup');
180+
expect(startupStart).toBeGreaterThan(-1);
181+
return code.slice(startupStart, startupStart + 600);
182+
}
183+
184+
function getRuntimeAndStartupSnippet(code: string): string {
185+
const runtimeStart = code.indexOf('runtime/repack/polyfills');
186+
expect(runtimeStart).toBeGreaterThan(-1);
187+
return code.slice(runtimeStart, runtimeStart + 900);
188+
}
189+
190+
class RemovePolyfillRuntimeRequirementsPlugin {
191+
apply(compiler: any) {
192+
compiler.hooks.compilation.tap(
193+
'RemovePolyfillRuntimeRequirementsPlugin',
194+
(compilation: any) => {
195+
compilation.hooks.additionalTreeRuntimeRequirements.tap(
196+
{
197+
name: 'RemovePolyfillRuntimeRequirementsPlugin',
198+
stage: 10_000,
199+
},
200+
(_chunk: unknown, runtimeRequirements: Set<string>) => {
201+
runtimeRequirements.delete(compiler.webpack.RuntimeGlobals.require);
202+
runtimeRequirements.delete(
203+
compiler.webpack.RuntimeGlobals.moduleFactories
204+
);
205+
}
206+
);
207+
}
208+
);
209+
}
210+
}
211+
112212
describe('NativeEntryPlugin', () => {
113213
describe('without Module Federation', () => {
114214
it('should execute polyfills runtime module before entry startup', async () => {
115-
const { code } = await compileBundle({
116-
'./index.js': 'globalThis.__APP_ENTRY__ = true;',
117-
});
215+
const { code } = await compileBundle(APP_ENTRY_VIRTUAL_MODULES);
118216

119217
// Polyfill modules were processed through the loader pipeline
120218
expect(code).toContain('__POLYFILL_1__');
@@ -132,6 +230,75 @@ describe('NativeEntryPlugin', () => {
132230

133231
expect(normalizeBundle(code)).toMatchSnapshot();
134232
});
233+
234+
describe('in production mode', () => {
235+
it('should expose inlined polyfills if runtime requirements are removed', async () => {
236+
const { code } = await compileBundle(
237+
APP_ENTRY_VIRTUAL_MODULES,
238+
[new RemovePolyfillRuntimeRequirementsPlugin()],
239+
undefined,
240+
{
241+
mode: 'production',
242+
output: { iife: true },
243+
optimization: PRODUCTION_OPTIMIZATION,
244+
}
245+
);
246+
247+
const runtimePolyfillIds = extractRuntimePolyfillRequireIds(code);
248+
expect(runtimePolyfillIds).toHaveLength(2);
249+
250+
const startupSection = getStartupSection(code);
251+
expect(startupSection).toContain('__webpack_modules__[');
252+
for (const moduleId of runtimePolyfillIds) {
253+
expect(startupSection).toContain(
254+
`__webpack_modules__[${moduleId}]();`
255+
);
256+
}
257+
258+
expect(
259+
normalizeBundle(getRuntimeAndStartupSnippet(code))
260+
).toMatchSnapshot();
261+
});
262+
263+
it('should keep runtime polyfill requires aligned with production module ids', async () => {
264+
const { code } = await compileBundle(
265+
APP_ENTRY_VIRTUAL_MODULES,
266+
[],
267+
undefined,
268+
{
269+
mode: 'production',
270+
output: { iife: true },
271+
optimization: PRODUCTION_OPTIMIZATION,
272+
}
273+
);
274+
275+
const runtimePolyfillIds = extractRuntimePolyfillRequireIds(code);
276+
expect(runtimePolyfillIds).toHaveLength(2);
277+
278+
const polyfillModuleIds = [
279+
extractModuleIdByMarker(code, '__POLYFILL_1__'),
280+
extractModuleIdByMarker(code, '__POLYFILL_2__'),
281+
];
282+
283+
expect(runtimePolyfillIds).toEqual(polyfillModuleIds);
284+
285+
const startupSection = getStartupSection(code);
286+
expect(startupSection).not.toContain('__webpack_modules__[');
287+
for (const moduleId of runtimePolyfillIds) {
288+
expect(startupSection).toContain(`__webpack_require__(${moduleId});`);
289+
}
290+
291+
// RuntimeGlobals.moduleFactories should keep module factories available.
292+
expect(code).toContain('__webpack_require__.m = __webpack_modules__');
293+
expect(code).toContain(
294+
'module factories are used so entry inlining is disabled'
295+
);
296+
297+
expect(
298+
normalizeBundle(getRuntimeAndStartupSnippet(code))
299+
).toMatchSnapshot();
300+
});
301+
});
135302
});
136303

137304
describe('with Module Federation v1', () => {

tests/integration/src/plugins/__snapshots__/rspack/NativeEntryPlugin.test.ts.snap

Lines changed: 59 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -730,7 +730,7 @@ __webpack_require__.ruid = "bundler=rspack@1.6.0";
730730
731731
})();
732732
/************************************************************************/
733-
// module cache are used so entry inlining is disabled
733+
// module factories are used so entry inlining is disabled
734734
// startup
735735
// Load entry module and return exports
736736
__webpack_require__("./__fixtures__/react-native/polyfill1.js");
@@ -936,7 +936,7 @@ __webpack_require__.ruid = "bundler=rspack@1.6.0";
936936
937937
})();
938938
/************************************************************************/
939-
// module cache are used so entry inlining is disabled
939+
// module factories are used so entry inlining is disabled
940940
// run startup
941941
var __webpack_exports__ = __webpack_require__.x();
942942
})()
@@ -1136,7 +1136,7 @@ __webpack_require__.ruid = "bundler=rspack@1.6.0";
11361136
11371137
})();
11381138
/************************************************************************/
1139-
// module cache are used so entry inlining is disabled
1139+
// module factories are used so entry inlining is disabled
11401140
// run startup
11411141
var __webpack_exports__ = __webpack_require__.x();
11421142
})()
@@ -1336,13 +1336,64 @@ __webpack_require__.ruid = "bundler=rspack@1.6.0";
13361336
13371337
})();
13381338
/************************************************************************/
1339-
// module cache are used so entry inlining is disabled
1339+
// module factories are used so entry inlining is disabled
13401340
// run startup
13411341
var __webpack_exports__ = __webpack_require__.x();
13421342
})()
13431343
;"
13441344
`;
13451345

1346+
exports[`NativeEntryPlugin > without Module Federation > in production mode > should expose inlined polyfills if runtime requirements are removed 1`] = `
1347+
"runtime/repack/polyfills
1348+
(() => {
1349+
__webpack_require__(101);
1350+
__webpack_require__(148);
1351+
})();
1352+
// webpack/runtime/rspack_unique_id
1353+
(() => {
1354+
__webpack_require__.ruid = "bundler=rspack@1.6.0";
1355+
1356+
})();
1357+
/************************************************************************/
1358+
// startup
1359+
// Load entry module and return exports
1360+
__webpack_modules__[101]();
1361+
__webpack_modules__[148]();
1362+
__webpack_modules__[570]();
1363+
// This entry module doesn't tell about it's top-level declarations so it can't be inlined
1364+
__webpack_modules__[17]();
1365+
__webpack_modules__[296]();
1366+
var __webpack_exports__ = {}
1367+
__webpack_modules__[340]();
1368+
})()
1369+
;"
1370+
`;
1371+
1372+
exports[`NativeEntryPlugin > without Module Federation > in production mode > should keep runtime polyfill requires aligned with production module ids 1`] = `
1373+
"runtime/repack/polyfills
1374+
(() => {
1375+
__webpack_require__(101);
1376+
__webpack_require__(148);
1377+
})();
1378+
// webpack/runtime/rspack_unique_id
1379+
(() => {
1380+
__webpack_require__.ruid = "bundler=rspack@1.6.0";
1381+
1382+
})();
1383+
/************************************************************************/
1384+
// module factories are used so entry inlining is disabled
1385+
// startup
1386+
// Load entry module and return exports
1387+
__webpack_require__(101);
1388+
__webpack_require__(148);
1389+
__webpack_require__(570);
1390+
__webpack_require__(17);
1391+
__webpack_require__(296);
1392+
var __webpack_exports__ = __webpack_require__(340);
1393+
})()
1394+
;"
1395+
`;
1396+
13461397
exports[`NativeEntryPlugin > without Module Federation > should execute polyfills runtime module before entry startup 1`] = `
13471398
"(() => { // webpackBootstrap
13481399
var __webpack_modules__ = ({
@@ -1434,6 +1485,9 @@ return module.exports;
14341485
14351486
}
14361487
1488+
// expose the modules object (__webpack_modules__)
1489+
__webpack_require__.m = __webpack_modules__;
1490+
14371491
/************************************************************************/
14381492
// webpack/runtime/make_namespace_object
14391493
(() => {
@@ -1460,12 +1514,12 @@ __webpack_require__.ruid = "bundler=rspack@1.6.0";
14601514
14611515
})();
14621516
/************************************************************************/
1517+
// module factories are used so entry inlining is disabled
14631518
// startup
14641519
// Load entry module and return exports
14651520
__webpack_require__("./__fixtures__/react-native/polyfill1.js");
14661521
__webpack_require__("./__fixtures__/react-native/polyfill2.js");
14671522
__webpack_require__("./__fixtures__/react-native/Libraries/Core/InitializeCore.js");
1468-
// This entry module doesn't tell about it's top-level declarations so it can't be inlined
14691523
__webpack_require__("../../../../packages/repack/dist/modules/InitializeScriptManager.js");
14701524
__webpack_require__("../../../../packages/repack/dist/modules/IncludeModules.js");
14711525
var __webpack_exports__ = __webpack_require__("./index.js");

0 commit comments

Comments
 (0)