diff --git a/.changeset/steady-workers-remotes.md b/.changeset/steady-workers-remotes.md new file mode 100644 index 00000000000..d37ee1ea5db --- /dev/null +++ b/.changeset/steady-workers-remotes.md @@ -0,0 +1,5 @@ +--- +"@module-federation/enhanced": patch +--- + +Keep async entry runtime helpers available when cloning runtimes for web workers and other dynamic entrypoints. diff --git a/.env b/.env index 166ab9c8e68..e871d6f9876 100644 --- a/.env +++ b/.env @@ -1 +1,2 @@ NX_DEAMON=false +NX_DAEMON=false diff --git a/.github/workflows/e2e-runtime.yml b/.github/workflows/e2e-runtime.yml index 01b24ddff61..400d9f818ae 100644 --- a/.github/workflows/e2e-runtime.yml +++ b/.github/workflows/e2e-runtime.yml @@ -62,4 +62,4 @@ jobs: - name: E2E Test for Runtime Demo if: steps.check-ci.outcome == 'success' - run: npx kill-port --port 3005,3006,3007 && pnpm run app:runtime:dev & echo "done" && sleep 20 && npx nx run-many --target=test:e2e --projects=3005-runtime-host --parallel=1 && lsof -ti tcp:3005,3006,3007 | xargs kill + run: node tools/scripts/run-runtime-e2e.mjs --mode=dev diff --git a/apps/3000-home/cypress.config.ts b/apps/3000-home/cypress.config.ts index ee8862e00a0..2fe627196b6 100644 --- a/apps/3000-home/cypress.config.ts +++ b/apps/3000-home/cypress.config.ts @@ -1,6 +1,8 @@ import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset'; import { defineConfig } from 'cypress'; +const isCI = !!process.env.CI; + export default defineConfig({ projectId: 'sa6wfn', e2e: { @@ -11,7 +13,7 @@ export default defineConfig({ }, defaultCommandTimeout: 20000, retries: { - runMode: 2, - openMode: 1, + runMode: isCI ? 2 : 0, // Retry in CI, fail fast locally + openMode: isCI ? 1 : 0, }, }); diff --git a/apps/3001-shop/cypress.config.ts b/apps/3001-shop/cypress.config.ts index 48060ad905c..cd895024683 100644 --- a/apps/3001-shop/cypress.config.ts +++ b/apps/3001-shop/cypress.config.ts @@ -1,6 +1,8 @@ import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset'; import { defineConfig } from 'cypress'; +const isCI = !!process.env.CI; + export default defineConfig({ e2e: { ...nxE2EPreset(__filename, { cypressDir: 'cypress' }), @@ -10,7 +12,7 @@ export default defineConfig({ }, defaultCommandTimeout: 10000, retries: { - runMode: 3, - openMode: 2, + runMode: isCI ? 3 : 0, // Retry in CI, fail fast locally + openMode: isCI ? 2 : 0, }, }); diff --git a/apps/3002-checkout/cypress.config.ts b/apps/3002-checkout/cypress.config.ts index 98ecee84114..373b9a501f0 100644 --- a/apps/3002-checkout/cypress.config.ts +++ b/apps/3002-checkout/cypress.config.ts @@ -1,6 +1,8 @@ import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset'; import { defineConfig } from 'cypress'; +const isCI = !!process.env.CI; + export default defineConfig({ e2e: { ...nxE2EPreset(__filename, { cypressDir: 'cypress' }), @@ -10,7 +12,7 @@ export default defineConfig({ }, defaultCommandTimeout: 15000, retries: { - runMode: 2, - openMode: 1, + runMode: isCI ? 2 : 0, // Retry in CI, fail fast locally + openMode: isCI ? 1 : 0, }, }); diff --git a/apps/runtime-demo/3005-runtime-host/cypress/e2e/app.cy.ts b/apps/runtime-demo/3005-runtime-host/cypress/e2e/app.cy.ts index 4383ea93ec1..126c137b961 100644 --- a/apps/runtime-demo/3005-runtime-host/cypress/e2e/app.cy.ts +++ b/apps/runtime-demo/3005-runtime-host/cypress/e2e/app.cy.ts @@ -77,4 +77,21 @@ describe('3005-runtime-host/', () => { }); }); }); + + describe('web worker check', () => { + it('should display value returned from worker', () => { + cy.get('.worker-native-result').should('contain.text', '"answer": "1"'); + cy.get('.worker-native-result').should( + 'contain.text', + '"federationKeys"', + ); + cy.get('.worker-loader-result').should('contain.text', '"answer": "1"'); + cy.get('.worker-loader-result').should( + 'contain.text', + '"federationKeys"', + ); + cy.get('.worker-blob-result').should('contain.text', '"answer": "1"'); + cy.get('.worker-blob-result').should('contain.text', '"federationKeys"'); + }); + }); }); diff --git a/apps/runtime-demo/3005-runtime-host/package.json b/apps/runtime-demo/3005-runtime-host/package.json index 993cae30289..13af902dec8 100644 --- a/apps/runtime-demo/3005-runtime-host/package.json +++ b/apps/runtime-demo/3005-runtime-host/package.json @@ -2,16 +2,25 @@ "name": "runtime-host", "private": true, "version": "0.0.0", + "scripts": { + "build": "webpack --config webpack.config.js --mode production", + "build:development": "webpack --config webpack.config.js --mode development", + "serve": "webpack serve --config webpack.config.js --mode development --host 127.0.0.1 --port 3005 --allowed-hosts all", + "serve:production": "webpack serve --config webpack.config.js --mode production --host 127.0.0.1 --port 3005 --allowed-hosts all --no-hot" + }, "devDependencies": { "@module-federation/core": "workspace:*", + "@module-federation/dts-plugin": "workspace:*", + "@module-federation/enhanced": "workspace:*", "@module-federation/runtime": "workspace:*", "@module-federation/typescript": "workspace:*", - "@module-federation/enhanced": "workspace:*", - "@module-federation/dts-plugin": "workspace:*", "@pmmmwh/react-refresh-webpack-plugin": "0.5.15", - "react-refresh": "0.14.2", "@types/react": "18.3.11", - "@types/react-dom": "18.3.0" + "@types/react-dom": "18.3.0", + "mini-css-extract-plugin": "2.9.2", + "react-refresh": "0.14.2", + "css-loader": "6.11.0", + "webpack-dev-server": "5.1.0" }, "dependencies": { "antd": "4.24.15", diff --git a/apps/runtime-demo/3005-runtime-host/project.json b/apps/runtime-demo/3005-runtime-host/project.json index 4453a83f674..edf63d2c86d 100644 --- a/apps/runtime-demo/3005-runtime-host/project.json +++ b/apps/runtime-demo/3005-runtime-host/project.json @@ -6,35 +6,21 @@ "tags": [], "targets": { "build": { - "executor": "@nx/webpack:webpack", - "outputs": ["{options.outputPath}"], + "executor": "nx:run-commands", + "outputs": ["{projectRoot}/dist"], "defaultConfiguration": "production", "options": { - "compiler": "babel", - "outputPath": "apps/runtime-demo/3005-runtime-host/dist", - "index": "apps/runtime-demo/3005-runtime-host/src/index.html", - "baseHref": "/", - "main": "apps/runtime-demo/3005-runtime-host/src/index.ts", - "tsConfig": "apps/runtime-demo/3005-runtime-host/tsconfig.app.json", - "styles": [], - "scripts": [], - "webpackConfig": "apps/runtime-demo/3005-runtime-host/webpack.config.js", - "babelUpwardRootMode": true + "command": "pnpm run build", + "cwd": "apps/runtime-demo/3005-runtime-host" }, "configurations": { "development": { - "extractLicenses": false, - "optimization": false, - "sourceMap": true, - "vendorChunk": true + "command": "pnpm run build:development", + "cwd": "apps/runtime-demo/3005-runtime-host" }, "production": { - "optimization": true, - "outputHashing": "all", - "sourceMap": false, - "namedChunks": false, - "extractLicenses": false, - "vendorChunk": false + "command": "pnpm run build", + "cwd": "apps/runtime-demo/3005-runtime-host" } }, "dependsOn": [ @@ -45,21 +31,20 @@ ] }, "serve": { - "executor": "@nx/webpack:dev-server", + "executor": "nx:run-commands", "defaultConfiguration": "production", "options": { - "buildTarget": "3005-runtime-host:build", - "hmr": true, - "port": 3005, - "devRemotes": ["3006-runtime-remote"] + "command": "pnpm run serve:production", + "cwd": "apps/runtime-demo/3005-runtime-host" }, "configurations": { "development": { - "buildTarget": "3005-runtime-host:build:development" + "command": "pnpm run serve", + "cwd": "apps/runtime-demo/3005-runtime-host" }, "production": { - "buildTarget": "3005-runtime-host:build:production", - "hmr": false + "command": "pnpm run serve:production", + "cwd": "apps/runtime-demo/3005-runtime-host" } }, "dependsOn": [ diff --git a/apps/runtime-demo/3005-runtime-host/src/Root.tsx b/apps/runtime-demo/3005-runtime-host/src/Root.tsx index ede13ed7b31..f1aa97de46b 100644 --- a/apps/runtime-demo/3005-runtime-host/src/Root.tsx +++ b/apps/runtime-demo/3005-runtime-host/src/Root.tsx @@ -5,6 +5,9 @@ import WebpackPng from './webpack.png'; import WebpackSvg from './webpack.svg'; import { WebpackPngRemote, WebpackSvgRemote } from './Remote1'; import Remote2 from './Remote2'; +import WorkerNativeDemo from './components/WorkerNativeDemo'; +import WorkerLoaderDemo from './components/WorkerLoaderDemo'; +import WorkerBlobDemo from './components/WorkerBlobDemo'; const Root = () => (
@@ -89,6 +92,50 @@ const Root = () => ( + +

check workers

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Test caseExpectedActual
Native new Worker(new URL(...)) +
Expected worker response: 1
+
+ +
worker-loader integration +
Expected worker response: 1
+
+ +
Blob-wrapped module worker importing loader-worker +
Expected worker response: 1
+
+ +
); diff --git a/apps/runtime-demo/3005-runtime-host/src/components/WorkerBlobDemo.tsx b/apps/runtime-demo/3005-runtime-host/src/components/WorkerBlobDemo.tsx new file mode 100644 index 00000000000..f3bcb01f187 --- /dev/null +++ b/apps/runtime-demo/3005-runtime-host/src/components/WorkerBlobDemo.tsx @@ -0,0 +1,61 @@ +import { useEffect, useState } from 'react'; + +export function WorkerBlobDemo() { + const [result, setResult] = useState(null); + const [error, setError] = useState(null); + + useEffect(() => { + let objectUrl: string | undefined; + let worker: Worker | undefined; + + const cleanup = () => { + if (worker) { + worker.terminate(); + } + if (objectUrl) { + URL.revokeObjectURL(objectUrl); + } + }; + + try { + const loaderUrl = new URL('../worker/loader-worker.js', import.meta.url); + const blob = new Blob([`import '${loaderUrl}';`], { + type: 'application/javascript', + }); + + objectUrl = URL.createObjectURL(blob); + worker = new Worker(objectUrl, { + name: 'mf-blob-worker', + type: 'module', + }); + + worker.onmessage = (event) => { + setResult(event.data ?? null); + }; + + worker.onerror = (event) => { + setError((event as unknown as ErrorEvent).message ?? 'Worker error'); + }; + + worker.postMessage({ value: 'foo' }); + } catch (err) { + setError((err as Error).message); + cleanup(); + return cleanup; + } + + return cleanup; + }, []); + + return ( +
+
Expected worker response: 1
+
+        {result ? JSON.stringify(result, null, 2) : 'n/a'}
+      
+ {error ?
Worker error: {error}
: null} +
+ ); +} + +export default WorkerBlobDemo; diff --git a/apps/runtime-demo/3005-runtime-host/src/components/WorkerLoaderDemo.tsx b/apps/runtime-demo/3005-runtime-host/src/components/WorkerLoaderDemo.tsx new file mode 100644 index 00000000000..6206cdf7760 --- /dev/null +++ b/apps/runtime-demo/3005-runtime-host/src/components/WorkerLoaderDemo.tsx @@ -0,0 +1,47 @@ +import { useEffect, useState } from 'react'; + +export function WorkerLoaderDemo() { + const [result, setResult] = useState(null); + const [error, setError] = useState(null); + + useEffect(() => { + try { + const worker = new Worker( + new URL('../worker/loader-worker.js', import.meta.url), + { + name: 'mf-loader-worker', + }, + ); + + worker.onmessage = (event) => { + setResult(event.data ?? null); + }; + + worker.onerror = (event) => { + setError((event as unknown as ErrorEvent).message ?? 'Worker error'); + }; + + worker.postMessage({ value: 'foo' }); + + return () => { + worker.terminate(); + }; + } catch (err) { + setError((err as Error).message); + } + + return undefined; + }, []); + + return ( +
+
Expected worker response: 1
+
+        {result ? JSON.stringify(result, null, 2) : 'n/a'}
+      
+ {error ?
Worker error: {error}
: null} +
+ ); +} + +export default WorkerLoaderDemo; diff --git a/apps/runtime-demo/3005-runtime-host/src/components/WorkerNativeDemo.tsx b/apps/runtime-demo/3005-runtime-host/src/components/WorkerNativeDemo.tsx new file mode 100644 index 00000000000..22b1d381082 --- /dev/null +++ b/apps/runtime-demo/3005-runtime-host/src/components/WorkerNativeDemo.tsx @@ -0,0 +1,46 @@ +import { useEffect, useState } from 'react'; + +export function WorkerNativeDemo() { + const [result, setResult] = useState(null); + const [error, setError] = useState(null); + + useEffect(() => { + try { + const worker = new Worker( + new URL('../worker/native-worker.js', import.meta.url), + { + name: 'mf-native-worker', + }, + ); + + worker.onmessage = (event) => { + setResult(event.data ?? null); + }; + + worker.onerror = (event) => { + setError((event as unknown as ErrorEvent).message ?? 'Worker error'); + }; + + worker.postMessage({ value: 'foo' }); + + return () => { + worker.terminate(); + }; + } catch (err) { + setError((err as Error).message); + } + + return undefined; + }, []); + + return ( +
+
+        {result ? JSON.stringify(result, null, 2) : 'n/a'}
+      
+ {error ?
Worker error: {error}
: null} +
+ ); +} + +export default WorkerNativeDemo; diff --git a/apps/runtime-demo/3005-runtime-host/src/worker/loader-worker.js b/apps/runtime-demo/3005-runtime-host/src/worker/loader-worker.js new file mode 100644 index 00000000000..a1d56ea6735 --- /dev/null +++ b/apps/runtime-demo/3005-runtime-host/src/worker/loader-worker.js @@ -0,0 +1,12 @@ +/* eslint-env worker */ +/* global __webpack_require__ */ +import { workerMap } from './map.js'; + +self.onmessage = (event) => { + const value = event.data && event.data.value; + const federationKeys = Object.keys(__webpack_require__.federation); + self.postMessage({ + answer: workerMap[value] ?? null, + federationKeys, + }); +}; diff --git a/apps/runtime-demo/3005-runtime-host/src/worker/map.js b/apps/runtime-demo/3005-runtime-host/src/worker/map.js new file mode 100644 index 00000000000..f9ab0ec2f3c --- /dev/null +++ b/apps/runtime-demo/3005-runtime-host/src/worker/map.js @@ -0,0 +1,4 @@ +export const workerMap = { + foo: '1', + bar: '2', +}; diff --git a/apps/runtime-demo/3005-runtime-host/src/worker/native-worker.js b/apps/runtime-demo/3005-runtime-host/src/worker/native-worker.js new file mode 100644 index 00000000000..a02258fb43d --- /dev/null +++ b/apps/runtime-demo/3005-runtime-host/src/worker/native-worker.js @@ -0,0 +1,15 @@ +/* eslint-env worker */ +import { workerMap } from './map.js'; + +self.onmessage = (event) => { + const value = event.data && event.data.value; + const federation = + typeof __webpack_require__ !== 'undefined' + ? __webpack_require__.federation || {} + : {}; + const federationKeys = Object.keys(federation); + self.postMessage({ + answer: workerMap[value] ?? null, + federationKeys, + }); +}; diff --git a/apps/runtime-demo/3005-runtime-host/src/worker/worker.js b/apps/runtime-demo/3005-runtime-host/src/worker/worker.js new file mode 100644 index 00000000000..69a165ec73e --- /dev/null +++ b/apps/runtime-demo/3005-runtime-host/src/worker/worker.js @@ -0,0 +1,9 @@ +/* eslint-env worker */ +import { workerMap } from './map'; + +self.onmessage = (event) => { + const value = event.data && event.data.value; + self.postMessage({ + answer: workerMap[value] ?? null, + }); +}; diff --git a/apps/runtime-demo/3005-runtime-host/webpack.config.js b/apps/runtime-demo/3005-runtime-host/webpack.config.js index 05b99f7e4b7..5f5ac54b828 100644 --- a/apps/runtime-demo/3005-runtime-host/webpack.config.js +++ b/apps/runtime-demo/3005-runtime-host/webpack.config.js @@ -1,108 +1,187 @@ const path = require('path'); // const { registerPluginTSTranspiler } = require('nx/src/utils/nx-plugin.js'); // registerPluginTSTranspiler(); +const HtmlWebpackPlugin = require('html-webpack-plugin'); +const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin'); +const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const { ModuleFederationPlugin, } = require('@module-federation/enhanced/webpack'); -const { composePlugins, withNx } = require('@nx/webpack'); -const { withReact } = require('@nx/react'); -module.exports = composePlugins(withNx(), withReact(), (config, context) => { - config.watchOptions = { - ignored: ['**/node_modules/**', '**/@mf-types/**', '**/dist/**'], - }; +const DIST_PATH = path.resolve(__dirname, 'dist'); +const SRC_PATH = path.resolve(__dirname, 'src'); - // const ModuleFederationPlugin = webpack.container.ModuleFederationPlugin; - config.plugins.push( - new ModuleFederationPlugin({ - name: 'runtime_host', - experiments: { asyncStartup: true }, - remotes: { - // remote2: 'runtime_remote2@http://localhost:3007/remoteEntry.js', - remote1: 'runtime_remote1@http://127.0.0.1:3006/mf-manifest.json', - // remote1: `promise new Promise((resolve)=>{ - // const raw = 'runtime_remote1@http://127.0.0.1:3006/remoteEntry.js' - // const [_, remoteUrlWithVersion] = raw.split('@') - // const script = document.createElement('script') - // script.src = remoteUrlWithVersion - // script.onload = () => { - // const proxy = { - // get: (request) => window.runtime_remote1.get(request), - // init: (arg) => { - // try { - // return window.runtime_remote1.init(arg) - // } catch(e) { - // console.log('runtime_remote1 container already initialized') - // } - // } - // } - // resolve(proxy) - // } - // document.head.appendChild(script); - // })`, - }, - // library: { type: 'var', name: 'runtime_remote' }, - filename: 'remoteEntry.js', - exposes: { - './Button': './src/Button.tsx', - }, - dts: { - tsConfigPath: path.resolve(__dirname, 'tsconfig.app.json'), +module.exports = (_env = {}, argv = {}) => { + const mode = argv.mode || process.env.NODE_ENV || 'development'; + const isDevelopment = mode === 'development'; + const isWebpackServe = Boolean( + argv.env?.WEBPACK_SERVE ?? process.env.WEBPACK_SERVE === 'true', + ); + + return { + mode, + devtool: false, + entry: path.join(SRC_PATH, 'index.ts'), + output: { + path: DIST_PATH, + filename: isDevelopment ? '[name].js' : '[name].[contenthash].js', + publicPath: 'auto', + clean: true, + scriptType: 'text/javascript', + environment: { + asyncFunction: true, }, - shareStrategy: 'loaded-first', - shared: { - lodash: { - singleton: true, - requiredVersion: '^4.0.0', + }, + resolve: { + extensions: ['.ts', '.tsx', '.js', '.jsx'], + }, + module: { + rules: [ + { + test: /\.[jt]sx?$/, + exclude: /node_modules/, + use: { + loader: require.resolve('swc-loader'), + options: { + swcrc: false, + sourceMaps: isDevelopment, + jsc: { + parser: { + syntax: 'typescript', + tsx: true, + }, + transform: { + react: { + runtime: 'automatic', + development: isDevelopment, + refresh: isWebpackServe && isDevelopment, + }, + }, + target: 'es2017', + }, + }, + }, + }, + { + test: /\.css$/i, + use: [ + MiniCssExtractPlugin.loader, + { + loader: require.resolve('css-loader'), + options: { + importLoaders: 0, + }, + }, + ], }, - antd: { - singleton: true, - requiredVersion: '^4.0.0', + { + test: /\.(png|svg|jpe?g|gif)$/i, + type: 'asset/resource', }, - react: { - singleton: true, - requiredVersion: '^18.2.0', + ], + }, + plugins: [ + // const ModuleFederationPlugin = webpack.container.ModuleFederationPlugin; + new HtmlWebpackPlugin({ + template: path.join(SRC_PATH, 'index.html'), + }), + new MiniCssExtractPlugin({ + filename: isDevelopment ? '[name].css' : '[name].[contenthash].css', + chunkFilename: isDevelopment ? '[id].css' : '[id].[contenthash].css', + }), + isWebpackServe && isDevelopment && new ReactRefreshWebpackPlugin(), + new ModuleFederationPlugin({ + name: 'runtime_host', + experiments: { asyncStartup: true }, + remotes: { + // remote2: 'runtime_remote2@http://localhost:3007/remoteEntry.js', + remote1: 'runtime_remote1@http://127.0.0.1:3006/mf-manifest.json', + // remote1: `promise new Promise((resolve)=>{ + // const raw = 'runtime_remote1@http://127.0.0.1:3006/remoteEntry.js' + // const [_, remoteUrlWithVersion] = raw.split('@') + // const script = document.createElement('script') + // script.src = remoteUrlWithVersion + // script.onload = () => { + // const proxy = { + // get: (request) => window.runtime_remote1.get(request), + // init: (arg) => { + // try { + // return window.runtime_remote1.init(arg) + // } catch(e) { + // console.log('runtime_remote1 container already initialized') + // } + // } + // } + // resolve(proxy) + // } + // document.head.appendChild(script); + // })`, }, - 'react/': { - singleton: true, - requiredVersion: '^18.2.0', + // library: { type: 'var', name: 'runtime_remote' }, + filename: 'remoteEntry.js', + exposes: { + './Button': './src/Button.tsx', }, - 'react-dom': { - singleton: true, - requiredVersion: '^18.2.0', + dts: { + tsConfigPath: path.resolve(__dirname, 'tsconfig.app.json'), }, - 'react-dom/': { - singleton: true, - requiredVersion: '^18.2.0', + shareStrategy: 'loaded-first', + shared: { + lodash: { + singleton: true, + requiredVersion: '^4.0.0', + }, + antd: { + singleton: true, + requiredVersion: '^4.0.0', + }, + react: { + singleton: true, + requiredVersion: '^18.2.0', + }, + 'react/': { + singleton: true, + requiredVersion: '^18.2.0', + }, + 'react-dom': { + singleton: true, + requiredVersion: '^18.2.0', + }, + 'react-dom/': { + singleton: true, + requiredVersion: '^18.2.0', + }, }, + }), + ].filter(Boolean), + optimization: { + runtimeChunk: false, + minimize: false, + moduleIds: 'named', + }, + performance: { + hints: false, + }, + experiments: { + // Temporary workaround - https://github.com/nrwl/nx/issues/16983 + outputModule: false, + }, + watchOptions: { + ignored: ['**/node_modules/**', '**/@mf-types/**', '**/dist/**'], + }, + devServer: { + host: '127.0.0.1', + allowedHosts: 'all', + headers: { + 'Access-Control-Allow-Origin': '*', }, - }), - ); - if (!config.devServer) { - config.devServer = {}; - } - config.devServer.host = '127.0.0.1'; - config.plugins.forEach((p) => { - if (p.constructor.name === 'ModuleFederationPlugin') { - //Temporary workaround - https://github.com/nrwl/nx/issues/16983 - p._options.library = undefined; - } - }); - - //Temporary workaround - https://github.com/nrwl/nx/issues/16983 - config.experiments = { outputModule: false }; - - // Update the webpack config as needed here. - // e.g. `config.plugins.push(new MyPlugin())` - config.output = { - ...config.output, - scriptType: 'text/javascript', - }; - config.optimization = { - runtimeChunk: false, - minimize: false, - moduleIds: 'named', + port: 3005, + hot: isWebpackServe && isDevelopment, + historyApiFallback: true, + static: DIST_PATH, + devMiddleware: { + writeToDisk: true, + }, + }, }; - // const mf = await withModuleFederation(defaultConfig); - return config; -}); +}; diff --git a/apps/runtime-demo/3006-runtime-remote/package.json b/apps/runtime-demo/3006-runtime-remote/package.json index 405e720e45a..b4c7dd8b0e9 100644 --- a/apps/runtime-demo/3006-runtime-remote/package.json +++ b/apps/runtime-demo/3006-runtime-remote/package.json @@ -2,14 +2,23 @@ "name": "runtime-remote1", "version": "0.0.1", "private": true, + "scripts": { + "build": "webpack --config webpack.config.js --mode production", + "build:development": "webpack --config webpack.config.js --mode development", + "serve": "webpack serve --config webpack.config.js --mode development --host 127.0.0.1 --port 3006 --allowed-hosts all", + "serve:production": "webpack serve --config webpack.config.js --mode production --host 127.0.0.1 --port 3006 --allowed-hosts all --no-hot" + }, "devDependencies": { "@module-federation/core": "workspace:*", "@module-federation/enhanced": "workspace:*", "@module-federation/typescript": "workspace:*", "@pmmmwh/react-refresh-webpack-plugin": "0.5.15", "react-refresh": "0.14.2", + "css-loader": "6.11.0", + "webpack-dev-server": "5.1.0", "@types/react": "18.3.11", - "@types/react-dom": "18.3.0" + "@types/react-dom": "18.3.0", + "mini-css-extract-plugin": "2.9.2" }, "dependencies": { "antd": "4.24.15", diff --git a/apps/runtime-demo/3006-runtime-remote/project.json b/apps/runtime-demo/3006-runtime-remote/project.json index 9ca44a376df..29666d4efe1 100644 --- a/apps/runtime-demo/3006-runtime-remote/project.json +++ b/apps/runtime-demo/3006-runtime-remote/project.json @@ -6,35 +6,21 @@ "tags": [], "targets": { "build": { - "executor": "@nx/webpack:webpack", - "outputs": ["{options.outputPath}"], + "executor": "nx:run-commands", + "outputs": ["{projectRoot}/dist"], "defaultConfiguration": "production", "options": { - "compiler": "babel", - "outputPath": "apps/runtime-demo/3006-runtime-remote/dist", - "index": "apps/runtime-demo/3006-runtime-remote/src/index.html", - "baseHref": "/", - "main": "apps/runtime-demo/3006-runtime-remote/src/index.tsx", - "tsConfig": "apps/runtime-demo/3006-runtime-remote/tsconfig.app.json", - "styles": [], - "scripts": [], - "webpackConfig": "apps/runtime-demo/3006-runtime-remote/webpack.config.js", - "babelUpwardRootMode": true + "command": "pnpm run build", + "cwd": "apps/runtime-demo/3006-runtime-remote" }, "configurations": { "development": { - "extractLicenses": false, - "optimization": false, - "sourceMap": true, - "vendorChunk": true + "command": "pnpm run build:development", + "cwd": "apps/runtime-demo/3006-runtime-remote" }, "production": { - "optimization": true, - "outputHashing": "all", - "sourceMap": false, - "namedChunks": false, - "extractLicenses": false, - "vendorChunk": false + "command": "pnpm run build", + "cwd": "apps/runtime-demo/3006-runtime-remote" } }, "dependsOn": [ @@ -45,12 +31,11 @@ ] }, "serve": { - "executor": "@nx/webpack:dev-server", + "executor": "nx:run-commands", "defaultConfiguration": "production", "options": { - "buildTarget": "3006-runtime-remote:build", - "hmr": true, - "port": 3006 + "command": "pnpm run serve:production", + "cwd": "apps/runtime-demo/3006-runtime-remote" }, "dependsOn": [ { @@ -60,11 +45,12 @@ ], "configurations": { "development": { - "buildTarget": "3006-runtime-remote:build:development" + "command": "pnpm run serve", + "cwd": "apps/runtime-demo/3006-runtime-remote" }, "production": { - "buildTarget": "3006-runtime-remote:build:production", - "hmr": false + "command": "pnpm run serve:production", + "cwd": "apps/runtime-demo/3006-runtime-remote" } } }, diff --git a/apps/runtime-demo/3006-runtime-remote/webpack.config.js b/apps/runtime-demo/3006-runtime-remote/webpack.config.js index 29cde381cb3..6c3cc47b762 100644 --- a/apps/runtime-demo/3006-runtime-remote/webpack.config.js +++ b/apps/runtime-demo/3006-runtime-remote/webpack.config.js @@ -1,37 +1,93 @@ -// const { registerPluginTSTranspiler } = require('nx/src/utils/nx-plugin.js'); -// registerPluginTSTranspiler(); - -const { composePlugins, withNx } = require('@nx/webpack'); -const { withReact } = require('@nx/react'); - const path = require('path'); -// const { withModuleFederation } = require('@nx/react/module-federation'); +const HtmlWebpackPlugin = require('html-webpack-plugin'); +const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin'); +const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const { ModuleFederationPlugin, } = require('@module-federation/enhanced/webpack'); -const packageJson = require('./package.json'); process.env.FEDERATION_DEBUG = true; -module.exports = composePlugins( - withNx(), - withReact(), - async (config, context) => { - config.watchOptions = { - ignored: ['**/node_modules/**', '**/@mf-types/**'], - }; - // const ModuleFederationPlugin = webpack.container.ModuleFederationPlugin; - config.watchOptions = { - ignored: ['**/dist/**'], - }; - if (!config.devServer) { - config.devServer = {}; - } - config.devServer.host = '127.0.0.1'; - config.plugins.push( +const DIST_PATH = path.resolve(__dirname, 'dist'); +const SRC_PATH = path.resolve(__dirname, 'src'); + +module.exports = (_env = {}, argv = {}) => { + const mode = argv.mode || process.env.NODE_ENV || 'development'; + const isDevelopment = mode === 'development'; + const isWebpackServe = Boolean( + argv.env?.WEBPACK_SERVE ?? process.env.WEBPACK_SERVE === 'true', + ); + + return { + mode, + devtool: isDevelopment ? 'source-map' : false, + entry: path.join(SRC_PATH, 'index.tsx'), + output: { + path: DIST_PATH, + filename: isDevelopment ? '[name].js' : '[name].[contenthash].js', + publicPath: 'http://127.0.0.1:3006/', + clean: true, + scriptType: 'text/javascript', + }, + resolve: { + extensions: ['.ts', '.tsx', '.js', '.jsx'], + }, + module: { + rules: [ + { + test: /\.[jt]sx?$/, + exclude: /node_modules/, + use: { + loader: require.resolve('swc-loader'), + options: { + swcrc: false, + sourceMaps: isDevelopment, + jsc: { + parser: { + syntax: 'typescript', + tsx: true, + }, + transform: { + react: { + runtime: 'automatic', + development: isDevelopment, + refresh: isWebpackServe && isDevelopment, + }, + }, + target: 'es2017', + }, + }, + }, + }, + { + test: /\.css$/i, + use: [ + MiniCssExtractPlugin.loader, + { + loader: require.resolve('css-loader'), + options: { + importLoaders: 0, + }, + }, + ], + }, + { + test: /\.(png|svg|jpe?g|gif)$/i, + type: 'asset/resource', + }, + ], + }, + plugins: [ + new HtmlWebpackPlugin({ + template: path.join(SRC_PATH, 'index.html'), + }), + new MiniCssExtractPlugin({ + filename: isDevelopment ? '[name].css' : '[name].[contenthash].css', + chunkFilename: isDevelopment ? '[id].css' : '[id].[contenthash].css', + }), + isWebpackServe && isDevelopment && new ReactRefreshWebpackPlugin(), new ModuleFederationPlugin({ name: 'runtime_remote1', - // library: { type: 'var', name: 'runtime_remote' }, filename: 'remoteEntry.js', exposes: { './useCustomRemoteHook': './src/components/useCustomRemoteHook', @@ -72,63 +128,31 @@ module.exports = composePlugins( tsConfigPath: path.resolve(__dirname, 'tsconfig.app.json'), }, }), - ); - // config.externals={ - // 'react':'React', - // 'react-dom':'ReactDom' - // } - config.optimization.runtimeChunk = false; - config.plugins.forEach((p) => { - if (p.constructor.name === 'ModuleFederationPlugin') { - //Temporary workaround - https://github.com/nrwl/nx/issues/16983 - p._options.library = undefined; - } - }); - - //Temporary workaround - https://github.com/nrwl/nx/issues/16983 - config.experiments = { outputModule: false }; - - // Update the webpack config as needed here. - // e.g. `config.plugins.push(new MyPlugin())` - config.output = { - ...config.output, - publicPath: 'http://127.0.0.1:3006/', - scriptType: 'text/javascript', - }; - config.optimization = { - // ...config.optimization, + ].filter(Boolean), + optimization: { runtimeChunk: false, minimize: false, moduleIds: 'named', - }; - // const mf = await withModuleFederation(defaultConfig); - return config; - /** @type {import('webpack').Configuration} */ - // const parsedConfig = mf(config, context); - - // parsedConfig.plugins.forEach((p) => { - // if (p.constructor.name === 'ModuleFederationPlugin') { - // //Temporary workaround - https://github.com/nrwl/nx/issues/16983 - // p._options.library = undefined; - // } - // }); - - // parsedConfig.devServer = { - // ...(parsedConfig.devServer || {}), - // //Needs to resolve static files from the dist folder (@mf-types) - // static: path.resolve(__dirname, '../../dist/apps/runtime-demo/remote'), - // }; - - // //Temporary workaround - https://github.com/nrwl/nx/issues/16983 - // parsedConfig.experiments = { outputModule: false }; - - // // Update the webpack config as needed here. - // // e.g. `config.plugins.push(new MyPlugin())` - // parsedConfig.output = { - // ...parsedConfig.output, - // scriptType: 'text/javascript', - // }; - - // return parsedConfig; - }, -); + }, + performance: { + hints: false, + }, + experiments: { + outputModule: false, + }, + watchOptions: { + ignored: ['**/node_modules/**', '**/@mf-types/**', '**/dist/**'], + }, + devServer: { + host: '127.0.0.1', + allowedHosts: 'all', + headers: { + 'Access-Control-Allow-Origin': '*', + }, + port: 3006, + hot: isWebpackServe && isDevelopment, + historyApiFallback: true, + static: DIST_PATH, + }, + }; +}; diff --git a/apps/runtime-demo/3007-runtime-remote/package.json b/apps/runtime-demo/3007-runtime-remote/package.json index 747452c27a7..91a75aed6fc 100644 --- a/apps/runtime-demo/3007-runtime-remote/package.json +++ b/apps/runtime-demo/3007-runtime-remote/package.json @@ -2,14 +2,23 @@ "name": "runtime-remote2", "version": "0.0.0", "private": true, + "scripts": { + "build": "webpack --config webpack.config.js --mode production", + "build:development": "webpack --config webpack.config.js --mode development", + "serve": "webpack serve --config webpack.config.js --mode development --host 127.0.0.1 --port 3007 --allowed-hosts all --no-live-reload", + "serve:production": "webpack serve --config webpack.config.js --mode production --host 127.0.0.1 --port 3007 --allowed-hosts all --no-live-reload --no-hot" + }, "devDependencies": { "@module-federation/core": "workspace:*", "@module-federation/enhanced": "workspace:*", "@module-federation/typescript": "workspace:*", "@pmmmwh/react-refresh-webpack-plugin": "0.5.15", "react-refresh": "0.14.2", + "css-loader": "6.11.0", "@types/react": "18.3.11", - "@types/react-dom": "18.3.0" + "@types/react-dom": "18.3.0", + "mini-css-extract-plugin": "2.9.2", + "webpack-dev-server": "5.1.0" }, "dependencies": { "antd": "4.24.15", diff --git a/apps/runtime-demo/3007-runtime-remote/project.json b/apps/runtime-demo/3007-runtime-remote/project.json index 31a6da2c311..c7c2d7263c3 100644 --- a/apps/runtime-demo/3007-runtime-remote/project.json +++ b/apps/runtime-demo/3007-runtime-remote/project.json @@ -6,35 +6,21 @@ "tags": [], "targets": { "build": { - "executor": "@nx/webpack:webpack", - "outputs": ["{options.outputPath}"], + "executor": "nx:run-commands", + "outputs": ["{projectRoot}/dist"], "defaultConfiguration": "production", "options": { - "compiler": "babel", - "outputPath": "apps/runtime-demo/3007-runtime-remote/dist", - "index": "apps/runtime-demo/3007-runtime-remote/src/index.html", - "baseHref": "/", - "main": "apps/runtime-demo/3007-runtime-remote/src/index.tsx", - "tsConfig": "apps/runtime-demo/3007-runtime-remote/tsconfig.app.json", - "styles": [], - "scripts": [], - "webpackConfig": "apps/runtime-demo/3007-runtime-remote/webpack.config.js", - "babelUpwardRootMode": true + "command": "pnpm run build", + "cwd": "apps/runtime-demo/3007-runtime-remote" }, "configurations": { "development": { - "extractLicenses": false, - "optimization": false, - "sourceMap": true, - "vendorChunk": true + "command": "pnpm run build:development", + "cwd": "apps/runtime-demo/3007-runtime-remote" }, "production": { - "optimization": true, - "outputHashing": "all", - "sourceMap": false, - "namedChunks": false, - "extractLicenses": false, - "vendorChunk": false + "command": "pnpm run build", + "cwd": "apps/runtime-demo/3007-runtime-remote" } }, "dependsOn": [ @@ -45,12 +31,11 @@ ] }, "serve": { - "executor": "@nx/webpack:dev-server", + "executor": "nx:run-commands", "defaultConfiguration": "production", "options": { - "buildTarget": "3007-runtime-remote:build", - "hmr": true, - "port": 3007 + "command": "pnpm run serve:production", + "cwd": "apps/runtime-demo/3007-runtime-remote" }, "dependsOn": [ { @@ -60,11 +45,12 @@ ], "configurations": { "development": { - "buildTarget": "3007-runtime-remote:build:development" + "command": "pnpm run serve", + "cwd": "apps/runtime-demo/3007-runtime-remote" }, "production": { - "buildTarget": "3007-runtime-remote:build:production", - "hmr": false + "command": "pnpm run serve:production", + "cwd": "apps/runtime-demo/3007-runtime-remote" } } }, diff --git a/apps/runtime-demo/3007-runtime-remote/webpack.config.js b/apps/runtime-demo/3007-runtime-remote/webpack.config.js index a1d1739431a..7efb638fbb5 100644 --- a/apps/runtime-demo/3007-runtime-remote/webpack.config.js +++ b/apps/runtime-demo/3007-runtime-remote/webpack.config.js @@ -1,26 +1,91 @@ -// const { registerPluginTSTranspiler } = require('nx/src/utils/nx-plugin.js'); -// registerPluginTSTranspiler(); - -const { composePlugins, withNx } = require('@nx/webpack'); -const { withReact } = require('@nx/react'); - const path = require('path'); -// const { withModuleFederation } = require('@nx/react/module-federation'); +const HtmlWebpackPlugin = require('html-webpack-plugin'); +const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin'); +const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const { ModuleFederationPlugin, } = require('@module-federation/enhanced/webpack'); -module.exports = composePlugins( - withNx(), - withReact(), - async (config, context) => { - config.watchOptions = { - ignored: ['**/node_modules/**', '**/@mf-types/**', '**/dist/**'], - }; - config.plugins.push( +const DIST_PATH = path.resolve(__dirname, 'dist'); +const SRC_PATH = path.resolve(__dirname, 'src'); + +module.exports = (_env = {}, argv = {}) => { + const mode = argv.mode || process.env.NODE_ENV || 'development'; + const isDevelopment = mode === 'development'; + const isWebpackServe = Boolean( + argv.env?.WEBPACK_SERVE ?? process.env.WEBPACK_SERVE === 'true', + ); + + return { + mode, + devtool: isDevelopment ? 'source-map' : false, + entry: path.join(SRC_PATH, 'index.tsx'), + output: { + path: DIST_PATH, + filename: isDevelopment ? '[name].js' : '[name].[contenthash].js', + publicPath: 'http://127.0.0.1:3007/', + clean: true, + scriptType: 'text/javascript', + }, + resolve: { + extensions: ['.ts', '.tsx', '.js', '.jsx'], + }, + module: { + rules: [ + { + test: /\.[jt]sx?$/, + exclude: /node_modules/, + use: { + loader: require.resolve('swc-loader'), + options: { + swcrc: false, + sourceMaps: isDevelopment, + jsc: { + parser: { + syntax: 'typescript', + tsx: true, + }, + transform: { + react: { + runtime: 'automatic', + development: isDevelopment, + refresh: isWebpackServe && isDevelopment, + }, + }, + target: 'es2017', + }, + }, + }, + }, + { + test: /\.css$/i, + use: [ + MiniCssExtractPlugin.loader, + { + loader: require.resolve('css-loader'), + options: { + importLoaders: 0, + }, + }, + ], + }, + { + test: /\.(png|svg|jpe?g|gif)$/i, + type: 'asset/resource', + }, + ], + }, + plugins: [ + new HtmlWebpackPlugin({ + template: path.join(SRC_PATH, 'index.html'), + }), + new MiniCssExtractPlugin({ + filename: isDevelopment ? '[name].css' : '[name].[contenthash].css', + chunkFilename: isDevelopment ? '[id].css' : '[id].[contenthash].css', + }), + isWebpackServe && isDevelopment && new ReactRefreshWebpackPlugin(), new ModuleFederationPlugin({ name: 'runtime_remote2', - // library: { type: 'var', name: 'runtime_remote' }, filename: 'remoteEntry.js', exposes: { './ButtonOldAnt': './src/components/ButtonOldAnt', @@ -56,35 +121,32 @@ module.exports = composePlugins( disableLiveReload: true, }, }), - ); - if (!config.devServer) { - config.devServer = {}; - } - config.devServer.host = '127.0.0.1'; - config.optimization.runtimeChunk = false; - config.plugins.forEach((p) => { - if (p.constructor.name === 'ModuleFederationPlugin') { - //Temporary workaround - https://github.com/nrwl/nx/issues/16983 - p._options.library = undefined; - } - }); - - //Temporary workaround - https://github.com/nrwl/nx/issues/16983 - config.experiments = { outputModule: false }; - - // Update the webpack config as needed here. - // e.g. `config.plugins.push(new MyPlugin())` - config.output = { - ...config.output, - publicPath: 'http://127.0.0.1:3007/', - scriptType: 'text/javascript', - }; - config.optimization = { - ...config.optimization, + ].filter(Boolean), + optimization: { runtimeChunk: false, minimize: false, - }; - // const mf = await withModuleFederation(defaultConfig); - return config; - }, -); + moduleIds: 'named', + }, + performance: { + hints: false, + }, + experiments: { + outputModule: false, + }, + watchOptions: { + ignored: ['**/node_modules/**', '**/@mf-types/**', '**/dist/**'], + }, + devServer: { + host: '127.0.0.1', + allowedHosts: 'all', + headers: { + 'Access-Control-Allow-Origin': '*', + }, + port: 3007, + hot: isWebpackServe && isDevelopment, + liveReload: false, + historyApiFallback: true, + static: DIST_PATH, + }, + }; +}; diff --git a/nx.json b/nx.json index 4d62dbd7190..ec518e9c5b5 100644 --- a/nx.json +++ b/nx.json @@ -50,6 +50,14 @@ "cache": false } }, + "tasksRunnerOptions": { + "default": { + "runner": "nx/tasks-runners/default", + "options": { + "useDaemonProcess": false + } + } + }, "namedInputs": { "default": ["{projectRoot}/**/*", "sharedGlobals"], "production": [ @@ -66,6 +74,9 @@ ], "sharedGlobals": ["{workspaceRoot}/babel.config.json"] }, + "tui": { + "enabled": false + }, "workspaceLayout": { "appsDir": "apps", "libsDir": "packages" diff --git a/package.json b/package.json index 5d25b332b76..9b0b5b822b6 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "f": "nx format:write", "enhanced:jest": "pnpm build && cd packages/enhanced && NODE_OPTIONS=--experimental-vm-modules npx jest test/ConfigTestCases.basictest.js test/unit", "lint": "nx run-many --target=lint", - "test": "nx run-many --target=test", + "test": "NX_TUI=false nx run-many --target=test --projects=tag:type:pkg --skip-nx-cache", "build": "NX_TUI=false nx run-many --target=build --parallel=5 --projects=tag:type:pkg", "build:pkg": "NX_TUI=false nx run-many --targets=build --projects=tag:type:pkg --skip-nx-cache", "test:pkg": "NX_TUI=false nx run-many --targets=test --projects=tag:type:pkg --skip-nx-cache", @@ -42,7 +42,7 @@ "app:next:build": "nx run-many --target=build --parallel=2 --configuration=production -p 3000-home,3001-shop,3002-checkout", "app:next:prod": "nx run-many --target=serve --configuration=production -p 3000-home,3001-shop,3002-checkout", "app:node:dev": "nx run-many --target=serve --parallel=10 --configuration=development -p node-host,node-local-remote,node-remote,node-dynamic-remote-new-version,node-dynamic-remote", - "app:runtime:dev": "nx run-many --target=serve -p 3005-runtime-host,3006-runtime-remote,3007-runtime-remote", + "app:runtime:dev": "NX_DAEMON=false nx run-many --target=serve -p 3005-runtime-host,3006-runtime-remote,3007-runtime-remote", "app:manifest:dev": "NX_TUI=false nx run-many --target=serve --configuration=development --parallel=100 -p modernjs,manifest-webpack-host,3009-webpack-provider,3010-rspack-provider,3011-rspack-manifest-provider,3012-rspack-js-entry-provider", "app:manifest:prod": "NX_TUI=false nx run-many --target=serve --configuration=production --parallel=100 -p modernjs,manifest-webpack-host,3009-webpack-provider,3010-rspack-provider,3011-rspack-manifest-provider,3012-rspack-js-entry-provider", "app:ts:dev": "nx run-many --target=serve -p react_ts_host,react_ts_nested_remote,react_ts_remote", diff --git a/packages/enhanced/jest.config.ts b/packages/enhanced/jest.config.ts index e9f78245ad8..a2c1baada04 100644 --- a/packages/enhanced/jest.config.ts +++ b/packages/enhanced/jest.config.ts @@ -1,8 +1,7 @@ /* eslint-disable */ -import { readFileSync, rmdirSync, existsSync } from 'fs'; +import { readFileSync, rmSync, existsSync } from 'fs'; import path from 'path'; import os from 'os'; -const rimraf = require('rimraf'); // Reading the SWC compilation config and remove the "exclude" // for the test files to be compiled by SWC @@ -10,7 +9,10 @@ const { exclude: _, ...swcJestConfig } = JSON.parse( readFileSync(`${__dirname}/.swcrc`, 'utf-8'), ); -rimraf.sync(__dirname + '/test/js'); +const transpiledDir = path.join(__dirname, 'test/js'); +if (existsSync(transpiledDir)) { + rmSync(transpiledDir, { recursive: true, force: 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" @@ -23,13 +25,11 @@ if (swcJestConfig.swcrc === undefined) { // jest needs EsModule Interop to find the default exported setup/teardown functions // swcJestConfig.module.noInterop = false; -const testMatch = []; - -if (process.env['TEST_TYPE'] === 'unit') { - testMatch.push('/test/unit/**/*.test.ts'); -} else { - testMatch.push('/test/*.basictest.js'); -} +const testMatch = [ + '/test/*.basictest.js', + '/test/unit/**/*.test.ts', + '/test/compiler-unit/**/*.test.ts', +]; export default { displayName: 'enhanced', diff --git a/packages/enhanced/jest.embed.ts b/packages/enhanced/jest.embed.ts index 4e09a7c2e17..7dcf0129a28 100644 --- a/packages/enhanced/jest.embed.ts +++ b/packages/enhanced/jest.embed.ts @@ -1,8 +1,7 @@ /* eslint-disable */ -import { readFileSync, rmdirSync, existsSync } from 'fs'; +import { readFileSync, rmSync, existsSync } from 'fs'; import path from 'path'; import os from 'os'; -const rimraf = require('rimraf'); // Reading the SWC compilation config and remove the "exclude" // for the test files to be compiled by SWC @@ -10,7 +9,10 @@ const { exclude: _, ...swcJestConfig } = JSON.parse( readFileSync(`${__dirname}/.swcrc`, 'utf-8'), ); -rimraf.sync(__dirname + '/test/js'); +const transpiledDir = path.join(__dirname, 'test/js'); +if (existsSync(transpiledDir)) { + rmSync(transpiledDir, { recursive: true, force: 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" diff --git a/packages/enhanced/project.json b/packages/enhanced/project.json index 4730f4dbe0d..d3d73607c2d 100644 --- a/packages/enhanced/project.json +++ b/packages/enhanced/project.json @@ -49,11 +49,7 @@ "parallel": false, "commands": [ { - "command": "TEST_TYPE=basic node --expose-gc --max-old-space-size=24576 --experimental-vm-modules --trace-deprecation ./node_modules/jest-cli/bin/jest --logHeapUsage --config packages/enhanced/jest.config.ts --silent", - "forwardAllArgs": false - }, - { - "command": "TEST_TYPE=unit node --expose-gc --max-old-space-size=24576 --experimental-vm-modules --trace-deprecation ./node_modules/jest-cli/bin/jest --logHeapUsage --config packages/enhanced/jest.config.ts --silent", + "command": "node --expose-gc --max-old-space-size=24576 --experimental-vm-modules --trace-deprecation ./node_modules/jest-cli/bin/jest --logHeapUsage --config packages/enhanced/jest.config.ts --silent", "forwardAllArgs": false } ] diff --git a/packages/enhanced/src/lib/container/HoistContainerReferencesPlugin.ts b/packages/enhanced/src/lib/container/HoistContainerReferencesPlugin.ts index 2e0d3e9fff1..d033a57451c 100644 --- a/packages/enhanced/src/lib/container/HoistContainerReferencesPlugin.ts +++ b/packages/enhanced/src/lib/container/HoistContainerReferencesPlugin.ts @@ -10,6 +10,7 @@ import { normalizeWebpackPath } from '@module-federation/sdk/normalize-webpack-p import FederationModulesPlugin from './runtime/FederationModulesPlugin'; import ContainerEntryDependency from './ContainerEntryDependency'; import FederationRuntimeDependency from './runtime/FederationRuntimeDependency'; +import FederationWorkerRuntimeDependency from './runtime/FederationWorkerRuntimeDependency'; import RemoteToExternalDependency from './RemoteToExternalDependency'; import FallbackDependency from './FallbackDependency'; @@ -27,10 +28,10 @@ class HoistContainerReferences implements WebpackPluginInstance { compiler.hooks.thisCompilation.tap( PLUGIN_NAME, (compilation: Compilation) => { - const logger = compilation.getLogger(PLUGIN_NAME); const hooks = FederationModulesPlugin.getCompilationHooks(compilation); const containerEntryDependencies = new Set(); const federationRuntimeDependencies = new Set(); + const workerRuntimeDependencies = new Set(); const remoteDependencies = new Set(); hooks.addContainerEntryDependency.tap( @@ -42,7 +43,11 @@ class HoistContainerReferences implements WebpackPluginInstance { hooks.addFederationRuntimeDependency.tap( 'HoistContainerReferences', (dep: FederationRuntimeDependency) => { - federationRuntimeDependencies.add(dep); + if (dep instanceof FederationWorkerRuntimeDependency) { + workerRuntimeDependencies.add(dep); + } else { + federationRuntimeDependencies.add(dep); + } }, ); hooks.addRemoteDependency.tap( @@ -61,12 +66,16 @@ class HoistContainerReferences implements WebpackPluginInstance { }, (chunks: Iterable) => { const runtimeChunks = this.getRuntimeChunks(compilation); + const debugLogger = compilation.getLogger(`${PLUGIN_NAME}:debug`); + debugLogger.warn( + `container=${containerEntryDependencies.size} runtime=${federationRuntimeDependencies.size} worker=${workerRuntimeDependencies.size} remote=${remoteDependencies.size}`, + ); this.hoistModulesInChunks( compilation, runtimeChunks, - logger, containerEntryDependencies, federationRuntimeDependencies, + workerRuntimeDependencies, remoteDependencies, ); }, @@ -79,13 +88,109 @@ class HoistContainerReferences implements WebpackPluginInstance { private hoistModulesInChunks( compilation: Compilation, runtimeChunks: Set, - logger: ReturnType, containerEntryDependencies: Set, federationRuntimeDependencies: Set, + workerRuntimeDependencies: Set, remoteDependencies: Set, ): void { const { chunkGraph, moduleGraph } = compilation; const allModulesToHoist = new Set(); + const runtimeDependencyChunks = new Set(); + const debugLogger = compilation.getLogger(`${PLUGIN_NAME}:hoist`); + + const runtimeChunkByName = new Map(); + for (const chunk of runtimeChunks) { + const registerName = (name?: string) => { + if (name && !runtimeChunkByName.has(name)) { + runtimeChunkByName.set(name, chunk); + } + }; + + registerName(chunk.name); + + const runtime = (chunk as unknown as { runtime?: Iterable }) + .runtime; + if (runtime && typeof (runtime as any)[Symbol.iterator] === 'function') { + for (const name of runtime as Iterable) { + registerName(name); + } + } + } + + const addRuntimeChunksByName = ( + targetChunks: Set, + runtimeKey: string, + ) => { + const direct = runtimeChunkByName.get(runtimeKey); + if (direct) { + targetChunks.add(direct); + } + + if (runtimeKey.includes('\n')) { + for (const fragment of runtimeKey.split(/\n/)) { + const chunk = runtimeChunkByName.get(fragment); + if (chunk) { + targetChunks.add(chunk); + } + } + } + + const namedChunk = compilation.namedChunks.get(runtimeKey); + if (namedChunk) { + targetChunks.add(namedChunk); + } + }; + + const collectTargetChunks = ( + targetModule: Module, + dependency?: Dependency, + ): Set => { + const targetChunks = new Set(); + + for (const chunk of chunkGraph.getModuleChunks(targetModule)) { + targetChunks.add(chunk); + } + + const moduleRuntimes = chunkGraph.getModuleRuntimes(targetModule); + for (const runtimeSpec of moduleRuntimes) { + compilation.compiler.webpack.util.runtime.forEachRuntime( + runtimeSpec, + (runtimeKey) => { + if (!runtimeKey) { + return; + } + addRuntimeChunksByName(targetChunks, runtimeKey); + }, + ); + } + + if (dependency) { + const parentBlock = moduleGraph.getParentBlock(dependency); + if (parentBlock instanceof AsyncDependenciesBlock) { + const chunkGroup = chunkGraph.getBlockChunkGroup(parentBlock); + if (chunkGroup) { + for (const chunk of chunkGroup.chunks) { + targetChunks.add(chunk); + } + } + } + } + + return targetChunks; + }; + + const connectModulesToChunks = ( + targetChunks: Iterable, + modules: Iterable, + ) => { + for (const chunk of targetChunks) { + for (const module of modules) { + if (!chunkGraph.isModuleInChunk(module, chunk)) { + chunkGraph.connectChunkAndModule(chunk, module); + } + } + } + }; // Process container entry dependencies (needed for nextjs-mf exposed modules) for (const dep of containerEntryDependencies) { @@ -97,27 +202,54 @@ class HoistContainerReferences implements WebpackPluginInstance { 'initial', ); referencedModules.forEach((m: Module) => allModulesToHoist.add(m)); - const moduleRuntimes = chunkGraph.getModuleRuntimes(containerEntryModule); - const runtimes = new Set(); - for (const runtimeSpec of moduleRuntimes) { - compilation.compiler.webpack.util.runtime.forEachRuntime( - runtimeSpec, - (runtimeKey) => { - if (runtimeKey) { - runtimes.add(runtimeKey); - } - }, - ); + const targetChunks = collectTargetChunks(containerEntryModule, dep); + if (targetChunks.size === 0) { + continue; } - for (const runtime of runtimes) { - const runtimeChunk = compilation.namedChunks.get(runtime); - if (!runtimeChunk) continue; - for (const module of referencedModules) { - if (!chunkGraph.isModuleInChunk(module, runtimeChunk)) { - chunkGraph.connectChunkAndModule(runtimeChunk, module); - } - } + + debugLogger.warn( + `container entry modules -> [${Array.from(referencedModules) + .map((module: Module) => + typeof (module as any)?.identifier === 'function' + ? (module as any).identifier() + : (module?.toString?.() ?? '[unknown]'), + ) + .join(', ')}]`, + ); + + connectModulesToChunks(targetChunks, referencedModules); + } + + // Worker federation runtime dependencies may originate from async blocks + // (e.g. web workers). Hoist the full dependency graph to keep worker + // runtimes self-contained. + for (const dep of workerRuntimeDependencies) { + const runtimeModule = moduleGraph.getModule(dep); + if (!runtimeModule) continue; + + const referencedModules = getAllReferencedModules( + compilation, + runtimeModule, + 'all', + ); + referencedModules.forEach((module) => allModulesToHoist.add(module)); + + const targetChunks = collectTargetChunks(runtimeModule, dep); + if (targetChunks.size === 0) { + continue; + } + + for (const chunk of targetChunks) { + runtimeDependencyChunks.add(chunk); } + + debugLogger.warn( + `worker runtime modules -> [${Array.from(targetChunks) + .map((chunk) => chunk.name ?? String(chunk.id ?? '')) + .join(', ')}]`, + ); + + connectModulesToChunks(targetChunks, referencedModules); } // Federation Runtime Dependencies: use 'initial' (not 'all') @@ -130,27 +262,22 @@ class HoistContainerReferences implements WebpackPluginInstance { 'initial', ); referencedModules.forEach((m: Module) => allModulesToHoist.add(m)); - const moduleRuntimes = chunkGraph.getModuleRuntimes(runtimeModule); - const runtimes = new Set(); - for (const runtimeSpec of moduleRuntimes) { - compilation.compiler.webpack.util.runtime.forEachRuntime( - runtimeSpec, - (runtimeKey) => { - if (runtimeKey) { - runtimes.add(runtimeKey); - } - }, - ); + const targetChunks = collectTargetChunks(runtimeModule, dep); + if (targetChunks.size === 0) { + continue; } - for (const runtime of runtimes) { - const runtimeChunk = compilation.namedChunks.get(runtime); - if (!runtimeChunk) continue; - for (const module of referencedModules) { - if (!chunkGraph.isModuleInChunk(module, runtimeChunk)) { - chunkGraph.connectChunkAndModule(runtimeChunk, module); - } - } + + for (const chunk of targetChunks) { + runtimeDependencyChunks.add(chunk); } + + debugLogger.warn( + `runtime modules -> [${Array.from(targetChunks) + .map((chunk) => chunk.name ?? String(chunk.id ?? '')) + .join(', ')}]`, + ); + + connectModulesToChunks(targetChunks, referencedModules); } // Process remote dependencies @@ -163,25 +290,30 @@ class HoistContainerReferences implements WebpackPluginInstance { 'initial', ); referencedRemoteModules.forEach((m: Module) => allModulesToHoist.add(m)); - const remoteModuleRuntimes = chunkGraph.getModuleRuntimes(remoteModule); - const remoteRuntimes = new Set(); - for (const runtimeSpec of remoteModuleRuntimes) { - compilation.compiler.webpack.util.runtime.forEachRuntime( - runtimeSpec, - (runtimeKey) => { - if (runtimeKey) remoteRuntimes.add(runtimeKey); - }, - ); + const targetChunks = collectTargetChunks(remoteModule, remoteDep); + for (const chunk of runtimeDependencyChunks) { + targetChunks.add(chunk); } - for (const runtime of remoteRuntimes) { - const runtimeChunk = compilation.namedChunks.get(runtime); - if (!runtimeChunk) continue; - for (const module of referencedRemoteModules) { - if (!chunkGraph.isModuleInChunk(module, runtimeChunk)) { - chunkGraph.connectChunkAndModule(runtimeChunk, module); - } - } + if (targetChunks.size === 0) { + continue; } + + debugLogger.warn( + `remote modules -> [${Array.from(targetChunks) + .map((chunk) => chunk.name ?? String(chunk.id ?? '')) + .join(', ')}]`, + ); + debugLogger.warn( + `remote module ids -> [${Array.from(referencedRemoteModules) + .map((module: Module) => + typeof (module as any)?.identifier === 'function' + ? (module as any).identifier() + : (module?.toString?.() ?? '[unknown]'), + ) + .join(', ')}]`, + ); + + connectModulesToChunks(targetChunks, referencedRemoteModules); } this.cleanUpChunks(compilation, allModulesToHoist); diff --git a/packages/enhanced/src/lib/container/RemoteRuntimeModule.ts b/packages/enhanced/src/lib/container/RemoteRuntimeModule.ts index d6a3b88b947..f97142721d0 100644 --- a/packages/enhanced/src/lib/container/RemoteRuntimeModule.ts +++ b/packages/enhanced/src/lib/container/RemoteRuntimeModule.ts @@ -136,6 +136,8 @@ class RemoteRuntimeModule extends RuntimeModule { ); return Template.asString([ + `${federationGlobal}.bundlerRuntimeOptions = ${federationGlobal}.bundlerRuntimeOptions || {};`, + `${federationGlobal}.bundlerRuntimeOptions.remotes = ${federationGlobal}.bundlerRuntimeOptions.remotes || {};`, `var chunkMapping = ${JSON.stringify( chunkToRemotesMapping, null, @@ -158,7 +160,9 @@ class RemoteRuntimeModule extends RuntimeModule { `${ RuntimeGlobals.ensureChunkHandlers }.remotes = ${runtimeTemplate.basicFunction('chunkId, promises', [ - `${federationGlobal}.bundlerRuntime.remotes({idToRemoteMap,chunkMapping, idToExternalAndNameMapping, chunkId, promises, webpackRequire:${RuntimeGlobals.require}});`, + `if(${federationGlobal}.bundlerRuntime && ${federationGlobal}.bundlerRuntime.remotes){`, + `\t${federationGlobal}.bundlerRuntime.remotes({idToRemoteMap,chunkMapping, idToExternalAndNameMapping, chunkId, promises, webpackRequire:${RuntimeGlobals.require}});`, + `}`, ])}`, ]); } diff --git a/packages/enhanced/src/lib/container/runtime/ChildCompilationRuntimePlugin.ts b/packages/enhanced/src/lib/container/runtime/ChildCompilationRuntimePlugin.ts deleted file mode 100644 index af5bf1c471c..00000000000 --- a/packages/enhanced/src/lib/container/runtime/ChildCompilationRuntimePlugin.ts +++ /dev/null @@ -1,315 +0,0 @@ -/* - MIT License http://www.opensource.org/licenses/mit-license.php - Author Zackary Jackson @ScriptedAlchemy -*/ - -// This stores the previous child compilation based solution -// it is not currently used - -import { normalizeWebpackPath } from '@module-federation/sdk/normalize-webpack-path'; -import type { Compiler, Compilation, Chunk, Module, ChunkGraph } from 'webpack'; -import { getFederationGlobalScope } from './utils'; -import fs from 'fs'; -import path from 'path'; -import { ConcatSource } from 'webpack-sources'; -import { transformSync } from '@swc/core'; -import { infrastructureLogger as logger } from '@module-federation/sdk'; - -const { RuntimeModule, Template, RuntimeGlobals } = require( - normalizeWebpackPath('webpack'), -) as typeof import('webpack'); - -const onceForCompilationMap = new WeakMap(); -const federationGlobal = getFederationGlobalScope(RuntimeGlobals); - -class RuntimeModuleChunkPlugin { - apply(compiler: Compiler): void { - compiler.hooks.thisCompilation.tap( - 'ModuleChunkFormatPlugin', - (compilation: Compilation) => { - compilation.hooks.optimizeModuleIds.tap( - 'ModuleChunkFormatPlugin', - (modules: Iterable) => { - for (const module of modules) { - const moduleId = compilation.chunkGraph.getModuleId(module); - if (typeof moduleId === 'string') { - compilation.chunkGraph.setModuleId( - module, - `(embed)${moduleId}`, - ); - } else { - compilation.chunkGraph.setModuleId(module, `1000${moduleId}`); - } - } - }, - ); - - const hooks = - compiler.webpack.javascript.JavascriptModulesPlugin.getCompilationHooks( - compilation, - ); - - hooks.renderChunk.tap( - 'ModuleChunkFormatPlugin', - ( - modules: any, - renderContext: { chunk: Chunk; chunkGraph: ChunkGraph }, - ) => { - const { chunk, chunkGraph } = renderContext; - - const source = new ConcatSource(); - source.add('var federation = '); - source.add(modules); - source.add('\n'); - const entries = Array.from( - chunkGraph.getChunkEntryModulesWithChunkGroupIterable(chunk), - ); - for (let i = 0; i < entries.length; i++) { - const [module, _entrypoint] = entries[i]; - const final = i + 1 === entries.length; - const moduleId = chunkGraph.getModuleId(module); - source.add('\n'); - if (final) { - source.add('for (var mod in federation) {\n'); - source.add( - `${RuntimeGlobals.moduleFactories}[mod] = federation[mod];\n`, - ); - source.add('}\n'); - source.add('federation = '); - } - source.add( - `${RuntimeGlobals.require}(${typeof moduleId === 'number' ? moduleId : JSON.stringify(moduleId)});\n`, - ); - } - return source; - }, - ); - }, - ); - } -} - -class CustomRuntimePlugin { - private entryModule?: string | number; - private bundlerRuntimePath: string; - private tempDir: string; - - constructor(path: string, tempDir: string) { - this.bundlerRuntimePath = path.replace('cjs', 'esm'); - this.tempDir = tempDir; - } - - apply(compiler: Compiler): void { - compiler.hooks.make.tapAsync( - 'CustomRuntimePlugin', - (compilation: Compilation, callback: (err?: Error) => void) => { - if (onceForCompilationMap.has(compilation)) return callback(); - onceForCompilationMap.set(compilation, null); - const target = compilation.options.target || 'default'; - const outputPath = path.join( - this.tempDir, - `${target}-custom-runtime-bundle.js`, - ); - - if (fs.existsSync(outputPath)) { - const source = fs.readFileSync(outputPath, 'utf-8'); - onceForCompilationMap.set(compiler, source); - return callback(); - } - - if (onceForCompilationMap.has(compiler)) return callback(); - onceForCompilationMap.set(compiler, null); - - const childCompiler = compilation.createChildCompiler( - 'EmbedFederationRuntimeCompiler', - { - filename: '[name].js', - library: { - type: 'var', - name: 'federation', - export: 'default', - }, - }, - [ - new compiler.webpack.EntryPlugin( - compiler.context, - this.bundlerRuntimePath, - { - name: 'custom-runtime-bundle', - runtime: 'other', - }, - ), - new compiler.webpack.library.EnableLibraryPlugin('var'), - new RuntimeModuleChunkPlugin(), - ], - ); - - childCompiler.context = compiler.context; - childCompiler.options.devtool = undefined; - childCompiler.options.optimization.splitChunks = false; - childCompiler.options.optimization.removeAvailableModules = true; - logger.log('Creating child compiler for', this.bundlerRuntimePath); - - childCompiler.hooks.thisCompilation.tap( - this.constructor.name, - (childCompilation) => { - childCompilation.hooks.processAssets.tap( - this.constructor.name, - () => { - const source = - childCompilation.assets['custom-runtime-bundle.js'] && - (childCompilation.assets[ - 'custom-runtime-bundle.js' - ].source() as string); - - const entry = childCompilation.entrypoints.get( - 'custom-runtime-bundle', - ); - const entryChunk = entry?.getEntrypointChunk(); - - if (entryChunk) { - const entryModule = Array.from( - childCompilation.chunkGraph.getChunkEntryModulesIterable( - entryChunk, - ), - )[0]; - this.entryModule = - childCompilation.chunkGraph.getModuleId(entryModule); - } - - onceForCompilationMap.set(compilation, source); - onceForCompilationMap.set(compiler, source); - fs.writeFileSync(outputPath, source); - logger.log('got compilation asset'); - childCompilation.chunks.forEach((chunk) => { - chunk.files.forEach((file) => { - childCompilation.deleteAsset(file); - }); - }); - }, - ); - }, - ); - childCompiler.runAsChild( - ( - err?: Error | null, - entries?: Chunk[], - childCompilation?: Compilation, - ) => { - if (err) { - return callback(err); - } - - if (!childCompilation) { - logger.warn( - 'Embed Federation Runtime: Child compilation is undefined', - ); - return callback(); - } - - if (childCompilation.errors.length) { - return callback(childCompilation.errors[0]); - } - - logger.log('Code built successfully'); - - callback(); - }, - ); - }, - ); - - compiler.hooks.thisCompilation.tap( - 'CustomRuntimePlugin', - (compilation: Compilation) => { - const handler = (chunk: Chunk, runtimeRequirements: Set) => { - if (chunk.id === 'build time chunk') { - return; - } - if (runtimeRequirements.has('embeddedFederationRuntime')) return; - if (!runtimeRequirements.has(federationGlobal)) { - return; - } - const bundledCode = onceForCompilationMap.get(compilation); - if (!bundledCode) return; - runtimeRequirements.add('embeddedFederationRuntime'); - const runtimeModule = new CustomRuntimeModule( - bundledCode, - this.entryModule, - ); - - compilation.addRuntimeModule(chunk, runtimeModule); - logger.log(`Custom runtime module added to chunk: ${chunk.name}`); - }; - compilation.hooks.runtimeRequirementInTree - .for(federationGlobal) - .tap('CustomRuntimePlugin', handler); - }, - ); - } -} - -class CustomRuntimeModule extends RuntimeModule { - private entryModuleId: string | number | undefined; - - constructor( - private readonly entryPath: string, - entryModuleId: string | number | undefined, - ) { - super('CustomRuntimeModule', RuntimeModule.STAGE_BASIC); - this.entryPath = entryPath; - this.entryModuleId = entryModuleId; - } - - override identifier() { - return 'webpack/runtime/embed/federation'; - } - - override generate(): string { - const runtimeModule = this.entryPath; - const { code: transformedCode } = transformSync( - this.entryPath.replace('var federation;', 'var federation = '), - { - jsc: { - parser: { - syntax: 'ecmascript', - jsx: false, - }, - target: 'es2022', - minify: { - compress: { - unused: true, - dead_code: true, - drop_debugger: true, - }, - mangle: false, - format: { - comments: false, - }, - }, - }, - }, - ); - - return Template.asString([ - runtimeModule, - transformedCode, - `for (var mod in federation) { - ${Template.indent(`${RuntimeGlobals.moduleFactories}[mod] = federation[mod];`)} - }`, - `federation = ${RuntimeGlobals.require}(${JSON.stringify(this.entryModuleId)});`, - `federation = ${RuntimeGlobals.compatGetDefaultExport}(federation)();`, - `var prevFederation = ${federationGlobal}`, - `${federationGlobal} = {}`, - `for (var key in federation) {`, - Template.indent(`${federationGlobal}[key] = federation[key];`), - `}`, - `for (var key in prevFederation) {`, - Template.indent(`${federationGlobal}[key] = prevFederation[key];`), - `}`, - 'federation = undefined;', - ]); - } -} - -export { CustomRuntimePlugin, CustomRuntimeModule, RuntimeModuleChunkPlugin }; diff --git a/packages/enhanced/src/lib/container/runtime/EmbedFederationRuntimeModule.ts b/packages/enhanced/src/lib/container/runtime/EmbedFederationRuntimeModule.ts index da5a72b6451..7332bb83139 100644 --- a/packages/enhanced/src/lib/container/runtime/EmbedFederationRuntimeModule.ts +++ b/packages/enhanced/src/lib/container/runtime/EmbedFederationRuntimeModule.ts @@ -22,8 +22,11 @@ class EmbedFederationRuntimeModule extends RuntimeModule { containerEntrySet: Set< ContainerEntryDependency | FederationRuntimeDependency >, + stage?: number, ) { - super('embed federation', RuntimeModule.STAGE_ATTACH); + // Use provided stage or default to STAGE_ATTACH (10) + // Worker chunks use STAGE_NORMAL - 2 (-2) to run before RemoteRuntimeModule + super('embed federation', stage ?? 10); this.containerEntrySet = containerEntrySet; this._cachedGeneratedCode = undefined; } @@ -61,24 +64,33 @@ class EmbedFederationRuntimeModule extends RuntimeModule { runtimeRequirements: new Set(), }); - const result = Template.asString([ - `var prevStartup = ${RuntimeGlobals.startup};`, - `var hasRun = false;`, - `${RuntimeGlobals.startup} = ${compilation.runtimeTemplate.basicFunction( - '', - [ - `if (!hasRun) {`, - ` hasRun = true;`, - ` ${initRuntimeModuleGetter};`, - `}`, - `if (typeof prevStartup === 'function') {`, - ` return prevStartup();`, - `} else {`, - ` console.warn('[Module Federation] prevStartup is not a function, skipping startup execution');`, - `}`, - ], - )};`, - ]); + // Generate different code based on stage + // Stage 10 (STAGE_ATTACH for JSONP): Load federation entry INSIDE startup hook BEFORE prevStartup() + // Stage 5 (STAGE_BASIC for workers): Load federation entry IMMEDIATELY to initialize bundlerRuntime early + const result = + this.stage === 5 + ? // Worker pattern: Load federation entry immediately + Template.asString([`${initRuntimeModuleGetter};`]) + : // JSONP/default pattern: Load inside startup hook + Template.asString([ + `var prevStartup = ${RuntimeGlobals.startup};`, + `var hasRun = false;`, + `${RuntimeGlobals.startup} = ${compilation.runtimeTemplate.basicFunction( + '', + [ + `if (!hasRun) {`, + ` hasRun = true;`, + ` ${initRuntimeModuleGetter};`, + `}`, + `if (typeof prevStartup === 'function') {`, + ` return prevStartup();`, + `} else {`, + ` console.warn('[Module Federation] prevStartup is not a function, skipping startup execution');`, + `}`, + ], + )};`, + ]); + this._cachedGeneratedCode = result; return result; } diff --git a/packages/enhanced/src/lib/container/runtime/EmbedFederationRuntimePlugin.ts b/packages/enhanced/src/lib/container/runtime/EmbedFederationRuntimePlugin.ts index c1fe93ee994..4bc708d02e5 100644 --- a/packages/enhanced/src/lib/container/runtime/EmbedFederationRuntimePlugin.ts +++ b/packages/enhanced/src/lib/container/runtime/EmbedFederationRuntimePlugin.ts @@ -147,8 +147,38 @@ class EmbedFederationRuntimePlugin { // Mark as embedded and add the runtime module. runtimeRequirements.add('embeddedFederationRuntime'); + + // Determine stage based on chunk loading type + // Following webpack's pattern from JsonpChunkLoadingPlugin and ImportScriptsChunkLoadingPlugin + const options = chunk.getEntryOptions(); + const globalChunkLoading = compilation.outputOptions.chunkLoading; + const chunkLoading = + options && options.chunkLoading !== undefined + ? options.chunkLoading + : globalChunkLoading; + + const { RuntimeModule } = compiler.webpack; + let stage: number; + + if (chunkLoading === 'jsonp') { + // For JSONP chunks (Next.js): use STAGE_ATTACH (10) + // This loads federation entry inside startup hook BEFORE prevStartup() + stage = RuntimeModule.STAGE_ATTACH; + } else if (chunkLoading === 'import-scripts') { + // For worker chunks: use STAGE_BASIC (5) + // Must run AFTER FederationRuntimeModule (stage -1) creates __webpack_require__.federation + // And AFTER RemoteRuntimeModule (stage 0) defines functions that use bundlerRuntime + // But BEFORE ImportScriptsChunkLoadingRuntimeModule (stage 10) calls those functions + stage = RuntimeModule.STAGE_BASIC; + } else { + // For other chunk types (async-node, require, etc): use STAGE_ATTACH (10) + // Same as JSONP - load before prevStartup() + stage = RuntimeModule.STAGE_ATTACH; + } + const runtimeModule = new EmbedFederationRuntimeModule( containerEntrySet, + stage, ); compilation.addRuntimeModule(chunk, runtimeModule); }; diff --git a/packages/enhanced/src/lib/container/runtime/FederationRuntimePlugin.ts b/packages/enhanced/src/lib/container/runtime/FederationRuntimePlugin.ts index 81b82c64623..3c28d8de956 100644 --- a/packages/enhanced/src/lib/container/runtime/FederationRuntimePlugin.ts +++ b/packages/enhanced/src/lib/container/runtime/FederationRuntimePlugin.ts @@ -24,6 +24,7 @@ import EmbedFederationRuntimePlugin from './EmbedFederationRuntimePlugin'; import FederationModulesPlugin from './FederationModulesPlugin'; import HoistContainerReferences from '../HoistContainerReferencesPlugin'; import FederationRuntimeDependency from './FederationRuntimeDependency'; +import FederationWorkerRuntimeDependency from './FederationWorkerRuntimeDependency'; const ModuleDependency = require( normalizeWebpackPath('webpack/lib/dependencies/ModuleDependency'), @@ -35,6 +36,9 @@ const { RuntimeGlobals, Template } = require( const { mkdirpSync } = require( normalizeWebpackPath('webpack/lib/util/fs'), ) as typeof import('webpack/lib/util/fs'); +const WorkerDependency = require( + normalizeWebpackPath('webpack/lib/dependencies/WorkerDependency'), +); const RuntimeToolsPath = require.resolve( '@module-federation/runtime-tools/dist/index.esm.js', @@ -249,6 +253,81 @@ class FederationRuntimePlugin { FederationRuntimeDependency, new ModuleDependency.Template(), ); + compilation.dependencyFactories.set( + FederationWorkerRuntimeDependency, + normalModuleFactory, + ); + compilation.dependencyTemplates.set( + FederationWorkerRuntimeDependency, + FederationWorkerRuntimeDependency.createTemplate(), + ); + + const federationHooks = + FederationModulesPlugin.getCompilationHooks(compilation); + const processedWorkerBlocks = new WeakSet(); + + const ensureWorkerRuntimeDependency = (block: any) => { + if (processedWorkerBlocks.has(block)) { + return; + } + + const dependencies = Array.isArray(block?.dependencies) + ? block.dependencies + : []; + + if ( + dependencies.some( + (dependency: any) => + dependency instanceof FederationWorkerRuntimeDependency, + ) + ) { + processedWorkerBlocks.add(block); + return; + } + + if ( + !dependencies.some( + (dependency: any) => dependency instanceof WorkerDependency, + ) + ) { + return; + } + + this.ensureFile(compiler); + const workerRuntimeDependency = new FederationWorkerRuntimeDependency( + this.entryFilePath, + ); + workerRuntimeDependency.loc = block?.loc; + block.addDependency(workerRuntimeDependency); + federationHooks.addFederationRuntimeDependency.call( + workerRuntimeDependency, + ); + processedWorkerBlocks.add(block); + }; + + const tapParser = (parser: any) => { + parser.hooks.finish.tap( + { name: this.constructor.name, stage: 100 }, + () => { + const currentModule = parser?.state?.module as { + blocks?: any[]; + }; + + if (!currentModule?.blocks?.length) return; + + for (const block of currentModule.blocks) { + ensureWorkerRuntimeDependency(block); + } + }, + ); + }; + + normalModuleFactory.hooks.parser + .for('javascript/auto') + .tap({ name: this.constructor.name, stage: 100 }, tapParser); + normalModuleFactory.hooks.parser + .for('javascript/esm') + .tap({ name: this.constructor.name, stage: 100 }, tapParser); }, ); compiler.hooks.make.tapAsync( diff --git a/packages/enhanced/src/lib/container/runtime/FederationWorkerRuntimeDependency.ts b/packages/enhanced/src/lib/container/runtime/FederationWorkerRuntimeDependency.ts new file mode 100644 index 00000000000..9c2a719c3cf --- /dev/null +++ b/packages/enhanced/src/lib/container/runtime/FederationWorkerRuntimeDependency.ts @@ -0,0 +1,24 @@ +import { normalizeWebpackPath } from '@module-federation/sdk/normalize-webpack-path'; +import FederationRuntimeDependency from './FederationRuntimeDependency'; + +const NullDependency = require( + normalizeWebpackPath('webpack/lib/dependencies/NullDependency'), +); + +/** + * Marker dependency used to differentiate worker runtime injections. + * Delegates chunk wiring to the hoist plugin. + */ +class FederationWorkerRuntimeDependency extends FederationRuntimeDependency { + override get type() { + return 'federation worker runtime dependency'; + } + + static override Template = class WorkerRuntimeDependencyTemplate extends NullDependency.Template {}; + + static createTemplate() { + return new this.Template(); + } +} + +export default FederationWorkerRuntimeDependency; diff --git a/packages/enhanced/test/compiler-unit/container/FederationRuntimePluginHostRuntime.test.ts b/packages/enhanced/test/compiler-unit/container/FederationRuntimePluginHostRuntime.test.ts new file mode 100644 index 00000000000..086c26f13a0 --- /dev/null +++ b/packages/enhanced/test/compiler-unit/container/FederationRuntimePluginHostRuntime.test.ts @@ -0,0 +1,303 @@ +// @ts-nocheck +/* + * @jest-environment node + */ + +import fs from 'fs'; +import os from 'os'; +import path from 'path'; +import type { Stats } from 'webpack'; + +const TEMP_PROJECT_PREFIX = 'mf-host-runtime-'; +const Module = require('module'); +const writeFile = (filePath: string, contents: string) => { + fs.mkdirSync(path.dirname(filePath), { recursive: true }); + fs.writeFileSync(filePath, contents); +}; + +/** + * Ensure Node can resolve workspace-bound packages (e.g. @module-federation/enhanced/webpack) + * when running webpack programmatically from Jest. + */ +function registerModulePaths(projectRoot: string) { + const additionalPaths = Module._nodeModulePaths(projectRoot); + for (const candidate of additionalPaths) { + if (!module.paths.includes(candidate)) { + module.paths.push(candidate); + } + } +} + +async function runWebpack( + projectRoot: string, + mode: 'development' | 'production' = 'development', +): Promise<{ stats: Stats; outputDir: string }> { + registerModulePaths(projectRoot); + const webpack = require('webpack'); + const configPath = path.join(projectRoot, 'webpack.config.js'); + const config = require(configPath); + + const outputDir = fs.mkdtempSync( + path.join(os.tmpdir(), 'mf-host-runtime-output-'), + ); + + config.context = projectRoot; + config.mode = mode; + config.devtool = false; + config.output = { + ...(config.output || {}), + path: outputDir, + clean: true, + }; + + // Disable declaration fetching/manifest networking for test environment + if (Array.isArray(config.plugins)) { + for (const plugin of config.plugins) { + if ( + plugin && + plugin.constructor && + plugin.constructor.name === 'ModuleFederationPlugin' && + plugin._options + ) { + plugin._options.dts = false; + if (plugin._options.manifest !== false) { + plugin._options.manifest = false; + } + } + } + } + + const compiler = webpack(config); + const stats: Stats = await new Promise((resolve, reject) => { + compiler.run((err: Error | null, result?: Stats) => { + if (err) { + reject(err); + return; + } + if (!result) { + reject(new Error('Expected webpack stats result')); + return; + } + resolve(result); + }); + }); + + await new Promise((resolve) => compiler.close(() => resolve())); + + return { stats, outputDir }; +} + +describe('FederationRuntimePlugin host runtime integration', () => { + jest.setTimeout(120000); + + const createdDirs: string[] = []; + + afterAll(() => { + for (const dir of createdDirs) { + if (fs.existsSync(dir)) { + try { + fs.rmSync(dir, { recursive: true, force: true }); + } catch (error) { + // eslint-disable-next-line no-console + console.warn(`Failed to remove temp dir ${dir}`, error); + } + } + } + }); + + const createTempProject = () => { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), TEMP_PROJECT_PREFIX)); + createdDirs.push(tempDir); + return tempDir; + }; + + it('attaches the federation runtime dependency to the host runtime chunk', async () => { + const projectDir = createTempProject(); + const srcDir = path.join(projectDir, 'src'); + + writeFile( + path.join(srcDir, 'index.js'), + ` +import('./bootstrap'); +`, + ); + + writeFile( + path.join(srcDir, 'bootstrap.js'), + ` +import('remoteContainer/feature'); + +export const run = () => 'ok'; +`, + ); + + writeFile( + path.join(projectDir, 'package.json'), + JSON.stringify({ name: 'mf-host-runtime', version: '1.0.0' }), + ); + + writeFile( + path.join(projectDir, 'webpack.config.js'), + ` +const path = require('path'); +const { ModuleFederationPlugin } = require('@module-federation/enhanced/webpack'); + +module.exports = { + mode: 'development', + devtool: false, + target: 'web', + context: __dirname, + entry: { + main: './src/index.js', + }, + output: { + path: path.resolve(__dirname, 'dist'), + filename: '[name].js', + chunkFilename: '[name].js', + publicPath: 'auto', + }, + optimization: { + runtimeChunk: false, + minimize: false, + moduleIds: 'named', + chunkIds: 'named', + }, + plugins: [ + new ModuleFederationPlugin({ + name: 'runtime_host_test', + filename: 'remoteEntry.js', + experiments: { asyncStartup: true }, + remotes: { + remoteContainer: 'remoteContainer@http://127.0.0.1:3006/remoteEntry.js', + }, + exposes: { + './feature': './src/index.js', + }, + shared: { + react: { singleton: true, requiredVersion: false }, + 'react-dom': { singleton: true, requiredVersion: false }, + }, + dts: false, + }), + ], +}; +`, + ); + + // Run webpack - should build successfully even with missing remote + const { stats, outputDir } = await runWebpack(projectDir, 'development'); + createdDirs.push(outputDir); + + expect(stats.hasErrors()).toBe(false); + + // Verify the build includes federation runtime dependencies + const mainBundlePath = path.join(outputDir, 'main.js'); + expect(fs.existsSync(mainBundlePath)).toBe(true); + + const mainContent = fs.readFileSync(mainBundlePath, 'utf-8'); + + // Verify webpack-bundler-runtime is included (the federation runtime dependency) + expect(mainContent).toContain('webpack-bundler-runtime'); + // Verify runtime modules are present + expect(mainContent).toContain('/* webpack/runtime/federation runtime */'); + }); + + it('generates main bundle with correct runtime module execution order', async () => { + const projectDir = createTempProject(); + const srcDir = path.join(projectDir, 'src'); + + writeFile( + path.join(srcDir, 'index.js'), + ` +export const app = 'host-app'; +`, + ); + + writeFile( + path.join(projectDir, 'package.json'), + JSON.stringify({ name: 'mf-host-runtime-order', version: '1.0.0' }), + ); + + writeFile( + path.join(projectDir, 'webpack.config.js'), + ` +const path = require('path'); +const { ModuleFederationPlugin } = require('@module-federation/enhanced/webpack'); + +module.exports = { + mode: 'development', + devtool: false, + target: 'web', + context: __dirname, + entry: { + main: './src/index.js', + }, + output: { + path: path.resolve(__dirname, 'dist'), + filename: '[name].js', + publicPath: 'auto', + environment: { + asyncFunction: true, + }, + }, + optimization: { + runtimeChunk: false, + minimize: false, + moduleIds: 'named', + }, + plugins: [ + new ModuleFederationPlugin({ + name: 'runtime_order_test', + filename: 'remoteEntry.js', + experiments: { asyncStartup: true }, + exposes: { + './app': './src/index.js', + }, + dts: false, + manifest: false, + }), + ], +}; +`, + ); + + const { stats, outputDir } = await runWebpack(projectDir, 'development'); + createdDirs.push(outputDir); + + expect(stats.hasErrors()).toBe(false); + + const mainBundlePath = path.join(outputDir, 'main.js'); + expect(fs.existsSync(mainBundlePath)).toBe(true); + + const mainContent = fs.readFileSync(mainBundlePath, 'utf-8'); + + // Verify federation runtime is embedded + expect(mainContent).toContain('/* webpack/runtime/federation runtime */'); + + // In host apps with asyncStartup, we may or may not have startup chunk dependencies + // depending on whether there are async chunks, but EmbedFederation should always be present + expect(mainContent).toContain('/* webpack/runtime/embed/federation */'); + + // If both are present, verify correct order + if ( + mainContent.includes('/* webpack/runtime/startup chunk dependencies */') + ) { + const startupDepsIndex = mainContent.indexOf( + '/* webpack/runtime/startup chunk dependencies */', + ); + const embedFederationIndex = mainContent.indexOf( + '/* webpack/runtime/embed/federation */', + ); + + expect(startupDepsIndex).toBeGreaterThan(-1); + expect(embedFederationIndex).toBeGreaterThan(-1); + // EmbedFederation (stage 40) should appear AFTER StartupChunkDependencies (stage 20) + expect(embedFederationIndex).toBeGreaterThan(startupDepsIndex); + } + + // Verify the runtime includes startup execution + // The key marker is the presence of __webpack_require__.x() calls which indicate + // the embed federation runtime module is working + expect(mainContent).toContain('__webpack_require__.x('); + }); +}); diff --git a/packages/enhanced/test/compiler-unit/container/FederationRuntimePluginWorkerRuntime.test.ts b/packages/enhanced/test/compiler-unit/container/FederationRuntimePluginWorkerRuntime.test.ts new file mode 100644 index 00000000000..f66067cb35d --- /dev/null +++ b/packages/enhanced/test/compiler-unit/container/FederationRuntimePluginWorkerRuntime.test.ts @@ -0,0 +1,295 @@ +// @ts-nocheck +/* + * @jest-environment node + */ + +import fs from 'fs'; +import os from 'os'; +import path from 'path'; +import type { Compiler, Stats } from 'webpack'; + +const Module = require('module'); +type ChunkSummary = { + files: string[]; + modules: string[]; +}; + +type CompilationCapture = { + chunks: Record; +}; + +class ChunkCapturePlugin { + private readonly projectRoot: string; + private readonly capture: CompilationCapture; + + constructor(projectRoot: string, capture: CompilationCapture) { + this.projectRoot = projectRoot; + this.capture = capture; + } + + apply(compiler: Compiler) { + compiler.hooks.thisCompilation.tap('ChunkCapturePlugin', (compilation) => { + compilation.hooks.afterSeal.tap('ChunkCapturePlugin', () => { + const { chunkGraph } = compilation; + for (const chunk of compilation.chunks) { + const name = chunk.name ?? chunk.id; + const modules = new Set(); + + for (const mod of chunkGraph.getChunkModulesIterable(chunk)) { + const resource = + mod.rootModule && mod.rootModule.resource + ? mod.rootModule.resource + : mod.resource; + if (resource) { + modules.add(path.relative(this.projectRoot, resource)); + } + } + + this.capture.chunks[name] = { + files: Array.from(chunk.files || []).sort(), + modules: Array.from(modules).sort(), + }; + } + }); + }); + } +} + +const ROOT = path.resolve(__dirname, '../../../../..'); +const HOST_APP_ROOT = path.join(ROOT, 'apps/runtime-demo/3005-runtime-host'); + +/** + * Ensure Node can resolve workspace-bound packages (e.g. @module-federation/enhanced/webpack) + * when running webpack programmatically from Jest. + */ +function registerModulePaths(projectRoot: string) { + const additionalPaths = Module._nodeModulePaths(projectRoot); + for (const candidate of additionalPaths) { + if (!module.paths.includes(candidate)) { + module.paths.push(candidate); + } + } +} + +type RunWebpackOptions = { + mode?: 'development' | 'production'; + mutateConfig?: (config: any) => void; +}; + +async function runWebpackProject( + projectRoot: string, + { mode = 'development', mutateConfig }: RunWebpackOptions, +) { + registerModulePaths(projectRoot); + const webpack = require('webpack'); + const configFactory = require(path.join(projectRoot, 'webpack.config.js')); + const config = configFactory({}, { mode }); + const outputDir = fs.mkdtempSync( + path.join(os.tmpdir(), 'mf-worker-runtime-test-'), + ); + + config.context = projectRoot; + config.mode = mode; + config.devtool = false; + config.output = { + ...(config.output || {}), + path: outputDir, + clean: true, + publicPath: 'auto', + }; + + // disable declaration fetching/manifest networking for test environment + if (Array.isArray(config.plugins)) { + for (const plugin of config.plugins) { + if ( + plugin && + plugin.constructor && + plugin.constructor.name === 'ModuleFederationPlugin' && + plugin._options + ) { + plugin._options.dts = false; + if (plugin._options.manifest !== false) { + plugin._options.manifest = false; + } + } + } + } + + if (mutateConfig) { + mutateConfig(config); + } + + const capture: CompilationCapture = { chunks: {} }; + config.plugins = [ + ...(config.plugins || []), + new ChunkCapturePlugin(projectRoot, capture), + ]; + + const compiler = webpack(config); + const stats: Stats = await new Promise((resolve, reject) => { + compiler.run((err: Error | null, result?: Stats) => { + if (err) { + reject(err); + return; + } + if (!result) { + reject(new Error('Expected webpack stats result')); + return; + } + resolve(result); + }); + }); + + await new Promise((resolve) => compiler.close(() => resolve())); + + return { + stats, + outputDir, + capture, + }; +} + +function collectInfrastructureErrors(stats: Stats) { + const compilation: any = stats.compilation; + const errors: { logger: string; message: string }[] = []; + + if (!compilation.logging) { + return errors; + } + + for (const [loggerName, log] of compilation.logging) { + const entries = log?.entries; + if (!entries || !Array.isArray(entries)) { + continue; + } + for (const entry of entries) { + if (entry.type === 'error') { + const message = (entry.args || []).join(' '); + errors.push({ logger: loggerName, message }); + } + } + } + + return errors; +} + +describe('FederationRuntimePlugin worker integration (3005 runtime host)', () => { + jest.setTimeout(120000); + + const tempDirs: string[] = []; + + afterAll(() => { + for (const dir of tempDirs) { + try { + fs.rmSync(dir, { recursive: true, force: true }); + } catch { + // ignore cleanup errors + } + } + }); + + it('emits worker chunks with federated runtime dependencies hoisted', async () => { + // Build the remote app once to ensure assets referenced by the host exist. + const hostResult = await runWebpackProject(HOST_APP_ROOT, { + mode: 'development', + mutateConfig: (config) => { + // keep the build deterministic for assertions + config.optimization = { + ...(config.optimization || {}), + minimize: false, + moduleIds: 'named', + chunkIds: 'named', + }; + }, + }); + + tempDirs.push(hostResult.outputDir); + + // Log errors for debugging if build fails + if (hostResult.stats.hasErrors()) { + const errors = hostResult.stats.compilation.errors.map((err: any) => + String(err.message || err), + ); + console.error('Build errors:', errors.join('\n')); + } + + expect(hostResult.stats.hasErrors()).toBe(false); + + const infraErrors = collectInfrastructureErrors(hostResult.stats); + expect(infraErrors).toStrictEqual([]); + + const chunkEntries = Object.entries(hostResult.capture.chunks); + expect(chunkEntries.length).toBeGreaterThan(0); + + const findChunk = ( + predicate: (pair: [string | number, ChunkSummary]) => boolean, + ) => chunkEntries.find(predicate); + + const runtimeChunk = findChunk(([, summary]) => + summary.modules.some((mod) => + mod.includes('node_modules/.federation/entry'), + ), + ); + + expect(runtimeChunk).toBeDefined(); + + const workerChunks = chunkEntries.filter( + ([chunkName]) => + typeof chunkName === 'string' && + chunkName.includes('mf-') && + chunkName.includes('worker'), + ); + expect(workerChunks.length).toBeGreaterThan(0); + + const runtimeModules = new Set(runtimeChunk![1].modules); + expect( + Array.from(runtimeModules).some((mod) => + mod.includes('packages/runtime/dist/index.esm.js'), + ), + ).toBe(true); + expect( + Array.from(runtimeModules).some((mod) => + mod.includes('packages/webpack-bundler-runtime/dist/index.esm.js'), + ), + ).toBe(true); + + for (const [workerChunkName, summary] of workerChunks) { + const modules = new Set(summary.modules); + expect( + Array.from(modules).some((mod) => + mod.includes('node_modules/.federation/entry'), + ), + ).toBe(true); + expect( + Array.from(modules).some((mod) => + mod.includes('packages/runtime/dist/index.esm.js'), + ), + ).toBe(true); + + // Verify the generated worker bundle has correct runtime module order + const workerFile = summary.files.find((f) => f.endsWith('.js')); + if (workerFile) { + const workerBundlePath = path.join(hostResult.outputDir, workerFile); + const workerContent = fs.readFileSync(workerBundlePath, 'utf-8'); + + // Check that embed federation runtime module is present + expect(workerContent).toContain( + '/* webpack/runtime/embed/federation */', + ); + + // For worker chunks using import-scripts, EmbedFederation uses immediate execution + // (stage 5) to initialize bundlerRuntime before RemoteRuntimeModule functions are called. + // The federation entry should be loaded directly without startup hook wrapper. + const embedFederationIndex = workerContent.indexOf( + '/* webpack/runtime/embed/federation */', + ); + expect(embedFederationIndex).toBeGreaterThan(-1); + + // Verify the federation entry is loaded (either immediately or via startup hook) + // The exact pattern depends on chunk loading type, but the entry must be present + const hasFederationEntry = workerContent.includes('.federation/entry'); + expect(hasFederationEntry).toBe(true); + } + } + }); +}); diff --git a/packages/enhanced/test/compiler-unit/container/HoistContainerReferencesPlugin.test.ts b/packages/enhanced/test/compiler-unit/container/HoistContainerReferencesPlugin.test.ts deleted file mode 100644 index bbaa6d0dc87..00000000000 --- a/packages/enhanced/test/compiler-unit/container/HoistContainerReferencesPlugin.test.ts +++ /dev/null @@ -1,569 +0,0 @@ -// @ts-nocheck - -import { - ModuleFederationPlugin, - dependencies, -} from '@module-federation/enhanced'; -// Import the helper function we need -import { getAllReferencedModules } from '../../../src/lib/container/HoistContainerReferencesPlugin'; -// Use require for webpack as per linter rule -import { normalizeWebpackPath } from '@module-federation/sdk/normalize-webpack-path'; -const webpack = require( - normalizeWebpackPath('webpack'), -) as typeof import('webpack'); -// Use type imports for webpack types -type Compilation = import('webpack').Compilation; -type Module = import('webpack').Module; -type Chunk = import('webpack').Chunk; -import path from 'path'; -import fs from 'fs'; // Use real fs -import os from 'os'; // Use os for temp dir -// Import FederationRuntimeDependency directly -import FederationRuntimeDependency from '../../../src/lib/container/runtime/FederationRuntimeDependency'; - -describe('HoistContainerReferencesPlugin', () => { - let tempDir: string; - let allTempDirs: string[] = []; - - beforeAll(() => { - // Track all temp directories for final cleanup - allTempDirs = []; - }); - - beforeEach(() => { - // Create temp dir before each test - tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'hoist-test-')); - allTempDirs.push(tempDir); - }); - - afterEach((done) => { - // Clean up temp dir after each test - if (tempDir && fs.existsSync(tempDir)) { - // Add a small delay to allow file handles to be released - setTimeout(() => { - try { - fs.rmSync(tempDir, { recursive: true, force: true }); - done(); - } catch (error) { - console.warn(`Failed to clean up temp directory ${tempDir}:`, error); - // Try alternative cleanup method - try { - fs.rmdirSync(tempDir, { recursive: true }); - done(); - } catch (fallbackError) { - console.error( - `Fallback cleanup also failed for ${tempDir}:`, - fallbackError, - ); - done(); - } - } - }, 100); // 100ms delay to allow file handles to close - } else { - done(); - } - }); - - afterAll(() => { - // Final cleanup of any remaining temp directories - allTempDirs.forEach((dir) => { - if (fs.existsSync(dir)) { - try { - fs.rmSync(dir, { recursive: true, force: true }); - } catch (error) { - console.warn(`Final cleanup failed for ${dir}:`, error); - } - } - }); - allTempDirs = []; - }); - - it('should hoist container runtime modules into the single runtime chunk when using remotes', (done) => { - // Define input file content - const mainJsContent = ` - import('remoteApp/utils') - .then(utils => console.log('Loaded remote utils:', utils)) - .catch(err => console.error('Error loading remote:', err)); - console.log('Host application started'); - `; - const packageJsonContent = '{ "name": "test-host", "version": "1.0.0" }'; - - // Write input files to tempDir - fs.writeFileSync(path.join(tempDir, 'main.js'), mainJsContent); - fs.writeFileSync(path.join(tempDir, 'package.json'), packageJsonContent); - - const outputPath = path.join(tempDir, 'dist'); - - const compiler = webpack({ - mode: 'development', - devtool: false, - context: tempDir, // Use tempDir as context - entry: { - main: './main.js', - }, - output: { - path: outputPath, // Use outputPath - filename: '[name].js', - chunkFilename: 'chunks/[name].[contenthash].js', - uniqueName: 'hoist-remote-test', - publicPath: 'auto', // Important for MF remotes - }, - optimization: { - runtimeChunk: 'single', // Critical for this test - chunkIds: 'named', - moduleIds: 'named', - }, - plugins: [ - new ModuleFederationPlugin({ - name: 'host', - remotes: { - // Define a remote - remoteApp: 'remoteApp@http://localhost:3001/remoteEntry.js', - }, - // No exposes or shared needed for this specific test - }), - ], - }); - - // Remove compiler fs assignments - - compiler.run((err, stats) => { - try { - if (err) { - return done(err); - } - if (!stats) { - return done(new Error('No stats object returned')); - } - if (stats.hasErrors()) { - // Add more detailed error logging - const info = stats.toJson({ - errorDetails: true, - all: false, - errors: true, - }); - console.error( - 'Webpack Errors:', - JSON.stringify(info.errors, null, 2), - ); - return done( - new Error( - info.errors - ?.map((e) => e.message + (e.details ? `\n${e.details}` : '')) - .join('\n'), - ), - ); - } - if (stats.hasWarnings()) { - console.warn( - 'Webpack Warnings:', - stats.toString({ colors: true, all: false, warnings: true }), - ); - } - - const compilation = stats.compilation; - const { chunkGraph, moduleGraph } = compilation; - - // 1. Find the runtime chunk - const runtimeChunk = Array.from(compilation.chunks).find( - (c: Chunk) => c.hasRuntime() && c.name === 'runtime', - ); - expect(runtimeChunk).toBeDefined(); - if (!runtimeChunk) return done(new Error('Runtime chunk not found')); - - // 2. Find the module that was created from FederationRuntimeDependency - let federationRuntimeModule: Module | null = null; - for (const module of compilation.modules) { - if (module.constructor.name === 'FederationRuntimeModule') { - federationRuntimeModule = module; - break; - } - } - expect(federationRuntimeModule).toBeDefined(); - if (!federationRuntimeModule) - return done( - new Error( - 'Module originating FederationRuntimeDependency not found', - ), - ); - - // 3. Assert the Federation Runtime Module is in the Runtime Chunk - const isRuntimeModuleInRuntime = chunkGraph.isModuleInChunk( - federationRuntimeModule, - runtimeChunk, - ); - expect(isRuntimeModuleInRuntime).toBe(true); - - // 4. Assert the Federation Runtime Module is NOT in the Main Chunk (if separate) - const mainChunk = Array.from(compilation.chunks).find( - (c: Chunk) => c.name === 'main', - ); - if (mainChunk && mainChunk !== runtimeChunk) { - const isRuntimeModuleInMain = chunkGraph.isModuleInChunk( - federationRuntimeModule, - mainChunk, - ); - expect(isRuntimeModuleInMain).toBe(false); - } - - // 5. Verify file output (Optional) - const runtimeFilePath = path.join(outputPath, 'runtime.js'); - expect(fs.existsSync(runtimeFilePath)).toBe(true); - - // Close compiler to release file handles - compiler.close(() => { - done(); - }); - } catch (e) { - // Close compiler even on error - compiler.close(() => { - done(e); - }); - } - }); - }); - - it('should NOT hoist container entry but hoist its deps when using exposes', (done) => { - // Define input file content - const mainJsContent = ` - console.log('Host application started, loading exposed module...'); - `; // Main entry might need to interact with container - const exposedJsContent = `export default () => 'exposed module content';`; - const packageJsonContent = - '{ "name": "test-host-exposes", "version": "1.0.0" }'; - - // Write input files to tempDir - fs.writeFileSync(path.join(tempDir, 'main.js'), mainJsContent); - fs.writeFileSync(path.join(tempDir, 'exposed.js'), exposedJsContent); - fs.writeFileSync(path.join(tempDir, 'package.json'), packageJsonContent); - - const outputPath = path.join(tempDir, 'dist'); - - const compiler = webpack({ - mode: 'development', - devtool: false, - context: tempDir, - entry: { - main: './main.js', - }, - output: { - path: outputPath, - filename: '[name].js', - chunkFilename: 'chunks/[name].[contenthash].js', - uniqueName: 'hoist-expose-test', - publicPath: 'auto', - }, - optimization: { - runtimeChunk: 'single', - chunkIds: 'named', - moduleIds: 'named', - }, - plugins: [ - new ModuleFederationPlugin({ - name: 'host_exposes', - filename: 'container.js', // Explicit container entry - exposes: { - './exposed': './exposed.js', // Expose a module - }, - // No remotes needed for this specific test - }), - ], - }); - - compiler.run((err, stats) => { - try { - if (err) return done(err); - if (!stats) return done(new Error('No stats object returned')); - if (stats.hasErrors()) { - const info = stats.toJson({ - errorDetails: true, - all: false, - errors: true, - }); - console.error( - 'Webpack Errors:', - JSON.stringify(info.errors, null, 2), - ); - return done( - new Error( - info.errors - ?.map((e) => e.message + (e.details ? `\n${e.details}` : '')) - .join('\n'), - ), - ); - } - if (stats.hasWarnings()) { - console.warn( - 'Webpack Warnings:', - stats.toString({ colors: true, all: false, warnings: true }), - ); - } - - const compilation = stats.compilation; - const { chunkGraph, moduleGraph } = compilation; - - // 1. Find the runtime chunk - const runtimeChunk = Array.from(compilation.chunks).find( - (c: Chunk) => c.hasRuntime() && c.name === 'runtime', - ); - expect(runtimeChunk).toBeDefined(); - if (!runtimeChunk) return done(new Error('Runtime chunk not found')); - - // 2. Find the Container Entry Module that was created from ContainerEntryDependency - let containerEntryModule: Module | null = null; - for (const module of compilation.modules) { - if (module.constructor.name === 'ContainerEntryModule') { - containerEntryModule = module; - break; - } - } - expect(containerEntryModule).toBeDefined(); - if (!containerEntryModule) - return done( - new Error('ContainerEntryModule not found via dependency check'), - ); - - // 3. Find the exposed module itself - const exposedModule = Array.from(compilation.modules).find((m) => - m.identifier().endsWith('exposed.js'), - ); - expect(exposedModule).toBeDefined(); - if (!exposedModule) - return done(new Error('Exposed module (exposed.js) not found')); - - // 4. Get all modules referenced by the container entry - const referencedModules = getAllReferencedModules( - compilation, - containerEntryModule, - 'all', - ); - expect(referencedModules.size).toBeGreaterThan(1); // container + exposed + runtime helpers - - // 5. Assert container entry itself is NOT in the runtime chunk - const isContainerInRuntime = chunkGraph.isModuleInChunk( - containerEntryModule, - runtimeChunk, - ); - expect(isContainerInRuntime).toBe(false); - - // 6. Assert the exposed module is NOT in the runtime chunk - const isExposedInRuntime = chunkGraph.isModuleInChunk( - exposedModule, - runtimeChunk, - ); - expect(isExposedInRuntime).toBe(false); - - // 7. Assert ALL OTHER referenced modules (runtime helpers) ARE in the runtime chunk - let hoistedCount = 0; - for (const module of referencedModules) { - // Skip the container entry and the actual exposed module - if (module === containerEntryModule || module === exposedModule) - continue; - - const isModuleInRuntime = chunkGraph.isModuleInChunk( - module, - runtimeChunk, - ); - expect(isModuleInRuntime).toBe(true); - if (isModuleInRuntime) { - hoistedCount++; - } - } - // Ensure at least one runtime helper module was found and hoisted - expect(hoistedCount).toBeGreaterThan(0); - - // 8. Verify file output (optional) - const runtimeFilePath = path.join(outputPath, 'runtime.js'); - const containerFilePath = path.join(outputPath, 'container.js'); - expect(fs.existsSync(runtimeFilePath)).toBe(true); - expect(fs.existsSync(containerFilePath)).toBe(true); - - // Close compiler to release file handles - compiler.close(() => { - done(); - }); - } catch (e) { - // Close compiler even on error - compiler.close(() => { - done(e); - }); - } - }); - }); - - xit('should hoist container runtime modules into the single runtime chunk when using remotes with federationRuntimeOriginModule', (done) => { - // Define input file content - const mainJsContent = ` - import('remoteApp/utils') - .then(utils => console.log('Loaded remote utils:', utils)) - .catch(err => console.error('Error loading remote:', err)); - console.log('Host application started'); - `; - const packageJsonContent = '{ "name": "test-host", "version": "1.0.0" }'; - - // Write input files to tempDir - fs.writeFileSync(path.join(tempDir, 'main.js'), mainJsContent); - fs.writeFileSync(path.join(tempDir, 'package.json'), packageJsonContent); - - const outputPath = path.join(tempDir, 'dist'); - - const compiler = webpack({ - mode: 'development', - devtool: false, - context: tempDir, // Use tempDir as context - entry: { - main: './main.js', - }, - output: { - path: outputPath, // Use outputPath - filename: '[name].js', - chunkFilename: 'chunks/[name].[contenthash].js', - uniqueName: 'hoist-remote-test', - publicPath: 'auto', // Important for MF remotes - }, - optimization: { - runtimeChunk: 'single', // Critical for this test - chunkIds: 'named', - moduleIds: 'named', - }, - plugins: [ - new ModuleFederationPlugin({ - name: 'host', - remotes: { - // Define a remote - remoteApp: 'remoteApp@http://localhost:3001/remoteEntry.js', - }, - // No exposes or shared needed for this specific test - }), - ], - }); - - // Remove compiler fs assignments - - compiler.run((err, stats) => { - try { - if (err) { - return done(err); - } - if (!stats) { - return done(new Error('No stats object returned')); - } - if (stats.hasErrors()) { - // Add more detailed error logging - const info = stats.toJson({ - errorDetails: true, - all: false, - errors: true, - }); - console.error( - 'Webpack Errors:', - JSON.stringify(info.errors, null, 2), - ); - return done( - new Error( - info.errors - ?.map((e) => e.message + (e.details ? `\n${e.details}` : '')) - .join('\n'), - ), - ); - } - if (stats.hasWarnings()) { - console.warn( - 'Webpack Warnings:', - stats.toString({ colors: true, all: false, warnings: true }), - ); - } - - const compilation = stats.compilation; - const { chunkGraph, moduleGraph } = compilation; - - // 1. Find the runtime chunk (using Array.from) - const runtimeChunk = Array.from(compilation.chunks).find( - (c: Chunk) => c.hasRuntime() && c.name === 'runtime', - ); - expect(runtimeChunk).toBeDefined(); - if (!runtimeChunk) return done(new Error('Runtime chunk not found')); - - // 2. Verify runtime chunk content directly - const runtimeFilePath = path.join(outputPath, 'runtime.js'); - expect(fs.existsSync(runtimeFilePath)).toBe(true); - const runtimeFileContent = fs.readFileSync(runtimeFilePath, 'utf-8'); - - // Check for presence of key MF runtime identifiers - const mfRuntimeKeywords = [ - '__webpack_require__.f.remotes', // Function for handling remotes - '__webpack_require__.S = ', // Share scope object - 'initializeSharing', // Function name for initializing sharing - '__webpack_require__.I = ', // Function for initializing consumes - ]; - - for (const keyword of mfRuntimeKeywords) { - expect(runtimeFileContent).toContain(keyword); - } - - // 3. Verify absence in main chunk (if separate) - const mainChunk = Array.from(compilation.chunks).find( - (c: Chunk) => c.name === 'main', - ); - if (mainChunk && mainChunk !== runtimeChunk) { - const mainFilePath = path.join(outputPath, 'main.js'); - expect(fs.existsSync(mainFilePath)).toBe(true); - const mainFileContent = fs.readFileSync(mainFilePath, 'utf-8'); - for (const keyword of mfRuntimeKeywords) { - expect(mainFileContent).not.toContain(keyword); - } - } - - // 4. Verify container file output (if applicable, not expected here) - const containerFilePath = path.join(outputPath, 'container.js'); // Filename was removed from config - // In remotes-only mode without filename, container.js might not exist or be empty - // expect(fs.existsSync(containerFilePath)).toBe(true); - - // 5. Find the federationRuntimeModule - let federationRuntimeModule: Module | null = null; - for (const module of compilation.modules) { - if (module.constructor.name === 'FederationRuntimeModule') { - federationRuntimeModule = module; - break; - } - } - expect(federationRuntimeModule).toBeDefined(); - if (!federationRuntimeModule) - return done( - new Error( - 'Module created from FederationRuntimeDependency not found', - ), - ); - - // 6. Assert the Federation Runtime Module is in the Runtime Chunk - const isRuntimeModuleInRuntime = chunkGraph.isModuleInChunk( - federationRuntimeModule, - runtimeChunk, - ); - expect(isRuntimeModuleInRuntime).toBe(true); - - // 7. Assert the Federation Runtime Module is NOT in the Main Chunk (if separate) - if (mainChunk && mainChunk !== runtimeChunk) { - const isRuntimeModuleInMain = chunkGraph.isModuleInChunk( - federationRuntimeModule, - mainChunk, - ); - expect(isRuntimeModuleInMain).toBe(false); - } - - // 8. Verify file output using real fs paths (Optional, but still useful) - expect(fs.existsSync(runtimeFilePath)).toBe(true); - - // Close compiler to release file handles - compiler.close(() => { - done(); - }); - } catch (e) { - // Close compiler even on error - compiler.close(() => { - done(e); - }); - } - }); - }); -}); diff --git a/packages/enhanced/test/compiler-unit/sharing/SharePlugin.memfs.test.ts b/packages/enhanced/test/compiler-unit/sharing/SharePlugin.memfs.test.ts index 5a52278656f..2c12387ca7f 100644 --- a/packages/enhanced/test/compiler-unit/sharing/SharePlugin.memfs.test.ts +++ b/packages/enhanced/test/compiler-unit/sharing/SharePlugin.memfs.test.ts @@ -66,8 +66,10 @@ describe('SharePlugin smoke (memfs)', () => { const provideOpts = (ProvideSharedPlugin as jest.Mock).mock.calls[0][0]; expect(consumeOpts.shareScope).toBe('default'); expect(Array.isArray(consumeOpts.consumes)).toBe(true); + expect(consumeOpts.consumes).toHaveLength(3); expect(provideOpts.shareScope).toBe('default'); expect(Array.isArray(provideOpts.provides)).toBe(true); + expect(provideOpts.provides).toHaveLength(3); // Simulate compilation lifecycle const compilation = createMemfsCompilation(compiler as any); diff --git a/packages/enhanced/test/compiler-unit/sharing/SharePlugin.test.ts b/packages/enhanced/test/compiler-unit/sharing/SharePlugin.test.ts index e88d49a3e95..739cd26de87 100644 --- a/packages/enhanced/test/compiler-unit/sharing/SharePlugin.test.ts +++ b/packages/enhanced/test/compiler-unit/sharing/SharePlugin.test.ts @@ -88,6 +88,37 @@ describe('SharePlugin Compiler Integration', () => { expect(consumeInstance.apply).toHaveBeenCalledWith(mockCompiler); expect(provideInstance.apply).toHaveBeenCalledWith(mockCompiler); + + const consumeOpts = (ConsumeSharedPlugin as jest.Mock).mock.calls[0][0]; + const provideOpts = (ProvideSharedPlugin as jest.Mock).mock.calls[0][0]; + expect(consumeOpts.consumes).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + react: expect.objectContaining({ + shareKey: 'react', + request: 'react', + requiredVersion: '^17.0.0', + }), + }), + expect.objectContaining({ + lodash: expect.objectContaining({ + singleton: true, + shareKey: 'lodash', + request: 'lodash', + }), + }), + ]), + ); + expect(provideOpts.provides).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + react: expect.objectContaining({ shareKey: 'react' }), + }), + expect.objectContaining({ + lodash: expect.objectContaining({ shareKey: 'lodash' }), + }), + ]), + ); }); it('should handle advanced configuration with filters', () => { @@ -228,6 +259,22 @@ describe('SharePlugin Compiler Integration', () => { // Should not throw during plugin application expect(() => plugin.apply(mockCompiler)).not.toThrow(); + + const consumeOpts = ( + ConsumeSharedPlugin as jest.Mock + ).mock.calls.pop()?.[0]; + const provideOpts = ( + ProvideSharedPlugin as jest.Mock + ).mock.calls.pop()?.[0]; + expect(consumeOpts?.shareScope).toBe('test-scope'); + expect(provideOpts?.shareScope).toBe('test-scope'); + + const requestEntries = consumeOpts?.consumes.flatMap((cfg: any) => + Object.values(cfg).map((entry: any) => entry.request), + ); + expect(requestEntries).toEqual( + expect.arrayContaining(['components/', 'react']), + ); }); describe('helper methods integration', () => { @@ -247,21 +294,56 @@ describe('SharePlugin Compiler Integration', () => { }, }); - // Test all helper methods - const options = plugin.getOptions(); - expect(options.shareScope).toBe('debug-scope'); - - const shareScope = plugin.getShareScope(); + // Access internal state for debug assertions + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore - accessing private fields intentionally for inspection + const shareScope = plugin._shareScope; expect(shareScope).toBe('debug-scope'); - const consumes = plugin.getConsumes(); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const consumes: Record[] = plugin._consumes; expect(consumes).toHaveLength(3); - const provides = plugin.getProvides(); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const provides: Record[] = plugin._provides; expect(provides).toHaveLength(2); // lodash excluded due to import: false - const sharedInfo = plugin.getSharedInfo(); - expect(sharedInfo).toEqual({ + const consumeEntries = new Set( + consumes.flatMap((consume) => + Object.entries(consume).map( + ([key, config]) => config.shareKey || config.request || key, + ), + ), + ); + const provideEntries = new Set( + provides.flatMap((provide) => + Object.entries(provide).map( + ([key, config]) => config.shareKey || config.request || key, + ), + ), + ); + + let provideAndConsume = 0; + for (const key of consumeEntries) { + if (provideEntries.has(key)) { + provideAndConsume++; + } + } + + const totalShared = consumes.length; + const consumeOnly = totalShared - provideAndConsume; + const shareScopes = Array.isArray(shareScope) + ? [...shareScope] + : [shareScope]; + + expect({ + totalShared, + consumeOnly, + provideAndConsume, + shareScopes, + }).toEqual({ totalShared: 3, consumeOnly: 1, provideAndConsume: 2, @@ -286,6 +368,14 @@ describe('SharePlugin Compiler Integration', () => { }, }); }).not.toThrow(); + + expect(() => { + new SharePlugin({ + shared: { + react: ['react', 'other'] as any, + }, + }); + }).toThrow(); }); }); }); diff --git a/packages/enhanced/test/configCases/container/worker/App.js b/packages/enhanced/test/configCases/container/worker/App.js new file mode 100644 index 00000000000..731b14455db --- /dev/null +++ b/packages/enhanced/test/configCases/container/worker/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/worker/ComponentA.js b/packages/enhanced/test/configCases/container/worker/ComponentA.js new file mode 100644 index 00000000000..0e5b6e1ed71 --- /dev/null +++ b/packages/enhanced/test/configCases/container/worker/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/worker/WorkerApp.js b/packages/enhanced/test/configCases/container/worker/WorkerApp.js new file mode 100644 index 00000000000..941090aa3fb --- /dev/null +++ b/packages/enhanced/test/configCases/container/worker/WorkerApp.js @@ -0,0 +1,30 @@ +// Main thread code that creates and communicates with the worker +// According to webpack docs, Node.js requires importing Worker from 'worker_threads' +// and only works with ESM: https://webpack.js.org/guides/web-workers/ + +export function createWorker() { + return new Worker(new URL('./worker.js', import.meta.url)); +} + +export function testWorker() { + return new Promise((resolve, reject) => { + const worker = createWorker(); + + worker.onmessage = function (e) { + if (e.data.success) { + resolve(e.data); + } else { + reject(new Error(`Worker failed: ${e.data.error}`)); + } + worker.terminate(); + }; + + worker.onerror = function (error) { + reject(error); + worker.terminate(); + }; + + // Send message to trigger worker + worker.postMessage({ test: true }); + }); +} diff --git a/packages/enhanced/test/configCases/container/worker/index.js b/packages/enhanced/test/configCases/container/worker/index.js new file mode 100644 index 00000000000..5e9da7242a4 --- /dev/null +++ b/packages/enhanced/test/configCases/container/worker/index.js @@ -0,0 +1,65 @@ +// Test that verifies webpack correctly handles worker syntax with Module Federation +// +// This test verifies: +// 1. Webpack can compile new Worker(new URL()) syntax +// 2. Module Federation works in worker file context +// 3. Remote modules are accessible from worker code +// +// Note: Actual Worker execution is not tested due to test environment limitations + +// Reset React version to initial state before tests +// This prevents contamination from other tests that may have run before +beforeEach(() => { + return import('react').then((React) => { + React.setVersion('0.1.2'); + }); +}); + +it('should compile worker with module federation support', () => { + // Verify the worker file exists and can be imported + return import('./worker.js').then((workerModule) => { + // The worker module should exist even if we can't run it as a worker + expect(workerModule).toBeDefined(); + expect(typeof workerModule.testWorkerFunctions).toBe('function'); + + // Test that the worker can access federated modules + const result = workerModule.testWorkerFunctions(); + expect(result.reactVersion).toBe('This is react 0.1.2'); + expect(result.componentOutput).toBe( + 'ComponentA rendered with [This is react 0.1.2]', + ); + }); +}); + +it('should load the component from container in main thread', () => { + 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]]', + ); + }); +}); + +it('should handle react upgrade in main thread', () => { + return import('./upgrade-react').then(({ default: upgrade }) => { + upgrade(); + 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]]', + ); + }); + }); +}); + +// Test that worker app module compiles correctly +it('should compile WorkerApp module with Worker creation code', () => { + return import('./WorkerApp').then(({ createWorker, testWorker }) => { + // Verify the exports exist + expect(typeof createWorker).toBe('function'); + expect(typeof testWorker).toBe('function'); + + // We can't actually run these in Node.js environment + // but their existence proves the module compiled correctly + }); +}); diff --git a/packages/enhanced/test/configCases/container/worker/node_modules/react.js b/packages/enhanced/test/configCases/container/worker/node_modules/react.js new file mode 100644 index 00000000000..3e50f72939c --- /dev/null +++ b/packages/enhanced/test/configCases/container/worker/node_modules/react.js @@ -0,0 +1,5 @@ +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/worker/test.config.js b/packages/enhanced/test/configCases/container/worker/test.config.js new file mode 100644 index 00000000000..5535c6bb396 --- /dev/null +++ b/packages/enhanced/test/configCases/container/worker/test.config.js @@ -0,0 +1,12 @@ +const { URL } = require('url'); + +module.exports = { + findBundle: function () { + return './module/main.mjs'; + }, + moduleScope(scope) { + // Add URL to scope for Node.js targets + // Node.js has URL as a global since v10.0.0 + scope.URL = URL; + }, +}; diff --git a/packages/enhanced/test/configCases/container/worker/test.filter.js b/packages/enhanced/test/configCases/container/worker/test.filter.js new file mode 100644 index 00000000000..d957a5d7b99 --- /dev/null +++ b/packages/enhanced/test/configCases/container/worker/test.filter.js @@ -0,0 +1,5 @@ +// Filter for worker test case +// We now rely on the ESM build exclusively, which works with Worker support. +module.exports = function () { + return true; +}; diff --git a/packages/enhanced/test/configCases/container/worker/upgrade-react.js b/packages/enhanced/test/configCases/container/worker/upgrade-react.js new file mode 100644 index 00000000000..5bf08a67d5a --- /dev/null +++ b/packages/enhanced/test/configCases/container/worker/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/worker/webpack.config.js b/packages/enhanced/test/configCases/container/worker/webpack.config.js new file mode 100644 index 00000000000..5637046434b --- /dev/null +++ b/packages/enhanced/test/configCases/container/worker/webpack.config.js @@ -0,0 +1,42 @@ +const { ModuleFederationPlugin } = require('../../../../dist/src'); + +const common = { + name: 'container', + exposes: { + './ComponentA': { + import: './ComponentA', + }, + }, + shared: { + react: { + version: false, + requiredVersion: false, + }, + }, +}; + +// Test worker compilation with Module Federation using ESM output, since +// worker support relies on `new Worker(new URL(...))` which requires module output. + +module.exports = { + experiments: { + outputModule: true, + }, + output: { + filename: 'module/[name].mjs', + uniqueName: 'worker-container-mjs', + }, + target: 'node14', + plugins: [ + new ModuleFederationPlugin({ + library: { type: 'module' }, + filename: 'module/container.mjs', + remotes: { + containerA: { + external: './container.mjs', + }, + }, + ...common, + }), + ], +}; diff --git a/packages/enhanced/test/configCases/container/worker/worker.js b/packages/enhanced/test/configCases/container/worker/worker.js new file mode 100644 index 00000000000..07a90b8b004 --- /dev/null +++ b/packages/enhanced/test/configCases/container/worker/worker.js @@ -0,0 +1,37 @@ +// Worker that uses Module Federation to import components +import React from 'react'; +import ComponentA from 'containerA/ComponentA'; + +// Check if we're in a worker context +if (typeof self !== 'undefined' && typeof self.onmessage !== 'undefined') { + self.onmessage = async function (e) { + try { + // Test that React and ComponentA are available in worker context + const reactVersion = React(); + const componentOutput = ComponentA(); + + self.postMessage({ + success: true, + reactVersion: reactVersion, + componentOutput: componentOutput, + message: `Worker successfully loaded: React=${reactVersion}, Component=${componentOutput}`, + }); + } catch (error) { + self.postMessage({ + success: false, + error: error.message, + stack: error.stack, + }); + } + }; +} + +// Export for testing purposes when not in worker context +export function testWorkerFunctions() { + const reactVersion = React(); + const componentOutput = ComponentA(); + return { + reactVersion, + componentOutput, + }; +} diff --git a/packages/enhanced/test/unit/container/EmbedFederationRuntimePlugin.test.ts b/packages/enhanced/test/unit/container/EmbedFederationRuntimePlugin.test.ts new file mode 100644 index 00000000000..f0bc6fef4f3 --- /dev/null +++ b/packages/enhanced/test/unit/container/EmbedFederationRuntimePlugin.test.ts @@ -0,0 +1,367 @@ +/* + * @jest-environment node + */ + +import EmbedFederationRuntimePlugin from '../../../src/lib/container/runtime/EmbedFederationRuntimePlugin'; +import EmbedFederationRuntimeModule from '../../../src/lib/container/runtime/EmbedFederationRuntimeModule'; +import type { Compiler, Compilation, Chunk } from 'webpack'; + +class MockConcatSource { + private readonly parts: any[]; + + constructor(...parts: any[]) { + this.parts = parts; + } + + toString(): string { + return this.parts + .map((part) => + typeof part === 'string' ? part : (part?.toString?.() ?? String(part)), + ) + .join(''); + } +} + +// Mock dependencies +jest.mock('@module-federation/sdk/normalize-webpack-path', () => ({ + normalizeWebpackPath: jest.fn((path) => path), +})); + +const { RuntimeGlobals } = require('webpack') as typeof import('webpack'); + +describe('EmbedFederationRuntimePlugin', () => { + let mockCompiler: any; + let mockCompilation: any; + let mockChunk: any; + let runtimeRequirementInTreeHandler: any; + let renderStartupHandler: any; + let additionalRuntimeHandler: any; + + beforeEach(() => { + jest.clearAllMocks(); + renderStartupHandler = undefined; + additionalRuntimeHandler = undefined; + + // Mock chunk + mockChunk = { + name: 'test-chunk', + id: 'test-chunk-id', + hasRuntime: jest.fn().mockReturnValue(true), + getEntryOptions: jest.fn(), + }; + + // Mock compilation + mockCompilation = { + outputOptions: { + chunkLoading: 'jsonp', + }, + addRuntimeModule: jest.fn(), + compiler: {}, // Required by FederationModulesPlugin validation + hooks: { + processAssets: { + tap: jest.fn(), + }, + runtimeRequirementInTree: { + for: jest.fn((requirement: string) => ({ + tap: jest.fn((pluginName: string, handler: Function) => { + runtimeRequirementInTreeHandler = handler; + }), + })), + }, + additionalChunkRuntimeRequirements: { + tap: jest.fn((_pluginName: string, handler: Function) => { + additionalRuntimeHandler = handler; + }), + }, + }, + }; + + // Mock compiler + mockCompiler = { + webpack: { + RuntimeModule: class MockRuntimeModule { + static STAGE_NORMAL = 0; + static STAGE_BASIC = 5; + static STAGE_ATTACH = 10; + static STAGE_TRIGGER = 20; + }, + sources: { + ConcatSource: MockConcatSource, + }, + javascript: { + JavascriptModulesPlugin: { + getCompilationHooks: jest.fn().mockReturnValue({ + renderStartup: { + tap: jest.fn((_pluginName: string, handler: Function) => { + renderStartupHandler = handler; + }), + }, + }), + }, + }, + }, + hooks: { + thisCompilation: { + tap: jest.fn((pluginName: string, handler: Function) => { + handler(mockCompilation); + }), + taps: [], + }, + }, + }; + }); + + describe('stage selection based on chunk loading type', () => { + it('should use STAGE_ATTACH (10) for JSONP chunks', () => { + const plugin = new EmbedFederationRuntimePlugin(); + plugin.apply(mockCompiler as Compiler); + + // Mock chunk with JSONP loading + mockChunk.getEntryOptions.mockReturnValue({ + chunkLoading: 'jsonp', + }); + + const runtimeRequirements = new Set(['__webpack_require__.federation']); + runtimeRequirementInTreeHandler(mockChunk, runtimeRequirements); + + expect(mockCompilation.addRuntimeModule).toHaveBeenCalledTimes(1); + const addedModule = mockCompilation.addRuntimeModule.mock.calls[0][1]; + expect(addedModule).toBeInstanceOf(EmbedFederationRuntimeModule); + expect(addedModule.stage).toBe(10); // STAGE_ATTACH + }); + + it('should use STAGE_BASIC (5) for import-scripts chunks (workers)', () => { + const plugin = new EmbedFederationRuntimePlugin(); + plugin.apply(mockCompiler as Compiler); + + // Mock chunk with import-scripts loading + mockChunk.getEntryOptions.mockReturnValue({ + chunkLoading: 'import-scripts', + }); + + const runtimeRequirements = new Set(['__webpack_require__.federation']); + runtimeRequirementInTreeHandler(mockChunk, runtimeRequirements); + + expect(mockCompilation.addRuntimeModule).toHaveBeenCalledTimes(1); + const addedModule = mockCompilation.addRuntimeModule.mock.calls[0][1]; + expect(addedModule).toBeInstanceOf(EmbedFederationRuntimeModule); + expect(addedModule.stage).toBe(5); // STAGE_BASIC + }); + + it('should use STAGE_ATTACH (10) for other chunk loading types', () => { + const plugin = new EmbedFederationRuntimePlugin(); + plugin.apply(mockCompiler as Compiler); + + // Mock chunk with async-node loading + mockChunk.getEntryOptions.mockReturnValue({ + chunkLoading: 'async-node', + }); + + const runtimeRequirements = new Set(['__webpack_require__.federation']); + runtimeRequirementInTreeHandler(mockChunk, runtimeRequirements); + + expect(mockCompilation.addRuntimeModule).toHaveBeenCalledTimes(1); + const addedModule = mockCompilation.addRuntimeModule.mock.calls[0][1]; + expect(addedModule).toBeInstanceOf(EmbedFederationRuntimeModule); + expect(addedModule.stage).toBe(10); // STAGE_ATTACH + }); + + it('should fallback to global chunkLoading when entry options is undefined', () => { + const plugin = new EmbedFederationRuntimePlugin(); + plugin.apply(mockCompiler as Compiler); + + // Mock chunk with no entry options + mockChunk.getEntryOptions.mockReturnValue(undefined); + mockCompilation.outputOptions.chunkLoading = 'jsonp'; + + const runtimeRequirements = new Set(['__webpack_require__.federation']); + runtimeRequirementInTreeHandler(mockChunk, runtimeRequirements); + + expect(mockCompilation.addRuntimeModule).toHaveBeenCalledTimes(1); + const addedModule = mockCompilation.addRuntimeModule.mock.calls[0][1]; + expect(addedModule.stage).toBe(10); // STAGE_ATTACH for JSONP + }); + + it('should fallback to global chunkLoading when entry chunkLoading is undefined', () => { + const plugin = new EmbedFederationRuntimePlugin(); + plugin.apply(mockCompiler as Compiler); + + // Mock chunk with entry options but no chunkLoading + mockChunk.getEntryOptions.mockReturnValue({}); + mockCompilation.outputOptions.chunkLoading = 'import-scripts'; + + const runtimeRequirements = new Set(['__webpack_require__.federation']); + runtimeRequirementInTreeHandler(mockChunk, runtimeRequirements); + + expect(mockCompilation.addRuntimeModule).toHaveBeenCalledTimes(1); + const addedModule = mockCompilation.addRuntimeModule.mock.calls[0][1]; + expect(addedModule.stage).toBe(5); // STAGE_BASIC for import-scripts + }); + + it('should prefer entry chunkLoading over global chunkLoading', () => { + const plugin = new EmbedFederationRuntimePlugin(); + plugin.apply(mockCompiler as Compiler); + + // Entry options has import-scripts, global is jsonp + mockChunk.getEntryOptions.mockReturnValue({ + chunkLoading: 'import-scripts', + }); + mockCompilation.outputOptions.chunkLoading = 'jsonp'; + + const runtimeRequirements = new Set(['__webpack_require__.federation']); + runtimeRequirementInTreeHandler(mockChunk, runtimeRequirements); + + expect(mockCompilation.addRuntimeModule).toHaveBeenCalledTimes(1); + const addedModule = mockCompilation.addRuntimeModule.mock.calls[0][1]; + expect(addedModule.stage).toBe(5); // STAGE_BASIC for import-scripts from entry options + }); + }); + + describe('runtime requirement handling', () => { + it('should not add runtime module when federation requirement is missing', () => { + const plugin = new EmbedFederationRuntimePlugin(); + plugin.apply(mockCompiler as Compiler); + + const runtimeRequirements = new Set(['other-requirement']); + runtimeRequirementInTreeHandler(mockChunk, runtimeRequirements); + + expect(mockCompilation.addRuntimeModule).not.toHaveBeenCalled(); + }); + + it('should add embeddedFederationRuntime requirement after adding module', () => { + const plugin = new EmbedFederationRuntimePlugin(); + plugin.apply(mockCompiler as Compiler); + + mockChunk.getEntryOptions.mockReturnValue({ chunkLoading: 'jsonp' }); + + const runtimeRequirements = new Set(['__webpack_require__.federation']); + runtimeRequirementInTreeHandler(mockChunk, runtimeRequirements); + + expect(runtimeRequirements.has('embeddedFederationRuntime')).toBe(true); + }); + + it('should not add module twice for same chunk', () => { + const plugin = new EmbedFederationRuntimePlugin(); + plugin.apply(mockCompiler as Compiler); + + mockChunk.getEntryOptions.mockReturnValue({ chunkLoading: 'jsonp' }); + + const runtimeRequirements = new Set(['__webpack_require__.federation']); + + // First call + runtimeRequirementInTreeHandler(mockChunk, runtimeRequirements); + expect(mockCompilation.addRuntimeModule).toHaveBeenCalledTimes(1); + + // Second call with embeddedFederationRuntime already present + runtimeRequirementInTreeHandler(mockChunk, runtimeRequirements); + expect(mockCompilation.addRuntimeModule).toHaveBeenCalledTimes(1); // Still 1 + }); + }); + + describe('renderStartup hook', () => { + it('appends a startup call when the chunk would otherwise skip startup', () => { + const plugin = new EmbedFederationRuntimePlugin(); + plugin.apply(mockCompiler as Compiler); + + expect(typeof renderStartupHandler).toBe('function'); + + const runtimeRequirements = new Set(); + const chunkGraph = { + getTreeRuntimeRequirements: jest.fn(() => runtimeRequirements), + getNumberOfEntryModules: jest.fn(() => 0), + }; + + const startupSource = { + toString: () => '/* bootstrap */', + }; + + const result = renderStartupHandler(startupSource, undefined, { + chunk: mockChunk, + chunkGraph, + }); + + expect(result).not.toBe(startupSource); + expect(result.toString()).toContain(`${RuntimeGlobals.startup}()`); + }); + + it('returns the original startup source when runtime already triggers startup', () => { + const plugin = new EmbedFederationRuntimePlugin(); + plugin.apply(mockCompiler as Compiler); + + const runtimeRequirements = new Set(); + const chunkGraph = { + getTreeRuntimeRequirements: jest.fn(() => runtimeRequirements), + getNumberOfEntryModules: jest.fn(() => 1), + }; + + const startupSource = { + toString: () => '/* bootstrap */', + }; + + const result = renderStartupHandler(startupSource, undefined, { + chunk: mockChunk, + chunkGraph, + }); + + expect(result).toBe(startupSource); + }); + }); + + describe('additional runtime requirements', () => { + it('registers startupOnlyBefore for eligible chunks', () => { + const plugin = new EmbedFederationRuntimePlugin(); + plugin.apply(mockCompiler as Compiler); + + expect(typeof additionalRuntimeHandler).toBe('function'); + + const runtimeRequirements = new Set(); + additionalRuntimeHandler(mockChunk, runtimeRequirements); + + expect(runtimeRequirements.has(RuntimeGlobals.startupOnlyBefore)).toBe( + true, + ); + }); + + it('skips startupOnlyBefore when the chunk has no runtime', () => { + const plugin = new EmbedFederationRuntimePlugin(); + plugin.apply(mockCompiler as Compiler); + + mockChunk.hasRuntime.mockReturnValue(false); + + const runtimeRequirements = new Set(); + additionalRuntimeHandler(mockChunk, runtimeRequirements); + + expect(runtimeRequirements.size).toBe(0); + }); + }); + + describe('plugin initialization', () => { + it('should not double-tap if already applied', () => { + const plugin = new EmbedFederationRuntimePlugin(); + + // Mock existing tap + mockCompiler.hooks.thisCompilation.taps = [ + { name: 'EmbedFederationRuntimePlugin' }, + ]; + + const tapSpy = jest.spyOn(mockCompiler.hooks.thisCompilation, 'tap'); + plugin.apply(mockCompiler as Compiler); + + expect(tapSpy).not.toHaveBeenCalled(); + }); + + it('should tap into thisCompilation if not already applied', () => { + const plugin = new EmbedFederationRuntimePlugin(); + + mockCompiler.hooks.thisCompilation.taps = []; + + const tapSpy = jest.spyOn(mockCompiler.hooks.thisCompilation, 'tap'); + plugin.apply(mockCompiler as Compiler); + + expect(tapSpy).toHaveBeenCalledWith( + 'EmbedFederationRuntimePlugin', + expect.any(Function), + ); + }); + }); +}); diff --git a/packages/enhanced/test/unit/container/HoistContainerReferencesPlugin.test.ts b/packages/enhanced/test/unit/container/HoistContainerReferencesPlugin.test.ts new file mode 100644 index 00000000000..ddb34bcd6dc --- /dev/null +++ b/packages/enhanced/test/unit/container/HoistContainerReferencesPlugin.test.ts @@ -0,0 +1,569 @@ +import HoistContainerReferences, { + getAllReferencedModules, +} from '../../../src/lib/container/HoistContainerReferencesPlugin'; +import FederationModulesPlugin from '../../../src/lib/container/runtime/FederationModulesPlugin'; +import type { Compiler, Compilation, Chunk, Module } from 'webpack'; + +jest.mock('@module-federation/sdk/normalize-webpack-path', () => ({ + normalizeWebpackPath: (path: string) => path, + getWebpackPath: jest.fn(() => 'webpack'), +})); + +describe('HoistContainerReferencesPlugin', () => { + let plugin: HoistContainerReferences; + let compiler: any; + let compilation: any; + + beforeEach(() => { + plugin = new HoistContainerReferences(); + + compiler = { + hooks: { + thisCompilation: { + tap: jest.fn(), + }, + }, + webpack: { + util: { + runtime: { + forEachRuntime: jest.fn((runtimeSpec, callback) => { + if (runtimeSpec) { + callback('main'); + } + }), + }, + }, + }, + }; + + compilation = { + hooks: { + optimizeChunks: { + tap: jest.fn(), + }, + processAssets: { + tap: jest.fn(), + }, + }, + chunkGraph: { + getModuleChunks: jest.fn(() => []), + getModuleRuntimes: jest.fn(() => []), + isModuleInChunk: jest.fn(() => false), + connectChunkAndModule: jest.fn(), + disconnectChunkAndModule: jest.fn(), + }, + moduleGraph: { + getModule: jest.fn(), + getParentBlock: jest.fn(), + _getModuleGraphModule: jest.fn(), + }, + namedChunks: new Map(), + chunks: [], + getLogger: jest.fn(() => ({ + warn: jest.fn(), + })), + compiler, + }; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('apply', () => { + it('should tap into thisCompilation hook', () => { + plugin.apply(compiler as Compiler); + + expect(compiler.hooks.thisCompilation.tap).toHaveBeenCalledWith( + 'HoistContainerReferences', + expect.any(Function), + ); + }); + + it('should tap into compilation hooks when thisCompilation is called', () => { + plugin.apply(compiler as Compiler); + + const thisCompilationCallback = + compiler.hooks.thisCompilation.tap.mock.calls[0][1]; + thisCompilationCallback(compilation); + + expect(compilation.hooks.optimizeChunks.tap).toHaveBeenCalledWith( + { + name: 'HoistContainerReferences', + stage: 11, + }, + expect.any(Function), + ); + }); + + it('should register hooks with FederationModulesPlugin', () => { + const mockHooks = { + addContainerEntryDependency: { tap: jest.fn() }, + addFederationRuntimeDependency: { tap: jest.fn() }, + addRemoteDependency: { tap: jest.fn() }, + }; + + jest + .spyOn(FederationModulesPlugin, 'getCompilationHooks') + .mockReturnValue(mockHooks as any); + + plugin.apply(compiler as Compiler); + const thisCompilationCallback = + compiler.hooks.thisCompilation.tap.mock.calls[0][1]; + thisCompilationCallback(compilation); + + expect(mockHooks.addContainerEntryDependency.tap).toHaveBeenCalledWith( + 'HoistContainerReferences', + expect.any(Function), + ); + expect(mockHooks.addFederationRuntimeDependency.tap).toHaveBeenCalledWith( + 'HoistContainerReferences', + expect.any(Function), + ); + expect(mockHooks.addRemoteDependency.tap).toHaveBeenCalledWith( + 'HoistContainerReferences', + expect.any(Function), + ); + }); + }); + + describe('getRuntimeChunks', () => { + it('should identify runtime chunks correctly', () => { + const runtimeChunk = { + hasRuntime: jest.fn(() => true), + name: 'runtime', + }; + const normalChunk = { + hasRuntime: jest.fn(() => false), + name: 'normal', + }; + + compilation.chunks = [runtimeChunk, normalChunk]; + + plugin.apply(compiler as Compiler); + const thisCompilationCallback = + compiler.hooks.thisCompilation.tap.mock.calls[0][1]; + thisCompilationCallback(compilation); + + const optimizeChunksCallback = + compilation.hooks.optimizeChunks.tap.mock.calls[0][1]; + optimizeChunksCallback(compilation.chunks); + + expect(runtimeChunk.hasRuntime).toHaveBeenCalled(); + expect(normalChunk.hasRuntime).toHaveBeenCalled(); + }); + + it('should return empty set when no runtime chunks exist', () => { + const normalChunk = { + hasRuntime: jest.fn(() => false), + name: 'normal', + }; + + compilation.chunks = [normalChunk]; + + plugin.apply(compiler as Compiler); + const thisCompilationCallback = + compiler.hooks.thisCompilation.tap.mock.calls[0][1]; + thisCompilationCallback(compilation); + + const optimizeChunksCallback = + compilation.hooks.optimizeChunks.tap.mock.calls[0][1]; + optimizeChunksCallback(compilation.chunks); + + expect(normalChunk.hasRuntime).toHaveBeenCalled(); + }); + }); + + describe('hoistModulesInChunks', () => { + it('should connect modules to runtime chunks', () => { + const runtimeChunk = { + hasRuntime: jest.fn(() => true), + name: 'runtime', + runtime: ['main'], + }; + + const mockModule = { + identifier: () => 'test-module', + }; + + const mockDependency = { + type: 'container-entry', + }; + + compilation.chunks = [runtimeChunk]; + compilation.chunkGraph.getModuleChunks.mockReturnValue([runtimeChunk]); + compilation.chunkGraph.getModuleRuntimes.mockReturnValue(['main']); + compilation.moduleGraph.getModule.mockReturnValue(mockModule); + compilation.moduleGraph._getModuleGraphModule.mockReturnValue({ + outgoingConnections: [], + }); + + const mockHooks = { + addContainerEntryDependency: { tap: jest.fn() }, + addFederationRuntimeDependency: { tap: jest.fn() }, + addRemoteDependency: { tap: jest.fn() }, + }; + + jest + .spyOn(FederationModulesPlugin, 'getCompilationHooks') + .mockReturnValue(mockHooks as any); + + plugin.apply(compiler as Compiler); + const thisCompilationCallback = + compiler.hooks.thisCompilation.tap.mock.calls[0][1]; + thisCompilationCallback(compilation); + + // Trigger container entry dependency + const containerCallback = + mockHooks.addContainerEntryDependency.tap.mock.calls[0][1]; + containerCallback(mockDependency); + + const optimizeChunksCallback = + compilation.hooks.optimizeChunks.tap.mock.calls[0][1]; + optimizeChunksCallback(compilation.chunks); + + expect(compilation.chunkGraph.connectChunkAndModule).toHaveBeenCalled(); + }); + + it('should handle runtime names provided as a string', () => { + const runtimeChunk = { + hasRuntime: jest.fn(() => true), + name: 'runtime', + runtime: 'main', + }; + + const mockModule = { + identifier: () => 'test-module', + }; + + compilation.namedChunks.set('main', runtimeChunk as unknown as Chunk); + compilation.chunks = [runtimeChunk]; + compilation.chunkGraph.getModuleChunks.mockReturnValue([]); + compilation.chunkGraph.getModuleRuntimes.mockReturnValue(['main']); + compilation.moduleGraph.getModule.mockReturnValue(mockModule); + compilation.moduleGraph._getModuleGraphModule.mockReturnValue({ + outgoingConnections: [], + }); + + const mockHooks = { + addContainerEntryDependency: { tap: jest.fn() }, + addFederationRuntimeDependency: { tap: jest.fn() }, + addRemoteDependency: { tap: jest.fn() }, + }; + + jest + .spyOn(FederationModulesPlugin, 'getCompilationHooks') + .mockReturnValue(mockHooks as any); + + plugin.apply(compiler as Compiler); + const thisCompilationCallback = + compiler.hooks.thisCompilation.tap.mock.calls[0][1]; + thisCompilationCallback(compilation); + + const runtimeCallback = + mockHooks.addFederationRuntimeDependency.tap.mock.calls[0][1]; + runtimeCallback({ type: 'runtime' }); + + const optimizeChunksCallback = + compilation.hooks.optimizeChunks.tap.mock.calls[0][1]; + optimizeChunksCallback(compilation.chunks); + + expect(compilation.chunkGraph.connectChunkAndModule).toHaveBeenCalledWith( + runtimeChunk, + mockModule, + ); + }); + + it('should handle worker runtime dependencies separately', () => { + const FederationWorkerRuntimeDependency = + require('../../../src/lib/container/runtime/FederationWorkerRuntimeDependency').default; + + const workerDep = new FederationWorkerRuntimeDependency( + 'worker-entry.js', + ); + const mockModule = { + identifier: () => 'worker-module', + }; + + const runtimeChunk = { + hasRuntime: jest.fn(() => true), + name: 'worker-chunk', + runtime: ['worker'], + }; + + compilation.chunks = [runtimeChunk]; + compilation.chunkGraph.getModuleChunks.mockReturnValue([runtimeChunk]); + compilation.chunkGraph.getModuleRuntimes.mockReturnValue(['worker']); + compilation.moduleGraph.getModule.mockReturnValue(mockModule); + compilation.moduleGraph._getModuleGraphModule.mockReturnValue({ + outgoingConnections: [], + }); + + const mockHooks = { + addContainerEntryDependency: { tap: jest.fn() }, + addFederationRuntimeDependency: { tap: jest.fn() }, + addRemoteDependency: { tap: jest.fn() }, + }; + + jest + .spyOn(FederationModulesPlugin, 'getCompilationHooks') + .mockReturnValue(mockHooks as any); + + plugin.apply(compiler as Compiler); + const thisCompilationCallback = + compiler.hooks.thisCompilation.tap.mock.calls[0][1]; + thisCompilationCallback(compilation); + + const runtimeCallback = + mockHooks.addFederationRuntimeDependency.tap.mock.calls[0][1]; + runtimeCallback(workerDep); + + const optimizeChunksCallback = + compilation.hooks.optimizeChunks.tap.mock.calls[0][1]; + optimizeChunksCallback(compilation.chunks); + + expect(compilation.moduleGraph.getModule).toHaveBeenCalledWith(workerDep); + }); + }); + + describe('cleanUpChunks', () => { + it('should disconnect modules from non-runtime chunks', () => { + const runtimeChunk = { + hasRuntime: jest.fn(() => true), + name: 'runtime', + }; + const nonRuntimeChunk = { + hasRuntime: jest.fn(() => false), + name: 'normal', + }; + + const mockModule = { + identifier: () => 'test-module', + }; + + compilation.chunks = [runtimeChunk, nonRuntimeChunk]; + compilation.chunkGraph.getModuleChunks.mockReturnValue([nonRuntimeChunk]); + compilation.moduleGraph.getModule.mockReturnValue(mockModule); + compilation.moduleGraph._getModuleGraphModule.mockReturnValue({ + outgoingConnections: [], + }); + + const mockHooks = { + addContainerEntryDependency: { tap: jest.fn() }, + addFederationRuntimeDependency: { tap: jest.fn() }, + addRemoteDependency: { tap: jest.fn() }, + }; + + jest + .spyOn(FederationModulesPlugin, 'getCompilationHooks') + .mockReturnValue(mockHooks as any); + + plugin.apply(compiler as Compiler); + const thisCompilationCallback = + compiler.hooks.thisCompilation.tap.mock.calls[0][1]; + thisCompilationCallback(compilation); + + const containerCallback = + mockHooks.addContainerEntryDependency.tap.mock.calls[0][1]; + containerCallback({ type: 'container-entry' }); + + const optimizeChunksCallback = + compilation.hooks.optimizeChunks.tap.mock.calls[0][1]; + optimizeChunksCallback(compilation.chunks); + + expect( + compilation.chunkGraph.disconnectChunkAndModule, + ).toHaveBeenCalledWith(nonRuntimeChunk, mockModule); + }); + }); + + describe('getAllReferencedModules', () => { + let mockCompilation: any; + + beforeEach(() => { + mockCompilation = { + moduleGraph: { + getParentBlock: jest.fn(), + _getModuleGraphModule: jest.fn(), + }, + }; + }); + + it('should collect all referenced modules with type "all"', () => { + const mockModule = { + identifier: () => 'root-module', + }; + + const connectedModule = { + identifier: () => 'connected-module', + }; + + mockCompilation.moduleGraph._getModuleGraphModule.mockReturnValue({ + outgoingConnections: [ + { + module: connectedModule, + dependency: {}, + }, + ], + }); + + const result = getAllReferencedModules( + mockCompilation, + mockModule as Module, + 'all', + ); + + expect(result.has(mockModule as Module)).toBe(true); + expect(result.has(connectedModule as Module)).toBe(true); + expect(result.size).toBe(2); + }); + + it('should skip async blocks when type is "initial"', () => { + const AsyncDependenciesBlock = require('webpack').AsyncDependenciesBlock; + + const mockModule = { + identifier: () => 'root-module', + }; + + const asyncModule = { + identifier: () => 'async-module', + }; + + const asyncBlock = new AsyncDependenciesBlock({}, null, 'async'); + + mockCompilation.moduleGraph._getModuleGraphModule + .mockReturnValueOnce({ + outgoingConnections: [ + { + module: asyncModule, + dependency: {}, + }, + ], + }) + .mockReturnValueOnce({ + outgoingConnections: [], + }); + + mockCompilation.moduleGraph.getParentBlock.mockReturnValue(asyncBlock); + + const result = getAllReferencedModules( + mockCompilation, + mockModule as Module, + 'initial', + ); + + expect(result.has(mockModule as Module)).toBe(true); + expect(result.has(asyncModule as Module)).toBe(false); + expect(result.size).toBe(1); + }); + + it('should collect only external modules when type is "external"', () => { + const ExternalModule = require('webpack').ExternalModule; + + const mockModule = { + identifier: () => 'root-module', + }; + + const externalModule = new ExternalModule( + 'external-lib', + 'commonjs', + 'external-lib', + ); + + const normalModule = { + identifier: () => 'normal-module', + }; + + mockCompilation.moduleGraph._getModuleGraphModule + .mockReturnValueOnce({ + outgoingConnections: [ + { + module: externalModule, + dependency: {}, + }, + { + module: normalModule, + dependency: {}, + }, + ], + }) + .mockReturnValue({ + outgoingConnections: [], + }); + + const result = getAllReferencedModules( + mockCompilation, + mockModule as Module, + 'external', + ); + + expect(result.has(mockModule as Module)).toBe(true); + expect(result.has(externalModule as Module)).toBe(true); + expect(result.has(normalModule as Module)).toBe(false); + }); + + it('should handle modules with no outgoing connections', () => { + const mockModule = { + identifier: () => 'isolated-module', + }; + + mockCompilation.moduleGraph._getModuleGraphModule.mockReturnValue({ + outgoingConnections: [], + }); + + const result = getAllReferencedModules( + mockCompilation, + mockModule as Module, + 'all', + ); + + expect(result.has(mockModule as Module)).toBe(true); + expect(result.size).toBe(1); + }); + + it('should avoid circular references', () => { + const moduleA = { + identifier: () => 'module-a', + }; + + const moduleB = { + identifier: () => 'module-b', + }; + + mockCompilation.moduleGraph._getModuleGraphModule.mockImplementation( + (module: any) => { + if (module === moduleA) { + return { + outgoingConnections: [ + { + module: moduleB, + dependency: {}, + }, + ], + }; + } else if (module === moduleB) { + return { + outgoingConnections: [ + { + module: moduleA, // Circular reference + dependency: {}, + }, + ], + }; + } + return { outgoingConnections: [] }; + }, + ); + + const result = getAllReferencedModules( + mockCompilation, + moduleA as Module, + 'all', + ); + + expect(result.has(moduleA as Module)).toBe(true); + expect(result.has(moduleB as Module)).toBe(true); + expect(result.size).toBe(2); + }); + }); +}); diff --git a/packages/enhanced/test/unit/container/RemoteRuntimeModule.test.ts b/packages/enhanced/test/unit/container/RemoteRuntimeModule.test.ts index cb548197d3b..405efc693f3 100644 --- a/packages/enhanced/test/unit/container/RemoteRuntimeModule.test.ts +++ b/packages/enhanced/test/unit/container/RemoteRuntimeModule.test.ts @@ -166,6 +166,8 @@ describe('RemoteRuntimeModule', () => { const { normalizeCode } = require('../../helpers/snapshots'); const normalized = normalizeCode(result as string); const expected = [ + '__FEDERATION__.bundlerRuntimeOptions = __FEDERATION__.bundlerRuntimeOptions || {};', + '__FEDERATION__.bundlerRuntimeOptions.remotes = __FEDERATION__.bundlerRuntimeOptions.remotes || {};', 'var chunkMapping = {};', 'var idToExternalAndNameMapping = {};', 'var idToRemoteMap = {};', @@ -173,7 +175,7 @@ describe('RemoteRuntimeModule', () => { '__FEDERATION__.bundlerRuntimeOptions.remotes.idToExternalAndNameMapping = idToExternalAndNameMapping;', '__FEDERATION__.bundlerRuntimeOptions.remotes.idToRemoteMap = idToRemoteMap;', '__webpack_require__.remotesLoadingData.moduleIdToRemoteDataMapping = {};', - '__webpack_require__.e.remotes = function(chunkId, promises) { __FEDERATION__.bundlerRuntime.remotes({idToRemoteMap,chunkMapping, idToExternalAndNameMapping, chunkId, promises, webpackRequire:__webpack_require__}); }', + '__webpack_require__.e.remotes = function(chunkId, promises) { if(__FEDERATION__.bundlerRuntime && __FEDERATION__.bundlerRuntime.remotes){\n __FEDERATION__.bundlerRuntime.remotes({idToRemoteMap,chunkMapping, idToExternalAndNameMapping, chunkId, promises, webpackRequire:__webpack_require__});\n} }', ].join('\n'); expect(normalized).toBe(expected); }); diff --git a/packages/enhanced/test/unit/container/WorkerAsyncRuntimeChunk.test.ts b/packages/enhanced/test/unit/container/WorkerAsyncRuntimeChunk.test.ts new file mode 100644 index 00000000000..6acf4b25159 --- /dev/null +++ b/packages/enhanced/test/unit/container/WorkerAsyncRuntimeChunk.test.ts @@ -0,0 +1,213 @@ +/* + * @jest-environment node + */ + +import { ModuleFederationPlugin } from '@module-federation/enhanced'; +import { normalizeWebpackPath } from '@module-federation/sdk/normalize-webpack-path'; + +const webpack = require( + normalizeWebpackPath('webpack'), +) as typeof import('webpack'); + +import fs from 'fs'; +import os from 'os'; +import path from 'path'; + +type RuntimeSetting = Parameters< + Required['optimization']['runtimeChunk'] +>[0]; + +const tempDirs: string[] = []; + +afterAll(() => { + for (const dir of tempDirs) { + if (fs.existsSync(dir)) { + try { + fs.rmSync(dir, { recursive: true, force: true }); + } catch (error) { + console.warn(`Failed to remove temp dir ${dir}:`, error); + } + } + } +}); + +async function buildWorkerApp(runtimeChunk: RuntimeSetting) { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mf-worker-test-')); + tempDirs.push(tempDir); + + const outputPath = path.join(tempDir, 'dist'); + + fs.writeFileSync( + path.join(tempDir, 'main.js'), + `import './startup'; +new Worker(new URL('./worker.js', import.meta.url)); +`, + ); + + fs.writeFileSync( + path.join(tempDir, 'startup.js'), + `import('remoteApp/bootstrap').catch(() => {}); +`, + ); + + fs.writeFileSync( + path.join(tempDir, 'worker.js'), + `self.addEventListener('message', () => { + import('remoteApp/feature') + .then(() => self.postMessage({ ok: true })) + .catch(() => self.postMessage({ ok: false })); +}); +`, + ); + + fs.writeFileSync( + path.join(tempDir, 'package.json'), + JSON.stringify({ name: 'worker-host', version: '1.0.0' }), + ); + + const compiler = webpack({ + mode: 'development', + devtool: false, + context: tempDir, + entry: { + main: './main.js', + }, + output: { + path: outputPath, + filename: '[name].js', + chunkFilename: '[name].js', + publicPath: 'auto', + }, + optimization: { + runtimeChunk, + }, + plugins: [ + new ModuleFederationPlugin({ + name: 'host', + remotes: { + remoteApp: 'remoteApp@http://localhost:3001/remoteEntry.js', + }, + dts: false, + }), + ], + }); + + const stats = await new Promise( + (resolve, reject) => { + compiler.run((err, result) => { + if (err) { + reject(err); + } else if (!result) { + reject(new Error('Expected webpack compilation stats')); + } else if (result.hasErrors()) { + const info = result.toJson({ + all: false, + errors: true, + errorDetails: true, + }); + reject( + new Error((info.errors || []).map((e) => e.message).join('\n')), + ); + } else { + resolve(result); + } + }); + }, + ); + + await new Promise((resolve) => compiler.close(() => resolve())); + + const { chunkGraph } = stats.compilation; + const chunks = Array.from(stats.compilation.chunks); + + const workerChunk = chunks.find((chunk) => { + for (const module of chunkGraph.getChunkModulesIterable(chunk)) { + if (module.resource && module.resource.endsWith('worker.js')) { + return true; + } + } + return false; + }); + + const runtimeInfo = chunks.map((chunk) => { + const runtimeModules = Array.from( + chunkGraph.getChunkRuntimeModulesIterable(chunk), + ); + + return { + name: chunk.name, + hasRuntime: chunk.hasRuntime(), + hasWorker: chunk === workerChunk, + hasRemoteRuntime: runtimeModules.some((runtimeModule) => + runtimeModule.constructor?.name?.includes('RemoteRuntimeModule'), + ), + }; + }); + + return { + runtimeInfo, + workerChunk, + normalizedRuntimeChunk: compiler.options.optimization?.runtimeChunk, + entrypoints: Array.from( + stats.compilation.entrypoints, + ([name, entrypoint]) => { + const runtimeChunk = entrypoint.getRuntimeChunk(); + const entryChunk = entrypoint.getEntrypointChunk(); + return { + name, + runtimeChunkName: runtimeChunk?.name || null, + runtimeChunkId: runtimeChunk?.id ?? null, + entryChunkName: entryChunk?.name || null, + sharesRuntimeWithEntry: runtimeChunk && runtimeChunk !== entryChunk, + }; + }, + ), + }; +} + +describe('Module Federation worker async runtime integration', () => { + jest.setTimeout(30000); + it('keeps remote runtime helpers on both the shared runtime chunk and worker chunk', async () => { + const { runtimeInfo, workerChunk, normalizedRuntimeChunk, entrypoints } = + await buildWorkerApp({ name: 'mf-runtime' }); + + expect(workerChunk).toBeDefined(); + expect(typeof (normalizedRuntimeChunk as any)?.name).toBe('function'); + expect((normalizedRuntimeChunk as any)?.name({ name: 'main' })).toBe( + 'mf-runtime', + ); + expect( + entrypoints.some( + (info) => + info.sharesRuntimeWithEntry && info.runtimeChunkName === 'mf-runtime', + ), + ).toBe(true); + + const sharedRuntime = runtimeInfo.find( + (info) => info.name === 'mf-runtime', + ); + expect(sharedRuntime).toBeDefined(); + expect(sharedRuntime?.hasRemoteRuntime).toBe(true); + + const workerRuntimeInfo = runtimeInfo.find((info) => info.hasWorker); + expect(workerRuntimeInfo).toBeDefined(); + expect(workerRuntimeInfo?.hasRemoteRuntime).toBe(true); + }); + + it('does not duplicate remote runtime helpers when runtimeChunk produces per-entry runtimes', async () => { + const { runtimeInfo, workerChunk, normalizedRuntimeChunk, entrypoints } = + await buildWorkerApp(true); + + expect(workerChunk).toBeDefined(); + expect(typeof normalizedRuntimeChunk).toBe('object'); + const mainRuntime = runtimeInfo.find( + (info) => info.hasRuntime && info.name && info.name.includes('main'), + ); + expect(mainRuntime).toBeDefined(); + expect(mainRuntime?.hasRemoteRuntime).toBe(true); + + const workerRuntimeInfo = runtimeInfo.find((info) => info.hasWorker); + expect(workerRuntimeInfo).toBeDefined(); + expect(workerRuntimeInfo?.hasRemoteRuntime).toBe(true); + }); +}); diff --git a/packages/managers/src/PKGJsonManager.ts b/packages/managers/src/PKGJsonManager.ts index a84812139dc..124f24795d8 100644 --- a/packages/managers/src/PKGJsonManager.ts +++ b/packages/managers/src/PKGJsonManager.ts @@ -24,12 +24,18 @@ export class PKGJsonManager { return pkg; } catch (_err) { try { - const pkg = finder.sync(root); + const pkgPath = finder.sync(root); + if (!pkgPath) { + this._pkg = {}; + return this._pkg; + } + const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); this._pkg = pkg; return pkg; } catch (err) { logger.error(err); - return {}; + this._pkg = {}; + return this._pkg; } } } diff --git a/packages/runtime/src/index.ts b/packages/runtime/src/index.ts index d4253306f07..1e45b1e976a 100644 --- a/packages/runtime/src/index.ts +++ b/packages/runtime/src/index.ts @@ -24,6 +24,8 @@ export { type Federation, } from '@module-federation/runtime-core'; +export { createLogger } from '@module-federation/sdk'; + export { ModuleFederation }; export function createInstance(options: UserOptions) { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ffc949ccf4b..f7b24d55800 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -160,7 +160,7 @@ importers: version: 21.2.3(@rspack/core@1.3.9)(@swc-node/register@1.10.10)(@swc/core@1.7.26)(esbuild@0.25.0)(html-webpack-plugin@5.6.2)(nx@21.2.3)(typescript@5.8.3)(verdaccio@6.1.2)(webpack-cli@5.1.4) '@pmmmwh/react-refresh-webpack-plugin': specifier: 0.5.15 - version: 0.5.15(react-refresh@0.14.2)(webpack@5.98.0) + version: 0.5.15(react-refresh@0.14.2)(webpack-dev-server@5.1.0)(webpack@5.98.0) '@rollup/plugin-alias': specifier: 5.1.1 version: 5.1.1(rollup@4.40.0) @@ -632,7 +632,7 @@ importers: version: link:../../packages/typescript '@pmmmwh/react-refresh-webpack-plugin': specifier: 0.5.15 - version: 0.5.15(react-refresh@0.14.2)(webpack@5.98.0) + version: 0.5.15(react-refresh@0.14.2)(webpack-dev-server@5.1.0)(webpack@5.98.0) '@types/react': specifier: 18.3.11 version: 18.3.11 @@ -709,7 +709,7 @@ importers: version: link:../../../packages/typescript '@pmmmwh/react-refresh-webpack-plugin': specifier: 0.5.15 - version: 0.5.15(react-refresh@0.14.2)(webpack@5.98.0) + version: 0.5.15(react-refresh@0.14.2)(webpack-dev-server@5.1.0)(webpack@5.98.0) '@types/react': specifier: 18.3.11 version: 18.3.11 @@ -740,7 +740,7 @@ importers: version: link:../../../packages/enhanced '@pmmmwh/react-refresh-webpack-plugin': specifier: 0.5.15 - version: 0.5.15(react-refresh@0.14.2)(webpack@5.98.0) + version: 0.5.15(react-refresh@0.14.2)(webpack-dev-server@5.1.0)(webpack@5.98.0) '@rspack/core': specifier: ^1.0.2 version: 1.0.8(@swc/helpers@0.5.13) @@ -777,7 +777,7 @@ importers: version: link:../../../packages/enhanced '@pmmmwh/react-refresh-webpack-plugin': specifier: 0.5.15 - version: 0.5.15(react-refresh@0.14.2)(webpack@5.98.0) + version: 0.5.15(react-refresh@0.14.2)(webpack-dev-server@5.1.0)(webpack@5.98.0) '@rspack/plugin-react-refresh': specifier: ^0.7.5 version: 0.7.5(react-refresh@0.14.2) @@ -811,7 +811,7 @@ importers: version: link:../../../packages/enhanced '@pmmmwh/react-refresh-webpack-plugin': specifier: 0.5.15 - version: 0.5.15(react-refresh@0.14.2)(webpack@5.98.0) + version: 0.5.15(react-refresh@0.14.2)(webpack-dev-server@5.1.0)(webpack@5.98.0) '@rspack/plugin-react-refresh': specifier: ^0.7.5 version: 0.7.5(react-refresh@0.14.2) @@ -851,7 +851,7 @@ importers: version: link:../../../packages/typescript '@pmmmwh/react-refresh-webpack-plugin': specifier: 0.5.15 - version: 0.5.15(react-refresh@0.14.2)(webpack@5.98.0) + version: 0.5.15(react-refresh@0.14.2)(webpack-dev-server@5.1.0)(webpack@5.98.0) '@types/react': specifier: 18.3.11 version: 18.3.11 @@ -1970,7 +1970,7 @@ importers: version: link:../../packages/runtime '@pmmmwh/react-refresh-webpack-plugin': specifier: 0.5.15 - version: 0.5.15(react-refresh@0.14.2)(webpack@5.98.0) + version: 0.5.15(react-refresh@0.14.2)(webpack-dev-server@5.1.0)(webpack@5.98.0) '@types/react': specifier: 18.3.11 version: 18.3.11 @@ -2524,16 +2524,25 @@ importers: version: link:../../../packages/typescript '@pmmmwh/react-refresh-webpack-plugin': specifier: 0.5.15 - version: 0.5.15(react-refresh@0.14.2)(webpack@5.98.0) + version: 0.5.15(react-refresh@0.14.2)(webpack-dev-server@5.1.0)(webpack@5.98.0) '@types/react': specifier: 18.3.11 version: 18.3.11 '@types/react-dom': specifier: 18.3.0 version: 18.3.0 + css-loader: + specifier: 6.11.0 + version: 6.11.0(@rspack/core@1.3.9)(webpack@5.98.0) + mini-css-extract-plugin: + specifier: 2.9.2 + version: 2.9.2(webpack@5.98.0) react-refresh: specifier: 0.14.2 version: 0.14.2 + webpack-dev-server: + specifier: 5.1.0 + version: 5.1.0(webpack-cli@5.1.4)(webpack@5.98.0) apps/runtime-demo/3006-runtime-remote: dependencies: @@ -2558,16 +2567,25 @@ importers: version: link:../../../packages/typescript '@pmmmwh/react-refresh-webpack-plugin': specifier: 0.5.15 - version: 0.5.15(react-refresh@0.14.2)(webpack@5.98.0) + version: 0.5.15(react-refresh@0.14.2)(webpack-dev-server@5.1.0)(webpack@5.98.0) '@types/react': specifier: 18.3.11 version: 18.3.11 '@types/react-dom': specifier: 18.3.0 version: 18.3.0 + css-loader: + specifier: 6.11.0 + version: 6.11.0(@rspack/core@1.3.9)(webpack@5.98.0) + mini-css-extract-plugin: + specifier: 2.9.2 + version: 2.9.2(webpack@5.98.0) react-refresh: specifier: 0.14.2 version: 0.14.2 + webpack-dev-server: + specifier: 5.1.0 + version: 5.1.0(webpack-cli@5.1.4)(webpack@5.98.0) apps/runtime-demo/3007-runtime-remote: dependencies: @@ -2592,16 +2610,25 @@ importers: version: link:../../../packages/typescript '@pmmmwh/react-refresh-webpack-plugin': specifier: 0.5.15 - version: 0.5.15(react-refresh@0.14.2)(webpack@5.98.0) + version: 0.5.15(react-refresh@0.14.2)(webpack-dev-server@5.1.0)(webpack@5.98.0) '@types/react': specifier: 18.3.11 version: 18.3.11 '@types/react-dom': specifier: 18.3.0 version: 18.3.0 + css-loader: + specifier: 6.11.0 + version: 6.11.0(@rspack/core@1.3.9)(webpack@5.98.0) + mini-css-extract-plugin: + specifier: 2.9.2 + version: 2.9.2(webpack@5.98.0) react-refresh: specifier: 0.14.2 version: 0.14.2 + webpack-dev-server: + specifier: 5.1.0 + version: 5.1.0(webpack-cli@5.1.4)(webpack@5.98.0) apps/runtime-demo/3008-runtime-remote: dependencies: @@ -4268,7 +4295,7 @@ packages: '@babel/traverse': 7.28.0 '@babel/types': 7.28.4 convert-source-map: 1.9.0 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@5.5.0) gensync: 1.0.0-beta.2 json5: 2.2.3 lodash: 4.17.21 @@ -4316,7 +4343,7 @@ packages: '@babel/types': 7.28.4 '@jridgewell/remapping': 2.3.5 convert-source-map: 2.0.0 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@5.5.0) gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -4506,7 +4533,7 @@ packages: '@babel/core': 7.28.0 '@babel/helper-compilation-targets': 7.27.2 '@babel/helper-plugin-utils': 7.27.1 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@5.5.0) lodash.debounce: 4.0.8 resolve: 1.22.10 transitivePeerDependencies: @@ -4520,7 +4547,7 @@ packages: '@babel/core': 7.28.4 '@babel/helper-compilation-targets': 7.27.2 '@babel/helper-plugin-utils': 7.27.1 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@5.5.0) lodash.debounce: 4.0.8 resolve: 1.22.10 transitivePeerDependencies: @@ -7140,7 +7167,7 @@ packages: '@babel/parser': 7.28.4 '@babel/template': 7.27.2 '@babel/types': 7.28.4 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@5.5.0) globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -7183,7 +7210,7 @@ packages: '@babel/parser': 7.28.4 '@babel/template': 7.27.2 '@babel/types': 7.28.4 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@5.5.0) transitivePeerDependencies: - supports-color @@ -7737,7 +7764,7 @@ packages: engines: {node: '>=v18'} dependencies: '@commitlint/types': 19.5.0 - semver: 7.7.3 + semver: 7.6.3 dev: true /@commitlint/lint@19.5.0: @@ -9740,7 +9767,7 @@ packages: engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} dependencies: ajv: 6.12.6 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@5.5.0) espree: 10.3.0 globals: 14.0.0 ignore: 5.3.2 @@ -9768,13 +9795,13 @@ packages: '@expo/spawn-async': 1.7.2 arg: 5.0.2 chalk: 4.1.2 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@5.5.0) find-up: 5.0.0 getenv: 1.0.0 minimatch: 3.1.2 p-limit: 3.1.0 resolve-from: 5.0.0 - semver: 7.7.3 + semver: 7.6.3 transitivePeerDependencies: - supports-color dev: true @@ -9861,7 +9888,7 @@ packages: deprecated: Use @eslint/config-array instead dependencies: '@humanwhocodes/object-schema': 2.0.3 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@5.5.0) minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -10566,7 +10593,7 @@ packages: nopt: 5.0.0 npmlog: 5.0.1 rimraf: 3.0.2 - semver: 7.7.3 + semver: 7.6.3 tar: 6.2.1 transitivePeerDependencies: - encoding @@ -11869,7 +11896,7 @@ packages: minimatch: 3.1.2 path-to-regexp: 6.3.0 ts-node: 10.9.1(@swc/core@1.7.26)(@types/node@18.16.9)(typescript@5.8.3) - ws: 8.18.3 + ws: 8.18.0 transitivePeerDependencies: - '@babel/traverse' - '@rsbuild/core' @@ -11909,7 +11936,7 @@ packages: minimatch: 3.1.2 path-to-regexp: 6.3.0 ts-node: 10.9.1(@swc/core@1.7.26)(@types/node@18.16.9)(typescript@5.8.3) - ws: 8.18.3 + ws: 8.18.0 transitivePeerDependencies: - '@babel/traverse' - '@rsbuild/core' @@ -12206,7 +12233,7 @@ packages: resolution: {integrity: sha512-ZGQup+zYHVl2RZoBJnwW/C/qNOI2ABX4B23YtyNDrmTHCk5kIHXTPScUScS7Eai637xzYfWSFeZGhfN1DOas2Q==} dependencies: '@babel/core': 7.28.0 - '@babel/preset-react': 7.27.1(@babel/core@7.28.0) + '@babel/preset-react': 7.26.3(@babel/core@7.28.0) '@babel/types': 7.28.4 '@modern-js/babel-preset': 2.68.2(@rsbuild/core@1.4.4) '@modern-js/flight-server-transform-plugin': 2.68.2 @@ -12699,7 +12726,7 @@ packages: '@module-federation/runtime-tools': 0.15.0 '@module-federation/sdk': 0.15.0 btoa: 1.2.1 - schema-utils: 4.3.2 + schema-utils: 4.3.3 typescript: 5.8.3 upath: 2.0.1 vue-tsc: 2.2.10(typescript@5.8.3) @@ -13390,7 +13417,7 @@ packages: '@open-draft/until': 1.0.3 '@types/debug': 4.1.12 '@xmldom/xmldom': 0.8.10 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@5.5.0) headers-polyfill: 3.2.5 outvariant: 1.4.3 strict-event-emitter: 0.2.8 @@ -15556,7 +15583,7 @@ packages: playwright: 1.49.1 dev: true - /@pmmmwh/react-refresh-webpack-plugin@0.5.15(react-refresh@0.14.2)(webpack@5.98.0): + /@pmmmwh/react-refresh-webpack-plugin@0.5.15(react-refresh@0.14.2)(webpack-dev-server@5.1.0)(webpack@5.98.0): resolution: {integrity: sha512-LFWllMA55pzB9D34w/wXUCf8+c+IYKuJDgxiZ3qMhl64KRMBHYM1I3VdGaD2BV5FNPV2/S2596bppxHbv2ZydQ==} engines: {node: '>= 10.13'} peerDependencies: @@ -15591,6 +15618,7 @@ packages: schema-utils: 4.3.0 source-map: 0.7.4 webpack: 5.98.0(@swc/core@1.7.26)(esbuild@0.25.0)(webpack-cli@5.1.4) + webpack-dev-server: 5.1.0(webpack-cli@5.1.4)(webpack@5.98.0) dev: true /@pmmmwh/react-refresh-webpack-plugin@0.5.16(react-refresh@0.14.2)(webpack@5.99.9): @@ -16961,7 +16989,7 @@ packages: '@react-native-community/cli': 19.1.1(typescript@5.0.4) '@react-native/dev-middleware': 0.80.0 chalk: 4.1.2 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@5.5.0) invariant: 2.2.4 metro: 0.82.5 metro-config: 0.82.5 @@ -16987,7 +17015,7 @@ packages: '@react-native-community/cli': 19.1.1(typescript@5.0.4) '@react-native/dev-middleware': 0.82.0 '@react-native/metro-config': 0.80.0(@babel/core@7.28.0) - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@5.5.0) invariant: 2.2.4 metro: 0.83.3 metro-config: 0.83.3 @@ -17051,7 +17079,7 @@ packages: chrome-launcher: 0.15.2 chromium-edge-launcher: 0.2.0 connect: 3.7.0 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@5.5.0) invariant: 2.2.4 nullthrows: 1.1.1 open: 7.4.2 @@ -17072,7 +17100,7 @@ packages: chrome-launcher: 0.15.2 chromium-edge-launcher: 0.2.0 connect: 3.7.0 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@5.5.0) invariant: 2.2.4 nullthrows: 1.1.1 open: 7.4.2 @@ -20718,7 +20746,7 @@ packages: conventional-changelog-writer: 8.2.0 conventional-commits-filter: 5.0.0 conventional-commits-parser: 6.2.0 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@5.5.0) import-from-esm: 2.0.0 lodash-es: 4.17.21 micromatch: 4.0.8 @@ -20812,7 +20840,7 @@ packages: '@octokit/plugin-throttling': 11.0.2(@octokit/core@7.0.5) '@semantic-release/error': 4.0.0 aggregate-error: 5.0.0 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@5.5.0) dir-glob: 3.0.1 http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.6 @@ -20881,7 +20909,7 @@ packages: conventional-changelog-writer: 8.2.0 conventional-commits-filter: 5.0.0 conventional-commits-parser: 6.2.0 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@5.5.0) get-stream: 7.0.1 import-from-esm: 2.0.0 into-stream: 7.0.0 @@ -21222,7 +21250,7 @@ packages: prompts: 2.4.2 puppeteer-core: 2.1.1 read-pkg-up: 7.0.1 - semver: 7.7.3 + semver: 7.6.3 strip-json-comments: 3.1.1 tempy: 1.0.1 ts-dedent: 2.2.0 @@ -21371,14 +21399,14 @@ packages: pretty-hrtime: 1.0.3 prompts: 2.4.2 read-pkg-up: 7.0.1 - semver: 7.7.3 + semver: 7.6.3 telejson: 7.2.0 tiny-invariant: 1.3.3 ts-dedent: 2.2.0 util: 0.12.5 util-deprecate: 1.0.2 watchpack: 2.4.2 - ws: 8.18.3 + ws: 8.18.0 transitivePeerDependencies: - bufferutil - encoding @@ -21628,7 +21656,7 @@ packages: '@babel/preset-react': 7.27.1(@babel/core@7.28.0) '@babel/preset-typescript': 7.27.1(@babel/core@7.28.0) '@babel/runtime': 7.28.2 - '@pmmmwh/react-refresh-webpack-plugin': 0.5.15(react-refresh@0.14.2)(webpack@5.98.0) + '@pmmmwh/react-refresh-webpack-plugin': 0.5.15(react-refresh@0.14.2)(webpack-dev-server@5.1.0)(webpack@5.98.0) '@storybook/builder-webpack5': 9.0.9(@rspack/core@1.3.9)(@swc/core@1.7.26)(esbuild@0.25.0)(storybook@9.0.9)(typescript@5.8.3)(webpack-cli@5.1.4) '@storybook/preset-react-webpack': 9.0.9(@swc/core@1.7.26)(esbuild@0.25.0)(react-dom@18.3.1)(react@18.3.1)(storybook@9.0.9)(typescript@5.8.3)(webpack-cli@5.1.4) '@storybook/react': 9.0.9(react-dom@18.3.1)(react@18.3.1)(storybook@9.0.9)(typescript@5.8.3) @@ -21748,7 +21776,7 @@ packages: typescript: '>= 3.x' webpack: '>= 4' dependencies: - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@5.5.0) endent: 2.1.0 find-cache-dir: 3.3.2 flat-cache: 3.2.0 @@ -21767,7 +21795,7 @@ packages: typescript: '>= 4.x' webpack: '>= 4' dependencies: - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@5.5.0) endent: 2.1.0 find-cache-dir: 3.3.2 flat-cache: 3.2.0 @@ -21786,7 +21814,7 @@ packages: typescript: '>= 4.x' webpack: '>= 4' dependencies: - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@5.5.0) endent: 2.1.0 find-cache-dir: 3.3.2 flat-cache: 3.2.0 @@ -22807,7 +22835,7 @@ packages: /@types/bonjour@3.5.13: resolution: {integrity: sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ==} dependencies: - '@types/node': 18.16.9 + '@types/node': 20.12.14 /@types/btoa@1.2.5: resolution: {integrity: sha512-BItINdjZRlcGdI2efwK4bwxY5vEAT0SnIVfMOZVT18wp4900F1Lurqk/9PNdF9hMP1zgFmWbjVEtAsQKVcbqxA==} @@ -22833,7 +22861,7 @@ packages: resolution: {integrity: sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw==} dependencies: '@types/express-serve-static-core': 5.0.0 - '@types/node': 18.16.9 + '@types/node': 20.12.14 /@types/connect@3.4.38: resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} @@ -23149,7 +23177,7 @@ packages: /@types/express-serve-static-core@5.0.0: resolution: {integrity: sha512-AbXMTZGt40T+KON9/Fdxx0B2WK5hsgxcfXJLr5bFpZ7b4JCex2WyQPTEKdXqfHiY5nKKBScZ7yCoO6Pvgxfvnw==} dependencies: - '@types/node': 18.16.9 + '@types/node': 20.12.14 '@types/qs': 6.9.16 '@types/range-parser': 1.2.7 '@types/send': 0.17.4 @@ -23216,7 +23244,7 @@ packages: /@types/graceful-fs@4.1.9: resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} dependencies: - '@types/node': 18.16.9 + '@types/node': 20.12.14 /@types/har-format@1.2.16: resolution: {integrity: sha512-fluxdy7ryD3MV6h8pTfTYpy/xQzCFC7m89nOH9y94cNqJ1mDIDPut7MnRHI3F6qRmh/cT2fUjG1MLdCNb4hE9A==} @@ -23406,7 +23434,7 @@ packages: /@types/node-forge@1.3.11: resolution: {integrity: sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==} dependencies: - '@types/node': 18.16.9 + '@types/node': 20.12.14 /@types/node-schedule@2.1.7: resolution: {integrity: sha512-G7Z3R9H7r3TowoH6D2pkzUHPhcJrDF4Jz1JOQ80AX0K2DWTHoN9VC94XzFAPNMdbW9TBzMZ3LjpFi7RYdbxtXA==} @@ -23586,7 +23614,7 @@ packages: resolution: {integrity: sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==} dependencies: '@types/mime': 1.3.5 - '@types/node': 18.16.9 + '@types/node': 20.12.14 /@types/serve-index@1.9.4: resolution: {integrity: sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug==} @@ -23597,7 +23625,7 @@ packages: resolution: {integrity: sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==} dependencies: '@types/http-errors': 2.0.4 - '@types/node': 18.16.9 + '@types/node': 20.12.14 '@types/send': 0.17.4 /@types/set-cookie-parser@2.4.10: @@ -23617,7 +23645,7 @@ packages: /@types/sockjs@0.3.36: resolution: {integrity: sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q==} dependencies: - '@types/node': 18.16.9 + '@types/node': 20.12.14 /@types/source-list-map@0.1.6: resolution: {integrity: sha512-5JcVt1u5HDmlXkwOD2nslZVllBBc7HDuOICfiZah2Z0is8M8g+ddAEawbmd3VjedfDHBzxCaXLs07QEmb7y54g==} @@ -23696,12 +23724,12 @@ packages: '@typescript-eslint/scope-manager': 5.62.0 '@typescript-eslint/type-utils': 5.62.0(eslint@8.57.1)(typescript@5.0.4) '@typescript-eslint/utils': 5.62.0(eslint@8.57.1)(typescript@5.0.4) - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@5.5.0) eslint: 8.57.1 graphemer: 1.4.0 ignore: 5.3.2 natural-compare-lite: 1.4.0 - semver: 7.7.3 + semver: 7.6.3 tsutils: 3.21.0(typescript@5.0.4) typescript: 5.0.4 transitivePeerDependencies: @@ -23775,7 +23803,7 @@ packages: '@typescript-eslint/scope-manager': 5.62.0 '@typescript-eslint/types': 5.62.0 '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.0.4) - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@5.5.0) eslint: 8.57.1 typescript: 5.0.4 transitivePeerDependencies: @@ -23796,7 +23824,7 @@ packages: '@typescript-eslint/types': 6.21.0 '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.8.3) '@typescript-eslint/visitor-keys': 6.21.0 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@5.5.0) eslint: 8.57.1 typescript: 5.8.3 transitivePeerDependencies: @@ -23817,7 +23845,7 @@ packages: '@typescript-eslint/types': 6.21.0 '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.4.5) '@typescript-eslint/visitor-keys': 6.21.0 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@5.5.0) eslint: 9.0.0 typescript: 5.4.5 transitivePeerDependencies: @@ -23918,7 +23946,7 @@ packages: dependencies: '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.0.4) '@typescript-eslint/utils': 5.62.0(eslint@8.57.1)(typescript@5.0.4) - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@5.5.0) eslint: 8.57.1 tsutils: 3.21.0(typescript@5.0.4) typescript: 5.0.4 @@ -23938,7 +23966,7 @@ packages: dependencies: '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.0.4) '@typescript-eslint/utils': 7.18.0(eslint@8.57.1)(typescript@5.0.4) - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@5.5.0) eslint: 8.57.1 ts-api-utils: 1.3.0(typescript@5.0.4) typescript: 5.0.4 @@ -23958,7 +23986,7 @@ packages: dependencies: '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.8.3) '@typescript-eslint/utils': 7.18.0(eslint@8.57.1)(typescript@5.8.3) - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@5.5.0) eslint: 8.57.1 ts-api-utils: 1.3.0(typescript@5.8.3) typescript: 5.8.3 @@ -23977,7 +24005,7 @@ packages: dependencies: '@typescript-eslint/typescript-estree': 8.8.0(typescript@5.8.3) '@typescript-eslint/utils': 8.8.0(eslint@8.57.1)(typescript@5.8.3) - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@5.5.0) ts-api-utils: 1.3.0(typescript@5.8.3) typescript: 5.8.3 transitivePeerDependencies: @@ -24021,7 +24049,7 @@ packages: dependencies: '@typescript-eslint/types': 5.62.0 '@typescript-eslint/visitor-keys': 5.62.0 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@5.5.0) globby: 11.1.0 is-glob: 4.0.3 semver: 7.6.3 @@ -24042,11 +24070,11 @@ packages: dependencies: '@typescript-eslint/types': 6.21.0 '@typescript-eslint/visitor-keys': 6.21.0 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@5.5.0) globby: 11.1.0 is-glob: 4.0.3 minimatch: 9.0.3 - semver: 7.7.3 + semver: 7.6.3 ts-api-utils: 1.3.0(typescript@5.4.5) typescript: 5.4.5 transitivePeerDependencies: @@ -24064,11 +24092,11 @@ packages: dependencies: '@typescript-eslint/types': 6.21.0 '@typescript-eslint/visitor-keys': 6.21.0 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@5.5.0) globby: 11.1.0 is-glob: 4.0.3 minimatch: 9.0.3 - semver: 7.7.3 + semver: 7.6.3 ts-api-utils: 1.3.0(typescript@5.8.3) typescript: 5.8.3 transitivePeerDependencies: @@ -24086,7 +24114,7 @@ packages: dependencies: '@typescript-eslint/types': 7.18.0 '@typescript-eslint/visitor-keys': 7.18.0 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@5.5.0) globby: 11.1.0 is-glob: 4.0.3 minimatch: 9.0.5 @@ -24108,7 +24136,7 @@ packages: dependencies: '@typescript-eslint/types': 7.18.0 '@typescript-eslint/visitor-keys': 7.18.0 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@5.5.0) globby: 11.1.0 is-glob: 4.0.3 minimatch: 9.0.5 @@ -24130,7 +24158,7 @@ packages: dependencies: '@typescript-eslint/types': 8.14.0 '@typescript-eslint/visitor-keys': 8.14.0 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@5.5.0) fast-glob: 3.3.2 is-glob: 4.0.3 minimatch: 9.0.5 @@ -24152,7 +24180,7 @@ packages: dependencies: '@typescript-eslint/types': 8.8.0 '@typescript-eslint/visitor-keys': 8.8.0 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@5.5.0) fast-glob: 3.3.2 is-glob: 4.0.3 minimatch: 9.0.5 @@ -26046,7 +26074,7 @@ packages: resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} engines: {node: '>= 6.0.0'} dependencies: - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@5.5.0) transitivePeerDependencies: - supports-color @@ -26054,7 +26082,7 @@ packages: resolution: {integrity: sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==} engines: {node: '>= 14'} dependencies: - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@5.5.0) transitivePeerDependencies: - supports-color dev: true @@ -27077,8 +27105,8 @@ packages: dependencies: '@babel/core': 7.28.0 find-cache-dir: 4.0.0 - schema-utils: 4.3.2 - webpack: 5.98.0(@swc/core@1.7.26)(esbuild@0.24.0)(webpack-cli@5.1.4) + schema-utils: 4.3.3 + webpack: 5.98.0(@swc/core@1.7.26)(esbuild@0.25.0)(webpack-cli@5.1.4) /babel-loader@9.2.1(@babel/core@7.28.0)(webpack@5.99.9): resolution: {integrity: sha512-fqe8naHt46e0yIdkjUZYqddSXfej3AHajX+CSO5X7oy0EmPc6o5Xh+RClNoHjnieWz9AW4kZxW9yyFMhVB1QLA==} @@ -27089,8 +27117,8 @@ packages: dependencies: '@babel/core': 7.28.0 find-cache-dir: 4.0.0 - schema-utils: 4.3.2 - webpack: 5.99.9(@swc/core@1.11.31)(esbuild@0.25.5)(webpack-cli@5.1.4) + schema-utils: 4.3.3 + webpack: 5.99.9(@swc/core@1.7.26)(esbuild@0.25.0)(webpack-cli@5.1.4) dev: true /babel-plugin-apply-mdx-type-prop@1.6.22(@babel/core@7.12.9): @@ -29014,9 +29042,9 @@ packages: glob-parent: 6.0.2 globby: 12.2.0 normalize-path: 3.0.0 - schema-utils: 4.3.2 + schema-utils: 4.3.3 serialize-javascript: 6.0.2 - webpack: 5.98.0(@swc/core@1.7.26)(esbuild@0.24.0)(webpack-cli@5.1.4) + webpack: 5.98.0(@swc/core@1.7.26)(esbuild@0.25.0)(webpack-cli@5.1.4) /copy-webpack-plugin@10.2.4(webpack@5.99.9): resolution: {integrity: sha512-xFVltahqlsRcyyJqQbDY6EYTtyQZF9rf+JPjwHObLdPFMEISqkFkr7mFoVOC6BfYS/dNThyoQKvziugm+OnwBg==} @@ -29028,7 +29056,7 @@ packages: glob-parent: 6.0.2 globby: 12.2.0 normalize-path: 3.0.0 - schema-utils: 4.3.2 + schema-utils: 4.3.3 serialize-javascript: 6.0.2 webpack: 5.99.9(@swc/core@1.7.26)(esbuild@0.25.0)(webpack-cli@5.1.4) dev: true @@ -29442,8 +29470,8 @@ packages: postcss-modules-scope: 3.2.0(postcss@8.4.38) postcss-modules-values: 4.0.0(postcss@8.4.38) postcss-value-parser: 4.2.0 - semver: 7.7.3 - webpack: 5.98.0(@swc/core@1.7.26)(esbuild@0.24.0)(webpack-cli@5.1.4) + semver: 7.6.3 + webpack: 5.98.0(@swc/core@1.7.26)(esbuild@0.25.0)(webpack-cli@5.1.4) /css-loader@6.11.0(@rspack/core@1.3.9)(webpack@5.99.9): resolution: {integrity: sha512-CTJ+AEQJjq5NzLga5pE39qdiSV56F8ywCIsqNIRF0r7BDgWsN25aazToqAFg7ZrtA/U016xudB3ffgweORxX7g==} @@ -29465,7 +29493,7 @@ packages: postcss-modules-scope: 3.2.0(postcss@8.4.38) postcss-modules-values: 4.0.0(postcss@8.4.38) postcss-value-parser: 4.2.0 - semver: 7.7.3 + semver: 7.6.3 webpack: 5.99.9(@swc/core@1.7.26)(esbuild@0.25.0)(webpack-cli@5.1.4) dev: true @@ -29499,7 +29527,7 @@ packages: esbuild: 0.18.20 jest-worker: 29.7.0 postcss: 8.4.38 - schema-utils: 4.3.2 + schema-utils: 4.3.3 serialize-javascript: 6.0.2 webpack: 5.99.9(@swc/core@1.11.31)(esbuild@0.18.20)(webpack-cli@5.1.4) dev: true @@ -29534,7 +29562,7 @@ packages: esbuild: 0.24.0 jest-worker: 29.7.0 postcss: 8.4.38 - schema-utils: 4.3.2 + schema-utils: 4.3.3 serialize-javascript: 6.0.2 webpack: 5.98.0(@swc/core@1.7.26)(esbuild@0.24.0)(webpack-cli@5.1.4) dev: false @@ -29569,7 +29597,7 @@ packages: esbuild: 0.25.0 jest-worker: 29.7.0 postcss: 8.4.38 - schema-utils: 4.3.2 + schema-utils: 4.3.3 serialize-javascript: 6.0.2 webpack: 5.99.9(@swc/core@1.7.26)(esbuild@0.25.0)(webpack-cli@5.1.4) dev: true @@ -29604,7 +29632,7 @@ packages: esbuild: 0.25.5 jest-worker: 29.7.0 postcss: 8.4.38 - schema-utils: 4.3.2 + schema-utils: 4.3.3 serialize-javascript: 6.0.2 webpack: 5.99.9(@swc/core@1.11.31)(esbuild@0.25.5)(webpack-cli@5.1.4) dev: true @@ -30246,6 +30274,7 @@ packages: dependencies: ms: 2.1.3 supports-color: 8.1.1 + dev: true /decamelize@1.2.0: resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} @@ -30495,7 +30524,7 @@ packages: hasBin: true dependencies: address: 1.2.2 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@5.5.0) transitivePeerDependencies: - supports-color @@ -31243,7 +31272,7 @@ packages: peerDependencies: esbuild: '>=0.12 <1' dependencies: - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@5.5.0) esbuild: 0.18.20 transitivePeerDependencies: - supports-color @@ -31254,7 +31283,7 @@ packages: peerDependencies: esbuild: '>=0.12 <1' dependencies: - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@5.5.0) esbuild: 0.24.0 transitivePeerDependencies: - supports-color @@ -31265,7 +31294,7 @@ packages: peerDependencies: esbuild: '>=0.12 <1' dependencies: - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@5.5.0) esbuild: 0.25.0 transitivePeerDependencies: - supports-color @@ -31275,7 +31304,7 @@ packages: peerDependencies: esbuild: '>=0.12 <1' dependencies: - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@5.5.0) esbuild: 0.25.5 transitivePeerDependencies: - supports-color @@ -31664,7 +31693,7 @@ packages: optional: true dependencies: '@nolyfill/is-core-module': 1.0.39 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@5.5.0) enhanced-resolve: 5.18.2 eslint: 9.0.0 eslint-module-utils: 2.12.0(@typescript-eslint/parser@6.21.0)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@9.0.0) @@ -33331,7 +33360,7 @@ packages: debug: optional: true dependencies: - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@5.5.0) /for-each@0.3.3: resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} @@ -34945,7 +34974,7 @@ packages: dependencies: '@tootallnate/once': 2.0.0 agent-base: 6.0.2 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@5.5.0) transitivePeerDependencies: - supports-color dev: true @@ -34955,7 +34984,7 @@ packages: engines: {node: '>= 14'} dependencies: agent-base: 7.1.1 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@5.5.0) transitivePeerDependencies: - supports-color dev: true @@ -34983,7 +35012,7 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: '@types/http-proxy': 1.17.15 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@5.5.0) http-proxy: 1.18.1(debug@4.4.3) is-glob: 4.0.3 is-plain-object: 5.0.0 @@ -35054,7 +35083,7 @@ packages: engines: {node: '>= 6.0.0'} dependencies: agent-base: 5.1.1 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@5.5.0) transitivePeerDependencies: - supports-color dev: true @@ -35064,7 +35093,7 @@ packages: engines: {node: '>= 6'} dependencies: agent-base: 6.0.2 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@5.5.0) transitivePeerDependencies: - supports-color @@ -35073,7 +35102,7 @@ packages: engines: {node: '>= 14'} dependencies: agent-base: 7.1.3 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@5.5.0) transitivePeerDependencies: - supports-color @@ -35260,7 +35289,7 @@ packages: resolution: {integrity: sha512-YVt14UZCgsX1vZQ3gKjkWVdBdHQ6eu3MPU1TBgL1H5orXe2+jWD006WCPPtOuwlQm10NuzOW5WawiF1Q9veW8g==} engines: {node: '>=18.20'} dependencies: - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@5.5.0) import-meta-resolve: 4.2.0 transitivePeerDependencies: - supports-color @@ -35563,7 +35592,7 @@ packages: /is-bun-module@1.2.1: resolution: {integrity: sha512-AmidtEM6D6NmUiLOvvU7+IePxjEjOzra2h0pSrsfSAcXwl/83zLLXDByafUJy9k/rKK0pvXMLdwKwGHlX2Ke6Q==} dependencies: - semver: 7.7.3 + semver: 7.6.3 dev: true /is-callable@1.2.7: @@ -36130,7 +36159,7 @@ packages: resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==} engines: {node: '>=10'} dependencies: - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@5.5.0) istanbul-lib-coverage: 3.2.2 source-map: 0.6.1 transitivePeerDependencies: @@ -36142,7 +36171,7 @@ packages: engines: {node: '>=10'} dependencies: '@jridgewell/trace-mapping': 0.3.31 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@5.5.0) istanbul-lib-coverage: 3.2.2 transitivePeerDependencies: - supports-color @@ -37122,7 +37151,7 @@ packages: content-disposition: 0.5.4 content-type: 1.0.5 cookies: 0.9.1 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@5.5.0) delegates: 1.0.0 depd: 2.0.0 destroy: 1.2.0 @@ -37153,7 +37182,7 @@ packages: content-disposition: 0.5.4 content-type: 1.0.5 cookies: 0.9.1 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@5.5.0) delegates: 1.0.0 depd: 2.0.0 destroy: 1.2.0 @@ -37329,7 +37358,7 @@ packages: webpack-sources: optional: true dependencies: - webpack: 5.98.0(@swc/core@1.7.26)(esbuild@0.24.0)(webpack-cli@5.1.4) + webpack: 5.98.0(@swc/core@1.7.26)(esbuild@0.25.0)(webpack-cli@5.1.4) webpack-sources: 3.2.3 /license-webpack-plugin@4.0.2(webpack@5.99.9): @@ -38398,7 +38427,7 @@ packages: resolution: {integrity: sha512-vpMDxkGIB+MTN8Af5hvSAanc6zXQipsAUO+XUx3PCQieKUfLwdoa8qaZ1WAQYRpaU+CJ8vhBcxtzzo3d9IsCIQ==} engines: {node: '>=18.18'} dependencies: - debug: 4.4.1(supports-color@5.5.0) + debug: 4.4.3(supports-color@5.5.0) fb-watchman: 2.0.2 flow-enums-runtime: 0.0.6 graceful-fs: 4.2.11 @@ -38414,7 +38443,7 @@ packages: resolution: {integrity: sha512-jg5AcyE0Q9Xbbu/4NAwwZkmQn7doJCKGW0SLeSJmzNB9Z24jBe0AL2PHNMy4eu0JiKtNWHz9IiONGZWq7hjVTA==} engines: {node: '>=20.19.4'} dependencies: - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@5.5.0) fb-watchman: 2.0.2 flow-enums-runtime: 0.0.6 graceful-fs: 4.2.11 @@ -38672,7 +38701,7 @@ packages: chalk: 4.1.2 ci-info: 2.0.0 connect: 3.7.0 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@5.5.0) error-stack-parser: 2.1.4 flow-enums-runtime: 0.0.6 graceful-fs: 4.2.11 @@ -39026,7 +39055,7 @@ packages: resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==} dependencies: '@types/debug': 4.1.12 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@5.5.0) decode-named-character-reference: 1.0.2 devlop: 1.1.0 micromark-core-commonmark: 2.0.3 @@ -39189,7 +39218,7 @@ packages: peerDependencies: webpack: ^5.0.0 dependencies: - schema-utils: 4.3.2 + schema-utils: 4.3.3 webpack: 5.98.0(@swc/core@1.7.26)(esbuild@0.24.0)(webpack-cli@5.1.4) dev: false @@ -39199,10 +39228,21 @@ packages: peerDependencies: webpack: ^5.0.0 dependencies: - schema-utils: 4.3.2 + schema-utils: 4.3.3 webpack: 5.99.9(@swc/core@1.7.26)(esbuild@0.25.0)(webpack-cli@5.1.4) dev: true + /mini-css-extract-plugin@2.9.2(webpack@5.98.0): + resolution: {integrity: sha512-GJuACcS//jtq4kCtd5ii/M0SZf7OZRH+BxdqXZHaJfb8TJiVl+NgQRPwiYt2EuqeSkNydn/7vP+bcE27C5mb9w==} + engines: {node: '>= 12.13.0'} + peerDependencies: + webpack: ^5.0.0 + dependencies: + schema-utils: 4.3.3 + tapable: 2.2.1 + webpack: 5.98.0(@swc/core@1.7.26)(esbuild@0.25.0)(webpack-cli@5.1.4) + dev: true + /mini-css-extract-plugin@2.9.2(webpack@5.99.9): resolution: {integrity: sha512-GJuACcS//jtq4kCtd5ii/M0SZf7OZRH+BxdqXZHaJfb8TJiVl+NgQRPwiYt2EuqeSkNydn/7vP+bcE27C5mb9w==} engines: {node: '>= 12.13.0'} @@ -39543,12 +39583,12 @@ packages: resolution: {integrity: sha512-OXpYvH2AQk+zN1lwT4f9UFvTHEKbd2W0eLHOWvDZN6CxYZKBev3Ij7MrHNLeE/6YvkX5lEhBD0ePXmoFyXh45g==} dependencies: '@vercel/nft': 0.27.3(encoding@0.1.13) - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@5.5.0) fs-extra: 11.3.0 mlly: 1.6.1 pkg-types: 1.3.1 pkg-up: 3.1.0 - semver: 7.7.3 + semver: 7.6.3 transitivePeerDependencies: - encoding - supports-color @@ -41846,7 +41886,7 @@ packages: cosmiconfig: 9.0.0(typescript@5.8.3) jiti: 1.21.7 postcss: 8.4.38 - semver: 7.7.3 + semver: 7.6.3 webpack: 5.98.0(@swc/core@1.7.26)(esbuild@0.25.0)(webpack-cli@5.1.4) transitivePeerDependencies: - typescript @@ -43145,7 +43185,7 @@ packages: engines: {node: '>=8.16.0'} dependencies: '@types/mime-types': 2.1.4 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@5.5.0) extract-zip: 1.7.0 https-proxy-agent: 4.0.0 mime: 2.6.0 @@ -47158,15 +47198,6 @@ packages: ajv-formats: 2.1.1(ajv@8.17.1) ajv-keywords: 5.1.0(ajv@8.17.1) - /schema-utils@4.3.2: - resolution: {integrity: sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==} - engines: {node: '>= 10.13.0'} - dependencies: - '@types/json-schema': 7.0.15 - ajv: 8.17.1 - ajv-formats: 2.1.1(ajv@8.17.1) - ajv-keywords: 5.1.0(ajv@8.17.1) - /schema-utils@4.3.3: resolution: {integrity: sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==} engines: {node: '>= 10.13.0'} @@ -47237,7 +47268,7 @@ packages: '@semantic-release/release-notes-generator': 14.1.0(semantic-release@24.2.9) aggregate-error: 5.0.0 cosmiconfig: 9.0.0(typescript@5.8.3) - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@5.5.0) env-ci: 11.2.0 execa: 9.6.0 figures: 6.1.0 @@ -47329,6 +47360,7 @@ packages: resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} engines: {node: '>=10'} hasBin: true + dev: true /send@0.19.0: resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==} @@ -47354,7 +47386,7 @@ packages: resolution: {integrity: sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==} engines: {node: '>= 18'} dependencies: - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@5.5.0) encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 @@ -47841,7 +47873,7 @@ packages: dependencies: iconv-lite: 0.6.3 source-map-js: 1.2.1 - webpack: 5.98.0(@swc/core@1.7.26)(esbuild@0.24.0)(webpack-cli@5.1.4) + webpack: 5.98.0(@swc/core@1.7.26)(esbuild@0.25.0)(webpack-cli@5.1.4) /source-map-loader@5.0.0(webpack@5.99.9): resolution: {integrity: sha512-k2Dur7CbSLcAH73sBcIkV5xjPV4SzqO1NJ7+XaQl8if3VODDUj3FNchNGpqgJSKbvUfJuhVdv8K2Eu8/TNl2eA==} @@ -47970,7 +48002,7 @@ packages: /spdy-transport@3.0.0: resolution: {integrity: sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==} dependencies: - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@5.5.0) detect-node: 2.1.0 hpack.js: 2.1.6 obuf: 1.1.2 @@ -47983,7 +48015,7 @@ packages: resolution: {integrity: sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==} engines: {node: '>=6.0.0'} dependencies: - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@5.5.0) handle-thing: 2.0.1 http-deceiver: 1.2.7 select-hose: 2.0.0 @@ -48336,7 +48368,7 @@ packages: engines: {node: '>=8.0'} dependencies: date-format: 4.0.14 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@5.5.0) fs-extra: 8.1.0 transitivePeerDependencies: - supports-color @@ -48610,7 +48642,7 @@ packages: peerDependencies: webpack: ^5.0.0 dependencies: - webpack: 5.98.0(@swc/core@1.7.26)(esbuild@0.24.0)(webpack-cli@5.1.4) + webpack: 5.98.0(@swc/core@1.7.26)(esbuild@0.25.0)(webpack-cli@5.1.4) /style-loader@3.3.4(webpack@5.99.9): resolution: {integrity: sha512-0WqXzrsMTyb8yjZJHDqwmnwRJvhALK9LfRtRc6B4UTWe8AijYLZYZ9thuJTZc2VfQWINADW/j+LiJnfy2RoC1w==} @@ -48829,7 +48861,7 @@ packages: hasBin: true dependencies: '@adobe/css-tools': 4.3.3 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@5.5.0) glob: 10.4.5 sax: 1.4.1 source-map: 0.7.4 @@ -49350,7 +49382,7 @@ packages: '@swc/core': 1.11.31(@swc/helpers@0.5.17) esbuild: 0.18.20 jest-worker: 27.5.1 - schema-utils: 4.3.2 + schema-utils: 4.3.3 serialize-javascript: 6.0.2 terser: 5.37.0 webpack: 5.99.9(@swc/core@1.11.31)(esbuild@0.18.20)(webpack-cli@5.1.4) @@ -49376,7 +49408,7 @@ packages: '@swc/core': 1.11.31(@swc/helpers@0.5.17) esbuild: 0.25.5 jest-worker: 27.5.1 - schema-utils: 4.3.2 + schema-utils: 4.3.3 serialize-javascript: 6.0.2 terser: 5.37.0 webpack: 5.99.9(@swc/core@1.11.31)(esbuild@0.25.5)(webpack-cli@5.1.4) @@ -49402,7 +49434,7 @@ packages: '@swc/core': 1.7.26(@swc/helpers@0.5.13) esbuild: 0.18.20 jest-worker: 27.5.1 - schema-utils: 4.3.2 + schema-utils: 4.3.3 serialize-javascript: 6.0.2 terser: 5.37.0 webpack: 5.98.0(@swc/core@1.7.26)(esbuild@0.18.20)(webpack-cli@5.1.4) @@ -49428,7 +49460,7 @@ packages: '@swc/core': 1.7.26(@swc/helpers@0.5.13) esbuild: 0.24.0 jest-worker: 27.5.1 - schema-utils: 4.3.2 + schema-utils: 4.3.3 serialize-javascript: 6.0.2 terser: 5.37.0 webpack: 5.98.0(@swc/core@1.7.26)(esbuild@0.24.0)(webpack-cli@5.1.4) @@ -49453,7 +49485,7 @@ packages: '@swc/core': 1.7.26(@swc/helpers@0.5.13) esbuild: 0.25.0 jest-worker: 27.5.1 - schema-utils: 4.3.2 + schema-utils: 4.3.3 serialize-javascript: 6.0.2 terser: 5.37.0 webpack: 5.98.0(@swc/core@1.7.26)(esbuild@0.25.0)(webpack-cli@5.1.4) @@ -49478,7 +49510,7 @@ packages: '@swc/core': 1.7.26(@swc/helpers@0.5.13) esbuild: 0.25.0 jest-worker: 27.5.1 - schema-utils: 4.3.2 + schema-utils: 4.3.3 serialize-javascript: 6.0.2 terser: 5.37.0 webpack: 5.99.9(@swc/core@1.7.26)(esbuild@0.25.0)(webpack-cli@5.1.4) @@ -49504,7 +49536,7 @@ packages: '@swc/core': 1.7.26(@swc/helpers@0.5.13) esbuild: 0.25.5 jest-worker: 27.5.1 - schema-utils: 4.3.2 + schema-utils: 4.3.3 serialize-javascript: 6.0.2 terser: 5.37.0 webpack: 5.99.9(@swc/core@1.7.26)(esbuild@0.25.5)(webpack-cli@5.1.4) @@ -50073,7 +50105,7 @@ packages: chalk: 4.1.2 enhanced-resolve: 5.18.2 micromatch: 4.0.8 - semver: 7.7.3 + semver: 7.6.3 typescript: 5.0.4 webpack: 5.99.9(@swc/core@1.11.31)(esbuild@0.25.5)(webpack-cli@5.1.4) dev: true @@ -50088,7 +50120,7 @@ packages: chalk: 4.1.2 enhanced-resolve: 5.18.2 micromatch: 4.0.8 - semver: 7.7.3 + semver: 7.6.3 typescript: 5.5.2 webpack: 5.99.9(@swc/core@1.11.31)(esbuild@0.25.5)(webpack-cli@5.1.4) dev: true @@ -51470,7 +51502,7 @@ packages: hasBin: true dependencies: cac: 6.7.14 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@5.5.0) pathe: 1.1.2 picocolors: 1.1.1 vite: 5.4.20(@types/node@20.12.14)(less@4.4.2)(stylus@0.64.0) @@ -51492,7 +51524,7 @@ packages: hasBin: true dependencies: cac: 6.7.14 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@5.5.0) pathe: 1.1.2 picocolors: 1.1.1 vite: 5.4.20(@types/node@18.16.9)(less@4.4.2)(stylus@0.64.0) @@ -51886,7 +51918,7 @@ packages: peerDependencies: eslint: '>=6.0.0' dependencies: - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3(supports-color@5.5.0) eslint: 8.57.1 eslint-scope: 7.2.2 eslint-visitor-keys: 3.4.3 @@ -52169,7 +52201,7 @@ packages: mime-types: 2.1.35 on-finished: 2.4.1 range-parser: 1.2.1 - schema-utils: 4.3.0 + schema-utils: 4.3.3 webpack: 5.98.0(@swc/core@1.7.26)(esbuild@0.25.0)(webpack-cli@5.1.4) /webpack-dev-middleware@7.4.2(webpack@5.99.9): @@ -52186,7 +52218,7 @@ packages: mime-types: 2.1.35 on-finished: 2.4.1 range-parser: 1.2.1 - schema-utils: 4.3.0 + schema-utils: 4.3.3 webpack: 5.99.9(@swc/core@1.7.26)(esbuild@0.25.0)(webpack-cli@5.1.4) dev: true @@ -52224,12 +52256,12 @@ packages: launch-editor: 2.9.1 open: 10.1.0 p-retry: 6.2.0 - schema-utils: 4.3.2 + schema-utils: 4.3.3 selfsigned: 2.4.1 serve-index: 1.9.1 sockjs: 0.3.24 spdy: 4.0.2 - webpack: 5.98.0(@swc/core@1.7.26)(esbuild@0.24.0)(webpack-cli@5.1.4) + webpack: 5.98.0(@swc/core@1.7.26)(esbuild@0.25.0)(webpack-cli@5.1.4) webpack-cli: 5.1.4(webpack@5.98.0) webpack-dev-middleware: 7.4.2(webpack@5.98.0) ws: 8.18.0 @@ -52238,7 +52270,6 @@ packages: - debug - supports-color - utf-8-validate - dev: false /webpack-dev-server@5.2.0(webpack-cli@5.1.4)(webpack@5.98.0): resolution: {integrity: sha512-90SqqYXA2SK36KcT6o1bvwvZfJFcmoamqeJY7+boioffX9g9C0wjjJRGUrQIuh43pb0ttX7+ssavmj/WN2RHtA==} @@ -52264,7 +52295,7 @@ packages: bonjour-service: 1.2.1 chokidar: 3.6.0 colorette: 2.0.20 - compression: 1.7.4 + compression: 1.8.0 connect-history-api-fallback: 2.0.0 express: 4.21.2 graceful-fs: 4.2.11 @@ -52273,7 +52304,7 @@ packages: launch-editor: 2.9.1 open: 10.1.0 p-retry: 6.2.0 - schema-utils: 4.3.0 + schema-utils: 4.3.3 selfsigned: 2.4.1 serve-index: 1.9.1 sockjs: 0.3.24 @@ -52331,7 +52362,7 @@ packages: webpack: 5.99.9(@swc/core@1.7.26)(esbuild@0.25.0)(webpack-cli@5.1.4) webpack-cli: 5.1.4(webpack@5.98.0) webpack-dev-middleware: 7.4.2(webpack@5.99.9) - ws: 8.18.3 + ws: 8.18.0 transitivePeerDependencies: - bufferutil - debug @@ -52600,7 +52631,7 @@ packages: loader-runner: 4.3.0 mime-types: 2.1.35 neo-async: 2.6.2 - schema-utils: 4.3.2 + schema-utils: 4.3.3 tapable: 2.2.1 terser-webpack-plugin: 5.3.14(@swc/core@1.7.26)(esbuild@0.18.20)(webpack@5.98.0) watchpack: 2.4.2 @@ -52640,7 +52671,7 @@ packages: loader-runner: 4.3.0 mime-types: 2.1.35 neo-async: 2.6.2 - schema-utils: 4.3.2 + schema-utils: 4.3.3 tapable: 2.2.1 terser-webpack-plugin: 5.3.14(@swc/core@1.7.26)(esbuild@0.24.0)(webpack@5.98.0) watchpack: 2.4.2 @@ -52679,7 +52710,7 @@ packages: loader-runner: 4.3.0 mime-types: 2.1.35 neo-async: 2.6.2 - schema-utils: 4.3.2 + schema-utils: 4.3.3 tapable: 2.2.1 terser-webpack-plugin: 5.3.14(@swc/core@1.7.26)(esbuild@0.25.0)(webpack@5.98.0) watchpack: 2.4.2 @@ -53116,19 +53147,6 @@ packages: utf-8-validate: optional: true - /ws@8.18.3: - resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==} - engines: {node: '>=10.0.0'} - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: '>=5.0.2' - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - dev: true - /xdg-app-paths@5.1.0: resolution: {integrity: sha512-RAQ3WkPf4KTU1A8RtFx3gWywzVKe00tfOPFfl2NDGqbIFENQO4kqAJp7mhQjNj/33W5x5hiWWUdyfPq/5SU3QA==} engines: {node: '>=6'} diff --git a/tools/scripts/run-next-e2e.mjs b/tools/scripts/run-next-e2e.mjs new file mode 100755 index 00000000000..20b4e9f94b3 --- /dev/null +++ b/tools/scripts/run-next-e2e.mjs @@ -0,0 +1,327 @@ +#!/usr/bin/env node +import { spawn } from 'node:child_process'; + +const NEXT_WAIT_TARGETS = ['tcp:3000', 'tcp:3001', 'tcp:3002']; + +const KILL_PORT_ARGS = ['npx', 'kill-port', '3000', '3001', '3002']; + +const SCENARIOS = { + dev: { + label: 'Next.js development', + serveCmd: ['pnpm', 'run', 'app:next:dev'], + e2eCmd: ['npx', 'nx', 'run', '3000-home:e2e:development'], + waitTargets: NEXT_WAIT_TARGETS, + }, + prod: { + label: 'Next.js production', + buildCmd: ['pnpm', 'run', 'app:next:build'], + serveCmd: ['pnpm', 'run', 'app:next:prod'], + e2eCmd: ['npx', 'nx', 'run', '3000-home:e2e:production'], + waitTargets: NEXT_WAIT_TARGETS, + }, +}; + +const VALID_MODES = new Set(['dev', 'prod', 'all']); + +async function main() { + const modeArg = process.argv.find((arg) => arg.startsWith('--mode=')); + const mode = modeArg ? modeArg.split('=')[1] : 'dev'; + + if (!VALID_MODES.has(mode)) { + console.error( + `Unknown mode "${mode}". Expected one of ${Array.from(VALID_MODES).join(', ')}`, + ); + process.exitCode = 1; + return; + } + + // Kill ports at the very start to ensure clean slate + console.log('[next-e2e] Killing ports at startup...'); + await runKillPort(); + + const targets = mode === 'all' ? ['dev', 'prod'] : [mode]; + + for (const target of targets) { + await runScenario(target); + } + + // Kill ports at the end to clean up + console.log('[next-e2e] Killing ports at shutdown...'); + await runKillPort(); +} + +async function runScenario(name) { + const scenario = SCENARIOS[name]; + if (!scenario) { + throw new Error(`Unknown scenario: ${name}`); + } + + console.log(`\n[next-e2e] Starting ${scenario.label}`); + + await runKillPort(); + + // Build first if production mode + if (scenario.buildCmd) { + console.log(`[next-e2e] Building Next.js apps for production...`); + const { promise: buildPromise } = spawnWithPromise( + scenario.buildCmd[0], + scenario.buildCmd.slice(1), + ); + await buildPromise; + } + + const serve = spawn(scenario.serveCmd[0], scenario.serveCmd.slice(1), { + stdio: 'inherit', + detached: true, + env: { + ...process.env, + NEXT_PRIVATE_LOCAL_WEBPACK: 'true', + }, + }); + + let serveExitInfo; + let shutdownRequested = false; + + const serveExitPromise = new Promise((resolve, reject) => { + serve.on('exit', (code, signal) => { + serveExitInfo = { code, signal }; + resolve(serveExitInfo); + }); + serve.on('error', reject); + }); + + try { + await runGuardedCommand( + 'waiting for Next.js dev servers', + serveExitPromise, + () => spawnWithPromise('npx', ['wait-on', ...scenario.waitTargets]), + () => shutdownRequested, + ); + + console.log(`[next-e2e] All servers are ready, running e2e tests...`); + + await runGuardedCommand( + 'running Next.js e2e tests', + serveExitPromise, + () => spawnWithPromise(scenario.e2eCmd[0], scenario.e2eCmd.slice(1)), + () => shutdownRequested, + ); + } finally { + shutdownRequested = true; + + let serveExitError = null; + try { + await shutdownServe(serve, serveExitPromise); + } catch (error) { + console.error('[next-e2e] Serve command emitted error:', error); + serveExitError = error; + } + + await runKillPort(); + + if (serveExitError) { + throw serveExitError; + } + } + + if (!isExpectedServeExit(serveExitInfo)) { + throw new Error( + `Serve command for ${scenario.label} exited unexpectedly with ${formatExit(serveExitInfo)}`, + ); + } + + console.log(`[next-e2e] Finished ${scenario.label}`); +} + +async function runKillPort() { + const { promise } = spawnWithPromise( + KILL_PORT_ARGS[0], + KILL_PORT_ARGS.slice(1), + ); + try { + await promise; + } catch (error) { + console.warn('[next-e2e] kill-port command failed:', error.message); + } +} + +function spawnWithPromise(cmd, args, options = {}) { + const child = spawn(cmd, args, { + stdio: 'inherit', + ...options, + }); + + const promise = new Promise((resolve, reject) => { + child.on('exit', (code, signal) => { + if (code === 0) { + resolve({ code, signal }); + } else { + reject( + new Error( + `${cmd} ${args.join(' ')} exited with ${formatExit({ code, signal })}`, + ), + ); + } + }); + child.on('error', reject); + }); + + return { child, promise }; +} + +async function shutdownServe(proc, exitPromise) { + if (proc.exitCode !== null || proc.signalCode !== null) { + return exitPromise; + } + + const sequence = [ + { signal: 'SIGINT', timeoutMs: 8000 }, + { signal: 'SIGTERM', timeoutMs: 5000 }, + { signal: 'SIGKILL', timeoutMs: 3000 }, + ]; + + for (const { signal, timeoutMs } of sequence) { + if (proc.exitCode !== null || proc.signalCode !== null) { + break; + } + + sendSignal(proc, signal); + + try { + await waitWithTimeout(exitPromise, timeoutMs); + break; + } catch (error) { + if (error?.name !== 'TimeoutError') { + throw error; + } + // escalate to next signal on timeout + } + } + + return exitPromise; +} + +function sendSignal(proc, signal) { + if (proc.exitCode !== null || proc.signalCode !== null) { + return; + } + + try { + process.kill(-proc.pid, signal); + } catch (error) { + if (error.code !== 'ESRCH' && error.code !== 'EPERM') { + throw error; + } + try { + proc.kill(signal); + } catch (innerError) { + if (innerError.code !== 'ESRCH') { + throw innerError; + } + } + } +} + +function waitWithTimeout(promise, timeoutMs) { + return new Promise((resolve, reject) => { + let settled = false; + + const timer = setTimeout(() => { + if (settled) { + return; + } + settled = true; + const timeoutError = new Error(`Timed out after ${timeoutMs}ms`); + timeoutError.name = 'TimeoutError'; + reject(timeoutError); + }, timeoutMs); + + promise.then( + (value) => { + if (settled) { + return; + } + settled = true; + clearTimeout(timer); + resolve(value); + }, + (error) => { + if (settled) { + return; + } + settled = true; + clearTimeout(timer); + reject(error); + }, + ); + }); +} + +function isExpectedServeExit(info) { + if (!info) { + return false; + } + + const { code, signal } = info; + + if (code === 0) { + return true; + } + + if (code === 130 || code === 137 || code === 143) { + return true; + } + + if (code == null && ['SIGINT', 'SIGTERM', 'SIGKILL'].includes(signal)) { + return true; + } + + return false; +} + +function formatExit({ code, signal }) { + const parts = []; + if (code !== null && code !== undefined) { + parts.push(`code ${code}`); + } + if (signal) { + parts.push(`signal ${signal}`); + } + return parts.length > 0 ? parts.join(', ') : 'unknown status'; +} + +main().catch((error) => { + console.error('[next-e2e] Error:', error); + process.exitCode = 1; +}); + +async function runGuardedCommand( + description, + serveExitPromise, + factory, + isShutdownRequested = () => false, +) { + const { child, promise } = factory(); + + const serveWatcher = serveExitPromise.then((info) => { + if (isShutdownRequested()) { + return info; + } + if (child.exitCode === null && child.signalCode === null) { + sendSignal(child, 'SIGINT'); + } + throw new Error( + `Serve process exited while ${description}: ${formatExit(info)}`, + ); + }); + + try { + return await Promise.race([promise, serveWatcher]); + } finally { + serveWatcher.catch(() => {}); + if (child.exitCode === null && child.signalCode === null) { + // ensure processes do not linger if the command resolved first + sendSignal(child, 'SIGINT'); + } + } +} diff --git a/tools/scripts/run-runtime-e2e.mjs b/tools/scripts/run-runtime-e2e.mjs new file mode 100755 index 00000000000..1dfcacd3c89 --- /dev/null +++ b/tools/scripts/run-runtime-e2e.mjs @@ -0,0 +1,363 @@ +#!/usr/bin/env node +import { spawn } from 'node:child_process'; + +const RUNTIME_WAIT_TARGETS = ['tcp:3005', 'tcp:3006', 'tcp:3007']; + +const KILL_PORT_ARGS = ['npx', 'kill-port', '3005', '3006', '3007']; + +const DEFAULT_CI_WAIT_MS = 10_000; + +// Marks child processes that run in their own process group so we can safely signal the group. +const DETACHED_PROCESS_GROUP = Symbol('detachedProcessGroup'); + +const SCENARIOS = { + dev: { + label: 'runtime development', + serveCmd: ['pnpm', 'run', 'app:runtime:dev'], + e2eCmd: [ + 'npx', + 'nx', + 'run-many', + '--target=test:e2e', + '--projects=3005-runtime-host', + '--parallel=1', + ], + waitTargets: RUNTIME_WAIT_TARGETS, + ciWaitMs: DEFAULT_CI_WAIT_MS, + }, +}; + +const VALID_MODES = new Set(['dev', 'all']); + +async function main() { + const modeArg = process.argv.find((arg) => arg.startsWith('--mode=')); + const mode = modeArg ? modeArg.split('=')[1] : 'all'; + + if (!VALID_MODES.has(mode)) { + console.error( + `Unknown mode "${mode}". Expected one of ${Array.from(VALID_MODES).join(', ')}`, + ); + process.exitCode = 1; + return; + } + + const targets = mode === 'all' ? ['dev'] : [mode]; + + for (const target of targets) { + await runScenario(target); + } +} + +async function runScenario(name) { + const scenario = SCENARIOS[name]; + if (!scenario) { + throw new Error(`Unknown scenario: ${name}`); + } + + console.log(`\n[runtime-e2e] Starting ${scenario.label}`); + + await runKillPort(); + + const serve = spawn(scenario.serveCmd[0], scenario.serveCmd.slice(1), { + stdio: 'inherit', + detached: true, + }); + serve[DETACHED_PROCESS_GROUP] = true; + + let serveExitInfo; + let shutdownRequested = false; + + const serveExitPromise = new Promise((resolve, reject) => { + serve.on('exit', (code, signal) => { + serveExitInfo = { code, signal }; + resolve(serveExitInfo); + }); + serve.on('error', reject); + }); + + try { + const { factory: waitFactory, note: waitFactoryNote } = + getWaitFactory(scenario); + if (waitFactoryNote) { + console.log(waitFactoryNote); + } + + await runGuardedCommand( + 'waiting for runtime demo ports', + serveExitPromise, + waitFactory, + () => shutdownRequested, + ); + + await runGuardedCommand( + 'running runtime e2e tests', + serveExitPromise, + () => spawnWithPromise(scenario.e2eCmd[0], scenario.e2eCmd.slice(1)), + () => shutdownRequested, + ); + } finally { + shutdownRequested = true; + + let serveExitError = null; + try { + await shutdownServe(serve, serveExitPromise); + } catch (error) { + console.error('[runtime-e2e] Serve command emitted error:', error); + serveExitError = error; + } + + await runKillPort(); + + if (serveExitError) { + throw serveExitError; + } + } + + if (!isExpectedServeExit(serveExitInfo)) { + throw new Error( + `Serve command for ${scenario.label} exited unexpectedly with ${formatExit(serveExitInfo)}`, + ); + } + + console.log(`[runtime-e2e] Finished ${scenario.label}`); +} + +async function runKillPort() { + const { promise } = spawnWithPromise( + KILL_PORT_ARGS[0], + KILL_PORT_ARGS.slice(1), + ); + try { + await promise; + } catch (error) { + console.warn('[runtime-e2e] kill-port command failed:', error.message); + } +} + +function spawnWithPromise(cmd, args, options = {}) { + const child = spawn(cmd, args, { + stdio: 'inherit', + ...options, + }); + if (options.detached) { + child[DETACHED_PROCESS_GROUP] = true; + } + + const promise = new Promise((resolve, reject) => { + child.on('exit', (code, signal) => { + if (code === 0) { + resolve({ code, signal }); + } else { + reject( + new Error( + `${cmd} ${args.join(' ')} exited with ${formatExit({ code, signal })}`, + ), + ); + } + }); + child.on('error', reject); + }); + + return { child, promise }; +} + +function getWaitFactory(scenario) { + const waitTargets = scenario.waitTargets ?? []; + if (!waitTargets.length) { + return { + factory: () => + spawnWithPromise(process.execPath, ['-e', 'process.exit(0)']), + }; + } + + if (process.env.CI) { + const waitMs = getCiWaitMs(scenario); + return { + factory: () => + spawnWithPromise(process.execPath, [ + '-e', + `setTimeout(() => process.exit(0), ${waitMs});`, + ]), + note: `[runtime-e2e] CI detected; sleeping for ${waitMs}ms before running runtime e2e tests`, + }; + } + + return { + factory: () => spawnWithPromise('npx', ['wait-on', ...waitTargets]), + }; +} + +function getCiWaitMs(scenario) { + const userOverride = Number.parseInt( + process.env.RUNTIME_E2E_CI_WAIT_MS ?? '', + 10, + ); + if (!Number.isNaN(userOverride) && userOverride >= 0) { + return userOverride; + } + if (typeof scenario.ciWaitMs === 'number' && scenario.ciWaitMs >= 0) { + return scenario.ciWaitMs; + } + return DEFAULT_CI_WAIT_MS; +} + +async function shutdownServe(proc, exitPromise) { + if (proc.exitCode !== null || proc.signalCode !== null) { + return exitPromise; + } + + const sequence = [ + { signal: 'SIGINT', timeoutMs: 8000 }, + { signal: 'SIGTERM', timeoutMs: 5000 }, + { signal: 'SIGKILL', timeoutMs: 3000 }, + ]; + + for (const { signal, timeoutMs } of sequence) { + if (proc.exitCode !== null || proc.signalCode !== null) { + break; + } + + sendSignal(proc, signal); + + try { + await waitWithTimeout(exitPromise, timeoutMs); + break; + } catch (error) { + if (error?.name !== 'TimeoutError') { + throw error; + } + // escalate to next signal on timeout + } + } + + return exitPromise; +} + +function sendSignal(proc, signal) { + if (proc.exitCode !== null || proc.signalCode !== null) { + return; + } + + if (proc[DETACHED_PROCESS_GROUP]) { + try { + process.kill(-proc.pid, signal); + return; + } catch (error) { + if (error.code !== 'ESRCH' && error.code !== 'EPERM') { + throw error; + } + } + } + + try { + proc.kill(signal); + } catch (error) { + if (error.code !== 'ESRCH') { + throw error; + } + } +} + +function waitWithTimeout(promise, timeoutMs) { + return new Promise((resolve, reject) => { + let settled = false; + + const timer = setTimeout(() => { + if (settled) { + return; + } + settled = true; + const timeoutError = new Error(`Timed out after ${timeoutMs}ms`); + timeoutError.name = 'TimeoutError'; + reject(timeoutError); + }, timeoutMs); + + promise.then( + (value) => { + if (settled) { + return; + } + settled = true; + clearTimeout(timer); + resolve(value); + }, + (error) => { + if (settled) { + return; + } + settled = true; + clearTimeout(timer); + reject(error); + }, + ); + }); +} + +function isExpectedServeExit(info) { + if (!info) { + return false; + } + + const { code, signal } = info; + + if (code === 0) { + return true; + } + + if (code === 130 || code === 137 || code === 143) { + return true; + } + + if (code == null && ['SIGINT', 'SIGTERM', 'SIGKILL'].includes(signal)) { + return true; + } + + return false; +} + +function formatExit({ code, signal }) { + const parts = []; + if (code !== null && code !== undefined) { + parts.push(`code ${code}`); + } + if (signal) { + parts.push(`signal ${signal}`); + } + return parts.length > 0 ? parts.join(', ') : 'unknown status'; +} + +main().catch((error) => { + console.error('[runtime-e2e] Error:', error); + process.exitCode = 1; +}); + +async function runGuardedCommand( + description, + serveExitPromise, + factory, + isShutdownRequested = () => false, +) { + const { child, promise } = factory(); + + const serveWatcher = serveExitPromise.then((info) => { + if (isShutdownRequested()) { + return info; + } + if (child.exitCode === null && child.signalCode === null) { + sendSignal(child, 'SIGINT'); + } + throw new Error( + `Serve process exited while ${description}: ${formatExit(info)}`, + ); + }); + + try { + return await Promise.race([promise, serveWatcher]); + } finally { + serveWatcher.catch(() => {}); + if (child.exitCode === null && child.signalCode === null) { + // ensure processes do not linger if the command resolved first + sendSignal(child, 'SIGINT'); + } + } +}