From 48db8ddc04d4fcebcf508470ee805637adcebd72 Mon Sep 17 00:00:00 2001 From: Colum Ferry Date: Mon, 4 Nov 2024 13:23:13 +0000 Subject: [PATCH] chore(react): separate module federation tests into separate test files for better distribution (#28765) ## Current Behavior Module Federation tests take ~21-25mins ![image](https://github.com/user-attachments/assets/771f6314-2fba-4340-9617-d7fb0dd40c93) ## Expected Behavior Module Federation tests take ~11-13mins ![image](https://github.com/user-attachments/assets/9a720178-5f0c-4b60-a3af-53b3f0db1111) ## Related Issue(s) Fixes # --- .../src/module-federation/core.rspack.test.ts | 528 +++++++ .../module-federation/core.webpack.test.ts | 348 +++++ .../dynamic-federation.rspack.test.ts | 112 ++ .../dynamic-federation.webpack.test.ts | 112 ++ .../federate-module.rspack.test.ts | 211 +++ .../federate-module.webpack.test.ts | 211 +++ .../independent-deployability.rspack.test.ts | 437 ++++++ .../independent-deployability.webpack.test.ts | 438 ++++++ e2e/react/src/module-federation/utils.ts | 12 + .../react-module-federation.rspack.test.ts | 1266 ----------------- e2e/react/src/react-module-federation.test.ts | 1092 -------------- nx.json | 2 +- 12 files changed, 2410 insertions(+), 2359 deletions(-) create mode 100644 e2e/react/src/module-federation/core.rspack.test.ts create mode 100644 e2e/react/src/module-federation/core.webpack.test.ts create mode 100644 e2e/react/src/module-federation/dynamic-federation.rspack.test.ts create mode 100644 e2e/react/src/module-federation/dynamic-federation.webpack.test.ts create mode 100644 e2e/react/src/module-federation/federate-module.rspack.test.ts create mode 100644 e2e/react/src/module-federation/federate-module.webpack.test.ts create mode 100644 e2e/react/src/module-federation/independent-deployability.rspack.test.ts create mode 100644 e2e/react/src/module-federation/independent-deployability.webpack.test.ts create mode 100644 e2e/react/src/module-federation/utils.ts delete mode 100644 e2e/react/src/react-module-federation.rspack.test.ts delete mode 100644 e2e/react/src/react-module-federation.test.ts diff --git a/e2e/react/src/module-federation/core.rspack.test.ts b/e2e/react/src/module-federation/core.rspack.test.ts new file mode 100644 index 0000000000000..c74cc0459ca5f --- /dev/null +++ b/e2e/react/src/module-federation/core.rspack.test.ts @@ -0,0 +1,528 @@ +import { 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 { readPort } from './utils'; + +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 apps/${shell} --name=${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 + ); + it('should generate host and remote apps and use playwright for e2es', async () => { + const shell = uniq('shell'); + const remote1 = uniq('remote1'); + const remote2 = uniq('remote2'); + const remote3 = uniq('remote3'); + + runCLI( + `generate @nx/react:host apps/${shell} --name=${shell} --remotes=${remote1},${remote2},${remote3} --bundler=rspack --e2eTestRunner=playwright --style=css --no-interactive --skipFormat` + ); + + 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' + ), + }); + + updateFile( + `apps/${shell}-e2e/src/example.spec.ts`, + stripIndents` + import { test, expect } from '@playwright/test'; + test('should display welcome message', async ({page}) => { + await page.goto("/"); + expect(await page.locator('h1').innerText()).toContain('Welcome'); + }); + + test('should load remote 1', async ({page}) => { + await page.goto("/${remote1}"); + expect(await page.locator('h1').innerText()).toContain('${remote1}'); + }); + + test('should load remote 2', async ({page}) => { + await page.goto("/${remote2}"); + expect(await page.locator('h1').innerText()).toContain('${remote2}'); + }); + + test('should load remote 3', async ({page}) => { + await page.goto("/${remote3}"); + expect(await page.locator('h1').innerText()).toContain('${remote3}'); + }); + ` + ); + + if (runE2ETests()) { + const e2eResultsSwc = await runCommandUntil( + `e2e ${shell}-e2e`, + (output) => output.includes('Successfully ran target e2e for project') + ); + + await killProcessAndPorts(e2eResultsSwc.pid, readPort(shell)); + + const e2eResultsTsNode = await runCommandUntil( + `e2e ${shell}-e2e`, + (output) => + output.includes('Successfully ran target e2e for project'), + { + env: { NX_PREFER_TS_NODE: 'true' }, + } + ); + await killProcessAndPorts(e2eResultsTsNode.pid, readPort(shell)); + } + }, 500_000); + + it('should generate host and remote apps in webpack, convert to rspack and use playwright for e2es', async () => { + const shell = uniq('shell'); + const remote1 = uniq('remote1'); + + runCLI( + `generate @nx/react:host ${shell} --remotes=${remote1} --bundler=webpack --e2eTestRunner=playwright --style=css --no-interactive --skipFormat` + ); + + runCLI( + `generate @nx/rspack:convert-webpack ${shell} --skipFormat --no-interactive` + ); + runCLI( + `generate @nx/rspack:convert-webpack ${remote1} --skipFormat --no-interactive` + ); + + updateFile( + `apps/${shell}-e2e/src/example.spec.ts`, + stripIndents` + import { test, expect } from '@playwright/test'; + test('should display welcome message', async ({page}) => { + await page.goto("/"); + expect(await page.locator('h1').innerText()).toContain('Welcome'); + }); + + test('should load remote 1', async ({page}) => { + await page.goto("/${remote1}"); + expect(await page.locator('h1').innerText()).toContain('${remote1}'); + }); + ` + ); + + if (runE2ETests()) { + const e2eResultsSwc = await runCommandUntil( + `e2e ${shell}-e2e`, + (output) => output.includes('Successfully ran target e2e for project') + ); + + await killProcessAndPorts(e2eResultsSwc.pid, readPort(shell)); + + const e2eResultsTsNode = await runCommandUntil( + `e2e ${shell}-e2e`, + (output) => + output.includes('Successfully ran target e2e for project'), + { + env: { NX_PREFER_TS_NODE: 'true' }, + } + ); + await killProcessAndPorts(e2eResultsTsNode.pid, readPort(shell)); + } + }, 500_000); + + it('should have interop between webpack host and rspack remote', async () => { + const shell = uniq('shell'); + const remote1 = uniq('remote1'); + const remote2 = uniq('remote2'); + + runCLI( + `generate @nx/react:host apps/${shell} --name=${shell} --remotes=${remote1} --bundler=webpack --e2eTestRunner=cypress --style=css --no-interactive --skipFormat` + ); + + runCLI( + `generate @nx/react:remote apps/${remote2} --name=${remote2} --host=${shell} --bundler=rspack --style=css --no-interactive --skipFormat` + ); + + 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}'); + }); + }); + ` + ); + + [shell, remote1, remote2].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); + + it('should have interop between rspack host and webpack remote', async () => { + const shell = uniq('shell'); + const remote1 = uniq('remote1'); + const remote2 = uniq('remote2'); + runCLI( + `generate @nx/react:host apps/${shell} --name=${shell} --remotes=${remote1} --bundler=rspack --e2eTestRunner=cypress --style=css --no-interactive --skipFormat` + ); + + runCLI( + `generate @nx/react:remote apps/${remote2} --name=${remote2} --host=${shell} --bundler=webpack --style=css --no-interactive --skipFormat` + ); + + 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}'); + }); + + }); + ` + ); + + 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 apps/${shell} --ssr --name=${shell} --remotes=${remote1},${remote2},${remote3} --bundler=rspack --style=css --no-interactive --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 apps/${shell} --ssr --name=${shell} --remotes=${remote1},${remote2},${remote3} --bundler=rspack --style=css --e2eTestRunner=cypress --no-interactive --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 apps/${shell} --ssr --name=${shell} --remotes=${remote1},${remote2},${remote3} --bundler=rspack --style=css --e2eTestRunner=cypress --no-interactive --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); + }); + + // TODO(Coly010): investigate this failure + xit('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} --no-interactive --skipFormat`); + runCLI( + `generate @nx/react:remote ${remote} --host=${shell} --bundler=rspack --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); + }); +}); diff --git a/e2e/react/src/module-federation/core.webpack.test.ts b/e2e/react/src/module-federation/core.webpack.test.ts new file mode 100644 index 0000000000000..e61ebb0ff9d2b --- /dev/null +++ b/e2e/react/src/module-federation/core.webpack.test.ts @@ -0,0 +1,348 @@ +import { stripIndents } from '@nx/devkit'; +import { + checkFilesExist, + cleanupProject, + killPorts, + killProcessAndPorts, + newProject, + runCLI, + runCLIAsync, + runCommandUntil, + runE2ETests, + uniq, + updateFile, +} from '@nx/e2e/utils'; +import { readPort } from './utils'; + +describe('React Module Federation', () => { + describe('Default Configuration', () => { + beforeAll(() => { + newProject({ packages: ['@nx/react', '@nx/webpack'] }); + }); + + 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=webpack --e2eTestRunner=cypress --style=css --no-interactive --skipFormat --js=${js}` + ); + + checkFilesExist( + `${shell}/module-federation.config.${js ? 'js' : 'ts'}` + ); + checkFilesExist( + `${remote1}/module-federation.config.${js ? 'js' : 'ts'}` + ); + checkFilesExist( + `${remote2}/module-federation.config.${js ? 'js' : 'ts'}` + ); + checkFilesExist( + `${remote3}/module-federation.config.${js ? 'js' : 'ts'}` + ); + + await expect(runCLIAsync(`test ${shell}`)).resolves.toMatchObject({ + combinedOutput: expect.stringContaining( + 'Test Suites: 1 passed, 1 total' + ), + }); + + updateFile( + `${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 + ); + + it('should generate host and remote apps and use playwright for e2es', async () => { + const shell = uniq('shell'); + const remote1 = uniq('remote1'); + const remote2 = uniq('remote2'); + const remote3 = uniq('remote3'); + + runCLI( + `generate @nx/react:host apps/${shell} --name=${shell} --remotes=${remote1},${remote2},${remote3} --bundler=webpack --e2eTestRunner=playwright --style=css --no-interactive --skipFormat` + ); + + 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' + ), + }); + + updateFile( + `apps/${shell}-e2e/src/example.spec.ts`, + stripIndents` + import { test, expect } from '@playwright/test'; + test('should display welcome message', async ({page}) => { + await page.goto("/"); + expect(await page.locator('h1').innerText()).toContain('Welcome'); + }); + + test('should load remote 1', async ({page}) => { + await page.goto("/${remote1}"); + expect(await page.locator('h1').innerText()).toContain('${remote1}'); + }); + + test('should load remote 2', async ({page}) => { + await page.goto("/${remote2}"); + expect(await page.locator('h1').innerText()).toContain('${remote2}'); + }); + + test('should load remote 3', async ({page}) => { + await page.goto("/${remote3}"); + expect(await page.locator('h1').innerText()).toContain('${remote3}'); + }); + ` + ); + + if (runE2ETests()) { + const e2eResultsSwc = await runCommandUntil( + `e2e ${shell}-e2e`, + (output) => output.includes('Successfully ran target e2e for project') + ); + + await killProcessAndPorts(e2eResultsSwc.pid, readPort(shell)); + + const e2eResultsTsNode = await runCommandUntil( + `e2e ${shell}-e2e`, + (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} --bundler=webpack --ssr --remotes=${remote1},${remote2},${remote3} --style=css --no-interactive --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( + `${app}/module-federation.config.ts`, + `${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 --bundler=webpack --remotes=${remote1},${remote2},${remote3} --style=css --e2eTestRunner=cypress --no-interactive --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} --bundler=webpack --ssr --remotes=${remote1},${remote2},${remote3} --style=css --e2eTestRunner=cypress --no-interactive --skipFormat` + ); + + const capitalize = (s: string) => + s.charAt(0).toUpperCase() + s.slice(1); + + updateFile(`${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} --bundler=webpack --no-interactive --skipFormat` + ); + runCLI( + `generate @nx/react:remote ${remote} --bundler=webpack --host=${shell} --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); + }); +}); diff --git a/e2e/react/src/module-federation/dynamic-federation.rspack.test.ts b/e2e/react/src/module-federation/dynamic-federation.rspack.test.ts new file mode 100644 index 0000000000000..e7384e4abdd2a --- /dev/null +++ b/e2e/react/src/module-federation/dynamic-federation.rspack.test.ts @@ -0,0 +1,112 @@ +import { + cleanupProject, + fileExists, + killProcessAndPorts, + newProject, + readJson, + runCLI, + runCommandUntil, + runE2ETests, + tmpProjPath, + uniq, + updateFile, + updateJson, +} from '@nx/e2e/utils'; +import { readPort } from './utils'; + +describe('Dynamic Module Federation', () => { + beforeAll(() => { + newProject({ packages: ['@nx/react'] }); + }); + + afterAll(() => cleanupProject()); + it('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 --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}/mf-manifest.json`, + }; + } + ); + + const manifest = readJson( + `${shell}/src/assets/module-federation.manifest.json` + ); + expect(manifest[remote]).toBeDefined(); + expect(manifest[remote]).toEqual('http://localhost:4205/mf-manifest.json'); + + // 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); +}); diff --git a/e2e/react/src/module-federation/dynamic-federation.webpack.test.ts b/e2e/react/src/module-federation/dynamic-federation.webpack.test.ts new file mode 100644 index 0000000000000..b7e3a1cfa7fe6 --- /dev/null +++ b/e2e/react/src/module-federation/dynamic-federation.webpack.test.ts @@ -0,0 +1,112 @@ +import { + cleanupProject, + fileExists, + killProcessAndPorts, + newProject, + readJson, + runCLI, + runCommandUntil, + runE2ETests, + tmpProjPath, + uniq, + updateFile, + updateJson, +} from '@nx/e2e/utils'; +import { readPort } from './utils'; + +describe('Dynamic Module Federation', () => { + beforeAll(() => { + newProject({ packages: ['@nx/react'] }); + }); + + afterAll(() => cleanupProject()); + it('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=webpack --e2eTestRunner=cypress --dynamic=true --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}/mf-manifest.json`, + }; + } + ); + + const manifest = readJson( + `${shell}/src/assets/module-federation.manifest.json` + ); + expect(manifest[remote]).toBeDefined(); + expect(manifest[remote]).toEqual('http://localhost:4205/mf-manifest.json'); + + // 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); +}); diff --git a/e2e/react/src/module-federation/federate-module.rspack.test.ts b/e2e/react/src/module-federation/federate-module.rspack.test.ts new file mode 100644 index 0000000000000..32acdbb365923 --- /dev/null +++ b/e2e/react/src/module-federation/federate-module.rspack.test.ts @@ -0,0 +1,211 @@ +import { + cleanupProject, + killProcessAndPorts, + newProject, + runCLI, + runCommandUntil, + runE2ETests, + uniq, + updateFile, +} from '@nx/e2e/utils'; +import { readPort } from './utils'; + +describe('Federate Module', () => { + let proj: string; + + beforeAll(() => { + 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 --skipFormat` + ); + + runCLI(`generate @nx/js:lib ${lib} --no-interactive --skipFormat`); + + // Federate Module + runCLI( + `generate @nx/react:federate-module ${lib}/src/index.ts --name=${module} --remote=${remote} --remoteDirectory=${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 --skipFormat` + ); + + runCLI(`generate @nx/js:lib ${lib} --no-interactive --skipFormat`); + + // Federate Module + runCLI( + `generate @nx/react:federate-module ${lib}/src/index.ts --name=${module} --remote=${childRemote} --remoteDirectory=${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); +}); diff --git a/e2e/react/src/module-federation/federate-module.webpack.test.ts b/e2e/react/src/module-federation/federate-module.webpack.test.ts new file mode 100644 index 0000000000000..91f26656f0446 --- /dev/null +++ b/e2e/react/src/module-federation/federate-module.webpack.test.ts @@ -0,0 +1,211 @@ +import { + cleanupProject, + killProcessAndPorts, + newProject, + runCLI, + runCommandUntil, + runE2ETests, + uniq, + updateFile, +} from '@nx/e2e/utils'; +import { readPort } from './utils'; + +describe('Federate Module', () => { + let proj: string; + + beforeAll(() => { + 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} --bundler=webpack --remotes=${remote} --e2eTestRunner=cypress --no-interactive --skipFormat` + ); + + runCLI(`generate @nx/js:lib ${lib} --no-interactive --skipFormat`); + + // Federate Module + runCLI( + `generate @nx/react:federate-module ${lib}/src/index.ts --bundler=webpack --name=${module} --remote=${remote} --remoteDirectory=${remote} --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=webpack --e2eTestRunner=cypress --no-interactive --skipFormat` + ); + + runCLI(`generate @nx/js:lib ${lib} --no-interactive --skipFormat`); + + // Federate Module + runCLI( + `generate @nx/react:federate-module ${lib}/src/index.ts --bundler=webpack --name=${module} --remote=${childRemote} --remoteDirectory=${childRemote} --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); +}); diff --git a/e2e/react/src/module-federation/independent-deployability.rspack.test.ts b/e2e/react/src/module-federation/independent-deployability.rspack.test.ts new file mode 100644 index 0000000000000..119bf504705f2 --- /dev/null +++ b/e2e/react/src/module-federation/independent-deployability.rspack.test.ts @@ -0,0 +1,437 @@ +import { + cleanupProject, + killProcessAndPorts, + newProject, + runCLI, + runCommandUntil, + runE2ETests, + uniq, + updateFile, + updateJson, +} from '@nx/e2e/utils'; +import { stripIndents } from 'nx/src/utils/strip-indents'; +import { readPort } from './utils'; + +describe('Independent Deployability', () => { + let proj: string; + + beforeAll(() => { + process.env.NX_ADD_PLUGINS = 'false'; + 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 --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 --skipFormat` + ); + + runCLI( + `generate @nx/js:lib ${lib} --importPath=@acme/${lib} --publishable=true --no-interactive --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 --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); +}); diff --git a/e2e/react/src/module-federation/independent-deployability.webpack.test.ts b/e2e/react/src/module-federation/independent-deployability.webpack.test.ts new file mode 100644 index 0000000000000..61c31ecbef534 --- /dev/null +++ b/e2e/react/src/module-federation/independent-deployability.webpack.test.ts @@ -0,0 +1,438 @@ +import { + cleanupProject, + killProcessAndPorts, + newProject, + runCLI, + runCommandUntil, + runE2ETests, + uniq, + updateFile, + updateJson, +} from '@nx/e2e/utils'; +import { stripIndents } from 'nx/src/utils/strip-indents'; +import { readPort } from './utils'; + +describe('Independent Deployability', () => { + let proj: string; + beforeAll(() => { + process.env.NX_ADD_PLUGINS = 'false'; + 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=webpack --e2eTestRunner=cypress --no-interactive --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=webpack --e2eTestRunner=cypress --no-interactive --skipFormat` + ); + + runCLI( + `generate @nx/js:lib ${lib} --importPath=@acme/${lib} --publishable=true --no-interactive --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( + `Web Development Server is listening at 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=webpack --e2eTestRunner=cypress --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); +}); diff --git a/e2e/react/src/module-federation/utils.ts b/e2e/react/src/module-federation/utils.ts new file mode 100644 index 0000000000000..a45fc7ec33918 --- /dev/null +++ b/e2e/react/src/module-federation/utils.ts @@ -0,0 +1,12 @@ +import { readJson } from '@nx/e2e/utils'; +import { join } from 'path'; + +export 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.rspack.test.ts b/e2e/react/src/react-module-federation.rspack.test.ts deleted file mode 100644 index d424db1735a73..0000000000000 --- a/e2e/react/src/react-module-federation.rspack.test.ts +++ /dev/null @@ -1,1266 +0,0 @@ -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 apps/${shell} --name=${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 - ); - it('should generate host and remote apps and use playwright for e2es', async () => { - const shell = uniq('shell'); - const remote1 = uniq('remote1'); - const remote2 = uniq('remote2'); - const remote3 = uniq('remote3'); - - runCLI( - `generate @nx/react:host apps/${shell} --name=${shell} --remotes=${remote1},${remote2},${remote3} --bundler=rspack --e2eTestRunner=playwright --style=css --no-interactive --skipFormat` - ); - - 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' - ), - }); - - updateFile( - `apps/${shell}-e2e/src/example.spec.ts`, - stripIndents` - import { test, expect } from '@playwright/test'; - test('should display welcome message', async ({page}) => { - await page.goto("/"); - expect(await page.locator('h1').innerText()).toContain('Welcome'); - }); - - test('should load remote 1', async ({page}) => { - await page.goto("/${remote1}"); - expect(await page.locator('h1').innerText()).toContain('${remote1}'); - }); - - test('should load remote 2', async ({page}) => { - await page.goto("/${remote2}"); - expect(await page.locator('h1').innerText()).toContain('${remote2}'); - }); - - test('should load remote 3', async ({page}) => { - await page.goto("/${remote3}"); - expect(await page.locator('h1').innerText()).toContain('${remote3}'); - }); - ` - ); - - if (runE2ETests()) { - const e2eResultsSwc = await runCommandUntil( - `e2e ${shell}-e2e`, - (output) => output.includes('Successfully ran target e2e for project') - ); - - await killProcessAndPorts(e2eResultsSwc.pid, readPort(shell)); - - const e2eResultsTsNode = await runCommandUntil( - `e2e ${shell}-e2e`, - (output) => - output.includes('Successfully ran target e2e for project'), - { - env: { NX_PREFER_TS_NODE: 'true' }, - } - ); - await killProcessAndPorts(e2eResultsTsNode.pid, readPort(shell)); - } - }, 500_000); - - it('should generate host and remote apps in webpack, convert to rspack and use playwright for e2es', async () => { - const shell = uniq('shell'); - const remote1 = uniq('remote1'); - - runCLI( - `generate @nx/react:host ${shell} --remotes=${remote1} --bundler=webpack --e2eTestRunner=playwright --style=css --no-interactive --skipFormat` - ); - - runCLI( - `generate @nx/rspack:convert-webpack ${shell} --skipFormat --no-interactive` - ); - runCLI( - `generate @nx/rspack:convert-webpack ${remote1} --skipFormat --no-interactive` - ); - - updateFile( - `apps/${shell}-e2e/src/example.spec.ts`, - stripIndents` - import { test, expect } from '@playwright/test'; - test('should display welcome message', async ({page}) => { - await page.goto("/"); - expect(await page.locator('h1').innerText()).toContain('Welcome'); - }); - - test('should load remote 1', async ({page}) => { - await page.goto("/${remote1}"); - expect(await page.locator('h1').innerText()).toContain('${remote1}'); - }); - ` - ); - - if (runE2ETests()) { - const e2eResultsSwc = await runCommandUntil( - `e2e ${shell}-e2e`, - (output) => output.includes('Successfully ran target e2e for project') - ); - - await killProcessAndPorts(e2eResultsSwc.pid, readPort(shell)); - - const e2eResultsTsNode = await runCommandUntil( - `e2e ${shell}-e2e`, - (output) => - output.includes('Successfully ran target e2e for project'), - { - env: { NX_PREFER_TS_NODE: 'true' }, - } - ); - await killProcessAndPorts(e2eResultsTsNode.pid, readPort(shell)); - } - }, 500_000); - - it('should have interop between webpack host and rspack remote', async () => { - const shell = uniq('shell'); - const remote1 = uniq('remote1'); - const remote2 = uniq('remote2'); - - runCLI( - `generate @nx/react:host apps/${shell} --name=${shell} --remotes=${remote1} --bundler=webpack --e2eTestRunner=cypress --style=css --no-interactive --skipFormat` - ); - - runCLI( - `generate @nx/react:remote apps/${remote2} --name=${remote2} --host=${shell} --bundler=rspack --style=css --no-interactive --skipFormat` - ); - - 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}'); - }); - }); - ` - ); - - [shell, remote1, remote2].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); - - it('should have interop between rspack host and webpack remote', async () => { - const shell = uniq('shell'); - const remote1 = uniq('remote1'); - const remote2 = uniq('remote2'); - runCLI( - `generate @nx/react:host apps/${shell} --name=${shell} --remotes=${remote1} --bundler=rspack --e2eTestRunner=cypress --style=css --no-interactive --skipFormat` - ); - - runCLI( - `generate @nx/react:remote apps/${remote2} --name=${remote2} --host=${shell} --bundler=webpack --style=css --no-interactive --skipFormat` - ); - - 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}'); - }); - - }); - ` - ); - - 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 apps/${shell} --ssr --name=${shell} --remotes=${remote1},${remote2},${remote3} --bundler=rspack --style=css --no-interactive --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 apps/${shell} --ssr --name=${shell} --remotes=${remote1},${remote2},${remote3} --bundler=rspack --style=css --e2eTestRunner=cypress --no-interactive --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 apps/${shell} --ssr --name=${shell} --remotes=${remote1},${remote2},${remote3} --bundler=rspack --style=css --e2eTestRunner=cypress --no-interactive --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); - }); - - // TODO(Coly010): investigate this failure - xit('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} --no-interactive --skipFormat`); - runCLI( - `generate @nx/react:remote ${remote} --host=${shell} --bundler=rspack --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 --skipFormat` - ); - - runCLI(`generate @nx/js:lib ${lib} --no-interactive --skipFormat`); - - // Federate Module - runCLI( - `generate @nx/react:federate-module ${lib}/src/index.ts --name=${module} --remote=${remote} --remoteDirectory=${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 --skipFormat` - ); - - runCLI(`generate @nx/js:lib ${lib} --no-interactive --skipFormat`); - - // Federate Module - runCLI( - `generate @nx/react:federate-module ${lib}/src/index.ts --name=${module} --remote=${childRemote} --remoteDirectory=${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 --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 --skipFormat` - ); - - runCLI( - `generate @nx/js:lib ${lib} --importPath=@acme/${lib} --publishable=true --no-interactive --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 --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('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 --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}/mf-manifest.json`, - }; - } - ); - - const manifest = readJson( - `${shell}/src/assets/module-federation.manifest.json` - ); - expect(manifest[remote]).toBeDefined(); - expect(manifest[remote]).toEqual( - 'http://localhost:4205/mf-manifest.json' - ); - - // 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 deleted file mode 100644 index 0558f9549b56a..0000000000000 --- a/e2e/react/src/react-module-federation.test.ts +++ /dev/null @@ -1,1092 +0,0 @@ -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 Module Federation', () => { - describe('Default Configuration', () => { - beforeAll(() => { - newProject({ packages: ['@nx/react', '@nx/webpack'] }); - }); - - 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=webpack --e2eTestRunner=cypress --style=css --no-interactive --skipFormat --js=${js}` - ); - - checkFilesExist( - `${shell}/module-federation.config.${js ? 'js' : 'ts'}` - ); - checkFilesExist( - `${remote1}/module-federation.config.${js ? 'js' : 'ts'}` - ); - checkFilesExist( - `${remote2}/module-federation.config.${js ? 'js' : 'ts'}` - ); - checkFilesExist( - `${remote3}/module-federation.config.${js ? 'js' : 'ts'}` - ); - - await expect(runCLIAsync(`test ${shell}`)).resolves.toMatchObject({ - combinedOutput: expect.stringContaining( - 'Test Suites: 1 passed, 1 total' - ), - }); - - updateFile( - `${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 - ); - - it('should generate host and remote apps and use playwright for e2es', async () => { - const shell = uniq('shell'); - const remote1 = uniq('remote1'); - const remote2 = uniq('remote2'); - const remote3 = uniq('remote3'); - - runCLI( - `generate @nx/react:host apps/${shell} --name=${shell} --remotes=${remote1},${remote2},${remote3} --bundler=webpack --e2eTestRunner=playwright --style=css --no-interactive --skipFormat` - ); - - 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' - ), - }); - - updateFile( - `apps/${shell}-e2e/src/example.spec.ts`, - stripIndents` - import { test, expect } from '@playwright/test'; - test('should display welcome message', async ({page}) => { - await page.goto("/"); - expect(await page.locator('h1').innerText()).toContain('Welcome'); - }); - - test('should load remote 1', async ({page}) => { - await page.goto("/${remote1}"); - expect(await page.locator('h1').innerText()).toContain('${remote1}'); - }); - - test('should load remote 2', async ({page}) => { - await page.goto("/${remote2}"); - expect(await page.locator('h1').innerText()).toContain('${remote2}'); - }); - - test('should load remote 3', async ({page}) => { - await page.goto("/${remote3}"); - expect(await page.locator('h1').innerText()).toContain('${remote3}'); - }); - ` - ); - - if (runE2ETests()) { - const e2eResultsSwc = await runCommandUntil( - `e2e ${shell}-e2e`, - (output) => output.includes('Successfully ran target e2e for project') - ); - - await killProcessAndPorts(e2eResultsSwc.pid, readPort(shell)); - - const e2eResultsTsNode = await runCommandUntil( - `e2e ${shell}-e2e`, - (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} --bundler=webpack --ssr --remotes=${remote1},${remote2},${remote3} --style=css --no-interactive --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( - `${app}/module-federation.config.ts`, - `${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 --bundler=webpack --remotes=${remote1},${remote2},${remote3} --style=css --e2eTestRunner=cypress --no-interactive --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} --bundler=webpack --ssr --remotes=${remote1},${remote2},${remote3} --style=css --e2eTestRunner=cypress --no-interactive --skipFormat` - ); - - const capitalize = (s: string) => - s.charAt(0).toUpperCase() + s.slice(1); - - updateFile(`${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} --bundler=webpack --no-interactive --skipFormat` - ); - runCLI( - `generate @nx/react:remote ${remote} --bundler=webpack --host=${shell} --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} --bundler=webpack --remotes=${remote} --e2eTestRunner=cypress --no-interactive --skipFormat` - ); - - runCLI(`generate @nx/js:lib ${lib} --no-interactive --skipFormat`); - - // Federate Module - runCLI( - `generate @nx/react:federate-module ${lib}/src/index.ts --bundler=webpack --name=${module} --remote=${remote} --remoteDirectory=${remote} --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=webpack --e2eTestRunner=cypress --no-interactive --skipFormat` - ); - - runCLI(`generate @nx/js:lib ${lib} --no-interactive --skipFormat`); - - // Federate Module - runCLI( - `generate @nx/react:federate-module ${lib}/src/index.ts --bundler=webpack --name=${module} --remote=${childRemote} --remoteDirectory=${childRemote} --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=webpack --e2eTestRunner=cypress --no-interactive --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=webpack --e2eTestRunner=cypress --no-interactive --skipFormat` - ); - - runCLI( - `generate @nx/js:lib ${lib} --importPath=@acme/${lib} --publishable=true --no-interactive --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( - `Web Development Server is listening at 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=webpack --e2eTestRunner=cypress --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('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=webpack --e2eTestRunner=cypress --dynamic=true --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}/mf-manifest.json`, - }; - } - ); - - const manifest = readJson( - `${shell}/src/assets/module-federation.manifest.json` - ); - expect(manifest[remote]).toBeDefined(); - expect(manifest[remote]).toEqual( - 'http://localhost:4205/mf-manifest.json' - ); - - // 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/nx.json b/nx.json index 5f803d2ae44d3..199a349e50b66 100644 --- a/nx.json +++ b/nx.json @@ -147,7 +147,7 @@ "e2e-macos-ci": { "inputs": ["e2eInputs", "^production"] }, - "e2e-ci--**/*": { + "e2e-ci--**/**": { "inputs": ["e2eInputs", "^production"], "parallelism": false, "dependsOn": ["@nx/nx-source:populate-local-registry-storage"]