Skip to content

Commit 0d821ee

Browse files
jbromaclaude
andcommitted
fix(NativeEntryPlugin): add polyfills as runtime modules for correct execution order
Polyfills are now added as runtime modules instead of entry modules. This ensures they execute before the webpack startup function, which means they run before Module Federation v2 embed_federation_runtime wrapper. Runtime modules execute during webpack runtime initialization, before __webpack_require__.x() (the startup function) is called. This guarantees polyfills run before any MF v2 federation runtime code. Changes: - Move NativeEntryPlugin to its own folder - Add PolyfillsRuntimeModule that inlines polyfill code - Use additionalTreeRuntimeRequirements hook to add polyfills - Add comprehensive tests with MF v2 integration Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 38aef14 commit 0d821ee

10 files changed

Lines changed: 413 additions & 9 deletions

File tree

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

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import path from 'node:path';
22
import type { ResolveAlias, Compiler as RspackCompiler } from '@rspack/core';
33
import type { Compiler as WebpackCompiler } from 'webpack';
4-
import { isRspackCompiler, moveElementBefore } from '../helpers/index.js';
4+
import { isRspackCompiler, moveElementBefore } from '../../helpers/index.js';
5+
import { makePolyfillsRuntimeModule } from './PolyfillsRuntimeModule.js';
56

