diff --git a/.github/workflows/e2e-next-dev.yml b/.github/workflows/e2e-next-dev.yml index 00f4d9e69fe..5bfb31f25e4 100644 --- a/.github/workflows/e2e-next-dev.yml +++ b/.github/workflows/e2e-next-dev.yml @@ -48,6 +48,7 @@ jobs: - name: E2E Test for Next.js Dev if: steps.check-ci.outcome == 'success' run: | + npx kill-port 3000,3001,3002 && pnpm run app:next:dev & sleep 1 && npx wait-on tcp:3001 && diff --git a/apps/manifest-demo/3009-webpack-provider/webpack.config.js b/apps/manifest-demo/3009-webpack-provider/webpack.config.js index 6df53bd0864..0a0c221eb0e 100644 --- a/apps/manifest-demo/3009-webpack-provider/webpack.config.js +++ b/apps/manifest-demo/3009-webpack-provider/webpack.config.js @@ -15,6 +15,12 @@ module.exports = composePlugins( config.watchOptions = { ignored: ['**/node_modules/**', '**/@mf-types/**'], }; + config.devServer = { + ...config.devServer, + devMiddleware: { + writeToDisk: true, + }, + }; // publicPath must be specific url config.output.publicPath = 'auto'; config.plugins.push( diff --git a/apps/manifest-demo/webpack-host/webpack.config.js b/apps/manifest-demo/webpack-host/webpack.config.js index 90ee7a73e78..825d4649285 100644 --- a/apps/manifest-demo/webpack-host/webpack.config.js +++ b/apps/manifest-demo/webpack-host/webpack.config.js @@ -13,6 +13,7 @@ module.exports = composePlugins(withNx(), withReact(), (config, context) => { }; config.plugins.push( new ModuleFederationPlugin({ + runtime: false, name: 'manifest_host', remotes: { remote1: 'webpack_provider@http://localhost:3009/mf-manifest.json', diff --git a/apps/modernjs/modern.config.ts b/apps/modernjs/modern.config.ts index 6d4c62fc090..66aaf2d73a3 100644 --- a/apps/modernjs/modern.config.ts +++ b/apps/modernjs/modern.config.ts @@ -35,6 +35,7 @@ export default defineConfig({ appendPlugins([ new ModuleFederationPlugin({ + runtime: false, name: 'app1', exposes: { './thing': './src/test.ts', diff --git a/apps/modernjs/project.json b/apps/modernjs/project.json index d9433da6bf2..953912b3513 100644 --- a/apps/modernjs/project.json +++ b/apps/modernjs/project.json @@ -8,13 +8,13 @@ "targets": { "build": { "executor": "nx:run-commands", + "dependsOn": [ + { + "target": "build", + "dependencies": true + } + ], "options": { - "dependsOn": [ - { - "target": "build", - "dependencies": true - } - ], "commands": [ { "command": "cd apps/modernjs; pnpm run build", @@ -25,13 +25,13 @@ }, "serve": { "executor": "nx:run-commands", + "dependsOn": [ + { + "target": "build", + "dependencies": true + } + ], "options": { - "dependsOn": [ - { - "target": "build", - "dependencies": true - } - ], "commands": [ { "command": "cd apps/modernjs; pnpm run dev", diff --git a/apps/node-host/src/bootstrap.js b/apps/node-host/src/bootstrap.js index 02b82f673fc..e69de29bb2d 100644 --- a/apps/node-host/src/bootstrap.js +++ b/apps/node-host/src/bootstrap.js @@ -1,93 +0,0 @@ -/** - * This is not a production server yet! - * This is only a minimal backend to get started. - */ - -import express from 'express'; -import * as path from 'path'; -import node_local_remote from 'node_local_remote/test'; -import { registerRemotes, loadRemote } from '@module-federation/runtime'; - -registerRemotes([ - { - name: 'node_dynamic_remote', - entry: 'http://localhost:3026/remoteEntry.js', - }, -]); - -const getMemoryUsage = () => { - const formatSize = (bytes) => `${(bytes / 1024 / 1024).toFixed(2)} MB`; - - const memory = process.memoryUsage(); - return `Time: ${new Date()}\nheap total: ${formatSize( - memory.heapTotal, - )} heapUsed: ${formatSize(memory.heapUsed)} rss: ${formatSize(memory.rss)}`; -}; - -const remoteMsg = import('node_remote/test').then((m) => { - console.log('\x1b[32m%s\x1b[0m', m.default || m); - return m.default || m; -}); -console.log('\x1b[32m%s\x1b[0m', node_local_remote); - -const app = express(); - -app.use('/assets', express.static(path.join(__dirname, 'assets'))); - -app.get('/api', async (req, res) => { - res.send({ - message: 'Welcome to node-host!', - remotes: { - // node_remote: await remoteMsg, - // node_local_remote, - }, - }); -}); - -app.get('/dynamic-remote', async (req, res) => { - const dynamicRemote = await loadRemote('node_dynamic_remote/test-with-axios'); - - res.send({ - message: 'dynamic remote', - dynamicRemote: dynamicRemote(), - }); -}); - -app.get('/upgrade-remote', async (req, res) => { - registerRemotes( - [ - { - name: 'node_dynamic_remote', - entry: 'http://localhost:3027/remoteEntry.js', - }, - ], - { force: true }, - ); - - res.send({ - message: 'Upgrade success!', - }); -}); - -app.get('/memory-cache', async (req, res) => { - const memoryUsage = getMemoryUsage(); - console.log(memoryUsage); - res.send({ - message: 'memory-cache', - memoryUsage: memoryUsage, - }); -}); - -app.get('/federation-info', async (req, res) => { - console.log(global.__FEDERATION__); - console.log(global.__FEDERATION__.__INSTANCES__[0].moduleCache); - res.send({ - message: 'federation info will be output in terminal !', - }); -}); - -const port = process.env.PORT || 3333; -const server = app.listen(port, () => { - console.log(`Listening at http://localhost:${port}/api`); -}); -server.on('error', console.error); diff --git a/apps/node-host/src/main.js b/apps/node-host/src/main.js index b93c7a0268a..39af3d1da44 100644 --- a/apps/node-host/src/main.js +++ b/apps/node-host/src/main.js @@ -1 +1,93 @@ -import('./bootstrap'); +/** + * This is not a production server yet! + * This is only a minimal backend to get started. + */ + +import express from 'express'; +import * as path from 'path'; +import node_local_remote from 'node_local_remote/test'; +import { registerRemotes, loadRemote } from '@module-federation/runtime'; + +registerRemotes([ + { + name: 'node_dynamic_remote', + entry: 'http://localhost:3026/remoteEntry.js', + }, +]); + +const getMemoryUsage = () => { + const formatSize = (bytes) => `${(bytes / 1024 / 1024).toFixed(2)} MB`; + + const memory = process.memoryUsage(); + return `Time: ${new Date()}\nheap total: ${formatSize( + memory.heapTotal, + )} heapUsed: ${formatSize(memory.heapUsed)} rss: ${formatSize(memory.rss)}`; +}; + +const remoteMsg = import('node_remote/test').then((m) => { + console.log('\x1b[32m%s\x1b[0m', m.default || m); + return m.default || m; +}); +console.log('\x1b[32m%s\x1b[0m', node_local_remote); + +const app = express(); + +app.use('/assets', express.static(path.join(__dirname, 'assets'))); + +app.get('/api', async (req, res) => { + res.send({ + message: 'Welcome to node-host!', + remotes: { + node_remote: await remoteMsg, + node_local_remote, + }, + }); +}); + +app.get('/dynamic-remote', async (req, res) => { + const dynamicRemote = await loadRemote('node_dynamic_remote/test-with-axios'); + + res.send({ + message: 'dynamic remote', + dynamicRemote: dynamicRemote(), + }); +}); + +app.get('/upgrade-remote', async (req, res) => { + registerRemotes( + [ + { + name: 'node_dynamic_remote', + entry: 'http://localhost:3027/remoteEntry.js', + }, + ], + { force: true }, + ); + + res.send({ + message: 'Upgrade success!', + }); +}); + +app.get('/memory-cache', async (req, res) => { + const memoryUsage = getMemoryUsage(); + console.log(memoryUsage); + res.send({ + message: 'memory-cache', + memoryUsage: memoryUsage, + }); +}); + +app.get('/federation-info', async (req, res) => { + console.log(global.__FEDERATION__); + console.log(global.__FEDERATION__.__INSTANCES__[0].moduleCache); + res.send({ + message: 'federation info will be output in terminal !', + }); +}); + +const port = process.env.PORT || 3333; +const server = app.listen(port, () => { + console.log(`Listening at http://localhost:${port}/api`); +}); +server.on('error', console.error); diff --git a/apps/node-host/webpack.config.js b/apps/node-host/webpack.config.js index 53f5f4e452e..77626757c55 100644 --- a/apps/node-host/webpack.config.js +++ b/apps/node-host/webpack.config.js @@ -20,12 +20,12 @@ module.exports = composePlugins(withNx(), async (config) => { runtimePlugins: [ require.resolve('@module-federation/node/runtimePlugin'), ], + experiments: { + asyncStartup: true, + }, remotes: { node_local_remote: 'commonjs ../../node-local-remote/dist/remoteEntry.js', - // node_local_remote: '__webpack_require__.federation.instance.moduleCache.get("node_local_remote")', - // node_remote: - // '__webpack_require__.federation.instance.moduleCache.get("node_remote")@http://localhost:3002/remoteEntry.js', node_remote: 'node_remote@http://localhost:3022/remoteEntry.js', }, }), diff --git a/packages/enhanced/src/lib/container/ContainerPlugin.ts b/packages/enhanced/src/lib/container/ContainerPlugin.ts index 25a3db206aa..4fa8ab3f79e 100644 --- a/packages/enhanced/src/lib/container/ContainerPlugin.ts +++ b/packages/enhanced/src/lib/container/ContainerPlugin.ts @@ -193,8 +193,6 @@ class ContainerPlugin { compilation: Compilation, callback: (error?: WebpackError | null | undefined) => void, ) => { - const hasSingleRuntimeChunk = - compilation.options?.optimization?.runtimeChunk; const hooks = FederationModulesPlugin.getCompilationHooks(compilation); const federationRuntimeDependency = federationRuntimePluginInstance.getDependency(compiler); @@ -215,7 +213,7 @@ class ContainerPlugin { { name, filename, - runtime: hasSingleRuntimeChunk ? false : runtime, + runtime, library, }, (error: WebpackError | null | undefined) => { diff --git a/packages/enhanced/src/lib/container/runtime/EmbedFederationRuntimeModule.ts b/packages/enhanced/src/lib/container/runtime/EmbedFederationRuntimeModule.ts index 93a5ae96d4f..ddbf900d619 100644 --- a/packages/enhanced/src/lib/container/runtime/EmbedFederationRuntimeModule.ts +++ b/packages/enhanced/src/lib/container/runtime/EmbedFederationRuntimeModule.ts @@ -63,9 +63,16 @@ class EmbedFederationRuntimeModule extends RuntimeModule { const result = Template.asString([ `var oldStartup = ${RuntimeGlobals.startup};`, + `var hasRun = false;`, `${RuntimeGlobals.startup} = ${compilation.runtimeTemplate.basicFunction( '', - [`${initRuntimeModuleGetter};`, `return oldStartup();`], + [ + `if (!hasRun) {`, + ` hasRun = true;`, + ` ${initRuntimeModuleGetter};`, + `}`, + `return oldStartup();`, + ], )};`, ]); this._cachedGeneratedCode = result; diff --git a/packages/enhanced/src/lib/container/runtime/EmbedFederationRuntimePlugin.ts b/packages/enhanced/src/lib/container/runtime/EmbedFederationRuntimePlugin.ts index ac05409b446..8ed75bb2698 100644 --- a/packages/enhanced/src/lib/container/runtime/EmbedFederationRuntimePlugin.ts +++ b/packages/enhanced/src/lib/container/runtime/EmbedFederationRuntimePlugin.ts @@ -17,14 +17,14 @@ const federationGlobal = getFederationGlobalScope(RuntimeGlobals); interface EmbedFederationRuntimePluginOptions { /** - * Whether to enable runtime module embedding for all chunks - * If false, will only embed for chunks that explicitly require it + * Whether to enable runtime module embedding for all chunks. + * If false, only chunks that explicitly require it will be embedded. */ enableForAllChunks?: boolean; } /** - * Plugin that handles embedding of Module Federation runtime code into chunks. + * Plugin that embeds Module Federation runtime code into chunks. * It ensures proper initialization of federated modules and manages runtime requirements. */ class EmbedFederationRuntimePlugin { @@ -39,16 +39,16 @@ class EmbedFederationRuntimePlugin { } /** - * Determines if runtime embedding should be enabled for a given chunk + * Determines if runtime embedding should be enabled for a given chunk. */ private isEnabledForChunk(chunk: Chunk): boolean { - if (this.options.enableForAllChunks) return true; + // Disable for our special "build time chunk" if (chunk.id === 'build time chunk') return false; - return chunk.hasRuntime(); + return this.options.enableForAllChunks || chunk.hasRuntime(); } /** - * Checks if a compilation hook has already been tapped by this plugin + * Checks if a hook has already been tapped by this plugin. */ private isHookAlreadyTapped( taps: Array<{ name: string }>, @@ -58,82 +58,81 @@ class EmbedFederationRuntimePlugin { } apply(compiler: Compiler): void { - // Prevent double application of plugin + // Prevent double application of the plugin. const compilationTaps = compiler.hooks.thisCompilation.taps || []; if (this.isHookAlreadyTapped(compilationTaps, PLUGIN_NAME)) { return; } + // Tap into the compilation to modify renderStartup and runtime requirements. compiler.hooks.thisCompilation.tap( PLUGIN_NAME, (compilation: Compilation) => { - debugger; + // --- Part 1: Modify renderStartup to append a startup call when none is added automatically --- const { renderStartup } = compiler.webpack.javascript.JavascriptModulesPlugin.getCompilationHooks( compilation, ); - // Prevent double tapping of renderStartup hook - const startupTaps = renderStartup.taps || []; - if (this.isHookAlreadyTapped(startupTaps, PLUGIN_NAME)) { - return; - } - renderStartup.tap( PLUGIN_NAME, - (startupSource, lastInlinedModule, renderContext) => { + (startupSource, _lastInlinedModule, renderContext) => { const { chunk, chunkGraph } = renderContext; if (!this.isEnabledForChunk(chunk)) { return startupSource; } - const treeRuntimeRequirements = + const runtimeRequirements = chunkGraph.getTreeRuntimeRequirements(chunk); - const chunkRuntimeRequirements = - chunkGraph.getChunkRuntimeRequirements(chunk); - - const federation = - chunkRuntimeRequirements.has(federationGlobal) || - treeRuntimeRequirements.has(federationGlobal); - - if (!federation) { - return startupSource; - } - - // Skip if chunk was already processed - if (this.processedChunks.get(chunk)) { + const entryModuleCount = chunkGraph.getNumberOfEntryModules(chunk); + + // The default renderBootstrap automatically pushes a startup call when either: + // - There is at least one entry module, OR + // - runtimeRequirements.has(RuntimeGlobals.startupNoDefault) is true. + if ( + entryModuleCount > 0 || + runtimeRequirements.has(RuntimeGlobals.startupNoDefault) + ) { return startupSource; } - // Mark chunk as processed - this.processedChunks.set(chunk, true); - - // Add basic startup call + // Otherwise, append a startup call. return new ConcatSource( startupSource, + '\n// Custom hook: appended startup call because none was added automatically\n', `${RuntimeGlobals.startup}();\n`, ); }, ); - }, - ); - compiler.hooks.thisCompilation.tap( - PLUGIN_NAME, - (compilation: Compilation) => { - const hooks = FederationModulesPlugin.getCompilationHooks(compilation); + // --- Part 2: Embed Federation Runtime Module and adjust runtime requirements --- + const federationHooks = + FederationModulesPlugin.getCompilationHooks(compilation); const containerEntrySet: Set< ContainerEntryDependency | FederationRuntimeDependency > = new Set(); - hooks.addFederationRuntimeModule.tap( + // Proactively add startupOnlyBefore target chunks. + compilation.hooks.additionalChunkRuntimeRequirements.tap( + PLUGIN_NAME, + (chunk: Chunk, runtimeRequirements: Set) => { + if (!this.isEnabledForChunk(chunk)) { + return; + } + runtimeRequirements.add(RuntimeGlobals.startupOnlyBefore); + }, + ); + + // Collect federation runtime dependencies. + federationHooks.addFederationRuntimeModule.tap( PLUGIN_NAME, (dependency: FederationRuntimeDependency) => { containerEntrySet.add(dependency); }, ); + // Handle additional runtime requirements when federation is enabled. const handleRuntimeRequirements = ( chunk: Chunk, runtimeRequirements: Set, @@ -141,14 +140,14 @@ class EmbedFederationRuntimePlugin { if (!this.isEnabledForChunk(chunk)) { return; } + // Skip if already processed or if not a federation chunk. if (runtimeRequirements.has('embeddedFederationRuntime')) return; if (!runtimeRequirements.has(federationGlobal)) { return; } - runtimeRequirements.add(RuntimeGlobals.startupOnlyBefore); + // Mark as embedded and add the runtime module. runtimeRequirements.add('embeddedFederationRuntime'); - const runtimeModule = new EmbedFederationRuntimeModule( containerEntrySet, ); diff --git a/packages/enhanced/src/lib/startup/MfStartupChunkDependenciesPlugin.ts b/packages/enhanced/src/lib/startup/MfStartupChunkDependenciesPlugin.ts index 31ae0959a16..46b8b563d81 100644 --- a/packages/enhanced/src/lib/startup/MfStartupChunkDependenciesPlugin.ts +++ b/packages/enhanced/src/lib/startup/MfStartupChunkDependenciesPlugin.ts @@ -12,7 +12,6 @@ import ContainerEntryModule from '../container/ContainerEntryModule'; const { RuntimeGlobals } = require( normalizeWebpackPath('webpack'), ) as typeof import('webpack'); - const StartupEntrypointRuntimeModule = require( normalizeWebpackPath('webpack/lib/runtime/StartupEntrypointRuntimeModule'), ) as typeof import('webpack/lib/runtime/StartupEntrypointRuntimeModule'); @@ -43,6 +42,7 @@ class StartupChunkDependenciesPlugin { compiler.hooks.thisCompilation.tap( 'MfStartupChunkDependenciesPlugin', (compilation) => { + // Add additional runtime requirements at the tree level. compilation.hooks.additionalTreeRuntimeRequirements.tap( 'StartupChunkDependenciesPlugin', (chunk, set, { chunkGraph }) => { @@ -55,6 +55,7 @@ class StartupChunkDependenciesPlugin { }, ); + // Add additional runtime requirements at the chunk level if there are entry modules. compilation.hooks.additionalChunkRuntimeRequirements.tap( 'MfStartupChunkDependenciesPlugin', (chunk, set, { chunkGraph }) => { @@ -64,6 +65,7 @@ class StartupChunkDependenciesPlugin { }, ); + // When the startupEntrypoint requirement is present, add extra keys and a runtime module. compilation.hooks.runtimeRequirementInTree .for(RuntimeGlobals.startupEntrypoint) .tap( @@ -80,6 +82,7 @@ class StartupChunkDependenciesPlugin { }, ); + // Replace the generated startup with a custom version if entry modules exist. const { renderStartup } = compiler.webpack.javascript.JavascriptModulesPlugin.getCompilationHooks( compilation, @@ -94,8 +97,9 @@ class StartupChunkDependenciesPlugin { return startupSource; } - if (chunkGraph.getNumberOfEntryModules(chunk) === 0) + if (chunkGraph.getNumberOfEntryModules(chunk) === 0) { return startupSource; + } const treeRuntimeRequirements = chunkGraph.getTreeRuntimeRequirements(chunk); diff --git a/packages/enhanced/src/lib/startup/StartupHelpers.ts b/packages/enhanced/src/lib/startup/StartupHelpers.ts index e43b28dd3a2..223671e4b6b 100644 --- a/packages/enhanced/src/lib/startup/StartupHelpers.ts +++ b/packages/enhanced/src/lib/startup/StartupHelpers.ts @@ -49,6 +49,7 @@ export const generateEntryStartup = ( '', '\n', 'var promises = [];', + '__webpack_require__.x();', ]; const treeRuntimeRequirements = chunkGraph.getTreeRuntimeRequirements(chunk); diff --git a/packages/enhanced/test/ConfigTestCases.basictest.js b/packages/enhanced/test/ConfigTestCases.basictest.js index ce08fc3df27..b57936c2c6e 100644 --- a/packages/enhanced/test/ConfigTestCases.basictest.js +++ b/packages/enhanced/test/ConfigTestCases.basictest.js @@ -14,3 +14,5 @@ const { describeCases } = require('./ConfigTestCases.template'); describeCases({ name: 'ConfigTestCases', }); + +describe('ConfigTestCases', () => {}); diff --git a/packages/enhanced/test/ConfigTestCases.template.js b/packages/enhanced/test/ConfigTestCases.template.js index 0e70c968312..30c6dd68946 100644 --- a/packages/enhanced/test/ConfigTestCases.template.js +++ b/packages/enhanced/test/ConfigTestCases.template.js @@ -95,9 +95,9 @@ const describeCases = (config) => { if (!mfp._options.experiments) { mfp._options.experiments = {}; } - if (config.federation?.federationRuntime) { + if (config.federation?.asyncStartup) { // dont override if explicitly set - if ('federationRuntime' in mfp._options.experiments) { + if ('asyncStartup' in mfp._options.experiments) { } else { Object.assign( mfp._options.experiments, diff --git a/packages/enhanced/test/configCases/container/exposed-overridables/webpack.config.js b/packages/enhanced/test/configCases/container/exposed-overridables/webpack.config.js index 4e71aca7e88..c1ea5c79f39 100644 --- a/packages/enhanced/test/configCases/container/exposed-overridables/webpack.config.js +++ b/packages/enhanced/test/configCases/container/exposed-overridables/webpack.config.js @@ -8,7 +8,7 @@ module.exports = { exposes: { './Button': './Button', }, - experiments: { federationRuntime: false }, + experiments: { asyncStartup: false }, shared: { react: { eager: true, diff --git a/packages/enhanced/test/configCases/container/module-federation/webpack.config.js b/packages/enhanced/test/configCases/container/module-federation/webpack.config.js index 313470f8eda..b1670b4db6c 100644 --- a/packages/enhanced/test/configCases/container/module-federation/webpack.config.js +++ b/packages/enhanced/test/configCases/container/module-federation/webpack.config.js @@ -11,7 +11,7 @@ function createConfig() { filename: 'container.js', library: { type: 'system' }, exposes: ['./other', './self', './dep'], - experiments: { federationRuntime: false }, + experiments: { asyncStartup: false }, remotes: { abc: 'ABC', def: 'DEF', @@ -24,7 +24,7 @@ function createConfig() { filename: 'container2.js', library: { type: 'system' }, exposes: ['./other', './self', './dep'], - experiments: { federationRuntime: false }, + experiments: { asyncStartup: false }, remotes: { abc: 'ABC', def: 'DEF', diff --git a/packages/nextjs-mf/src/plugins/container/InvertedContainerRuntimeModule.ts b/packages/nextjs-mf/src/plugins/container/InvertedContainerRuntimeModule.ts index b87d149bd55..f10204fa43b 100644 --- a/packages/nextjs-mf/src/plugins/container/InvertedContainerRuntimeModule.ts +++ b/packages/nextjs-mf/src/plugins/container/InvertedContainerRuntimeModule.ts @@ -1,6 +1,4 @@ import { normalizeWebpackPath } from '@module-federation/sdk/normalize-webpack-path'; -import type { Module } from 'webpack'; -import { container } from '@module-federation/enhanced'; import type ContainerEntryModule from '@module-federation/enhanced/src/lib/container/ContainerEntryModule'; const { RuntimeModule, Template, RuntimeGlobals } = require( normalizeWebpackPath('webpack'), @@ -27,6 +25,15 @@ class InvertedContainerRuntimeModule extends RuntimeModule { if (chunk.runtime === 'webpack-api-runtime') { return ''; } + + const runtimeChunk = compilation.options.optimization?.runtimeChunk; + if (runtimeChunk === 'single' || typeof runtimeChunk === 'object') { + const logger = compilation.getLogger('InvertedContainerRuntimeModule'); + logger.info( + 'Runtime chunk is set to single. Consider adding runtime: false to your ModuleFederationPlugin configuration to prevent runtime conflicts.', + ); + } + let containerEntryModule; for (const containerDep of this.options.containers) { const mod = compilation.moduleGraph.getModule(containerDep); @@ -41,7 +48,7 @@ class InvertedContainerRuntimeModule extends RuntimeModule { if ( compilation.chunkGraph.isEntryModuleInChunk(containerEntryModule, chunk) ) { - // dont apply to remote entry itself + // Don't apply to the remote entry itself return ''; } const initRuntimeModuleGetter = compilation.runtimeTemplate.moduleRaw({ @@ -55,16 +62,29 @@ class InvertedContainerRuntimeModule extends RuntimeModule { const nameJSON = JSON.stringify(containerEntryModule._name); return Template.asString([ - `var innerRemote;`, - `function attachRemote () {`, - Template.indent([ - `innerRemote = ${initRuntimeModuleGetter};`, - `var gs = ${RuntimeGlobals.global} || globalThis`, - `gs[${nameJSON}] = innerRemote`, - `return innerRemote;`, - ]), - `};`, - `attachRemote();`, + `var prevStartup = ${RuntimeGlobals.startup};`, + `var hasRun = false;`, + `var cachedRemote;`, + `${RuntimeGlobals.startup} = ${compilation.runtimeTemplate.basicFunction( + '', + Template.asString([ + `if (!hasRun) {`, + Template.indent( + Template.asString([ + `hasRun = true;`, + `if (typeof prevStartup === 'function') {`, + Template.indent(Template.asString([`prevStartup();`])), + `}`, + `cachedRemote = ${initRuntimeModuleGetter};`, + `var gs = ${RuntimeGlobals.global} || globalThis;`, + `gs[${nameJSON}] = cachedRemote;`, + ]), + ), + `} else if (typeof prevStartup === 'function') {`, + Template.indent(`prevStartup();`), + `}`, + ]), + )};`, ]); } }