diff --git a/docs/generated/packages/react/generators/federate-module.json b/docs/generated/packages/react/generators/federate-module.json
index b490fdb591..147ad0c03a 100644
--- a/docs/generated/packages/react/generators/federate-module.json
+++ b/docs/generated/packages/react/generators/federate-module.json
@@ -76,6 +76,12 @@
"host": {
"type": "string",
"description": "The host / shell application for this remote."
+ },
+ "bundler": {
+ "description": "The bundler to use.",
+ "type": "string",
+ "enum": ["rspack", "webpack"],
+ "default": "rspack"
}
},
"required": ["name", "path", "remote"],
diff --git a/docs/generated/packages/react/generators/host.json b/docs/generated/packages/react/generators/host.json
index 0dd107cc2d..d1da213fac 100644
--- a/docs/generated/packages/react/generators/host.json
+++ b/docs/generated/packages/react/generators/host.json
@@ -177,6 +177,14 @@
"type": "boolean",
"default": false,
"x-priority": "internal"
+ },
+ "bundler": {
+ "description": "The bundler to use.",
+ "type": "string",
+ "enum": ["rspack", "webpack"],
+ "x-prompt": "Which bundler do you want to use to build the application?",
+ "default": "rspack",
+ "x-priority": "important"
}
},
"required": ["name"],
diff --git a/docs/generated/packages/react/generators/remote.json b/docs/generated/packages/react/generators/remote.json
index 80f0e4ad06..c9400aa7ea 100644
--- a/docs/generated/packages/react/generators/remote.json
+++ b/docs/generated/packages/react/generators/remote.json
@@ -176,6 +176,14 @@
"type": "boolean",
"default": false,
"x-priority": "internal"
+ },
+ "bundler": {
+ "description": "The bundler to use.",
+ "type": "string",
+ "enum": ["rspack", "webpack"],
+ "x-prompt": "Which bundler do you want to use to build the application?",
+ "default": "rspack",
+ "x-priority": "important"
}
},
"required": ["name"],
diff --git a/docs/generated/packages/react/generators/setup-ssr.json b/docs/generated/packages/react/generators/setup-ssr.json
index 874b7dbf48..3969e6765d 100644
--- a/docs/generated/packages/react/generators/setup-ssr.json
+++ b/docs/generated/packages/react/generators/setup-ssr.json
@@ -39,6 +39,12 @@
"hidden": true,
"description": "Extra include entries in tsconfig.",
"default": []
+ },
+ "bundler": {
+ "description": "The bundler to use.",
+ "type": "string",
+ "enum": ["rspack", "webpack"],
+ "default": "webpack"
}
},
"required": ["project"],
diff --git a/e2e/react/src/react-module-federation.rspack.test.ts b/e2e/react/src/react-module-federation.rspack.test.ts
new file mode 100644
index 0000000000..4db6867ab0
--- /dev/null
+++ b/e2e/react/src/react-module-federation.rspack.test.ts
@@ -0,0 +1,1025 @@
+import { Tree, stripIndents } from '@nx/devkit';
+import {
+ checkFilesExist,
+ cleanupProject,
+ fileExists,
+ killPorts,
+ killProcessAndPorts,
+ newProject,
+ readJson,
+ runCLI,
+ runCLIAsync,
+ runCommandUntil,
+ runE2ETests,
+ tmpProjPath,
+ uniq,
+ updateFile,
+ updateJson,
+} from '@nx/e2e/utils';
+import { join } from 'path';
+import { createTreeWithEmptyWorkspace } from 'nx/src/devkit-testing-exports';
+
+describe('React Rspack Module Federation', () => {
+ describe('Default Configuration', () => {
+ beforeAll(() => {
+ newProject({ packages: ['@nx/react'] });
+ });
+
+ // afterAll(() => cleanupProject());
+
+ it.each`
+ js
+ ${false}
+ ${true}
+ `(
+ 'should generate host and remote apps with "--js=$js"',
+ async ({ js }) => {
+ const shell = uniq('shell');
+ const remote1 = uniq('remote1');
+ const remote2 = uniq('remote2');
+ const remote3 = uniq('remote3');
+
+ runCLI(
+ `generate @nx/react:host ${shell} --remotes=${remote1},${remote2},${remote3} --bundler=rspack --e2eTestRunner=cypress --style=css --no-interactive --skipFormat --js=${js}`
+ );
+
+ checkFilesExist(
+ `apps/${shell}/module-federation.config.${js ? 'js' : 'ts'}`
+ );
+ checkFilesExist(
+ `apps/${remote1}/module-federation.config.${js ? 'js' : 'ts'}`
+ );
+ checkFilesExist(
+ `apps/${remote2}/module-federation.config.${js ? 'js' : 'ts'}`
+ );
+ checkFilesExist(
+ `apps/${remote3}/module-federation.config.${js ? 'js' : 'ts'}`
+ );
+
+ await expect(runCLIAsync(`test ${shell}`)).resolves.toMatchObject({
+ combinedOutput: expect.stringContaining(
+ 'Test Suites: 1 passed, 1 total'
+ ),
+ });
+
+ updateFile(
+ `apps/${shell}-e2e/src/integration/app.spec.${js ? 'js' : 'ts'}`,
+ stripIndents`
+ import { getGreeting } from '../support/app.po';
+
+ describe('shell app', () => {
+ it('should display welcome message', () => {
+ cy.visit('/')
+ getGreeting().contains('Welcome ${shell}');
+ });
+
+ it('should load remote 1', () => {
+ cy.visit('/${remote1}')
+ getGreeting().contains('Welcome ${remote1}');
+ });
+
+ it('should load remote 2', () => {
+ cy.visit('/${remote2}')
+ getGreeting().contains('Welcome ${remote2}');
+ });
+
+ it('should load remote 3', () => {
+ cy.visit('/${remote3}')
+ getGreeting().contains('Welcome ${remote3}');
+ });
+ });
+ `
+ );
+
+ [shell, remote1, remote2, remote3].forEach((app) => {
+ ['development', 'production'].forEach(async (configuration) => {
+ const cliOutput = runCLI(`run ${app}:build:${configuration}`);
+ expect(cliOutput).toContain('Successfully ran target');
+ });
+ });
+
+ const serveResult = await runCommandUntil(`serve ${shell}`, (output) =>
+ output.includes(`http://localhost:${readPort(shell)}`)
+ );
+
+ await killProcessAndPorts(serveResult.pid, readPort(shell));
+
+ if (runE2ETests()) {
+ const e2eResultsSwc = await runCommandUntil(
+ `e2e ${shell}-e2e --no-watch --verbose`,
+ (output) => output.includes('All specs passed!')
+ );
+
+ await killProcessAndPorts(e2eResultsSwc.pid, readPort(shell));
+
+ const e2eResultsTsNode = await runCommandUntil(
+ `e2e ${shell}-e2e --no-watch --verbose`,
+ (output) =>
+ output.includes('Successfully ran target e2e for project'),
+ {
+ env: { NX_PREFER_TS_NODE: 'true' },
+ }
+ );
+ await killProcessAndPorts(e2eResultsTsNode.pid, readPort(shell));
+ }
+ },
+ 500_000
+ );
+
+ describe('ssr', () => {
+ it('should generate host and remote apps with ssr', async () => {
+ const shell = uniq('shell');
+ const remote1 = uniq('remote1');
+ const remote2 = uniq('remote2');
+ const remote3 = uniq('remote3');
+
+ await runCLIAsync(
+ `generate @nx/react:host ${shell} --ssr --remotes=${remote1},${remote2},${remote3} --bundler=rspack --style=css --no-interactive --projectNameAndRootFormat=derived --skipFormat`
+ );
+
+ expect(readPort(shell)).toEqual(4200);
+ expect(readPort(remote1)).toEqual(4201);
+ expect(readPort(remote2)).toEqual(4202);
+ expect(readPort(remote3)).toEqual(4203);
+
+ [shell, remote1, remote2, remote3].forEach((app) => {
+ checkFilesExist(
+ `apps/${app}/module-federation.config.ts`,
+ `apps/${app}/module-federation.server.config.ts`
+ );
+ ['build', 'server'].forEach((target) => {
+ ['development', 'production'].forEach(async (configuration) => {
+ const cliOutput = runCLI(`run ${app}:${target}:${configuration}`);
+ expect(cliOutput).toContain('Successfully ran target');
+
+ await killPorts(readPort(app));
+ });
+ });
+ });
+ }, 500_000);
+
+ it('should serve remotes as static when running the host by default', async () => {
+ const shell = uniq('shell');
+ const remote1 = uniq('remote1');
+ const remote2 = uniq('remote2');
+ const remote3 = uniq('remote3');
+
+ await runCLIAsync(
+ `generate @nx/react:host ${shell} --ssr --remotes=${remote1},${remote2},${remote3} --bundler=rspack --style=css --e2eTestRunner=cypress --no-interactive --projectNameAndRootFormat=derived --skipFormat`
+ );
+
+ const serveResult = await runCommandUntil(`serve ${shell}`, (output) =>
+ output.includes(`Nx SSR Static remotes proxies started successfully`)
+ );
+
+ await killProcessAndPorts(serveResult.pid);
+ }, 500_000);
+
+ it('should serve remotes as static and they should be able to be accessed from the host', async () => {
+ const shell = uniq('shell');
+ const remote1 = uniq('remote1');
+ const remote2 = uniq('remote2');
+ const remote3 = uniq('remote3');
+
+ await runCLIAsync(
+ `generate @nx/react:host ${shell} --ssr --remotes=${remote1},${remote2},${remote3} --bundler=rspack --style=css --e2eTestRunner=cypress --no-interactive --projectNameAndRootFormat=derived --skipFormat`
+ );
+
+ const capitalize = (s: string) =>
+ s.charAt(0).toUpperCase() + s.slice(1);
+
+ updateFile(`apps/${shell}-e2e/src/e2e/app.cy.ts`, (content) => {
+ return `
+ describe('${shell}-e2e', () => {
+ beforeEach(() => cy.visit('/'));
+
+ it('should display welcome message', () => {
+ expect(cy.get('ul li').should('have.length', 4));
+ expect(cy.get('ul li').eq(0).should('have.text', 'Home'));
+ expect(cy.get('ul li').eq(1).should('have.text', '${capitalize(
+ remote1
+ )}'));
+ expect(cy.get('ul li').eq(2).should('have.text', '${capitalize(
+ remote2
+ )}'));
+ expect(cy.get('ul li').eq(3).should('have.text', '${capitalize(
+ remote3
+ )}'));
+ });
+ });
+ `;
+ });
+
+ if (runE2ETests()) {
+ const hostE2eResults = await runCommandUntil(
+ `e2e ${shell}-e2e --no-watch --verbose`,
+ (output) => output.includes('All specs passed!')
+ );
+ await killProcessAndPorts(hostE2eResults.pid);
+ }
+ }, 600_000);
+ });
+
+ it('should should support generating host and remote apps with the new name and root format', async () => {
+ const shell = uniq('shell');
+ const remote = uniq('remote');
+
+ runCLI(
+ `generate @nx/react:host ${shell} --project-name-and-root-format=as-provided --no-interactive --skipFormat`
+ );
+ runCLI(
+ `generate @nx/react:remote ${remote} --host=${shell} --bundler=rspack --project-name-and-root-format=as-provided --no-interactive --skipFormat`
+ );
+
+ const shellPort = readPort(shell);
+ const remotePort = readPort(remote);
+
+ // check files are generated without the layout directory ("apps/") and
+ // using the project name as the directory when no directory is provided
+ checkFilesExist(`${shell}/module-federation.config.ts`);
+ checkFilesExist(`${remote}/module-federation.config.ts`);
+
+ // check default generated host is built successfully
+ const buildOutputSwc = runCLI(`run ${shell}:build:development`);
+ expect(buildOutputSwc).toContain('Successfully ran target build');
+
+ const buildOutputTsNode = runCLI(`run ${shell}:build:development`, {
+ env: { NX_PREFER_TS_NODE: 'true' },
+ });
+ expect(buildOutputTsNode).toContain('Successfully ran target build');
+
+ // check serves devRemotes ok
+ const shellProcessSwc = await runCommandUntil(
+ `serve ${shell} --devRemotes=${remote} --verbose`,
+ (output) => {
+ return output.includes(
+ `All remotes started, server ready at http://localhost:${shellPort}`
+ );
+ }
+ );
+ await killProcessAndPorts(
+ shellProcessSwc.pid,
+ shellPort,
+ remotePort + 1,
+ remotePort
+ );
+
+ const shellProcessTsNode = await runCommandUntil(
+ `serve ${shell} --devRemotes=${remote} --verbose`,
+ (output) => {
+ return output.includes(
+ `All remotes started, server ready at http://localhost:${shellPort}`
+ );
+ },
+ {
+ env: { NX_PREFER_TS_NODE: 'true' },
+ }
+ );
+ await killProcessAndPorts(
+ shellProcessTsNode.pid,
+ shellPort,
+ remotePort + 1,
+ remotePort
+ );
+ }, 500_000);
+ });
+ // Federate Module
+ describe('Federate Module', () => {
+ let proj: string;
+ let tree: Tree;
+
+ beforeAll(() => {
+ tree = createTreeWithEmptyWorkspace();
+ proj = newProject();
+ });
+
+ afterAll(() => cleanupProject());
+ it('should federate a module from a library and update an existing remote', async () => {
+ const lib = uniq('lib');
+ const remote = uniq('remote');
+ const module = uniq('module');
+ const host = uniq('host');
+
+ runCLI(
+ `generate @nx/react:host ${host} --remotes=${remote} --bundler=rspack --e2eTestRunner=cypress --no-interactive --projectNameAndRootFormat=as-provided --skipFormat`
+ );
+
+ runCLI(
+ `generate @nx/js:lib ${lib} --no-interactive --projectNameAndRootFormat=as-provided --skipFormat`
+ );
+
+ // Federate Module
+ runCLI(
+ `generate @nx/react:federate-module ${lib}/src/index.ts --name=${module} --remote=${remote} --bundler=rspack --no-interactive --skipFormat`
+ );
+
+ updateFile(
+ `${lib}/src/index.ts`,
+ `export { default } from './lib/${lib}';`
+ );
+ updateFile(
+ `${lib}/src/lib/${lib}.ts`,
+ `export default function lib() { return 'Hello from ${lib}'; };`
+ );
+
+ // Update Host to use the module
+ updateFile(
+ `${host}/src/app/app.tsx`,
+ `
+ import * as React from 'react';
+ import NxWelcome from './nx-welcome';
+ import { Link, Route, Routes } from 'react-router-dom';
+
+ import myLib from '${remote}/${module}';
+
+ export function App() {
+ return (
+
+
+ My Remote Library: { myLib() }
+
+
+
+ } />
+
+
+ );
+ }
+
+ export default App;
+ `
+ );
+
+ // Update e2e test to check the module
+ updateFile(
+ `${host}-e2e/src/e2e/app.cy.ts`,
+ `
+ describe('${host}', () => {
+ beforeEach(() => cy.visit('/'));
+
+ it('should display contain the remote library', () => {
+ expect(cy.get('div.remote')).to.exist;
+ expect(cy.get('div.remote').contains('My Remote Library: Hello from ${lib}'));
+ });
+ });
+
+ `
+ );
+
+ const hostPort = readPort(host);
+ const remotePort = readPort(remote);
+
+ // Build host and remote
+ const buildOutput = runCLI(`build ${host}`);
+ const remoteOutput = runCLI(`build ${remote}`);
+
+ expect(buildOutput).toContain('Successfully ran target build');
+ expect(remoteOutput).toContain('Successfully ran target build');
+
+ if (runE2ETests()) {
+ const hostE2eResults = await runCommandUntil(
+ `e2e ${host}-e2e --no-watch --verbose`,
+ (output) => output.includes('All specs passed!')
+ );
+ await killProcessAndPorts(
+ hostE2eResults.pid,
+ hostPort,
+ hostPort + 1,
+ remotePort
+ );
+ }
+ }, 500_000);
+
+ it('should federate a module from a library and create a remote and serve it recursively', async () => {
+ const lib = uniq('lib');
+ const remote = uniq('remote');
+ const childRemote = uniq('childremote');
+ const module = uniq('module');
+ const host = uniq('host');
+
+ runCLI(
+ `generate @nx/react:host ${host} --remotes=${remote} --bundler=rspack --e2eTestRunner=cypress --no-interactive --projectNameAndRootFormat=as-provided --skipFormat`
+ );
+
+ runCLI(
+ `generate @nx/js:lib ${lib} --no-interactive --projectNameAndRootFormat=as-provided --skipFormat`
+ );
+
+ // Federate Module
+ runCLI(
+ `generate @nx/react:federate-module ${lib}/src/index.ts --name=${module} --remote=${childRemote} --bundler=rspack --no-interactive --skipFormat`
+ );
+
+ updateFile(
+ `${lib}/src/index.ts`,
+ `export { default } from './lib/${lib}';`
+ );
+ updateFile(
+ `${lib}/src/lib/${lib}.ts`,
+ `export default function lib() { return 'Hello from ${lib}'; };`
+ );
+
+ // Update Host to use the module
+ updateFile(
+ `${remote}/src/app/app.tsx`,
+ `
+ import * as React from 'react';
+ import NxWelcome from './nx-welcome';
+
+ import myLib from '${childRemote}/${module}';
+
+ export function App() {
+ return (
+
+
+ My Remote Library: { myLib() }
+
+
+
+ );
+ }
+
+ export default App;
+ `
+ );
+
+ // Update e2e test to check the module
+ updateFile(
+ `${host}-e2e/src/e2e/app.cy.ts`,
+ `
+ describe('${host}', () => {
+ beforeEach(() => cy.visit('/${remote}'));
+
+ it('should display contain the remote library', () => {
+ expect(cy.get('div.remote')).to.exist;
+ expect(cy.get('div.remote').contains('My Remote Library: Hello from ${lib}'));
+ });
+ });
+
+ `
+ );
+
+ const hostPort = readPort(host);
+ const remotePort = readPort(remote);
+ const childRemotePort = readPort(childRemote);
+
+ // Build host and remote
+ const buildOutput = runCLI(`build ${host}`);
+ const remoteOutput = runCLI(`build ${remote}`);
+
+ expect(buildOutput).toContain('Successfully ran target build');
+ expect(remoteOutput).toContain('Successfully ran target build');
+
+ if (runE2ETests()) {
+ const hostE2eResults = await runCommandUntil(
+ `e2e ${host}-e2e --no-watch --verbose`,
+ (output) => output.includes('All specs passed!')
+ );
+ await killProcessAndPorts(
+ hostE2eResults.pid,
+ hostPort,
+ hostPort + 1,
+ remotePort,
+ childRemotePort
+ );
+ }
+ }, 500_000);
+ });
+
+ describe('Independent Deployability', () => {
+ let proj: string;
+ let tree: Tree;
+
+ beforeAll(() => {
+ process.env.NX_ADD_PLUGINS = 'false';
+ tree = createTreeWithEmptyWorkspace();
+ proj = newProject();
+ });
+
+ afterAll(() => {
+ cleanupProject();
+ delete process.env.NX_ADD_PLUGINS;
+ });
+
+ it('should support promised based remotes', async () => {
+ const remote = uniq('remote');
+ const host = uniq('host');
+
+ runCLI(
+ `generate @nx/react:host ${host} --remotes=${remote} --bundler=rspack --e2eTestRunner=cypress --no-interactive --projectNameAndRootFormat=as-provided --typescriptConfiguration=false --skipFormat`
+ );
+
+ // Update remote to be loaded via script
+ updateFile(
+ `${remote}/module-federation.config.js`,
+ stripIndents`
+ module.exports = {
+ name: '${remote}',
+ library: { type: 'var', name: '${remote}' },
+ exposes: {
+ './Module': './src/remote-entry.ts',
+ },
+ };
+ `
+ );
+
+ updateFile(
+ `${remote}/webpack.config.prod.js`,
+ `module.exports = require('./webpack.config');`
+ );
+
+ // Update host to use promise based remote
+ updateFile(
+ `${host}/module-federation.config.js`,
+ `module.exports = {
+ name: '${host}',
+ library: { type: 'var', name: '${host}' },
+ remotes: [
+ [
+ '${remote}',
+ \`promise new Promise(resolve => {
+ const remoteUrl = 'http://localhost:4201/remoteEntry.js';
+ const script = document.createElement('script');
+ script.src = remoteUrl;
+ script.onload = () => {
+ const proxy = {
+ get: (request) => window.${remote}.get(request),
+ init: (arg) => {
+ try {
+ window.${remote}.init(arg);
+ } catch (e) {
+ console.log('Remote container already initialized');
+ }
+ }
+ };
+ resolve(proxy);
+ }
+ document.head.appendChild(script);
+ })\`,
+ ],
+ ],
+ };
+ `
+ );
+
+ updateFile(
+ `${host}/webpack.config.prod.js`,
+ `module.exports = require('./webpack.config');`
+ );
+
+ // Update e2e project.json
+ updateJson(`${host}-e2e/project.json`, (json) => {
+ return {
+ ...json,
+ targets: {
+ ...json.targets,
+ e2e: {
+ ...json.targets.e2e,
+ options: {
+ ...json.targets.e2e.options,
+ devServerTarget: `${host}:serve-static:production`,
+ },
+ },
+ },
+ };
+ });
+
+ // update e2e
+ updateFile(
+ `${host}-e2e/src/e2e/app.cy.ts`,
+ `
+ import { getGreeting } from '../support/app.po';
+
+ describe('${host}', () => {
+ beforeEach(() => cy.visit('/'));
+
+ it('should display welcome message', () => {
+ getGreeting().contains('Welcome ${host}');
+ });
+
+ it('should navigate to /${remote} from /', () => {
+ cy.get('a').contains('${remote[0].toUpperCase()}${remote.slice(
+ 1
+ )}').click();
+ cy.url().should('include', '/${remote}');
+ getGreeting().contains('Welcome ${remote}');
+ });
+ });
+ `
+ );
+
+ const hostPort = readPort(host);
+ const remotePort = readPort(remote);
+
+ // Build host and remote
+ const buildOutput = runCLI(`build ${host}`);
+ const remoteOutput = runCLI(`build ${remote}`);
+
+ expect(buildOutput).toContain('Successfully ran target build');
+ expect(remoteOutput).toContain('Successfully ran target build');
+
+ if (runE2ETests()) {
+ const remoteProcess = await runCommandUntil(
+ `serve-static ${remote} --no-watch --verbose`,
+ () => {
+ return true;
+ }
+ );
+ const hostE2eResults = await runCommandUntil(
+ `e2e ${host}-e2e --no-watch --verbose`,
+ (output) => output.includes('All specs passed!')
+ );
+ await killProcessAndPorts(hostE2eResults.pid, hostPort, hostPort + 1);
+ await killProcessAndPorts(remoteProcess.pid, remotePort);
+ }
+ }, 500_000);
+
+ it('should support different versions workspace libs for host and remote', async () => {
+ const shell = uniq('shell');
+ const remote = uniq('remote');
+ const lib = uniq('lib');
+
+ runCLI(
+ `generate @nx/react:host ${shell} --remotes=${remote} --bundler=rspack --e2eTestRunner=cypress --no-interactive --projectNameAndRootFormat=as-provided --skipFormat`
+ );
+
+ runCLI(
+ `generate @nx/js:lib ${lib} --importPath=@acme/${lib} --publishable=true --no-interactive --projectNameAndRootFormat=as-provided --skipFormat`
+ );
+
+ const shellPort = readPort(shell);
+ const remotePort = readPort(remote);
+
+ updateFile(
+ `${lib}/src/lib/${lib}.ts`,
+ stripIndents`
+ export const version = '0.0.1';
+ `
+ );
+
+ updateJson(`${lib}/package.json`, (json) => {
+ return {
+ ...json,
+ version: '0.0.1',
+ };
+ });
+
+ // Update host to use the lib
+ updateFile(
+ `${shell}/src/app/app.tsx`,
+ `
+ import * as React from 'react';
+
+ import NxWelcome from './nx-welcome';
+ import { version } from '@acme/${lib}';
+ import { Link, Route, Routes } from 'react-router-dom';
+
+ const About = React.lazy(() => import('${remote}/Module'));
+
+ export function App() {
+ return (
+
+
+ Lib version: { version }
+
+
+ -
+ Home
+
+
+ -
+ About
+
+
+
+ } />
+
+ } />
+
+
+ );
+ }
+
+ export default App;`
+ );
+
+ // Update remote to use the lib
+ updateFile(
+ `${remote}/src/app/app.tsx`,
+ `// eslint-disable-next-line @typescript-eslint/no-unused-vars
+
+ import styles from './app.module.css';
+ import { version } from '@acme/${lib}';
+
+ import NxWelcome from './nx-welcome';
+
+ export function App() {
+ return (
+
+
+ Lib version: { version }
+
+
+ );
+ }
+
+ export default App;`
+ );
+
+ // update remote e2e test to check the version
+ updateFile(
+ `${remote}-e2e/src/e2e/app.cy.ts`,
+ `describe('${remote}', () => {
+ beforeEach(() => cy.visit('/'));
+
+ it('should check the lib version', () => {
+ cy.get('div.remote').contains('Lib version: 0.0.1');
+ });
+ });
+ `
+ );
+
+ // update shell e2e test to check the version
+ updateFile(
+ `${shell}-e2e/src/e2e/app.cy.ts`,
+ `
+ describe('${shell}', () => {
+ beforeEach(() => cy.visit('/'));
+
+ it('should check the lib version', () => {
+ cy.get('div.home').contains('Lib version: 0.0.1');
+ });
+ });
+ `
+ );
+
+ if (runE2ETests()) {
+ // test remote e2e
+ const remoteE2eResults = await runCommandUntil(
+ `e2e ${remote}-e2e --no-watch --verbose`,
+ (output) => output.includes('All specs passed!')
+ );
+ await killProcessAndPorts(remoteE2eResults.pid, remotePort);
+
+ // test shell e2e
+ // serve remote first
+ const remoteProcess = await runCommandUntil(
+ `serve ${remote} --no-watch --verbose`,
+ (output) => {
+ return output.includes(`Loopback: http://localhost:${remotePort}/`);
+ }
+ );
+ await killProcessAndPorts(remoteProcess.pid, remotePort);
+ const shellE2eResults = await runCommandUntil(
+ `e2e ${shell}-e2e --no-watch --verbose`,
+ (output) => output.includes('All specs passed!')
+ );
+ await killProcessAndPorts(
+ shellE2eResults.pid,
+ shellPort,
+ shellPort + 1,
+ remotePort
+ );
+ }
+ }, 500_000);
+
+ it('should support host and remote with library type var', async () => {
+ const shell = uniq('shell');
+ const remote = uniq('remote');
+
+ runCLI(
+ `generate @nx/react:host ${shell} --remotes=${remote} --bundler=rspack --e2eTestRunner=cypress --project-name-and-root-format=as-provided --no-interactive --skipFormat`
+ );
+
+ const shellPort = readPort(shell);
+ const remotePort = readPort(remote);
+
+ // update host and remote to use library type var
+ updateFile(
+ `${shell}/module-federation.config.ts`,
+ stripIndents`
+ import { ModuleFederationConfig } from '@nx/webpack';
+
+ const config: ModuleFederationConfig = {
+ name: '${shell}',
+ library: { type: 'var', name: '${shell}' },
+ remotes: ['${remote}'],
+ };
+
+ export default config;
+ `
+ );
+
+ updateFile(
+ `${shell}/webpack.config.prod.ts`,
+ `export { default } from './webpack.config';`
+ );
+
+ updateFile(
+ `${remote}/module-federation.config.ts`,
+ stripIndents`
+ import { ModuleFederationConfig } from '@nx/webpack';
+
+ const config: ModuleFederationConfig = {
+ name: '${remote}',
+ library: { type: 'var', name: '${remote}' },
+ exposes: {
+ './Module': './src/remote-entry.ts',
+ },
+ };
+
+ export default config;
+ `
+ );
+
+ updateFile(
+ `${remote}/webpack.config.prod.ts`,
+ `export { default } from './webpack.config';`
+ );
+
+ // Update host e2e test to check that the remote works with library type var via navigation
+ updateFile(
+ `${shell}-e2e/src/e2e/app.cy.ts`,
+ `
+ import { getGreeting } from '../support/app.po';
+
+ describe('${shell}', () => {
+ beforeEach(() => cy.visit('/'));
+
+ it('should display welcome message', () => {
+ getGreeting().contains('Welcome ${shell}');
+
+ });
+
+ it('should navigate to /about from /', () => {
+ cy.get('a').contains('${remote[0].toUpperCase()}${remote.slice(
+ 1
+ )}').click();
+ cy.url().should('include', '/${remote}');
+ getGreeting().contains('Welcome ${remote}');
+ });
+ });
+ `
+ );
+
+ // Build host and remote
+ const buildOutput = runCLI(`build ${shell}`);
+ const remoteOutput = runCLI(`build ${remote}`);
+
+ expect(buildOutput).toContain('Successfully ran target build');
+ expect(remoteOutput).toContain('Successfully ran target build');
+
+ if (runE2ETests()) {
+ const hostE2eResultsSwc = await runCommandUntil(
+ `e2e ${shell}-e2e --no-watch --verbose`,
+ (output) => output.includes('All specs passed!')
+ );
+ await killProcessAndPorts(
+ hostE2eResultsSwc.pid,
+ shellPort,
+ shellPort + 1,
+ remotePort
+ );
+
+ const remoteE2eResultsSwc = await runCommandUntil(
+ `e2e ${remote}-e2e --no-watch --verbose`,
+ (output) => output.includes('All specs passed!')
+ );
+
+ await killProcessAndPorts(remoteE2eResultsSwc.pid, remotePort);
+
+ const hostE2eResultsTsNode = await runCommandUntil(
+ `e2e ${shell}-e2e --no-watch --verbose`,
+ (output) => output.includes('All specs passed!'),
+ { env: { NX_PREFER_TS_NODE: 'true' } }
+ );
+
+ await killProcessAndPorts(
+ hostE2eResultsTsNode.pid,
+ shellPort,
+ shellPort + 1,
+ remotePort
+ );
+
+ const remoteE2eResultsTsNode = await runCommandUntil(
+ `e2e ${remote}-e2e --no-watch --verbose`,
+ (output) => output.includes('All specs passed!'),
+ { env: { NX_PREFER_TS_NODE: 'true' } }
+ );
+
+ await killProcessAndPorts(remoteE2eResultsTsNode.pid, remotePort);
+ }
+ }, 500_000);
+ });
+
+ describe('Dynamic Module Federation', () => {
+ beforeAll(() => {
+ newProject({ packages: ['@nx/react'] });
+ });
+
+ afterAll(() => cleanupProject());
+ it('ttt should load remote dynamic module', async () => {
+ const shell = uniq('shell');
+ const remote = uniq('remote');
+ const remotePort = 4205;
+
+ runCLI(
+ `generate @nx/react:host ${shell} --remotes=${remote} --bundler=rspack --e2eTestRunner=cypress --dynamic=true --project-name-and-root-format=as-provided --no-interactive --skipFormat`
+ );
+
+ updateJson(`${remote}/project.json`, (project) => {
+ project.targets.serve.options.port = remotePort;
+ return project;
+ });
+
+ // Webpack prod config should not exists when loading dynamic modules
+ expect(
+ fileExists(`${tmpProjPath()}/${shell}/webpack.config.prod.ts`)
+ ).toBeFalsy();
+ expect(
+ fileExists(
+ `${tmpProjPath()}/${shell}/src/assets/module-federation.manifest.json`
+ )
+ ).toBeTruthy();
+
+ updateJson(
+ `${shell}/src/assets/module-federation.manifest.json`,
+ (json) => {
+ return {
+ [remote]: `http://localhost:${remotePort}`,
+ };
+ }
+ );
+
+ const manifest = readJson(
+ `${shell}/src/assets/module-federation.manifest.json`
+ );
+ expect(manifest[remote]).toBeDefined();
+ expect(manifest[remote]).toEqual('http://localhost:4205');
+
+ // update e2e
+ updateFile(
+ `${shell}-e2e/src/e2e/app.cy.ts`,
+ `
+ import { getGreeting } from '../support/app.po';
+
+ describe('${shell}', () => {
+ beforeEach(() => cy.visit('/'));
+
+ it('should display welcome message', () => {
+ getGreeting().contains('Welcome ${shell}');
+ });
+
+ it('should navigate to /${remote} from /', () => {
+ cy.get('a').contains('${remote[0].toUpperCase()}${remote.slice(
+ 1
+ )}').click();
+ cy.url().should('include', '/${remote}');
+ getGreeting().contains('Welcome ${remote}');
+ });
+ });
+ `
+ );
+
+ // Build host and remote
+ const buildOutput = runCLI(`build ${shell}`);
+ const remoteOutput = runCLI(`build ${remote}`);
+
+ expect(buildOutput).toContain('Successfully ran target build');
+ expect(remoteOutput).toContain('Successfully ran target build');
+
+ const shellPort = readPort(shell);
+
+ if (runE2ETests()) {
+ // Serve Remote since it is dynamic and won't be started with the host
+ const remoteProcess = await runCommandUntil(
+ `serve-static ${remote} --no-watch --verbose`,
+ () => {
+ return true;
+ }
+ );
+ const hostE2eResultsSwc = await runCommandUntil(
+ `e2e ${shell}-e2e --no-watch --verbose`,
+ (output) => output.includes('All specs passed!')
+ );
+
+ await killProcessAndPorts(remoteProcess.pid, remotePort);
+ await killProcessAndPorts(hostE2eResultsSwc.pid, shellPort);
+ }
+ }, 500_000);
+ });
+});
+
+function readPort(appName: string): number {
+ let config;
+ try {
+ config = readJson(join('apps', appName, 'project.json'));
+ } catch {
+ config = readJson(join(appName, 'project.json'));
+ }
+ return config.targets.serve.options.port;
+}
diff --git a/e2e/react/src/react-module-federation.test.ts b/e2e/react/src/react-module-federation.test.ts
index 271b37d73a..b5c85a1c47 100644
--- a/e2e/react/src/react-module-federation.test.ts
+++ b/e2e/react/src/react-module-federation.test.ts
@@ -40,7 +40,7 @@ describe('React Module Federation', () => {
const remote3 = uniq('remote3');
runCLI(
- `generate @nx/react:host ${shell} --remotes=${remote1},${remote2},${remote3} --e2eTestRunner=cypress --style=css --no-interactive --skipFormat --js=${js}`
+ `generate @nx/react:host ${shell} --remotes=${remote1},${remote2},${remote3} --bundler=webpack --e2eTestRunner=cypress --style=css --no-interactive --skipFormat --js=${js}`
);
checkFilesExist(
@@ -134,7 +134,7 @@ describe('React Module Federation', () => {
const remote3 = uniq('remote3');
await runCLIAsync(
- `generate @nx/react:host ${shell} --ssr --remotes=${remote1},${remote2},${remote3} --style=css --no-interactive --projectNameAndRootFormat=derived --skipFormat`
+ `generate @nx/react:host ${shell} --bundler=webpack --ssr --remotes=${remote1},${remote2},${remote3} --style=css --no-interactive --projectNameAndRootFormat=derived --skipFormat`
);
expect(readPort(shell)).toEqual(4200);
@@ -165,7 +165,7 @@ describe('React Module Federation', () => {
const remote3 = uniq('remote3');
await runCLIAsync(
- `generate @nx/react:host ${shell} --ssr --remotes=${remote1},${remote2},${remote3} --style=css --e2eTestRunner=cypress --no-interactive --projectNameAndRootFormat=derived --skipFormat`
+ `generate @nx/react:host ${shell} --bundler=webpack --ssr --remotes=${remote1},${remote2},${remote3} --style=css --e2eTestRunner=cypress --no-interactive --projectNameAndRootFormat=derived --skipFormat`
);
const serveResult = await runCommandUntil(`serve ${shell}`, (output) =>
@@ -182,7 +182,7 @@ describe('React Module Federation', () => {
const remote3 = uniq('remote3');
await runCLIAsync(
- `generate @nx/react:host ${shell} --ssr --remotes=${remote1},${remote2},${remote3} --style=css --e2eTestRunner=cypress --no-interactive --projectNameAndRootFormat=derived --skipFormat`
+ `generate @nx/react:host ${shell} --bundler=webpack --ssr --remotes=${remote1},${remote2},${remote3} --style=css --e2eTestRunner=cypress --no-interactive --projectNameAndRootFormat=derived --skipFormat`
);
const capitalize = (s: string) =>
@@ -225,10 +225,10 @@ describe('React Module Federation', () => {
const remote = uniq('remote');
runCLI(
- `generate @nx/react:host ${shell} --project-name-and-root-format=as-provided --no-interactive --skipFormat`
+ `generate @nx/react:host ${shell} --bundler=webpack --project-name-and-root-format=as-provided --no-interactive --skipFormat`
);
runCLI(
- `generate @nx/react:remote ${remote} --host=${shell} --project-name-and-root-format=as-provided --no-interactive --skipFormat`
+ `generate @nx/react:remote ${remote} --bundler=webpack --host=${shell} --project-name-and-root-format=as-provided --no-interactive --skipFormat`
);
const shellPort = readPort(shell);
@@ -301,7 +301,7 @@ describe('React Module Federation', () => {
const host = uniq('host');
runCLI(
- `generate @nx/react:host ${host} --remotes=${remote} --e2eTestRunner=cypress --no-interactive --projectNameAndRootFormat=as-provided --skipFormat`
+ `generate @nx/react:host ${host} --bundler=webpack --remotes=${remote} --e2eTestRunner=cypress --no-interactive --projectNameAndRootFormat=as-provided --skipFormat`
);
runCLI(
@@ -310,7 +310,7 @@ describe('React Module Federation', () => {
// Federate Module
runCLI(
- `generate @nx/react:federate-module ${lib}/src/index.ts --name=${module} --remote=${remote} --no-interactive --skipFormat`
+ `generate @nx/react:federate-module ${lib}/src/index.ts --bundler=webpack --name=${module} --remote=${remote} --no-interactive --skipFormat`
);
updateFile(
@@ -402,7 +402,7 @@ describe('React Module Federation', () => {
const host = uniq('host');
runCLI(
- `generate @nx/react:host ${host} --remotes=${remote} --e2eTestRunner=cypress --no-interactive --projectNameAndRootFormat=as-provided --skipFormat`
+ `generate @nx/react:host ${host} --remotes=${remote} --bundler=webpack --e2eTestRunner=cypress --no-interactive --projectNameAndRootFormat=as-provided --skipFormat`
);
runCLI(
@@ -411,7 +411,7 @@ describe('React Module Federation', () => {
// Federate Module
runCLI(
- `generate @nx/react:federate-module ${lib}/src/index.ts --name=${module} --remote=${childRemote} --no-interactive --skipFormat`
+ `generate @nx/react:federate-module ${lib}/src/index.ts --bundler=webpack --name=${module} --remote=${childRemote} --no-interactive --skipFormat`
);
updateFile(
@@ -510,7 +510,7 @@ describe('React Module Federation', () => {
const host = uniq('host');
runCLI(
- `generate @nx/react:host ${host} --remotes=${remote} --e2eTestRunner=cypress --no-interactive --projectNameAndRootFormat=as-provided --typescriptConfiguration=false --skipFormat`
+ `generate @nx/react:host ${host} --remotes=${remote} --bundler=webpack --e2eTestRunner=cypress --no-interactive --projectNameAndRootFormat=as-provided --typescriptConfiguration=false --skipFormat`
);
// Update remote to be loaded via script
@@ -644,7 +644,7 @@ describe('React Module Federation', () => {
const lib = uniq('lib');
runCLI(
- `generate @nx/react:host ${shell} --remotes=${remote} --e2eTestRunner=cypress --no-interactive --projectNameAndRootFormat=as-provided --skipFormat`
+ `generate @nx/react:host ${shell} --remotes=${remote} --bundler=webpack --e2eTestRunner=cypress --no-interactive --projectNameAndRootFormat=as-provided --skipFormat`
);
runCLI(
@@ -794,7 +794,7 @@ describe('React Module Federation', () => {
const remote = uniq('remote');
runCLI(
- `generate @nx/react:host ${shell} --remotes=${remote} --e2eTestRunner=cypress --project-name-and-root-format=as-provided --no-interactive --skipFormat`
+ `generate @nx/react:host ${shell} --remotes=${remote} --bundler=webpack --e2eTestRunner=cypress --project-name-and-root-format=as-provided --no-interactive --skipFormat`
);
const shellPort = readPort(shell);
@@ -930,7 +930,7 @@ describe('React Module Federation', () => {
const remotePort = 4205;
runCLI(
- `generate @nx/react:host ${shell} --remotes=${remote} --e2eTestRunner=cypress --dynamic=true --project-name-and-root-format=as-provided --no-interactive --skipFormat`
+ `generate @nx/react:host ${shell} --remotes=${remote} --bundler=webpack --e2eTestRunner=cypress --dynamic=true --project-name-and-root-format=as-provided --no-interactive --skipFormat`
);
updateJson(`${remote}/project.json`, (project) => {
diff --git a/packages/react/src/generators/application/application.ts b/packages/react/src/generators/application/application.ts
index 782b0d8028..7fadc80a1f 100644
--- a/packages/react/src/generators/application/application.ts
+++ b/packages/react/src/generators/application/application.ts
@@ -211,7 +211,13 @@ export async function applicationGeneratorInternal(
project: options.projectName,
main: joinPathFragments(
options.appProjectRoot,
- maybeJs(options, `src/main.tsx`)
+ maybeJs(
+ {
+ js: options.js,
+ useJsx: true,
+ },
+ `src/main.tsx`
+ )
),
tsConfig: joinPathFragments(options.appProjectRoot, 'tsconfig.app.json'),
target: 'web',
diff --git a/packages/react/src/generators/application/lib/add-routing.ts b/packages/react/src/generators/application/lib/add-routing.ts
index 95eaebe96f..f7e0fc5073 100644
--- a/packages/react/src/generators/application/lib/add-routing.ts
+++ b/packages/react/src/generators/application/lib/add-routing.ts
@@ -22,7 +22,13 @@ export function addRouting(host: Tree, options: NormalizedSchema) {
}
const appPath = joinPathFragments(
options.appProjectRoot,
- maybeJs(options, `src/app/${options.fileName}.tsx`)
+ maybeJs(
+ {
+ js: options.js,
+ useJsx: options.bundler === 'vite' || options.bundler === 'rspack',
+ },
+ `src/app/${options.fileName}.tsx`
+ )
);
const appFileContent = host.read(appPath, 'utf-8');
const appSource = tsModule.createSourceFile(
diff --git a/packages/react/src/generators/application/lib/create-application-files.ts b/packages/react/src/generators/application/lib/create-application-files.ts
index fcd079c55a..1fac2cad3e 100644
--- a/packages/react/src/generators/application/lib/create-application-files.ts
+++ b/packages/react/src/generators/application/lib/create-application-files.ts
@@ -177,7 +177,7 @@ export async function createApplicationFiles(
if (options.js) {
toJS(host, {
- useJsx: options.bundler === 'vite',
+ useJsx: options.bundler === 'vite' || options.bundler === 'rspack',
});
}
@@ -204,7 +204,13 @@ function createNxWebpackPluginOptions(
),
index: './src/index.html',
baseHref: '/',
- main: maybeJs(options, `./src/main.tsx`),
+ main: maybeJs(
+ {
+ js: options.js,
+ useJsx: options.bundler === 'vite' || options.bundler === 'rspack',
+ },
+ `./src/main.tsx`
+ ),
tsConfig: './tsconfig.app.json',
assets: ['./src/favicon.ico', './src/assets'],
styles:
diff --git a/packages/react/src/generators/federate-module/federate-module.spec.ts b/packages/react/src/generators/federate-module/federate-module.spec.ts
index 237f2de9a1..0b1782f163 100644
--- a/packages/react/src/generators/federate-module/federate-module.spec.ts
+++ b/packages/react/src/generators/federate-module/federate-module.spec.ts
@@ -16,6 +16,7 @@ describe('federate-module', () => {
path: 'my-remote/src/my-federated-module.ts',
style: 'css',
skipFormat: true,
+ bundler: 'webpack',
};
// TODO(@jaysoo): Turn this back to adding the plugin
let originalEnv: string;
@@ -82,6 +83,7 @@ describe('federate-module', () => {
linter: Linter.EsLint,
style: 'css',
unitTestRunner: 'none',
+ bundler: 'webpack',
};
beforeEach(async () => {
diff --git a/packages/react/src/generators/federate-module/federate-module.ts b/packages/react/src/generators/federate-module/federate-module.ts
index 51556950c7..6f70af9c0e 100644
--- a/packages/react/src/generators/federate-module/federate-module.ts
+++ b/packages/react/src/generators/federate-module/federate-module.ts
@@ -6,6 +6,8 @@ import {
readJson,
runTasksInSerial,
stripIndents,
+ offsetFromRoot,
+ joinPathFragments,
} from '@nx/devkit';
import { Schema } from './schema';
@@ -39,6 +41,7 @@ export async function federateModuleGenerator(tree: Tree, schema: Schema) {
unitTestRunner: schema.unitTestRunner,
host: schema.host,
projectNameAndRootFormat: schema.projectNameAndRootFormat ?? 'derived',
+ bundler: schema.bundler ?? 'rspack',
});
tasks.push(remoteGenerator);
@@ -60,7 +63,11 @@ export async function federateModuleGenerator(tree: Tree, schema: Schema) {
}
// add path to exposes property
- addPathToExposes(tree, projectRoot, schema.name, schema.path);
+ const normalizedModulePath =
+ schema.bundler === 'rspack'
+ ? joinPathFragments(offsetFromRoot(projectRoot), schema.path)
+ : schema.path;
+ addPathToExposes(tree, projectRoot, schema.name, normalizedModulePath);
// Add new path to tsconfig
const rootJSON = readJson(tree, getRootTsConfigPathInTree(tree));
diff --git a/packages/react/src/generators/federate-module/schema.d.ts b/packages/react/src/generators/federate-module/schema.d.ts
index 923200f245..2906f803fe 100644
--- a/packages/react/src/generators/federate-module/schema.d.ts
+++ b/packages/react/src/generators/federate-module/schema.d.ts
@@ -12,4 +12,5 @@ export interface Schema {
skipFormat?: boolean;
style?: SupportedStyles;
unitTestRunner?: 'jest' | 'vitest' | 'none';
+ bundler?: 'rspack' | 'webpack';
}
diff --git a/packages/react/src/generators/federate-module/schema.json b/packages/react/src/generators/federate-module/schema.json
index 85c4868390..c2c381ed63 100644
--- a/packages/react/src/generators/federate-module/schema.json
+++ b/packages/react/src/generators/federate-module/schema.json
@@ -76,6 +76,12 @@
"host": {
"type": "string",
"description": "The host / shell application for this remote."
+ },
+ "bundler": {
+ "description": "The bundler to use.",
+ "type": "string",
+ "enum": ["rspack", "webpack"],
+ "default": "rspack"
}
},
"required": ["name", "path", "remote"],
diff --git a/packages/react/src/generators/host/__snapshots__/host.spec.ts.snap b/packages/react/src/generators/host/__snapshots__/host.webpack.spec.ts.snap
similarity index 82%
rename from packages/react/src/generators/host/__snapshots__/host.spec.ts.snap
rename to packages/react/src/generators/host/__snapshots__/host.webpack.spec.ts.snap
index 78744755a2..d0743ef5f8 100644
--- a/packages/react/src/generators/host/__snapshots__/host.spec.ts.snap
+++ b/packages/react/src/generators/host/__snapshots__/host.webpack.spec.ts.snap
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`hostGenerator should generate host files and configs for SSR 1`] = `
+exports[`hostGenerator bundler=webpack should generate host files and configs for SSR 1`] = `
"const { composePlugins, withNx } = require('@nx/webpack');
const { withReact } = require('@nx/react');
const { withModuleFederationForSSR } = require('@nx/react/module-federation');
@@ -25,7 +25,7 @@ module.exports = composePlugins(
"
`;
-exports[`hostGenerator should generate host files and configs for SSR 2`] = `
+exports[`hostGenerator bundler=webpack should generate host files and configs for SSR 2`] = `
"// @ts-check
/**
@@ -40,7 +40,7 @@ module.exports = moduleFederationConfig;
"
`;
-exports[`hostGenerator should generate host files and configs for SSR when --typescriptConfiguration=true 1`] = `
+exports[`hostGenerator bundler=webpack should generate host files and configs for SSR when --typescriptConfiguration=true 1`] = `
"import { composePlugins, withNx } from '@nx/webpack';
import { withReact } from '@nx/react';
import { withModuleFederationForSSR } from '@nx/react/module-federation';
@@ -65,7 +65,7 @@ export default composePlugins(
"
`;
-exports[`hostGenerator should generate host files and configs for SSR when --typescriptConfiguration=true 2`] = `
+exports[`hostGenerator bundler=webpack should generate host files and configs for SSR when --typescriptConfiguration=true 2`] = `
"import { ModuleFederationConfig } from '@nx/webpack';
const config: ModuleFederationConfig = {
@@ -77,7 +77,7 @@ export default config;
"
`;
-exports[`hostGenerator should generate host files and configs when --typescriptConfiguration=false 1`] = `
+exports[`hostGenerator bundler=webpack should generate host files and configs when --typescriptConfiguration=false 1`] = `
"const { composePlugins, withNx } = require('@nx/webpack');
const { withReact } = require('@nx/react');
const { withModuleFederation } = require('@nx/react/module-federation');
@@ -102,7 +102,7 @@ module.exports = composePlugins(
"
`;
-exports[`hostGenerator should generate host files and configs when --typescriptConfiguration=false 2`] = `
+exports[`hostGenerator bundler=webpack should generate host files and configs when --typescriptConfiguration=false 2`] = `
"module.exports = {
name: 'test',
/**
@@ -122,7 +122,7 @@ exports[`hostGenerator should generate host files and configs when --typescriptC
"
`;
-exports[`hostGenerator should generate host files and configs when --typescriptConfiguration=true 1`] = `
+exports[`hostGenerator bundler=webpack should generate host files and configs when --typescriptConfiguration=true 1`] = `
"import {composePlugins, withNx, ModuleFederationConfig} from '@nx/webpack';
import {withReact} from '@nx/react';
import {withModuleFederation} from '@nx/react/module-federation';
@@ -143,7 +143,7 @@ export default composePlugins(withNx(), withReact(), withModuleFederation(config
"
`;
-exports[`hostGenerator should generate host files and configs when --typescriptConfiguration=true 2`] = `
+exports[`hostGenerator bundler=webpack should generate host files and configs when --typescriptConfiguration=true 2`] = `
"import { ModuleFederationConfig } from '@nx/webpack';
const config: ModuleFederationConfig = {
diff --git a/packages/react/src/generators/host/files/rspack-common/src/app/__fileName__.jsx__tmpl__ b/packages/react/src/generators/host/files/rspack-common/src/app/__fileName__.jsx__tmpl__
new file mode 100644
index 0000000000..00edc49cc2
--- /dev/null
+++ b/packages/react/src/generators/host/files/rspack-common/src/app/__fileName__.jsx__tmpl__
@@ -0,0 +1,33 @@
+import * as React from 'react';
+<% if (!minimal) { %>
+import NxWelcome from "./nx-welcome";
+<% } %>
+import { Link, Route, Routes } from 'react-router-dom';
+
+<% if (remotes.length > 0) { %>
+<% remotes.forEach(function(r) { %>
+ const <%= r.className %> = React.lazy(() => import('<%= r.fileName %>/Module'));
+ <% }); %>
+<% } %>
+export function App() {
+ return (
+
+
+ - Home
+ <% remotes.forEach(function(r) { %>
+ - <%=r.className%>
+ <% }); %>
+
+
+ <% if (!minimal) { %>
+ } />
+ <% } %>
+ <% remotes.forEach(function(r) { %>
+ />} />
+ <% }); %>
+
+
+ );
+}
+
+export default App;
diff --git a/packages/react/src/generators/host/files/rspack-common/src/main.jsx__tmpl__ b/packages/react/src/generators/host/files/rspack-common/src/main.jsx__tmpl__
new file mode 100644
index 0000000000..a4061a83ea
--- /dev/null
+++ b/packages/react/src/generators/host/files/rspack-common/src/main.jsx__tmpl__
@@ -0,0 +1,10 @@
+<% if (dynamic) { %>
+ import { setRemoteDefinitions } from '@nx/react/mf';
+
+ fetch('/assets/module-federation.manifest.json')
+ .then((res) => res.json())
+ .then(definitions => setRemoteDefinitions(definitions))
+ .then(() => import('./bootstrap').catch(err => console.error(err)));
+<% } else { %>
+ import('./bootstrap').catch(err => console.error(err));
+<% } %>
\ No newline at end of file
diff --git a/packages/react/src/generators/remote/files/module-federation-ssr-ts/tsconfig.lint.json__tmpl__ b/packages/react/src/generators/host/files/rspack-common/tsconfig.lint.json__tmpl__
similarity index 100%
rename from packages/react/src/generators/remote/files/module-federation-ssr-ts/tsconfig.lint.json__tmpl__
rename to packages/react/src/generators/host/files/rspack-common/tsconfig.lint.json__tmpl__
diff --git a/packages/react/src/generators/host/files/rspack-module-federation-ssr-ts/module-federation.server.config.ts__tmpl__ b/packages/react/src/generators/host/files/rspack-module-federation-ssr-ts/module-federation.server.config.ts__tmpl__
new file mode 100644
index 0000000000..095da07303
--- /dev/null
+++ b/packages/react/src/generators/host/files/rspack-module-federation-ssr-ts/module-federation.server.config.ts__tmpl__
@@ -0,0 +1,13 @@
+import { ModuleFederationConfig } from '@nx/rspack/module-federation';
+
+const config: ModuleFederationConfig = {
+ name: '<%= projectName %>',
+ remotes: [
+ <% if (static) {
+ remotes.forEach(function(r) { %> "<%= r.fileName %>", <% });
+ }
+ %>
+ ],
+};
+
+export default config;
diff --git a/packages/react/src/generators/host/files/rspack-module-federation-ssr-ts/rspack.server.config.ts__tmpl__ b/packages/react/src/generators/host/files/rspack-module-federation-ssr-ts/rspack.server.config.ts__tmpl__
new file mode 100644
index 0000000000..7200fe9de1
--- /dev/null
+++ b/packages/react/src/generators/host/files/rspack-module-federation-ssr-ts/rspack.server.config.ts__tmpl__
@@ -0,0 +1,16 @@
+import {composePlugins, withNx, withReact} from '@nx/rspack';
+import {withModuleFederationForSSR} from '@nx/rspack/module-federation';
+
+import baseConfig from './module-federation.config';
+
+const defaultConfig = {
+ ...baseConfig
+};
+
+// Nx plugins for rspack to build config object from Nx options and context.
+/**
+ * DTS Plugin is disabled in Nx Workspaces as Nx already provides Typing support for Module Federation
+ * The DTS Plugin can be enabled by setting dts: true
+ * Learn more about the DTS Plugin here: https://module-federation.io/configure/dts.html
+ */
+export default composePlugins(withNx(), withReact({ssr: true}), withModuleFederationForSSR(defaultConfig, { dts: false }));
diff --git a/packages/react/src/generators/host/files/module-federation-ssr-ts/server.ts__tmpl__ b/packages/react/src/generators/host/files/rspack-module-federation-ssr-ts/server.ts__tmpl__
similarity index 100%
rename from packages/react/src/generators/host/files/module-federation-ssr-ts/server.ts__tmpl__
rename to packages/react/src/generators/host/files/rspack-module-federation-ssr-ts/server.ts__tmpl__
diff --git a/packages/react/src/generators/host/files/module-federation-ssr-ts/tsconfig.server.json__tmpl__ b/packages/react/src/generators/host/files/rspack-module-federation-ssr-ts/tsconfig.server.json__tmpl__
similarity index 100%
rename from packages/react/src/generators/host/files/module-federation-ssr-ts/tsconfig.server.json__tmpl__
rename to packages/react/src/generators/host/files/rspack-module-federation-ssr-ts/tsconfig.server.json__tmpl__
diff --git a/packages/react/src/generators/host/files/rspack-module-federation-ssr/module-federation.server.config.js__tmpl__ b/packages/react/src/generators/host/files/rspack-module-federation-ssr/module-federation.server.config.js__tmpl__
new file mode 100644
index 0000000000..d20a8c91c5
--- /dev/null
+++ b/packages/react/src/generators/host/files/rspack-module-federation-ssr/module-federation.server.config.js__tmpl__
@@ -0,0 +1,16 @@
+// @ts-check
+
+/**
+ * @type {import('@nx/rspack/module-federation').ModuleFederationConfig}
+ **/
+const moduleFederationConfig = {
+ name: '<%= projectName %>',
+ remotes: [
+ <% if (static) {
+ remotes.forEach(function(r) { %> "<%= r.fileName %>", <% });
+ }
+ %>
+ ],
+};
+
+module.exports = moduleFederationConfig;
diff --git a/packages/react/src/generators/host/files/rspack-module-federation-ssr/rspack.server.config.js__tmpl__ b/packages/react/src/generators/host/files/rspack-module-federation-ssr/rspack.server.config.js__tmpl__
new file mode 100644
index 0000000000..931aa3bac0
--- /dev/null
+++ b/packages/react/src/generators/host/files/rspack-module-federation-ssr/rspack.server.config.js__tmpl__
@@ -0,0 +1,16 @@
+const {composePlugins, withNx, withReact} = require('@nx/rspack');
+const {withModuleFederationForSSR} = require('@nx/rspack/module-federation');
+
+const baseConfig = require('./module-federation.config');
+
+const defaultConfig = {
+ ...baseConfig
+};
+
+// Nx plugins for rspack to build config object from Nx options and context.
+/**
+ * DTS Plugin is disabled in Nx Workspaces as Nx already provides Typing support for Module Federation
+ * The DTS Plugin can be enabled by setting dts: true
+ * Learn more about the DTS Plugin here: https://module-federation.io/configure/dts.html
+ */
+module.exports = composePlugins(withNx(), withReact({ssr: true}), withModuleFederationForSSR(defaultConfig, { dts: false }));
diff --git a/packages/react/src/generators/host/files/module-federation-ssr/server.ts__tmpl__ b/packages/react/src/generators/host/files/rspack-module-federation-ssr/server.ts__tmpl__
similarity index 100%
rename from packages/react/src/generators/host/files/module-federation-ssr/server.ts__tmpl__
rename to packages/react/src/generators/host/files/rspack-module-federation-ssr/server.ts__tmpl__
diff --git a/packages/react/src/generators/host/files/module-federation-ssr/tsconfig.server.json__tmpl__ b/packages/react/src/generators/host/files/rspack-module-federation-ssr/tsconfig.server.json__tmpl__
similarity index 100%
rename from packages/react/src/generators/host/files/module-federation-ssr/tsconfig.server.json__tmpl__
rename to packages/react/src/generators/host/files/rspack-module-federation-ssr/tsconfig.server.json__tmpl__
diff --git a/packages/react/src/generators/host/files/rspack-module-federation-ts/module-federation.config.ts__tmpl__ b/packages/react/src/generators/host/files/rspack-module-federation-ts/module-federation.config.ts__tmpl__
new file mode 100644
index 0000000000..b76c2881f2
--- /dev/null
+++ b/packages/react/src/generators/host/files/rspack-module-federation-ts/module-federation.config.ts__tmpl__
@@ -0,0 +1,25 @@
+import { ModuleFederationConfig } from '@nx/rspack/module-federation';
+
+const config: ModuleFederationConfig = {
+ name: '<%= projectName %>',
+ /**
+ * To use a remote that does not exist in your current Nx Workspace
+ * You can use the tuple-syntax to define your remote
+ *
+ * remotes: [['my-external-remote', 'https://nx-angular-remote.netlify.app']]
+ *
+ * You _may_ need to add a `remotes.d.ts` file to your `src/` folder declaring the external remote for tsc, with the
+ * following content:
+ *
+ * declare module 'my-external-remote';
+ *
+ */
+ remotes: [
+ <% if (static) {
+ remotes.forEach(function(r) { %> "<%= r.fileName %>", <% });
+ }
+ %>
+ ],
+};
+
+export default config;
diff --git a/packages/react/src/generators/host/files/rspack-module-federation-ts/rspack.config.prod.ts__tmpl__ b/packages/react/src/generators/host/files/rspack-module-federation-ts/rspack.config.prod.ts__tmpl__
new file mode 100644
index 0000000000..14798436c6
--- /dev/null
+++ b/packages/react/src/generators/host/files/rspack-module-federation-ts/rspack.config.prod.ts__tmpl__
@@ -0,0 +1,36 @@
+import { composePlugins, withNx, withReact } from '@nx/rspack';
+import { withModuleFederation, ModuleFederationConfig } from '@nx/rspack/module-federation';
+
+import baseConfig from './module-federation.config';
+
+const prodConfig: ModuleFederationConfig = {
+ ...baseConfig,
+ /*
+ * Remote overrides for production.
+ * Each entry is a pair of a unique name and the URL where it is deployed.
+ *
+ * e.g.
+ * remotes: [
+ * ['app1', 'http://app1.example.com'],
+ * ['app2', 'http://app2.example.com'],
+ * ]
+ *
+ * You can also use a full path to the remoteEntry.js file if desired.
+ *
+ * remotes: [
+ * ['app1', 'http://example.com/path/to/app1/remoteEntry.js'],
+ * ['app2', 'http://example.com/path/to/app2/remoteEntry.js'],
+ * ]
+ */
+ remotes: [
+ <% remotes.forEach(function(r) {%>['<%= r.fileName %>', 'http://localhost:<%= r.port %>/'],<% }); %>
+],
+};
+
+// Nx plugins for rspack to build config object from Nx options and context.
+/**
+ * DTS Plugin is disabled in Nx Workspaces as Nx already provides Typing support for Module Federation
+ * The DTS Plugin can be enabled by setting dts: true
+ * Learn more about the DTS Plugin here: https://module-federation.io/configure/dts.html
+ */
+export default composePlugins(withNx(), withReact(), withModuleFederation(prodConfig, { dts: false }));
diff --git a/packages/react/src/generators/host/files/rspack-module-federation-ts/rspack.config.ts__tmpl__ b/packages/react/src/generators/host/files/rspack-module-federation-ts/rspack.config.ts__tmpl__
new file mode 100644
index 0000000000..25f9c67bce
--- /dev/null
+++ b/packages/react/src/generators/host/files/rspack-module-federation-ts/rspack.config.ts__tmpl__
@@ -0,0 +1,16 @@
+import {composePlugins, withNx, withReact} from '@nx/rspack';
+import {withModuleFederation, ModuleFederationConfig} from '@nx/rspack/module-federation';
+
+import baseConfig from './module-federation.config';
+
+const config: ModuleFederationConfig = {
+ ...baseConfig,
+};
+
+// Nx plugins for rspack to build config object from Nx options and context.
+/**
+ * DTS Plugin is disabled in Nx Workspaces as Nx already provides Typing support for Module Federation
+ * The DTS Plugin can be enabled by setting dts: true
+ * Learn more about the DTS Plugin here: https://module-federation.io/configure/dts.html
+ */
+export default composePlugins(withNx(), withReact(), withModuleFederation(config, { dts: false }));
diff --git a/packages/react/src/generators/host/files/module-federation/module-federation.config.js__tmpl__ b/packages/react/src/generators/host/files/rspack-module-federation/module-federation.config.js__tmpl__
similarity index 100%
rename from packages/react/src/generators/host/files/module-federation/module-federation.config.js__tmpl__
rename to packages/react/src/generators/host/files/rspack-module-federation/module-federation.config.js__tmpl__
diff --git a/packages/react/src/generators/host/files/rspack-module-federation/rspack.config.js__tmpl__ b/packages/react/src/generators/host/files/rspack-module-federation/rspack.config.js__tmpl__
new file mode 100644
index 0000000000..acf84ac0a4
--- /dev/null
+++ b/packages/react/src/generators/host/files/rspack-module-federation/rspack.config.js__tmpl__
@@ -0,0 +1,16 @@
+const { composePlugins, withNx, withReact } = require('@nx/rspack');
+const { withModuleFederation } = require('@nx/rspack/module-federation');
+
+const baseConfig = require('./module-federation.config');
+
+const config = {
+ ...baseConfig,
+};
+
+// Nx plugins for rspack to build config object from Nx options and context.
+/**
+ * DTS Plugin is disabled in Nx Workspaces as Nx already provides Typing support for Module Federation
+ * The DTS Plugin can be enabled by setting dts: true
+ * Learn more about the DTS Plugin here: https://module-federation.io/configure/dts.html
+ */
+module.exports = composePlugins(withNx(), withReact(), withModuleFederation(config, { dts: false }));
diff --git a/packages/react/src/generators/host/files/rspack-module-federation/rspack.config.prod.js__tmpl__ b/packages/react/src/generators/host/files/rspack-module-federation/rspack.config.prod.js__tmpl__
new file mode 100644
index 0000000000..b3a45249e0
--- /dev/null
+++ b/packages/react/src/generators/host/files/rspack-module-federation/rspack.config.prod.js__tmpl__
@@ -0,0 +1,36 @@
+const { composePlugins, withNx, withReact } = require('@nx/rspack');
+const { withModuleFederation } = require('@nx/rspack/module-federation');
+
+const baseConfig = require('./module-federation.config');
+
+const prodConfig = {
+ ...baseConfig,
+ /*
+ * Remote overrides for production.
+ * Each entry is a pair of a unique name and the URL where it is deployed.
+ *
+ * e.g.
+ * remotes: [
+ * ['app1', 'http://app1.example.com'],
+ * ['app2', 'http://app2.example.com'],
+ * ]
+ *
+ * You can also use a full path to the remoteEntry.js file if desired.
+ *
+ * remotes: [
+ * ['app1', 'http://example.com/path/to/app1/remoteEntry.js'],
+ * ['app2', 'http://example.com/path/to/app2/remoteEntry.js'],
+ * ]
+ */
+ remotes: [
+ <% remotes.forEach(function(r) {%>['<%= r.fileName %>', 'http://localhost:<%= r.port %>/'],<% }); %>
+ ],
+};
+
+// Nx plugins for rspack to build config object from Nx options and context.
+/**
+ * DTS Plugin is disabled in Nx Workspaces as Nx already provides Typing support for Module Federation
+ * The DTS Plugin can be enabled by setting dts: true
+ * Learn more about the DTS Plugin here: https://module-federation.io/configure/dts.html
+ */
+module.exports = composePlugins(withNx(), withReact(), withModuleFederation(prodConfig, { dts: false }));
diff --git a/packages/react/src/generators/host/files/module-federation-ssr-ts/module-federation.server.config.ts__tmpl__ b/packages/react/src/generators/host/files/webpack-module-federation-ssr-ts/module-federation.server.config.ts__tmpl__
similarity index 100%
rename from packages/react/src/generators/host/files/module-federation-ssr-ts/module-federation.server.config.ts__tmpl__
rename to packages/react/src/generators/host/files/webpack-module-federation-ssr-ts/module-federation.server.config.ts__tmpl__
diff --git a/packages/react/src/generators/host/files/webpack-module-federation-ssr-ts/server.ts__tmpl__ b/packages/react/src/generators/host/files/webpack-module-federation-ssr-ts/server.ts__tmpl__
new file mode 100644
index 0000000000..ea686b7bc5
--- /dev/null
+++ b/packages/react/src/generators/host/files/webpack-module-federation-ssr-ts/server.ts__tmpl__
@@ -0,0 +1,28 @@
+import * as path from 'path';
+import express from 'express';
+import cors from 'cors';
+
+import { handleRequest } from './src/main.server';
+
+const port = process.env['PORT'] || <%= port %>;
+const app = express();
+
+const browserDist = path.join(process.cwd(), '<%= browserBuildOutputPath %>');
+const indexPath = path.join(browserDist, 'index.html');
+
+app.use(cors());
+
+app.get(
+ '*.*',
+ express.static(browserDist, {
+ maxAge: '1y',
+ })
+);
+
+app.use('*', handleRequest(indexPath));
+
+const server = app.listen(port, () => {
+ console.log(`Express server listening on http://localhost:${port}`);
+});
+
+server.on('error', console.error);
diff --git a/packages/react/src/generators/host/files/webpack-module-federation-ssr-ts/tsconfig.server.json__tmpl__ b/packages/react/src/generators/host/files/webpack-module-federation-ssr-ts/tsconfig.server.json__tmpl__
new file mode 100644
index 0000000000..db225b7a99
--- /dev/null
+++ b/packages/react/src/generators/host/files/webpack-module-federation-ssr-ts/tsconfig.server.json__tmpl__
@@ -0,0 +1,17 @@
+{
+ "extends": "./tsconfig.app.json",
+ "compilerOptions": {
+ "outDir": "../../out-tsc/server",
+ "target": "es2019",
+ "types": [
+ "node",
+ "@nx/react/typings/cssmodule.d.ts",
+ "@nx/react/typings/image.d.ts"
+ ]
+ },
+ "include": [
+ "src/remotes.d.ts",
+ "src/main.server.tsx",
+ "server.ts"
+ ]
+}
diff --git a/packages/react/src/generators/host/files/module-federation-ssr-ts/webpack.server.config.ts__tmpl__ b/packages/react/src/generators/host/files/webpack-module-federation-ssr-ts/webpack.server.config.ts__tmpl__
similarity index 100%
rename from packages/react/src/generators/host/files/module-federation-ssr-ts/webpack.server.config.ts__tmpl__
rename to packages/react/src/generators/host/files/webpack-module-federation-ssr-ts/webpack.server.config.ts__tmpl__
diff --git a/packages/react/src/generators/host/files/module-federation-ssr/module-federation.server.config.js__tmpl__ b/packages/react/src/generators/host/files/webpack-module-federation-ssr/module-federation.server.config.js__tmpl__
similarity index 100%
rename from packages/react/src/generators/host/files/module-federation-ssr/module-federation.server.config.js__tmpl__
rename to packages/react/src/generators/host/files/webpack-module-federation-ssr/module-federation.server.config.js__tmpl__
diff --git a/packages/react/src/generators/host/files/webpack-module-federation-ssr/server.ts__tmpl__ b/packages/react/src/generators/host/files/webpack-module-federation-ssr/server.ts__tmpl__
new file mode 100644
index 0000000000..ea686b7bc5
--- /dev/null
+++ b/packages/react/src/generators/host/files/webpack-module-federation-ssr/server.ts__tmpl__
@@ -0,0 +1,28 @@
+import * as path from 'path';
+import express from 'express';
+import cors from 'cors';
+
+import { handleRequest } from './src/main.server';
+
+const port = process.env['PORT'] || <%= port %>;
+const app = express();
+
+const browserDist = path.join(process.cwd(), '<%= browserBuildOutputPath %>');
+const indexPath = path.join(browserDist, 'index.html');
+
+app.use(cors());
+
+app.get(
+ '*.*',
+ express.static(browserDist, {
+ maxAge: '1y',
+ })
+);
+
+app.use('*', handleRequest(indexPath));
+
+const server = app.listen(port, () => {
+ console.log(`Express server listening on http://localhost:${port}`);
+});
+
+server.on('error', console.error);
diff --git a/packages/react/src/generators/host/files/webpack-module-federation-ssr/tsconfig.server.json__tmpl__ b/packages/react/src/generators/host/files/webpack-module-federation-ssr/tsconfig.server.json__tmpl__
new file mode 100644
index 0000000000..db225b7a99
--- /dev/null
+++ b/packages/react/src/generators/host/files/webpack-module-federation-ssr/tsconfig.server.json__tmpl__
@@ -0,0 +1,17 @@
+{
+ "extends": "./tsconfig.app.json",
+ "compilerOptions": {
+ "outDir": "../../out-tsc/server",
+ "target": "es2019",
+ "types": [
+ "node",
+ "@nx/react/typings/cssmodule.d.ts",
+ "@nx/react/typings/image.d.ts"
+ ]
+ },
+ "include": [
+ "src/remotes.d.ts",
+ "src/main.server.tsx",
+ "server.ts"
+ ]
+}
diff --git a/packages/react/src/generators/host/files/module-federation-ssr/webpack.server.config.js__tmpl__ b/packages/react/src/generators/host/files/webpack-module-federation-ssr/webpack.server.config.js__tmpl__
similarity index 100%
rename from packages/react/src/generators/host/files/module-federation-ssr/webpack.server.config.js__tmpl__
rename to packages/react/src/generators/host/files/webpack-module-federation-ssr/webpack.server.config.js__tmpl__
diff --git a/packages/react/src/generators/host/files/module-federation-ts/module-federation.config.ts__tmpl__ b/packages/react/src/generators/host/files/webpack-module-federation-ts/module-federation.config.ts__tmpl__
similarity index 100%
rename from packages/react/src/generators/host/files/module-federation-ts/module-federation.config.ts__tmpl__
rename to packages/react/src/generators/host/files/webpack-module-federation-ts/module-federation.config.ts__tmpl__
diff --git a/packages/react/src/generators/host/files/module-federation-ts/webpack.config.prod.ts__tmpl__ b/packages/react/src/generators/host/files/webpack-module-federation-ts/webpack.config.prod.ts__tmpl__
similarity index 100%
rename from packages/react/src/generators/host/files/module-federation-ts/webpack.config.prod.ts__tmpl__
rename to packages/react/src/generators/host/files/webpack-module-federation-ts/webpack.config.prod.ts__tmpl__
diff --git a/packages/react/src/generators/host/files/module-federation-ts/webpack.config.ts__tmpl__ b/packages/react/src/generators/host/files/webpack-module-federation-ts/webpack.config.ts__tmpl__
similarity index 100%
rename from packages/react/src/generators/host/files/module-federation-ts/webpack.config.ts__tmpl__
rename to packages/react/src/generators/host/files/webpack-module-federation-ts/webpack.config.ts__tmpl__
diff --git a/packages/react/src/generators/host/files/webpack-module-federation/module-federation.config.js__tmpl__ b/packages/react/src/generators/host/files/webpack-module-federation/module-federation.config.js__tmpl__
new file mode 100644
index 0000000000..04b6e6394d
--- /dev/null
+++ b/packages/react/src/generators/host/files/webpack-module-federation/module-federation.config.js__tmpl__
@@ -0,0 +1,21 @@
+module.exports = {
+ name: '<%= projectName %>',
+ /**
+ * To use a remote that does not exist in your current Nx Workspace
+ * You can use the tuple-syntax to define your remote
+ *
+ * remotes: [['my-external-remote', 'https://nx-angular-remote.netlify.app']]
+ *
+ * You _may_ need to add a `remotes.d.ts` file to your `src/` folder declaring the external remote for tsc, with the
+ * following content:
+ *
+ * declare module 'my-external-remote';
+ *
+ */
+ remotes: [
+ <% if (static) {
+ remotes.forEach(function(r) { %> "<%= r.fileName %>", <% });
+ }
+ %>
+ ],
+};
\ No newline at end of file
diff --git a/packages/react/src/generators/host/files/module-federation/webpack.config.js__tmpl__ b/packages/react/src/generators/host/files/webpack-module-federation/webpack.config.js__tmpl__
similarity index 100%
rename from packages/react/src/generators/host/files/module-federation/webpack.config.js__tmpl__
rename to packages/react/src/generators/host/files/webpack-module-federation/webpack.config.js__tmpl__
diff --git a/packages/react/src/generators/host/files/module-federation/webpack.config.prod.js__tmpl__ b/packages/react/src/generators/host/files/webpack-module-federation/webpack.config.prod.js__tmpl__
similarity index 100%
rename from packages/react/src/generators/host/files/module-federation/webpack.config.prod.js__tmpl__
rename to packages/react/src/generators/host/files/webpack-module-federation/webpack.config.prod.js__tmpl__
diff --git a/packages/react/src/generators/host/host.rspack.spec.ts b/packages/react/src/generators/host/host.rspack.spec.ts
new file mode 100644
index 0000000000..199d5baf01
--- /dev/null
+++ b/packages/react/src/generators/host/host.rspack.spec.ts
@@ -0,0 +1,379 @@
+import * as devkit from '@nx/devkit';
+import type { Tree } from '@nx/devkit';
+import { ProjectGraph, readJson } from '@nx/devkit';
+import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
+import hostGenerator from './host';
+import { Linter } from '@nx/eslint';
+
+jest.mock('@nx/devkit', () => {
+ const original = jest.requireActual('@nx/devkit');
+ return {
+ ...original,
+ readCachedProjectGraph: jest.fn().mockImplementation(
+ (): ProjectGraph => ({
+ dependencies: {},
+ nodes: {
+ test: {
+ name: 'test',
+ type: 'app',
+ data: {
+ root: 'test',
+ sourceRoot: 'test/src',
+ targets: {
+ build: {
+ executor: '@nx/rspack:rspack',
+ outputs: ['{options.outputPath}'],
+ defaultConfiguration: 'production',
+ options: {
+ compiler: 'babel',
+ outputPath: 'dist/test',
+ index: 'test/src/index.html',
+ baseHref: '/',
+ main: `test/src/main.tsx`,
+ tsConfig: 'test/tsconfig.app.json',
+ assets: ['test/src/favicon.ico', 'src/assets'],
+ styles: [`test/src/styles.css`],
+ scripts: [],
+ rspackConfig: 'test/rspack.config.js',
+ },
+ configurations: {
+ development: {
+ extractLicenses: false,
+ optimization: false,
+ sourceMap: true,
+ vendorChunk: true,
+ },
+ production: {
+ fileReplacements: [
+ {
+ replace: `test/src/environments/environment.ts`,
+ with: `test/src/environments/environment.prod.ts`,
+ },
+ ],
+ optimization: true,
+ outputHashing: 'all',
+ sourceMap: false,
+ namedChunks: false,
+ extractLicenses: true,
+ vendorChunk: false,
+ },
+ },
+ },
+ serve: {
+ executor: '@nx/rspack:dev-server',
+ defaultConfiguration: 'development',
+ options: {
+ buildTarget: `test:build`,
+ hmr: true,
+ },
+ configurations: {
+ development: {
+ buildTarget: `test:build:development`,
+ },
+ production: {
+ buildTarget: `test:build:production`,
+ hmr: false,
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ })
+ ),
+ };
+});
+
+// TODO(colum): turn these on when rspack is moved into the main repo
+xdescribe('hostGenerator', () => {
+ let tree: Tree;
+
+ // TODO(@jaysoo): Turn this back to adding the plugin
+ let originalEnv: string;
+
+ beforeEach(() => {
+ originalEnv = process.env.NX_ADD_PLUGINS;
+ process.env.NX_ADD_PLUGINS = 'false';
+ });
+
+ afterEach(() => {
+ process.env.NX_ADD_PLUGINS = originalEnv;
+ });
+
+ beforeEach(() => {
+ tree = createTreeWithEmptyWorkspace();
+ });
+
+ describe('bundler=rspack', () => {
+ it('should generate host files and configs when --js=true', async () => {
+ await hostGenerator(tree, {
+ name: 'test',
+ style: 'css',
+ linter: Linter.None,
+ unitTestRunner: 'none',
+ e2eTestRunner: 'none',
+ projectNameAndRootFormat: 'as-provided',
+ typescriptConfiguration: false,
+ skipFormat: true,
+ js: true,
+ bundler: 'rspack',
+ });
+
+ expect(tree.exists('test/tsconfig.json')).toBeTruthy();
+
+ expect(tree.exists('test/src/bootstrap.js')).toBeTruthy();
+ expect(tree.exists('test/src/main.js')).toBeTruthy();
+ expect(tree.exists('test/src/app/app.js')).toBeTruthy();
+ });
+
+ it('should generate host files and configs when --js=false', async () => {
+ await hostGenerator(tree, {
+ name: 'test',
+ style: 'css',
+ linter: Linter.None,
+ unitTestRunner: 'none',
+ e2eTestRunner: 'none',
+ projectNameAndRootFormat: 'as-provided',
+ typescriptConfiguration: false,
+ bundler: 'rspack',
+ });
+
+ expect(tree.exists('test/tsconfig.json')).toBeTruthy();
+
+ expect(tree.exists('test/src/bootstrap.tsx')).toBeTruthy();
+ expect(tree.exists('test/src/main.ts')).toBeTruthy();
+ expect(tree.exists('test/src/app/app.tsx')).toBeTruthy();
+ });
+
+ it('should generate host files and configs when --typescriptConfiguration=true', async () => {
+ await hostGenerator(tree, {
+ name: 'test',
+ style: 'css',
+ linter: Linter.None,
+ unitTestRunner: 'none',
+ e2eTestRunner: 'none',
+ projectNameAndRootFormat: 'as-provided',
+ typescriptConfiguration: true,
+ skipFormat: true,
+ bundler: 'rspack',
+ });
+
+ expect(tree.exists('test/tsconfig.json')).toBeTruthy();
+
+ expect(tree.exists('test/rspack.config.prod.ts')).toBeTruthy();
+
+ expect(tree.exists('test/rspack.config.ts')).toBeTruthy();
+ expect(tree.read('test/rspack.config.ts', 'utf-8')).toMatchSnapshot();
+
+ expect(tree.exists('test/module-federation.config.ts')).toBeTruthy();
+ expect(
+ tree.read('test/module-federation.config.ts', 'utf-8')
+ ).toMatchSnapshot();
+ });
+
+ it('should generate host files and configs when --typescriptConfiguration=false', async () => {
+ await hostGenerator(tree, {
+ name: 'test',
+ style: 'css',
+ linter: Linter.None,
+ unitTestRunner: 'none',
+ e2eTestRunner: 'none',
+ projectNameAndRootFormat: 'as-provided',
+ typescriptConfiguration: false,
+ bundler: 'rspack',
+ });
+
+ expect(tree.exists('test/tsconfig.json')).toBeTruthy();
+
+ expect(tree.exists('test/rspack.config.prod.js')).toBeTruthy();
+
+ expect(tree.exists('test/rspack.config.js')).toBeTruthy();
+ expect(tree.read('test/rspack.config.js', 'utf-8')).toMatchSnapshot();
+
+ expect(tree.exists('test/module-federation.config.js')).toBeTruthy();
+ expect(
+ tree.read('test/module-federation.config.js', 'utf-8')
+ ).toMatchSnapshot();
+ });
+
+ it('should install @nx/web for the file-server executor', async () => {
+ const tree = createTreeWithEmptyWorkspace();
+ await hostGenerator(tree, {
+ name: 'test',
+ style: 'css',
+ linter: Linter.None,
+ unitTestRunner: 'none',
+ e2eTestRunner: 'none',
+ projectNameAndRootFormat: 'as-provided',
+ skipFormat: true,
+ bundler: 'rspack',
+ });
+
+ const packageJson = readJson(tree, 'package.json');
+ expect(packageJson.devDependencies['@nx/web']).toBeDefined();
+ });
+
+ it('should generate host files and configs for SSR', async () => {
+ await hostGenerator(tree, {
+ name: 'test',
+ ssr: true,
+ style: 'css',
+ linter: Linter.None,
+ unitTestRunner: 'none',
+ e2eTestRunner: 'none',
+ projectNameAndRootFormat: 'as-provided',
+ typescriptConfiguration: false,
+ bundler: 'rspack',
+ });
+
+ expect(tree.exists('test/tsconfig.json')).toBeTruthy();
+ expect(tree.exists('test/rspack.config.prod.js')).toBeTruthy();
+ expect(tree.exists('test/rspack.server.config.js')).toBeTruthy();
+ expect(tree.exists('test/rspack.config.js')).toBeTruthy();
+ expect(tree.exists('test/module-federation.config.js')).toBeTruthy();
+ expect(
+ tree.exists('test/module-federation.server.config.js')
+ ).toBeTruthy();
+ expect(tree.exists('test/src/main.server.tsx')).toBeTruthy();
+ expect(tree.exists('test/src/bootstrap.tsx')).toBeTruthy();
+ expect(tree.exists('test/src/main.ts')).toBeTruthy();
+
+ expect(readJson(tree, 'test/tsconfig.server.json')).toEqual({
+ compilerOptions: {
+ outDir: '../../out-tsc/server',
+ target: 'es2019',
+ types: [
+ 'node',
+ '@nx/react/typings/cssmodule.d.ts',
+ '@nx/react/typings/image.d.ts',
+ ],
+ },
+ extends: './tsconfig.app.json',
+ include: ['src/remotes.d.ts', 'src/main.server.tsx', 'server.ts'],
+ });
+
+ expect(
+ tree.read('test/rspack.server.config.js', 'utf-8')
+ ).toMatchSnapshot();
+ expect(
+ tree.read('test/module-federation.server.config.js', 'utf-8')
+ ).toMatchSnapshot();
+ });
+
+ it('should generate host files and configs for SSR when --typescriptConfiguration=true', async () => {
+ await hostGenerator(tree, {
+ name: 'test',
+ ssr: true,
+ style: 'css',
+ linter: Linter.None,
+ unitTestRunner: 'none',
+ e2eTestRunner: 'none',
+ projectNameAndRootFormat: 'as-provided',
+ typescriptConfiguration: true,
+ bundler: 'rspack',
+ });
+
+ expect(tree.exists('test/tsconfig.json')).toBeTruthy();
+ expect(tree.exists('test/rspack.config.prod.ts')).toBeTruthy();
+ expect(tree.exists('test/rspack.server.config.ts')).toBeTruthy();
+ expect(tree.exists('test/rspack.config.ts')).toBeTruthy();
+ expect(tree.exists('test/module-federation.config.ts')).toBeTruthy();
+ expect(
+ tree.exists('test/module-federation.server.config.ts')
+ ).toBeTruthy();
+ expect(tree.exists('test/src/main.server.tsx')).toBeTruthy();
+ expect(tree.exists('test/src/bootstrap.tsx')).toBeTruthy();
+ expect(tree.exists('test/src/main.ts')).toBeTruthy();
+
+ expect(readJson(tree, 'test/tsconfig.server.json')).toEqual({
+ compilerOptions: {
+ outDir: '../../out-tsc/server',
+ target: 'es2019',
+ types: [
+ 'node',
+ '@nx/react/typings/cssmodule.d.ts',
+ '@nx/react/typings/image.d.ts',
+ ],
+ },
+ extends: './tsconfig.app.json',
+ include: ['src/remotes.d.ts', 'src/main.server.tsx', 'server.ts'],
+ });
+
+ expect(
+ tree.read('test/rspack.server.config.ts', 'utf-8')
+ ).toMatchSnapshot();
+ expect(
+ tree.read('test/module-federation.server.config.ts', 'utf-8')
+ ).toMatchSnapshot();
+ });
+
+ it('should generate a host and remotes in a directory correctly when using --projectNameAndRootFormat=as-provided', async () => {
+ const tree = createTreeWithEmptyWorkspace();
+
+ await hostGenerator(tree, {
+ name: 'host-app',
+ directory: 'foo/host-app',
+ remotes: ['remote1', 'remote2', 'remote3'],
+ projectNameAndRootFormat: 'as-provided',
+ e2eTestRunner: 'none',
+ linter: Linter.None,
+ style: 'css',
+ unitTestRunner: 'none',
+ typescriptConfiguration: false,
+ bundler: 'rspack',
+ });
+
+ expect(tree.exists('foo/remote1/project.json')).toBeTruthy();
+ expect(tree.exists('foo/remote2/project.json')).toBeTruthy();
+ expect(tree.exists('foo/remote3/project.json')).toBeTruthy();
+ expect(
+ tree.read('foo/host-app/module-federation.config.js', 'utf-8')
+ ).toContain(`'remote1', 'remote2', 'remote3'`);
+ });
+
+ it('should generate a host and remotes in a directory correctly when using --projectNameAndRootFormat=as-provided and --typescriptConfiguration=true', async () => {
+ const tree = createTreeWithEmptyWorkspace();
+
+ await hostGenerator(tree, {
+ name: 'host-app',
+ directory: 'foo/host-app',
+ remotes: ['remote1', 'remote2', 'remote3'],
+ projectNameAndRootFormat: 'as-provided',
+ e2eTestRunner: 'none',
+ linter: Linter.None,
+ style: 'css',
+ unitTestRunner: 'none',
+ typescriptConfiguration: true,
+ bundler: 'rspack',
+ });
+
+ expect(tree.exists('foo/remote1/project.json')).toBeTruthy();
+ expect(tree.exists('foo/remote2/project.json')).toBeTruthy();
+ expect(tree.exists('foo/remote3/project.json')).toBeTruthy();
+ expect(
+ tree.read('foo/host-app/module-federation.config.ts', 'utf-8')
+ ).toContain(`'remote1', 'remote2', 'remote3'`);
+ });
+
+ it('should throw an error if invalid remotes names are provided and --dynamic is set to true', async () => {
+ const tree = createTreeWithEmptyWorkspace();
+ const remote = 'invalid-remote-name';
+
+ await expect(
+ hostGenerator(tree, {
+ name: 'myhostapp',
+ remotes: [remote],
+ dynamic: true,
+ projectNameAndRootFormat: 'as-provided',
+ e2eTestRunner: 'none',
+ linter: Linter.None,
+ style: 'css',
+ unitTestRunner: 'none',
+ typescriptConfiguration: false,
+ bundler: 'rspack',
+ })
+ ).rejects.toThrowError(`Invalid remote name provided: ${remote}.`);
+ });
+ });
+});
diff --git a/packages/react/src/generators/host/host.spec.ts b/packages/react/src/generators/host/host.spec.ts
deleted file mode 100644
index 4faae04b64..0000000000
--- a/packages/react/src/generators/host/host.spec.ts
+++ /dev/null
@@ -1,362 +0,0 @@
-import * as devkit from '@nx/devkit';
-import type { Tree } from '@nx/devkit';
-import { ProjectGraph, readJson } from '@nx/devkit';
-import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
-import hostGenerator from './host';
-import { Linter } from '@nx/eslint';
-
-jest.mock('@nx/devkit', () => {
- const original = jest.requireActual('@nx/devkit');
- return {
- ...original,
- readCachedProjectGraph: jest.fn().mockImplementation(
- (): ProjectGraph => ({
- dependencies: {},
- nodes: {
- test: {
- name: 'test',
- type: 'app',
- data: {
- root: 'test',
- sourceRoot: 'test/src',
- targets: {
- build: {
- executor: '@nx/webpack:webpack',
- outputs: ['{options.outputPath}'],
- defaultConfiguration: 'production',
- options: {
- compiler: 'babel',
- outputPath: 'dist/test',
- index: 'test/src/index.html',
- baseHref: '/',
- main: `test/src/main.tsx`,
- tsConfig: 'test/tsconfig.app.json',
- assets: ['test/src/favicon.ico', 'src/assets'],
- styles: [`test/src/styles.css`],
- scripts: [],
- webpackConfig: 'test/webpack.config.js',
- },
- configurations: {
- development: {
- extractLicenses: false,
- optimization: false,
- sourceMap: true,
- vendorChunk: true,
- },
- production: {
- fileReplacements: [
- {
- replace: `test/src/environments/environment.ts`,
- with: `test/src/environments/environment.prod.ts`,
- },
- ],
- optimization: true,
- outputHashing: 'all',
- sourceMap: false,
- namedChunks: false,
- extractLicenses: true,
- vendorChunk: false,
- },
- },
- },
- serve: {
- executor: '@nx/webpack:dev-server',
- defaultConfiguration: 'development',
- options: {
- buildTarget: `test:build`,
- hmr: true,
- },
- configurations: {
- development: {
- buildTarget: `test:build:development`,
- },
- production: {
- buildTarget: `test:build:production`,
- hmr: false,
- },
- },
- },
- },
- },
- },
- },
- })
- ),
- };
-});
-
-describe('hostGenerator', () => {
- let tree: Tree;
-
- // TODO(@jaysoo): Turn this back to adding the plugin
- let originalEnv: string;
-
- beforeEach(() => {
- originalEnv = process.env.NX_ADD_PLUGINS;
- process.env.NX_ADD_PLUGINS = 'false';
- });
-
- afterEach(() => {
- process.env.NX_ADD_PLUGINS = originalEnv;
- });
-
- beforeEach(() => {
- tree = createTreeWithEmptyWorkspace();
- });
-
- it('should generate host files and configs when --js=true', async () => {
- await hostGenerator(tree, {
- name: 'test',
- style: 'css',
- linter: Linter.None,
- unitTestRunner: 'none',
- e2eTestRunner: 'none',
- projectNameAndRootFormat: 'as-provided',
- typescriptConfiguration: false,
- skipFormat: true,
- js: true,
- });
-
- expect(tree.exists('test/tsconfig.json')).toBeTruthy();
-
- expect(tree.exists('test/src/bootstrap.js')).toBeTruthy();
- expect(tree.exists('test/src/main.js')).toBeTruthy();
- expect(tree.exists('test/src/app/app.js')).toBeTruthy();
- });
-
- it('should generate host files and configs when --js=false', async () => {
- await hostGenerator(tree, {
- name: 'test',
- style: 'css',
- linter: Linter.None,
- unitTestRunner: 'none',
- e2eTestRunner: 'none',
- projectNameAndRootFormat: 'as-provided',
- typescriptConfiguration: false,
- });
-
- expect(tree.exists('test/tsconfig.json')).toBeTruthy();
-
- expect(tree.exists('test/src/bootstrap.tsx')).toBeTruthy();
- expect(tree.exists('test/src/main.ts')).toBeTruthy();
- expect(tree.exists('test/src/app/app.tsx')).toBeTruthy();
- });
-
- it('should generate host files and configs when --typescriptConfiguration=true', async () => {
- await hostGenerator(tree, {
- name: 'test',
- style: 'css',
- linter: Linter.None,
- unitTestRunner: 'none',
- e2eTestRunner: 'none',
- projectNameAndRootFormat: 'as-provided',
- typescriptConfiguration: true,
- skipFormat: true,
- });
-
- expect(tree.exists('test/tsconfig.json')).toBeTruthy();
-
- expect(tree.exists('test/webpack.config.prod.ts')).toBeTruthy();
-
- expect(tree.exists('test/webpack.config.ts')).toBeTruthy();
- expect(tree.read('test/webpack.config.ts', 'utf-8')).toMatchSnapshot();
-
- expect(tree.exists('test/module-federation.config.ts')).toBeTruthy();
- expect(
- tree.read('test/module-federation.config.ts', 'utf-8')
- ).toMatchSnapshot();
- });
-
- it('should generate host files and configs when --typescriptConfiguration=false', async () => {
- await hostGenerator(tree, {
- name: 'test',
- style: 'css',
- linter: Linter.None,
- unitTestRunner: 'none',
- e2eTestRunner: 'none',
- projectNameAndRootFormat: 'as-provided',
- typescriptConfiguration: false,
- });
-
- expect(tree.exists('test/tsconfig.json')).toBeTruthy();
-
- expect(tree.exists('test/webpack.config.prod.js')).toBeTruthy();
-
- expect(tree.exists('test/webpack.config.js')).toBeTruthy();
- expect(tree.read('test/webpack.config.js', 'utf-8')).toMatchSnapshot();
-
- expect(tree.exists('test/module-federation.config.js')).toBeTruthy();
- expect(
- tree.read('test/module-federation.config.js', 'utf-8')
- ).toMatchSnapshot();
- });
-
- it('should install @nx/web for the file-server executor', async () => {
- const tree = createTreeWithEmptyWorkspace();
- await hostGenerator(tree, {
- name: 'test',
- style: 'css',
- linter: Linter.None,
- unitTestRunner: 'none',
- e2eTestRunner: 'none',
- projectNameAndRootFormat: 'as-provided',
- skipFormat: true,
- });
-
- const packageJson = readJson(tree, 'package.json');
- expect(packageJson.devDependencies['@nx/web']).toBeDefined();
- });
-
- it('should generate host files and configs for SSR', async () => {
- await hostGenerator(tree, {
- name: 'test',
- ssr: true,
- style: 'css',
- linter: Linter.None,
- unitTestRunner: 'none',
- e2eTestRunner: 'none',
- projectNameAndRootFormat: 'as-provided',
- typescriptConfiguration: false,
- });
-
- expect(tree.exists('test/tsconfig.json')).toBeTruthy();
- expect(tree.exists('test/webpack.config.prod.js')).toBeTruthy();
- expect(tree.exists('test/webpack.server.config.js')).toBeTruthy();
- expect(tree.exists('test/webpack.config.js')).toBeTruthy();
- expect(tree.exists('test/module-federation.config.js')).toBeTruthy();
- expect(tree.exists('test/module-federation.server.config.js')).toBeTruthy();
- expect(tree.exists('test/src/main.server.tsx')).toBeTruthy();
- expect(tree.exists('test/src/bootstrap.tsx')).toBeTruthy();
- expect(tree.exists('test/src/main.ts')).toBeTruthy();
-
- expect(readJson(tree, 'test/tsconfig.server.json')).toEqual({
- compilerOptions: {
- outDir: '../../out-tsc/server',
- target: 'es2019',
- types: [
- 'node',
- '@nx/react/typings/cssmodule.d.ts',
- '@nx/react/typings/image.d.ts',
- ],
- },
- extends: './tsconfig.app.json',
- include: ['src/remotes.d.ts', 'src/main.server.tsx', 'server.ts'],
- });
-
- expect(
- tree.read('test/webpack.server.config.js', 'utf-8')
- ).toMatchSnapshot();
- expect(
- tree.read('test/module-federation.server.config.js', 'utf-8')
- ).toMatchSnapshot();
- });
-
- it('should generate host files and configs for SSR when --typescriptConfiguration=true', async () => {
- await hostGenerator(tree, {
- name: 'test',
- ssr: true,
- style: 'css',
- linter: Linter.None,
- unitTestRunner: 'none',
- e2eTestRunner: 'none',
- projectNameAndRootFormat: 'as-provided',
- typescriptConfiguration: true,
- });
-
- expect(tree.exists('test/tsconfig.json')).toBeTruthy();
- expect(tree.exists('test/webpack.config.prod.ts')).toBeTruthy();
- expect(tree.exists('test/webpack.server.config.ts')).toBeTruthy();
- expect(tree.exists('test/webpack.config.ts')).toBeTruthy();
- expect(tree.exists('test/module-federation.config.ts')).toBeTruthy();
- expect(tree.exists('test/module-federation.server.config.ts')).toBeTruthy();
- expect(tree.exists('test/src/main.server.tsx')).toBeTruthy();
- expect(tree.exists('test/src/bootstrap.tsx')).toBeTruthy();
- expect(tree.exists('test/src/main.ts')).toBeTruthy();
-
- expect(readJson(tree, 'test/tsconfig.server.json')).toEqual({
- compilerOptions: {
- outDir: '../../out-tsc/server',
- target: 'es2019',
- types: [
- 'node',
- '@nx/react/typings/cssmodule.d.ts',
- '@nx/react/typings/image.d.ts',
- ],
- },
- extends: './tsconfig.app.json',
- include: ['src/remotes.d.ts', 'src/main.server.tsx', 'server.ts'],
- });
-
- expect(
- tree.read('test/webpack.server.config.ts', 'utf-8')
- ).toMatchSnapshot();
- expect(
- tree.read('test/module-federation.server.config.ts', 'utf-8')
- ).toMatchSnapshot();
- });
-
- it('should generate a host and remotes in a directory correctly when using --projectNameAndRootFormat=as-provided', async () => {
- const tree = createTreeWithEmptyWorkspace();
-
- await hostGenerator(tree, {
- name: 'host-app',
- directory: 'foo/host-app',
- remotes: ['remote1', 'remote2', 'remote3'],
- projectNameAndRootFormat: 'as-provided',
- e2eTestRunner: 'none',
- linter: Linter.None,
- style: 'css',
- unitTestRunner: 'none',
- typescriptConfiguration: false,
- });
-
- expect(tree.exists('foo/remote1/project.json')).toBeTruthy();
- expect(tree.exists('foo/remote2/project.json')).toBeTruthy();
- expect(tree.exists('foo/remote3/project.json')).toBeTruthy();
- expect(
- tree.read('foo/host-app/module-federation.config.js', 'utf-8')
- ).toContain(`'remote1', 'remote2', 'remote3'`);
- });
-
- it('should generate a host and remotes in a directory correctly when using --projectNameAndRootFormat=as-provided and --typescriptConfiguration=true', async () => {
- const tree = createTreeWithEmptyWorkspace();
-
- await hostGenerator(tree, {
- name: 'host-app',
- directory: 'foo/host-app',
- remotes: ['remote1', 'remote2', 'remote3'],
- projectNameAndRootFormat: 'as-provided',
- e2eTestRunner: 'none',
- linter: Linter.None,
- style: 'css',
- unitTestRunner: 'none',
- typescriptConfiguration: true,
- });
-
- expect(tree.exists('foo/remote1/project.json')).toBeTruthy();
- expect(tree.exists('foo/remote2/project.json')).toBeTruthy();
- expect(tree.exists('foo/remote3/project.json')).toBeTruthy();
- expect(
- tree.read('foo/host-app/module-federation.config.ts', 'utf-8')
- ).toContain(`'remote1', 'remote2', 'remote3'`);
- });
-
- it('should throw an error if invalid remotes names are provided and --dynamic is set to true', async () => {
- const tree = createTreeWithEmptyWorkspace();
- const remote = 'invalid-remote-name';
-
- await expect(
- hostGenerator(tree, {
- name: 'myhostapp',
- remotes: [remote],
- dynamic: true,
- projectNameAndRootFormat: 'as-provided',
- e2eTestRunner: 'none',
- linter: Linter.None,
- style: 'css',
- unitTestRunner: 'none',
- typescriptConfiguration: false,
- })
- ).rejects.toThrowError(`Invalid remote name provided: ${remote}.`);
- });
-});
diff --git a/packages/react/src/generators/host/host.ts b/packages/react/src/generators/host/host.ts
index d2b84948b8..633dac3f23 100644
--- a/packages/react/src/generators/host/host.ts
+++ b/packages/react/src/generators/host/host.ts
@@ -49,6 +49,7 @@ export async function hostGeneratorInternal(
dynamic: schema.dynamic ?? false,
// TODO(colum): remove when MF works with Crystal
addPlugin: false,
+ bundler: schema.bundler ?? 'rspack',
};
// Check to see if remotes are provided and also check if --dynamic is provided
@@ -68,8 +69,6 @@ export async function hostGeneratorInternal(
...options,
// The target use-case is loading remotes as child routes, thus always enable routing.
routing: true,
- // Only webpack works with module federation for now.
- bundler: 'webpack',
skipFormat: true,
});
tasks.push(initTask);
@@ -98,6 +97,7 @@ export async function hostGeneratorInternal(
dynamic: options.dynamic,
host: options.name,
skipPackageJson: options.skipPackageJson,
+ bundler: options.bundler,
});
tasks.push(remoteTask);
remotePort++;
@@ -125,10 +125,19 @@ export async function hostGeneratorInternal(
tasks.push(setupSsrForHostTask);
const projectConfig = readProjectConfiguration(host, options.projectName);
- projectConfig.targets.server.options.webpackConfig = joinPathFragments(
- projectConfig.root,
- `webpack.server.config.${options.typescriptConfiguration ? 'ts' : 'js'}`
- );
+ if (options.bundler === 'rspack') {
+ projectConfig.targets.server.executor = '@nx/rspack:rspack';
+ projectConfig.targets.server.options.rspackConfig = joinPathFragments(
+ projectConfig.root,
+ `rspack.server.config.${options.typescriptConfiguration ? 'ts' : 'js'}`
+ );
+ delete projectConfig.targets.server.options.webpackConfig;
+ } else {
+ projectConfig.targets.server.options.webpackConfig = joinPathFragments(
+ projectConfig.root,
+ `webpack.server.config.${options.typescriptConfiguration ? 'ts' : 'js'}`
+ );
+ }
updateProjectConfiguration(host, options.projectName, projectConfig);
}
diff --git a/packages/react/src/generators/host/host.webpack.spec.ts b/packages/react/src/generators/host/host.webpack.spec.ts
new file mode 100644
index 0000000000..fd9ccead74
--- /dev/null
+++ b/packages/react/src/generators/host/host.webpack.spec.ts
@@ -0,0 +1,378 @@
+import * as devkit from '@nx/devkit';
+import type { Tree } from '@nx/devkit';
+import { ProjectGraph, readJson } from '@nx/devkit';
+import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
+import hostGenerator from './host';
+import { Linter } from '@nx/eslint';
+
+jest.mock('@nx/devkit', () => {
+ const original = jest.requireActual('@nx/devkit');
+ return {
+ ...original,
+ readCachedProjectGraph: jest.fn().mockImplementation(
+ (): ProjectGraph => ({
+ dependencies: {},
+ nodes: {
+ test: {
+ name: 'test',
+ type: 'app',
+ data: {
+ root: 'test',
+ sourceRoot: 'test/src',
+ targets: {
+ build: {
+ executor: '@nx/webpack:webpack',
+ outputs: ['{options.outputPath}'],
+ defaultConfiguration: 'production',
+ options: {
+ compiler: 'babel',
+ outputPath: 'dist/test',
+ index: 'test/src/index.html',
+ baseHref: '/',
+ main: `test/src/main.tsx`,
+ tsConfig: 'test/tsconfig.app.json',
+ assets: ['test/src/favicon.ico', 'src/assets'],
+ styles: [`test/src/styles.css`],
+ scripts: [],
+ webpackConfig: 'test/webpack.config.js',
+ },
+ configurations: {
+ development: {
+ extractLicenses: false,
+ optimization: false,
+ sourceMap: true,
+ vendorChunk: true,
+ },
+ production: {
+ fileReplacements: [
+ {
+ replace: `test/src/environments/environment.ts`,
+ with: `test/src/environments/environment.prod.ts`,
+ },
+ ],
+ optimization: true,
+ outputHashing: 'all',
+ sourceMap: false,
+ namedChunks: false,
+ extractLicenses: true,
+ vendorChunk: false,
+ },
+ },
+ },
+ serve: {
+ executor: '@nx/webpack:dev-server',
+ defaultConfiguration: 'development',
+ options: {
+ buildTarget: `test:build`,
+ hmr: true,
+ },
+ configurations: {
+ development: {
+ buildTarget: `test:build:development`,
+ },
+ production: {
+ buildTarget: `test:build:production`,
+ hmr: false,
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ })
+ ),
+ };
+});
+
+describe('hostGenerator', () => {
+ let tree: Tree;
+
+ // TODO(@jaysoo): Turn this back to adding the plugin
+ let originalEnv: string;
+
+ beforeEach(() => {
+ originalEnv = process.env.NX_ADD_PLUGINS;
+ process.env.NX_ADD_PLUGINS = 'false';
+ });
+
+ afterEach(() => {
+ process.env.NX_ADD_PLUGINS = originalEnv;
+ });
+
+ beforeEach(() => {
+ tree = createTreeWithEmptyWorkspace();
+ });
+
+ describe('bundler=webpack', () => {
+ it('should generate host files and configs when --js=true', async () => {
+ await hostGenerator(tree, {
+ name: 'test',
+ style: 'css',
+ linter: Linter.None,
+ unitTestRunner: 'none',
+ e2eTestRunner: 'none',
+ projectNameAndRootFormat: 'as-provided',
+ typescriptConfiguration: false,
+ skipFormat: true,
+ js: true,
+ bundler: 'webpack',
+ });
+
+ expect(tree.exists('test/tsconfig.json')).toBeTruthy();
+
+ expect(tree.exists('test/src/bootstrap.js')).toBeTruthy();
+ expect(tree.exists('test/src/main.js')).toBeTruthy();
+ expect(tree.exists('test/src/app/app.js')).toBeTruthy();
+ });
+
+ it('should generate host files and configs when --js=false', async () => {
+ await hostGenerator(tree, {
+ name: 'test',
+ style: 'css',
+ linter: Linter.None,
+ unitTestRunner: 'none',
+ e2eTestRunner: 'none',
+ projectNameAndRootFormat: 'as-provided',
+ typescriptConfiguration: false,
+ bundler: 'webpack',
+ });
+
+ expect(tree.exists('test/tsconfig.json')).toBeTruthy();
+
+ expect(tree.exists('test/src/bootstrap.tsx')).toBeTruthy();
+ expect(tree.exists('test/src/main.ts')).toBeTruthy();
+ expect(tree.exists('test/src/app/app.tsx')).toBeTruthy();
+ });
+
+ it('should generate host files and configs when --typescriptConfiguration=true', async () => {
+ await hostGenerator(tree, {
+ name: 'test',
+ style: 'css',
+ linter: Linter.None,
+ unitTestRunner: 'none',
+ e2eTestRunner: 'none',
+ projectNameAndRootFormat: 'as-provided',
+ typescriptConfiguration: true,
+ skipFormat: true,
+ bundler: 'webpack',
+ });
+
+ expect(tree.exists('test/tsconfig.json')).toBeTruthy();
+
+ expect(tree.exists('test/webpack.config.prod.ts')).toBeTruthy();
+
+ expect(tree.exists('test/webpack.config.ts')).toBeTruthy();
+ expect(tree.read('test/webpack.config.ts', 'utf-8')).toMatchSnapshot();
+
+ expect(tree.exists('test/module-federation.config.ts')).toBeTruthy();
+ expect(
+ tree.read('test/module-federation.config.ts', 'utf-8')
+ ).toMatchSnapshot();
+ });
+
+ it('should generate host files and configs when --typescriptConfiguration=false', async () => {
+ await hostGenerator(tree, {
+ name: 'test',
+ style: 'css',
+ linter: Linter.None,
+ unitTestRunner: 'none',
+ e2eTestRunner: 'none',
+ projectNameAndRootFormat: 'as-provided',
+ typescriptConfiguration: false,
+ bundler: 'webpack',
+ });
+
+ expect(tree.exists('test/tsconfig.json')).toBeTruthy();
+
+ expect(tree.exists('test/webpack.config.prod.js')).toBeTruthy();
+
+ expect(tree.exists('test/webpack.config.js')).toBeTruthy();
+ expect(tree.read('test/webpack.config.js', 'utf-8')).toMatchSnapshot();
+
+ expect(tree.exists('test/module-federation.config.js')).toBeTruthy();
+ expect(
+ tree.read('test/module-federation.config.js', 'utf-8')
+ ).toMatchSnapshot();
+ });
+
+ it('should install @nx/web for the file-server executor', async () => {
+ const tree = createTreeWithEmptyWorkspace();
+ await hostGenerator(tree, {
+ name: 'test',
+ style: 'css',
+ linter: Linter.None,
+ unitTestRunner: 'none',
+ e2eTestRunner: 'none',
+ projectNameAndRootFormat: 'as-provided',
+ skipFormat: true,
+ bundler: 'webpack',
+ });
+
+ const packageJson = readJson(tree, 'package.json');
+ expect(packageJson.devDependencies['@nx/web']).toBeDefined();
+ });
+
+ it('should generate host files and configs for SSR', async () => {
+ await hostGenerator(tree, {
+ name: 'test',
+ ssr: true,
+ style: 'css',
+ linter: Linter.None,
+ unitTestRunner: 'none',
+ e2eTestRunner: 'none',
+ projectNameAndRootFormat: 'as-provided',
+ typescriptConfiguration: false,
+ bundler: 'webpack',
+ });
+
+ expect(tree.exists('test/tsconfig.json')).toBeTruthy();
+ expect(tree.exists('test/webpack.config.prod.js')).toBeTruthy();
+ expect(tree.exists('test/webpack.server.config.js')).toBeTruthy();
+ expect(tree.exists('test/webpack.config.js')).toBeTruthy();
+ expect(tree.exists('test/module-federation.config.js')).toBeTruthy();
+ expect(
+ tree.exists('test/module-federation.server.config.js')
+ ).toBeTruthy();
+ expect(tree.exists('test/src/main.server.tsx')).toBeTruthy();
+ expect(tree.exists('test/src/bootstrap.tsx')).toBeTruthy();
+ expect(tree.exists('test/src/main.ts')).toBeTruthy();
+
+ expect(readJson(tree, 'test/tsconfig.server.json')).toEqual({
+ compilerOptions: {
+ outDir: '../../out-tsc/server',
+ target: 'es2019',
+ types: [
+ 'node',
+ '@nx/react/typings/cssmodule.d.ts',
+ '@nx/react/typings/image.d.ts',
+ ],
+ },
+ extends: './tsconfig.app.json',
+ include: ['src/remotes.d.ts', 'src/main.server.tsx', 'server.ts'],
+ });
+
+ expect(
+ tree.read('test/webpack.server.config.js', 'utf-8')
+ ).toMatchSnapshot();
+ expect(
+ tree.read('test/module-federation.server.config.js', 'utf-8')
+ ).toMatchSnapshot();
+ });
+
+ it('should generate host files and configs for SSR when --typescriptConfiguration=true', async () => {
+ await hostGenerator(tree, {
+ name: 'test',
+ ssr: true,
+ style: 'css',
+ linter: Linter.None,
+ unitTestRunner: 'none',
+ e2eTestRunner: 'none',
+ projectNameAndRootFormat: 'as-provided',
+ typescriptConfiguration: true,
+ bundler: 'webpack',
+ });
+
+ expect(tree.exists('test/tsconfig.json')).toBeTruthy();
+ expect(tree.exists('test/webpack.config.prod.ts')).toBeTruthy();
+ expect(tree.exists('test/webpack.server.config.ts')).toBeTruthy();
+ expect(tree.exists('test/webpack.config.ts')).toBeTruthy();
+ expect(tree.exists('test/module-federation.config.ts')).toBeTruthy();
+ expect(
+ tree.exists('test/module-federation.server.config.ts')
+ ).toBeTruthy();
+ expect(tree.exists('test/src/main.server.tsx')).toBeTruthy();
+ expect(tree.exists('test/src/bootstrap.tsx')).toBeTruthy();
+ expect(tree.exists('test/src/main.ts')).toBeTruthy();
+
+ expect(readJson(tree, 'test/tsconfig.server.json')).toEqual({
+ compilerOptions: {
+ outDir: '../../out-tsc/server',
+ target: 'es2019',
+ types: [
+ 'node',
+ '@nx/react/typings/cssmodule.d.ts',
+ '@nx/react/typings/image.d.ts',
+ ],
+ },
+ extends: './tsconfig.app.json',
+ include: ['src/remotes.d.ts', 'src/main.server.tsx', 'server.ts'],
+ });
+
+ expect(
+ tree.read('test/webpack.server.config.ts', 'utf-8')
+ ).toMatchSnapshot();
+ expect(
+ tree.read('test/module-federation.server.config.ts', 'utf-8')
+ ).toMatchSnapshot();
+ });
+
+ it('should generate a host and remotes in a directory correctly when using --projectNameAndRootFormat=as-provided', async () => {
+ const tree = createTreeWithEmptyWorkspace();
+
+ await hostGenerator(tree, {
+ name: 'host-app',
+ directory: 'foo/host-app',
+ remotes: ['remote1', 'remote2', 'remote3'],
+ projectNameAndRootFormat: 'as-provided',
+ e2eTestRunner: 'none',
+ linter: Linter.None,
+ style: 'css',
+ unitTestRunner: 'none',
+ typescriptConfiguration: false,
+ bundler: 'webpack',
+ });
+
+ expect(tree.exists('foo/remote1/project.json')).toBeTruthy();
+ expect(tree.exists('foo/remote2/project.json')).toBeTruthy();
+ expect(tree.exists('foo/remote3/project.json')).toBeTruthy();
+ expect(
+ tree.read('foo/host-app/module-federation.config.js', 'utf-8')
+ ).toContain(`'remote1', 'remote2', 'remote3'`);
+ });
+
+ it('should generate a host and remotes in a directory correctly when using --projectNameAndRootFormat=as-provided and --typescriptConfiguration=true', async () => {
+ const tree = createTreeWithEmptyWorkspace();
+
+ await hostGenerator(tree, {
+ name: 'host-app',
+ directory: 'foo/host-app',
+ remotes: ['remote1', 'remote2', 'remote3'],
+ projectNameAndRootFormat: 'as-provided',
+ e2eTestRunner: 'none',
+ linter: Linter.None,
+ style: 'css',
+ unitTestRunner: 'none',
+ typescriptConfiguration: true,
+ bundler: 'webpack',
+ });
+
+ expect(tree.exists('foo/remote1/project.json')).toBeTruthy();
+ expect(tree.exists('foo/remote2/project.json')).toBeTruthy();
+ expect(tree.exists('foo/remote3/project.json')).toBeTruthy();
+ expect(
+ tree.read('foo/host-app/module-federation.config.ts', 'utf-8')
+ ).toContain(`'remote1', 'remote2', 'remote3'`);
+ });
+
+ it('should throw an error if invalid remotes names are provided and --dynamic is set to true', async () => {
+ const tree = createTreeWithEmptyWorkspace();
+ const remote = 'invalid-remote-name';
+
+ await expect(
+ hostGenerator(tree, {
+ name: 'myhostapp',
+ remotes: [remote],
+ dynamic: true,
+ projectNameAndRootFormat: 'as-provided',
+ e2eTestRunner: 'none',
+ linter: Linter.None,
+ style: 'css',
+ unitTestRunner: 'none',
+ typescriptConfiguration: false,
+ bundler: 'webpack',
+ })
+ ).rejects.toThrowError(`Invalid remote name provided: ${remote}.`);
+ });
+ });
+});
diff --git a/packages/react/src/generators/host/lib/add-module-federation-files.ts b/packages/react/src/generators/host/lib/add-module-federation-files.ts
index a3a6f97326..59dbffdf0b 100644
--- a/packages/react/src/generators/host/lib/add-module-federation-files.ts
+++ b/packages/react/src/generators/host/lib/add-module-federation-files.ts
@@ -36,10 +36,19 @@ export function addModuleFederationFiles(
// Renaming original entry file so we can use `import(./bootstrap)` in
// new entry file.
host.rename(
- joinPathFragments(options.appProjectRoot, maybeJs(options, 'src/main.tsx')),
joinPathFragments(
options.appProjectRoot,
- maybeJs(options, 'src/bootstrap.tsx')
+ maybeJs(
+ { js: options.js, useJsx: options.bundler === 'rspack' },
+ 'src/main.tsx'
+ )
+ ),
+ joinPathFragments(
+ options.appProjectRoot,
+ maybeJs(
+ { js: options.js, useJsx: options.bundler === 'rspack' },
+ 'src/bootstrap.tsx'
+ )
)
);
@@ -47,15 +56,25 @@ export function addModuleFederationFiles(
host,
joinPathFragments(
__dirname,
- `../files/${options.js ? 'common' : 'common-ts'}`
+ `../files/${
+ options.js
+ ? options.bundler === 'rspack'
+ ? 'rspack-common'
+ : 'common'
+ : 'common-ts'
+ }`
),
options.appProjectRoot,
templateVariables
);
const pathToModuleFederationFiles = options.typescriptConfiguration
- ? 'module-federation-ts'
- : 'module-federation';
+ ? `${
+ options.bundler === 'rspack' ? 'rspack-' : 'webpack-'
+ }module-federation-ts`
+ : `${
+ options.bundler === 'rspack' ? 'rspack-' : 'webpack-'
+ }module-federation`;
// New entry file is created here.
generateFiles(
host,
@@ -70,22 +89,29 @@ export function addModuleFederationFiles(
}
}
- function processWebpackConfig(options, host, fileName) {
- const pathToWebpackConfig = joinPathFragments(
+ function processBundlerConfigFile(options, host, fileName) {
+ const pathToBundlerConfig = joinPathFragments(
options.appProjectRoot,
fileName
);
- deleteFileIfExists(host, pathToWebpackConfig);
+ deleteFileIfExists(host, pathToBundlerConfig);
}
if (options.typescriptConfiguration) {
- processWebpackConfig(options, host, 'webpack.config.js');
- processWebpackConfig(options, host, 'webpack.config.prod.js');
+ if (options.bundler === 'rspack') {
+ processBundlerConfigFile(options, host, 'rspack.config.js');
+ processBundlerConfigFile(options, host, 'rspack.config.prod.js');
+ } else {
+ processBundlerConfigFile(options, host, 'webpack.config.js');
+ processBundlerConfigFile(options, host, 'webpack.config.prod.js');
+ }
}
if (options.dynamic) {
- processWebpackConfig(options, host, 'webpack.config.prod.js');
- processWebpackConfig(options, host, 'webpack.config.prod.ts');
+ processBundlerConfigFile(options, host, 'webpack.config.prod.js');
+ processBundlerConfigFile(options, host, 'webpack.config.prod.ts');
+ processBundlerConfigFile(options, host, 'rspack.config.prod.js');
+ processBundlerConfigFile(options, host, 'rspack.config.prod.ts');
if (!host.exists(pathToMFManifest)) {
host.write(
pathToMFManifest,
diff --git a/packages/react/src/generators/host/lib/setup-ssr-for-host.ts b/packages/react/src/generators/host/lib/setup-ssr-for-host.ts
index 1a6ff12434..a97765b7b5 100644
--- a/packages/react/src/generators/host/lib/setup-ssr-for-host.ts
+++ b/packages/react/src/generators/host/lib/setup-ssr-for-host.ts
@@ -20,12 +20,19 @@ export async function setupSsrForHost(
) {
const tasks: GeneratorCallback[] = [];
let project = readProjectConfiguration(tree, appName);
- project.targets.serve.executor = '@nx/react:module-federation-ssr-dev-server';
+ project.targets.serve.executor =
+ options.bundler === 'rspack'
+ ? '@nx/rspack:module-federation-ssr-dev-server'
+ : '@nx/react:module-federation-ssr-dev-server';
updateProjectConfiguration(tree, appName, project);
const pathToModuleFederationSsrFiles = options.typescriptConfiguration
- ? 'module-federation-ssr-ts'
- : 'module-federation-ssr';
+ ? `${
+ options.bundler === 'rspack' ? 'rspack-' : 'webpack-'
+ }module-federation-ssr-ts`
+ : `${
+ options.bundler === 'rspack' ? 'rspack-' : 'webpack-'
+ }module-federation-ssr`;
generateFiles(
tree,
diff --git a/packages/react/src/generators/host/schema.d.ts b/packages/react/src/generators/host/schema.d.ts
index b48aa4b0b5..99dac92d91 100644
--- a/packages/react/src/generators/host/schema.d.ts
+++ b/packages/react/src/generators/host/schema.d.ts
@@ -28,6 +28,7 @@ export interface Schema {
typescriptConfiguration?: boolean;
dynamic?: boolean;
addPlugin?: boolean;
+ bundler?: 'rspack' | 'webpack';
}
export interface NormalizedSchema extends Schema {
diff --git a/packages/react/src/generators/host/schema.json b/packages/react/src/generators/host/schema.json
index 15b48643f2..0a97083fc4 100644
--- a/packages/react/src/generators/host/schema.json
+++ b/packages/react/src/generators/host/schema.json
@@ -183,6 +183,14 @@
"type": "boolean",
"default": false,
"x-priority": "internal"
+ },
+ "bundler": {
+ "description": "The bundler to use.",
+ "type": "string",
+ "enum": ["rspack", "webpack"],
+ "x-prompt": "Which bundler do you want to use to build the application?",
+ "default": "rspack",
+ "x-priority": "important"
}
},
"required": ["name"],
diff --git a/packages/react/src/generators/remote/__snapshots__/remote.spec.ts.snap b/packages/react/src/generators/remote/__snapshots__/remote.webpack.spec.ts.snap
similarity index 72%
rename from packages/react/src/generators/remote/__snapshots__/remote.spec.ts.snap
rename to packages/react/src/generators/remote/__snapshots__/remote.webpack.spec.ts.snap
index 808de1b3c5..b2e3367a07 100644
--- a/packages/react/src/generators/remote/__snapshots__/remote.spec.ts.snap
+++ b/packages/react/src/generators/remote/__snapshots__/remote.webpack.spec.ts.snap
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`remote generator should create the remote with the correct config files 1`] = `
+exports[`remote generator bundler=webpack should create the remote with the correct config files 1`] = `
"const { composePlugins, withNx } = require('@nx/webpack');
const { withReact } = require('@nx/react');
const { withModuleFederation } = require('@nx/react/module-federation');
@@ -21,9 +21,9 @@ module.exports = composePlugins(withNx(), withReact(), withModuleFederation(conf
"
`;
-exports[`remote generator should create the remote with the correct config files 2`] = `"module.exports = require('./webpack.config');"`;
+exports[`remote generator bundler=webpack should create the remote with the correct config files 2`] = `"module.exports = require('./webpack.config');"`;
-exports[`remote generator should create the remote with the correct config files 3`] = `
+exports[`remote generator bundler=webpack should create the remote with the correct config files 3`] = `
"module.exports = {
name: 'test',
@@ -34,7 +34,7 @@ exports[`remote generator should create the remote with the correct config files
"
`;
-exports[`remote generator should create the remote with the correct config files when --js=true 1`] = `
+exports[`remote generator bundler=webpack should create the remote with the correct config files when --js=true 1`] = `
"const { composePlugins, withNx } = require('@nx/webpack');
const { withReact } = require('@nx/react');
const { withModuleFederation } = require('@nx/react/module-federation');
@@ -55,9 +55,9 @@ module.exports = composePlugins(withNx(), withReact(), withModuleFederation(conf
"
`;
-exports[`remote generator should create the remote with the correct config files when --js=true 2`] = `"module.exports = require('./webpack.config');"`;
+exports[`remote generator bundler=webpack should create the remote with the correct config files when --js=true 2`] = `"module.exports = require('./webpack.config');"`;
-exports[`remote generator should create the remote with the correct config files when --js=true 3`] = `
+exports[`remote generator bundler=webpack should create the remote with the correct config files when --js=true 3`] = `
"module.exports = {
name: 'test',
@@ -68,7 +68,7 @@ exports[`remote generator should create the remote with the correct config files
"
`;
-exports[`remote generator should create the remote with the correct config files when --typescriptConfiguration=true 1`] = `
+exports[`remote generator bundler=webpack should create the remote with the correct config files when --typescriptConfiguration=true 1`] = `
"import { composePlugins, withNx } from '@nx/webpack';
import { withReact } from '@nx/react';
import { withModuleFederation } from '@nx/react/module-federation';
@@ -93,12 +93,12 @@ export default composePlugins(
"
`;
-exports[`remote generator should create the remote with the correct config files when --typescriptConfiguration=true 2`] = `
+exports[`remote generator bundler=webpack should create the remote with the correct config files when --typescriptConfiguration=true 2`] = `
"export default require('./webpack.config');
"
`;
-exports[`remote generator should create the remote with the correct config files when --typescriptConfiguration=true 3`] = `
+exports[`remote generator bundler=webpack should create the remote with the correct config files when --typescriptConfiguration=true 3`] = `
"import { ModuleFederationConfig } from '@nx/webpack';
const config: ModuleFederationConfig = {
@@ -113,7 +113,7 @@ export default config;
"
`;
-exports[`remote generator should generate correct remote with config files when using --ssr 1`] = `
+exports[`remote generator bundler=webpack should generate correct remote with config files when using --ssr 1`] = `
"const {composePlugins, withNx} = require('@nx/webpack');
const {withReact} = require('@nx/react');
const {withModuleFederationForSSR} = require('@nx/react/module-federation');
@@ -134,7 +134,7 @@ module.exports = composePlugins(withNx(), withReact({ssr: true}), withModuleFede
"
`;
-exports[`remote generator should generate correct remote with config files when using --ssr 2`] = `
+exports[`remote generator bundler=webpack should generate correct remote with config files when using --ssr 2`] = `
"module.exports = {
name: 'test',
exposes: {
@@ -144,7 +144,7 @@ exports[`remote generator should generate correct remote with config files when
"
`;
-exports[`remote generator should generate correct remote with config files when using --ssr and --typescriptConfiguration=true 1`] = `
+exports[`remote generator bundler=webpack should generate correct remote with config files when using --ssr and --typescriptConfiguration=true 1`] = `
"import { composePlugins, withNx } from '@nx/webpack';
import { withReact } from '@nx/react';
import { withModuleFederationForSSR } from '@nx/react/module-federation';
@@ -169,7 +169,7 @@ export default composePlugins(
"
`;
-exports[`remote generator should generate correct remote with config files when using --ssr and --typescriptConfiguration=true 2`] = `
+exports[`remote generator bundler=webpack should generate correct remote with config files when using --ssr and --typescriptConfiguration=true 2`] = `
"import { ModuleFederationConfig } from '@nx/webpack';
const config: ModuleFederationConfig = {
diff --git a/packages/react/src/generators/remote/files/rspack-common/src/main.jsx__tmpl__ b/packages/react/src/generators/remote/files/rspack-common/src/main.jsx__tmpl__
new file mode 100644
index 0000000000..b93c7a0268
--- /dev/null
+++ b/packages/react/src/generators/remote/files/rspack-common/src/main.jsx__tmpl__
@@ -0,0 +1 @@
+import('./bootstrap');
diff --git a/packages/react/src/generators/remote/files/rspack-common/src/remote-entry.js__tmpl__ b/packages/react/src/generators/remote/files/rspack-common/src/remote-entry.js__tmpl__
new file mode 100644
index 0000000000..8c1fd1008a
--- /dev/null
+++ b/packages/react/src/generators/remote/files/rspack-common/src/remote-entry.js__tmpl__
@@ -0,0 +1 @@
+export { default } from './app/app';
diff --git a/packages/react/src/generators/remote/files/rspack-module-federation-ssr-ts/module-federation.server.config.ts__tmpl__ b/packages/react/src/generators/remote/files/rspack-module-federation-ssr-ts/module-federation.server.config.ts__tmpl__
new file mode 100644
index 0000000000..75b68d2082
--- /dev/null
+++ b/packages/react/src/generators/remote/files/rspack-module-federation-ssr-ts/module-federation.server.config.ts__tmpl__
@@ -0,0 +1,10 @@
+import {ModuleFederationConfig} from '@nx/rspack/module-federation';
+
+const config: ModuleFederationConfig = {
+ name: '<%= projectName %>',
+ exposes: {
+ './Module': './src/remote-entry.<%= js ? 'js' : 'ts' %>',
+ },
+};
+
+export default config;
diff --git a/packages/react/src/generators/remote/files/rspack-module-federation-ssr-ts/rspack.server.config.ts__tmpl__ b/packages/react/src/generators/remote/files/rspack-module-federation-ssr-ts/rspack.server.config.ts__tmpl__
new file mode 100644
index 0000000000..d97d3731ae
--- /dev/null
+++ b/packages/react/src/generators/remote/files/rspack-module-federation-ssr-ts/rspack.server.config.ts__tmpl__
@@ -0,0 +1,16 @@
+import {composePlugins, withNx, withReact} from '@nx/rspack';
+import {withModuleFederationForSSR} from '@nx/rspack/module-federation';
+
+import baseConfig from "./module-federation.server.config";
+
+const defaultConfig = {
+ ...baseConfig,
+};
+
+// Nx plugins for rspack to build config object from Nx options and context.
+/**
+ * DTS Plugin is disabled in Nx Workspaces as Nx already provides Typing support Module Federation
+ * The DTS Plugin can be enabled by setting dts: true
+ * Learn more about the DTS Plugin here: https://module-federation.io/configure/dts.html
+ */
+export default composePlugins(withNx(), withReact({ssr: true}), withModuleFederationForSSR(defaultConfig, { dts: false }));
diff --git a/packages/react/src/generators/remote/files/module-federation-ssr-ts/server.ts__tmpl__ b/packages/react/src/generators/remote/files/rspack-module-federation-ssr-ts/server.ts__tmpl__
similarity index 100%
rename from packages/react/src/generators/remote/files/module-federation-ssr-ts/server.ts__tmpl__
rename to packages/react/src/generators/remote/files/rspack-module-federation-ssr-ts/server.ts__tmpl__
diff --git a/packages/react/src/generators/remote/files/rspack-module-federation-ssr-ts/tsconfig.lint.json__tmpl__ b/packages/react/src/generators/remote/files/rspack-module-federation-ssr-ts/tsconfig.lint.json__tmpl__
new file mode 100644
index 0000000000..e279c41b31
--- /dev/null
+++ b/packages/react/src/generators/remote/files/rspack-module-federation-ssr-ts/tsconfig.lint.json__tmpl__
@@ -0,0 +1,19 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../dist/out-tsc",
+ "types": [
+ "node",
+ "@nx/react/typings/cssmodule.d.ts",
+ "@nx/react/typings/image.d.ts"
+ ]
+ },
+ "include": [
+ "src/**/*.js",
+ "src/**/*.jsx",
+ "src/**/*.ts",
+ "src/**/*.tsx",
+ "rspack.config.ts",
+ "rspack.prod.config.ts"
+ ]
+}
diff --git a/packages/react/src/generators/remote/files/rspack-module-federation-ssr/module-federation.server.config.js__tmpl__ b/packages/react/src/generators/remote/files/rspack-module-federation-ssr/module-federation.server.config.js__tmpl__
new file mode 100644
index 0000000000..a24cbeabb6
--- /dev/null
+++ b/packages/react/src/generators/remote/files/rspack-module-federation-ssr/module-federation.server.config.js__tmpl__
@@ -0,0 +1,6 @@
+module.exports = {
+ name: '<%= projectName %>',
+ exposes: {
+ './Module': './src/remote-entry.<%= js ? 'js' : 'ts' %>',
+ },
+};
diff --git a/packages/react/src/generators/remote/files/rspack-module-federation-ssr/rspack.server.config.js__tmpl__ b/packages/react/src/generators/remote/files/rspack-module-federation-ssr/rspack.server.config.js__tmpl__
new file mode 100644
index 0000000000..0fad19d265
--- /dev/null
+++ b/packages/react/src/generators/remote/files/rspack-module-federation-ssr/rspack.server.config.js__tmpl__
@@ -0,0 +1,16 @@
+const {composePlugins, withNx, withReact} = require('@nx/rspack');
+const {withModuleFederationForSSR} = require('@nx/rspack/module-federation');
+
+const baseConfig = require("./module-federation.server.config");
+
+const defaultConfig = {
+ ...baseConfig,
+};
+
+// Nx plugins for rspack to build config object from Nx options and context.
+/**
+ * DTS Plugin is disabled in Nx Workspaces as Nx already provides Typing support Module Federation
+ * The DTS Plugin can be enabled by setting dts: true
+ * Learn more about the DTS Plugin here: https://module-federation.io/configure/dts.html
+ */
+module.exports = composePlugins(withNx(), withReact({ssr: true}), withModuleFederationForSSR(defaultConfig, { dts: false }));
diff --git a/packages/react/src/generators/remote/files/module-federation-ssr/server.ts__tmpl__ b/packages/react/src/generators/remote/files/rspack-module-federation-ssr/server.ts__tmpl__
similarity index 100%
rename from packages/react/src/generators/remote/files/module-federation-ssr/server.ts__tmpl__
rename to packages/react/src/generators/remote/files/rspack-module-federation-ssr/server.ts__tmpl__
diff --git a/packages/react/src/generators/remote/files/rspack-module-federation-ts/module-federation.config.ts__tmpl__ b/packages/react/src/generators/remote/files/rspack-module-federation-ts/module-federation.config.ts__tmpl__
new file mode 100644
index 0000000000..b2e98cfaa5
--- /dev/null
+++ b/packages/react/src/generators/remote/files/rspack-module-federation-ts/module-federation.config.ts__tmpl__
@@ -0,0 +1,13 @@
+import {ModuleFederationConfig} from '@nx/rspack/module-federation';
+
+const config: ModuleFederationConfig = {
+ name: '<%= projectName %>',
+ <% if (dynamic) { %>
+ library: { type: 'var', name: '<%= projectName %>'},
+ <% } %>
+ exposes: {
+ './Module': './src/remote-entry.<%= js ? 'js' : 'ts' %>',
+ },
+};
+
+export default config;
diff --git a/packages/react/src/generators/remote/files/rspack-module-federation-ts/rspack.config.prod.ts__tmpl__ b/packages/react/src/generators/remote/files/rspack-module-federation-ts/rspack.config.prod.ts__tmpl__
new file mode 100644
index 0000000000..0c2be66b3a
--- /dev/null
+++ b/packages/react/src/generators/remote/files/rspack-module-federation-ts/rspack.config.prod.ts__tmpl__
@@ -0,0 +1 @@
+export default require('./rspack.config');
diff --git a/packages/react/src/generators/remote/files/rspack-module-federation-ts/rspack.config.ts__tmpl__ b/packages/react/src/generators/remote/files/rspack-module-federation-ts/rspack.config.ts__tmpl__
new file mode 100644
index 0000000000..91b3276f5e
--- /dev/null
+++ b/packages/react/src/generators/remote/files/rspack-module-federation-ts/rspack.config.ts__tmpl__
@@ -0,0 +1,16 @@
+import {composePlugins, withNx, withReact} from '@nx/rspack';
+import {withModuleFederation} from '@nx/rspack/module-federation';
+
+import baseConfig from './module-federation.config';
+
+const config = {
+ ...baseConfig,
+};
+
+// Nx plugins for rspack to build config object from Nx options and context.
+/**
+ * DTS Plugin is disabled in Nx Workspaces as Nx already provides Typing support Module Federation
+ * The DTS Plugin can be enabled by setting dts: true
+ * Learn more about the DTS Plugin here: https://module-federation.io/configure/dts.html
+ */
+export default composePlugins(withNx(), withReact(), withModuleFederation(config, { dts: false }));
diff --git a/packages/react/src/generators/remote/files/rspack-module-federation-ts/tsconfig.lint.json__tmpl__ b/packages/react/src/generators/remote/files/rspack-module-federation-ts/tsconfig.lint.json__tmpl__
new file mode 100644
index 0000000000..e279c41b31
--- /dev/null
+++ b/packages/react/src/generators/remote/files/rspack-module-federation-ts/tsconfig.lint.json__tmpl__
@@ -0,0 +1,19 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../dist/out-tsc",
+ "types": [
+ "node",
+ "@nx/react/typings/cssmodule.d.ts",
+ "@nx/react/typings/image.d.ts"
+ ]
+ },
+ "include": [
+ "src/**/*.js",
+ "src/**/*.jsx",
+ "src/**/*.ts",
+ "src/**/*.tsx",
+ "rspack.config.ts",
+ "rspack.prod.config.ts"
+ ]
+}
diff --git a/packages/react/src/generators/remote/files/module-federation/module-federation.config.js__tmpl__ b/packages/react/src/generators/remote/files/rspack-module-federation/module-federation.config.js__tmpl__
similarity index 100%
rename from packages/react/src/generators/remote/files/module-federation/module-federation.config.js__tmpl__
rename to packages/react/src/generators/remote/files/rspack-module-federation/module-federation.config.js__tmpl__
diff --git a/packages/react/src/generators/remote/files/rspack-module-federation/rspack.config.js__tmpl__ b/packages/react/src/generators/remote/files/rspack-module-federation/rspack.config.js__tmpl__
new file mode 100644
index 0000000000..f85ac78d1d
--- /dev/null
+++ b/packages/react/src/generators/remote/files/rspack-module-federation/rspack.config.js__tmpl__
@@ -0,0 +1,16 @@
+const { composePlugins, withNx, withReact } = require('@nx/rspack');
+const { withModuleFederation } = require('@nx/rspack/module-federation');
+
+const baseConfig = require('./module-federation.config');
+
+const config = {
+ ...baseConfig,
+};
+
+// Nx plugins for rspack to build config object from Nx options and context.
+/**
+ * DTS Plugin is disabled in Nx Workspaces as Nx already provides Typing support Module Federation
+ * The DTS Plugin can be enabled by setting dts: true
+ * Learn more about the DTS Plugin here: https://module-federation.io/configure/dts.html
+ */
+module.exports = composePlugins(withNx(), withReact(), withModuleFederation(config, { dts: false }));
diff --git a/packages/react/src/generators/remote/files/rspack-module-federation/rspack.config.prod.js__tmpl__ b/packages/react/src/generators/remote/files/rspack-module-federation/rspack.config.prod.js__tmpl__
new file mode 100644
index 0000000000..fdb0fc62c7
--- /dev/null
+++ b/packages/react/src/generators/remote/files/rspack-module-federation/rspack.config.prod.js__tmpl__
@@ -0,0 +1 @@
+module.exports = require('./rspack.config');
\ No newline at end of file
diff --git a/packages/react/src/generators/remote/files/module-federation-ssr-ts/module-federation.server.config.ts__tmpl__ b/packages/react/src/generators/remote/files/webpack-module-federation-ssr-ts/module-federation.server.config.ts__tmpl__
similarity index 100%
rename from packages/react/src/generators/remote/files/module-federation-ssr-ts/module-federation.server.config.ts__tmpl__
rename to packages/react/src/generators/remote/files/webpack-module-federation-ssr-ts/module-federation.server.config.ts__tmpl__
diff --git a/packages/react/src/generators/remote/files/webpack-module-federation-ssr-ts/server.ts__tmpl__ b/packages/react/src/generators/remote/files/webpack-module-federation-ssr-ts/server.ts__tmpl__
new file mode 100644
index 0000000000..9723e09478
--- /dev/null
+++ b/packages/react/src/generators/remote/files/webpack-module-federation-ssr-ts/server.ts__tmpl__
@@ -0,0 +1,45 @@
+import * as path from 'path';
+import express from 'express';
+import cors from 'cors';
+
+import { handleRequest } from './src/main.server';
+
+const port = process.env['PORT'] || <%= port %>;
+const app = express();
+
+const browserDist = path.join(process.cwd(), '<%= browserBuildOutputPath %>');
+const serverDist = path.join(process.cwd(), '<%= serverBuildOutputPath %>');
+const indexPath = path.join(browserDist, 'index.html');
+
+app.use(cors());
+
+// Client-side static bundles
+app.get(
+ '*.*',
+ express.static(browserDist, {
+ maxAge: '1y',
+ })
+);
+
+// Static bundles for server-side module federation
+app.use('/server',
+ express.static(serverDist, {
+ maxAge: '1y'
+ })
+);
+
+app.use('*', handleRequest(indexPath));
+
+const server = app.listen(port, () => {
+ console.log(`Express server listening on http://localhost:${port}`);
+
+ /**
+ * DO NOT REMOVE IF USING @nx/react:module-federation-dev-ssr executor
+ * to serve your Host application with this Remote application.
+ * This message allows Nx to determine when the Remote is ready to be
+ * consumed by the Host.
+ */
+ process.send?.('nx.server.ready');
+});
+
+server.on('error', console.error);
diff --git a/packages/react/src/generators/remote/files/module-federation-ts/tsconfig.lint.json__tmpl__ b/packages/react/src/generators/remote/files/webpack-module-federation-ssr-ts/tsconfig.lint.json__tmpl__
similarity index 100%
rename from packages/react/src/generators/remote/files/module-federation-ts/tsconfig.lint.json__tmpl__
rename to packages/react/src/generators/remote/files/webpack-module-federation-ssr-ts/tsconfig.lint.json__tmpl__
diff --git a/packages/react/src/generators/remote/files/module-federation-ssr-ts/webpack.server.config.ts__tmpl__ b/packages/react/src/generators/remote/files/webpack-module-federation-ssr-ts/webpack.server.config.ts__tmpl__
similarity index 100%
rename from packages/react/src/generators/remote/files/module-federation-ssr-ts/webpack.server.config.ts__tmpl__
rename to packages/react/src/generators/remote/files/webpack-module-federation-ssr-ts/webpack.server.config.ts__tmpl__
diff --git a/packages/react/src/generators/remote/files/module-federation-ssr/module-federation.server.config.js__tmpl__ b/packages/react/src/generators/remote/files/webpack-module-federation-ssr/module-federation.server.config.js__tmpl__
similarity index 100%
rename from packages/react/src/generators/remote/files/module-federation-ssr/module-federation.server.config.js__tmpl__
rename to packages/react/src/generators/remote/files/webpack-module-federation-ssr/module-federation.server.config.js__tmpl__
diff --git a/packages/react/src/generators/remote/files/webpack-module-federation-ssr/server.ts__tmpl__ b/packages/react/src/generators/remote/files/webpack-module-federation-ssr/server.ts__tmpl__
new file mode 100644
index 0000000000..9723e09478
--- /dev/null
+++ b/packages/react/src/generators/remote/files/webpack-module-federation-ssr/server.ts__tmpl__
@@ -0,0 +1,45 @@
+import * as path from 'path';
+import express from 'express';
+import cors from 'cors';
+
+import { handleRequest } from './src/main.server';
+
+const port = process.env['PORT'] || <%= port %>;
+const app = express();
+
+const browserDist = path.join(process.cwd(), '<%= browserBuildOutputPath %>');
+const serverDist = path.join(process.cwd(), '<%= serverBuildOutputPath %>');
+const indexPath = path.join(browserDist, 'index.html');
+
+app.use(cors());
+
+// Client-side static bundles
+app.get(
+ '*.*',
+ express.static(browserDist, {
+ maxAge: '1y',
+ })
+);
+
+// Static bundles for server-side module federation
+app.use('/server',
+ express.static(serverDist, {
+ maxAge: '1y'
+ })
+);
+
+app.use('*', handleRequest(indexPath));
+
+const server = app.listen(port, () => {
+ console.log(`Express server listening on http://localhost:${port}`);
+
+ /**
+ * DO NOT REMOVE IF USING @nx/react:module-federation-dev-ssr executor
+ * to serve your Host application with this Remote application.
+ * This message allows Nx to determine when the Remote is ready to be
+ * consumed by the Host.
+ */
+ process.send?.('nx.server.ready');
+});
+
+server.on('error', console.error);
diff --git a/packages/react/src/generators/remote/files/module-federation-ssr/webpack.server.config.js__tmpl__ b/packages/react/src/generators/remote/files/webpack-module-federation-ssr/webpack.server.config.js__tmpl__
similarity index 100%
rename from packages/react/src/generators/remote/files/module-federation-ssr/webpack.server.config.js__tmpl__
rename to packages/react/src/generators/remote/files/webpack-module-federation-ssr/webpack.server.config.js__tmpl__
diff --git a/packages/react/src/generators/remote/files/module-federation-ts/module-federation.config.ts__tmpl__ b/packages/react/src/generators/remote/files/webpack-module-federation-ts/module-federation.config.ts__tmpl__
similarity index 100%
rename from packages/react/src/generators/remote/files/module-federation-ts/module-federation.config.ts__tmpl__
rename to packages/react/src/generators/remote/files/webpack-module-federation-ts/module-federation.config.ts__tmpl__
diff --git a/packages/react/src/generators/remote/files/webpack-module-federation-ts/tsconfig.lint.json__tmpl__ b/packages/react/src/generators/remote/files/webpack-module-federation-ts/tsconfig.lint.json__tmpl__
new file mode 100644
index 0000000000..0bf57fc48c
--- /dev/null
+++ b/packages/react/src/generators/remote/files/webpack-module-federation-ts/tsconfig.lint.json__tmpl__
@@ -0,0 +1,19 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../dist/out-tsc",
+ "types": [
+ "node",
+ "@nx/react/typings/cssmodule.d.ts",
+ "@nx/react/typings/image.d.ts"
+ ]
+ },
+ "include": [
+ "src/**/*.js",
+ "src/**/*.jsx",
+ "src/**/*.ts",
+ "src/**/*.tsx",
+ "webpack.config.ts",
+ "webpack.prod.config.ts"
+ ]
+}
diff --git a/packages/react/src/generators/remote/files/module-federation-ts/webpack.config.prod.ts__tmpl__ b/packages/react/src/generators/remote/files/webpack-module-federation-ts/webpack.config.prod.ts__tmpl__
similarity index 100%
rename from packages/react/src/generators/remote/files/module-federation-ts/webpack.config.prod.ts__tmpl__
rename to packages/react/src/generators/remote/files/webpack-module-federation-ts/webpack.config.prod.ts__tmpl__
diff --git a/packages/react/src/generators/remote/files/module-federation-ts/webpack.config.ts__tmpl__ b/packages/react/src/generators/remote/files/webpack-module-federation-ts/webpack.config.ts__tmpl__
similarity index 100%
rename from packages/react/src/generators/remote/files/module-federation-ts/webpack.config.ts__tmpl__
rename to packages/react/src/generators/remote/files/webpack-module-federation-ts/webpack.config.ts__tmpl__
diff --git a/packages/react/src/generators/remote/files/webpack-module-federation/module-federation.config.js__tmpl__ b/packages/react/src/generators/remote/files/webpack-module-federation/module-federation.config.js__tmpl__
new file mode 100644
index 0000000000..da1a17ef1c
--- /dev/null
+++ b/packages/react/src/generators/remote/files/webpack-module-federation/module-federation.config.js__tmpl__
@@ -0,0 +1,9 @@
+module.exports = {
+ name: '<%= projectName %>',
+ <% if (dynamic) { %>
+ library: { type: 'var', name: '<%= projectName %>'},
+ <% } %>
+ exposes: {
+ './Module': './src/remote-entry.<%= js ? 'js' : 'ts' %>',
+ },
+};
diff --git a/packages/react/src/generators/remote/files/module-federation/webpack.config.js__tmpl__ b/packages/react/src/generators/remote/files/webpack-module-federation/webpack.config.js__tmpl__
similarity index 100%
rename from packages/react/src/generators/remote/files/module-federation/webpack.config.js__tmpl__
rename to packages/react/src/generators/remote/files/webpack-module-federation/webpack.config.js__tmpl__
diff --git a/packages/react/src/generators/remote/files/module-federation/webpack.config.prod.js__tmpl__ b/packages/react/src/generators/remote/files/webpack-module-federation/webpack.config.prod.js__tmpl__
similarity index 100%
rename from packages/react/src/generators/remote/files/module-federation/webpack.config.prod.js__tmpl__
rename to packages/react/src/generators/remote/files/webpack-module-federation/webpack.config.prod.js__tmpl__
diff --git a/packages/react/src/generators/remote/lib/setup-ssr-for-remote.ts b/packages/react/src/generators/remote/lib/setup-ssr-for-remote.ts
index a19938152a..862f7a4a9d 100644
--- a/packages/react/src/generators/remote/lib/setup-ssr-for-remote.ts
+++ b/packages/react/src/generators/remote/lib/setup-ssr-for-remote.ts
@@ -21,8 +21,12 @@ export async function setupSsrForRemote(
const project = readProjectConfiguration(tree, appName);
const pathToModuleFederationSsrFiles = options.typescriptConfiguration
- ? 'module-federation-ssr-ts'
- : 'module-federation-ssr';
+ ? `${
+ options.bundler === 'rspack' ? 'rspack-' : 'webpack-'
+ }module-federation-ssr-ts`
+ : `${
+ options.bundler === 'rspack' ? 'rspack-' : 'webpack-'
+ }module-federation-ssr`;
generateFiles(
tree,
diff --git a/packages/react/src/generators/remote/lib/update-host-with-remote.ts b/packages/react/src/generators/remote/lib/update-host-with-remote.ts
index f8d799b500..5a904aa001 100644
--- a/packages/react/src/generators/remote/lib/update-host-with-remote.ts
+++ b/packages/react/src/generators/remote/lib/update-host-with-remote.ts
@@ -88,11 +88,15 @@ function findAppComponentPath(host: Tree, sourceRoot: string) {
'app/app.tsx',
'app/App.tsx',
'app/app.js',
+ 'app/app.jsx',
'app/App.js',
+ 'app/App.jsx',
'app.tsx',
'App.tsx',
'app.js',
'App.js',
+ 'app.jsx',
+ 'App.jsx',
];
for (const loc of locations) {
if (host.exists(joinPathFragments(sourceRoot, loc))) {
diff --git a/packages/react/src/generators/remote/remote.rspack.spec.ts b/packages/react/src/generators/remote/remote.rspack.spec.ts
new file mode 100644
index 0000000000..e3b312be5f
--- /dev/null
+++ b/packages/react/src/generators/remote/remote.rspack.spec.ts
@@ -0,0 +1,337 @@
+import 'nx/src/internal-testing-utils/mock-project-graph';
+
+import { ProjectGraph, readJson, readNxJson } from '@nx/devkit';
+import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
+import { Linter } from '@nx/eslint';
+import remote from './remote';
+import { getRootTsConfigPathInTree } from '@nx/js';
+
+jest.mock('@nx/devkit', () => {
+ const original = jest.requireActual('@nx/devkit');
+ return {
+ ...original,
+ readCachedProjectGraph: jest.fn().mockImplementation(
+ (): ProjectGraph => ({
+ dependencies: {},
+ nodes: {
+ test: {
+ name: 'test',
+ type: 'app',
+ data: {
+ root: 'test',
+ sourceRoot: 'test/src',
+ targets: {
+ build: {
+ executor: '@nx/rspack:rspack',
+ outputs: ['{options.outputPath}'],
+ defaultConfiguration: 'production',
+ options: {
+ compiler: 'babel',
+ outputPath: 'dist/test',
+ index: 'test/src/index.html',
+ baseHref: '/',
+ main: `test/src/main.tsx`,
+ tsConfig: 'test/tsconfig.app.json',
+ assets: ['test/src/favicon.ico', 'src/assets'],
+ styles: [`test/src/styles.css`],
+ scripts: [],
+ rspackConfig: 'test/rspack.config.js',
+ },
+ configurations: {
+ development: {
+ extractLicenses: false,
+ optimization: false,
+ sourceMap: true,
+ vendorChunk: true,
+ },
+ production: {
+ fileReplacements: [
+ {
+ replace: `test/src/environments/environment.ts`,
+ with: `test/src/environments/environment.prod.ts`,
+ },
+ ],
+ optimization: true,
+ outputHashing: 'all',
+ sourceMap: false,
+ namedChunks: false,
+ extractLicenses: true,
+ vendorChunk: false,
+ },
+ },
+ },
+ serve: {
+ executor: '@nx/rspack:dev-server',
+ defaultConfiguration: 'development',
+ options: {
+ buildTarget: `test:build`,
+ hmr: true,
+ },
+ configurations: {
+ development: {
+ buildTarget: `test:build:development`,
+ },
+ production: {
+ buildTarget: `test:build:production`,
+ hmr: false,
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ })
+ ),
+ };
+});
+
+// TODO(colum): turn these on when rspack is moved into the main repo
+xdescribe('remote generator', () => {
+ // TODO(@jaysoo): Turn this back to adding the plugin
+ let originalEnv: string;
+
+ beforeEach(() => {
+ originalEnv = process.env.NX_ADD_PLUGINS;
+ process.env.NX_ADD_PLUGINS = 'false';
+ });
+
+ afterEach(() => {
+ process.env.NX_ADD_PLUGINS = originalEnv;
+ });
+
+ describe('bundler=rspack', () => {
+ it('should create the remote with the correct config files', async () => {
+ const tree = createTreeWithEmptyWorkspace();
+ await remote(tree, {
+ name: 'test',
+ devServerPort: 4201,
+ e2eTestRunner: 'cypress',
+ linter: Linter.EsLint,
+ skipFormat: true,
+ style: 'css',
+ unitTestRunner: 'jest',
+ projectNameAndRootFormat: 'as-provided',
+ typescriptConfiguration: false,
+ bundler: 'rspack',
+ });
+
+ expect(tree.exists('test/rspack.config.js')).toBeTruthy();
+ expect(tree.exists('test/rspack.config.prod.js')).toBeTruthy();
+ expect(tree.exists('test/module-federation.config.js')).toBeTruthy();
+
+ expect(tree.read('test/rspack.config.js', 'utf-8')).toMatchSnapshot();
+ expect(
+ tree.read('test/rspack.config.prod.js', 'utf-8')
+ ).toMatchSnapshot();
+ expect(
+ tree.read('test/module-federation.config.js', 'utf-8')
+ ).toMatchSnapshot();
+
+ const tsconfigJson = readJson(tree, getRootTsConfigPathInTree(tree));
+ expect(tsconfigJson.compilerOptions.paths['test/Module']).toEqual([
+ 'test/src/remote-entry.ts',
+ ]);
+ });
+
+ it('should create the remote with the correct config files when --js=true', async () => {
+ const tree = createTreeWithEmptyWorkspace();
+ await remote(tree, {
+ name: 'test',
+ devServerPort: 4201,
+ e2eTestRunner: 'cypress',
+ linter: Linter.EsLint,
+ skipFormat: true,
+ style: 'css',
+ unitTestRunner: 'jest',
+ projectNameAndRootFormat: 'as-provided',
+ typescriptConfiguration: false,
+ js: true,
+ bundler: 'rspack',
+ });
+
+ expect(tree.exists('test/rspack.config.js')).toBeTruthy();
+ expect(tree.exists('test/rspack.config.prod.js')).toBeTruthy();
+ expect(tree.exists('test/module-federation.config.js')).toBeTruthy();
+
+ expect(tree.read('test/rspack.config.js', 'utf-8')).toMatchSnapshot();
+ expect(
+ tree.read('test/rspack.config.prod.js', 'utf-8')
+ ).toMatchSnapshot();
+ expect(
+ tree.read('test/module-federation.config.js', 'utf-8')
+ ).toMatchSnapshot();
+
+ const tsconfigJson = readJson(tree, getRootTsConfigPathInTree(tree));
+ expect(tsconfigJson.compilerOptions.paths['test/Module']).toEqual([
+ 'test/src/remote-entry.js',
+ ]);
+ });
+
+ it('should create the remote with the correct config files when --typescriptConfiguration=true', async () => {
+ const tree = createTreeWithEmptyWorkspace();
+ await remote(tree, {
+ name: 'test',
+ devServerPort: 4201,
+ e2eTestRunner: 'cypress',
+ linter: Linter.EsLint,
+ skipFormat: false,
+ style: 'css',
+ unitTestRunner: 'jest',
+ projectNameAndRootFormat: 'as-provided',
+ typescriptConfiguration: true,
+ bundler: 'rspack',
+ });
+
+ expect(tree.exists('test/rspack.config.ts')).toBeTruthy();
+ expect(tree.exists('test/rspack.config.prod.ts')).toBeTruthy();
+ expect(tree.exists('test/module-federation.config.ts')).toBeTruthy();
+
+ expect(tree.read('test/rspack.config.ts', 'utf-8')).toMatchSnapshot();
+ expect(
+ tree.read('test/rspack.config.prod.ts', 'utf-8')
+ ).toMatchSnapshot();
+ expect(
+ tree.read('test/module-federation.config.ts', 'utf-8')
+ ).toMatchSnapshot();
+ });
+
+ it('should install @nx/web for the file-server executor', async () => {
+ const tree = createTreeWithEmptyWorkspace();
+ await remote(tree, {
+ name: 'test',
+ devServerPort: 4201,
+ e2eTestRunner: 'cypress',
+ linter: Linter.EsLint,
+ skipFormat: true,
+ style: 'css',
+ unitTestRunner: 'jest',
+ projectNameAndRootFormat: 'as-provided',
+ bundler: 'rspack',
+ });
+
+ const packageJson = readJson(tree, 'package.json');
+ expect(packageJson.devDependencies['@nx/web']).toBeDefined();
+ });
+
+ it('should not set the remote as the default project', async () => {
+ const tree = createTreeWithEmptyWorkspace();
+ await remote(tree, {
+ name: 'test',
+ devServerPort: 4201,
+ e2eTestRunner: 'cypress',
+ linter: Linter.EsLint,
+ skipFormat: true,
+ style: 'css',
+ unitTestRunner: 'jest',
+ projectNameAndRootFormat: 'as-provided',
+ bundler: 'rspack',
+ });
+
+ const { defaultProject } = readNxJson(tree);
+ expect(defaultProject).toBeUndefined();
+ });
+
+ it('should generate a remote-specific server.ts file for --ssr', async () => {
+ const tree = createTreeWithEmptyWorkspace();
+
+ await remote(tree, {
+ name: 'test',
+ devServerPort: 4201,
+ e2eTestRunner: 'cypress',
+ linter: Linter.EsLint,
+ skipFormat: true,
+ style: 'css',
+ unitTestRunner: 'jest',
+ ssr: true,
+ projectNameAndRootFormat: 'as-provided',
+ bundler: 'rspack',
+ });
+
+ const mainFile = tree.read('test/server.ts', 'utf-8');
+ expect(mainFile).toContain(`join(process.cwd(), 'dist/test/browser')`);
+ expect(mainFile).toContain('nx.server.ready');
+ });
+
+ it('should generate correct remote with config files when using --ssr', async () => {
+ const tree = createTreeWithEmptyWorkspace();
+
+ await remote(tree, {
+ name: 'test',
+ devServerPort: 4201,
+ e2eTestRunner: 'cypress',
+ linter: Linter.EsLint,
+ skipFormat: true,
+ style: 'css',
+ unitTestRunner: 'jest',
+ ssr: true,
+ projectNameAndRootFormat: 'as-provided',
+ typescriptConfiguration: false,
+ bundler: 'rspack',
+ });
+
+ expect(tree.exists('test/rspack.server.config.js')).toBeTruthy();
+ expect(
+ tree.exists('test/module-federation.server.config.js')
+ ).toBeTruthy();
+
+ expect(
+ tree.read('test/rspack.server.config.js', 'utf-8')
+ ).toMatchSnapshot();
+ expect(
+ tree.read('test/module-federation.server.config.js', 'utf-8')
+ ).toMatchSnapshot();
+ });
+
+ it('should generate correct remote with config files when using --ssr and --typescriptConfiguration=true', async () => {
+ const tree = createTreeWithEmptyWorkspace();
+
+ await remote(tree, {
+ name: 'test',
+ devServerPort: 4201,
+ e2eTestRunner: 'cypress',
+ linter: Linter.EsLint,
+ skipFormat: false,
+ style: 'css',
+ unitTestRunner: 'jest',
+ ssr: true,
+ projectNameAndRootFormat: 'as-provided',
+ typescriptConfiguration: true,
+ bundler: 'rspack',
+ });
+
+ expect(tree.exists('test/rspack.server.config.ts')).toBeTruthy();
+ expect(
+ tree.exists('test/module-federation.server.config.ts')
+ ).toBeTruthy();
+
+ expect(
+ tree.read('test/rspack.server.config.ts', 'utf-8')
+ ).toMatchSnapshot();
+ expect(
+ tree.read('test/module-federation.server.config.ts', 'utf-8')
+ ).toMatchSnapshot();
+ });
+
+ it('should throw an error if invalid remotes names are provided and --dynamic is set to true', async () => {
+ const tree = createTreeWithEmptyWorkspace();
+ const name = 'invalid-dynamic-remote-name';
+ await expect(
+ remote(tree, {
+ name,
+ devServerPort: 4209,
+ dynamic: true,
+ e2eTestRunner: 'cypress',
+ linter: Linter.EsLint,
+ skipFormat: false,
+ style: 'css',
+ unitTestRunner: 'jest',
+ ssr: true,
+ projectNameAndRootFormat: 'as-provided',
+ typescriptConfiguration: true,
+ bundler: 'rspack',
+ })
+ ).rejects.toThrowError(`Invalid remote name provided: ${name}.`);
+ });
+ });
+});
diff --git a/packages/react/src/generators/remote/remote.spec.ts b/packages/react/src/generators/remote/remote.spec.ts
deleted file mode 100644
index bc755b9b83..0000000000
--- a/packages/react/src/generators/remote/remote.spec.ts
+++ /dev/null
@@ -1,315 +0,0 @@
-import 'nx/src/internal-testing-utils/mock-project-graph';
-
-import { ProjectGraph, readJson, readNxJson } from '@nx/devkit';
-import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
-import { Linter } from '@nx/eslint';
-import remote from './remote';
-import { getRootTsConfigPathInTree } from '@nx/js';
-
-jest.mock('@nx/devkit', () => {
- const original = jest.requireActual('@nx/devkit');
- return {
- ...original,
- readCachedProjectGraph: jest.fn().mockImplementation(
- (): ProjectGraph => ({
- dependencies: {},
- nodes: {
- test: {
- name: 'test',
- type: 'app',
- data: {
- root: 'test',
- sourceRoot: 'test/src',
- targets: {
- build: {
- executor: '@nx/webpack:webpack',
- outputs: ['{options.outputPath}'],
- defaultConfiguration: 'production',
- options: {
- compiler: 'babel',
- outputPath: 'dist/test',
- index: 'test/src/index.html',
- baseHref: '/',
- main: `test/src/main.tsx`,
- tsConfig: 'test/tsconfig.app.json',
- assets: ['test/src/favicon.ico', 'src/assets'],
- styles: [`test/src/styles.css`],
- scripts: [],
- webpackConfig: 'test/webpack.config.js',
- },
- configurations: {
- development: {
- extractLicenses: false,
- optimization: false,
- sourceMap: true,
- vendorChunk: true,
- },
- production: {
- fileReplacements: [
- {
- replace: `test/src/environments/environment.ts`,
- with: `test/src/environments/environment.prod.ts`,
- },
- ],
- optimization: true,
- outputHashing: 'all',
- sourceMap: false,
- namedChunks: false,
- extractLicenses: true,
- vendorChunk: false,
- },
- },
- },
- serve: {
- executor: '@nx/webpack:dev-server',
- defaultConfiguration: 'development',
- options: {
- buildTarget: `test:build`,
- hmr: true,
- },
- configurations: {
- development: {
- buildTarget: `test:build:development`,
- },
- production: {
- buildTarget: `test:build:production`,
- hmr: false,
- },
- },
- },
- },
- },
- },
- },
- })
- ),
- };
-});
-
-describe('remote generator', () => {
- // TODO(@jaysoo): Turn this back to adding the plugin
- let originalEnv: string;
-
- beforeEach(() => {
- originalEnv = process.env.NX_ADD_PLUGINS;
- process.env.NX_ADD_PLUGINS = 'false';
- });
-
- afterEach(() => {
- process.env.NX_ADD_PLUGINS = originalEnv;
- });
-
- it('should create the remote with the correct config files', async () => {
- const tree = createTreeWithEmptyWorkspace();
- await remote(tree, {
- name: 'test',
- devServerPort: 4201,
- e2eTestRunner: 'cypress',
- linter: Linter.EsLint,
- skipFormat: true,
- style: 'css',
- unitTestRunner: 'jest',
- projectNameAndRootFormat: 'as-provided',
- typescriptConfiguration: false,
- });
-
- expect(tree.exists('test/webpack.config.js')).toBeTruthy();
- expect(tree.exists('test/webpack.config.prod.js')).toBeTruthy();
- expect(tree.exists('test/module-federation.config.js')).toBeTruthy();
-
- expect(tree.read('test/webpack.config.js', 'utf-8')).toMatchSnapshot();
- expect(tree.read('test/webpack.config.prod.js', 'utf-8')).toMatchSnapshot();
- expect(
- tree.read('test/module-federation.config.js', 'utf-8')
- ).toMatchSnapshot();
-
- const tsconfigJson = readJson(tree, getRootTsConfigPathInTree(tree));
- expect(tsconfigJson.compilerOptions.paths['test/Module']).toEqual([
- 'test/src/remote-entry.ts',
- ]);
- });
-
- it('should create the remote with the correct config files when --js=true', async () => {
- const tree = createTreeWithEmptyWorkspace();
- await remote(tree, {
- name: 'test',
- devServerPort: 4201,
- e2eTestRunner: 'cypress',
- linter: Linter.EsLint,
- skipFormat: true,
- style: 'css',
- unitTestRunner: 'jest',
- projectNameAndRootFormat: 'as-provided',
- typescriptConfiguration: false,
- js: true,
- });
-
- expect(tree.exists('test/webpack.config.js')).toBeTruthy();
- expect(tree.exists('test/webpack.config.prod.js')).toBeTruthy();
- expect(tree.exists('test/module-federation.config.js')).toBeTruthy();
-
- expect(tree.read('test/webpack.config.js', 'utf-8')).toMatchSnapshot();
- expect(tree.read('test/webpack.config.prod.js', 'utf-8')).toMatchSnapshot();
- expect(
- tree.read('test/module-federation.config.js', 'utf-8')
- ).toMatchSnapshot();
-
- const tsconfigJson = readJson(tree, getRootTsConfigPathInTree(tree));
- expect(tsconfigJson.compilerOptions.paths['test/Module']).toEqual([
- 'test/src/remote-entry.js',
- ]);
- });
-
- it('should create the remote with the correct config files when --typescriptConfiguration=true', async () => {
- const tree = createTreeWithEmptyWorkspace();
- await remote(tree, {
- name: 'test',
- devServerPort: 4201,
- e2eTestRunner: 'cypress',
- linter: Linter.EsLint,
- skipFormat: false,
- style: 'css',
- unitTestRunner: 'jest',
- projectNameAndRootFormat: 'as-provided',
- typescriptConfiguration: true,
- });
-
- expect(tree.exists('test/webpack.config.ts')).toBeTruthy();
- expect(tree.exists('test/webpack.config.prod.ts')).toBeTruthy();
- expect(tree.exists('test/module-federation.config.ts')).toBeTruthy();
-
- expect(tree.read('test/webpack.config.ts', 'utf-8')).toMatchSnapshot();
- expect(tree.read('test/webpack.config.prod.ts', 'utf-8')).toMatchSnapshot();
- expect(
- tree.read('test/module-federation.config.ts', 'utf-8')
- ).toMatchSnapshot();
- });
-
- it('should install @nx/web for the file-server executor', async () => {
- const tree = createTreeWithEmptyWorkspace();
- await remote(tree, {
- name: 'test',
- devServerPort: 4201,
- e2eTestRunner: 'cypress',
- linter: Linter.EsLint,
- skipFormat: true,
- style: 'css',
- unitTestRunner: 'jest',
- projectNameAndRootFormat: 'as-provided',
- });
-
- const packageJson = readJson(tree, 'package.json');
- expect(packageJson.devDependencies['@nx/web']).toBeDefined();
- });
-
- it('should not set the remote as the default project', async () => {
- const tree = createTreeWithEmptyWorkspace();
- await remote(tree, {
- name: 'test',
- devServerPort: 4201,
- e2eTestRunner: 'cypress',
- linter: Linter.EsLint,
- skipFormat: true,
- style: 'css',
- unitTestRunner: 'jest',
- projectNameAndRootFormat: 'as-provided',
- });
-
- const { defaultProject } = readNxJson(tree);
- expect(defaultProject).toBeUndefined();
- });
-
- it('should generate a remote-specific server.ts file for --ssr', async () => {
- const tree = createTreeWithEmptyWorkspace();
-
- await remote(tree, {
- name: 'test',
- devServerPort: 4201,
- e2eTestRunner: 'cypress',
- linter: Linter.EsLint,
- skipFormat: true,
- style: 'css',
- unitTestRunner: 'jest',
- ssr: true,
- projectNameAndRootFormat: 'as-provided',
- });
-
- const mainFile = tree.read('test/server.ts', 'utf-8');
- expect(mainFile).toContain(`join(process.cwd(), 'dist/test/browser')`);
- expect(mainFile).toContain('nx.server.ready');
- });
-
- it('should generate correct remote with config files when using --ssr', async () => {
- const tree = createTreeWithEmptyWorkspace();
-
- await remote(tree, {
- name: 'test',
- devServerPort: 4201,
- e2eTestRunner: 'cypress',
- linter: Linter.EsLint,
- skipFormat: true,
- style: 'css',
- unitTestRunner: 'jest',
- ssr: true,
- projectNameAndRootFormat: 'as-provided',
- typescriptConfiguration: false,
- });
-
- expect(tree.exists('test/webpack.server.config.js')).toBeTruthy();
- expect(tree.exists('test/module-federation.server.config.js')).toBeTruthy();
-
- expect(
- tree.read('test/webpack.server.config.js', 'utf-8')
- ).toMatchSnapshot();
- expect(
- tree.read('test/module-federation.server.config.js', 'utf-8')
- ).toMatchSnapshot();
- });
-
- it('should generate correct remote with config files when using --ssr and --typescriptConfiguration=true', async () => {
- const tree = createTreeWithEmptyWorkspace();
-
- await remote(tree, {
- name: 'test',
- devServerPort: 4201,
- e2eTestRunner: 'cypress',
- linter: Linter.EsLint,
- skipFormat: false,
- style: 'css',
- unitTestRunner: 'jest',
- ssr: true,
- projectNameAndRootFormat: 'as-provided',
- typescriptConfiguration: true,
- });
-
- expect(tree.exists('test/webpack.server.config.ts')).toBeTruthy();
- expect(tree.exists('test/module-federation.server.config.ts')).toBeTruthy();
-
- expect(
- tree.read('test/webpack.server.config.ts', 'utf-8')
- ).toMatchSnapshot();
- expect(
- tree.read('test/module-federation.server.config.ts', 'utf-8')
- ).toMatchSnapshot();
- });
-
- it('should throw an error if invalid remotes names are provided and --dynamic is set to true', async () => {
- const tree = createTreeWithEmptyWorkspace();
- const name = 'invalid-dynamic-remote-name';
- await expect(
- remote(tree, {
- name,
- devServerPort: 4209,
- dynamic: true,
- e2eTestRunner: 'cypress',
- linter: Linter.EsLint,
- skipFormat: false,
- style: 'css',
- unitTestRunner: 'jest',
- ssr: true,
- projectNameAndRootFormat: 'as-provided',
- typescriptConfiguration: true,
- })
- ).rejects.toThrowError(`Invalid remote name provided: ${name}.`);
- });
-});
diff --git a/packages/react/src/generators/remote/remote.ts b/packages/react/src/generators/remote/remote.ts
index c878b179bc..b993f4acb7 100644
--- a/packages/react/src/generators/remote/remote.ts
+++ b/packages/react/src/generators/remote/remote.ts
@@ -39,14 +39,27 @@ export function addModuleFederationFiles(
generateFiles(
host,
- join(__dirname, `./files/${options.js ? 'common' : 'common-ts'}`),
+ join(
+ __dirname,
+ `./files/${
+ options.js
+ ? options.bundler === 'rspack'
+ ? 'rspack-common'
+ : 'common'
+ : 'common-ts'
+ }`
+ ),
options.appProjectRoot,
templateVariables
);
const pathToModuleFederationFiles = options.typescriptConfiguration
- ? 'module-federation-ts'
- : 'module-federation';
+ ? `${
+ options.bundler === 'rspack' ? 'rspack-' : 'webpack-'
+ }module-federation-ts`
+ : `${
+ options.bundler === 'rspack' ? 'rspack-' : 'webpack-'
+ }module-federation`;
generateFiles(
host,
@@ -56,16 +69,18 @@ export function addModuleFederationFiles(
);
if (options.typescriptConfiguration) {
- const pathToWebpackConfig = joinPathFragments(
+ const pathToBundlerConfig = joinPathFragments(
options.appProjectRoot,
- 'webpack.config.js'
+ options.bundler === 'rspack' ? 'rspack.config.js' : 'webpack.config.js'
);
const pathToWebpackProdConfig = joinPathFragments(
options.appProjectRoot,
- 'webpack.config.prod.js'
+ options.bundler === 'rspack'
+ ? 'rspack.config.prod.js'
+ : 'webpack.config.prod.js'
);
- if (host.exists(pathToWebpackConfig)) {
- host.delete(pathToWebpackConfig);
+ if (host.exists(pathToBundlerConfig)) {
+ host.delete(pathToBundlerConfig);
}
if (host.exists(pathToWebpackProdConfig)) {
host.delete(pathToWebpackProdConfig);
@@ -92,6 +107,7 @@ export async function remoteGeneratorInternal(host: Tree, schema: Schema) {
dynamic: schema.dynamic ?? false,
// TODO(colum): remove when MF works with Crystal
addPlugin: false,
+ bundler: schema.bundler ?? 'rspack',
};
if (options.dynamic) {
@@ -107,8 +123,6 @@ export async function remoteGeneratorInternal(host: Tree, schema: Schema) {
const initAppTask = await applicationGenerator(host, {
...options,
- // Only webpack works with module federation for now.
- bundler: 'webpack',
skipFormat: true,
});
tasks.push(initAppTask);
@@ -121,8 +135,20 @@ export async function remoteGeneratorInternal(host: Tree, schema: Schema) {
// Renaming original entry file so we can use `import(./bootstrap)` in
// new entry file.
host.rename(
- join(options.appProjectRoot, maybeJs(options, 'src/main.tsx')),
- join(options.appProjectRoot, maybeJs(options, 'src/bootstrap.tsx'))
+ join(
+ options.appProjectRoot,
+ maybeJs(
+ { js: options.js, useJsx: options.bundler === 'rspack' },
+ 'src/main.tsx'
+ )
+ ),
+ join(
+ options.appProjectRoot,
+ maybeJs(
+ { js: options.js, useJsx: options.bundler === 'rspack' },
+ 'src/bootstrap.tsx'
+ )
+ )
);
addModuleFederationFiles(host, options);
@@ -134,6 +160,7 @@ export async function remoteGeneratorInternal(host: Tree, schema: Schema) {
project: options.projectName,
serverPort: options.devServerPort,
skipFormat: true,
+ bundler: options.bundler,
});
tasks.push(setupSsrTask);
@@ -145,10 +172,19 @@ export async function remoteGeneratorInternal(host: Tree, schema: Schema) {
tasks.push(setupSsrForRemoteTask);
const projectConfig = readProjectConfiguration(host, options.projectName);
- projectConfig.targets.server.options.webpackConfig = joinPathFragments(
- projectConfig.root,
- `webpack.server.config.${options.typescriptConfiguration ? 'ts' : 'js'}`
- );
+ if (options.bundler === 'rspack') {
+ projectConfig.targets.server.executor = '@nx/rspack:rspack';
+ projectConfig.targets.server.options.rspackConfig = joinPathFragments(
+ projectConfig.root,
+ `rspack.server.config.${options.typescriptConfiguration ? 'ts' : 'js'}`
+ );
+ delete projectConfig.targets.server.options.webpackConfig;
+ } else {
+ projectConfig.targets.server.options.webpackConfig = joinPathFragments(
+ projectConfig.root,
+ `webpack.server.config.${options.typescriptConfiguration ? 'ts' : 'js'}`
+ );
+ }
updateProjectConfiguration(host, options.projectName, projectConfig);
}
if (!options.setParserOptionsProject) {
diff --git a/packages/react/src/generators/remote/remote.webpack.spec.ts b/packages/react/src/generators/remote/remote.webpack.spec.ts
new file mode 100644
index 0000000000..296487058c
--- /dev/null
+++ b/packages/react/src/generators/remote/remote.webpack.spec.ts
@@ -0,0 +1,336 @@
+import 'nx/src/internal-testing-utils/mock-project-graph';
+
+import { ProjectGraph, readJson, readNxJson } from '@nx/devkit';
+import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
+import { Linter } from '@nx/eslint';
+import remote from './remote';
+import { getRootTsConfigPathInTree } from '@nx/js';
+
+jest.mock('@nx/devkit', () => {
+ const original = jest.requireActual('@nx/devkit');
+ return {
+ ...original,
+ readCachedProjectGraph: jest.fn().mockImplementation(
+ (): ProjectGraph => ({
+ dependencies: {},
+ nodes: {
+ test: {
+ name: 'test',
+ type: 'app',
+ data: {
+ root: 'test',
+ sourceRoot: 'test/src',
+ targets: {
+ build: {
+ executor: '@nx/webpack:webpack',
+ outputs: ['{options.outputPath}'],
+ defaultConfiguration: 'production',
+ options: {
+ compiler: 'babel',
+ outputPath: 'dist/test',
+ index: 'test/src/index.html',
+ baseHref: '/',
+ main: `test/src/main.tsx`,
+ tsConfig: 'test/tsconfig.app.json',
+ assets: ['test/src/favicon.ico', 'src/assets'],
+ styles: [`test/src/styles.css`],
+ scripts: [],
+ webpackConfig: 'test/webpack.config.js',
+ },
+ configurations: {
+ development: {
+ extractLicenses: false,
+ optimization: false,
+ sourceMap: true,
+ vendorChunk: true,
+ },
+ production: {
+ fileReplacements: [
+ {
+ replace: `test/src/environments/environment.ts`,
+ with: `test/src/environments/environment.prod.ts`,
+ },
+ ],
+ optimization: true,
+ outputHashing: 'all',
+ sourceMap: false,
+ namedChunks: false,
+ extractLicenses: true,
+ vendorChunk: false,
+ },
+ },
+ },
+ serve: {
+ executor: '@nx/webpack:dev-server',
+ defaultConfiguration: 'development',
+ options: {
+ buildTarget: `test:build`,
+ hmr: true,
+ },
+ configurations: {
+ development: {
+ buildTarget: `test:build:development`,
+ },
+ production: {
+ buildTarget: `test:build:production`,
+ hmr: false,
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ })
+ ),
+ };
+});
+
+describe('remote generator', () => {
+ // TODO(@jaysoo): Turn this back to adding the plugin
+ let originalEnv: string;
+
+ beforeEach(() => {
+ originalEnv = process.env.NX_ADD_PLUGINS;
+ process.env.NX_ADD_PLUGINS = 'false';
+ });
+
+ afterEach(() => {
+ process.env.NX_ADD_PLUGINS = originalEnv;
+ });
+
+ describe('bundler=webpack', () => {
+ it('should create the remote with the correct config files', async () => {
+ const tree = createTreeWithEmptyWorkspace();
+ await remote(tree, {
+ name: 'test',
+ devServerPort: 4201,
+ e2eTestRunner: 'cypress',
+ linter: Linter.EsLint,
+ skipFormat: true,
+ style: 'css',
+ unitTestRunner: 'jest',
+ projectNameAndRootFormat: 'as-provided',
+ typescriptConfiguration: false,
+ bundler: 'webpack',
+ });
+
+ expect(tree.exists('test/webpack.config.js')).toBeTruthy();
+ expect(tree.exists('test/webpack.config.prod.js')).toBeTruthy();
+ expect(tree.exists('test/module-federation.config.js')).toBeTruthy();
+
+ expect(tree.read('test/webpack.config.js', 'utf-8')).toMatchSnapshot();
+ expect(
+ tree.read('test/webpack.config.prod.js', 'utf-8')
+ ).toMatchSnapshot();
+ expect(
+ tree.read('test/module-federation.config.js', 'utf-8')
+ ).toMatchSnapshot();
+
+ const tsconfigJson = readJson(tree, getRootTsConfigPathInTree(tree));
+ expect(tsconfigJson.compilerOptions.paths['test/Module']).toEqual([
+ 'test/src/remote-entry.ts',
+ ]);
+ });
+
+ it('should create the remote with the correct config files when --js=true', async () => {
+ const tree = createTreeWithEmptyWorkspace();
+ await remote(tree, {
+ name: 'test',
+ devServerPort: 4201,
+ e2eTestRunner: 'cypress',
+ linter: Linter.EsLint,
+ skipFormat: true,
+ style: 'css',
+ unitTestRunner: 'jest',
+ projectNameAndRootFormat: 'as-provided',
+ typescriptConfiguration: false,
+ js: true,
+ bundler: 'webpack',
+ });
+
+ expect(tree.exists('test/webpack.config.js')).toBeTruthy();
+ expect(tree.exists('test/webpack.config.prod.js')).toBeTruthy();
+ expect(tree.exists('test/module-federation.config.js')).toBeTruthy();
+
+ expect(tree.read('test/webpack.config.js', 'utf-8')).toMatchSnapshot();
+ expect(
+ tree.read('test/webpack.config.prod.js', 'utf-8')
+ ).toMatchSnapshot();
+ expect(
+ tree.read('test/module-federation.config.js', 'utf-8')
+ ).toMatchSnapshot();
+
+ const tsconfigJson = readJson(tree, getRootTsConfigPathInTree(tree));
+ expect(tsconfigJson.compilerOptions.paths['test/Module']).toEqual([
+ 'test/src/remote-entry.js',
+ ]);
+ });
+
+ it('should create the remote with the correct config files when --typescriptConfiguration=true', async () => {
+ const tree = createTreeWithEmptyWorkspace();
+ await remote(tree, {
+ name: 'test',
+ devServerPort: 4201,
+ e2eTestRunner: 'cypress',
+ linter: Linter.EsLint,
+ skipFormat: false,
+ style: 'css',
+ unitTestRunner: 'jest',
+ projectNameAndRootFormat: 'as-provided',
+ typescriptConfiguration: true,
+ bundler: 'webpack',
+ });
+
+ expect(tree.exists('test/webpack.config.ts')).toBeTruthy();
+ expect(tree.exists('test/webpack.config.prod.ts')).toBeTruthy();
+ expect(tree.exists('test/module-federation.config.ts')).toBeTruthy();
+
+ expect(tree.read('test/webpack.config.ts', 'utf-8')).toMatchSnapshot();
+ expect(
+ tree.read('test/webpack.config.prod.ts', 'utf-8')
+ ).toMatchSnapshot();
+ expect(
+ tree.read('test/module-federation.config.ts', 'utf-8')
+ ).toMatchSnapshot();
+ });
+
+ it('should install @nx/web for the file-server executor', async () => {
+ const tree = createTreeWithEmptyWorkspace();
+ await remote(tree, {
+ name: 'test',
+ devServerPort: 4201,
+ e2eTestRunner: 'cypress',
+ linter: Linter.EsLint,
+ skipFormat: true,
+ style: 'css',
+ unitTestRunner: 'jest',
+ projectNameAndRootFormat: 'as-provided',
+ bundler: 'webpack',
+ });
+
+ const packageJson = readJson(tree, 'package.json');
+ expect(packageJson.devDependencies['@nx/web']).toBeDefined();
+ });
+
+ it('should not set the remote as the default project', async () => {
+ const tree = createTreeWithEmptyWorkspace();
+ await remote(tree, {
+ name: 'test',
+ devServerPort: 4201,
+ e2eTestRunner: 'cypress',
+ linter: Linter.EsLint,
+ skipFormat: true,
+ style: 'css',
+ unitTestRunner: 'jest',
+ projectNameAndRootFormat: 'as-provided',
+ bundler: 'webpack',
+ });
+
+ const { defaultProject } = readNxJson(tree);
+ expect(defaultProject).toBeUndefined();
+ });
+
+ it('should generate a remote-specific server.ts file for --ssr', async () => {
+ const tree = createTreeWithEmptyWorkspace();
+
+ await remote(tree, {
+ name: 'test',
+ devServerPort: 4201,
+ e2eTestRunner: 'cypress',
+ linter: Linter.EsLint,
+ skipFormat: true,
+ style: 'css',
+ unitTestRunner: 'jest',
+ ssr: true,
+ projectNameAndRootFormat: 'as-provided',
+ bundler: 'webpack',
+ });
+
+ const mainFile = tree.read('test/server.ts', 'utf-8');
+ expect(mainFile).toContain(`join(process.cwd(), 'dist/test/browser')`);
+ expect(mainFile).toContain('nx.server.ready');
+ });
+
+ it('should generate correct remote with config files when using --ssr', async () => {
+ const tree = createTreeWithEmptyWorkspace();
+
+ await remote(tree, {
+ name: 'test',
+ devServerPort: 4201,
+ e2eTestRunner: 'cypress',
+ linter: Linter.EsLint,
+ skipFormat: true,
+ style: 'css',
+ unitTestRunner: 'jest',
+ ssr: true,
+ projectNameAndRootFormat: 'as-provided',
+ typescriptConfiguration: false,
+ bundler: 'webpack',
+ });
+
+ expect(tree.exists('test/webpack.server.config.js')).toBeTruthy();
+ expect(
+ tree.exists('test/module-federation.server.config.js')
+ ).toBeTruthy();
+
+ expect(
+ tree.read('test/webpack.server.config.js', 'utf-8')
+ ).toMatchSnapshot();
+ expect(
+ tree.read('test/module-federation.server.config.js', 'utf-8')
+ ).toMatchSnapshot();
+ });
+
+ it('should generate correct remote with config files when using --ssr and --typescriptConfiguration=true', async () => {
+ const tree = createTreeWithEmptyWorkspace();
+
+ await remote(tree, {
+ name: 'test',
+ devServerPort: 4201,
+ e2eTestRunner: 'cypress',
+ linter: Linter.EsLint,
+ skipFormat: false,
+ style: 'css',
+ unitTestRunner: 'jest',
+ ssr: true,
+ projectNameAndRootFormat: 'as-provided',
+ typescriptConfiguration: true,
+ bundler: 'webpack',
+ });
+
+ expect(tree.exists('test/webpack.server.config.ts')).toBeTruthy();
+ expect(
+ tree.exists('test/module-federation.server.config.ts')
+ ).toBeTruthy();
+
+ expect(
+ tree.read('test/webpack.server.config.ts', 'utf-8')
+ ).toMatchSnapshot();
+ expect(
+ tree.read('test/module-federation.server.config.ts', 'utf-8')
+ ).toMatchSnapshot();
+ });
+
+ it('should throw an error if invalid remotes names are provided and --dynamic is set to true', async () => {
+ const tree = createTreeWithEmptyWorkspace();
+ const name = 'invalid-dynamic-remote-name';
+ await expect(
+ remote(tree, {
+ name,
+ devServerPort: 4209,
+ dynamic: true,
+ e2eTestRunner: 'cypress',
+ linter: Linter.EsLint,
+ skipFormat: false,
+ style: 'css',
+ unitTestRunner: 'jest',
+ ssr: true,
+ projectNameAndRootFormat: 'as-provided',
+ typescriptConfiguration: true,
+ bundler: 'webpack',
+ })
+ ).rejects.toThrowError(`Invalid remote name provided: ${name}.`);
+ });
+ });
+});
diff --git a/packages/react/src/generators/remote/schema.d.ts b/packages/react/src/generators/remote/schema.d.ts
index c2cdf37d9d..6683b4c4f6 100644
--- a/packages/react/src/generators/remote/schema.d.ts
+++ b/packages/react/src/generators/remote/schema.d.ts
@@ -28,6 +28,7 @@ export interface Schema {
unitTestRunner: 'jest' | 'vitest' | 'none';
typescriptConfiguration?: boolean;
dynamic?: boolean;
+ bundler?: 'rspack' | 'webpack';
}
export interface NormalizedSchema extends ApplicationNormalizedSchema {
diff --git a/packages/react/src/generators/remote/schema.json b/packages/react/src/generators/remote/schema.json
index 65baf48768..254918bddd 100644
--- a/packages/react/src/generators/remote/schema.json
+++ b/packages/react/src/generators/remote/schema.json
@@ -182,6 +182,14 @@
"type": "boolean",
"default": false,
"x-priority": "internal"
+ },
+ "bundler": {
+ "description": "The bundler to use.",
+ "type": "string",
+ "enum": ["rspack", "webpack"],
+ "x-prompt": "Which bundler do you want to use to build the application?",
+ "default": "rspack",
+ "x-priority": "important"
}
},
"required": ["name"],
diff --git a/packages/react/src/generators/setup-ssr/schema.d.ts b/packages/react/src/generators/setup-ssr/schema.d.ts
index be0ffec270..8603287bf7 100644
--- a/packages/react/src/generators/setup-ssr/schema.d.ts
+++ b/packages/react/src/generators/setup-ssr/schema.d.ts
@@ -4,4 +4,5 @@ export interface Schema {
serverPort?: number;
skipFormat?: boolean;
extraInclude?: string[];
+ bundler?: 'rspack' | 'webpack';
}
diff --git a/packages/react/src/generators/setup-ssr/schema.json b/packages/react/src/generators/setup-ssr/schema.json
index 877fcba8cc..b0200bd5c6 100644
--- a/packages/react/src/generators/setup-ssr/schema.json
+++ b/packages/react/src/generators/setup-ssr/schema.json
@@ -41,6 +41,12 @@
"hidden": true,
"description": "Extra include entries in tsconfig.",
"default": []
+ },
+ "bundler": {
+ "description": "The bundler to use.",
+ "type": "string",
+ "enum": ["rspack", "webpack"],
+ "default": "webpack"
}
},
"required": ["project"],
diff --git a/packages/react/src/generators/setup-ssr/setup-ssr.ts b/packages/react/src/generators/setup-ssr/setup-ssr.ts
index 2a6d9f4c3b..7cff67a4b4 100644
--- a/packages/react/src/generators/setup-ssr/setup-ssr.ts
+++ b/packages/react/src/generators/setup-ssr/setup-ssr.ts
@@ -128,7 +128,10 @@ export async function setupSsrGenerator(tree: Tree, options: Schema) {
...projectConfig.targets,
server: {
dependsOn: ['build'],
- executor: '@nx/webpack:webpack',
+ executor:
+ options.bundler === 'rspack'
+ ? '@nx/rspack:rspack'
+ : '@nx/webpack:webpack',
outputs: ['{options.outputPath}'],
defaultConfiguration: 'production',
options: {
@@ -140,7 +143,14 @@ export async function setupSsrGenerator(tree: Tree, options: Schema) {
compiler: 'babel',
externalDependencies: 'all',
outputHashing: 'none',
- webpackConfig: joinPathFragments(projectRoot, 'webpack.config.js'),
+ ...(options.bundler === 'rspack'
+ ? { rspackConfig: joinPathFragments(projectRoot, 'rspack.config.js') }
+ : {
+ webpackConfig: joinPathFragments(
+ projectRoot,
+ 'webpack.config.js'
+ ),
+ }),
},
configurations: {
development: {
@@ -176,7 +186,10 @@ export async function setupSsrGenerator(tree: Tree, options: Schema) {
},
},
serve: {
- executor: '@nx/webpack:ssr-dev-server',
+ executor:
+ options.bundler === 'rspack'
+ ? '@nx/rspack:ssr-dev-server'
+ : '@nx/webpack:ssr-dev-server',
defaultConfiguration: 'development',
options: {
browserTarget: `${options.project}:build:development`,
diff --git a/packages/react/src/migrations/update-19-6-0/update-ssr-server-port.spec.ts b/packages/react/src/migrations/update-19-6-0/update-ssr-server-port.spec.ts
index c05b991bb4..b6cf62fb83 100644
--- a/packages/react/src/migrations/update-19-6-0/update-ssr-server-port.spec.ts
+++ b/packages/react/src/migrations/update-19-6-0/update-ssr-server-port.spec.ts
@@ -20,6 +20,7 @@ describe('update-19-6-0 update-ssr-server-port migration', () => {
projectNameAndRootFormat: 'as-provided',
style: 'css',
remotes: ['product'],
+ bundler: 'webpack',
});
const remotePort = readProjectConfiguration(tree, 'product').targets.serve
.options.port;
@@ -32,10 +33,7 @@ describe('update-19-6-0 update-ssr-server-port migration', () => {
'product/server.ts',
tree
.read('product/server.ts', 'utf-8')
- .replace(
- 'const port = 4201;',
- `const port = process.env['PORT'] || 4200;`
- )
+ .replace('const port = 4201;', `const port = process.env.PORT || 4200;`)
);
updateSsrServerPort(tree);
@@ -107,25 +105,25 @@ describe('update-19-6-0 update-ssr-server-port migration', () => {
`port = process.env.PORT || ${shellPort}`
);
expect(tree.read('shell/server.ts', 'utf-8')).toMatchInlineSnapshot(`
- "import * as path from 'path';
- import express from 'express';
- import cors from 'cors';
- import { handleRequest } from './src/main.server';
- const port = process.env.PORT || 4200;
- const app = express();
- const browserDist = path.join(process.cwd(), 'dist/shell/browser');
- const indexPath = path.join(browserDist, 'index.html');
- app.use(cors());
- app.get('*.*', express.static(browserDist, {
- maxAge: '1y',
- }));
- app.use('*', handleRequest(indexPath));
- const server = app.listen(port, () => {
- console.log(\`Express server listening on http://localhost:\${port}\`);
- });
- server.on('error', console.error);
- "
- `);
+ "import * as path from 'path';
+ import express from 'express';
+ import cors from 'cors';
+ import { handleRequest } from './src/main.server';
+ const port = process.env.PORT || 4200;
+ const app = express();
+ const browserDist = path.join(process.cwd(), 'dist/shell/browser');
+ const indexPath = path.join(browserDist, 'index.html');
+ app.use(cors());
+ app.get('*.*', express.static(browserDist, {
+ maxAge: '1y',
+ }));
+ app.use('*', handleRequest(indexPath));
+ const server = app.listen(port, () => {
+ console.log(\`Express server listening on http://localhost:\${port}\`);
+ });
+ server.on('error', console.error);
+ "
+ `);
});
it('should update a host project server file', async () => {
@@ -137,6 +135,7 @@ describe('update-19-6-0 update-ssr-server-port migration', () => {
linter: Linter.EsLint,
projectNameAndRootFormat: 'as-provided',
style: 'css',
+ bundler: 'webpack',
});
const hostPort = readProjectConfiguration(tree, 'host').targets.serve
@@ -158,25 +157,25 @@ describe('update-19-6-0 update-ssr-server-port migration', () => {
`port = process.env.PORT || ${hostPort}`
);
expect(tree.read('host/server.ts', 'utf-8')).toMatchInlineSnapshot(`
- "import * as path from 'path';
- import express from 'express';
- import cors from 'cors';
- import { handleRequest } from './src/main.server';
- const port = process.env.PORT || 4200;
- const app = express();
- const browserDist = path.join(process.cwd(), 'dist/host/browser');
- const indexPath = path.join(browserDist, 'index.html');
- app.use(cors());
- app.get('*.*', express.static(browserDist, {
- maxAge: '1y',
- }));
- app.use('*', handleRequest(indexPath));
- const server = app.listen(port, () => {
- console.log(\`Express server listening on http://localhost:\${port}\`);
- });
- server.on('error', console.error);
- "
- `);
+ "import * as path from 'path';
+ import express from 'express';
+ import cors from 'cors';
+ import { handleRequest } from './src/main.server';
+ const port = process.env.PORT || 4200;
+ const app = express();
+ const browserDist = path.join(process.cwd(), 'dist/host/browser');
+ const indexPath = path.join(browserDist, 'index.html');
+ app.use(cors());
+ app.get('*.*', express.static(browserDist, {
+ maxAge: '1y',
+ }));
+ app.use('*', handleRequest(indexPath));
+ const server = app.listen(port, () => {
+ console.log(\`Express server listening on http://localhost:\${port}\`);
+ });
+ server.on('error', console.error);
+ "
+ `);
});
it('should not update a mfe project that is not ssr', async () => {
@@ -188,6 +187,7 @@ describe('update-19-6-0 update-ssr-server-port migration', () => {
linter: Linter.EsLint,
projectNameAndRootFormat: 'as-provided',
style: 'css',
+ bundler: 'webpack',
});
tree.write('shell-not-ssr/server.ts', 'const port = 9999;');
diff --git a/packages/react/src/rules/update-module-federation-project.ts b/packages/react/src/rules/update-module-federation-project.ts
index a77662e324..95b0df9ea8 100644
--- a/packages/react/src/rules/update-module-federation-project.ts
+++ b/packages/react/src/rules/update-module-federation-project.ts
@@ -18,42 +18,86 @@ export function updateModuleFederationProject(
devServerPort?: number;
typescriptConfiguration?: boolean;
dynamic?: boolean;
+ bundler?: 'rspack' | 'webpack';
}
): GeneratorCallback {
const projectConfig = readProjectConfiguration(host, options.projectName);
- projectConfig.targets.build.options = {
- ...projectConfig.targets.build.options,
- main: maybeJs(options, `${options.appProjectRoot}/src/main.ts`),
- webpackConfig: `${options.appProjectRoot}/webpack.config.${
- options.typescriptConfiguration && !options.js ? 'ts' : 'js'
- }`,
- };
+ if (options.bundler === 'rspack') {
+ projectConfig.targets.build.executor = '@nx/rspack:rspack';
+ projectConfig.targets.build.options = {
+ ...projectConfig.targets.build.options,
+ main: maybeJs(
+ { js: options.js, useJsx: true },
+ `${options.appProjectRoot}/src/main.ts`
+ ),
+ rspackConfig: `${options.appProjectRoot}/rspack.config.${
+ options.typescriptConfiguration && !options.js ? 'ts' : 'js'
+ }`,
+ target: 'web',
+ };
- projectConfig.targets.build.configurations.production = {
- ...projectConfig.targets.build.configurations.production,
- webpackConfig: `${options.appProjectRoot}/webpack.config.prod.${
- options.typescriptConfiguration && !options.js ? 'ts' : 'js'
- }`,
- };
+ projectConfig.targets.build.configurations.production = {
+ ...projectConfig.targets.build.configurations.production,
+ rspackConfig: `${options.appProjectRoot}/rspack.config.prod.${
+ options.typescriptConfiguration && !options.js ? 'ts' : 'js'
+ }`,
+ };
+ } else {
+ projectConfig.targets.build.options = {
+ ...projectConfig.targets.build.options,
+ main: maybeJs(options, `${options.appProjectRoot}/src/main.ts`),
+ webpackConfig: `${options.appProjectRoot}/webpack.config.${
+ options.typescriptConfiguration && !options.js ? 'ts' : 'js'
+ }`,
+ };
+
+ projectConfig.targets.build.configurations.production = {
+ ...projectConfig.targets.build.configurations.production,
+ webpackConfig: `${options.appProjectRoot}/webpack.config.prod.${
+ options.typescriptConfiguration && !options.js ? 'ts' : 'js'
+ }`,
+ };
+ }
// If host should be configured to use dynamic federation
if (options.dynamic) {
- const pathToProdWebpackConfig = joinPathFragments(
- projectConfig.root,
- `webpack.prod.config.${
- options.typescriptConfiguration && !options.js ? 'ts' : 'js'
- }`
- );
- if (host.exists(pathToProdWebpackConfig)) {
- host.delete(pathToProdWebpackConfig);
- }
+ if (options.bundler === 'rspack') {
+ const pathToProdRspackConfig = joinPathFragments(
+ projectConfig.root,
+ `rspack.prod.config.${
+ options.typescriptConfiguration && !options.js ? 'ts' : 'js'
+ }`
+ );
+ if (host.exists(pathToProdRspackConfig)) {
+ host.delete(pathToProdRspackConfig);
+ }
- delete projectConfig.targets.build.configurations.production?.webpackConfig;
+ delete projectConfig.targets.build.configurations.production
+ ?.rspackConfig;
+ } else {
+ const pathToProdWebpackConfig = joinPathFragments(
+ projectConfig.root,
+ `webpack.prod.config.${
+ options.typescriptConfiguration && !options.js ? 'ts' : 'js'
+ }`
+ );
+ if (host.exists(pathToProdWebpackConfig)) {
+ host.delete(pathToProdWebpackConfig);
+ }
+
+ delete projectConfig.targets.build.configurations.production
+ ?.webpackConfig;
+ }
}
- projectConfig.targets.serve.executor =
- '@nx/react:module-federation-dev-server';
+ if (options.bundler === 'rspack') {
+ projectConfig.targets.serve.executor =
+ '@nx/rspack:module-federation-dev-server';
+ } else {
+ projectConfig.targets.serve.executor =
+ '@nx/react:module-federation-dev-server';
+ }
projectConfig.targets.serve.options.port = options.devServerPort;
// `serve-static` for remotes that don't need to be in development mode
diff --git a/packages/react/src/utils/maybe-js.ts b/packages/react/src/utils/maybe-js.ts
index 17daba9548..c294899221 100644
--- a/packages/react/src/utils/maybe-js.ts
+++ b/packages/react/src/utils/maybe-js.ts
@@ -1,5 +1,8 @@
-export function maybeJs(options: { js?: boolean }, path: string): string {
+export function maybeJs(
+ options: { js?: boolean; useJsx?: boolean },
+ path: string
+): string {
return options.js && (path.endsWith('.ts') || path.endsWith('.tsx'))
- ? path.replace(/\.tsx?$/, '.js')
+ ? path.replace(/\.tsx?$/, options.useJsx ? '.jsx' : '.js')
: path;
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 1eef225bf4..7e2ebf1ab1 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -24955,7 +24955,7 @@ snapshots:
'@npmcli/fs@3.1.0':
dependencies:
- semver: 7.6.2
+ semver: 7.6.3
'@npmcli/git@4.1.0':
dependencies:
@@ -24965,7 +24965,7 @@ snapshots:
proc-log: 3.0.0
promise-inflight: 1.0.1
promise-retry: 2.0.1
- semver: 7.6.2
+ semver: 7.6.3
which: 3.0.1
transitivePeerDependencies:
- bluebird
@@ -24978,7 +24978,7 @@ snapshots:
proc-log: 3.0.0
promise-inflight: 1.0.1
promise-retry: 2.0.1
- semver: 7.6.2
+ semver: 7.6.3
which: 4.0.0
transitivePeerDependencies:
- bluebird
@@ -25424,7 +25424,7 @@ snapshots:
pkg-types: 1.0.3
rc9: 2.1.1
scule: 1.2.0
- semver: 7.6.2
+ semver: 7.6.3
simple-git: 3.22.0
sirv: 2.0.4
unimport: 3.7.1(rollup@4.14.3)
@@ -28587,7 +28587,7 @@ snapshots:
globby: 11.1.0
is-glob: 4.0.3
minimatch: 9.0.3
- semver: 7.6.2
+ semver: 7.6.3
ts-api-utils: 1.3.0(typescript@5.5.3)
optionalDependencies:
typescript: 5.5.3
@@ -30121,7 +30121,7 @@ snapshots:
bin-version-check@5.0.0:
dependencies:
bin-version: 6.0.0
- semver: 7.6.2
+ semver: 7.6.3
semver-truncate: 2.0.0
bin-version@6.0.0:
@@ -33087,7 +33087,7 @@ snapshots:
minimatch: 3.1.2
node-abort-controller: 3.1.1
schema-utils: 3.2.0
- semver: 7.6.2
+ semver: 7.6.3
tapable: 2.2.1
typescript: 5.5.3
webpack: 5.88.0(@swc/core@1.5.7(@swc/helpers@0.5.11))(esbuild@0.19.5)(webpack-cli@5.1.4)
@@ -34897,7 +34897,7 @@ snapshots:
jest-util: 29.7.0
natural-compare: 1.4.0
pretty-format: 29.7.0
- semver: 7.6.2
+ semver: 7.6.3
transitivePeerDependencies:
- supports-color
@@ -35149,7 +35149,7 @@ snapshots:
lodash.isstring: 4.0.1
lodash.once: 4.1.1
ms: 2.1.3
- semver: 7.6.2
+ semver: 7.6.3
jsprim@2.0.2:
dependencies:
@@ -36695,7 +36695,7 @@ snapshots:
rollup: 4.18.0
rollup-plugin-visualizer: 5.12.0(rollup@4.18.0)
scule: 1.2.0
- semver: 7.6.2
+ semver: 7.6.3
serve-placeholder: 2.0.1
serve-static: 1.15.0
std-env: 3.7.0
@@ -36848,14 +36848,14 @@ snapshots:
dependencies:
hosted-git-info: 4.1.0
is-core-module: 2.13.1
- semver: 7.6.2
+ semver: 7.6.3
validate-npm-package-license: 3.0.4
normalize-package-data@5.0.0:
dependencies:
hosted-git-info: 6.1.1
is-core-module: 2.13.1
- semver: 7.6.2
+ semver: 7.6.3
validate-npm-package-license: 3.0.4
normalize-package-data@6.0.0:
@@ -36877,7 +36877,7 @@ snapshots:
npm-install-checks@6.0.0:
dependencies:
- semver: 7.6.2
+ semver: 7.6.3
npm-normalize-package-bin@1.0.1: {}