67
export interface NativeEntryPluginConfig {
78
/**
@@ -55,18 +56,35 @@ export class NativeEntryPlugin {
5556
path.join(reactNativePath, 'Libraries/Core/InitializeCore.js');
5657

5758
const initializeScriptManagerPath = require.resolve(
58-
'../modules/InitializeScriptManager.js'
59+
'../../modules/InitializeScriptManager.js'
5960
);
6061

61-
const includeModulesPath = require.resolve('../modules/IncludeModules.js');
62+
const includeModulesPath = require.resolve(
63+
'../../modules/IncludeModules.js'
64+
);
65+
66+
const polyfillPaths = getReactNativePolyfills();
6267

6368
const nativeEntries = [
64-
...getReactNativePolyfills(),
6569
initializeCorePath,
6670
initializeScriptManagerPath,
6771
includeModulesPath,
6872
];
6973

74+
// Add polyfills as runtime modules so they execute before the startup function.
75+
// This ensures polyfills run before Module Federation's embed_federation_runtime wrapper.
76+
compiler.hooks.compilation.tap('RepackNativeEntryPlugin', (compilation) => {
77+
compilation.hooks.additionalTreeRuntimeRequirements.tap(
78+
'RepackNativeEntryPlugin',
79+
(chunk) => {
80+
compilation.addRuntimeModule(
81+
chunk,
82+
makePolyfillsRuntimeModule(compiler, { polyfillPaths })
83+
);
84+
}
85+
);
86+
});
87+
7088
compiler.hooks.entryOption.tap(
7189
{ name: 'RepackNativeEntryPlugin', before: 'RepackDevelopmentPlugin' },
7290
(_, entry) => {
@@ -76,14 +94,12 @@ export class NativeEntryPlugin {
7694
);
7795
}
7896

97+
// add native entries to each declared entry point
7998
Object.keys(entry).forEach((entryName) => {
80-
// runtime property defines the chunk name, otherwise it defaults to the entry key
8199
const entryChunkName = entry[entryName].runtime || entryName;
82-
83-
// add native entries to all declared entry points
84100
for (const nativeEntry of nativeEntries) {
85101
new compiler.webpack.EntryPlugin(compiler.context, nativeEntry, {
86-
name: entryChunkName, // prepends the entry to the chunk of specified name
102+
name: entryChunkName,
87103
}).apply(compiler);
88104
}
89105
});
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import fs from 'node:fs';
2+
import type {
3+
Compiler,
4+
RuntimeModule as RuntimeModuleType,
5+
} from '@rspack/core';
6+
7+
interface PolyfillsRuntimeModuleConfig {
8+
polyfillPaths: string[];
9+
}
10+
11+
/**
12+
* Creates a runtime module that inlines React Native polyfills.
13+
* Runtime modules are executed before the startup function (__webpack_require__.x),
14+
* which means they run before Module Federation's embed_federation_runtime wrapper.
15+
*/
16+
export const makePolyfillsRuntimeModule = (
17+
compiler: Compiler,
18+
moduleConfig: PolyfillsRuntimeModuleConfig
19+
): RuntimeModuleType => {
20+
const Template = compiler.webpack.Template;
21+
const RuntimeModule = compiler.webpack.RuntimeModule;
22+
23+
const PolyfillsRuntimeModule = class extends RuntimeModule {
24+
constructor(private config: PolyfillsRuntimeModuleConfig) {
25+
// Use STAGE_BASIC to ensure polyfills run early among runtime modules
26+
super('repack/polyfills', RuntimeModule.STAGE_BASIC);
27+
}
28+
29+
generate() {
30+
const polyfillCode = this.config.polyfillPaths.map((polyfillPath) => {
31+
const content = fs.readFileSync(polyfillPath, 'utf-8');
32+
return Template.asString([
33+
`// Polyfill: ${polyfillPath.split('/').pop()}`,
34+
'(function() {',
35+
Template.indent(content),
36+
'})();',
37+
]);
38+
});
39+
40+
return Template.asString(polyfillCode);
41+
}
42+
};
43+
44+
return new PolyfillsRuntimeModule(moduleConfig);
45+
};
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { NativeEntryPlugin } from './NativeEntryPlugin.js';
2+
export type { NativeEntryPluginConfig } from './NativeEntryPlugin.js';

packages/repack/src/plugins/RepackPlugin.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type { Compiler as WebpackCompiler } from 'webpack';
33
import { BabelPlugin } from './BabelPlugin.js';
44
import { DevelopmentPlugin } from './DevelopmentPlugin.js';
55
import { LoggerPlugin, type LoggerPluginConfig } from './LoggerPlugin.js';
6-
import { NativeEntryPlugin } from './NativeEntryPlugin.js';
6+
import { NativeEntryPlugin } from './NativeEntryPlugin/index.js';
77
import { OutputPlugin, type OutputPluginConfig } from './OutputPlugin/index.js';
88
import { RepackTargetPlugin } from './RepackTargetPlugin/index.js';
99
import { SourceMapPlugin } from './SourceMapPlugin.js';
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import path from 'node:path';
2+
import {
3+
type RspackPluginInstance,
4+
type StatsModule,
5+
rspack,
6+
} from '@rspack/core';
7+
import memfs from 'memfs';
8+
import { RspackVirtualModulePlugin } from 'rspack-plugin-virtual-module';
9+
import { ModuleFederationPluginV2 } from '../ModuleFederationPluginV2.js';
10+
import { NativeEntryPlugin } from '../NativeEntryPlugin/index.js';
11+
12+
const FIXTURES_PATH = path.join(__dirname, '__fixtures__');
13+
const REACT_NATIVE_PATH = path.join(FIXTURES_PATH, 'react-native');
14+
15+
interface CompileBundleOptions {
16+
extraAliases?: Record<string, string>;
17+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
18+
externals?: any;
19+
}
20+
21+
async function compileBundle(
22+
entry: Record<string, { import: string[] }>,
23+
virtualModules: Record<string, string>,
24+
extraPlugins: RspackPluginInstance[] = [],
25+
options: CompileBundleOptions = {}
26+
) {
27+
const compiler = rspack({
28+
context: __dirname,
29+
mode: 'development',
30+
devtool: false,
31+
entry,
32+
output: {
33+
path: '/out',
34+
filename: '[name].js',
35+
},
36+
resolve: {
37+
alias: {
38+
'react-native': REACT_NATIVE_PATH,
39+
...options.extraAliases,
40+
},
41+
},
42+
externals: options.externals,
43+
plugins: [
44+
new NativeEntryPlugin({}),
45+
...extraPlugins,
46+
new RspackVirtualModulePlugin({
47+
...virtualModules,
48+
}),
49+
],
50+
});
51+
52+
const volume = new memfs.Volume();
53+
const fileSystem = memfs.createFsFromVolume(volume);
54+
// @ts-expect-error memfs is compatible enough
55+
compiler.outputFileSystem = fileSystem;
56+
57+
return new Promise<{
58+
code: string;
59+
fileSystem: typeof memfs.fs;
60+
volume: typeof memfs.vol;
61+
modules: StatsModule[];
62+
}>((resolve, reject) =>
63+
compiler.run((error, stats) => {
64+
if (error) {
65+
reject(error);
66+
} else {
67+
const statsJson = stats?.toJson({ modules: true });
68+
resolve({
69+
code: fileSystem.readFileSync('/out/main.js', 'utf-8') as string,
70+
fileSystem,
71+
volume,
72+
modules: statsJson?.modules ?? [],
73+
});
74+
}
75+
})
76+
);
77+
}
78+
79+
describe('NativeEntryPlugin', () => {
80+
it('should add polyfills as runtime modules that execute before MF v2 federation runtime', async () => {
81+
const { code } = await compileBundle(
82+
{ main: { import: ['./index.js'] } },
83+
{
84+
'./index.js': 'globalThis.__APP_ENTRY__ = true;',
85+
'./App.js': 'export default globalThis.__FEDERATED_EXPORT__ = true;',
86+
},
87+
[
88+
new ModuleFederationPluginV2({
89+
name: 'testContainer',
90+
exposes: {
91+
'./App': './App.js',
92+
},
93+
shared: {
94+
react: { singleton: true, eager: true },
95+
'react-native': { singleton: true, eager: true },
96+
},
97+
// Disable default runtime plugins to simplify test
98+
defaultRuntimePlugins: [],
99+
}),
100+
],
101+
{
102+
externals: (
103+
{ request, context }: { request?: string; context?: string },
104+
callback: (err: Error | null, result?: string) => void
105+
) => {
106+
// Externalize all @module-federation packages and their internal paths
107+
if (
108+
request?.includes('@module-federation') ||
109+
context?.includes('@module-federation') ||
110+
request?.includes('isomorphic-rslog')
111+
) {
112+
return callback(null, 'globalThis.__MF_EXTERNAL__');
113+
}
114+
callback(null);
115+
},
116+
}
117+
);
118+
119+
expect(code).toMatchSnapshot('mf-v2');
120+
121+
// MF v2 uses embed_federation_runtime which wraps the startup function
122+
expect(code).toContain('embed_federation_runtime');
123+
124+
// Polyfills are now runtime modules (webpack/runtime/repack/polyfills)
125+
// Runtime modules execute BEFORE __webpack_require__.x() (the startup function)
126+
// This means polyfills run before MF v2's embed_federation_runtime wrapper
127+
expect(code).toContain('webpack/runtime/repack/polyfills');
128+
129+
// Verify polyfills are in the runtime section (before startup execution)
130+
const polyfillsRuntimePos = code.indexOf(
131+
'webpack/runtime/repack/polyfills'
132+
);
133+
const startupExecutionPos = code.indexOf('__webpack_require__.x()');
134+
135+
expect(polyfillsRuntimePos).toBeGreaterThan(-1);
136+
expect(startupExecutionPos).toBeGreaterThan(-1);
137+
expect(polyfillsRuntimePos).toBeLessThan(startupExecutionPos);
138+
});
139+
});
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
globalThis.__INITIALIZE_CORE__ = true;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
globalThis.__POLYFILL_1__ = true;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
globalThis.__POLYFILL_2__ = true;
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
const path = require('node:path');
2+
3+
module.exports = function getPolyfills() {
4+
return [
5+
path.join(__dirname, 'polyfill1.js'),
6+
path.join(__dirname, 'polyfill2.js'),
7+
];
8+
};

0 commit comments

Comments
 (0)