feat(react): add crystal mf support to host and remote (#30424)

## Current Behavior
The `@nx/react` `host` and `remote` generators currently use executors
to support Module Federation


## Expected Behavior
When `bundler=rspack` use Crystal Module Federation with no executors
for Module Federation

## Related Issues
#30391
This commit is contained in:
Colum Ferry 2025-04-02 16:58:45 +01:00 committed by GitHub
parent 176e8f985a
commit 9669dfdb62
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
58 changed files with 1797 additions and 973 deletions

View File

@ -15,11 +15,11 @@ import { readPort, runCLI } from './utils';
describe('React Rspack Module Federation', () => {
describe('Default Configuration', () => {
beforeAll(() => {
beforeEach(() => {
newProject({ packages: ['@nx/react'] });
});
afterAll(() => cleanupProject());
afterEach(() => cleanupProject());
it.each`
js
@ -100,21 +100,11 @@ describe('React Rspack Module Federation', () => {
if (runE2ETests()) {
const e2eResultsSwc = await runCommandUntil(
`e2e ${shell}-e2e --no-watch --verbose`,
`e2e ${shell}-e2e --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
@ -173,291 +163,9 @@ describe('React Rspack Module Federation', () => {
);
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');

View File

@ -94,13 +94,13 @@ describe('Dynamic Module Federation', () => {
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`,
`serve ${remote} --verbose`,
() => {
return true;
}
);
const hostE2eResultsSwc = await runCommandUntil(
`e2e ${shell}-e2e --no-watch --verbose`,
`e2e ${shell}-e2e --verbose`,
(output) => output.includes('All specs passed!')
);

View File

@ -103,7 +103,7 @@ describe('Federate Module', () => {
if (runE2ETests()) {
const hostE2eResults = await runCommandUntil(
`e2e ${host}-e2e --no-watch --verbose`,
`e2e ${host}-e2e --verbose`,
(output) => output.includes('All specs passed!')
);
await killProcessAndPorts(
@ -195,7 +195,7 @@ describe('Federate Module', () => {
if (runE2ETests()) {
const hostE2eResults = await runCommandUntil(
`e2e ${host}-e2e --no-watch --verbose`,
`e2e ${host}-e2e --verbose`,
(output) => output.includes('All specs passed!')
);
await killProcessAndPorts(

View File

@ -15,13 +15,11 @@ 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 () => {
@ -47,8 +45,8 @@ describe('Independent Deployability', () => {
);
updateFile(
`${remote}/webpack.config.prod.js`,
`module.exports = require('./webpack.config');`
`${remote}/rspack.config.prod.js`,
`module.exports = require('./rspack.config');`
);
// Update host to use promise based remote
@ -86,27 +84,10 @@ describe('Independent Deployability', () => {
);
updateFile(
`${host}/webpack.config.prod.js`,
`module.exports = require('./webpack.config');`
`${host}/rspack.config.prod.js`,
`module.exports = require('./rspack.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`,
@ -142,18 +123,16 @@ describe('Independent Deployability', () => {
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`,
`e2e ${host}-e2e --verbose`,
(output) => output.includes('All specs passed!')
);
await killProcessAndPorts(hostE2eResults.pid, hostPort, hostPort + 1);
await killProcessAndPorts(remoteProcess.pid, remotePort);
await killProcessAndPorts(
hostE2eResults.pid,
hostPort,
hostPort + 1,
remotePort
);
}
}, 500_000);
@ -279,7 +258,7 @@ describe('Independent Deployability', () => {
if (runE2ETests()) {
// test remote e2e
const remoteE2eResults = await runCommandUntil(
`e2e ${remote}-e2e --no-watch --verbose`,
`e2e ${remote}-e2e --verbose`,
(output) => output.includes('All specs passed!')
);
await killProcessAndPorts(remoteE2eResults.pid, remotePort);
@ -294,7 +273,7 @@ describe('Independent Deployability', () => {
);
await killProcessAndPorts(remoteProcess.pid, remotePort);
const shellE2eResults = await runCommandUntil(
`e2e ${shell}-e2e --no-watch --verbose`,
`e2e ${shell}-e2e --verbose`,
(output) => output.includes('All specs passed!')
);
await killProcessAndPorts(
@ -321,7 +300,7 @@ describe('Independent Deployability', () => {
updateFile(
`${shell}/module-federation.config.ts`,
stripIndents`
import { ModuleFederationConfig } from '@nx/webpack';
import { ModuleFederationConfig } from '@nx/module-federation';
const config: ModuleFederationConfig = {
name: '${shell}',
@ -334,14 +313,14 @@ describe('Independent Deployability', () => {
);
updateFile(
`${shell}/webpack.config.prod.ts`,
`export { default } from './webpack.config';`
`${shell}/rspack.config.prod.ts`,
`export { default } from './rspack.config';`
);
updateFile(
`${remote}/module-federation.config.ts`,
stripIndents`
import { ModuleFederationConfig } from '@nx/webpack';
import { ModuleFederationConfig } from '@nx/module-federation';
const config: ModuleFederationConfig = {
name: '${remote}',
@ -356,8 +335,8 @@ describe('Independent Deployability', () => {
);
updateFile(
`${remote}/webpack.config.prod.ts`,
`export { default } from './webpack.config';`
`${remote}/rspack.config.prod.ts`,
`export { default } from './rspack.config';`
);
// Update host e2e test to check that the remote works with library type var via navigation
@ -394,8 +373,9 @@ describe('Independent Deployability', () => {
if (runE2ETests()) {
const hostE2eResultsSwc = await runCommandUntil(
`e2e ${shell}-e2e --no-watch --verbose`,
(output) => output.includes('All specs passed!')
`e2e ${shell}-e2e --verbose`,
(output) =>
output.includes('NX Successfully ran target e2e for project')
);
await killProcessAndPorts(
hostE2eResultsSwc.pid,
@ -405,32 +385,12 @@ describe('Independent Deployability', () => {
);
const remoteE2eResultsSwc = await runCommandUntil(
`e2e ${remote}-e2e --no-watch --verbose`,
(output) => output.includes('All specs passed!')
`e2e ${remote}-e2e --verbose`,
(output) =>
output.includes('NX Successfully ran target e2e for project')
);
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);
});

View File

@ -0,0 +1,183 @@
import {
cleanupProject,
killProcessAndPorts,
newProject,
runCommandUntil,
runE2ETests,
uniq,
updateFile,
} from '@nx/e2e/utils';
import { readPort, runCLI } from './utils';
import { stripIndents } from 'nx/src/utils/strip-indents';
describe('React Rspack Module Federation Misc', () => {
describe('Convert To Rspack', () => {
beforeAll(() => {
process.env.NX_ADD_PLUGINS = 'false';
newProject({ packages: ['@nx/react', '@nx/rspack'] });
});
afterAll(() => {
cleanupProject();
delete process.env.NX_ADD_PLUGINS;
});
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));
}
}, 500_000);
});
describe('Interoperability', () => {
beforeEach(() => {
process.env.NX_ADD_PLUGINS = 'false';
newProject({ packages: ['@nx/react'] });
});
afterEach(() => {
cleanupProject();
delete process.env.NX_ADD_PLUGINS;
});
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 --verbose`,
(output) => output.includes('All specs passed!')
);
await killProcessAndPorts(e2eResultsSwc.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.cy.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 --verbose`,
(output) => output.includes('Successfully ran target e2e')
);
await killProcessAndPorts(e2eResultsSwc.pid, readPort(shell));
}
}, 500_000);
});
});

View File

@ -0,0 +1,110 @@
import {
checkFilesExist,
cleanupProject,
killPorts,
killProcessAndPorts,
newProject,
runCLIAsync,
runCommandUntil,
runE2ETests,
uniq,
updateFile,
} from '@nx/e2e/utils';
import { readPort, runCLI } from './utils';
describe('React Rspack SSR Module Federation', () => {
describe('ssr', () => {
beforeEach(() => {
newProject({ packages: ['@nx/react'] });
});
afterEach(() => cleanupProject());
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(4000);
expect(readPort(remote1)).toEqual(4201);
expect(readPort(remote2)).toEqual(4202);
expect(readPort(remote3)).toEqual(4203);
for (const app of [shell, remote1, remote2, remote3]) {
checkFilesExist(
`apps/${app}/module-federation.config.ts`,
`apps/${app}/module-federation.server.config.ts`
);
const cliOutput = runCLI(`run ${app}:build`);
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 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 --verbose`,
(output) => output.includes('All specs passed!')
);
await killProcessAndPorts(hostE2eResults.pid);
}
}, 600_000);
});
});

View File

@ -24,12 +24,10 @@ import {
startStaticRemotesFileServer,
} from '../../utils';
import { NxModuleFederationDevServerConfig } from '../../models';
import { ChildProcess, fork } from 'node:child_process';
const PLUGIN_NAME = 'NxModuleFederationDevServerPlugin';
export class NxModuleFederationDevServerPlugin implements RspackPluginInstance {
private devServerProcess: ChildProcess | undefined;
private nxBin = require.resolve('nx/bin/nx');
constructor(
@ -44,42 +42,52 @@ export class NxModuleFederationDevServerPlugin implements RspackPluginInstance {
}
apply(compiler: Compiler) {
compiler.hooks.beforeCompile.tapAsync(
const isDevServer = process.env['WEBPACK_SERVE'];
if (!isDevServer) {
return;
}
compiler.hooks.watchRun.tapAsync(
PLUGIN_NAME,
async (params, callback) => {
const staticRemotesConfig = await this.setup(compiler);
async (compiler, callback) => {
compiler.hooks.beforeCompile.tapAsync(
PLUGIN_NAME,
async (params, callback) => {
const staticRemotesConfig = await this.setup();
logger.info(
`NX Starting module federation dev-server for ${pc.bold(
this._options.config.name
)} with ${Object.keys(staticRemotesConfig).length} remotes`
logger.info(
`NX Starting module federation dev-server for ${pc.bold(
this._options.config.name
)} with ${Object.keys(staticRemotesConfig).length} remotes`
);
const mappedLocationOfRemotes = await buildStaticRemotes(
staticRemotesConfig,
this._options.devServerConfig,
this.nxBin
);
startStaticRemotesFileServer(
staticRemotesConfig,
workspaceRoot,
this._options.devServerConfig.staticRemotesPort
);
startRemoteProxies(staticRemotesConfig, mappedLocationOfRemotes, {
pathToCert: this._options.devServerConfig.sslCert,
pathToKey: this._options.devServerConfig.sslCert,
});
new DefinePlugin({
'process.env.NX_MF_DEV_REMOTES': process.env.NX_MF_DEV_REMOTES,
}).apply(compiler);
callback();
}
);
const mappedLocationOfRemotes = await buildStaticRemotes(
staticRemotesConfig,
this._options.devServerConfig,
this.nxBin
);
startStaticRemotesFileServer(
staticRemotesConfig,
workspaceRoot,
this._options.devServerConfig.staticRemotesPort
);
startRemoteProxies(staticRemotesConfig, mappedLocationOfRemotes, {
pathToCert: this._options.devServerConfig.sslCert,
pathToKey: this._options.devServerConfig.sslCert,
});
new DefinePlugin({
'process.env.NX_MF_DEV_REMOTES': process.env.NX_MF_DEV_REMOTES,
}).apply(compiler);
callback();
}
);
}
private async setup(compiler: Compiler) {
private async setup() {
const projectGraph = readCachedProjectGraph();
const { projects: workspaceProjects } =
readProjectsConfigurationFromProjectGraph(projectGraph);

View File

@ -4,14 +4,11 @@ import {
NxModuleFederationConfigOverride,
} from '../../../utils/models';
import { getModuleFederationConfig } from '../../../with-module-federation/rspack/utils';
import { NxModuleFederationDevServerConfig } from '../../models';
import { NxModuleFederationDevServerPlugin } from './nx-module-federation-dev-server-plugin';
export class NxModuleFederationPlugin implements RspackPluginInstance {
constructor(
private _options: {
config: ModuleFederationConfig;
devServerConfig?: NxModuleFederationDevServerConfig;
isServer?: boolean;
},
private configOverride?: NxModuleFederationConfigOverride
@ -23,6 +20,7 @@ export class NxModuleFederationPlugin implements RspackPluginInstance {
}
// This is required to ensure Module Federation will build the project correctly
compiler.options.optimization ??= {};
compiler.options.optimization.runtimeChunk = false;
compiler.options.output.uniqueName = this._options.config.name;
if (this._options.isServer) {

View File

@ -46,6 +46,10 @@ export class NxModuleFederationSSRDevServerPlugin
}
apply(compiler: Compiler) {
const isDevServer = process.env['WEBPACK_SERVE'];
if (!isDevServer) {
return;
}
compiler.hooks.watchRun.tapAsync(
PLUGIN_NAME,
async (compiler, callback) => {
@ -95,7 +99,7 @@ export class NxModuleFederationSSRDevServerPlugin
}
private async startServer(compiler: Compiler) {
compiler.hooks.afterEmit.tapAsync(PLUGIN_NAME, async (_, callback) => {
compiler.hooks.done.tapAsync(PLUGIN_NAME, async (_, callback) => {
const serverPath = join(
compiler.options.output.path,
(compiler.options.output.filename as string) ?? 'server.js'
@ -105,14 +109,14 @@ export class NxModuleFederationSSRDevServerPlugin
this.devServerProcess.on('exit', () => {
res();
});
this.devServerProcess.kill();
this.devServerProcess.kill('SIGKILL');
this.devServerProcess = undefined;
});
}
if (!existsSync(serverPath)) {
for (let retries = 0; retries < 10; retries++) {
await new Promise<void>((res) => setTimeout(res, 100));
await new Promise<void>((res) => setTimeout(res, 200));
if (existsSync(serverPath)) {
break;
}
@ -124,10 +128,10 @@ export class NxModuleFederationSSRDevServerPlugin
this.devServerProcess = fork(serverPath);
process.on('exit', () => {
this.devServerProcess?.kill();
this.devServerProcess?.kill('SIGKILL');
});
process.on('SIGINT', () => {
this.devServerProcess?.kill();
this.devServerProcess?.kill('SIGKILL');
});
callback();
});

View File

@ -27,7 +27,7 @@ export function parseRemotesConfig(
workspaceRoot,
}
);
if (outputPath.startsWith(projectRoot)) {
if (!outputPath.startsWith(workspaceRoot)) {
outputPath = joinPathFragments(workspaceRoot, outputPath);
}
const basePath = dirname(outputPath);

View File

@ -59,7 +59,19 @@ function collectRemoteProjects(
collected.add(remote);
const remoteProjectRoot = remoteProject.root;
const remoteProjectTsConfig = remoteProject.targets['build'].options.tsConfig;
let remoteProjectTsConfig =
remoteProject.targets['build'].options.tsConfig ??
[
join(remoteProjectRoot, 'tsconfig.app.json'),
join(remoteProjectRoot, 'tsconfig.json'),
join(context.root, 'tsconfig.json'),
join(context.root, 'tsconfig.base.json'),
].find((p) => existsSync(p));
if (!remoteProjectTsConfig) {
throw new Error(
`Could not find a tsconfig for remote project ${remote}. Please add a tsconfig.app.json or tsconfig.json to the project.`
);
}
const remoteProjectConfig = getModuleFederationConfig(
remoteProjectTsConfig,
context.root,

View File

@ -1,5 +1,6 @@
import type { ExecutorContext } from '@nx/devkit';
import { ExecutorContext, joinPathFragments } from '@nx/devkit';
import { basename, dirname } from 'path';
import { interpolate } from 'nx/src/tasks-runner/utils';
export type StaticRemoteConfig = {
basePath: string;
@ -22,8 +23,21 @@ export function parseStaticRemotesConfig(
const config: Record<string, StaticRemoteConfig> = {};
for (const app of staticRemotes) {
const outputPath =
context.projectGraph.nodes[app].data.targets['build'].options.outputPath; // dist || dist/checkout
const projectGraph = context.projectGraph;
const projectRoot = projectGraph.nodes[app].data.root;
let outputPath = interpolate(
projectGraph.nodes[app].data.targets?.['build']?.options?.outputPath ??
projectGraph.nodes[app].data.targets?.['build']?.outputs?.[0] ??
`${context.root}/${projectGraph.nodes[app].data.root}/dist`,
{
projectName: projectGraph.nodes[app].data.name,
projectRoot,
workspaceRoot: context.root,
}
);
if (outputPath.startsWith(projectRoot)) {
outputPath = joinPathFragments(context.root, outputPath);
}
const basePath = ['', '/', '.'].some((p) => dirname(outputPath) === p)
? outputPath
: dirname(outputPath); // dist || dist/checkout -> dist
@ -45,10 +59,22 @@ export function parseStaticSsrRemotesConfig(
}
const config: Record<string, StaticRemoteConfig> = {};
for (const app of staticRemotes) {
let outputPath = context.projectGraph.nodes[app].data.targets['build']
.options.outputPath as string;
outputPath = dirname(outputPath); // dist/browser => dist || dist/checkout/browser -> checkout
const projectGraph = context.projectGraph;
const projectRoot = projectGraph.nodes[app].data.root;
let outputPath = interpolate(
projectGraph.nodes[app].data.targets?.['build']?.options?.outputPath ??
projectGraph.nodes[app].data.targets?.['build']?.outputs?.[0] ??
`${context.root}/${projectGraph.nodes[app].data.root}/dist`,
{
projectName: projectGraph.nodes[app].data.name,
projectRoot,
workspaceRoot: context.root,
}
);
if (outputPath.startsWith(projectRoot)) {
outputPath = joinPathFragments(context.root, outputPath);
}
outputPath = dirname(outputPath);
const basePath = ['', '/', '.'].some((p) => dirname(outputPath) === p)
? outputPath
: dirname(outputPath); // dist || dist/checkout -> dist

View File

@ -454,7 +454,6 @@ function normalizeOutput(
relative(workspaceRoot, fullPath)
);
}
return joinPathFragments('{projectRoot}', pathRelativeToProjectRoot);
}

View File

@ -59,6 +59,7 @@
"@nx/jest",
"@nx/rollup",
"@nx/rsbuild",
"@nx/rspack",
"@nx/storybook",
"@nx/vite",
"@nx/webpack",

View File

@ -58,6 +58,20 @@ export async function addE2e(
options.addPlugin,
options.devServerPort ?? 4200
);
} else if (options.bundler === 'rspack') {
const { getRspackE2EWebServerInfo } = ensurePackage<
typeof import('@nx/rspack')
>('@nx/rspack', nxVersion);
e2eWebServerInfo = await getRspackE2EWebServerInfo(
tree,
options.projectName,
joinPathFragments(
options.appProjectRoot,
`rspack.config.${options.js ? 'js' : 'ts'}`
),
options.addPlugin,
options.devServerPort ?? 4200
);
} else if (options.bundler === 'vite') {
const { getViteE2EWebServerInfo, getReactRouterE2EWebServerInfo } =
ensurePackage<typeof import('@nx/vite')>('@nx/vite', nxVersion);

View File

@ -33,41 +33,13 @@ import {
typesReactVersion,
} from '../../../utils/versions';
export async function createApplicationFiles(
export function getDefaultTemplateVariables(
host: Tree,
options: NormalizedSchema
) {
let styleSolutionSpecificAppFiles: string;
if (options.styledModule && options.style !== 'styled-jsx') {
styleSolutionSpecificAppFiles = '../files/style-styled-module';
} else if (options.style === 'styled-jsx') {
styleSolutionSpecificAppFiles = '../files/style-styled-jsx';
} else if (options.style === 'tailwind') {
styleSolutionSpecificAppFiles = '../files/style-tailwind';
} else if (options.style === 'none') {
styleSolutionSpecificAppFiles = '../files/style-none';
} else if (options.globalCss) {
styleSolutionSpecificAppFiles = '../files/style-global-css';
} else {
styleSolutionSpecificAppFiles = '../files/style-css-module';
}
const hasStyleFile = ['scss', 'css', 'less'].includes(options.style);
const onBoardingStatus = await createNxCloudOnboardingURLForWelcomeApp(
host,
options.nxCloudToken
);
const connectCloudUrl =
onBoardingStatus === 'unclaimed' &&
(await getNxCloudAppOnBoardingUrl(options.nxCloudToken));
const relativePathToRootTsConfig = getRelativePathToRootTsConfig(
host,
options.appProjectRoot
);
const appTests = getAppTests(options);
const templateVariables = {
return {
...options.names,
...options,
typesNodeVersion,
@ -86,6 +58,79 @@ export async function createApplicationFiles(
hasStyleFile,
isUsingTsSolutionSetup: isUsingTsSolutionSetup(host),
};
}
export function createNxRspackPluginOptions(
options: NormalizedSchema,
rootOffset: string,
tsx: boolean = true
): WithNxOptions & WithReactOptions {
return {
target: 'web',
outputPath: options.isUsingTsSolutionConfig
? 'dist'
: joinPathFragments(
rootOffset,
'dist',
options.appProjectRoot != '.'
? options.appProjectRoot
: options.projectName
),
index: './src/index.html',
baseHref: '/',
main: maybeJs(
{
js: options.js,
useJsx: true,
},
`./src/main.${tsx ? 'tsx' : 'ts'}`
),
tsConfig: './tsconfig.app.json',
assets: ['./src/favicon.ico', './src/assets'],
styles:
options.styledModule || !options.hasStyles
? []
: [
`./src/styles.${
options.style !== 'tailwind' ? options.style : 'css'
}`,
],
};
}
export async function createApplicationFiles(
host: Tree,
options: NormalizedSchema
) {
let styleSolutionSpecificAppFiles: string;
if (options.styledModule && options.style !== 'styled-jsx') {
styleSolutionSpecificAppFiles = '../files/style-styled-module';
} else if (options.style === 'styled-jsx') {
styleSolutionSpecificAppFiles = '../files/style-styled-jsx';
} else if (options.style === 'tailwind') {
styleSolutionSpecificAppFiles = '../files/style-tailwind';
} else if (options.style === 'none') {
styleSolutionSpecificAppFiles = '../files/style-none';
} else if (options.globalCss) {
styleSolutionSpecificAppFiles = '../files/style-global-css';
} else {
styleSolutionSpecificAppFiles = '../files/style-css-module';
}
const onBoardingStatus = await createNxCloudOnboardingURLForWelcomeApp(
host,
options.nxCloudToken
);
const connectCloudUrl =
onBoardingStatus === 'unclaimed' &&
(await getNxCloudAppOnBoardingUrl(options.nxCloudToken));
const relativePathToRootTsConfig = getRelativePathToRootTsConfig(
host,
options.appProjectRoot
);
const templateVariables = getDefaultTemplateVariables(host, options);
if (options.bundler === 'vite' && !options.useReactRouter) {
generateFiles(
@ -279,43 +324,6 @@ function createNxWebpackPluginOptions(
};
}
function createNxRspackPluginOptions(
options: NormalizedSchema,
rootOffset: string
): WithNxOptions & WithReactOptions {
return {
target: 'web',
outputPath: options.isUsingTsSolutionConfig
? 'dist'
: joinPathFragments(
rootOffset,
'dist',
options.appProjectRoot != '.'
? options.appProjectRoot
: options.projectName
),
index: './src/index.html',
baseHref: '/',
main: maybeJs(
{
js: options.js,
useJsx: true,
},
`./src/main.tsx`
),
tsConfig: './tsconfig.app.json',
assets: ['./src/favicon.ico', './src/assets'],
styles:
options.styledModule || !options.hasStyles
? []
: [
`./src/styles.${
options.style !== 'tailwind' ? options.style : 'css'
}`,
],
};
}
function generateReactRouterFiles(
tree: Tree,
options: NormalizedSchema,

View File

@ -1,30 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`hostGenerator bundler=rspack should generate host files and configs for SSR 1`] = `
"const { composePlugins, withNx, withReact } = require('@nx/rspack');
const { withModuleFederationForSSR } = require('@nx/module-federation/rspack');
const baseConfig = require('./module-federation.config');
const defaultConfig = {
...baseConfig,
};
// Nx plugins for rspack to build config object from Nx options and context.
/**
* DTS Plugin is disabled in Nx Workspaces as Nx already provides Typing support for Module Federation
* The DTS Plugin can be enabled by setting dts: true
* Learn more about the DTS Plugin here: https://module-federation.io/configure/dts.html
*/
module.exports = composePlugins(
withNx(),
withReact({ ssr: true }),
withModuleFederationForSSR(defaultConfig, { dts: false })
);
"
`;
exports[`hostGenerator bundler=rspack should generate host files and configs for SSR 2`] = `
"// @ts-check
/**
@ -33,6 +9,12 @@ exports[`hostGenerator bundler=rspack should generate host files and configs for
const moduleFederationConfig = {
name: 'test',
remotes: [],
shared: (libraryName, libraryConfig) => {
return {
...libraryConfig,
eager: true,
};
},
};
/**
@ -43,35 +25,17 @@ module.exports = moduleFederationConfig;
`;
exports[`hostGenerator bundler=rspack should generate host files and configs for SSR when --typescriptConfiguration=true 1`] = `
"import { composePlugins, withNx, withReact } from '@nx/rspack';
import { withModuleFederationForSSR } from '@nx/module-federation/rspack';
import baseConfig from './module-federation.config';
const defaultConfig = {
...baseConfig,
};
// Nx plugins for rspack to build config object from Nx options and context.
/**
* DTS Plugin is disabled in Nx Workspaces as Nx already provides Typing support for Module Federation
* The DTS Plugin can be enabled by setting dts: true
* Learn more about the DTS Plugin here: https://module-federation.io/configure/dts.html
*/
export default composePlugins(
withNx(),
withReact({ ssr: true }),
withModuleFederationForSSR(defaultConfig, { dts: false })
);
"
`;
exports[`hostGenerator bundler=rspack should generate host files and configs for SSR when --typescriptConfiguration=true 2`] = `
"import { ModuleFederationConfig } from '@nx/module-federation';
const config: ModuleFederationConfig = {
name: 'test',
remotes: [],
shared: (libraryName, libraryConfig) => {
return {
...libraryConfig,
eager: true,
};
},
};
/**
@ -82,26 +46,49 @@ export default config;
`;
exports[`hostGenerator bundler=rspack should generate host files and configs when --typescriptConfiguration=false 1`] = `
"const { composePlugins, withNx, withReact } = require('@nx/rspack');
const { withModuleFederation } = require('@nx/module-federation/rspack');
"const { NxAppRspackPlugin } = require('@nx/rspack/app-plugin');
const { NxReactRspackPlugin } = require('@nx/rspack/react-plugin');
const {
NxModuleFederationPlugin,
NxModuleFederationDevServerPlugin,
} = require('@nx/module-federation/rspack');
const { join } = require('path');
const baseConfig = require('./module-federation.config');
const config = require('./module-federation.config');
const config = {
...baseConfig,
module.exports = {
output: {
path: join(__dirname, '../dist/test'),
publicPath: 'auto',
},
devServer: {
port: 4200,
historyApiFallback: {
index: '/index.html',
disableDotRule: true,
htmlAcceptHeaders: ['text/html', 'application/xhtml+xml'],
},
},
plugins: [
new NxAppRspackPlugin({
tsConfig: './tsconfig.app.json',
main: './src/main.ts',
index: './src/index.html',
baseHref: '/',
assets: ['./src/favicon.ico', './src/assets'],
styles: ['./src/styles.css'],
outputHashing: process.env['NODE_ENV'] === 'production' ? 'all' : 'none',
optimization: process.env['NODE_ENV'] === 'production',
}),
new NxReactRspackPlugin({
// Uncomment this line if you don't want to use SVGR
// See: https://react-svgr.com/
// svgr: false
}),
new NxModuleFederationPlugin({ config }, { dts: false }),
new NxModuleFederationDevServerPlugin({ config }),
],
};
// Nx plugins for rspack to build config object from Nx options and context.
/**
* DTS Plugin is disabled in Nx Workspaces as Nx already provides Typing support for Module Federation
* The DTS Plugin can be enabled by setting dts: true
* Learn more about the DTS Plugin here: https://module-federation.io/configure/dts.html
*/
module.exports = composePlugins(
withNx(),
withReact(),
withModuleFederation(config, { dts: false })
);
"
`;
@ -129,23 +116,46 @@ module.exports = {
`;
exports[`hostGenerator bundler=rspack should generate host files and configs when --typescriptConfiguration=true 1`] = `
"import {composePlugins, withNx, withReact} from '@nx/rspack';
import { withModuleFederation } from '@nx/module-federation/rspack';
import { ModuleFederationConfig } from '@nx/module-federation';
"import { NxAppRspackPlugin } from '@nx/rspack/app-plugin';
import { NxReactRspackPlugin } from '@nx/rspack/react-plugin';
import { NxModuleFederationPlugin, NxModuleFederationDevServerPlugin } from '@nx/module-federation/rspack';
import { join } from 'path';
import baseConfig from './module-federation.config';
import config from './module-federation.config';
const config: ModuleFederationConfig = {
...baseConfig,
export default {
output: {
path: join(__dirname, '../dist/test'),
publicPath: 'auto'
},
devServer: {
port: 4200,
historyApiFallback: {
index: '/index.html',
disableDotRule: true,
htmlAcceptHeaders: ['text/html', 'application/xhtml+xml'],
},
},
plugins: [
new NxAppRspackPlugin({
tsConfig: './tsconfig.app.json',
main: './src/main.ts',
index: './src/index.html',
baseHref: '/',
assets: ["./src/favicon.ico","./src/assets"],
styles: ["./src/styles.css"],
outputHashing: process.env['NODE_ENV'] === 'production' ? 'all' : 'none',
optimization: process.env['NODE_ENV'] === 'production',
}),
new NxReactRspackPlugin({
// Uncomment this line if you don't want to use SVGR
// See: https://react-svgr.com/
// svgr: false
}),
new NxModuleFederationPlugin({ config }, { dts: false }),
new NxModuleFederationDevServerPlugin({ config }),
],
};
// Nx plugins for rspack to build config object from Nx options and context.
/**
* DTS Plugin is disabled in Nx Workspaces as Nx already provides Typing support for Module Federation
* The DTS Plugin can be enabled by setting dts: true
* Learn more about the DTS Plugin here: https://module-federation.io/configure/dts.html
*/
export default composePlugins(withNx(), withReact(), withModuleFederation(config, { dts: false }));
"
`;

View File

@ -8,6 +8,12 @@ const config: ModuleFederationConfig = {
"<%= r.fileName %>",
<%_ }); } _%>
],
shared: (libraryName, libraryConfig) => {
return {
...libraryConfig,
eager: true
}
},
};
/**

View File

@ -0,0 +1,66 @@
import { NxAppRspackPlugin } from '@nx/rspack/app-plugin';
import { NxReactRspackPlugin } from '@nx/rspack/react-plugin';
import { NxModuleFederationPlugin, NxModuleFederationSSRDevServerPlugin } from '@nx/module-federation/rspack';
import { join } from 'path';
import browserMfConfig from './module-federation.config';
import serverMfConfig from './module-federation.server.config';
const browserRspackConfig = {
name: 'browser',
output: {
path: join(__dirname, '<%= rspackPluginOptions.outputPath %>', 'browser'),
publicPath: 'auto'
},
devServer: {
port: <%= devServerPort %>,
historyApiFallback: {
index: '/index.html',
disableDotRule: true,
htmlAcceptHeaders: ['text/html', 'application/xhtml+xml'],
},
devMiddleware: {
writeToDisk: (file: string) => !file.includes('.hot-update.'),
},
},
plugins: [
new NxAppRspackPlugin({
tsConfig: '<%= rspackPluginOptions.tsConfig %>',
main: '<%= rspackPluginOptions.main %>',
index: '<%= rspackPluginOptions.index %>',
baseHref: '<%= rspackPluginOptions.baseHref %>',
assets: <%- JSON.stringify(rspackPluginOptions.assets) %>,
styles: <%- JSON.stringify(rspackPluginOptions.styles) %>,
outputHashing: process.env['NODE_ENV'] === 'production' ? 'all' : 'none',
optimization: process.env['NODE_ENV'] === 'production',
}),
new NxReactRspackPlugin({
// Uncomment this line if you don't want to use SVGR
// See: https://react-svgr.com/
// svgr: false
}),
new NxModuleFederationPlugin({ config: browserMfConfig }, { dts: false }),
],
};
const serverRspackConfig = {
name: 'server',
target: 'async-node',
output: {
path: join(__dirname, '<%= rspackPluginOptions.outputPath %>', 'server'),
filename: 'server.js'
},
plugins: [
new NxAppRspackPlugin({
outputPath: join(__dirname, '<%= rspackPluginOptions.outputPath %>', 'server'),
outputFileName: 'server.js',
tsConfig: '<%= rspackPluginOptions.tsConfig %>',
main: '<%= rspackPluginOptions.mainServer %>',
baseHref: '<%= rspackPluginOptions.baseHref %>',
}),
new NxModuleFederationPlugin({ config: serverMfConfig, isServer: true }, { dts: false }),
new NxModuleFederationSSRDevServerPlugin({ config: serverMfConfig }),
],
};
export default [browserRspackConfig, serverRspackConfig];

View File

@ -1,16 +0,0 @@
import {composePlugins, withNx, withReact} from '@nx/rspack';
import {withModuleFederationForSSR} from '@nx/module-federation/rspack';
import baseConfig from './module-federation.config';
const defaultConfig = {
...baseConfig
};
// Nx plugins for rspack to build config object from Nx options and context.
/**
* DTS Plugin is disabled in Nx Workspaces as Nx already provides Typing support for Module Federation
* The DTS Plugin can be enabled by setting dts: true
* Learn more about the DTS Plugin here: https://module-federation.io/configure/dts.html
*/
export default composePlugins(withNx(), withReact({ssr: true}), withModuleFederationForSSR(defaultConfig, { dts: false }));

View File

@ -7,7 +7,7 @@ import { handleRequest } from './src/main.server';
const port = process.env['PORT'] || <%= port %>;
const app = express();
const browserDist = path.join(process.cwd(), '<%= browserBuildOutputPath %>');
const browserDist = path.join(process.cwd(), '<%= rspackPluginOptions.outputPath %>', 'browser');
const indexPath = path.join(browserDist, 'index.html');
app.use(cors());

View File

@ -0,0 +1,49 @@
import type { Request, Response } from 'express';
import * as fs from 'fs';
import * as ReactDOMServer from 'react-dom/server';
import isbot from 'isbot'
import App from './app/app';
import { StaticRouter } from 'react-router-dom/server';
let indexHtml: null | string = null;
export function handleRequest(indexPath: string) {
return function render(req: Request, res: Response) {
let didError = false;
if (!indexHtml) {
indexHtml = fs.readFileSync(indexPath).toString();
}
const [htmlStart, htmlEnd] = indexHtml.split(`<div id="root"></div>`);
// For bots (e.g. search engines), the content will not be streamed but render all at once.
// For users, content should be streamed to the user as they are ready.
const callbackName = isbot(req.headers['user-agent']) ? 'onAllReady' : 'onShellReady';
const stream = ReactDOMServer.renderToPipeableStream(
<StaticRouter location={req.originalUrl}><App /></StaticRouter>,
{
[callbackName]() {
res.statusCode = didError ? 500 : 200;
res.setHeader('Content-type', 'text/html; charset=utf-8');
res.write(`${htmlStart}<div id="root">`);
stream.pipe(res);
res.write(`</div>${htmlEnd}`);
},
onShellError(error) {
console.error(error);
res.statusCode = 500;
res.send('<!doctype html><h1>Server Error</h1>');
},
onError(error) {
didError = true;
console.error(error);
}
}
);
}
}

View File

@ -13,6 +13,12 @@ const moduleFederationConfig = {
}
_%>
],
shared: (libraryName, libraryConfig) => {
return {
...libraryConfig,
eager: true
}
},
};
/**

View File

@ -0,0 +1,66 @@
const { NxAppRspackPlugin } = require('@nx/rspack/app-plugin');
const { NxReactRspackPlugin } = require('@nx/rspack/react-plugin');
const { NxModuleFederationPlugin, NxModuleFederationSSRDevServerPlugin } = require('@nx/module-federation/rspack');
const { join } = require('path');
const browserMfConfig = require('./module-federation.config');
const serverMfConfig = require('./module-federation.server.config');
const browserRspackConfig = {
name: 'browser',
output: {
path: join(__dirname, '<%= rspackPluginOptions.outputPath %>', 'browser'),
publicPath: 'auto'
},
devServer: {
port: <%= devServerPort %>,
historyApiFallback: {
index: '/index.html',
disableDotRule: true,
htmlAcceptHeaders: ['text/html', 'application/xhtml+xml'],
},
devMiddleware: {
writeToDisk: (file: string) => !file.includes('.hot-update.'),
},
},
plugins: [
new NxAppRspackPlugin({
tsConfig: '<%= rspackPluginOptions.tsConfig %>',
main: '<%= rspackPluginOptions.main %>',
index: '<%= rspackPluginOptions.index %>',
baseHref: '<%= rspackPluginOptions.baseHref %>',
assets: <%- JSON.stringify(rspackPluginOptions.assets) %>,
styles: <%- JSON.stringify(rspackPluginOptions.styles) %>,
outputHashing: process.env['NODE_ENV'] === 'production' ? 'all' : 'none',
optimization: process.env['NODE_ENV'] === 'production',
}),
new NxReactRspackPlugin({
// Uncomment this line if you don't want to use SVGR
// See: https://react-svgr.com/
// svgr: false
}),
new NxModuleFederationPlugin({ config: browserMfConfig }, { dts: false }),
],
};
const serverRspackConfig = {
name: 'server',
target: 'async-node',
output: {
path: join(__dirname, '<%= rspackPluginOptions.outputPath %>', 'server'),
filename: 'server.js'
},
plugins: [
new NxAppRspackPlugin({
outputPath: join(__dirname, '<%= rspackPluginOptions.outputPath %>', 'server'),
outputFileName: 'server.js',
tsConfig: '<%= rspackPluginOptions.tsConfig %>',
main: '<%= rspackPluginOptions.mainServer %>',
baseHref: '<%= rspackPluginOptions.baseHref %>',
}),
new NxModuleFederationPlugin({ config: serverMfConfig, isServer: true }, { dts: false }),
new NxModuleFederationSSRDevServerPlugin({ config: serverMfConfig }),
],
};
module.exports = [browserRspackConfig, serverRspackConfig];

View File

@ -1,16 +0,0 @@
const {composePlugins, withNx, withReact} = require('@nx/rspack');
const {withModuleFederationForSSR} = require('@nx/module-federation/rspack');
const baseConfig = require('./module-federation.config');
const defaultConfig = {
...baseConfig
};
// Nx plugins for rspack to build config object from Nx options and context.
/**
* DTS Plugin is disabled in Nx Workspaces as Nx already provides Typing support for Module Federation
* The DTS Plugin can be enabled by setting dts: true
* Learn more about the DTS Plugin here: https://module-federation.io/configure/dts.html
*/
module.exports = composePlugins(withNx(), withReact({ssr: true}), withModuleFederationForSSR(defaultConfig, { dts: false }));

View File

@ -7,7 +7,7 @@ import { handleRequest } from './src/main.server';
const port = process.env['PORT'] || <%= port %>;
const app = express();
const browserDist = path.join(process.cwd(), '<%= browserBuildOutputPath %>');
const browserDist = path.join(process.cwd(), '<%= rspackPluginOptions.outputPath %>', 'browser');
const indexPath = path.join(browserDist, 'index.html');
app.use(cors());

View File

@ -0,0 +1,49 @@
import type { Request, Response } from 'express';
import * as fs from 'fs';
import * as ReactDOMServer from 'react-dom/server';
import isbot from 'isbot'
import App from './app/app';
import { StaticRouter } from 'react-router-dom/server';
let indexHtml: null | string = null;
export function handleRequest(indexPath: string) {
return function render(req: Request, res: Response) {
let didError = false;
if (!indexHtml) {
indexHtml = fs.readFileSync(indexPath).toString();
}
const [htmlStart, htmlEnd] = indexHtml.split(`<div id="root"></div>`);
// For bots (e.g. search engines), the content will not be streamed but render all at once.
// For users, content should be streamed to the user as they are ready.
const callbackName = isbot(req.headers['user-agent']) ? 'onAllReady' : 'onShellReady';
const stream = ReactDOMServer.renderToPipeableStream(
<StaticRouter location={req.originalUrl}><App /></StaticRouter>,
{
[callbackName]() {
res.statusCode = didError ? 500 : 200;
res.setHeader('Content-type', 'text/html; charset=utf-8');
res.write(`${htmlStart}<div id="root">`);
stream.pipe(res);
res.write(`</div>${htmlEnd}`);
},
onShellError(error) {
console.error(error);
res.statusCode = 500;
res.send('<!doctype html><h1>Server Error</h1>');
},
onError(error) {
didError = true;
console.error(error);
}
}
);
}
}

View File

@ -1,6 +1,8 @@
import { composePlugins, withNx, withReact } from '@nx/rspack';
import { withModuleFederation } from '@nx/module-federation/rspack';
import { NxAppRspackPlugin } from '@nx/rspack/app-plugin';
import { NxReactRspackPlugin } from '@nx/rspack/react-plugin';
import { NxModuleFederationPlugin, NxModuleFederationDevServerPlugin } from '@nx/module-federation/rspack';
import { ModuleFederationConfig } from '@nx/module-federation';
import { join } from 'path';
import baseConfig from './module-federation.config';
@ -30,10 +32,36 @@ const prodConfig: ModuleFederationConfig = {
],
};
// Nx plugins for rspack to build config object from Nx options and context.
/**
* DTS Plugin is disabled in Nx Workspaces as Nx already provides Typing support for Module Federation
* The DTS Plugin can be enabled by setting dts: true
* Learn more about the DTS Plugin here: https://module-federation.io/configure/dts.html
*/
export default composePlugins(withNx(), withReact(), withModuleFederation(prodConfig, { dts: false }));
export default {
output: {
path: join(__dirname, '<%= rspackPluginOptions.outputPath %>'),
publicPath: 'auto'
},
devServer: {
port: <%= devServerPort %>,
historyApiFallback: {
index: '/index.html',
disableDotRule: true,
htmlAcceptHeaders: ['text/html', 'application/xhtml+xml'],
},
},
plugins: [
new NxAppRspackPlugin({
tsConfig: '<%= rspackPluginOptions.tsConfig %>',
main: '<%= rspackPluginOptions.main %>',
index: '<%= rspackPluginOptions.index %>',
baseHref: '<%= rspackPluginOptions.baseHref %>',
assets: <%- JSON.stringify(rspackPluginOptions.assets) %>,
styles: <%- JSON.stringify(rspackPluginOptions.styles) %>,
outputHashing: process.env['NODE_ENV'] === 'production' ? 'all' : 'none',
optimization: process.env['NODE_ENV'] === 'production',
}),
new NxReactRspackPlugin({
// Uncomment this line if you don't want to use SVGR
// See: https://react-svgr.com/
// svgr: false
}),
new NxModuleFederationPlugin({ config: prodConfig }, { dts: false }),
new NxModuleFederationDevServerPlugin({ config: prodConfig }),
],
};

View File

@ -1,17 +1,40 @@
import {composePlugins, withNx, withReact} from '@nx/rspack';
import { withModuleFederation } from '@nx/module-federation/rspack';
import { ModuleFederationConfig } from '@nx/module-federation';
import { NxAppRspackPlugin } from '@nx/rspack/app-plugin';
import { NxReactRspackPlugin } from '@nx/rspack/react-plugin';
import { NxModuleFederationPlugin, NxModuleFederationDevServerPlugin } from '@nx/module-federation/rspack';
import { join } from 'path';
import baseConfig from './module-federation.config';
import config from './module-federation.config';
const config: ModuleFederationConfig = {
...baseConfig,
export default {
output: {
path: join(__dirname, '<%= rspackPluginOptions.outputPath %>'),
publicPath: 'auto'
},
devServer: {
port: <%= devServerPort %>,
historyApiFallback: {
index: '/index.html',
disableDotRule: true,
htmlAcceptHeaders: ['text/html', 'application/xhtml+xml'],
},
},
plugins: [
new NxAppRspackPlugin({
tsConfig: '<%= rspackPluginOptions.tsConfig %>',
main: '<%= rspackPluginOptions.main %>',
index: '<%= rspackPluginOptions.index %>',
baseHref: '<%= rspackPluginOptions.baseHref %>',
assets: <%- JSON.stringify(rspackPluginOptions.assets) %>,
styles: <%- JSON.stringify(rspackPluginOptions.styles) %>,
outputHashing: process.env['NODE_ENV'] === 'production' ? 'all' : 'none',
optimization: process.env['NODE_ENV'] === 'production',
}),
new NxReactRspackPlugin({
// Uncomment this line if you don't want to use SVGR
// See: https://react-svgr.com/
// svgr: false
}),
new NxModuleFederationPlugin({ config }, { dts: false }),
new NxModuleFederationDevServerPlugin({ config }),
],
};
// Nx plugins for rspack to build config object from Nx options and context.
/**
* DTS Plugin is disabled in Nx Workspaces as Nx already provides Typing support for Module Federation
* The DTS Plugin can be enabled by setting dts: true
* Learn more about the DTS Plugin here: https://module-federation.io/configure/dts.html
*/
export default composePlugins(withNx(), withReact(), withModuleFederation(config, { dts: false }));

View File

@ -1,16 +1,40 @@
const { composePlugins, withNx, withReact } = require('@nx/rspack');
const { withModuleFederation } = require('@nx/module-federation/rspack');
const { NxAppRspackPlugin } = require('@nx/rspack/app-plugin');
const { NxReactRspackPlugin } = require('@nx/rspack/react-plugin');
const { NxModuleFederationPlugin, NxModuleFederationDevServerPlugin } = require('@nx/module-federation/rspack');
const { join } = require('path');
const baseConfig = require('./module-federation.config');
const config = require('./module-federation.config');
const config = {
...baseConfig,
module.exports = {
output: {
path: join(__dirname, '<%= rspackPluginOptions.outputPath %>'),
publicPath: 'auto'
},
devServer: {
port: <%= devServerPort %>,
historyApiFallback: {
index: '/index.html',
disableDotRule: true,
htmlAcceptHeaders: ['text/html', 'application/xhtml+xml'],
},
},
plugins: [
new NxAppRspackPlugin({
tsConfig: '<%= rspackPluginOptions.tsConfig %>',
main: '<%= rspackPluginOptions.main %>',
index: '<%= rspackPluginOptions.index %>',
baseHref: '<%= rspackPluginOptions.baseHref %>',
assets: <%- JSON.stringify(rspackPluginOptions.assets) %>,
styles: <%- JSON.stringify(rspackPluginOptions.styles) %>,
outputHashing: process.env['NODE_ENV'] === 'production' ? 'all' : 'none',
optimization: process.env['NODE_ENV'] === 'production',
}),
new NxReactRspackPlugin({
// Uncomment this line if you don't want to use SVGR
// See: https://react-svgr.com/
// svgr: false
}),
new NxModuleFederationPlugin({ config }, { dts: false }),
new NxModuleFederationDevServerPlugin({ config }),
],
};
// Nx plugins for rspack to build config object from Nx options and context.
/**
* DTS Plugin is disabled in Nx Workspaces as Nx already provides Typing support for Module Federation
* The DTS Plugin can be enabled by setting dts: true
* Learn more about the DTS Plugin here: https://module-federation.io/configure/dts.html
*/
module.exports = composePlugins(withNx(), withReact(), withModuleFederation(config, { dts: false }));

View File

@ -1,38 +1,66 @@
const { composePlugins, withNx, withReact } = require('@nx/rspack');
const { withModuleFederation } = require('@nx/module-federation/rspack');
const { NxAppRspackPlugin } = require('@nx/rspack/app-plugin');
const { NxReactRspackPlugin } = require('@nx/rspack/react-plugin');
const { NxModuleFederationPlugin, NxModuleFederationDevServerPlugin } = require('@nx/module-federation/rspack');
const { join } = require('path');
const baseConfig = require('./module-federation.config');
const prodConfig = {
...baseConfig,
/*
* Remote overrides for production.
* Each entry is a pair of a unique name and the URL where it is deployed.
*
* e.g.
* remotes: [
* ['app1', 'http://app1.example.com'],
* ['app2', 'http://app2.example.com'],
* ]
*
* You can also use a full path to the remoteEntry.js file if desired.
*
* remotes: [
* ['app1', 'http://example.com/path/to/app1/remoteEntry.js'],
* ['app2', 'http://example.com/path/to/app2/remoteEntry.js'],
* ]
*/
remotes: [
<%_ remotes.forEach(function(r) { _%>
['<%= r.fileName %>', 'http://localhost:<%= r.port %>/'],
<%_ }); _%>
],
...baseConfig,
/*
* Remote overrides for production.
* Each entry is a pair of a unique name and the URL where it is deployed.
*
* e.g.
* remotes: [
* ['app1', 'http://app1.example.com'],
* ['app2', 'http://app2.example.com'],
* ]
*
* You can also use a full path to the remoteEntry.js file if desired.
*
* remotes: [
* ['app1', 'http://example.com/path/to/app1/remoteEntry.js'],
* ['app2', 'http://example.com/path/to/app2/remoteEntry.js'],
* ]
*/
remotes: [
<%_ remotes.forEach(function(r) { _%>
['<%= r.fileName %>', 'http://localhost:<%= r.port %>/'],
<%_ }); _%>
],
};
// Nx plugins for rspack to build config object from Nx options and context.
/**
* DTS Plugin is disabled in Nx Workspaces as Nx already provides Typing support for Module Federation
* The DTS Plugin can be enabled by setting dts: true
* Learn more about the DTS Plugin here: https://module-federation.io/configure/dts.html
*/
module.exports = composePlugins(withNx(), withReact(), withModuleFederation(prodConfig, { dts: false }));
module.exports = {
output: {
path: join(__dirname, '<%= rspackPluginOptions.outputPath %>'),
publicPath: 'auto'
},
devServer: {
port: <%= devServerPort %>,
historyApiFallback: {
index: '/index.html',
disableDotRule: true,
htmlAcceptHeaders: ['text/html', 'application/xhtml+xml'],
},
},
plugins: [
new NxAppRspackPlugin({
tsConfig: '<%= rspackPluginOptions.tsConfig %>',
main: '<%= rspackPluginOptions.main %>',
index: '<%= rspackPluginOptions.index %>',
baseHref: '<%= rspackPluginOptions.baseHref %>',
assets: <%- JSON.stringify(rspackPluginOptions.assets) %>,
styles: <%- JSON.stringify(rspackPluginOptions.styles) %>,
outputHashing: process.env['NODE_ENV'] === 'production' ? 'all' : 'none',
optimization: process.env['NODE_ENV'] === 'production',
}),
new NxReactRspackPlugin({
// Uncomment this line if you don't want to use SVGR
// See: https://react-svgr.com/
// svgr: false
}),
new NxModuleFederationPlugin({ config: prodConfig }, { dts: false }),
new NxModuleFederationDevServerPlugin({ config: prodConfig }),
],
};

View File

@ -226,7 +226,6 @@ describe('hostGenerator', () => {
expect(tree.exists('test/tsconfig.json')).toBeTruthy();
expect(tree.exists('test/rspack.config.prod.js')).toBeTruthy();
expect(tree.exists('test/rspack.server.config.js')).toBeTruthy();
expect(tree.exists('test/rspack.config.js')).toBeTruthy();
expect(tree.exists('test/module-federation.config.js')).toBeTruthy();
expect(
@ -250,9 +249,6 @@ describe('hostGenerator', () => {
include: ['src/remotes.d.ts', 'src/main.server.tsx', 'server.ts'],
});
expect(
tree.read('test/rspack.server.config.js', 'utf-8')
).toMatchSnapshot();
expect(
tree.read('test/module-federation.server.config.js', 'utf-8')
).toMatchSnapshot();
@ -272,7 +268,6 @@ describe('hostGenerator', () => {
expect(tree.exists('test/tsconfig.json')).toBeTruthy();
expect(tree.exists('test/rspack.config.prod.ts')).toBeTruthy();
expect(tree.exists('test/rspack.server.config.ts')).toBeTruthy();
expect(tree.exists('test/rspack.config.ts')).toBeTruthy();
expect(tree.exists('test/module-federation.config.ts')).toBeTruthy();
expect(
@ -296,9 +291,6 @@ describe('hostGenerator', () => {
include: ['src/remotes.d.ts', 'src/main.server.tsx', 'server.ts'],
});
expect(
tree.read('test/rspack.server.config.ts', 'utf-8')
).toMatchSnapshot();
expect(
tree.read('test/module-federation.server.config.ts', 'utf-8')
).toMatchSnapshot();

View File

@ -29,15 +29,18 @@ import {
} from '../../utils/versions';
import { ensureRootProjectName } from '@nx/devkit/src/generators/project-name-and-root-utils';
import { updateModuleFederationTsconfig } from './lib/update-module-federation-tsconfig';
import { normalizeHostName } from './lib/normalize-host-name';
export async function hostGenerator(
host: Tree,
schema: Schema
): Promise<GeneratorCallback> {
const tasks: GeneratorCallback[] = [];
const name = await normalizeHostName(host, schema.directory, schema.name);
const options: NormalizedSchema = {
...(await normalizeOptions<Schema>(host, {
...schema,
name,
useProjectJson: true,
})),
js: schema.js ?? false,
@ -45,8 +48,8 @@ export async function hostGenerator(
? false
: schema.typescriptConfiguration ?? true,
dynamic: schema.dynamic ?? false,
// TODO(colum): remove when MF works with Crystal
addPlugin: false,
// TODO(colum): remove when Webpack MF works with Crystal
addPlugin: !schema.bundler || schema.bundler === 'rspack' ? true : false,
bundler: schema.bundler ?? 'rspack',
};
@ -106,17 +109,19 @@ export async function hostGenerator(
}
addModuleFederationFiles(host, options, remotesWithPorts);
updateModuleFederationProject(host, options);
updateModuleFederationProject(host, options, true);
updateModuleFederationE2eProject(host, options);
updateModuleFederationTsconfig(host, options);
if (options.ssr) {
const setupSsrTask = await setupSsrGenerator(host, {
project: options.projectName,
serverPort: options.devServerPort,
skipFormat: true,
});
tasks.push(setupSsrTask);
if (options.bundler !== 'rspack') {
const setupSsrTask = await setupSsrGenerator(host, {
project: options.projectName,
serverPort: options.devServerPort,
skipFormat: true,
});
tasks.push(setupSsrTask);
}
const setupSsrForHostTask = await setupSsrForHost(
host,
@ -127,14 +132,7 @@ export async function hostGenerator(
tasks.push(setupSsrForHostTask);
const projectConfig = readProjectConfiguration(host, options.projectName);
if (options.bundler === 'rspack') {
projectConfig.targets.server.executor = '@nx/rspack:rspack';
projectConfig.targets.server.options.rspackConfig = joinPathFragments(
projectConfig.root,
`rspack.server.config.${options.typescriptConfiguration ? 'ts' : 'js'}`
);
delete projectConfig.targets.server.options.webpackConfig;
} else {
if (options.bundler !== 'rspack') {
projectConfig.targets.server.options.webpackConfig = joinPathFragments(
projectConfig.root,
`webpack.server.config.${options.typescriptConfiguration ? 'ts' : 'js'}`

View File

@ -4,27 +4,52 @@ import {
joinPathFragments,
names,
readProjectConfiguration,
offsetFromRoot,
} from '@nx/devkit';
import { maybeJs } from '../../../utils/maybe-js';
import { NormalizedSchema } from '../schema';
import {
createNxRspackPluginOptions,
getDefaultTemplateVariables,
} from '../../application/lib/create-application-files';
export function addModuleFederationFiles(
host: Tree,
options: NormalizedSchema,
defaultRemoteManifest: { name: string; port: number }[]
) {
const templateVariables = {
...names(options.projectName),
...options,
static: !options?.dynamic,
tmpl: '',
remotes: defaultRemoteManifest.map(({ name, port }) => {
return {
...names(name),
port,
};
}),
};
const templateVariables =
options.bundler === 'rspack'
? {
...getDefaultTemplateVariables(host, options as any),
rspackPluginOptions: {
...createNxRspackPluginOptions(
options as any,
offsetFromRoot(options.appProjectRoot),
false
),
mainServer: `./server.ts`,
},
static: !options?.dynamic,
remotes: defaultRemoteManifest.map(({ name, port }) => {
return {
...names(name),
port,
};
}),
}
: {
...names(options.projectName),
...options,
static: !options?.dynamic,
tmpl: '',
remotes: defaultRemoteManifest.map(({ name, port }) => {
return {
...names(name),
port,
};
}),
};
const projectConfig = readProjectConfiguration(host, options.projectName);
const pathToMFManifest = joinPathFragments(

View File

@ -0,0 +1,15 @@
import { Tree } from '@nx/devkit';
import { determineProjectNameAndRootOptions } from '@nx/devkit/src/generators/project-name-and-root-utils';
export async function normalizeHostName(
tree: Tree,
directory: string,
name?: string
): Promise<string> {
const { projectName } = await determineProjectNameAndRootOptions(tree, {
name,
directory,
projectType: 'application',
});
return projectName;
}

View File

@ -1,4 +1,4 @@
import type { GeneratorCallback, Tree } from '@nx/devkit';
import { GeneratorCallback, offsetFromRoot, Tree } from '@nx/devkit';
import {
addDependenciesToPackageJson,
generateFiles,
@ -8,23 +8,32 @@ import {
runTasksInSerial,
updateProjectConfiguration,
} from '@nx/devkit';
import type { Schema } from '../schema';
import { moduleFederationNodeVersion } from '../../../utils/versions';
import type { NormalizedSchema } from '../schema';
import {
corsVersion,
expressVersion,
isbotVersion,
moduleFederationNodeVersion,
typesExpressVersion,
} from '../../../utils/versions';
import {
createNxRspackPluginOptions,
getDefaultTemplateVariables,
} from '../../application/lib/create-application-files';
export async function setupSsrForHost(
tree: Tree,
options: Schema,
options: NormalizedSchema,
appName: string,
defaultRemoteManifest: { name: string; port: number }[]
) {
const tasks: GeneratorCallback[] = [];
let project = readProjectConfiguration(tree, appName);
project.targets.serve.executor =
options.bundler === 'rspack'
? '@nx/rspack:module-federation-ssr-dev-server'
: '@nx/react:module-federation-ssr-dev-server';
updateProjectConfiguration(tree, appName, project);
if (options.bundler !== 'rspack') {
project.targets.serve.executor =
'@nx/react:module-federation-ssr-dev-server';
updateProjectConfiguration(tree, appName, project);
}
const pathToModuleFederationSsrFiles = options.typescriptConfiguration
? `${
@ -34,30 +43,58 @@ export async function setupSsrForHost(
options.bundler === 'rspack' ? 'rspack-' : 'webpack-'
}module-federation-ssr`;
const templateVariables =
options.bundler === 'rspack'
? {
...getDefaultTemplateVariables(tree, options as any),
rspackPluginOptions: {
...createNxRspackPluginOptions(
options as any,
offsetFromRoot(options.appProjectRoot),
false
),
mainServer: `./server.ts`,
},
port: Number(options?.devServerPort) || 4200,
appName,
static: !options?.dynamic,
remotes: defaultRemoteManifest.map(({ name, port }) => {
return {
...names(name),
port,
};
}),
}
: {
...options,
static: !options?.dynamic,
port: Number(options?.devServerPort) || 4200,
appName,
tmpl: '',
browserBuildOutputPath: project.targets.build?.options?.outputPath,
remotes: defaultRemoteManifest.map(({ name, port }) => {
return {
...names(name),
port,
};
}),
};
generateFiles(
tree,
joinPathFragments(__dirname, `../files/${pathToModuleFederationSsrFiles}`),
project.root,
{
...options,
static: !options?.dynamic,
port: Number(options?.devServerPort) || 4200,
remotes: defaultRemoteManifest.map(({ name, port }) => {
return {
...names(name),
port,
};
}),
appName,
tmpl: '',
browserBuildOutputPath: project.targets.build.options.outputPath,
}
templateVariables
);
const installTask = addDependenciesToPackageJson(
tree,
{
'@module-federation/node': moduleFederationNodeVersion,
cors: corsVersion,
isbot: isbotVersion,
express: expressVersion,
'@types/express': typesExpressVersion,
},
{}
);

View File

@ -1,22 +1,49 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`remote generator bundler=rspack should create the remote with the correct config files 1`] = `
"const { composePlugins, withNx, withReact } = require('@nx/rspack');
const { withModuleFederation } = require('@nx/module-federation/rspack');
"const { NxAppRspackPlugin } = require('@nx/rspack/app-plugin');
const { NxReactRspackPlugin } = require('@nx/rspack/react-plugin');
const { NxModuleFederationPlugin, NxModuleFederationDevServerPlugin } = require('@nx/module-federation/rspack');
const { join } = require('path');
const baseConfig = require('./module-federation.config');
const config = require('./module-federation.config');
const config = {
...baseConfig,
module.exports = {
output: {
path: join(__dirname, '../dist/test'),
publicPath: 'auto'
},
devServer: {
port: 4201,
headers: {
"Access-Control-Allow-Origin": "*"
},
historyApiFallback: {
index: '/index.html',
disableDotRule: true,
htmlAcceptHeaders: ['text/html', 'application/xhtml+xml'],
},
},
plugins: [
new NxAppRspackPlugin({
tsConfig: './tsconfig.app.json',
main: './src/main.ts',
index: './src/index.html',
baseHref: '/',
assets: ["./src/favicon.ico","./src/assets"],
styles: ["./src/styles.css"],
outputHashing: process.env['NODE_ENV'] === 'production' ? 'all' : 'none',
optimization: process.env['NODE_ENV'] === 'production',
}),
new NxReactRspackPlugin({
// Uncomment this line if you don't want to use SVGR
// See: https://react-svgr.com/
// svgr: false
}),
new NxModuleFederationPlugin({ config }, { dts: false }),
new NxModuleFederationDevServerPlugin({ config }),
],
};
// Nx plugins for rspack to build config object from Nx options and context.
/**
* DTS Plugin is disabled in Nx Workspaces as Nx already provides Typing support Module Federation
* The DTS Plugin can be enabled by setting dts: true
* Learn more about the DTS Plugin here: https://module-federation.io/configure/dts.html
*/
module.exports = composePlugins(withNx(), withReact(), withModuleFederation(config, { dts: false }));
"
`;
@ -36,22 +63,49 @@ module.exports = {
`;
exports[`remote generator bundler=rspack should create the remote with the correct config files when --js=true 1`] = `
"const { composePlugins, withNx, withReact } = require('@nx/rspack');
const { withModuleFederation } = require('@nx/module-federation/rspack');
"const { NxAppRspackPlugin } = require('@nx/rspack/app-plugin');
const { NxReactRspackPlugin } = require('@nx/rspack/react-plugin');
const { NxModuleFederationPlugin, NxModuleFederationDevServerPlugin } = require('@nx/module-federation/rspack');
const { join } = require('path');
const baseConfig = require('./module-federation.config');
const config = require('./module-federation.config');
const config = {
...baseConfig,
module.exports = {
output: {
path: join(__dirname, '../dist/test'),
publicPath: 'auto'
},
devServer: {
port: 4201,
headers: {
"Access-Control-Allow-Origin": "*"
},
historyApiFallback: {
index: '/index.html',
disableDotRule: true,
htmlAcceptHeaders: ['text/html', 'application/xhtml+xml'],
},
},
plugins: [
new NxAppRspackPlugin({
tsConfig: './tsconfig.app.json',
main: './src/main.jsx',
index: './src/index.html',
baseHref: '/',
assets: ["./src/favicon.ico","./src/assets"],
styles: ["./src/styles.css"],
outputHashing: process.env['NODE_ENV'] === 'production' ? 'all' : 'none',
optimization: process.env['NODE_ENV'] === 'production',
}),
new NxReactRspackPlugin({
// Uncomment this line if you don't want to use SVGR
// See: https://react-svgr.com/
// svgr: false
}),
new NxModuleFederationPlugin({ config }, { dts: false }),
new NxModuleFederationDevServerPlugin({ config }),
],
};
// Nx plugins for rspack to build config object from Nx options and context.
/**
* DTS Plugin is disabled in Nx Workspaces as Nx already provides Typing support Module Federation
* The DTS Plugin can be enabled by setting dts: true
* Learn more about the DTS Plugin here: https://module-federation.io/configure/dts.html
*/
module.exports = composePlugins(withNx(), withReact(), withModuleFederation(config, { dts: false }));
"
`;
@ -71,26 +125,52 @@ module.exports = {
`;
exports[`remote generator bundler=rspack should create the remote with the correct config files when --typescriptConfiguration=true 1`] = `
"import { composePlugins, withNx, withReact } from '@nx/rspack';
import { withModuleFederation } from '@nx/module-federation/rspack';
"import { NxAppRspackPlugin } from '@nx/rspack/app-plugin';
import { NxReactRspackPlugin } from '@nx/rspack/react-plugin';
import {
NxModuleFederationPlugin,
NxModuleFederationDevServerPlugin,
} from '@nx/module-federation/rspack';
import { join } from 'path';
import baseConfig from './module-federation.config';
import config from './module-federation.config';
const config = {
...baseConfig,
export default {
output: {
path: join(__dirname, '../dist/test'),
publicPath: 'auto',
},
devServer: {
port: 4201,
headers: {
'Access-Control-Allow-Origin': '*',
},
historyApiFallback: {
index: '/index.html',
disableDotRule: true,
htmlAcceptHeaders: ['text/html', 'application/xhtml+xml'],
},
},
plugins: [
new NxAppRspackPlugin({
tsConfig: './tsconfig.app.json',
main: './src/main.ts',
index: './src/index.html',
baseHref: '/',
assets: ['./src/favicon.ico', './src/assets'],
styles: ['./src/styles.css'],
outputHashing: process.env['NODE_ENV'] === 'production' ? 'all' : 'none',
optimization: process.env['NODE_ENV'] === 'production',
}),
new NxReactRspackPlugin({
// Uncomment this line if you don't want to use SVGR
// See: https://react-svgr.com/
// svgr: false
}),
new NxModuleFederationPlugin({ config }, { dts: false }),
new NxModuleFederationDevServerPlugin({ config }),
],
};
// Nx plugins for rspack to build config object from Nx options and context.
/**
* DTS Plugin is disabled in Nx Workspaces as Nx already provides Typing support Module Federation
* The DTS Plugin can be enabled by setting dts: true
* Learn more about the DTS Plugin here: https://module-federation.io/configure/dts.html
*/
export default composePlugins(
withNx(),
withReact(),
withModuleFederation(config, { dts: false })
);
"
`;
@ -117,26 +197,6 @@ export default config;
`;
exports[`remote generator bundler=rspack should generate correct remote with config files when using --ssr 1`] = `
"const {composePlugins, withNx, withReact} = require('@nx/rspack');
const {withModuleFederationForSSR} = require('@nx/module-federation/rspack');
const baseConfig = require("./module-federation.server.config");
const defaultConfig = {
...baseConfig,
};
// Nx plugins for rspack to build config object from Nx options and context.
/**
* DTS Plugin is disabled in Nx Workspaces as Nx already provides Typing support Module Federation
* The DTS Plugin can be enabled by setting dts: true
* Learn more about the DTS Plugin here: https://module-federation.io/configure/dts.html
*/
module.exports = composePlugins(withNx(), withReact({ssr: true}), withModuleFederationForSSR(defaultConfig, { dts: false }));
"
`;
exports[`remote generator bundler=rspack should generate correct remote with config files when using --ssr 2`] = `
"/**
* Nx requires a default export of the config to allow correct resolution of the module federation graph.
**/
@ -145,35 +205,17 @@ module.exports = {
exposes: {
'./Module': './src/remote-entry.ts',
},
shared: (libraryName, libraryConfig) => {
return {
...libraryConfig,
eager: true
}
},
};
"
`;
exports[`remote generator bundler=rspack should generate correct remote with config files when using --ssr and --typescriptConfiguration=true 1`] = `
"import { composePlugins, withNx, withReact } from '@nx/rspack';
import { withModuleFederationForSSR } from '@nx/module-federation/rspack';
import baseConfig from './module-federation.server.config';
const defaultConfig = {
...baseConfig,
};
// Nx plugins for rspack to build config object from Nx options and context.
/**
* DTS Plugin is disabled in Nx Workspaces as Nx already provides Typing support Module Federation
* The DTS Plugin can be enabled by setting dts: true
* Learn more about the DTS Plugin here: https://module-federation.io/configure/dts.html
*/
export default composePlugins(
withNx(),
withReact({ ssr: true }),
withModuleFederationForSSR(defaultConfig, { dts: false })
);
"
`;
exports[`remote generator bundler=rspack should generate correct remote with config files when using --ssr and --typescriptConfiguration=true 2`] = `
"import { ModuleFederationConfig } from '@nx/module-federation';
const config: ModuleFederationConfig = {
@ -181,6 +223,12 @@ const config: ModuleFederationConfig = {
exposes: {
'./Module': './src/remote-entry.ts',
},
shared: (libraryName, libraryConfig) => {
return {
...libraryConfig,
eager: true,
};
},
};
/**

View File

@ -5,6 +5,12 @@ const config: ModuleFederationConfig = {
exposes: {
'./Module': './src/remote-entry.<%= js ? 'js' : 'ts' %>',
},
shared: (libraryName, libraryConfig) => {
return {
...libraryConfig,
eager: true
}
},
};
/**

View File

@ -0,0 +1,69 @@
import { NxAppRspackPlugin } from '@nx/rspack/app-plugin';
import { NxReactRspackPlugin } from '@nx/rspack/react-plugin';
import { NxModuleFederationPlugin, NxModuleFederationSSRDevServerPlugin } from '@nx/module-federation/rspack';
import { join } from 'path';
import browserMfConfig from './module-federation.config';
import serverMfConfig from './module-federation.server.config';
const browserRspackConfig = {
name: 'browser',
output: {
path: join(__dirname, '<%= rspackPluginOptions.outputPath %>', 'browser'),
publicPath: 'auto'
},
devServer: {
port: <%= devServerPort %>,
headers: {
"Access-Control-Allow-Origin": "*"
},
historyApiFallback: {
index: '/index.html',
disableDotRule: true,
htmlAcceptHeaders: ['text/html', 'application/xhtml+xml'],
},
devMiddleware: {
writeToDisk: (file: string) => !file.includes('.hot-update.'),
},
},
plugins: [
new NxAppRspackPlugin({
tsConfig: '<%= rspackPluginOptions.tsConfig %>',
main: '<%= rspackPluginOptions.main %>',
index: '<%= rspackPluginOptions.index %>',
baseHref: '<%= rspackPluginOptions.baseHref %>',
assets: <%- JSON.stringify(rspackPluginOptions.assets) %>,
styles: <%- JSON.stringify(rspackPluginOptions.styles) %>,
outputHashing: process.env['NODE_ENV'] === 'production' ? 'all' : 'none',
optimization: process.env['NODE_ENV'] === 'production',
}),
new NxReactRspackPlugin({
// Uncomment this line if you don't want to use SVGR
// See: https://react-svgr.com/
// svgr: false
}),
new NxModuleFederationPlugin({ config: browserMfConfig }, { dts: false }),
],
};
const serverRspackConfig = {
name: 'server',
target: 'async-node',
output: {
path: join(__dirname, '<%= rspackPluginOptions.outputPath %>', 'server'),
filename: 'server.js'
},
plugins: [
new NxAppRspackPlugin({
outputPath: join(__dirname, '<%= rspackPluginOptions.outputPath %>', 'server'),
outputFileName: 'server.js',
tsConfig: '<%= rspackPluginOptions.tsConfig %>',
main: '<%= rspackPluginOptions.mainServer %>',
baseHref: '<%= rspackPluginOptions.baseHref %>',
}),
new NxModuleFederationPlugin({ config: serverMfConfig, isServer: true }, { dts: false }),
new NxModuleFederationSSRDevServerPlugin({ config: serverMfConfig }),
],
};
export default [browserRspackConfig, serverRspackConfig];

View File

@ -1,16 +0,0 @@
import {composePlugins, withNx, withReact} from '@nx/rspack';
import {withModuleFederationForSSR} from '@nx/module-federation/rspack';
import baseConfig from "./module-federation.server.config";
const defaultConfig = {
...baseConfig,
};
// Nx plugins for rspack to build config object from Nx options and context.
/**
* DTS Plugin is disabled in Nx Workspaces as Nx already provides Typing support Module Federation
* The DTS Plugin can be enabled by setting dts: true
* Learn more about the DTS Plugin here: https://module-federation.io/configure/dts.html
*/
export default composePlugins(withNx(), withReact({ssr: true}), withModuleFederationForSSR(defaultConfig, { dts: false }));

View File

@ -7,8 +7,8 @@ import { handleRequest } from './src/main.server';
const port = process.env['PORT'] || <%= port %>;
const app = express();
const browserDist = path.join(process.cwd(), '<%= browserBuildOutputPath %>');
const serverDist = path.join(process.cwd(), '<%= serverBuildOutputPath %>');
const browserDist = path.join(process.cwd(), '<%= rspackPluginOptions.outputPath %>', 'browser');
const serverDist = path.join(process.cwd(), '<%= rspackPluginOptions.outputPath %>', 'server');
const indexPath = path.join(browserDist, 'index.html');
app.use(cors());

View File

@ -0,0 +1,45 @@
import type { Request, Response } from 'express';
import * as fs from 'fs';
import * as ReactDOMServer from 'react-dom/server';
import isbot from 'isbot';
import App from './app/app';
let indexHtml: null | string = null;
export function handleRequest(indexPath: string) {
return function render(req: Request, res: Response) {
let didError = false;
if (!indexHtml) {
indexHtml = fs.readFileSync(indexPath).toString();
}
const [htmlStart, htmlEnd] = indexHtml.split(`<div id="root"></div>`);
// For bots (e.g. search engines), the content will not be streamed but render all at once.
// For users, content should be streamed to the user as they are ready.
const callbackName = isbot(req.headers['user-agent'])
? 'onAllReady'
: 'onShellReady';
const stream = ReactDOMServer.renderToPipeableStream(<App />, {
[callbackName]() {
res.statusCode = didError ? 500 : 200;
res.setHeader('Content-type', 'text/html; charset=utf-8');
res.write(`${htmlStart}<div id="root">`);
stream.pipe(res);
res.write(`</div>${htmlEnd}`);
},
onShellError(error) {
console.error(error);
res.statusCode = 500;
res.send('<!doctype html><h1>Server Error</h1>');
},
onError(error) {
didError = true;
console.error(error);
},
});
};
}

View File

@ -6,4 +6,10 @@ module.exports = {
exposes: {
'./Module': './src/remote-entry.<%= js ? 'js' : 'ts' %>',
},
shared: (libraryName, libraryConfig) => {
return {
...libraryConfig,
eager: true
}
},
};

View File

@ -0,0 +1,69 @@
const { NxAppRspackPlugin } = require('@nx/rspack/app-plugin');
const { NxReactRspackPlugin } = require('@nx/rspack/react-plugin');
const { NxModuleFederationPlugin, NxModuleFederationSSRDevServerPlugin } = require('@nx/module-federation/rspack');
const { join } = require('path');
const browserMfConfig = require('./module-federation.config');
const serverMfConfig = require('./module-federation.server.config');
const browserRspackConfig = {
name: 'browser',
output: {
path: join(__dirname, '<%= rspackPluginOptions.outputPath %>', 'browser'),
publicPath: 'auto'
},
devServer: {
port: <%= devServerPort %>,
headers: {
"Access-Control-Allow-Origin": "*"
},
historyApiFallback: {
index: '/index.html',
disableDotRule: true,
htmlAcceptHeaders: ['text/html', 'application/xhtml+xml'],
},
devMiddleware: {
writeToDisk: (file: string) => !file.includes('.hot-update.'),
},
},
plugins: [
new NxAppRspackPlugin({
tsConfig: '<%= rspackPluginOptions.tsConfig %>',
main: '<%= rspackPluginOptions.main %>',
index: '<%= rspackPluginOptions.index %>',
baseHref: '<%= rspackPluginOptions.baseHref %>',
assets: <%- JSON.stringify(rspackPluginOptions.assets) %>,
styles: <%- JSON.stringify(rspackPluginOptions.styles) %>,
outputHashing: process.env['NODE_ENV'] === 'production' ? 'all' : 'none',
optimization: process.env['NODE_ENV'] === 'production',
}),
new NxReactRspackPlugin({
// Uncomment this line if you don't want to use SVGR
// See: https://react-svgr.com/
// svgr: false
}),
new NxModuleFederationPlugin({ config: browserMfConfig }, { dts: false }),
],
};
const serverRspackConfig = {
name: 'server',
target: 'async-node',
output: {
path: join(__dirname, '<%= rspackPluginOptions.outputPath %>', 'server'),
filename: 'server.js'
},
plugins: [
new NxAppRspackPlugin({
outputPath: join(__dirname, '<%= rspackPluginOptions.outputPath %>', 'server'),
outputFileName: 'server.js',
tsConfig: '<%= rspackPluginOptions.tsConfig %>',
main: '<%= rspackPluginOptions.mainServer %>',
baseHref: '<%= rspackPluginOptions.baseHref %>',
}),
new NxModuleFederationPlugin({ config: serverMfConfig, isServer: true }, { dts: false }),
new NxModuleFederationSSRDevServerPlugin({ config: serverMfConfig }),
],
};
module.exports = [browserRspackConfig, serverRspackConfig];

View File

@ -1,16 +0,0 @@
const {composePlugins, withNx, withReact} = require('@nx/rspack');
const {withModuleFederationForSSR} = require('@nx/module-federation/rspack');
const baseConfig = require("./module-federation.server.config");
const defaultConfig = {
...baseConfig,
};
// Nx plugins for rspack to build config object from Nx options and context.
/**
* DTS Plugin is disabled in Nx Workspaces as Nx already provides Typing support Module Federation
* The DTS Plugin can be enabled by setting dts: true
* Learn more about the DTS Plugin here: https://module-federation.io/configure/dts.html
*/
module.exports = composePlugins(withNx(), withReact({ssr: true}), withModuleFederationForSSR(defaultConfig, { dts: false }));

View File

@ -7,8 +7,8 @@ import { handleRequest } from './src/main.server';
const port = process.env['PORT'] || <%= port %>;
const app = express();
const browserDist = path.join(process.cwd(), '<%= browserBuildOutputPath %>');
const serverDist = path.join(process.cwd(), '<%= serverBuildOutputPath %>');
const browserDist = path.join(process.cwd(), '<%= rspackPluginOptions.outputPath %>', 'browser');
const serverDist = path.join(process.cwd(), '<%= rspackPluginOptions.outputPath %>', 'server');
const indexPath = path.join(browserDist, 'index.html');
app.use(cors());

View File

@ -0,0 +1,45 @@
import type { Request, Response } from 'express';
import * as fs from 'fs';
import * as ReactDOMServer from 'react-dom/server';
import isbot from 'isbot';
import App from './app/app';
let indexHtml: null | string = null;
export function handleRequest(indexPath: string) {
return function render(req: Request, res: Response) {
let didError = false;
if (!indexHtml) {
indexHtml = fs.readFileSync(indexPath).toString();
}
const [htmlStart, htmlEnd] = indexHtml.split(`<div id="root"></div>`);
// For bots (e.g. search engines), the content will not be streamed but render all at once.
// For users, content should be streamed to the user as they are ready.
const callbackName = isbot(req.headers['user-agent'])
? 'onAllReady'
: 'onShellReady';
const stream = ReactDOMServer.renderToPipeableStream(<App />, {
[callbackName]() {
res.statusCode = didError ? 500 : 200;
res.setHeader('Content-type', 'text/html; charset=utf-8');
res.write(`${htmlStart}<div id="root">`);
stream.pipe(res);
res.write(`</div>${htmlEnd}`);
},
onShellError(error) {
console.error(error);
res.statusCode = 500;
res.send('<!doctype html><h1>Server Error</h1>');
},
onError(error) {
didError = true;
console.error(error);
},
});
};
}

View File

@ -1,16 +1,43 @@
import {composePlugins, withNx, withReact} from '@nx/rspack';
import {withModuleFederation} from '@nx/module-federation/rspack';
import { NxAppRspackPlugin } from '@nx/rspack/app-plugin';
import { NxReactRspackPlugin } from '@nx/rspack/react-plugin';
import { NxModuleFederationPlugin, NxModuleFederationDevServerPlugin } from '@nx/module-federation/rspack';
import { join } from 'path';
import baseConfig from './module-federation.config';
import config from './module-federation.config';
const config = {
...baseConfig,
export default {
output: {
path: join(__dirname, '<%= rspackPluginOptions.outputPath %>'),
publicPath: 'auto'
},
devServer: {
port: <%= devServerPort %>,
headers: {
"Access-Control-Allow-Origin": "*"
},
historyApiFallback: {
index: '/index.html',
disableDotRule: true,
htmlAcceptHeaders: ['text/html', 'application/xhtml+xml'],
},
},
plugins: [
new NxAppRspackPlugin({
tsConfig: '<%= rspackPluginOptions.tsConfig %>',
main: '<%= rspackPluginOptions.main %>',
index: '<%= rspackPluginOptions.index %>',
baseHref: '<%= rspackPluginOptions.baseHref %>',
assets: <%- JSON.stringify(rspackPluginOptions.assets) %>,
styles: <%- JSON.stringify(rspackPluginOptions.styles) %>,
outputHashing: process.env['NODE_ENV'] === 'production' ? 'all' : 'none',
optimization: process.env['NODE_ENV'] === 'production',
}),
new NxReactRspackPlugin({
// Uncomment this line if you don't want to use SVGR
// See: https://react-svgr.com/
// svgr: false
}),
new NxModuleFederationPlugin({ config }, { dts: false }),
new NxModuleFederationDevServerPlugin({ config }),
],
};
// Nx plugins for rspack to build config object from Nx options and context.
/**
* DTS Plugin is disabled in Nx Workspaces as Nx already provides Typing support Module Federation
* The DTS Plugin can be enabled by setting dts: true
* Learn more about the DTS Plugin here: https://module-federation.io/configure/dts.html
*/
export default composePlugins(withNx(), withReact(), withModuleFederation(config, { dts: false }));

View File

@ -1,16 +1,43 @@
const { composePlugins, withNx, withReact } = require('@nx/rspack');
const { withModuleFederation } = require('@nx/module-federation/rspack');
const { NxAppRspackPlugin } = require('@nx/rspack/app-plugin');
const { NxReactRspackPlugin } = require('@nx/rspack/react-plugin');
const { NxModuleFederationPlugin, NxModuleFederationDevServerPlugin } = require('@nx/module-federation/rspack');
const { join } = require('path');
const baseConfig = require('./module-federation.config');
const config = require('./module-federation.config');
const config = {
...baseConfig,
module.exports = {
output: {
path: join(__dirname, '<%= rspackPluginOptions.outputPath %>'),
publicPath: 'auto'
},
devServer: {
port: <%= devServerPort %>,
headers: {
"Access-Control-Allow-Origin": "*"
},
historyApiFallback: {
index: '/index.html',
disableDotRule: true,
htmlAcceptHeaders: ['text/html', 'application/xhtml+xml'],
},
},
plugins: [
new NxAppRspackPlugin({
tsConfig: '<%= rspackPluginOptions.tsConfig %>',
main: '<%= rspackPluginOptions.main %>',
index: '<%= rspackPluginOptions.index %>',
baseHref: '<%= rspackPluginOptions.baseHref %>',
assets: <%- JSON.stringify(rspackPluginOptions.assets) %>,
styles: <%- JSON.stringify(rspackPluginOptions.styles) %>,
outputHashing: process.env['NODE_ENV'] === 'production' ? 'all' : 'none',
optimization: process.env['NODE_ENV'] === 'production',
}),
new NxReactRspackPlugin({
// Uncomment this line if you don't want to use SVGR
// See: https://react-svgr.com/
// svgr: false
}),
new NxModuleFederationPlugin({ config }, { dts: false }),
new NxModuleFederationDevServerPlugin({ config }),
],
};
// Nx plugins for rspack to build config object from Nx options and context.
/**
* DTS Plugin is disabled in Nx Workspaces as Nx already provides Typing support Module Federation
* The DTS Plugin can be enabled by setting dts: true
* Learn more about the DTS Plugin here: https://module-federation.io/configure/dts.html
*/
module.exports = composePlugins(withNx(), withReact(), withModuleFederation(config, { dts: false }));

View File

@ -1,4 +1,4 @@
import type { GeneratorCallback, Tree } from '@nx/devkit';
import { GeneratorCallback, names, offsetFromRoot, Tree } from '@nx/devkit';
import {
addDependenciesToPackageJson,
generateFiles,
@ -10,7 +10,17 @@ import {
import { NormalizedSchema } from '../../application/schema';
import type { Schema } from '../schema';
import { moduleFederationNodeVersion } from '../../../utils/versions';
import {
corsVersion,
expressVersion,
isbotVersion,
moduleFederationNodeVersion,
typesExpressVersion,
} from '../../../utils/versions';
import {
createNxRspackPluginOptions,
getDefaultTemplateVariables,
} from '../../application/lib/create-application-files';
export async function setupSsrForRemote(
tree: Tree,
@ -28,22 +38,49 @@ export async function setupSsrForRemote(
options.bundler === 'rspack' ? 'rspack-' : 'webpack-'
}module-federation-ssr`;
const templateVariables =
options.bundler === 'rspack'
? {
...getDefaultTemplateVariables(tree, options),
rspackPluginOptions: {
...createNxRspackPluginOptions(
options,
offsetFromRoot(options.appProjectRoot),
false
),
mainServer: `./server.ts`,
},
port: Number(options?.devServerPort) || 4200,
appName,
}
: {
...options,
port: Number(options?.devServerPort) || 4200,
appName,
tmpl: '',
browserBuildOutputPath: project.targets.build?.options?.outputPath,
serverBuildOutputPath: project.targets.server?.options?.outputPath,
};
generateFiles(
tree,
joinPathFragments(__dirname, `../files/${pathToModuleFederationSsrFiles}`),
project.root,
{
...options,
port: Number(options?.devServerPort) || 4200,
appName,
tmpl: '',
browserBuildOutputPath: project.targets.build.options.outputPath,
serverBuildOutputPath: project.targets.server.options.outputPath,
}
templateVariables
);
// For hosts to use when running remotes in static mode.
const originalOutputPath = project.targets.build?.options?.outputPath;
const originalOutputPath =
project.targets.build?.options?.outputPath ??
options.isUsingTsSolutionConfig
? 'dist'
: joinPathFragments(
offsetFromRoot(options.appProjectRoot),
'dist',
options.appProjectRoot != '.'
? options.appProjectRoot
: options.projectName
);
const serverOptions = project.targets.server?.options;
const serverOutputPath =
serverOptions?.outputPath ??
@ -66,6 +103,10 @@ export async function setupSsrForRemote(
tree,
{
'@module-federation/node': moduleFederationNodeVersion,
cors: corsVersion,
isbot: isbotVersion,
express: expressVersion,
'@types/express': typesExpressVersion,
},
{}
);

View File

@ -242,7 +242,9 @@ describe('remote generator', () => {
});
const mainFile = tree.read('test/server.ts', 'utf-8');
expect(mainFile).toContain(`join(process.cwd(), 'dist/test/browser')`);
expect(mainFile).toContain(
`join(process.cwd(), '../dist/test', 'browser')`
);
expect(mainFile).toContain('nx.server.ready');
});
@ -262,14 +264,10 @@ describe('remote generator', () => {
bundler: 'rspack',
});
expect(tree.exists('test/rspack.server.config.js')).toBeTruthy();
expect(
tree.exists('test/module-federation.server.config.js')
).toBeTruthy();
expect(
tree.read('test/rspack.server.config.js', 'utf-8')
).toMatchSnapshot();
expect(
tree.read('test/module-federation.server.config.js', 'utf-8')
).toMatchSnapshot();
@ -291,14 +289,10 @@ describe('remote generator', () => {
bundler: 'rspack',
});
expect(tree.exists('test/rspack.server.config.ts')).toBeTruthy();
expect(
tree.exists('test/module-federation.server.config.ts')
).toBeTruthy();
expect(
tree.read('test/rspack.server.config.ts', 'utf-8')
).toMatchSnapshot();
expect(
tree.read('test/module-federation.server.config.ts', 'utf-8')
).toMatchSnapshot();

View File

@ -6,6 +6,7 @@ import {
GeneratorCallback,
joinPathFragments,
names,
offsetFromRoot,
readProjectConfiguration,
runTasksInSerial,
stripIndents,
@ -31,16 +32,33 @@ import {
nxVersion,
} from '../../utils/versions';
import { ensureRootProjectName } from '@nx/devkit/src/generators/project-name-and-root-utils';
import {
createNxRspackPluginOptions,
getDefaultTemplateVariables,
} from '../application/lib/create-application-files';
export function addModuleFederationFiles(
host: Tree,
options: NormalizedSchema<Schema>
) {
const templateVariables = {
...names(options.projectName),
...options,
tmpl: '',
};
const templateVariables =
options.bundler === 'rspack'
? {
...getDefaultTemplateVariables(host, options),
rspackPluginOptions: {
...createNxRspackPluginOptions(
options,
offsetFromRoot(options.appProjectRoot),
false
),
mainServer: `./server.ts`,
},
}
: {
...names(options.projectName),
...options,
tmpl: '',
};
generateFiles(
host,
@ -106,8 +124,8 @@ export async function remoteGenerator(host: Tree, schema: Schema) {
? false
: schema.typescriptConfiguration ?? true,
dynamic: schema.dynamic ?? false,
// TODO(colum): remove when MF works with Crystal
addPlugin: false,
// TODO(colum): remove when Webpack MF works with Crystal
addPlugin: !schema.bundler || schema.bundler === 'rspack' ? true : false,
bundler: schema.bundler ?? 'rspack',
};
@ -170,13 +188,15 @@ export async function remoteGenerator(host: Tree, schema: Schema) {
setupTspathForRemote(host, options);
if (options.ssr) {
const setupSsrTask = await setupSsrGenerator(host, {
project: options.projectName,
serverPort: options.devServerPort,
skipFormat: true,
bundler: options.bundler,
});
tasks.push(setupSsrTask);
if (options.bundler !== 'rspack') {
const setupSsrTask = await setupSsrGenerator(host, {
project: options.projectName,
serverPort: options.devServerPort,
skipFormat: true,
bundler: options.bundler,
});
tasks.push(setupSsrTask);
}
const setupSsrForRemoteTask = await setupSsrForRemote(
host,
@ -186,20 +206,13 @@ export async function remoteGenerator(host: Tree, schema: Schema) {
tasks.push(setupSsrForRemoteTask);
const projectConfig = readProjectConfiguration(host, options.projectName);
if (options.bundler === 'rspack') {
projectConfig.targets.server.executor = '@nx/rspack:rspack';
projectConfig.targets.server.options.rspackConfig = joinPathFragments(
projectConfig.root,
`rspack.server.config.${options.typescriptConfiguration ? 'ts' : 'js'}`
);
delete projectConfig.targets.server.options.webpackConfig;
} else {
if (options.bundler !== 'rspack') {
projectConfig.targets.server.options.webpackConfig = joinPathFragments(
projectConfig.root,
`webpack.server.config.${options.typescriptConfiguration ? 'ts' : 'js'}`
);
updateProjectConfiguration(host, options.projectName, projectConfig);
}
updateProjectConfiguration(host, options.projectName, projectConfig);
}
if (!options.setParserOptionsProject) {
host.delete(

View File

@ -20,33 +20,13 @@ export function updateModuleFederationProject(
typescriptConfiguration?: boolean;
dynamic?: boolean;
bundler?: 'rspack' | 'webpack';
}
ssr?: boolean;
},
isHost = false
) {
const projectConfig = readProjectConfiguration(host, options.projectName);
if (options.bundler === 'rspack') {
projectConfig.targets.build.executor = '@nx/rspack:rspack';
projectConfig.targets.build.options = {
...(projectConfig.targets.build.options ?? {}),
main: maybeJs(
{ js: options.js, useJsx: true },
`${options.appProjectRoot}/src/main.ts`
),
rspackConfig: `${options.appProjectRoot}/rspack.config.${
options.typescriptConfiguration && !options.js ? 'ts' : 'js'
}`,
target: 'web',
};
projectConfig.targets.build.configurations ??= {};
projectConfig.targets.build.configurations.production = {
...(projectConfig.targets.build.configurations?.production ?? {}),
rspackConfig: `${options.appProjectRoot}/rspack.config.prod.${
options.typescriptConfiguration && !options.js ? 'ts' : 'js'
}`,
};
} else {
if (options.bundler !== 'rspack') {
projectConfig.targets.build.options = {
...(projectConfig.targets.build.options ?? {}),
main: maybeJs(options, `${options.appProjectRoot}/src/main.ts`),
@ -67,20 +47,7 @@ export function updateModuleFederationProject(
// If host should be configured to use dynamic federation
if (options.dynamic) {
if (options.bundler === 'rspack') {
const pathToProdRspackConfig = joinPathFragments(
projectConfig.root,
`rspack.prod.config.${
options.typescriptConfiguration && !options.js ? 'ts' : 'js'
}`
);
if (host.exists(pathToProdRspackConfig)) {
host.delete(pathToProdRspackConfig);
}
delete projectConfig.targets.build.configurations.production
?.rspackConfig;
} else {
if (options.bundler !== 'rspack') {
const pathToProdWebpackConfig = joinPathFragments(
projectConfig.root,
`webpack.prod.config.${
@ -96,38 +63,41 @@ export function updateModuleFederationProject(
}
}
if (options.bundler === 'rspack') {
projectConfig.targets.serve.executor =
'@nx/rspack:module-federation-dev-server';
} else {
if (options.bundler !== 'rspack') {
projectConfig.targets.serve.executor =
'@nx/react:module-federation-dev-server';
}
projectConfig.targets.serve.options.port = options.devServerPort;
projectConfig.targets.serve ??= {};
projectConfig.targets.serve.options ??= {};
projectConfig.targets.serve.options.port =
options.bundler === 'rspack' && options.ssr && isHost
? 4000
: options.devServerPort;
// `serve-static` for remotes that don't need to be in development mode
const serveStaticExecutor =
options.bundler === 'rspack'
? '@nx/rspack:module-federation-static-server'
: '@nx/react:module-federation-static-server';
projectConfig.targets['serve-static'] = {
executor: serveStaticExecutor,
defaultConfiguration: 'production',
options: {
serveTarget: `${options.projectName}:serve`,
},
configurations: {
development: {
serveTarget: `${options.projectName}:serve:development`,
if (options.bundler !== 'rspack') {
const serveStaticExecutor = '@nx/react:module-federation-static-server';
projectConfig.targets['serve-static'] = {
executor: serveStaticExecutor,
defaultConfiguration: 'production',
options: {
serveTarget: `${options.projectName}:serve`,
},
production: {
serveTarget: `${options.projectName}:serve:production`,
configurations: {
development: {
serveTarget: `${options.projectName}:serve:development`,
},
production: {
serveTarget: `${options.projectName}:serve:production`,
},
},
},
};
};
}
// Typechecks must be performed first before build and serve to generate remote d.ts files.
if (isUsingTsSolutionSetup(host)) {
projectConfig.targets.build ??= {};
projectConfig.targets.serve ??= {};
projectConfig.targets.build.dependsOn = ['^build', 'typecheck'];
projectConfig.targets.serve.dependsOn = ['typecheck'];
}

View File

@ -45,6 +45,7 @@
"@module-federation/node",
// @nx/workspace is only required in < 15.8
"@nx/workspace",
"@nx/react",
// Imported types only
"@module-federation/sdk",
"@module-federation/enhanced",

View File

@ -1,3 +1,4 @@
/* eslint-disable @nx/enforce-module-boundaries */
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import { readProjectConfiguration } from '@nx/devkit';
// nx-ignore-next-line

View File

@ -2,7 +2,9 @@ import {
addDependenciesToPackageJson,
formatFiles,
getProjects,
readNxJson,
type Tree,
updateNxJson,
updateProjectConfiguration,
} from '@nx/devkit';
import { Schema } from './schema';
@ -78,6 +80,37 @@ export default async function (tree: Tree, options: Schema) {
}
updateProjectConfiguration(tree, options.project, project);
const nxJson = readNxJson(tree);
if (nxJson.plugins !== undefined && nxJson.plugins.length > 0) {
const nonRspackPlugins = nxJson.plugins.filter(
(plugin) =>
(typeof plugin !== 'string' && plugin.plugin !== '@nx/rspack/plugin') ||
(typeof plugin === 'string' && plugin !== '@nx/rspack/plugin')
);
let rspackPlugins = nxJson.plugins.filter(
(plugin) =>
(typeof plugin !== 'string' && plugin.plugin === '@nx/rspack/plugin') ||
(typeof plugin === 'string' && plugin === '@nx/rspack/plugin')
);
if (rspackPlugins.length === 0) {
rspackPlugins = rspackPlugins.map((plugin) => {
if (typeof plugin === 'string') {
return {
plugin: plugin,
exclude: [`${project.root}/*`],
};
}
return {
...plugin,
exclude: [...(plugin.exclude ?? []), `${project.root}/*`],
};
});
nxJson.plugins = [...nonRspackPlugins, ...rspackPlugins];
updateNxJson(tree, nxJson);
}
}
const installTask = addDependenciesToPackageJson(
tree,
{},

View File

@ -4,4 +4,5 @@ export * from './utils/config';
export * from './utils/with-nx';
export * from './utils/with-react';
export * from './utils/with-web';
export * from './utils/e2e-web-server-info-utils';
export * from './plugins/use-legacy-nx-plugin/use-legacy-nx-plugin';

View File

@ -0,0 +1,39 @@
import { type Tree, readNxJson } from '@nx/devkit';
import { getE2EWebServerInfo } from '@nx/devkit/src/generators/e2e-web-server-info-utils';
export async function getRspackE2EWebServerInfo(
tree: Tree,
projectName: string,
configFilePath: string,
isPluginBeingAdded: boolean,
e2ePortOverride?: number
) {
const nxJson = readNxJson(tree);
let e2ePort = e2ePortOverride ?? 4200;
if (
nxJson.targetDefaults?.['serve'] &&
nxJson.targetDefaults?.['serve'].options?.port
) {
e2ePort = nxJson.targetDefaults?.['serve'].options?.port;
}
return getE2EWebServerInfo(
tree,
projectName,
{
plugin: '@nx/rspack/plugin',
serveTargetName: 'serveTargetName',
serveStaticTargetName: 'previewTargetName',
configFilePath,
},
{
defaultServeTargetName: 'serve',
defaultServeStaticTargetName: 'preview',
defaultE2EWebServerAddress: `http://localhost:${e2ePort}`,
defaultE2ECiBaseUrl: 'http://localhost:4200',
defaultE2EPort: e2ePort,
},
isPluginBeingAdded
);
}