diff --git a/.changeset/early-kids-suffer.md b/.changeset/early-kids-suffer.md new file mode 100644 index 00000000000..aa58d6148d6 --- /dev/null +++ b/.changeset/early-kids-suffer.md @@ -0,0 +1,5 @@ +--- +'@module-federation/enhanced': patch +--- + +Support multiple runtime chunks, single runtime chunks in reference hoisting diff --git a/.changeset/fifty-swans-call.md b/.changeset/fifty-swans-call.md new file mode 100644 index 00000000000..df7a9f08ca7 --- /dev/null +++ b/.changeset/fifty-swans-call.md @@ -0,0 +1,5 @@ +--- +'@module-federation/node': minor +--- + +entry tracking and improved hot reloading for node diff --git a/.changeset/happy-crabs-know.md b/.changeset/happy-crabs-know.md new file mode 100644 index 00000000000..d1be74abbcf --- /dev/null +++ b/.changeset/happy-crabs-know.md @@ -0,0 +1,7 @@ +--- +'@module-federation/enhanced': patch +'@module-federation/runtime': patch +'@module-federation/sdk': patch +--- + +chore: adjust add federation init process diff --git a/.changeset/moody-brooms-burn.md b/.changeset/moody-brooms-burn.md new file mode 100644 index 00000000000..0330a81e0f0 --- /dev/null +++ b/.changeset/moody-brooms-burn.md @@ -0,0 +1,5 @@ +--- +'@module-federation/sdk': patch +--- + +Remove log of container exports in sdk diff --git a/.changeset/smooth-turkeys-raise.md b/.changeset/smooth-turkeys-raise.md new file mode 100644 index 00000000000..3457fcf76f7 --- /dev/null +++ b/.changeset/smooth-turkeys-raise.md @@ -0,0 +1,6 @@ +--- +'@module-federation/runtime': patch +'@module-federation/sdk': patch +--- + +chore: redefine prefetch types diff --git a/.changeset/stupid-mangos-switch.md b/.changeset/stupid-mangos-switch.md new file mode 100644 index 00000000000..1b7849c1e49 --- /dev/null +++ b/.changeset/stupid-mangos-switch.md @@ -0,0 +1,7 @@ +--- +'@module-federation/nextjs-mf': minor +'@module-federation/enhanced': minor +'@module-federation/sdk': patch +--- + +use chunk integration to initalize federation runtime and plugins in runtime bootstrap diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 37e222d59eb..4812a6cce76 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -47,11 +47,14 @@ jobs: - name: Run Affected Test run: npx nx affected -t test --parallel=3 --exclude='*,!tag:package' - - name: E2E Test for 3000-home - run: pnpm run app:next:dev & echo "done" && sleep 20 && npx nx run-many --target=test:e2e --projects=3000-home && lsof -ti tcp:3000,3001,3002 | xargs kill + - name: E2E Test for Next.js + run: pnpm run app:next:dev & echo "done" && sleep 50 && npx nx run-many --target=test:e2e --projects=3000-home,3001-shop,3002-checkout --parallel=2 && lsof -ti tcp:3000,3001,3002 | xargs kill - - name: E2E Test for 3001-shop - run: pnpm run app:next:dev & echo "done" && sleep 20 && npx nx run-many --target=test:e2e --projects=3001-shop && lsof -ti tcp:3000,3001,3002 | xargs kill + # - name: E2E Test for 3001-shop + # run: pnpm run app:next:dev & echo "done" && sleep 20 && npx nx run-many --target=test:e2e --projects=3001-shop && lsof -ti tcp:3000,3001,3002 | xargs kill + + - name: E2E Test for ModernJS + run: npx nx run-many --target=test:e2e --projects=modernjs --parallel=1 && lsof -ti tcp:8080 | xargs kill # - name: E2E Test for 3002-checkout # run: pnpm run app:next:dev & echo "done" && sleep 15 && npx nx run-many --target=test:e2e --projects=3002-checkout && lsof -ti tcp:3000,3001,3002 | xargs kill diff --git a/apps/3000-home/cypress.config.ts b/apps/3000-home/cypress.config.ts index a23e4a4582b..988862e43cc 100644 --- a/apps/3000-home/cypress.config.ts +++ b/apps/3000-home/cypress.config.ts @@ -5,4 +5,8 @@ export default defineConfig({ projectId: 'sa6wfn', e2e: nxE2EPreset(__filename, { cypressDir: 'cypress' }), defaultCommandTimeout: 20000, + retries: { + runMode: 2, + openMode: 1, + }, }); diff --git a/apps/3000-home/cypress/e2e/app.cy.ts b/apps/3000-home/cypress/e2e/app.cy.ts index 99c1d55a178..769b9afa9e4 100644 --- a/apps/3000-home/cypress/e2e/app.cy.ts +++ b/apps/3000-home/cypress/e2e/app.cy.ts @@ -31,7 +31,7 @@ describe('3000-home/', () => { }); describe('Routing checks', () => { - xit('check that clicking back and forwards in client side routeing still renders the content correctly', () => { + it('check that clicking back and forwards in client side routeing still renders the content correctly', () => { cy.visit('/'); cy.visit('/shop'); cy.visit('/'); @@ -51,7 +51,11 @@ describe('3000-home/', () => { }); describe('3000-home/checkout', () => { - beforeEach(() => cy.visit('/checkout')); + beforeEach(() => { + cy.visit('/checkout'); + cy.visit('/'); + cy.visit('/checkout'); + }); describe('Welcome message', () => { it('should display welcome message', () => { @@ -102,7 +106,8 @@ describe('3000-home/', () => { cy.request(src).its('status').should('eq', 200); }); }); - xit('should check that shop-webpack-png images are not 404 between route clicks', () => { + it('should check that shop-webpack-png images are not 404 between route clicks', () => { + cy.visit('/'); cy.visit('/shop'); cy.url().should('include', '/shop'); getH1().contains('Shop Page'); diff --git a/apps/3000-home/package.json b/apps/3000-home/package.json index 59e55bb0896..bc1e1a12ec2 100644 --- a/apps/3000-home/package.json +++ b/apps/3000-home/package.json @@ -25,6 +25,7 @@ "tapable": "2.2.1", "terser-webpack-plugin": "5.3.10", "typescript": "5.3.3", + "upath": "2.0.1", "url": "0.11.3", "util": "0.12.5", "webpack-sources": "3.2.3" diff --git a/apps/3000-home/pages/_app.tsx b/apps/3000-home/pages/_app.tsx index 7bfd5c70b1a..bada00c2a2b 100644 --- a/apps/3000-home/pages/_app.tsx +++ b/apps/3000-home/pages/_app.tsx @@ -24,7 +24,9 @@ function MyApp(props) { } }; // handle first route hit. - React.useMemo(() => handleRouteChange(asPath), [asPath]); + React.useEffect(() => { + handleRouteChange(asPath); + }, [asPath]); //handle route change React.useEffect(() => { diff --git a/apps/3000-home/pages/_document.js b/apps/3000-home/pages/_document.js index acf81025bc3..f0e5fd7b8b5 100644 --- a/apps/3000-home/pages/_document.js +++ b/apps/3000-home/pages/_document.js @@ -8,15 +8,19 @@ import { class MyDocument extends Document { static async getInitialProps(ctx) { + if (ctx.pathname) { + if (!ctx.pathname.endsWith('_error')) { + await revalidate().then((shouldUpdate) => { + if (shouldUpdate) { + console.log('should HMR', shouldUpdate); + } + }); + } + } + const initialProps = await Document.getInitialProps(ctx); + const chunks = await flushChunks(); - ctx?.res?.on('finish', () => { - revalidate().then((shouldUpdate) => { - if (shouldUpdate) { - console.log('should HMR', shouldUpdate); - } - }); - }); return { ...initialProps, diff --git a/apps/3001-shop/cypress.config.ts b/apps/3001-shop/cypress.config.ts index 1b53a12014c..22f84885c36 100644 --- a/apps/3001-shop/cypress.config.ts +++ b/apps/3001-shop/cypress.config.ts @@ -4,4 +4,8 @@ import { defineConfig } from 'cypress'; export default defineConfig({ e2e: nxE2EPreset(__filename, { cypressDir: 'cypress' }), defaultCommandTimeout: 10000, + retries: { + runMode: 2, + openMode: 1, + }, }); diff --git a/apps/3001-shop/cypress/e2e/app.cy.ts b/apps/3001-shop/cypress/e2e/app.cy.ts index 8d2fd89d1db..0df777c9f97 100644 --- a/apps/3001-shop/cypress/e2e/app.cy.ts +++ b/apps/3001-shop/cypress/e2e/app.cy.ts @@ -3,9 +3,6 @@ import { getH1, getH3 } from '../support/app.po'; describe('3001-shop/', () => { beforeEach(() => { cy.visit('/'); - cy.visit('/shop'); - cy.visit('/checkout'); - cy.visit('/'); }); describe('Welcome message', () => { diff --git a/apps/3001-shop/package.json b/apps/3001-shop/package.json index f6911bca648..fbc6a8c5dd0 100644 --- a/apps/3001-shop/package.json +++ b/apps/3001-shop/package.json @@ -24,6 +24,7 @@ "tapable": "2.2.1", "terser-webpack-plugin": "5.3.10", "typescript": "5.3.3", + "upath": "2.0.1", "url": "0.11.3", "util": "0.12.5", "webpack-sources": "3.2.3" diff --git a/apps/3001-shop/pages/_app.tsx b/apps/3001-shop/pages/_app.tsx index e2a88e7b60e..3a7fe21757b 100644 --- a/apps/3001-shop/pages/_app.tsx +++ b/apps/3001-shop/pages/_app.tsx @@ -26,7 +26,9 @@ function MyApp({ Component, pageProps }) { } }; // handle first route hit. - React.useMemo(() => handleRouteChange(asPath), [asPath]); + React.useEffect(() => { + handleRouteChange(asPath); + }, [asPath]); //handle route change React.useEffect(() => { diff --git a/apps/3001-shop/pages/_document.js b/apps/3001-shop/pages/_document.js index e87bfdfd6b0..2edbb9485d1 100644 --- a/apps/3001-shop/pages/_document.js +++ b/apps/3001-shop/pages/_document.js @@ -8,21 +8,18 @@ import { class MyDocument extends Document { static async getInitialProps(ctx) { - await revalidate().then((shouldUpdate) => { - if (shouldUpdate) { - ctx.res.writeHead(307, { Location: ctx.req.url }); - ctx.res.end(); + if (ctx.pathname) { + if (!ctx.pathname.endsWith('_error')) { + await revalidate().then((shouldUpdate) => { + if (shouldUpdate) { + console.log('should HMR', shouldUpdate); + } + }); } - }); + } + const initialProps = await Document.getInitialProps(ctx); const chunks = await flushChunks(); - ctx?.res?.on('finish', () => { - // revalidate().then((shouldUpdate) => { - // if (shouldUpdate) { - // console.log('should HMR', shouldUpdate); - // } - // }); - }); return { ...initialProps, diff --git a/apps/3002-checkout/cypress.config.ts b/apps/3002-checkout/cypress.config.ts index 82aa9031ccc..43cb7233f82 100644 --- a/apps/3002-checkout/cypress.config.ts +++ b/apps/3002-checkout/cypress.config.ts @@ -4,4 +4,8 @@ import { defineConfig } from 'cypress'; export default defineConfig({ e2e: nxE2EPreset(__filename, { cypressDir: 'cypress' }), defaultCommandTimeout: 15000, + retries: { + runMode: 2, + openMode: 1, + }, }); diff --git a/apps/3002-checkout/cypress/e2e/app.cy.ts b/apps/3002-checkout/cypress/e2e/app.cy.ts index a65b8937914..99be63f4ad9 100644 --- a/apps/3002-checkout/cypress/e2e/app.cy.ts +++ b/apps/3002-checkout/cypress/e2e/app.cy.ts @@ -101,7 +101,7 @@ describe('3002-checkout/', () => { cy.request(src).its('status').should('eq', 200); }); }); - xit('should check that shop-webpack-png images are not 404 between route clicks', () => { + it('should check that shop-webpack-png images are not 404 between route clicks', () => { cy.visit('/shop'); cy.url().should('include', '/shop'); getH1().contains('Shop Page'); diff --git a/apps/3002-checkout/package.json b/apps/3002-checkout/package.json index 83bd8b989ab..dbb8aa40a2b 100644 --- a/apps/3002-checkout/package.json +++ b/apps/3002-checkout/package.json @@ -23,15 +23,16 @@ "styled-jsx": "5.1.2", "tapable": "2.2.1", "terser-webpack-plugin": "5.3.10", + "typescript": "5.3.3", + "upath": "2.0.1", "url": "0.11.3", "util": "0.12.5", - "webpack-sources": "3.2.3", - "typescript": "5.3.3" + "webpack-sources": "3.2.3" }, "devDependencies": { "@module-federation/nextjs-mf": "workspace:*", - "@module-federation/sdk": "workspace:*", "@module-federation/runtime": "workspace:*", + "@module-federation/sdk": "workspace:*", "@module-federation/utilities": "workspace:*" }, "scripts": { diff --git a/apps/3002-checkout/pages/_app.tsx b/apps/3002-checkout/pages/_app.tsx index b5e2ce2fa96..dd7dafccdaf 100644 --- a/apps/3002-checkout/pages/_app.tsx +++ b/apps/3002-checkout/pages/_app.tsx @@ -25,7 +25,9 @@ function MyApp({ Component, pageProps }) { } }; // handle first route hit. - React.useMemo(() => handleRouteChange(asPath), [asPath]); + React.useEffect(() => { + handleRouteChange(asPath); + }, [asPath]); //handle route change React.useEffect(() => { diff --git a/apps/3002-checkout/pages/_document.js b/apps/3002-checkout/pages/_document.js index acf81025bc3..2edbb9485d1 100644 --- a/apps/3002-checkout/pages/_document.js +++ b/apps/3002-checkout/pages/_document.js @@ -8,15 +8,18 @@ import { class MyDocument extends Document { static async getInitialProps(ctx) { + if (ctx.pathname) { + if (!ctx.pathname.endsWith('_error')) { + await revalidate().then((shouldUpdate) => { + if (shouldUpdate) { + console.log('should HMR', shouldUpdate); + } + }); + } + } + const initialProps = await Document.getInitialProps(ctx); const chunks = await flushChunks(); - ctx?.res?.on('finish', () => { - revalidate().then((shouldUpdate) => { - if (shouldUpdate) { - console.log('should HMR', shouldUpdate); - } - }); - }); return { ...initialProps, diff --git a/apps/modernjs/project.json b/apps/modernjs/project.json index 3d8477b1076..2077efa727e 100644 --- a/apps/modernjs/project.json +++ b/apps/modernjs/project.json @@ -33,7 +33,7 @@ "commands": [ { "command": "cd apps/modernjs; pnpm run dev", - "forwardAllArgs": true + "forwardAllArgs": false } ] } @@ -61,11 +61,11 @@ "parallel": true, "commands": [ { - "command": "lsof -i :8080 || nx run modernjs:serve && echo 'done'", + "command": "lsof -i :8080 || nx run modernjs:serve & echo 'done'", "forwardAllArgs": false }, { - "command": "sleep 4 && nx run modernjs:e2e", + "command": "sleep 20 && nx run modernjs:e2e", "forwardAllArgs": true } ] diff --git a/apps/node-host/src/bootstrap.js b/apps/node-host/src/bootstrap.js index 7132b1681fc..d2fa35e7bf4 100644 --- a/apps/node-host/src/bootstrap.js +++ b/apps/node-host/src/bootstrap.js @@ -5,27 +5,23 @@ import express from 'express'; import * as path from 'path'; - +import node_local_remote from 'node_local_remote/test'; 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) => { - const localRemoteMsg = import('node_local_remote/test').then((m) => { - console.log('\x1b[32m%s\x1b[0m', m.default || m); - return m.default || m; - }); - res.send({ message: 'Welcome to node-host!', remotes: { node_remote: await remoteMsg, - node_local_remote: await localRemoteMsg, + node_local_remote, }, }); }); diff --git a/apps/node-remote/webpack.config.js b/apps/node-remote/webpack.config.js index 297954fb85b..3a3a0da3941 100644 --- a/apps/node-remote/webpack.config.js +++ b/apps/node-remote/webpack.config.js @@ -24,7 +24,6 @@ module.exports = composePlugins(withNx(), (config) => { exposes: { './test': './src/expose.js', }, - experiments: {}, }), ); return config; diff --git a/package.json b/package.json index e67ab635b2a..08ad78d8fe9 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,8 @@ "app:manifest:dev": "nx run-many --target=serve --parallel=5 -p 3008-webpack-host,3009-webpack-provider,3010-rspack-provider,3011-rspack-manifest-provider,3012-rspack-js-entry-provider", "commitlint": "commitlint --edit", "prepare": "husky install", - "changeset": "changeset" + "changeset": "changeset", + "build:packages": "npx nx affected -t build --parallel=10 --exclude='*,!tag:package'" }, "dependencies": { "adm-zip": "0.5.10", @@ -65,6 +66,7 @@ "unplugin": "1.9.0" }, "devDependencies": { + "terser-webpack-plugin": "^5.3.10", "@antora/cli": "3.1.5", "@antora/lunr-extension": "1.0.0-alpha.8", "@antora/site-generator": "3.1.7", diff --git a/packages/enhanced/jest.config.ts b/packages/enhanced/jest.config.ts index be809c93de6..b03d8d625da 100644 --- a/packages/enhanced/jest.config.ts +++ b/packages/enhanced/jest.config.ts @@ -1,5 +1,5 @@ /* eslint-disable */ -import { readFileSync } from 'fs'; +import { readFileSync, rmdirSync, existsSync } from 'fs'; import path from 'path'; // Reading the SWC compilation config and remove the "exclude" @@ -8,6 +8,10 @@ const { exclude: _, ...swcJestConfig } = JSON.parse( readFileSync(`${__dirname}/.swcrc`, 'utf-8'), ); +if (existsSync(__dirname + '/test/js')) { + rmdirSync(__dirname + '/test/js', { recursive: true }); +} + // disable .swcrc look-up by SWC core because we're passing in swcJestConfig ourselves. // If we do not disable this, SWC Core will read .swcrc and won't transform our test files due to "exclude" if (swcJestConfig.swcrc === undefined) { diff --git a/packages/enhanced/src/lib/container/ContainerEntryModule.ts b/packages/enhanced/src/lib/container/ContainerEntryModule.ts index 363f7b692e0..7e3d063b285 100644 --- a/packages/enhanced/src/lib/container/ContainerEntryModule.ts +++ b/packages/enhanced/src/lib/container/ContainerEntryModule.ts @@ -189,7 +189,6 @@ class ContainerEntryModule extends Module { // @ts-ignore new EntryDependency(this._injectRuntimeEntry), ); - callback(); } diff --git a/packages/enhanced/src/lib/container/ContainerPlugin.ts b/packages/enhanced/src/lib/container/ContainerPlugin.ts index 2bf0c22ecf5..5c2bc4af6e7 100644 --- a/packages/enhanced/src/lib/container/ContainerPlugin.ts +++ b/packages/enhanced/src/lib/container/ContainerPlugin.ts @@ -149,6 +149,8 @@ class ContainerPlugin { if (!useModuleFederationPlugin) { ContainerPlugin.patchChunkSplit(compiler, this._options.name); + ContainerPlugin.patchChunkSplit(compiler, 'federation-runtime'); + ContainerPlugin.patchChunkSplit(compiler, 'mfp-runtime-plugins'); } const federationRuntimePluginInstance = new FederationRuntimePlugin(); federationRuntimePluginInstance.apply(compiler); @@ -164,6 +166,16 @@ class ContainerPlugin { ) { compiler.options.output.enabledLibraryTypes.push(library.type); } + const hasSingleRuntimeChunk = compiler.options?.optimization?.runtimeChunk; + + new compiler.webpack.EntryPlugin( + compiler.options.context || '', + federationRuntimePluginInstance.entryFilePath, + { + name, + runtime: hasSingleRuntimeChunk ? false : runtime, + }, + ).apply(compiler); compiler.hooks.make.tapAsync(PLUGIN_NAME, (compilation, callback) => { const dep = new ContainerEntryDependency( @@ -199,21 +211,45 @@ class ContainerPlugin { ); // Function to add entry for undefined runtime - const addEntryToSingleRuntimeChunk = () => { - compilation.addEntry( - compilation.options.context || '', - //@ts-ignore - dep, - { - name: name ? name + '_partial' : undefined, // give unique name name - runtime: undefined, - library, - }, - (error: WebpackError | null | undefined) => { - if (error) return callback(error); - callback(); - }, - ); + const addEntryToSingleRuntimeChunk = async () => { + const entries = + typeof compiler.options.entry === 'function' + ? await compiler.options.entry() + : compiler.options.entry; + const runtimes: Set = new Set(); + + Object.keys(entries).forEach((key) => { + if (entries[key].runtime) { + runtimes.add(entries[key].runtime); + } else if (entries[key].runtime === undefined) { + runtimes.add(undefined); + } + }); + + //Add container entry for each runtime that exists + for (const runtime of runtimes) { + const name = runtime + ? 'federation-runtime-' + runtime + : 'federation-runtime'; + await new Promise((resolve, reject) => { + compilation.addEntry( + compilation.options.context || '', + //@ts-ignore + dep, + { + name: name, // merge container into federation entrypoint added to compilation + runtime: runtime, + library, + }, + (error: WebpackError | null | undefined) => { + if (error) return reject(error); + resolve(true); + }, + ); + }).catch(callback); + } + + callback(); }; }); diff --git a/packages/enhanced/src/lib/container/HoistContainerReferencesPlugin.ts b/packages/enhanced/src/lib/container/HoistContainerReferencesPlugin.ts index bb4bc008cee..fd6b28f7c95 100644 --- a/packages/enhanced/src/lib/container/HoistContainerReferencesPlugin.ts +++ b/packages/enhanced/src/lib/container/HoistContainerReferencesPlugin.ts @@ -4,24 +4,116 @@ import type { Chunk, WebpackPluginInstance, } from 'webpack'; +import { normalizeWebpackPath } from '@module-federation/sdk/normalize-webpack-path'; import ContainerEntryModule from './ContainerEntryModule'; -/** - * This class is used to hoist container references in the code. - * @constructor - */ -export class HoistContainerReferences implements WebpackPluginInstance { +const runtime = require( + normalizeWebpackPath('webpack/lib/util/runtime'), +) as typeof import('webpack/lib/util/runtime'); + +export class HoistContainerReferencesPlugin implements WebpackPluginInstance { + private integratedChunks: Set = new Set(); + integrateChunks( + chunkA: Chunk, + chunkB: Chunk, + compilation: Compilation, + ): void { + const { chunkGraph, compiler } = compilation; + // Merge id name hints + for (const hint of chunkB.idNameHints) { + chunkA.idNameHints.add(hint); + } + this.integratedChunks.add(chunkB); + // Merge runtime + //@ts-ignore + chunkA.runtime = runtime.mergeRuntime(chunkA.runtime, chunkB.runtime); + + // getChunkModules is used here to create a clone, because disconnectChunkAndModule modifies + for (const module of chunkGraph.getChunkModules(chunkB)) { + // chunkGraph.disconnectChunkAndModule(chunkB, module); + chunkGraph.connectChunkAndModule(chunkA, module); + } + + for (const [module, chunkGroup] of Array.from( + chunkGraph.getChunkEntryModulesWithChunkGroupIterable(chunkB), + )) { + // dont disconnect as module may need to be copied into multiple chunks + // chunkGraph.disconnectChunkAndEntryModule(chunkB, module); + //connect as normal module not entry module to preserve existing entrypoint modules + chunkGraph.connectChunkAndModule(chunkA, module); + // chunkGraph.connectChunkAndEntryModule(chunkA, module,chunkGroup); + } + + for (const chunkGroup of chunkB.groupsIterable) { + chunkGroup.replaceChunk(chunkB, chunkA); + chunkA.addGroup(chunkGroup); + chunkB.removeGroup(chunkGroup); + } + compiler.webpack.ChunkGraph.clearChunkGraphForChunk(chunkB); + } + apply(compiler: Compiler): void { - compiler.hooks.thisCompilation.tap( - 'HoistContainerReferences', + let hasMultipleRuntime = 0; + compiler.hooks.make.tapPromise( + this.constructor.name, + async (compilation) => { + let entry: any; + if (typeof compilation.options.entry === 'function') { + entry = await compilation.options.entry(); + } else { + entry = compilation.options.entry; + } + + Object.keys(entry).forEach((entryItem) => { + if (entry[entryItem].runtime) { + hasMultipleRuntime++; + } + }); + }, + ); + compiler.hooks.compilation.tap( + 'HoistContainerReferencesPlugin', (compilation: Compilation) => { + const runtimes: Set = new Set(); + compilation.hooks.afterOptimizeChunks.tap( - 'HoistContainerReferences', - (chunks: Iterable) => { - for (const chunk of chunks) { - if (this.chunkContainsContainerEntryModule(chunk, compilation)) { - this.hoistModulesInChunk(chunk, compilation); - } + { + name: 'HoistContainerReferencesPlugin', + stage: 10, // Advanced stage chunk optimization. + }, + (chunks: Iterable) => this.processChunks(chunks, compilation), + ); + + compilation.hooks.beforeChunkAssets.tap( + 'HoistContainerReferencesPlugin', + () => { + // the federation-runtime chunk is integrated into multiple other runtime chunks, like main, or runtime.js + // because this entrypoint is integrated using chunk group updates - this chunk cannot be emitted without causing multiple writes to same runtime + // the federation-runtime serves no output process, it is used as a reference to hoist federation runtime once into all runtime chunks for eager consumption + // this plugin serves + const federationRuntimeChunk = + compilation.namedChunks.get('federation-runtime'); + + const federationRuntimePluginsChunk = compilation.namedChunks.get( + 'mfp-runtime-plugins', + ); + + if (federationRuntimeChunk) { + compilation.chunks.delete(federationRuntimeChunk); + this.integratedChunks.delete(federationRuntimeChunk); + } + + if (federationRuntimePluginsChunk) { + compilation.chunks.delete(federationRuntimePluginsChunk); + this.integratedChunks.delete(federationRuntimePluginsChunk); + } + + compilation.namedChunks.delete('federation-runtime'); + compilation.namedChunks.delete('mfp-runtime-plugins'); + + for (const chunk of this.integratedChunks) { + compilation.chunks.delete(chunk); + if (chunk.name) compilation.namedChunks.delete(chunk.name); } }, ); @@ -29,40 +121,154 @@ export class HoistContainerReferences implements WebpackPluginInstance { ); } - private chunkContainsContainerEntryModule( - chunk: Chunk, - compilation: Compilation, - ): boolean { - for (const module of compilation.chunkGraph.getChunkModulesIterable( - chunk, - )) { - if (module instanceof ContainerEntryModule) { - return true; + processChunks(chunks: Iterable, compilation: Compilation) { + const { chunkGraph, compiler } = compilation; + const runtimes = new Set(); + this.collectRuntimes(chunks, runtimes); + + if (!compiler.options.optimization.runtimeChunk) { + this.optimizeWithoutRuntimeChunk(chunks, compilation); + } else { + this.optimizeWithRuntimeChunk(compilation, runtimes); + } + } + + collectRuntimes(chunks: Iterable, runtimes: Set) { + for (const chunk of chunks) { + if (!chunk.runtime) continue; + if (typeof chunk.runtime === 'string') { + runtimes.add(chunk.runtime); + } else { + for (const runtime of chunk.runtime) { + runtimes.add(runtime); + } } } - return false; } - private hoistModulesInChunk(chunk: Chunk, compilation: Compilation): void { - const chunkGraph = compilation.chunkGraph; - const runtimeChunks = this.getRuntimeChunks(chunk, compilation); + optimizeWithoutRuntimeChunk( + chunks: Iterable, + compilation: Compilation, + ) { + const federationRuntimeChunk = + compilation.namedChunks.get('federation-runtime'); + const federationRuntimePlugins = compilation.namedChunks.get( + 'mfp-runtime-plugins', + ); - for (const module of chunkGraph.getChunkModulesIterable(chunk)) { - for (const runtimeChunk of runtimeChunks) { - chunkGraph.connectChunkAndModule(runtimeChunk, module); + if (federationRuntimeChunk && federationRuntimePlugins) { + this.integrateRuntimeChunks( + chunks, + federationRuntimeChunk, + federationRuntimePlugins, + compilation, + ); + this.disconnectModulesFromChunk(compilation, federationRuntimeChunk); + this.disconnectModulesFromChunk(compilation, federationRuntimePlugins); + } + } + + integrateRuntimeChunks( + chunks: Iterable, + federationRuntimeChunk: Chunk, + federationRuntimePlugins: Chunk, + compilation: Compilation, + ) { + for (const chunk of chunks) { + if ( + chunk.hasRuntime() && + !this.chunkContainsContainerEntryModule(chunk, compilation) + ) { + if (chunk !== federationRuntimeChunk) { + this.integrateChunks(chunk, federationRuntimeChunk, compilation); + } + if (chunk !== federationRuntimePlugins) { + this.integrateChunks(chunk, federationRuntimePlugins, compilation); + } } } } - private getRuntimeChunks(chunk: Chunk, compilation: Compilation): Chunk[] { - const runtimeChunks = []; - for (const c of compilation.chunks) { - if (c.hasRuntime() && c !== chunk) { - runtimeChunks.push(c); + disconnectModulesFromChunk(compilation: Compilation, chunk: Chunk) { + const { chunkGraph } = compilation; + for (const module of chunkGraph.getChunkModules(chunk)) { + chunkGraph.disconnectChunkAndModule(chunk, module); + } + } + + optimizeWithRuntimeChunk(compilation: Compilation, runtimes: Set) { + const baseRuntimeName = 'federation-runtime'; + const basePluginsName = 'mfp-runtime-plugins'; + + for (const runtime of runtimes) { + this.handleRuntimeChunks( + compilation, + runtime, + baseRuntimeName, + basePluginsName, + ); + } + } + + handleRuntimeChunks( + compilation: Compilation, + runtime: string, + baseRuntimeName: string, + basePluginsName: string, + ) { + const runtimeChunk = compilation.namedChunks.get(runtime); + if ( + !runtimeChunk || + this.chunkContainsContainerEntryModule(runtimeChunk, compilation) + ) + return; + + const federationRuntimeChunk = this.getNamedChunk( + compilation, + `${baseRuntimeName}-${runtime}`, + baseRuntimeName, + ); + const pluginsRuntimeChunk = this.getNamedChunk( + compilation, + `${basePluginsName}-${runtime}`, + basePluginsName, + ); + + if (federationRuntimeChunk) { + this.integrateChunks(runtimeChunk, federationRuntimeChunk, compilation); + } + if (pluginsRuntimeChunk) { + this.integrateChunks(runtimeChunk, pluginsRuntimeChunk, compilation); + } + } + + getNamedChunk( + compilation: Compilation, + newChunkName: string, + defaultChunkName: string, + ): Chunk | undefined { + return ( + // search for runtime allocated entry chunk, fall back to default runtime chunk (undefined, false, etc) + compilation.namedChunks.get(newChunkName) || + compilation.namedChunks.get(defaultChunkName) + ); + } + + private chunkContainsContainerEntryModule( + chunk: Chunk, + compilation: Compilation, + ): boolean { + let hasContainerEntryModule = false; + for (const module of compilation.chunkGraph.getChunkModulesIterable( + chunk, + )) { + if (module instanceof ContainerEntryModule) { + hasContainerEntryModule = true; + break; } } - return runtimeChunks; + return hasContainerEntryModule; } } -export default HoistContainerReferences; +export default HoistContainerReferencesPlugin; diff --git a/packages/enhanced/src/lib/container/ModuleFederationPlugin.ts b/packages/enhanced/src/lib/container/ModuleFederationPlugin.ts index a84f0aedd8f..b32cc96a408 100644 --- a/packages/enhanced/src/lib/container/ModuleFederationPlugin.ts +++ b/packages/enhanced/src/lib/container/ModuleFederationPlugin.ts @@ -52,7 +52,6 @@ class ModuleFederationPlugin implements WebpackPluginInstance { */ apply(compiler: Compiler): void { const { _options: options } = this; - // @ts-ignore new FederationRuntimePlugin(options).apply(compiler); const library = options.library || { type: 'var', name: options.name }; const remoteType = @@ -71,6 +70,8 @@ class ModuleFederationPlugin implements WebpackPluginInstance { if (useContainerPlugin) { // @ts-ignore ContainerPlugin.patchChunkSplit(compiler, this._options.name); + ContainerPlugin.patchChunkSplit(compiler, 'federation-runtime'); + ContainerPlugin.patchChunkSplit(compiler, 'mfp-runtime-plugins'); } if (!disableManifest && useContainerPlugin) { @@ -105,7 +106,6 @@ class ModuleFederationPlugin implements WebpackPluginInstance { //@ts-ignore exposes: options.exposes, runtimePlugins: options.runtimePlugins, - //@ts-ignore }).apply(compiler); } if ( diff --git a/packages/enhanced/src/lib/container/runtime/FederationInitModule.ts b/packages/enhanced/src/lib/container/runtime/FederationInitModule.ts new file mode 100644 index 00000000000..d4b760c8d4b --- /dev/null +++ b/packages/enhanced/src/lib/container/runtime/FederationInitModule.ts @@ -0,0 +1,137 @@ +import { normalizeWebpackPath } from '@module-federation/sdk/normalize-webpack-path'; +import type { Chunk, Compilation, Module } from 'webpack'; + +const { RuntimeModule, Template } = require( + normalizeWebpackPath('webpack'), +) as typeof import('webpack'); + +class FederationInitModule extends RuntimeModule { + constructor( + public containerName: string, + public entryFilePath: string, + public chunksRuntimePluginsDependsOn: Set | undefined, + ) { + super('federation runtime init', RuntimeModule.STAGE_ATTACH); + } + + private chunkContainsFederationRuntime( + chunk: Chunk, + compilation: Compilation, + ): { + federationRuntimeModule: Module | null; + federationRuntimePluginModule: Module | null; + } { + let federationRuntimeModule: Module | null = null; + let federationRuntimePluginModule: Module | null = null; + for (const module of compilation.chunkGraph.getChunkModulesIterable( + chunk, + )) { + if ( + !federationRuntimeModule && + module.identifier?.()?.includes('.federation/federation') + ) { + federationRuntimeModule = module; + } else if ( + !federationRuntimePluginModule && + module.identifier?.()?.includes('.federation/plugin') + ) { + federationRuntimePluginModule = module; + } + if (federationRuntimeModule && federationRuntimePluginModule) break; + } + return { federationRuntimeModule, federationRuntimePluginModule }; + } + + getModuleByInstance(): { + federationRuntimeModuleId: string | number | undefined; + runtimePluginModuleId: string | number | undefined; + chunk: Chunk; + } | null { + if ( + !this.compilation || + !this.chunk || + !this.compilation.chunkGraph || + !this.chunk.hasRuntime() + ) + return null; + + const { federationRuntimeModule, federationRuntimePluginModule } = + this.chunkContainsFederationRuntime(this.chunk, this.compilation); + let runtimePluginModuleId: string | number | undefined; + let federationRuntimeModuleId: string | number | undefined; + + if (federationRuntimeModule) { + federationRuntimeModuleId = this.compilation.chunkGraph.getModuleId( + federationRuntimeModule, + ); + } + + if (federationRuntimePluginModule) { + runtimePluginModuleId = this.compilation.chunkGraph.getModuleId( + federationRuntimePluginModule, + ); + } + + return { + federationRuntimeModuleId, + runtimePluginModuleId, + chunk: this.chunk, + }; + } + + override generate(): string | null { + if (!this.compilation || !this.chunk) return ''; + + const moduleInstance = this.getModuleByInstance(); + // Early return if no moduleInstance is found + if (!moduleInstance) return ''; + + const { federationRuntimeModuleId, runtimePluginModuleId } = moduleInstance; + const requireStatements: string[] = []; + + // Directly push federationRuntimeModuleId require statement if it exists + if (federationRuntimeModuleId) { + requireStatements.push( + `__webpack_require__(${JSON.stringify(federationRuntimeModuleId)});`, + ); + } + + if (runtimePluginModuleId) { + // check if needs async boundary + const chunkConsumesStatements = this.chunksRuntimePluginsDependsOn + ? Array.from(this.chunksRuntimePluginsDependsOn) + .map( + (chunk) => + `__webpack_require__.f.consumes(${JSON.stringify( + chunk.id || chunk.name, + )}, consumes);`, + ) + .join('\n') + : ''; + + if (chunkConsumesStatements) { + requireStatements.push( + Template.asString([ + `var consumes = [];`, + `if(__webpack_require__.f && __webpack_require__.f.consumes){`, + Template.indent(chunkConsumesStatements), + `}`, + `Promise.all(consumes).then(function() {`, + Template.indent( + `__webpack_require__(${JSON.stringify(runtimePluginModuleId)});`, + ), + `});`, + ]), + ); + } else { + requireStatements.push( + `__webpack_require__(${JSON.stringify(runtimePluginModuleId)});`, + ); + } + } + + return Template.asString(requireStatements); + } +} + +export default FederationInitModule; diff --git a/packages/enhanced/src/lib/container/runtime/FederationRuntimePlugin.ts b/packages/enhanced/src/lib/container/runtime/FederationRuntimePlugin.ts index 1218b2155d5..20dd76e9105 100644 --- a/packages/enhanced/src/lib/container/runtime/FederationRuntimePlugin.ts +++ b/packages/enhanced/src/lib/container/runtime/FederationRuntimePlugin.ts @@ -1,6 +1,7 @@ -import type { Compiler, sources } from 'webpack'; +import type { Compiler, Chunk } from 'webpack'; import { normalizeWebpackPath } from '@module-federation/sdk/normalize-webpack-path'; import FederationRuntimeModule from './FederationRuntimeModule'; +import FederationInitModule from './FederationInitModule'; import { getFederationGlobalScope, normalizeRuntimeInitOptionsWithOutShared, @@ -12,6 +13,7 @@ import fs from 'fs'; import path from 'path'; import { TEMP_DIR } from '../constant'; import type { moduleFederationPlugin } from '@module-federation/sdk'; +import HoistContainerReferences from '../HoistContainerReferencesPlugin'; const { RuntimeGlobals, Template } = require( normalizeWebpackPath('webpack'), @@ -37,22 +39,43 @@ const federationGlobal = getFederationGlobalScope(RuntimeGlobals); class FederationRuntimePlugin { options?: moduleFederationPlugin.ModuleFederationPluginOptions; entryFilePath: string; + pluginsFilePath: string; // New path for plugins file bundlerRuntimePath: string; constructor(options?: moduleFederationPlugin.ModuleFederationPluginOptions) { this.options = options ? { ...options } : undefined; this.entryFilePath = ''; + this.pluginsFilePath = ''; // Initialize plugins file path this.bundlerRuntimePath = BundlerRuntimePath; } - static getTemplate(runtimePlugins: string[], bundlerRuntimePath?: string) { + static getTemplate(bundlerRuntimePath: string) { // internal runtime plugin const normalizedBundlerRuntimePath = normalizeToPosixPath( bundlerRuntimePath || BundlerRuntimePath, ); + return Template.asString([ + `import federation from '${normalizedBundlerRuntimePath}';`, + `${federationGlobal} = {...federation,...${federationGlobal}};`, + `if(!${federationGlobal}.instance){`, + Template.indent([ + `${federationGlobal}.instance = ${federationGlobal}.runtime.init(${federationGlobal}.initOptions);`, + `if(${federationGlobal}.attachShareScopeMap){`, + Template.indent([ + `${federationGlobal}.attachShareScopeMap(${RuntimeGlobals.require})`, + ]), + '}', + `if(${federationGlobal}.installInitialConsumes){`, + Template.indent([`${federationGlobal}.installInitialConsumes()`]), + '}', + ]), + '}', + ]); + } + static getPluginsTemplate(runtimePlugins: string[]) { let runtimePluginTemplates = ''; - const runtimePLuginNames: string[] = []; + const runtimePluginNames: string[] = []; if (Array.isArray(runtimePlugins)) { runtimePlugins.forEach((runtimePlugin, index) => { @@ -64,103 +87,80 @@ class FederationRuntimePlugin { ); runtimePluginTemplates += `import ${runtimePluginName} from '${runtimePluginPath}';\n`; - runtimePLuginNames.push(runtimePluginName); + runtimePluginNames.push(runtimePluginName); }); } return Template.asString([ - `import federation from '${normalizedBundlerRuntimePath}';`, runtimePluginTemplates, - `${federationGlobal} = {...federation,...${federationGlobal}};`, - `if(!${federationGlobal}.instance){`, + `if(${federationGlobal}.instance){`, Template.indent([ - runtimePLuginNames.length + runtimePluginNames.length ? Template.asString([ - `${federationGlobal}.initOptions.plugins = ([`, - Template.indent(runtimePLuginNames.map((item) => `${item}(),`)), - '])', + `${federationGlobal}.initOptions.plugins = ${federationGlobal}.initOptions.plugins ? ${federationGlobal}.initOptions.plugins.concat([`, + Template.indent(runtimePluginNames.map((item) => `${item}(),`)), + ']) : [', + Template.indent(runtimePluginNames.map((item) => `${item}(),`)), + '];', ]) : '', - `${federationGlobal}.instance = ${federationGlobal}.runtime.init(${federationGlobal}.initOptions);`, - `if(${federationGlobal}.attachShareScopeMap){`, - Template.indent([ - `${federationGlobal}.attachShareScopeMap(${RuntimeGlobals.require})`, - ]), - '}', - `if(${federationGlobal}.installInitialConsumes){`, - Template.indent([`${federationGlobal}.installInitialConsumes()`]), - '}', + `${federationGlobal}.runtime.init(${federationGlobal}.initOptions);`, //init again with plugins attached. ]), '}', ]); } - static getFilePath( - containerName: string, - runtimePlugins: string[], - bundlerRuntimePath?: string, - ) { - const hash = createHash( - `${containerName} ${FederationRuntimePlugin.getTemplate( - runtimePlugins, - bundlerRuntimePath, - )}`, - ); - return path.join(TEMP_DIR, `entry.${hash}.js`); - } - - getFilePath() { - if (this.entryFilePath) { - return this.entryFilePath; - } - + ensureFiles() { if (!this.options) { - return ''; + return; } - this.entryFilePath = FederationRuntimePlugin.getFilePath( - this.options.name!, - this.options.runtimePlugins!, + const federationTemplate = FederationRuntimePlugin.getTemplate( this.bundlerRuntimePath, ); - return this.entryFilePath; + const pluginsTemplate = FederationRuntimePlugin.getPluginsTemplate( + this.options.runtimePlugins || [], + ); + + const federationHash = createHash(federationTemplate); + const pluginsHash = createHash(pluginsTemplate); + + this.entryFilePath = path.join(TEMP_DIR, `federation.${federationHash}.js`); + this.pluginsFilePath = path.join(TEMP_DIR, `plugins.${pluginsHash}.js`); + + this.writeFile(this.entryFilePath, federationTemplate); + this.writeFile(this.pluginsFilePath, pluginsTemplate); } - ensureFile() { - if (!this.options) { - return; - } - const filePath = this.getFilePath(); + writeFile(filePath: string, content: string) { try { fs.readFileSync(filePath); } catch (err) { - mkdirpSync(fs, TEMP_DIR); - fs.writeFileSync( - filePath, - FederationRuntimePlugin.getTemplate( - this.options.runtimePlugins!, - this.bundlerRuntimePath, - ), - ); + mkdirpSync(fs, path.dirname(filePath)); + fs.writeFileSync(filePath, content); } } prependEntry(compiler: Compiler) { - this.ensureFile(); - const entryFilePath = this.getFilePath(); - + this.ensureFiles(); modifyEntry({ compiler, prependEntry: (entry) => { - Object.keys(entry).forEach((entryName) => { - const entryItem = entry[entryName]; - if (!entryItem.import) { - // TODO: maybe set this variable as constant is better https://github.com/webpack/webpack/blob/main/lib/config/defaults.js#L176 - entryItem.import = ['./src']; - } - if (!entryItem.import.includes(entryFilePath)) { - entryItem.import.unshift(entryFilePath); - } + Object.keys(entry).forEach((key) => { + const entryItem = entry[key]; + const prefix = entryItem.runtime ? `-${entryItem.runtime}` : ''; + const runtimePluginKey = `mfp-runtime-plugins${prefix}`; + const federationRuntimeKey = `federation-runtime${prefix}`; + + entry[runtimePluginKey] = { + import: [this.pluginsFilePath], + runtime: entryItem.runtime, + }; + + entry[federationRuntimeKey] = { + import: [this.entryFilePath], + runtime: entryItem.runtime, + }; }); }, }); @@ -181,6 +181,19 @@ class FederationRuntimePlugin { compiler.hooks.thisCompilation.tap( this.constructor.name, (compilation, { normalModuleFactory }) => { + let chunksRuntimePluginsDependsOn: Set | undefined = undefined; + compilation.hooks.afterOptimizeChunks.tap( + this.constructor.name, + (chunk) => { + const runtimePluginEntry = compilation.namedChunks.get( + 'mfp-runtime-plugins', + ); + if (runtimePluginEntry) { + chunksRuntimePluginsDependsOn = + runtimePluginEntry.getAllInitialChunks(); + } + }, + ); compilation.hooks.additionalTreeRuntimeRequirements.tap( this.constructor.name, (chunk, runtimeRequirements) => { @@ -199,6 +212,14 @@ class FederationRuntimePlugin { initOptionsWithoutShared, ), ); + compilation.addRuntimeModule( + chunk, + new FederationInitModule( + name, + this.entryFilePath, + chunksRuntimePluginsDependsOn, + ), + ); }, ); }, @@ -280,6 +301,8 @@ class FederationRuntimePlugin { this.prependEntry(compiler); this.injectRuntime(compiler); this.setRuntimeAlias(compiler); + + new HoistContainerReferences().apply(compiler); } } diff --git a/packages/enhanced/test/configCases/container/multiple-entrypoints-1/App.js b/packages/enhanced/test/configCases/container/multiple-entrypoints-1/App.js new file mode 100644 index 00000000000..731b14455db --- /dev/null +++ b/packages/enhanced/test/configCases/container/multiple-entrypoints-1/App.js @@ -0,0 +1,6 @@ +import React from 'react'; +import ComponentA from 'containerA/ComponentA'; + +export default () => { + return `App rendered with [${React()}] and [${ComponentA()}]`; +}; diff --git a/packages/enhanced/test/configCases/container/multiple-entrypoints-1/ComponentA.js b/packages/enhanced/test/configCases/container/multiple-entrypoints-1/ComponentA.js new file mode 100644 index 00000000000..0e5b6e1ed71 --- /dev/null +++ b/packages/enhanced/test/configCases/container/multiple-entrypoints-1/ComponentA.js @@ -0,0 +1,5 @@ +import React from 'react'; + +export default () => { + return `ComponentA rendered with [${React()}]`; +}; diff --git a/packages/enhanced/test/configCases/container/multiple-entrypoints-1/index.js b/packages/enhanced/test/configCases/container/multiple-entrypoints-1/index.js new file mode 100644 index 00000000000..a965d0d82c1 --- /dev/null +++ b/packages/enhanced/test/configCases/container/multiple-entrypoints-1/index.js @@ -0,0 +1,8 @@ +it('main.js should load the component from container', () => { + return import('./App').then(({ default: App }) => { + const rendered = App(); + expect(rendered).toBe( + 'App rendered with [This is react 0.1.2] and [ComponentA rendered with [This is react 0.1.2]]', + ); + }); +}); diff --git a/packages/enhanced/test/configCases/container/multiple-entrypoints-1/node_modules/react.js b/packages/enhanced/test/configCases/container/multiple-entrypoints-1/node_modules/react.js new file mode 100644 index 00000000000..bcf433f2afb --- /dev/null +++ b/packages/enhanced/test/configCases/container/multiple-entrypoints-1/node_modules/react.js @@ -0,0 +1,3 @@ +let version = "0.1.2"; +export default () => `This is react ${version}`; +export function setVersion(v) { version = v; } diff --git a/packages/enhanced/test/configCases/container/multiple-entrypoints-1/other.js b/packages/enhanced/test/configCases/container/multiple-entrypoints-1/other.js new file mode 100644 index 00000000000..618e400cc83 --- /dev/null +++ b/packages/enhanced/test/configCases/container/multiple-entrypoints-1/other.js @@ -0,0 +1,8 @@ +it('other.js should load the component from container', () => { + return import('./App').then(({ default: App }) => { + const rendered = App(); + expect(rendered).toBe( + 'App rendered with [This is react 1.2.3] and [ComponentA rendered with [This is react 1.2.3]]', + ); + }); +}); diff --git a/packages/enhanced/test/configCases/container/multiple-entrypoints-1/test.config.js b/packages/enhanced/test/configCases/container/multiple-entrypoints-1/test.config.js new file mode 100644 index 00000000000..ea81a87d8a9 --- /dev/null +++ b/packages/enhanced/test/configCases/container/multiple-entrypoints-1/test.config.js @@ -0,0 +1,5 @@ +module.exports = { + findBundle: function (i, options) { + return i === 0 ? './other.js' : './module/main.mjs'; + }, +}; diff --git a/packages/enhanced/test/configCases/container/multiple-entrypoints-1/webpack.config.js b/packages/enhanced/test/configCases/container/multiple-entrypoints-1/webpack.config.js new file mode 100644 index 00000000000..0b0c95ce6bd --- /dev/null +++ b/packages/enhanced/test/configCases/container/multiple-entrypoints-1/webpack.config.js @@ -0,0 +1,66 @@ +const { ModuleFederationPlugin } = require('../../../../dist/src'); + +const common = { + name: 'container', + exposes: { + './ComponentA': { + import: './ComponentA', + }, + }, + shared: { + react: { + version: false, + requiredVersion: false, + }, + }, +}; + +module.exports = [ + { + entry: { + main: './index.js', + other: './other.js', + }, + output: { + filename: '[name].js', + uniqueName: '0-container-full', + }, + optimization: { + runtimeChunk: false, + }, + plugins: [ + new ModuleFederationPlugin({ + library: { type: 'commonjs-module' }, + filename: 'container.js', + remotes: { + containerA: { + external: './container.js', + }, + }, + ...common, + }), + ], + }, + { + experiments: { + outputModule: true, + }, + output: { + filename: 'module/[name].mjs', + uniqueName: '0-container-full-mjs', + }, + plugins: [ + new ModuleFederationPlugin({ + library: { type: 'module' }, + filename: 'module/container.mjs', + remotes: { + containerA: { + external: './container.mjs', + }, + }, + ...common, + }), + ], + target: 'node14', + }, +]; diff --git a/packages/enhanced/test/configCases/container/multiple-entrypoints/App.js b/packages/enhanced/test/configCases/container/multiple-entrypoints/App.js new file mode 100644 index 00000000000..731b14455db --- /dev/null +++ b/packages/enhanced/test/configCases/container/multiple-entrypoints/App.js @@ -0,0 +1,6 @@ +import React from 'react'; +import ComponentA from 'containerA/ComponentA'; + +export default () => { + return `App rendered with [${React()}] and [${ComponentA()}]`; +}; diff --git a/packages/enhanced/test/configCases/container/multiple-entrypoints/ComponentA.js b/packages/enhanced/test/configCases/container/multiple-entrypoints/ComponentA.js new file mode 100644 index 00000000000..0e5b6e1ed71 --- /dev/null +++ b/packages/enhanced/test/configCases/container/multiple-entrypoints/ComponentA.js @@ -0,0 +1,5 @@ +import React from 'react'; + +export default () => { + return `ComponentA rendered with [${React()}]`; +}; diff --git a/packages/enhanced/test/configCases/container/multiple-entrypoints/index.js b/packages/enhanced/test/configCases/container/multiple-entrypoints/index.js new file mode 100644 index 00000000000..b1d1998e70f --- /dev/null +++ b/packages/enhanced/test/configCases/container/multiple-entrypoints/index.js @@ -0,0 +1,15 @@ +it('main.js should load the component from container', () => { + return import('./App').then(({ default: App }) => { + const rendered = App(); + expect(rendered).toBe( + 'App rendered with [This is react 0.1.2] and [ComponentA rendered with [This is react 0.1.2]]', + ); + return import('./upgrade-react').then(({ default: upgrade }) => { + upgrade(); + const rendered = App(); + expect(rendered).toBe( + 'App rendered with [This is react 1.2.3] and [ComponentA rendered with [This is react 1.2.3]]', + ); + }); + }); +}); diff --git a/packages/enhanced/test/configCases/container/multiple-entrypoints/node_modules/react.js b/packages/enhanced/test/configCases/container/multiple-entrypoints/node_modules/react.js new file mode 100644 index 00000000000..bcf433f2afb --- /dev/null +++ b/packages/enhanced/test/configCases/container/multiple-entrypoints/node_modules/react.js @@ -0,0 +1,3 @@ +let version = "0.1.2"; +export default () => `This is react ${version}`; +export function setVersion(v) { version = v; } diff --git a/packages/enhanced/test/configCases/container/multiple-entrypoints/other.js b/packages/enhanced/test/configCases/container/multiple-entrypoints/other.js new file mode 100644 index 00000000000..8ee24e0c9bf --- /dev/null +++ b/packages/enhanced/test/configCases/container/multiple-entrypoints/other.js @@ -0,0 +1,15 @@ +it('other.js should load the component from container', () => { + return import('./App').then(({ default: App }) => { + const rendered = App(); + expect(rendered).toBe( + 'App rendered with [This is react 3.2.1] and [ComponentA rendered with [This is react 3.2.1]]', + ); + return import('./upgrade-react').then(({ default: upgrade }) => { + upgrade(); + const rendered = App(); + expect(rendered).toBe( + 'App rendered with [This is react 1.2.3] and [ComponentA rendered with [This is react 1.2.3]]', + ); + }); + }); +}); diff --git a/packages/enhanced/test/configCases/container/multiple-entrypoints/test.config.js b/packages/enhanced/test/configCases/container/multiple-entrypoints/test.config.js new file mode 100644 index 00000000000..ea81a87d8a9 --- /dev/null +++ b/packages/enhanced/test/configCases/container/multiple-entrypoints/test.config.js @@ -0,0 +1,5 @@ +module.exports = { + findBundle: function (i, options) { + return i === 0 ? './other.js' : './module/main.mjs'; + }, +}; diff --git a/packages/enhanced/test/configCases/container/multiple-entrypoints/upgrade-react.js b/packages/enhanced/test/configCases/container/multiple-entrypoints/upgrade-react.js new file mode 100644 index 00000000000..5bf08a67d5a --- /dev/null +++ b/packages/enhanced/test/configCases/container/multiple-entrypoints/upgrade-react.js @@ -0,0 +1,5 @@ +import { setVersion } from 'react'; + +export default function upgrade() { + setVersion('1.2.3'); +} diff --git a/packages/enhanced/test/configCases/container/multiple-entrypoints/webpack.config.js b/packages/enhanced/test/configCases/container/multiple-entrypoints/webpack.config.js new file mode 100644 index 00000000000..f45600dc012 --- /dev/null +++ b/packages/enhanced/test/configCases/container/multiple-entrypoints/webpack.config.js @@ -0,0 +1,66 @@ +const { ModuleFederationPlugin } = require('../../../../dist/src'); + +const common = { + name: 'container', + exposes: { + './ComponentA': { + import: './ComponentA', + }, + }, + shared: { + react: { + version: false, + requiredVersion: false, + }, + }, +}; + +module.exports = [ + { + entry: { + main: './index.js', + other: './other.js', + }, + output: { + filename: '[name].js', + uniqueName: '0-container-full', + }, + optimization: { + runtimeChunk: 'multiple', + }, + plugins: [ + new ModuleFederationPlugin({ + library: { type: 'commonjs-module' }, + filename: 'container.js', + remotes: { + containerA: { + external: './container.js', + }, + }, + ...common, + }), + ], + }, + { + experiments: { + outputModule: true, + }, + output: { + filename: 'module/[name].mjs', + uniqueName: '0-container-full-mjs', + }, + plugins: [ + new ModuleFederationPlugin({ + library: { type: 'module' }, + filename: 'module/container.mjs', + remotes: { + containerA: { + external: './container.mjs', + }, + }, + ...common, + }), + ], + target: 'node14', + }, +]; diff --git a/packages/enhanced/test/configCases/container/multiple-runtime-chunk/App.js b/packages/enhanced/test/configCases/container/multiple-runtime-chunk/App.js new file mode 100644 index 00000000000..40ef934441f --- /dev/null +++ b/packages/enhanced/test/configCases/container/multiple-runtime-chunk/App.js @@ -0,0 +1,10 @@ +import React from 'react'; +import ComponentA from 'containerA/ComponentA'; +import ComponentB from 'containerB/ComponentB'; +import LocalComponentB from './ComponentB'; + +export default () => { + return `App rendered with [${React()}] and [${ComponentA()}] and [${ComponentB()}]`; +}; + +expect(ComponentB).not.toBe(LocalComponentB); diff --git a/packages/enhanced/test/configCases/container/multiple-runtime-chunk/ComponentB.js b/packages/enhanced/test/configCases/container/multiple-runtime-chunk/ComponentB.js new file mode 100644 index 00000000000..bd88caedbb0 --- /dev/null +++ b/packages/enhanced/test/configCases/container/multiple-runtime-chunk/ComponentB.js @@ -0,0 +1,5 @@ +import React from 'react'; + +export default () => { + return `ComponentB rendered with [${React()}]`; +}; diff --git a/packages/enhanced/test/configCases/container/multiple-runtime-chunk/ComponentC.js b/packages/enhanced/test/configCases/container/multiple-runtime-chunk/ComponentC.js new file mode 100644 index 00000000000..6e6fea21c9b --- /dev/null +++ b/packages/enhanced/test/configCases/container/multiple-runtime-chunk/ComponentC.js @@ -0,0 +1,7 @@ +import React from 'react'; +import ComponentA from 'containerA/ComponentA'; +import ComponentB from 'containerB/ComponentB'; + +export default () => { + return `ComponentC rendered with [${React()}] and [${ComponentA()}] and [${ComponentB()}]`; +}; diff --git a/packages/enhanced/test/configCases/container/multiple-runtime-chunk/index.js b/packages/enhanced/test/configCases/container/multiple-runtime-chunk/index.js new file mode 100644 index 00000000000..ab4ec00eb88 --- /dev/null +++ b/packages/enhanced/test/configCases/container/multiple-runtime-chunk/index.js @@ -0,0 +1,11 @@ +it('should load the component from container', () => { + return import('./App').then(({ default: App }) => { + const rendered = App(); + expect(rendered).toContain('App rendered'); + return import('./upgrade-react').then(({ default: upgrade }) => { + upgrade(); + const rendered = App(); + expect(rendered).toContain('App rendered'); + }); + }); +}); diff --git a/packages/enhanced/test/configCases/container/multiple-runtime-chunk/node_modules/package.json b/packages/enhanced/test/configCases/container/multiple-runtime-chunk/node_modules/package.json new file mode 100644 index 00000000000..87032da008a --- /dev/null +++ b/packages/enhanced/test/configCases/container/multiple-runtime-chunk/node_modules/package.json @@ -0,0 +1,3 @@ +{ + "version": "2.1.0" +} diff --git a/packages/enhanced/test/configCases/container/multiple-runtime-chunk/node_modules/react.js b/packages/enhanced/test/configCases/container/multiple-runtime-chunk/node_modules/react.js new file mode 100644 index 00000000000..97d35a4bc9c --- /dev/null +++ b/packages/enhanced/test/configCases/container/multiple-runtime-chunk/node_modules/react.js @@ -0,0 +1,3 @@ +let version = "2.1.0"; +export default () => `This is react ${version}`; +export function setVersion(v) { version = v; } diff --git a/packages/enhanced/test/configCases/container/multiple-runtime-chunk/package.json b/packages/enhanced/test/configCases/container/multiple-runtime-chunk/package.json new file mode 100644 index 00000000000..be6238fec84 --- /dev/null +++ b/packages/enhanced/test/configCases/container/multiple-runtime-chunk/package.json @@ -0,0 +1,9 @@ +{ + "private": true, + "engines": { + "node": ">=10.13.0" + }, + "dependencies": { + "react": "*" + } +} diff --git a/packages/enhanced/test/configCases/container/multiple-runtime-chunk/test.config.js b/packages/enhanced/test/configCases/container/multiple-runtime-chunk/test.config.js new file mode 100644 index 00000000000..861157bc4ed --- /dev/null +++ b/packages/enhanced/test/configCases/container/multiple-runtime-chunk/test.config.js @@ -0,0 +1,5 @@ +module.exports = { + findBundle: function (i, options) { + return i === 0 ? './main.js' : './module/main.mjs'; + }, +}; diff --git a/packages/enhanced/test/configCases/container/multiple-runtime-chunk/upgrade-react.js b/packages/enhanced/test/configCases/container/multiple-runtime-chunk/upgrade-react.js new file mode 100644 index 00000000000..fd400f3d5a3 --- /dev/null +++ b/packages/enhanced/test/configCases/container/multiple-runtime-chunk/upgrade-react.js @@ -0,0 +1,5 @@ +import { setVersion } from 'react'; + +export default function upgrade() { + setVersion('3.2.1'); +} diff --git a/packages/enhanced/test/configCases/container/multiple-runtime-chunk/webpack.config.js b/packages/enhanced/test/configCases/container/multiple-runtime-chunk/webpack.config.js new file mode 100644 index 00000000000..0009e6536db --- /dev/null +++ b/packages/enhanced/test/configCases/container/multiple-runtime-chunk/webpack.config.js @@ -0,0 +1,73 @@ +const { ModuleFederationPlugin } = require('../../../../dist/src'); + +const common = { + entry: { + main: { + import: './index.js', + runtime: 'other', + }, + another: { + import: './index.js', + runtime: 'webpack', + }, + }, + optimization: { + runtimeChunk: 'single', + }, +}; + +const commonMF = { + runtime: false, + exposes: { + './ComponentB': './ComponentB', + './ComponentC': './ComponentC', + }, + shared: ['react'], +}; + +/** @type {import("../../../../").Configuration[]} */ +module.exports = [ + { + mode: 'production', + ...common, + output: { + filename: '[name].js', + uniqueName: '1-container-full', + }, + plugins: [ + new ModuleFederationPlugin({ + name: 'container', + library: { type: 'commonjs-module' }, + filename: 'container.js', + remotes: { + containerA: '../0-container-full/container.js', + containerB: './container.js', + }, + ...commonMF, + }), + ], + }, + { + ...common, + experiments: { + outputModule: true, + }, + output: { + filename: 'module/[name].mjs', + uniqueName: '1-container-full-mjs', + }, + plugins: [ + new ModuleFederationPlugin({ + name: 'container', + library: { type: 'module' }, + filename: 'module/container.mjs', + remotes: { + containerA: '../../0-container-full/module/container.mjs', + containerB: './container.mjs', + }, + ...commonMF, + }), + ], + target: 'node14', + }, +]; diff --git a/packages/nextjs-mf/README.md b/packages/nextjs-mf/README.md index 21eaa0c3168..71f2aed6258 100644 --- a/packages/nextjs-mf/README.md +++ b/packages/nextjs-mf/README.md @@ -287,26 +287,42 @@ loadRemote('home/exposedModule') ``` **revalidate** +### Hot Reloading with `revalidate` in Production Environments -Enables hot reloading of node server (not client) in production. -This is recommended, without it - servers will not be able to pull remote updates without a full restart. +In production environments, ensuring that your server can dynamically reload and update without requiring a full restart is crucial for maintaining uptime and providing the latest features to your users without disruption. The `revalidate` utility from `@module-federation/nextjs-mf/utils` facilitates this by enabling hot reloading of the node server (not the client). This section outlines two implementations for integrating `revalidate` into your Next.js application to leverage hot reloading capabilities. -More info here: https://github.com/module-federation/nextjs-mf/tree/main/packages/node#utilities +#### Preferred Implementation: Blocking Updates Before Rendering + +This implementation is recommended for most use cases as it helps avoid hydration errors by ensuring that the server and client are always in sync. By blocking and checking for updates before rendering, you can guarantee that your application is always up-to-date without negatively impacting the user experience. + +**How it Works:** + +- **Before rendering the page**, the server checks if there are any updates available. +- **If updates are available**, it proceeds with Hot Module Replacement (HMR) before responding to the client request. +- **This method ensures** that all users receive the latest version of the application without encountering inconsistencies between the server-rendered and client-rendered content. + +**Implementation Example:** ```js // __document.js import { revalidate } from '@module-federation/nextjs-mf/utils'; import Document, { Html, Head, Main, NextScript } from 'next/document'; + class MyDocument extends Document { static async getInitialProps(ctx) { - await revalidate().then((shouldUpdate) => { - console.log('finished sending response', shouldUpdate); - }); - const initialProps = await Document.getInitialProps(ctx); + if (ctx?.pathname && !ctx?.pathname?.endsWith('_error')) { + await revalidate().then((shouldUpdate) => { + if (shouldUpdate) { + console.log('Hot Module Replacement (HMR) activated', shouldUpdate); + } + }); + } + const initialProps = await Document.getInitialProps(ctx); return initialProps; } + render() { return ( @@ -321,6 +337,27 @@ class MyDocument extends Document { } ``` +#### Stale Method: Post-Response Update Checks + +While not recommended due to the potential for hydration errors, this method involves listening for the 'finish' event on the response object and then checking for updates. This could be useful in specific scenarios where updates can be applied less frequently or where immediate consistency between server and client is not as critical. + +**How it Works:** + +- **After responding to the client**, the server listens for the 'finish' event on the response object. +- **Once the response has been sent**, it checks for updates. +- **If updates are found**, it logs or acts upon these updates, although the updates will only apply to subsequent requests. + +**Implementation Example:** + +```js +// Included in the `getInitialProps` method as shown in the preferred implementation +ctx?.res?.on('finish', () => { + revalidate().then((shouldUpdate) => { + console.log('Response sent, checking for updates:', shouldUpdate); + }); +}); +``` + ## For Express.js Hot reloading Express.js required additional steps: https://github.com/module-federation/universe/blob/main/packages/node/README.md diff --git a/packages/nextjs-mf/src/internal.ts b/packages/nextjs-mf/src/internal.ts index ad92fcd44da..af1b4ac9a77 100644 --- a/packages/nextjs-mf/src/internal.ts +++ b/packages/nextjs-mf/src/internal.ts @@ -123,15 +123,6 @@ export const DEFAULT_SHARE_SCOPE_BROWSER: SharedObject = Object.entries( const isInternalOrPromise = (value: string): boolean => ['internal ', 'promise '].some((prefix) => value.startsWith(prefix)); -/** - * Checks if the remote value is using the standard remote syntax. - * - * @param {string} value - The remote value to check. - * @returns {boolean} - True if the value is using the standard remote syntax, false otherwise. - */ -const isStandardRemoteSyntax = (value: string): boolean => { - return value.includes('@'); -}; /** * Parses the remotes object and checks if they are using a custom promise template or not. * If it's a custom promise template, the remote syntax is parsed to get the module name and version number. @@ -183,74 +174,6 @@ export const getDelegates = ( {}, ); -/** - * This function validates the type of the shared item and constructs a shared configuration object based on the item and key. - * If the item is identical to the key or if the item does not necessitate a specific version, - * the function returns an object with the import property set to the item. - * Otherwise, it returns an object with the import property set to the key and the requiredVersion property set to the item. - * - * @param {string | string[]} item - The shared item to be validated and used to construct the shared configuration object. It can be a string or an array of strings. - * @param {string} key - The key associated with the shared item. - * @returns {object} - The constructed shared configuration object. - * @throws {Error} - An error is thrown if the item type is not a string or an array of strings. - */ -const getSharedConfig = (item: string | string[], key: string) => { - if (Array.isArray(item)) { - // This handles the case where item is an array - // Replace the following line with your actual logic - return item.map((i) => ({ - import: i === key || !isRequiredVersion(i) ? i : key, - requiredVersion: i === key || !isRequiredVersion(i) ? undefined : i, - })); - } else if (typeof item === 'string') { - // Handle the case where item is a string - return { - import: item === key || !isRequiredVersion(item) ? item : key, - requiredVersion: - item === key || !isRequiredVersion(item) ? undefined : item, - }; - } else { - throw new Error('Unexpected type in shared'); - } -}; - -/** - * Parses the share options from the provided ModuleFederationPluginOptions object and constructs a new object containing all shared configurations. - * This newly constructed object is then used as the value for the 'shared' property of the Module Federation Plugin Options. - * The function uses the 'parseOptions' utility function from webpack to parse the 'shared' property of the provided options object. - * The 'getSharedConfig' function is used as the 'config' argument for 'parseOptions' to construct the shared configuration object for each shared item. - * The 'item' argument for 'parseOptions' is a function that simply returns the item as it is. - * The function then reduces the parsed shared options into a new object with the shared configuration for each shared item. - * - * @param {ModuleFederationPluginOptions} options - The ModuleFederationPluginOptions object to parse the share options from. - * @returns {Record} - An object containing the shared configuration for each shared item. - */ -const parseShareOptions = (options: ModuleFederationPluginOptions) => { - if (!options.shared) return options; - const sharedOptions: [string, SharedConfig][] = parseOptions( - options.shared, - getSharedConfig, - (item: any) => item, - ); - - return sharedOptions.reduce( - (acc, [key, options]) => { - acc[key] = { - import: options.import, - shareKey: options.shareKey || key, - shareScope: options.shareScope, - requiredVersion: options.requiredVersion, - strictVersion: options.strictVersion, - singleton: options.singleton, - packageName: options.packageName, - eager: options.eager, - }; - return acc; - }, - {} as Record, - ); -}; - /** * Takes an error object and formats it into a displayable string. * If the error object contains a stack trace, it is appended to the error message. diff --git a/packages/nextjs-mf/src/loaders/fixImageLoader.ts b/packages/nextjs-mf/src/loaders/fixImageLoader.ts index 861ed71db66..83fe695dd13 100644 --- a/packages/nextjs-mf/src/loaders/fixImageLoader.ts +++ b/packages/nextjs-mf/src/loaders/fixImageLoader.ts @@ -80,13 +80,9 @@ export async function fixImageLoader( Template.indent([ 'try {', Template.indent([ - `if(typeof document === 'undefined')`, Template.indent( `return ${publicPath} && ${publicPath}.indexOf('://') > 0 ? new URL(${publicPath}).origin : ''`, ), - `const path = (document.currentScript && document.currentScript.src) || new URL(${publicPath}).origin;`, - `const splitted = path.split('/_next')`, - `return splitted.length === 2 ? splitted[0] : '';`, ]), '} catch (e) {', Template.indent([ diff --git a/packages/nextjs-mf/src/plugins/NextFederationPlugin/apply-client-plugins.ts b/packages/nextjs-mf/src/plugins/NextFederationPlugin/apply-client-plugins.ts index db13a675631..2a23fa30f2c 100644 --- a/packages/nextjs-mf/src/plugins/NextFederationPlugin/apply-client-plugins.ts +++ b/packages/nextjs-mf/src/plugins/NextFederationPlugin/apply-client-plugins.ts @@ -5,7 +5,6 @@ import { } from '@module-federation/utilities'; import { ChunkCorrelationPlugin } from '@module-federation/node'; import InvertedContainerPlugin from '../container/InvertedContainerPlugin'; -import { HoistContainerReferencesPlugin } from '@module-federation/enhanced'; /** * Applies client-specific plugins. @@ -38,7 +37,6 @@ export function applyClientPlugins( compiler.options.output.publicPath = 'auto'; // Build will hang without this. Likely something in my plugin compiler.options.optimization.splitChunks = undefined; - new HoistContainerReferencesPlugin().apply(compiler); // If automatic page stitching is enabled, add a new rule to the compiler's module rules if (extraOptions.automaticPageStitching) { diff --git a/packages/nextjs-mf/src/plugins/NextFederationPlugin/apply-server-plugins.ts b/packages/nextjs-mf/src/plugins/NextFederationPlugin/apply-server-plugins.ts index 2eb3d3b0f7d..96e20d092c5 100644 --- a/packages/nextjs-mf/src/plugins/NextFederationPlugin/apply-server-plugins.ts +++ b/packages/nextjs-mf/src/plugins/NextFederationPlugin/apply-server-plugins.ts @@ -1,6 +1,5 @@ import type { Compiler } from 'webpack'; import { ModuleFederationPluginOptions } from '@module-federation/utilities'; -import { HoistContainerReferencesPlugin } from '@module-federation/enhanced'; import path from 'path'; import InvertedContainerPlugin from '../container/InvertedContainerPlugin'; import { ModuleFederationPlugin } from '@module-federation/enhanced'; @@ -33,7 +32,6 @@ export function applyServerPlugins( } // Hoist container references into runtime chunks //@ts-ignore - new HoistContainerReferencesPlugin().apply(compiler); // Add the StreamingTargetPlugin with the ModuleFederationPlugin from the webpack container new StreamingTargetPlugin(options, { diff --git a/packages/nextjs-mf/src/plugins/container/HoistPseudoEagerModules.ts b/packages/nextjs-mf/src/plugins/container/HoistPseudoEagerModules.ts deleted file mode 100644 index 39c4d1f864c..00000000000 --- a/packages/nextjs-mf/src/plugins/container/HoistPseudoEagerModules.ts +++ /dev/null @@ -1,94 +0,0 @@ -import type { Compiler, Chunk, Module, Compilation, ChunkGroup } from 'webpack'; - -/** - * @typedef {import("webpack").Compiler} Compiler - * @typedef {import("webpack").Compilation} Compilation - * @typedef {import("webpack").Chunk} Chunk - * @typedef {import("webpack").Module} Module - */ - -/** - * This class is responsible for hoisting container references in the code. - * @constructor - */ -export class HoistPseudoEager { - /** - * @function apply - * @param {Compiler} compiler The webpack compiler object - */ - apply(compiler: Compiler): void { - // Hook into the compilation process - compiler.hooks.thisCompilation.tap( - 'HoistPseudoEager', - (compilation: Compilation) => { - // Perform the hoisting after chunks are optimized - compilation.hooks.afterOptimizeChunks.tap( - 'HoistPseudoEager', - (chunks: Iterable, chunkGroups: ChunkGroup[]) => { - // Create a map to store chunks by their id or name - /** @type {Map<(string|number), Chunk>} */ - const chunkSet = new Map(); - // Create a set to store external module requests - /** @type {Set} */ - const externalRequests = new Set(); - // Populate the chunkSet with chunks - for (const chunk of chunks) { - const ident = chunk.id || chunk.name; - if (ident) { - chunkSet.set(ident, chunk); - } - } - - // Iterate over chunks again to handle remote modules - for (const chunk of chunks) { - // Get iterable of remote modules for the chunk - const remoteModules = - compilation.chunkGraph.getChunkModulesIterableBySourceType( - chunk, - 'remote', - ); - if (!remoteModules) continue; - const runtime = - chunkSet.get('webpack-runtime') || chunkSet.get('webpack'); - const runtimeRoots = runtime - ? compilation.chunkGraph.getChunkRootModules(runtime) - : null; - const refChunks = runtime - ? Array.from(runtime.getAllReferencedChunks()) - : null; - if (refChunks) { - for (const refChunk of refChunks) { - const consumeSharedModules = - compilation.chunkGraph.getChunkModulesIterableBySourceType( - refChunk, - 'consume-shared', - ); - if (!consumeSharedModules) continue; - //loop through consume-shared modules - for (const module of consumeSharedModules) { - // Get the module associated with the dependency - for (const block of module.blocks) { - for (const dep of block.dependencies) { - const mod = compilation.moduleGraph.getModule(dep); - // If the module exists and the chunk has a runtime, add the module to externalRequests - if (mod !== null && runtime) { - // Get the runtime chunk from the chunkSet - // If the runtime chunk exists, connect it with the module in the chunk graph - compilation.chunkGraph.connectChunkAndModule( - runtime, - mod, - ); - } - } - } - } - } - } - } - }, - ); - }, - ); - } -} -export default HoistPseudoEager; diff --git a/packages/nextjs-mf/src/plugins/container/RemoveEagerModulesFromRuntimePlugin.ts b/packages/nextjs-mf/src/plugins/container/RemoveEagerModulesFromRuntimePlugin.ts deleted file mode 100644 index cbada7fdf78..00000000000 --- a/packages/nextjs-mf/src/plugins/container/RemoveEagerModulesFromRuntimePlugin.ts +++ /dev/null @@ -1,97 +0,0 @@ -import type { Compiler, Compilation, Chunk, Module } from 'webpack'; - -/** - * This plugin removes eager modules from the runtime. - * @class RemoveEagerModulesFromRuntimePlugin - */ -class RemoveEagerModulesFromRuntimePlugin { - private container: string | undefined; - private debug: boolean; - private modulesToProcess: Set; - - /** - * Creates an instance of RemoveEagerModulesFromRuntimePlugin. - * @param {Object} options - The options for the plugin. - * @param {string} options.container - The container to remove modules from. - * @param {boolean} options.debug - Whether to log debug information. - */ - constructor(options: { container?: string; debug?: boolean }) { - this.container = options.container; - this.debug = options.debug || false; - this.modulesToProcess = new Set(); - } - - /** - * Applies the plugin to the compiler. - * @param {Compiler} compiler - The webpack compiler. - */ - apply(compiler: Compiler) { - if (!this.container) { - console.warn( - '[nextjs-mf]:', - 'RemoveEagerModulesFromRuntimePlugin container is not defined:', - this.container, - ); - return; - } - - compiler.hooks.thisCompilation.tap( - 'RemoveEagerModulesFromRuntimePlugin', - (compilation: Compilation) => { - compilation.hooks.optimizeChunkModules.tap( - 'RemoveEagerModulesFromRuntimePlugin', - (chunks: Iterable, modules: Iterable) => { - for (const chunk of chunks) { - if (chunk.hasRuntime() && chunk.name === this.container) { - this.processModules(compilation, chunk, modules); - } - } - }, - ); - }, - ); - } - - /** - * Processes the modules in the chunk. - * @param {Compilation} compilation - The webpack compilation. - * @param {Chunk} chunk - The chunk to process. - * @param {Iterable} modules - The modules in the chunk. - */ - private processModules( - compilation: Compilation, - chunk: Chunk, - modules: Iterable, - ) { - for (const module of modules) { - if (!compilation.chunkGraph.isModuleInChunk(module, chunk)) { - continue; - } - - if (module.constructor.name === 'NormalModule') { - this.modulesToProcess.add(module); - } - } - - this.removeModules(compilation, chunk); - } - - /** - * Removes the modules from the chunk. - * @param {Compilation} compilation - The webpack compilation. - * @param {Chunk} chunk - The chunk to remove modules from. - */ - private removeModules(compilation: Compilation, chunk: Chunk) { - for (const moduleToRemove of this.modulesToProcess) { - if (this.debug) { - console.log('removing', moduleToRemove.constructor.name); - } - - if (compilation.chunkGraph.isModuleInChunk(moduleToRemove, chunk)) { - compilation.chunkGraph.disconnectChunkAndModule(chunk, moduleToRemove); - } - } - } -} - -export default RemoveEagerModulesFromRuntimePlugin; diff --git a/packages/nextjs-mf/src/plugins/container/runtimePlugin.ts b/packages/nextjs-mf/src/plugins/container/runtimePlugin.ts index 6266ac3ba1e..1451b26952b 100644 --- a/packages/nextjs-mf/src/plugins/container/runtimePlugin.ts +++ b/packages/nextjs-mf/src/plugins/container/runtimePlugin.ts @@ -1,5 +1,4 @@ import { FederationRuntimePlugin } from '@module-federation/runtime/types'; - export default function (): FederationRuntimePlugin { return { name: 'next-internal-plugin', @@ -168,7 +167,6 @@ export default function (): FederationRuntimePlugin { return args; } const { shareScopeMap, scope, pkgName, version, GlobalFederation } = args; - const host = GlobalFederation['__INSTANCES__'][0]; if (!host) { return args; diff --git a/packages/node/src/plugins/EntryChunkTrackerPlugin.ts b/packages/node/src/plugins/EntryChunkTrackerPlugin.ts new file mode 100644 index 00000000000..cc4dec11210 --- /dev/null +++ b/packages/node/src/plugins/EntryChunkTrackerPlugin.ts @@ -0,0 +1,91 @@ +import { normalizeWebpackPath } from '@module-federation/sdk/normalize-webpack-path'; +import type { + Compiler, + Compilation, + Chunk, + sources, + Module, + RuntimeGlobals, + javascript, +} from 'webpack'; +import type { SyncWaterfallHook } from 'tapable'; + +const SortableSet = require( + normalizeWebpackPath('webpack/lib/util/SortableSet'), +) as typeof import('webpack/lib/util/SortableSet'); + +type CompilationHooksJavascriptModulesPlugin = ReturnType< + typeof javascript.JavascriptModulesPlugin.getCompilationHooks +>; +type RenderStartup = CompilationHooksJavascriptModulesPlugin['renderStartup']; + +type InferStartupRenderContext = T extends SyncWaterfallHook< + [infer Source, infer Module, infer StartupRenderContext] +> + ? StartupRenderContext + : never; + +type StartupRenderContext = InferStartupRenderContext; + +export interface Options { + eager?: RegExp | ((module: Module) => boolean); + excludeChunk?: (chunk: Chunk) => boolean; +} + +class EntryChunkTrackerPlugin { + private _options: Options; + + constructor(options?: Options) { + this._options = options || {}; + } + + apply(compiler: Compiler) { + compiler.hooks.thisCompilation.tap( + 'EntryChunkTrackerPlugin', + (compilation: Compilation) => { + this._handleRenderStartup(compiler, compilation); + }, + ); + } + private _handleRenderStartup(compiler: Compiler, compilation: Compilation) { + compiler.webpack.javascript.JavascriptModulesPlugin.getCompilationHooks( + compilation, + ).renderStartup.tap( + 'EntryChunkTrackerPlugin', + ( + source: sources.Source, + _renderContext: Module, + upperContext: StartupRenderContext, + ) => { + if ( + this._options.excludeChunk && + this._options.excludeChunk(upperContext.chunk) + ) { + return source; + } + + const templateString = this._getTemplateString(compiler, source); + + return new compiler.webpack.sources.ConcatSource(templateString); + }, + ); + } + + private _getTemplateString(compiler: Compiler, source: sources.Source) { + const { Template } = compiler.webpack; + return Template.asString([ + `if(typeof module !== 'undefined') { + globalThis.entryChunkCache = globalThis.entryChunkCache || new Set(); + globalThis.entryChunkCache.add(module.filename); + if(module.children) { + module.children.forEach(function(c) { + globalThis.entryChunkCache.add(c.filename); + }) +} + }`, + Template.indent(source.source().toString()), + ]); + } +} + +export default EntryChunkTrackerPlugin; diff --git a/packages/node/src/plugins/NodeFederationPlugin.ts b/packages/node/src/plugins/NodeFederationPlugin.ts index a6b54862453..509e5241da2 100644 --- a/packages/node/src/plugins/NodeFederationPlugin.ts +++ b/packages/node/src/plugins/NodeFederationPlugin.ts @@ -2,7 +2,7 @@ import type { Compiler, container } from 'webpack'; import type { ModuleFederationPluginOptions } from '../types'; - +import EntryChunkTrackerPlugin from './EntryChunkTrackerPlugin'; /** * Interface for NodeFederationOptions which extends ModuleFederationPluginOptions * @interface @@ -61,6 +61,7 @@ class NodeFederationPlugin { webpack, ); new ModuleFederationPlugin(pluginOptions).apply(compiler); + new EntryChunkTrackerPlugin({}).apply(compiler); } private preparePluginOptions(): ModuleFederationPluginOptions { diff --git a/packages/node/src/plugins/webpackChunkUtilities.ts b/packages/node/src/plugins/webpackChunkUtilities.ts index c9f9ec4b020..0969a674a6d 100644 --- a/packages/node/src/plugins/webpackChunkUtilities.ts +++ b/packages/node/src/plugins/webpackChunkUtilities.ts @@ -300,6 +300,7 @@ export function generateLoadScript(runtimeTemplate: any): string { } catch (error) { callback(error); } + }`, `executeLoad(url, callback, chunkId);`, ]), diff --git a/packages/node/src/utils/hot-reload.ts b/packages/node/src/utils/hot-reload.ts index c88341b020a..d1fd4079ce7 100644 --- a/packages/node/src/utils/hot-reload.ts +++ b/packages/node/src/utils/hot-reload.ts @@ -6,7 +6,7 @@ import crypto from 'crypto'; const requireCacheRegex = /(remote|server|hot-reload|react-loadable-manifest|runtime|styled-jsx)/; -export const performReload = (shouldReload: any) => { +export const performReload = async (shouldReload: any) => { if (!shouldReload) { return false; } @@ -21,14 +21,20 @@ export const performReload = (shouldReload: any) => { req = __non_webpack_require__ as NodeRequire; } - Object.keys(req.cache).forEach((key) => { - //delete req.cache[key]; - if (requireCacheRegex.test(key)) { - delete req.cache[key]; - } - }); - const gs = new Function('return globalThis')(); + const entries = Array.from(gs.entryChunkCache || []); + + if (!gs.entryChunkCache) { + Object.keys(req.cache).forEach((key) => { + //delete req.cache[key]; + if (requireCacheRegex.test(key)) { + delete req.cache[key]; + } + }); + } else { + gs.entryChunkCache.clear(); + } + //@ts-ignore __webpack_require__.federation.instance.moduleCache.clear(); gs.__GLOBAL_LOADING_REMOTE_ENTRY__ = {}; @@ -40,6 +46,17 @@ export const performReload = (shouldReload: any) => { } }); gs.__FEDERATION__.__INSTANCES__ = []; + + for (const entry of entries) { + //@ts-ignore + delete __non_webpack_require__.cache[entry]; + } + + //reload entries again + for (const entry of entries) { + await __non_webpack_require__(entry); + } + return true; }; @@ -152,7 +169,7 @@ export const fetchRemote = (remoteScope: any, fetchModule: any) => { }); }; //@ts-ignore -export const revalidate = ( +export const revalidate = async ( fetchModule: any = getFetchModule() || (() => {}), force: boolean = false, ) => { @@ -160,8 +177,10 @@ export const revalidate = ( //@ts-ignore return new Promise((res) => { if (force) { - res(true); - return; + if (Object.keys(hashmap).length !== 0) { + res(true); + return; + } } if (checkMedusaConfigChange(remotesFromAPI, fetchModule)) { res(true); @@ -175,7 +194,7 @@ export const revalidate = ( res(val); }); }).then((shouldReload) => { - return performReload(force || shouldReload); + return performReload(shouldReload); }); }; diff --git a/packages/runtime/src/core.ts b/packages/runtime/src/core.ts index 27022ed4266..55e1fb03291 100644 --- a/packages/runtime/src/core.ts +++ b/packages/runtime/src/core.ts @@ -123,8 +123,10 @@ export class FederationHost { { id: string; name: string; + remote: Remote; remoteSnapshot: ModuleInfo; preloadConfig: PreloadRemoteArgs; + origin: FederationHost; }, void >('handlePreloadModule'), diff --git a/packages/runtime/src/index.ts b/packages/runtime/src/index.ts index 52b06cb4900..7f212f5b8d5 100644 --- a/packages/runtime/src/index.ts +++ b/packages/runtime/src/index.ts @@ -10,6 +10,7 @@ import { assert } from './utils/logger'; export { FederationHost } from './core'; export { registerGlobalPlugins } from './global'; +export { getRemoteEntry, getRemoteInfo } from './utils'; export { loadScript, loadScriptNode } from '@module-federation/sdk'; export type { Federation } from './global'; diff --git a/packages/runtime/src/plugins/generate-preload-assets.ts b/packages/runtime/src/plugins/generate-preload-assets.ts index abce840472b..8d8c4b22459 100644 --- a/packages/runtime/src/plugins/generate-preload-assets.ts +++ b/packages/runtime/src/plugins/generate-preload-assets.ts @@ -213,6 +213,8 @@ export function generatePreloadAssets( name: remoteInfo.name, remoteSnapshot: moduleInfoSnapshot, preloadConfig, + remote: remoteInfo, + origin, }); const preloaded = getPreloaded(exposeFullPath); if (preloaded) { diff --git a/packages/runtime/src/utils/index.ts b/packages/runtime/src/utils/index.ts index e009b248650..3667045c19e 100644 --- a/packages/runtime/src/utils/index.ts +++ b/packages/runtime/src/utils/index.ts @@ -3,3 +3,4 @@ export * from './tool'; export * from './manifest'; export * from './logger'; export * from './plugin'; +export * from './load'; diff --git a/packages/sdk/src/generateSnapshotFromManifest.ts b/packages/sdk/src/generateSnapshotFromManifest.ts index a5325fa4540..6fcea03aeda 100644 --- a/packages/sdk/src/generateSnapshotFromManifest.ts +++ b/packages/sdk/src/generateSnapshotFromManifest.ts @@ -137,6 +137,15 @@ export function generateSnapshotFromManifest( })), }; + if (manifest.metaData?.prefetchInterface) { + const prefetchInterface = manifest.metaData.prefetchInterface; + + basicRemoteSnapshot = { + ...basicRemoteSnapshot, + prefetchInterface, + }; + } + if (manifest.metaData?.prefetchEntry) { const { path, name, type } = manifest.metaData.prefetchEntry; diff --git a/packages/sdk/src/node.ts b/packages/sdk/src/node.ts index 4e4511cf7ef..3fcb7194cd9 100644 --- a/packages/sdk/src/node.ts +++ b/packages/sdk/src/node.ts @@ -57,7 +57,7 @@ export function createScriptNode( try { const script = new vm.Script( `(function(exports, module, require, __dirname, __filename) {${data}\n})`, - { filename }, + filename, ); script.runInThisContext()( scriptContext.exports, diff --git a/packages/sdk/src/types/snapshot.ts b/packages/sdk/src/types/snapshot.ts index 06b7b4b0f31..78e59265ce8 100644 --- a/packages/sdk/src/types/snapshot.ts +++ b/packages/sdk/src/types/snapshot.ts @@ -25,6 +25,7 @@ export interface BasicProviderModuleInfo extends BasicModuleInfo { modulePath?: string; assets: StatsAssets; }>; + prefetchInterface?: boolean; prefetchEntry?: string; prefetchEntryType?: RemoteEntryType; } diff --git a/packages/sdk/src/types/stats.ts b/packages/sdk/src/types/stats.ts index c46b6b7ab12..05dca8653a5 100644 --- a/packages/sdk/src/types/stats.ts +++ b/packages/sdk/src/types/stats.ts @@ -19,6 +19,7 @@ export interface BasicStatsMetaData { globalName: string; buildInfo: StatsBuildInfo; remoteEntry: ResourceInfo; + prefetchInterface?: boolean; prefetchEntry?: ResourceInfo; types: Omit; type: string; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9bace901de4..329a2f21761 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -498,6 +498,9 @@ importers: tailwindcss: specifier: 3.4.1 version: 3.4.1(ts-node@10.9.1) + terser-webpack-plugin: + specifier: ^5.3.10 + version: 5.3.10(@swc/core@1.3.102)(esbuild@0.20.1)(webpack@5.90.3) ts-jest: specifier: 29.1.2 version: 29.1.2(@babel/core@7.24.0)(babel-jest@29.7.0)(esbuild@0.20.1)(jest@29.7.0)(typescript@5.3.3) @@ -615,6 +618,9 @@ importers: typescript: specifier: 5.3.3 version: 5.3.3 + upath: + specifier: 2.0.1 + version: 2.0.1 url: specifier: 0.11.3 version: 0.11.3 @@ -697,6 +703,9 @@ importers: typescript: specifier: 5.3.3 version: 5.3.3 + upath: + specifier: 2.0.1 + version: 2.0.1 url: specifier: 0.11.3 version: 0.11.3 @@ -782,6 +791,9 @@ importers: typescript: specifier: 5.3.3 version: 5.3.3 + upath: + specifier: 2.0.1 + version: 2.0.1 url: specifier: 0.11.3 version: 0.11.3 @@ -4992,7 +5004,7 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@jest/types': 29.6.3 - '@types/node': 18.19.26 + '@types/node': 16.11.68 chalk: 4.1.2 jest-message-util: 29.7.0 jest-util: 29.7.0 @@ -5202,7 +5214,7 @@ packages: dependencies: '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-reports': 3.0.4 - '@types/node': 18.19.26 + '@types/node': 16.11.68 '@types/yargs': 16.0.9 chalk: 4.1.2 dev: true @@ -12452,7 +12464,7 @@ packages: /@types/bonjour@3.5.13: resolution: {integrity: sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ==} dependencies: - '@types/node': 18.19.26 + '@types/node': 16.11.68 dev: true /@types/cacheable-request@6.0.3: @@ -12460,7 +12472,7 @@ packages: dependencies: '@types/http-cache-semantics': 4.0.4 '@types/keyv': 3.1.4 - '@types/node': 18.19.26 + '@types/node': 16.11.68 '@types/responselike': 1.0.3 dev: true @@ -12474,14 +12486,14 @@ packages: /@types/connect@3.4.38: resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} dependencies: - '@types/node': 18.19.26 + '@types/node': 16.11.68 dev: true /@types/conventional-commits-parser@5.0.0: resolution: {integrity: sha512-loB369iXNmAZglwWATL+WRe+CRMmmBPtpolYzIebFaX4YA3x+BEfLqhUAV9WanycKI3TG1IMr5bMJDajDKLlUQ==} requiresBuild: true dependencies: - '@types/node': 18.19.26 + '@types/node': 16.11.68 dev: true optional: true @@ -12585,7 +12597,7 @@ packages: resolution: {integrity: sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==} dependencies: '@types/minimatch': 5.1.2 - '@types/node': 18.19.26 + '@types/node': 16.11.68 dev: true /@types/got@9.6.12: @@ -12599,7 +12611,7 @@ packages: /@types/graceful-fs@4.1.9: resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} dependencies: - '@types/node': 18.19.26 + '@types/node': 16.11.68 dev: true /@types/hast@2.3.10: @@ -12634,7 +12646,7 @@ packages: /@types/http-proxy@1.17.14: resolution: {integrity: sha512-SSrD0c1OQzlFX7pGu1eXxSEjemej64aaNPRhhVYUGqXh0BtldAAx37MG8btcumvpgKyZp1F5Gn3JkktdxiFv6w==} dependencies: - '@types/node': 18.19.26 + '@types/node': 16.11.68 dev: true /@types/is-function@1.0.3: @@ -12686,7 +12698,7 @@ packages: /@types/keyv@3.1.4: resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==} dependencies: - '@types/node': 18.19.26 + '@types/node': 16.11.68 dev: true /@types/loadable__component@5.13.9: @@ -12745,7 +12757,7 @@ packages: /@types/node-forge@1.3.11: resolution: {integrity: sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==} dependencies: - '@types/node': 18.19.26 + '@types/node': 16.11.68 dev: true /@types/node@12.20.55: @@ -12777,7 +12789,7 @@ packages: /@types/npmlog@4.1.6: resolution: {integrity: sha512-0l3z16vnlJGl2Mi/rgJFrdwfLZ4jfNYgE6ZShEpjqhHuGTqdEzNles03NpYHwUMVYZa+Tj46UxKIEpE78lQ3DQ==} dependencies: - '@types/node': 18.19.26 + '@types/node': 16.11.68 dev: true /@types/parse-json@4.0.2: @@ -12843,13 +12855,13 @@ packages: /@types/resolve@1.17.1: resolution: {integrity: sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==} dependencies: - '@types/node': 18.19.26 + '@types/node': 16.11.68 dev: true /@types/responselike@1.0.3: resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==} dependencies: - '@types/node': 18.19.26 + '@types/node': 16.11.68 dev: true /@types/retry@0.12.0: @@ -12867,7 +12879,7 @@ packages: resolution: {integrity: sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==} dependencies: '@types/mime': 1.3.5 - '@types/node': 18.19.26 + '@types/node': 16.11.68 dev: true /@types/serve-index@1.9.4: @@ -12887,7 +12899,7 @@ packages: /@types/set-cookie-parser@2.4.7: resolution: {integrity: sha512-+ge/loa0oTozxip6zmhRIk8Z/boU51wl9Q6QdLZcokIGMzY5lFXYy/x7Htj2HTC6/KZP1hUbZ1ekx8DYXICvWg==} dependencies: - '@types/node': 18.19.26 + '@types/node': 16.11.68 dev: true /@types/sinonjs__fake-timers@8.1.1: @@ -12901,7 +12913,7 @@ packages: /@types/sockjs@0.3.36: resolution: {integrity: sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q==} dependencies: - '@types/node': 18.19.26 + '@types/node': 16.11.68 dev: true /@types/source-list-map@0.1.6: @@ -12955,7 +12967,7 @@ packages: /@types/ws@8.5.10: resolution: {integrity: sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==} dependencies: - '@types/node': 18.19.26 + '@types/node': 16.11.68 dev: true /@types/yargs-parser@21.0.3: @@ -12978,7 +12990,7 @@ packages: resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} requiresBuild: true dependencies: - '@types/node': 18.19.26 + '@types/node': 16.11.68 dev: true optional: true @@ -22312,7 +22324,7 @@ packages: '@jest/expect': 29.7.0 '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 18.19.26 + '@types/node': 16.11.68 chalk: 4.1.2 co: 4.6.0 dedent: 1.5.1 @@ -22476,7 +22488,7 @@ packages: dependencies: '@jest/types': 29.6.3 '@types/graceful-fs': 4.1.9 - '@types/node': 18.19.26 + '@types/node': 16.11.68 anymatch: 3.1.3 fb-watchman: 2.0.2 graceful-fs: 4.2.11 @@ -22582,7 +22594,7 @@ packages: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 18.19.26 + '@types/node': 16.11.68 chalk: 4.1.2 emittery: 0.13.1 graceful-fs: 4.2.11 @@ -22613,7 +22625,7 @@ packages: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 18.19.26 + '@types/node': 16.11.68 chalk: 4.1.2 cjs-module-lexer: 1.2.3 collect-v8-coverage: 1.0.2 @@ -22690,7 +22702,7 @@ packages: dependencies: '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 18.19.26 + '@types/node': 16.11.68 ansi-escapes: 4.3.2 chalk: 4.1.2 emittery: 0.13.1