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 } +
+ + + } /> + + } /> + +
+ ); + } + + 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 ( + + + + <% 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: {}