Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import path from 'node:path';
import type { ResolveAlias, Compiler as RspackCompiler } from '@rspack/core';
import type { Compiler as WebpackCompiler } from 'webpack';
import { isRspackCompiler, moveElementBefore } from '../helpers/index.js';
import { isRspackCompiler, moveElementBefore } from '../../helpers/index.js';
import { makePolyfillsRuntimeModule } from './PolyfillsRuntimeModule.js';

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

const initializeScriptManagerPath = require.resolve(
'../modules/InitializeScriptManager.js'
'../../modules/InitializeScriptManager.js'
);

const includeModulesPath = require.resolve('../modules/IncludeModules.js');
const includeModulesPath = require.resolve(
'../../modules/IncludeModules.js'
);

const polyfillPaths = getReactNativePolyfills();

const nativeEntries = [
...getReactNativePolyfills(),
initializeCorePath,
initializeScriptManagerPath,
includeModulesPath,
];

// Add polyfills as runtime modules so they execute before the startup function.
// This ensures polyfills run before Module Federation's embed_federation_runtime wrapper.
compiler.hooks.compilation.tap('RepackNativeEntryPlugin', (compilation) => {
compilation.hooks.additionalTreeRuntimeRequirements.tap(
'RepackNativeEntryPlugin',
(chunk) => {
compilation.addRuntimeModule(
chunk,
makePolyfillsRuntimeModule(compiler, { polyfillPaths })
);
}
);
});

