@@ -23,17 +23,41 @@ ObjectMiddleware.register = (...args: unknown[]) => {
2323const __dirname = path . dirname ( fileURLToPath ( import . meta. url ) ) ;
2424const REACT_NATIVE_PATH = path . join ( __dirname , '__fixtures__' , 'react-native' ) ;
2525const 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 */
3153function 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 - z A - Z 0 - 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 / \. f e d e r a t i o n \/ e n t r y \. [ a - f 0 - 9 ] + \. j s / g,
3963 '.federation/entry.HASH.js'
@@ -48,18 +72,21 @@ function normalizeBundle(code: string): string {
4872async 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 * \( f u n c t i o n \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 ( / _ _ w e b p a c k _ r e q u i r e _ _ \( ( [ ^ ) ] + ) \) ; / 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+
112212describe ( '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' , ( ) => {
0 commit comments