import { stripIndents } from '@nx/devkit'; import { checkFilesExist, cleanupProject, killProcessAndPorts, newProject, readJson, runCLI, runCLIAsync, runCommandUntil, runE2ETests, uniq, updateFile, updateJson, } from '@nx/e2e/utils'; import { join } from 'path'; describe('React Module Federation', () => { let proj: string; beforeAll(() => (proj = newProject())); afterAll(() => cleanupProject()); it('should generate host and remote apps', async () => { const shell = uniq('shell'); const remote1 = uniq('remote1'); const remote2 = uniq('remote2'); const remote3 = uniq('remote3'); runCLI(`generate @nx/react:host ${shell} --style=css --no-interactive`); runCLI( `generate @nx/react:remote ${remote1} --style=css --host=${shell} --no-interactive` ); runCLI( `generate @nx/react:remote ${remote2} --style=css --host=${shell} --no-interactive` ); runCLI( `generate @nx/react:remote ${remote3} --style=css --host=${shell} --no-interactive` ); checkFilesExist(`apps/${shell}/module-federation.config.ts`); checkFilesExist(`apps/${remote1}/module-federation.config.ts`); checkFilesExist(`apps/${remote2}/module-federation.config.ts`); checkFilesExist(`apps/${remote3}/module-federation.config.ts`); await expect(runCLIAsync(`test ${shell}`)).resolves.toMatchObject({ combinedOutput: expect.stringContaining('Test Suites: 1 passed, 1 total'), }); expect(readPort(shell)).toEqual(4200); expect(readPort(remote1)).toEqual(4201); expect(readPort(remote2)).toEqual(4202); expect(readPort(remote3)).toEqual(4203); updateFile( `apps/${shell}/webpack.config.ts`, stripIndents` import { composePlugins, withNx, ModuleFederationConfig } from '@nx/webpack'; import { withReact } from '@nx/react'; import { withModuleFederation } from '@nx/react/module-federation'); const baseConfig = require('./module-federation.config'); const config: ModuleFederationConfig = { ...baseConfig, remotes: [ '${remote1}', ['${remote2}', 'http://localhost:${readPort( remote2 )}/remoteEntry.js'], ['${remote3}', 'http://localhost:${readPort(remote3)}'], ], }; // Nx plugins for webpack to build config object from Nx options and context. module.exports = composePlugins(withNx(), withReact(), withModuleFederation(config)); ` ); updateFile( `apps/${shell}-e2e/src/integration/app.spec.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}'); }); }); ` ); // TODO(caleb): cypress isn't able to find the element and then throws error with an address already in use error. // https://staging.nx.app/runs/ASAokpXhnE/task/e2e-react:e2e // if (runCypressTests()) { // const e2eResults = runCLI(`e2e ${shell}-e2e --no-watch --verbose`); // expect(e2eResults).toContain('All specs passed!'); // expect( // await killPorts([ // readPort(shell), // readPort(remote1), // readPort(remote2), // readPort(remote3), // ]) // ).toBeTruthy(); // } }, 500_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` ); runCLI( `generate @nx/react:remote ${remote} --host=${shell} --project-name-and-root-format=as-provided --no-interactive` ); // 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 buildOutput = runCLI(`run ${shell}:build:development`); expect(buildOutput).toContain('Successfully ran target build'); }, 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} --no-interactive --projectNameAndRootFormat=as-provided` ); runCLI( `generate @nx/js:lib ${lib} --importPath=@acme/${lib} --publishable=true --no-interactive --projectNameAndRootFormat=as-provided` ); 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 = runCLI(`e2e ${remote}-e2e --no-watch --verbose`); expect(remoteE2eResults).toContain('All specs passed!'); // test shell e2e // serve remote first const remotePort = 4201; const remoteProcess = await runCommandUntil( `serve ${remote} --no-watch --verbose`, (output) => { return output.includes( `Web Development Server is listening at http://localhost:${remotePort}/` ); } ); const shellE2eResults = runCLI(`e2e ${shell}-e2e --no-watch --verbose`); expect(shellE2eResults).toContain('All specs passed!'); await killProcessAndPorts(remoteProcess.pid, remotePort); } }, 500_000); function readPort(appName: string): number { const config = readJson(join('apps', appName, 'project.json')); return config.targets.serve.options.port; } });