compiler.hooks.entryOption.tap(
{ name: 'RepackNativeEntryPlugin', before: 'RepackDevelopmentPlugin' },
(_, entry) => {
Expand All @@ -76,14 +94,12 @@ export class NativeEntryPlugin {
);
}

// add native entries to each declared entry point
Object.keys(entry).forEach((entryName) => {
// runtime property defines the chunk name, otherwise it defaults to the entry key
const entryChunkName = entry[entryName].runtime || entryName;

// add native entries to all declared entry points
for (const nativeEntry of nativeEntries) {
new compiler.webpack.EntryPlugin(compiler.context, nativeEntry, {
name: entryChunkName, // prepends the entry to the chunk of specified name
name: entryChunkName,
}).apply(compiler);
}
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import fs from 'node:fs';
import type {
Compiler,
RuntimeModule as RuntimeModuleType,
} from '@rspack/core';

interface PolyfillsRuntimeModuleConfig {
polyfillPaths: string[];
}

/**
* Creates a runtime module that inlines React Native polyfills.
* Runtime modules are executed before the startup function (__webpack_require__.x),
* which means they run before Module Federation's embed_federation_runtime wrapper.
*/
export const makePolyfillsRuntimeModule = (
compiler: Compiler,
moduleConfig: PolyfillsRuntimeModuleConfig
): RuntimeModuleType => {
const Template = compiler.webpack.Template;
const RuntimeModule = compiler.webpack.RuntimeModule;

const PolyfillsRuntimeModule = class extends RuntimeModule {
constructor(private config: PolyfillsRuntimeModuleConfig) {
// Use STAGE_BASIC to ensure polyfills run early among runtime modules
super('repack/polyfills', RuntimeModule.STAGE_BASIC);
}

generate() {
const polyfillCode = this.config.polyfillPaths.map((polyfillPath) => {
const content = fs.readFileSync(polyfillPath, 'utf-8');
return Template.asString([
`// Polyfill: ${polyfillPath.split('/').pop()}`,
'(function() {',
Template.indent(content),
'})();',
]);
});

return Template.asString(polyfillCode);
}
};

return new PolyfillsRuntimeModule(moduleConfig);
};
2 changes: 2 additions & 0 deletions packages/repack/src/plugins/NativeEntryPlugin/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { NativeEntryPlugin } from './NativeEntryPlugin.js';
export type { NativeEntryPluginConfig } from './NativeEntryPlugin.js';
2 changes: 1 addition & 1 deletion packages/repack/src/plugins/RepackPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { Compiler as WebpackCompiler } from 'webpack';
import { BabelPlugin } from './BabelPlugin.js';
import { DevelopmentPlugin } from './DevelopmentPlugin.js';
import { LoggerPlugin, type LoggerPluginConfig } from './LoggerPlugin.js';
import { NativeEntryPlugin } from './NativeEntryPlugin.js';
import { NativeEntryPlugin } from './NativeEntryPlugin/index.js';
import { OutputPlugin, type OutputPluginConfig } from './OutputPlugin/index.js';
import { RepackTargetPlugin } from './RepackTargetPlugin/index.js';
import { SourceMapPlugin } from './SourceMapPlugin.js';
Expand Down
139 changes: 139 additions & 0 deletions packages/repack/src/plugins/__tests__/NativeEntryPlugin.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import path from 'node:path';
import {
type RspackPluginInstance,
type StatsModule,
rspack,
} from '@rspack/core';
import memfs from 'memfs';
import { RspackVirtualModulePlugin } from 'rspack-plugin-virtual-module';
import { ModuleFederationPluginV2 } from '../ModuleFederationPluginV2.js';
import { NativeEntryPlugin } from '../NativeEntryPlugin/index.js';

const FIXTURES_PATH = path.join(__dirname, '__fixtures__');
const REACT_NATIVE_PATH = path.join(FIXTURES_PATH, 'react-native');

interface CompileBundleOptions {
extraAliases?: Record<string, string>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
externals?: any;
}

async function compileBundle(
entry: Record<string, { import: string[] }>,
virtualModules: Record<string, string>,
extraPlugins: RspackPluginInstance[] = [],
options: CompileBundleOptions = {}
) {
const compiler = rspack({
context: __dirname,
mode: 'development',
devtool: false,
entry,
output: {
path: '/out',
filename: '[name].js',
},
resolve: {
alias: {
'react-native': REACT_NATIVE_PATH,
...options.extraAliases,
},
},
externals: options.externals,
plugins: [
new NativeEntryPlugin({}),
...extraPlugins,
new RspackVirtualModulePlugin({
...virtualModules,
}),
],
});

const volume = new memfs.Volume();
const fileSystem = memfs.createFsFromVolume(volume);
// @ts-expect-error memfs is compatible enough
compiler.outputFileSystem = fileSystem;

return new Promise<{
code: string;
fileSystem: typeof memfs.fs;
volume: typeof memfs.vol;
modules: StatsModule[];
}>((resolve, reject) =>
compiler.run((error, stats) => {
if (error) {
reject(error);
} else {
const statsJson = stats?.toJson({ modules: true });
resolve({
code: fileSystem.readFileSync('/out/main.js', 'utf-8') as string,
fileSystem,
volume,
modules: statsJson?.modules ?? [],
});
}
})
);
}

describe('NativeEntryPlugin', () => {
it('should add polyfills as runtime modules that execute before MF v2 federation runtime', async () => {
const { code } = await compileBundle(
{ main: { import: ['./index.js'] } },
{
'./index.js': 'globalThis.__APP_ENTRY__ = true;',
'./App.js': 'export default globalThis.__FEDERATED_EXPORT__ = true;',
},
[
new ModuleFederationPluginV2({
name: 'testContainer',
exposes: {
'./App': './App.js',
},
shared: {
react: { singleton: true, eager: true },
'react-native': { singleton: true, eager: true },
},
// Disable default runtime plugins to simplify test
defaultRuntimePlugins: [],
}),
],
{
externals: (
{ request, context }: { request?: string; context?: string },
callback: (err: Error | null, result?: string) => void
) => {
// Externalize all @module-federation packages and their internal paths
if (
request?.includes('@module-federation') ||
context?.includes('@module-federation') ||
request?.includes('isomorphic-rslog')
) {
return callback(null, 'globalThis.__MF_EXTERNAL__');
}
callback(null);
},
}
);

expect(code).toMatchSnapshot('mf-v2');

// MF v2 uses embed_federation_runtime which wraps the startup function
expect(code).toContain('embed_federation_runtime');

// Polyfills are now runtime modules (webpack/runtime/repack/polyfills)
// Runtime modules execute BEFORE __webpack_require__.x() (the startup function)
// This means polyfills run before MF v2's embed_federation_runtime wrapper
expect(code).toContain('webpack/runtime/repack/polyfills');

// Verify polyfills are in the runtime section (before startup execution)
const polyfillsRuntimePos = code.indexOf(
'webpack/runtime/repack/polyfills'
);
const startupExecutionPos = code.indexOf('__webpack_require__.x()');

expect(polyfillsRuntimePos).toBeGreaterThan(-1);
expect(startupExecutionPos).toBeGreaterThan(-1);
expect(polyfillsRuntimePos).toBeLessThan(startupExecutionPos);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
globalThis.__INITIALIZE_CORE__ = true;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
globalThis.__POLYFILL_1__ = true;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
globalThis.__POLYFILL_2__ = true;
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
const path = require('node:path');

module.exports = function getPolyfills() {
return [
path.join(__dirname, 'polyfill1.js'),
path.join(__dirname, 'polyfill2.js'),
];
};
Loading
Loading