feat(module-federation): use single file-server for static remotes (#20006)

This commit is contained in:
Colum Ferry 2023-11-10 16:25:09 +00:00 committed by GitHub
parent e8e8f94f7a
commit a73e9fd562
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 975 additions and 636 deletions

View File

@ -132,6 +132,10 @@
"parallel": {
"type": "number",
"description": "Max number of parallel processes for building static remotes"
},
"staticRemotesPort": {
"type": "number",
"description": "The port at which to serve the file-server for the static remotes."
}
},
"additionalProperties": false,

View File

@ -104,6 +104,10 @@
"parallel": {
"type": "number",
"description": "Max number of parallel processes for building static remotes"
},
"staticRemotesPort": {
"type": "number",
"description": "The port at which to serve the file-server for the static remotes."
}
},
"presets": []

View File

@ -93,7 +93,6 @@
}
},
"additionalProperties": false,
"required": ["buildTarget"],
"presets": []
},
"description": "Serve a web application from a folder.",

View File

@ -316,6 +316,8 @@ describe('Angular Module Federation', () => {
const module = uniq('module');
const host = uniq('host');
const hostPort = 4200;
runCLI(
`generate @nx/angular:host ${host} --remotes=${remote} --no-interactive --projectNameAndRootFormat=as-provided`
);
@ -383,7 +385,7 @@ describe('Angular Module Federation', () => {
`e2e ${host}-e2e --no-watch --verbose`,
(output) => output.includes('All specs passed!')
);
await killProcessAndPorts(hostE2eResults.pid);
await killProcessAndPorts(hostE2eResults.pid, hostPort, hostPort + 1);
}
}, 500_000);
@ -393,6 +395,7 @@ describe('Angular Module Federation', () => {
const childRemote = uniq('childremote');
const module = uniq('module');
const host = uniq('host');
const hostPort = 4200;
runCLI(
`generate @nx/angular:host ${host} --remotes=${remote} --no-interactive --projectNameAndRootFormat=as-provided`
@ -478,7 +481,7 @@ describe('Angular Module Federation', () => {
`e2e ${host}-e2e --no-watch --verbose`,
(output) => output.includes('All specs passed!')
);
await killProcessAndPorts(hostE2eResults.pid);
await killProcessAndPorts(hostE2eResults.pid, hostPort, hostPort + 1);
}
}, 500_000);
});

View File

@ -114,21 +114,33 @@ describe('React Module Federation', () => {
);
if (runE2ETests()) {
const e2eResultsSwc = runCLI(`e2e ${shell}-e2e --no-watch --verbose`);
expect(e2eResultsSwc).toContain('All specs passed!');
await killPorts(readPort(shell));
await killPorts(readPort(remote1));
await killPorts(readPort(remote2));
await killPorts(readPort(remote3));
const e2eResultsSwc = await runCommandUntil(
`e2e ${shell}-e2e --no-watch --verbose`,
(output) => output.includes('All specs passed!')
);
const e2eResultsTsNode = runCLI(`e2e ${shell}-e2e --no-watch --verbose`, {
env: { NX_PREFER_TS_NODE: 'true' },
});
expect(e2eResultsTsNode).toContain('All specs passed!');
await killPorts(readPort(shell));
await killPorts(readPort(remote1));
await killPorts(readPort(remote2));
await killPorts(readPort(remote3));
await killProcessAndPorts(
e2eResultsSwc.pid,
readPort(shell),
readPort(remote1),
readPort(remote2),
readPort(remote3)
);
const e2eResultsTsNode = await runCommandUntil(
`e2e ${shell}-e2e --no-watch --verbose`,
(output) => output.includes('All specs passed!'),
{
env: { NX_PREFER_TS_NODE: 'true' },
}
);
await killProcessAndPorts(
e2eResultsTsNode.pid,
readPort(shell),
readPort(remote1),
readPort(remote2),
readPort(remote3)
);
}
}, 500_000);
@ -164,7 +176,6 @@ describe('React Module Federation', () => {
it('should should support generating host and remote apps with the new name and root format', async () => {
const shell = uniq('shell');
const remote = uniq('remote');
const shellPort = 4200;
runCLI(
`generate @nx/react:host ${shell} --project-name-and-root-format=as-provided --no-interactive`
@ -173,6 +184,9 @@ describe('React Module Federation', () => {
`generate @nx/react:remote ${remote} --host=${shell} --project-name-and-root-format=as-provided --no-interactive`
);
const shellPort = readPort(shell);
const remotePort = readPort(remote);
// check files are generated without the layout directory ("apps/") and
// using the project name as the directory when no directory is provided
checkFilesExist(`${shell}/module-federation.config.ts`);
@ -196,7 +210,12 @@ describe('React Module Federation', () => {
);
}
);
await killProcessAndPorts(shellProcessSwc.pid, shellPort);
await killProcessAndPorts(
shellProcessSwc.pid,
shellPort,
remotePort + 1,
remotePort
);
const shellProcessTsNode = await runCommandUntil(
`serve ${shell} --devRemotes=${remote} --verbose`,
@ -209,256 +228,25 @@ describe('React Module Federation', () => {
env: { NX_PREFER_TS_NODE: 'true' },
}
);
await killProcessAndPorts(shellProcessTsNode.pid, shellPort);
}, 500_000);
it('should support different versions workspace libs for host and remote', async () => {
const shell = uniq('shell');
const remote = uniq('remote');
const lib = uniq('lib');
runCLI(
`generate @nx/react:host ${shell} --remotes=${remote} --no-interactive --projectNameAndRootFormat=as-provided`
await killProcessAndPorts(
shellProcessTsNode.pid,
shellPort,
remotePort + 1,
remotePort
);
runCLI(
`generate @nx/js:lib ${lib} --importPath=@acme/${lib} --publishable=true --no-interactive --projectNameAndRootFormat=as-provided`
);
updateFile(
`${lib}/src/lib/${lib}.ts`,
stripIndents`
export const version = '0.0.1';
`
);
updateJson(`${lib}/package.json`, (json) => {
return {
...json,
version: '0.0.1',
};
});
// Update host to use the lib
updateFile(
`${shell}/src/app/app.tsx`,
`
import * as React from 'react';
import NxWelcome from './nx-welcome';
import { version } from '@acme/${lib}';
import { Link, Route, Routes } from 'react-router-dom';
const About = React.lazy(() => import('${remote}/Module'));
export function App() {
return (
<React.Suspense fallback={null}>
<div className="home">
Lib version: { version }
</div>
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/About">About</Link>
</li>
</ul>
<Routes>
<Route path="/" element={<NxWelcome title="home" />} />
<Route path="/About" element={<About />} />
</Routes>
</React.Suspense>
);
}
export default App;`
);
// Update remote to use the lib
updateFile(
`${remote}/src/app/app.tsx`,
`// eslint-disable-next-line @typescript-eslint/no-unused-vars
import styles from './app.module.css';
import { version } from '@acme/${lib}';
import NxWelcome from './nx-welcome';
export function App() {
return (
<div className='remote'>
Lib version: { version }
<NxWelcome title="${remote}" />
</div>
);
}
export default App;`
);
// update remote e2e test to check the version
updateFile(
`${remote}-e2e/src/e2e/app.cy.ts`,
`describe('${remote}', () => {
beforeEach(() => cy.visit('/'));
it('should check the lib version', () => {
cy.get('div.remote').contains('Lib version: 0.0.1');
});
});
`
);
// update shell e2e test to check the version
updateFile(
`${shell}-e2e/src/e2e/app.cy.ts`,
`
describe('${shell}', () => {
beforeEach(() => cy.visit('/'));
it('should check the lib version', () => {
cy.get('div.home').contains('Lib version: 0.0.1');
});
});
`
);
if (runE2ETests()) {
// test remote e2e
const remoteE2eResults = runCLI(`e2e ${remote}-e2e --no-watch --verbose`);
expect(remoteE2eResults).toContain('All specs passed!');
// test shell e2e
// serve remote first
const remotePort = 4201;
const remoteProcess = await runCommandUntil(
`serve ${remote} --no-watch --verbose`,
(output) => {
return output.includes(
`Web Development Server is listening at http://localhost:${remotePort}/`
);
}
);
const shellE2eResults = runCLI(`e2e ${shell}-e2e --no-watch --verbose`);
expect(shellE2eResults).toContain('All specs passed!');
await killProcessAndPorts(remoteProcess.pid, remotePort);
}
}, 500_000);
it('should support host and remote with library type var', async () => {
const shell = uniq('shell');
const remote = uniq('remote');
runCLI(
`generate @nx/react:host ${shell} --remotes=${remote} --project-name-and-root-format=as-provided --no-interactive`
);
// update host and remote to use library type var
updateFile(
`${shell}/module-federation.config.ts`,
stripIndents`
import { ModuleFederationConfig } from '@nx/webpack';
const config: ModuleFederationConfig = {
name: '${shell}',
library: { type: 'var', name: '${shell}' },
remotes: ['${remote}'],
};
export default config;
`
);
updateFile(
`${shell}/webpack.config.prod.ts`,
`export { default } from './webpack.config';`
);
updateFile(
`${remote}/module-federation.config.ts`,
stripIndents`
import { ModuleFederationConfig } from '@nx/webpack';
const config: ModuleFederationConfig = {
name: '${remote}',
library: { type: 'var', name: '${remote}' },
exposes: {
'./Module': './src/remote-entry.ts',
},
};
export default config;
`
);
updateFile(
`${remote}/webpack.config.prod.ts`,
`export { default } from './webpack.config';`
);
// Update host e2e test to check that the remote works with library type var via navigation
updateFile(
`${shell}-e2e/src/e2e/app.cy.ts`,
`
import { getGreeting } from '../support/app.po';
describe('${shell}', () => {
beforeEach(() => cy.visit('/'));
it('should display welcome message', () => {
getGreeting().contains('Welcome ${shell}');
});
it('should navigate to /about from /', () => {
cy.get('a').contains('${remote[0].toUpperCase()}${remote.slice(
1
)}').click();
cy.url().should('include', '/${remote}');
getGreeting().contains('Welcome ${remote}');
});
});
`
);
// Build host and remote
const buildOutput = runCLI(`build ${shell}`);
const remoteOutput = runCLI(`build ${remote}`);
expect(buildOutput).toContain('Successfully ran target build');
expect(remoteOutput).toContain('Successfully ran target build');
if (runE2ETests()) {
const hostE2eResultsSwc = runCLI(`e2e ${shell}-e2e --no-watch --verbose`);
const remoteE2eResultsSwc = runCLI(
`e2e ${remote}-e2e --no-watch --verbose`
);
expect(hostE2eResultsSwc).toContain('All specs passed!');
expect(remoteE2eResultsSwc).toContain('All specs passed!');
const hostE2eResultsTsNode = runCLI(
`e2e ${shell}-e2e --no-watch --verbose`,
{ env: { NX_PREFER_TS_NODE: 'true' } }
);
const remoteE2eResultsTsNode = runCLI(
`e2e ${remote}-e2e --no-watch --verbose`,
{ env: { NX_PREFER_TS_NODE: 'true' } }
);
expect(hostE2eResultsTsNode).toContain('All specs passed!');
expect(remoteE2eResultsTsNode).toContain('All specs passed!');
}
}, 500_000);
// Federate Module
describe('Federate Module', () => {
let proj: string;
let tree: Tree;
beforeAll(() => {
tree = createTreeWithEmptyWorkspace();
proj = newProject();
});
afterAll(() => cleanupProject());
it('should federate a module from a library and update an existing remote', async () => {
const lib = uniq('lib');
const remote = uniq('remote');
@ -535,6 +323,9 @@ describe('React Module Federation', () => {
`
);
const hostPort = readPort(host);
const remotePort = readPort(remote);
// Build host and remote
const buildOutput = runCLI(`build ${host}`);
const remoteOutput = runCLI(`build ${remote}`);
@ -543,9 +334,16 @@ describe('React Module Federation', () => {
expect(remoteOutput).toContain('Successfully ran target build');
if (runE2ETests()) {
const hostE2eResults = runCLI(`e2e ${host}-e2e --no-watch --verbose`);
expect(hostE2eResults).toContain('All specs passed!');
const hostE2eResults = await runCommandUntil(
`e2e ${host}-e2e --no-watch --verbose`,
(output) => output.includes('All specs passed!')
);
await killProcessAndPorts(
hostE2eResults.pid,
hostPort,
hostPort + 1,
remotePort
);
}
}, 500_000);
@ -618,6 +416,10 @@ describe('React Module Federation', () => {
`
);
const hostPort = readPort(host);
const remotePort = readPort(remote);
const childRemotePort = readPort(childRemote);
// Build host and remote
const buildOutput = runCLI(`build ${host}`);
const remoteOutput = runCLI(`build ${remote}`);
@ -626,25 +428,44 @@ describe('React Module Federation', () => {
expect(remoteOutput).toContain('Successfully ran target build');
if (runE2ETests()) {
const hostE2eResults = runCLI(`e2e ${host}-e2e --no-watch --verbose`);
expect(hostE2eResults).toContain('All specs passed!');
const hostE2eResults = await runCommandUntil(
`e2e ${host}-e2e --no-watch --verbose`,
(output) => output.includes('All specs passed!')
);
await killProcessAndPorts(
hostE2eResults.pid,
hostPort,
hostPort + 1,
remotePort,
childRemotePort
);
}
}, 500_000);
});
describe('Promised based remotes', () => {
it('should support promised based remotes', async () => {
const remote = uniq('remote');
const host = uniq('host');
describe('Independent Deployability', () => {
let proj: string;
let tree: Tree;
runCLI(
`generate @nx/react:host ${host} --remotes=${remote} --no-interactive --projectNameAndRootFormat=as-provided --typescriptConfiguration=false`
);
beforeAll(() => {
tree = createTreeWithEmptyWorkspace();
proj = newProject();
});
// Update remote to be loaded via script
updateFile(
`${remote}/module-federation.config.js`,
stripIndents`
afterAll(() => cleanupProject());
it('should support promised based remotes', async () => {
const remote = uniq('remote');
const host = uniq('host');
runCLI(
`generate @nx/react:host ${host} --remotes=${remote} --no-interactive --projectNameAndRootFormat=as-provided --typescriptConfiguration=false`
);
// Update remote to be loaded via script
updateFile(
`${remote}/module-federation.config.js`,
stripIndents`
module.exports = {
name: '${remote}',
library: { type: 'var', name: '${remote}' },
@ -653,17 +474,17 @@ describe('React Module Federation', () => {
},
};
`
);
);
updateFile(
`${remote}/webpack.config.prod.js`,
`module.exports = require('./webpack.config');`
);
updateFile(
`${remote}/webpack.config.prod.js`,
`module.exports = require('./webpack.config');`
);
// Update host to use promise based remote
updateFile(
`${host}/module-federation.config.js`,
`module.exports = {
// Update host to use promise based remote
updateFile(
`${host}/module-federation.config.js`,
`module.exports = {
name: '${host}',
library: { type: 'var', name: '${host}' },
remotes: [
@ -692,34 +513,34 @@ describe('React Module Federation', () => {
],
};
`
);
);
updateFile(
`${host}/webpack.config.prod.js`,
`module.exports = require('./webpack.config');`
);
updateFile(
`${host}/webpack.config.prod.js`,
`module.exports = require('./webpack.config');`
);
// Update e2e project.json
updateJson(`${host}-e2e/project.json`, (json) => {
return {
...json,
targets: {
...json.targets,
e2e: {
...json.targets.e2e,
options: {
...json.targets.e2e.options,
devServerTarget: `${host}:serve-static:production`,
},
// Update e2e 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`,
`
// update e2e
updateFile(
`${host}-e2e/src/e2e/app.cy.ts`,
`
import { getGreeting } from '../support/app.po';
describe('${host}', () => {
@ -731,41 +552,328 @@ describe('React Module Federation', () => {
it('should navigate to /${remote} from /', () => {
cy.get('a').contains('${remote[0].toUpperCase()}${remote.slice(
1
)}').click();
1
)}').click();
cy.url().should('include', '/${remote}');
getGreeting().contains('Welcome ${remote}');
});
});
`
);
const hostPort = readPort(host);
const remotePort = readPort(remote);
// Build host and remote
const buildOutput = runCLI(`build ${host}`);
const remoteOutput = runCLI(`build ${remote}`);
expect(buildOutput).toContain('Successfully ran target build');
expect(remoteOutput).toContain('Successfully ran target build');
if (runE2ETests()) {
const remoteProcess = await runCommandUntil(
`serve-static ${remote} --no-watch --verbose`,
() => {
return true;
}
);
const hostE2eResults = await runCommandUntil(
`e2e ${host}-e2e --no-watch --verbose`,
(output) => output.includes('All specs passed!')
);
await killProcessAndPorts(hostE2eResults.pid, hostPort, hostPort + 1);
await killProcessAndPorts(remoteProcess.pid, remotePort);
}
}, 500_000);
it('should support different versions workspace libs for host and remote', async () => {
const shell = uniq('shell');
const remote = uniq('remote');
const lib = uniq('lib');
runCLI(
`generate @nx/react:host ${shell} --remotes=${remote} --no-interactive --projectNameAndRootFormat=as-provided`
);
runCLI(
`generate @nx/js:lib ${lib} --importPath=@acme/${lib} --publishable=true --no-interactive --projectNameAndRootFormat=as-provided`
);
const shellPort = readPort(shell);
const remotePort = readPort(remote);
updateFile(
`${lib}/src/lib/${lib}.ts`,
stripIndents`
export const version = '0.0.1';
`
);
updateJson(`${lib}/package.json`, (json) => {
return {
...json,
version: '0.0.1',
};
});
// Update host to use the lib
updateFile(
`${shell}/src/app/app.tsx`,
`
import * as React from 'react';
import NxWelcome from './nx-welcome';
import { version } from '@acme/${lib}';
import { Link, Route, Routes } from 'react-router-dom';
const About = React.lazy(() => import('${remote}/Module'));
export function App() {
return (
<React.Suspense fallback={null}>
<div className="home">
Lib version: { version }
</div>
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/About">About</Link>
</li>
</ul>
<Routes>
<Route path="/" element={<NxWelcome title="home" />} />
<Route path="/About" element={<About />} />
</Routes>
</React.Suspense>
);
}
export default App;`
);
// Update remote to use the lib
updateFile(
`${remote}/src/app/app.tsx`,
`// eslint-disable-next-line @typescript-eslint/no-unused-vars
import styles from './app.module.css';
import { version } from '@acme/${lib}';
import NxWelcome from './nx-welcome';
export function App() {
return (
<div className='remote'>
Lib version: { version }
<NxWelcome title="${remote}" />
</div>
);
}
export default App;`
);
// update remote e2e test to check the version
updateFile(
`${remote}-e2e/src/e2e/app.cy.ts`,
`describe('${remote}', () => {
beforeEach(() => cy.visit('/'));
it('should check the lib version', () => {
cy.get('div.remote').contains('Lib version: 0.0.1');
});
});
`
);
// update shell e2e test to check the version
updateFile(
`${shell}-e2e/src/e2e/app.cy.ts`,
`
describe('${shell}', () => {
beforeEach(() => cy.visit('/'));
it('should check the lib version', () => {
cy.get('div.home').contains('Lib version: 0.0.1');
});
});
`
);
if (runE2ETests()) {
// test remote e2e
const remoteE2eResults = await runCommandUntil(
`e2e ${remote}-e2e --no-watch --verbose`,
(output) => output.includes('All specs passed!')
);
await killProcessAndPorts(remoteE2eResults.pid, remotePort);
// test shell e2e
// serve remote first
const remoteProcess = await runCommandUntil(
`serve ${remote} --no-watch --verbose`,
(output) => {
return output.includes(
`Web Development Server is listening at http://localhost:${remotePort}/`
);
}
);
await killProcessAndPorts(remoteProcess.pid, remotePort);
const shellE2eResults = await runCommandUntil(
`e2e ${shell}-e2e --no-watch --verbose`,
(output) => output.includes('All specs passed!')
);
await killProcessAndPorts(
shellE2eResults.pid,
shellPort,
shellPort + 1,
remotePort
);
}
}, 500_000);
it('should support host and remote with library type var', async () => {
const shell = uniq('shell');
const remote = uniq('remote');
runCLI(
`generate @nx/react:host ${shell} --remotes=${remote} --project-name-and-root-format=as-provided --no-interactive`
);
const shellPort = readPort(shell);
const remotePort = readPort(remote);
// update host and remote to use library type var
updateFile(
`${shell}/module-federation.config.ts`,
stripIndents`
import { ModuleFederationConfig } from '@nx/webpack';
const config: ModuleFederationConfig = {
name: '${shell}',
library: { type: 'var', name: '${shell}' },
remotes: ['${remote}'],
};
export default config;
`
);
updateFile(
`${shell}/webpack.config.prod.ts`,
`export { default } from './webpack.config';`
);
updateFile(
`${remote}/module-federation.config.ts`,
stripIndents`
import { ModuleFederationConfig } from '@nx/webpack';
const config: ModuleFederationConfig = {
name: '${remote}',
library: { type: 'var', name: '${remote}' },
exposes: {
'./Module': './src/remote-entry.ts',
},
};
export default config;
`
);
updateFile(
`${remote}/webpack.config.prod.ts`,
`export { default } from './webpack.config';`
);
// Update host e2e test to check that the remote works with library type var via navigation
updateFile(
`${shell}-e2e/src/e2e/app.cy.ts`,
`
import { getGreeting } from '../support/app.po';
describe('${shell}', () => {
beforeEach(() => cy.visit('/'));
it('should display welcome message', () => {
getGreeting().contains('Welcome ${shell}');
});
it('should navigate to /about from /', () => {
cy.get('a').contains('${remote[0].toUpperCase()}${remote.slice(
1
)}').click();
cy.url().should('include', '/${remote}');
getGreeting().contains('Welcome ${remote}');
});
});
`
);
// Build host and remote
const buildOutput = runCLI(`build ${shell}`);
const remoteOutput = runCLI(`build ${remote}`);
expect(buildOutput).toContain('Successfully ran target build');
expect(remoteOutput).toContain('Successfully ran target build');
if (runE2ETests()) {
const hostE2eResultsSwc = await runCommandUntil(
`e2e ${shell}-e2e --no-watch --verbose`,
(output) => output.includes('All specs passed!')
);
await killProcessAndPorts(
hostE2eResultsSwc.pid,
shellPort,
shellPort + 1,
remotePort
);
// Build host and remote
const buildOutput = runCLI(`build ${host}`);
const remoteOutput = runCLI(`build ${remote}`);
const remoteE2eResultsSwc = await runCommandUntil(
`e2e ${remote}-e2e --no-watch --verbose`,
(output) => output.includes('All specs passed!')
);
expect(buildOutput).toContain('Successfully ran target build');
expect(remoteOutput).toContain('Successfully ran target build');
await killProcessAndPorts(remoteE2eResultsSwc.pid, remotePort);
if (runE2ETests()) {
const remoteProcess = await runCommandUntil(
`serve-static ${remote} --no-watch --verbose`,
() => {
return true;
}
);
const hostE2eResults = runCLI(`e2e ${host}-e2e --no-watch --verbose`);
expect(hostE2eResults).toContain('All specs passed!');
const hostE2eResultsTsNode = await runCommandUntil(
`e2e ${shell}-e2e --no-watch --verbose`,
(output) => output.includes('All specs passed!'),
{ env: { NX_PREFER_TS_NODE: 'true' } }
);
remoteProcess.kill('SIGKILL');
await killProcessAndPorts(remoteProcess.pid, 4201);
}
}, 500_000);
});
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);
});
function readPort(appName: string): number {
const config = readJson(join('apps', appName, 'project.json'));
return config.targets.serve.options.port;
}
});
function readPort(appName: string): number {
let config;
try {
config = readJson(join('apps', appName, 'project.json'));
} catch {
config = readJson(join(appName, 'project.json'));
}
return config.targets.serve.options.port;
}

View File

@ -1,28 +1,242 @@
import {
logger,
readCachedProjectGraph,
readNxJson,
workspaceRoot,
} from '@nx/devkit';
import {
getModuleFederationConfig,
getRemotes,
} from '@nx/webpack/src/utils/module-federation';
import { fork } from 'child_process';
import { existsSync } from 'fs';
import { scheduleTarget } from 'nx/src/adapter/ngcli-adapter';
import { getExecutorInformation } from 'nx/src/command-line/run/executor-utils';
import { readProjectsConfigurationFromProjectGraph } from 'nx/src/project-graph/project-graph';
import { extname, join } from 'path';
import { combineLatest, concatMap, from, switchMap } from 'rxjs';
import { validateDevRemotes } from '../utilities/module-federation';
import { executeWebpackDevServerBuilder } from '../webpack-dev-server/webpack-dev-server.impl';
import type {
NormalizedSchema,
Schema,
SchemaWithBrowserTarget,
SchemaWithBuildTarget,
} from './schema';
import {
logger,
type ProjectConfiguration,
type ProjectGraph,
readCachedProjectGraph,
readNxJson,
workspaceRoot,
} from '@nx/devkit';
import { scheduleTarget } from 'nx/src/adapter/ngcli-adapter';
import { executeWebpackDevServerBuilder } from '../webpack-dev-server/webpack-dev-server.impl';
import { readProjectsConfigurationFromProjectGraph } from 'nx/src/project-graph/project-graph';
import { getExecutorInformation } from 'nx/src/command-line/run/executor-utils';
import { validateDevRemotes } from '../utilities/module-federation';
import { existsSync } from 'fs';
import { dirname, extname, join } from 'path';
import {
getModuleFederationConfig,
getRemotes,
} from '@nx/webpack/src/utils/module-federation';
import { fork } from 'child_process';
import { combineLatest, concatMap, from, switchMap } from 'rxjs';
import { cpSync } from 'fs';
function buildStaticRemotes(
remotes: {
remotePorts: any[];
staticRemotes: string[];
devRemotes: string[];
},
nxBin,
context: import('@angular-devkit/architect').BuilderContext,
options: Schema
) {
const mappedLocationOfRemotes: Record<string, string> = {};
for (const app of remotes.staticRemotes) {
mappedLocationOfRemotes[app] = `http${options.ssl ? 's' : ''}://${
options.host
}:${options.staticRemotesPort}/${app}`;
}
process.env.NX_MF_DEV_SERVER_STATIC_REMOTES = JSON.stringify(
mappedLocationOfRemotes
);
const staticRemoteBuildPromise = new Promise<void>((res) => {
logger.info(
`NX Building ${remotes.staticRemotes.length} static remotes...`
);
const staticProcess = fork(
nxBin,
[
'run-many',
`--target=build`,
`--projects=${remotes.staticRemotes.join(',')}`,
...(context.target.configuration
? [`--configuration=${context.target.configuration}`]
: []),
...(options.parallel ? [`--parallel=${options.parallel}`] : []),
],
{
cwd: context.workspaceRoot,
stdio: ['ignore', 'pipe', 'pipe', 'ipc'],
}
);
staticProcess.stdout.on('data', (data) => {
const ANSII_CODE_REGEX =
/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g;
const stdoutString = data.toString().replace(ANSII_CODE_REGEX, '');
if (stdoutString.includes('Successfully ran target build')) {
staticProcess.stdout.removeAllListeners('data');
logger.info(`NX Built ${remotes.staticRemotes.length} static remotes`);
res();
}
});
staticProcess.stderr.on('data', (data) => logger.info(data.toString()));
staticProcess.on('exit', (code) => {
if (code !== 0) {
throw new Error(`Remotes failed to build. See above for errors.`);
}
});
process.on('SIGTERM', () => staticProcess.kill('SIGTERM'));
process.on('exit', () => staticProcess.kill('SIGTERM'));
});
return staticRemoteBuildPromise;
}
function startStaticRemotesFileServer(
remotes: {
remotePorts: any[];
staticRemotes: string[];
devRemotes: string[];
},
projectGraph: ProjectGraph,
options: Schema,
context: import('@angular-devkit/architect').BuilderContext
) {
let shouldMoveToCommonLocation = false;
let commonOutputDirectory: string;
for (const app of remotes.staticRemotes) {
const outputPath =
projectGraph.nodes[app].data.targets['build'].options.outputPath;
const directoryOfOutputPath = dirname(outputPath);
if (!commonOutputDirectory) {
commonOutputDirectory = directoryOfOutputPath;
} else if (commonOutputDirectory !== directoryOfOutputPath) {
shouldMoveToCommonLocation = true;
}
}
if (shouldMoveToCommonLocation) {
commonOutputDirectory = join(workspaceRoot, 'tmp/static-remotes');
for (const app of remotes.staticRemotes) {
const outputPath =
projectGraph.nodes[app].data.targets['build'].options.outputPath;
const outputPathParts = outputPath.split('/');
cpSync(
outputPath,
join(
commonOutputDirectory,
outputPathParts[outputPathParts.length - 1]
),
{
force: true,
recursive: true,
}
);
}
}
const staticRemotesIter$ = from(
import('@nx/web/src/executors/file-server/file-server.impl')
).pipe(
switchMap((fileServerExecutor) =>
fileServerExecutor.default(
{
cors: true,
watch: false,
staticFilePath: commonOutputDirectory,
parallel: false,
spa: false,
withDeps: false,
host: options.host,
port: options.staticRemotesPort,
ssl: options.ssl,
sslCert: options.sslCert,
sslKey: options.sslKey,
},
{
projectGraph,
root: context.workspaceRoot,
target:
projectGraph.nodes[context.target.project].data.targets[
context.target.target
],
targetName: context.target.target,
projectName: context.target.project,
configurationName: context.target.configuration,
cwd: context.currentDirectory,
isVerbose: options.verbose,
projectsConfigurations:
readProjectsConfigurationFromProjectGraph(projectGraph),
nxJsonConfiguration: readNxJson(),
}
)
)
);
return staticRemotesIter$;
}
function startDevRemotes(
remotes: {
remotePorts: any[];
staticRemotes: string[];
devRemotes: string[];
},
workspaceProjects: Record<string, ProjectConfiguration>,
options: Schema,
context: import('@angular-devkit/architect').BuilderContext
) {
const devRemotes$ = [];
for (const app of remotes.devRemotes) {
if (!workspaceProjects[app].targets?.['serve']) {
throw new Error(`Could not find "serve" target in "${app}" project.`);
} else if (!workspaceProjects[app].targets?.['serve'].executor) {
throw new Error(
`Could not find executor for "serve" target in "${app}" project.`
);
}
const runOptions: { verbose?: boolean; isInitialHost?: boolean } = {};
const [collection, executor] =
workspaceProjects[app].targets['serve'].executor.split(':');
const isUsingModuleFederationDevServerExecutor = executor.includes(
'module-federation-dev-server'
);
const { schema } = getExecutorInformation(
collection,
executor,
workspaceRoot
);
if (
(options.verbose && schema.additionalProperties) ||
'verbose' in schema.properties
) {
runOptions.verbose = options.verbose;
}
if (isUsingModuleFederationDevServerExecutor) {
runOptions.isInitialHost = false;
}
const serve$ = scheduleTarget(
context.workspaceRoot,
{
project: app,
target: 'serve',
configuration: context.target.configuration,
runOptions,
},
options.verbose
).then((obs) => {
obs.toPromise().catch((err) => {
throw new Error(
`Remote '${app}' failed to serve correctly due to the following: \r\n${err.toString()}`
);
});
});
devRemotes$.push(serve$);
}
return devRemotes$;
}
export function executeModuleFederationDevServerBuilder(
schema: Schema,
@ -31,6 +245,8 @@ export function executeModuleFederationDevServerBuilder(
// Force Node to resolve to look for the nx binary that is inside node_modules
const nxBin = require.resolve('nx/bin/nx');
const options = normalizeOptions(schema);
options.staticRemotesPort ??= options.port + 1;
const projectGraph = readCachedProjectGraph();
const { projects: workspaceProjects } =
readProjectsConfigurationFromProjectGraph(projectGraph);
@ -122,150 +338,47 @@ export function executeModuleFederationDevServerBuilder(
pathToManifestFile
);
const staticRemoteBuildPromise = new Promise<void>((res) => {
logger.info(
`NX Building ${remotes.staticRemotes.length} static remotes...`
);
const staticProcess = fork(
nxBin,
[
'run-many',
`--target=build`,
`--projects=${remotes.staticRemotes.join(',')}`,
...(context.target.configuration
? [`--configuration=${context.target.configuration}`]
: []),
...(options.parallel ? [`--parallel=${options.parallel}`] : []),
],
{
cwd: context.workspaceRoot,
stdio: ['ignore', 'pipe', 'pipe', 'ipc'],
if (remotes.devRemotes.length > 0 && !schema.staticRemotesPort) {
options.staticRemotesPort = options.devRemotes.reduce((portToUse, r) => {
const remotePort =
projectGraph.nodes[r].data.targets['serve'].options.port;
if (remotePort >= portToUse) {
return remotePort + 1;
}
);
staticProcess.stdout.on('data', (data) => {
const ANSII_CODE_REGEX =
/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g;
const stdoutString = data.toString().replace(ANSII_CODE_REGEX, '');
if (stdoutString.includes('Successfully ran target build')) {
staticProcess.stdout.removeAllListeners('data');
logger.info(`NX Built ${remotes.staticRemotes.length} static remotes`);
res();
}
});
staticProcess.stderr.on('data', (data) => logger.info(data.toString()));
staticProcess.on('exit', (code) => {
if (code !== 0) {
throw new Error(`Remotes failed to build. See above for errors.`);
}
});
process.on('SIGTERM', () => staticProcess.kill('SIGTERM'));
process.on('exit', () => staticProcess.kill('SIGTERM'));
});
}, options.staticRemotesPort);
}
const staticRemoteBuildPromise = buildStaticRemotes(
remotes,
nxBin,
context,
options
);
return from(staticRemoteBuildPromise).pipe(
concatMap(() => {
let isCollectingStaticRemoteOutput = true;
const staticRemotesIter$ =
remotes.staticRemotes.length > 0
? startStaticRemotesFileServer(
remotes,
projectGraph,
options,
context
)
: from(Promise.resolve());
for (const app of remotes.staticRemotes) {
const remoteProjectServeTarget =
projectGraph.nodes[app].data.targets['serve-static'];
const isUsingModuleFederationDevServerExecutor =
remoteProjectServeTarget.executor.includes(
'module-federation-dev-server'
);
let outWithErr: null | string[] = [];
const staticProcess = fork(
nxBin,
[
'run',
`${app}:serve-static${
context.target.configuration
? `:${context.target.configuration}`
: ''
}`,
...(isUsingModuleFederationDevServerExecutor
? [`--isInitialHost=false`]
: []),
],
{
cwd: context.workspaceRoot,
stdio: ['ignore', 'pipe', 'pipe', 'ipc'],
}
);
staticProcess.stdout.on('data', (data) => {
if (isCollectingStaticRemoteOutput) {
outWithErr.push(data.toString());
} else {
outWithErr = null;
staticProcess.stdout.removeAllListeners('data');
}
});
staticProcess.stderr.on('data', (data) => logger.info(data.toString()));
staticProcess.on('exit', (code) => {
if (code !== 0) {
logger.info(outWithErr.join(''));
throw new Error(`Remote failed to start. See above for errors.`);
}
});
process.on('SIGTERM', () => staticProcess.kill('SIGTERM'));
process.on('exit', () => staticProcess.kill('SIGTERM'));
}
const devRemotes$ = [];
for (const app of remotes.devRemotes) {
if (!workspaceProjects[app].targets?.['serve']) {
throw new Error(`Could not find "serve" target in "${app}" project.`);
} else if (!workspaceProjects[app].targets?.['serve'].executor) {
throw new Error(
`Could not find executor for "serve" target in "${app}" project.`
);
}
const runOptions: { verbose?: boolean; isInitialHost?: boolean } = {};
const [collection, executor] =
workspaceProjects[app].targets['serve'].executor.split(':');
const isUsingModuleFederationDevServerExecutor = executor.includes(
'module-federation-dev-server'
);
const { schema } = getExecutorInformation(
collection,
executor,
workspaceRoot
);
if (
(options.verbose && schema.additionalProperties) ||
'verbose' in schema.properties
) {
runOptions.verbose = options.verbose;
}
if (isUsingModuleFederationDevServerExecutor) {
runOptions.isInitialHost = false;
}
const serve$ = scheduleTarget(
context.workspaceRoot,
{
project: app,
target: 'serve',
configuration: context.target.configuration,
runOptions,
},
options.verbose
).then((obs) => {
obs.toPromise().catch((err) => {
throw new Error(
`Remote '${app}' failed to serve correctly due to the following: \r\n${err.toString()}`
);
});
});
devRemotes$.push(serve$);
}
const devRemotes$ = startDevRemotes(
remotes,
workspaceProjects,
options,
context
);
return devRemotes$.length > 0
? combineLatest([...devRemotes$]).pipe(concatMap(() => currExecutor))
: currExecutor;
? combineLatest([...devRemotes$, staticRemotesIter$]).pipe(
concatMap(() => currExecutor)
)
: from(staticRemotesIter$).pipe(concatMap(() => currExecutor));
})
);
}

View File

@ -22,6 +22,7 @@ interface BaseSchema {
static?: boolean;
isInitialHost?: boolean;
parallel?: number;
staticRemotesPort?: number;
}
export type SchemaWithBrowserTarget = BaseSchema & {

View File

@ -142,6 +142,10 @@
"parallel": {
"type": "number",
"description": "Max number of parallel processes for building static remotes"
},
"staticRemotesPort": {
"type": "number",
"description": "The port at which to serve the file-server for the static remotes."
}
},
"additionalProperties": false,

View File

@ -48,6 +48,14 @@ export function getFunctionDeterminateRemoteUrl(isServer: boolean = false) {
const remoteEntry = isServer ? 'server/remoteEntry.js' : 'remoteEntry.mjs';
return function (remote: string) {
const mappedStaticRemotesFromEnv = process.env
.NX_MF_DEV_SERVER_STATIC_REMOTES
? JSON.parse(process.env.NX_MF_DEV_SERVER_STATIC_REMOTES)
: undefined;
if (mappedStaticRemotesFromEnv && mappedStaticRemotesFromEnv[remote]) {
return `${mappedStaticRemotesFromEnv[remote]}/${remoteEntry}`;
}
let remoteConfiguration = null;
try {
remoteConfiguration = readCachedProjectConfiguration(remote);

View File

@ -4,6 +4,7 @@ import {
parseTargetString,
readTargetOptions,
runExecutor,
workspaceRoot,
} from '@nx/devkit';
import devServerExecutor from '@nx/webpack/src/executors/dev-server/dev-server.impl';
import fileServerExecutor from '@nx/web/src/executors/file-server/file-server.impl';
@ -18,6 +19,8 @@ import {
} from '@nx/devkit/src/utils/async-iterable';
import { waitForPortOpen } from '@nx/web/src/utils/wait-for-port-open';
import { fork } from 'child_process';
import { dirname, join } from 'path';
import { cpSync } from 'fs';
type ModuleFederationDevServerOptions = WebDevServerOptions & {
devRemotes?: string[];
@ -25,6 +28,7 @@ type ModuleFederationDevServerOptions = WebDevServerOptions & {
static?: boolean;
isInitialHost?: boolean;
parallel?: number;
staticRemotesPort?: number;
};
function getBuildOptions(buildTarget: string, context: ExecutorContext) {
@ -37,10 +41,166 @@ function getBuildOptions(buildTarget: string, context: ExecutorContext) {
};
}
function startStaticRemotesFileServer(
remotes: {
remotePorts: any[];
staticRemotes: string[];
devRemotes: string[];
},
context: ExecutorContext,
options: ModuleFederationDevServerOptions
) {
let shouldMoveToCommonLocation = false;
let commonOutputDirectory: string;
for (const app of remotes.staticRemotes) {
const outputPath =
context.projectGraph.nodes[app].data.targets['build'].options.outputPath;
const directoryOfOutputPath = dirname(outputPath);
if (!commonOutputDirectory) {
commonOutputDirectory = directoryOfOutputPath;
} else if (commonOutputDirectory !== directoryOfOutputPath) {
shouldMoveToCommonLocation = true;
}
}
if (shouldMoveToCommonLocation) {
commonOutputDirectory = join(workspaceRoot, 'tmp/static-remotes');
for (const app of remotes.staticRemotes) {
const outputPath =
context.projectGraph.nodes[app].data.targets['build'].options
.outputPath;
cpSync(outputPath, commonOutputDirectory, {
force: true,
recursive: true,
});
}
}
const staticRemotesIter = fileServerExecutor(
{
cors: true,
watch: false,
staticFilePath: commonOutputDirectory,
parallel: false,
spa: false,
withDeps: false,
host: options.host,
port: options.staticRemotesPort,
ssl: options.ssl,
sslCert: options.sslCert,
sslKey: options.sslKey,
},
context
);
return staticRemotesIter;
}
async function startDevRemotes(
remotes: {
remotePorts: any[];
staticRemotes: string[];
devRemotes: string[];
},
context: ExecutorContext
) {
const devRemoteIters: AsyncIterable<{ success: boolean }>[] = [];
for (const app of remotes.devRemotes) {
const remoteProjectServeTarget =
context.projectGraph.nodes[app].data.targets['serve'];
const isUsingModuleFederationDevServerExecutor =
remoteProjectServeTarget.executor.includes(
'module-federation-dev-server'
);
devRemoteIters.push(
await runExecutor(
{
project: app,
target: 'serve',
configuration: context.configurationName,
},
{
watch: true,
...(isUsingModuleFederationDevServerExecutor
? { isInitialHost: false }
: {}),
},
context
)
);
}
return devRemoteIters;
}
async function buildStaticRemotes(
remotes: {
remotePorts: any[];
staticRemotes: string[];
devRemotes: string[];
},
nxBin,
context: ExecutorContext,
options: ModuleFederationDevServerOptions
) {
logger.info(`NX Building ${remotes.staticRemotes.length} static remotes...`);
const mappedLocationOfRemotes: Record<string, string> = {};
for (const app of remotes.staticRemotes) {
mappedLocationOfRemotes[app] = `http${options.ssl ? 's' : ''}://${
options.host
}:${options.staticRemotesPort}/${app}`;
}
process.env.NX_MF_DEV_SERVER_STATIC_REMOTES = JSON.stringify(
mappedLocationOfRemotes
);
await new Promise<void>((res) => {
const staticProcess = fork(
nxBin,
[
'run-many',
`--target=build`,
`--projects=${remotes.staticRemotes.join(',')}`,
...(context.configurationName
? [`--configuration=${context.configurationName}`]
: []),
...(options.parallel ? [`--parallel=${options.parallel}`] : []),
],
{
cwd: context.root,
stdio: ['ignore', 'pipe', 'pipe', 'ipc'],
}
);
staticProcess.stdout.on('data', (data) => {
const ANSII_CODE_REGEX =
/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g;
const stdoutString = data.toString().replace(ANSII_CODE_REGEX, '');
if (stdoutString.includes('Successfully ran target build')) {
staticProcess.stdout.removeAllListeners('data');
logger.info(`NX Built ${remotes.staticRemotes.length} static remotes`);
res();
}
});
staticProcess.stderr.on('data', (data) => logger.info(data.toString()));
staticProcess.on('exit', (code) => {
if (code !== 0) {
throw new Error(`Remote failed to start. See above for errors.`);
}
});
process.on('SIGTERM', () => staticProcess.kill('SIGTERM'));
process.on('exit', () => staticProcess.kill('SIGTERM'));
});
}
export default async function* moduleFederationDevServer(
options: ModuleFederationDevServerOptions,
context: ExecutorContext
): AsyncIterableIterator<{ success: boolean; baseUrl?: string }> {
const initialStaticRemotesPorts = options.staticRemotesPort;
options.staticRemotesPort ??= options.port + 1;
// Force Node to resolve to look for the nx binary that is inside node_modules
const nxBin = require.resolve('nx/bin/nx');
const currIter = options.static
@ -81,118 +241,29 @@ export default async function* moduleFederationDevServer(
}
);
logger.info(`NX Building ${remotes.staticRemotes.length} static remotes...`);
await new Promise<void>((res) => {
const staticProcess = fork(
nxBin,
[
'run-many',
`--target=build`,
`--projects=${remotes.staticRemotes.join(',')}`,
...(context.configurationName
? [`--configuration=${context.configurationName}`]
: []),
...(options.parallel ? [`--parallel=${options.parallel}`] : []),
],
{
cwd: context.root,
stdio: ['ignore', 'pipe', 'pipe', 'ipc'],
if (remotes.devRemotes.length > 0 && !initialStaticRemotesPorts) {
options.staticRemotesPort = options.devRemotes.reduce((portToUse, r) => {
const remotePort =
context.projectGraph.nodes[r].data.targets['serve'].options.port;
if (remotePort >= portToUse) {
return remotePort + 1;
}
);
staticProcess.stdout.on('data', (data) => {
const ANSII_CODE_REGEX =
/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g;
const stdoutString = data.toString().replace(ANSII_CODE_REGEX, '');
if (stdoutString.includes('Successfully ran target build')) {
staticProcess.stdout.removeAllListeners('data');
logger.info(`NX Built ${remotes.staticRemotes.length} static remotes`);
res();
}
});
staticProcess.stderr.on('data', (data) => logger.info(data.toString()));
staticProcess.on('exit', (code) => {
if (code !== 0) {
throw new Error(`Remote failed to start. See above for errors.`);
}
});
process.on('SIGTERM', () => staticProcess.kill('SIGTERM'));
process.on('exit', () => staticProcess.kill('SIGTERM'));
});
let isCollectingStaticRemoteOutput = true;
const devRemoteIters: AsyncIterable<{ success: boolean }>[] = [];
for (const app of remotes.devRemotes) {
const remoteProjectServeTarget =
context.projectGraph.nodes[app].data.targets['serve'];
const isUsingModuleFederationDevServerExecutor =
remoteProjectServeTarget.executor.includes(
'module-federation-dev-server'
);
devRemoteIters.push(
await runExecutor(
{
project: app,
target: 'serve',
configuration: context.configurationName,
},
{
watch: true,
...(isUsingModuleFederationDevServerExecutor
? { isInitialHost: false }
: {}),
},
context
)
);
}
for (const app of remotes.staticRemotes) {
const remoteProjectServeTarget =
context.projectGraph.nodes[app].data.targets['serve-static'];
const isUsingModuleFederationDevServerExecutor =
remoteProjectServeTarget.executor.includes(
'module-federation-dev-server'
);
let outWithErr: null | string[] = [];
const staticProcess = fork(
nxBin,
[
'run',
`${app}:serve-static${
context.configurationName ? `:${context.configurationName}` : ''
}`,
...(isUsingModuleFederationDevServerExecutor
? [`--isInitialHost=false`]
: []),
],
{
cwd: context.root,
stdio: ['ignore', 'pipe', 'pipe', 'ipc'],
}
);
staticProcess.stdout.on('data', (data) => {
if (isCollectingStaticRemoteOutput) {
outWithErr.push(data.toString());
} else {
outWithErr = null;
staticProcess.stdout.removeAllListeners('data');
}
});
staticProcess.stderr.on('data', (data) => logger.info(data.toString()));
staticProcess.on('exit', (code) => {
if (code !== 0) {
logger.info(outWithErr.join(''));
throw new Error(`Remote failed to start. See above for errors.`);
}
});
process.on('SIGTERM', () => staticProcess.kill('SIGTERM'));
process.on('exit', () => staticProcess.kill('SIGTERM'));
}, options.staticRemotesPort);
}
await buildStaticRemotes(remotes, nxBin, context, options);
const devRemoteIters = await startDevRemotes(remotes, context);
const staticRemotesIter =
remotes.staticRemotes.length > 0
? startStaticRemotesFileServer(remotes, context, options)
: undefined;
return yield* combineAsyncIterables(
currIter,
...devRemoteIters,
...(staticRemotesIter ? [staticRemotesIter] : []),
createAsyncIterable<{ success: true; baseUrl: string }>(
async ({ next, done }) => {
if (!options.isInitialHost) {
@ -204,10 +275,11 @@ export default async function* moduleFederationDevServer(
return;
}
try {
const portsToWaitFor = staticRemotesIter
? [options.staticRemotesPort, ...remotes.remotePorts]
: [...remotes.remotePorts];
await Promise.all(
remotes.remotePorts.map((port) =>
// Allow 20 minutes for each remote to start, which is plenty of time but we can tweak it later if needed.
// Most remotes should start in under 1 minute.
portsToWaitFor.map((port) =>
waitForPortOpen(port, {
retries: 480,
retryDelay: 2500,
@ -215,7 +287,7 @@ export default async function* moduleFederationDevServer(
})
)
);
isCollectingStaticRemoteOutput = false;
logger.info(
`NX All remotes started, server ready at http://localhost:${options.port}`
);

View File

@ -105,6 +105,10 @@
"parallel": {
"type": "number",
"description": "Max number of parallel processes for building static remotes"
},
"staticRemotesPort": {
"type": "number",
"description": "The port at which to serve the file-server for the static remotes."
}
}
}

View File

@ -21,6 +21,14 @@ export function getFunctionDeterminateRemoteUrl(isServer: boolean = false) {
const remoteEntry = isServer ? 'server/remoteEntry.js' : 'remoteEntry.js';
return function (remote: string) {
const mappedStaticRemotesFromEnv = process.env
.NX_MF_DEV_SERVER_STATIC_REMOTES
? JSON.parse(process.env.NX_MF_DEV_SERVER_STATIC_REMOTES)
: undefined;
if (mappedStaticRemotesFromEnv && mappedStaticRemotesFromEnv[remote]) {
return `${mappedStaticRemotesFromEnv[remote]}/${remoteEntry}`;
}
let remoteConfiguration = null;
try {
remoteConfiguration = readCachedProjectConfiguration(remote);

View File

@ -113,35 +113,47 @@ export default async function* fileServerExecutor(
options: Schema,
context: ExecutorContext
) {
let running = false;
const run = () => {
if (!running) {
running = true;
try {
const args = getBuildTargetCommand(options);
execFileSync(pmCmd, args, {
stdio: [0, 1, 2],
});
} catch {
throw new Error(
`Build target failed: ${chalk.bold(options.buildTarget)}`
);
} finally {
running = false;
}
}
};
let disposeWatch: () => void;
if (options.watch) {
const projectRoot =
context.projectsConfigurations.projects[context.projectName].root;
disposeWatch = await createFileWatcher(context.projectName, run);
if (!options.buildTarget && !options.staticFilePath) {
throw new Error("You must set either 'buildTarget' or 'staticFilePath'.");
}
// perform initial run
run();
if (options.watch && !options.buildTarget) {
throw new Error(
"Watch error: You can only specify 'watch' when 'buildTarget' is set."
);
}
let running = false;
let disposeWatch: () => void;
if (options.buildTarget) {
const run = () => {
if (!running) {
running = true;
try {
const args = getBuildTargetCommand(options);
execFileSync(pmCmd, args, {
stdio: [0, 1, 2],
});
} catch {
throw new Error(
`Build target failed: ${chalk.bold(options.buildTarget)}`
);
} finally {
running = false;
}
}
};
if (options.watch) {
const projectRoot =
context.projectsConfigurations.projects[context.projectName].root;
disposeWatch = await createFileWatcher(context.projectName, run);
}
// perform initial run
run();
}
const outputPath = getBuildTargetOutputPath(options, context);

View File

@ -5,7 +5,7 @@ export interface Schema {
sslKey?: string;
sslCert?: string;
proxyUrl?: string;
buildTarget: string;
buildTarget?: string;
parallel: boolean;
maxParallel?: number;
withDeps: boolean;

View File

@ -94,6 +94,5 @@
"default": -1
}
},
"additionalProperties": false,
"required": ["buildTarget"]
"additionalProperties": false
}

View File

@ -114,7 +114,7 @@ export function getRemotes(
const staticRemotes = knownRemotes.filter((r) => !devServeApps.has(r));
const devServeRemotes = knownRemotes.filter((r) => devServeApps.has(r));
const remotePorts = knownRemotes.map(
const remotePorts = devServeRemotes.map(
(r) => context.projectGraph.nodes[r].data.targets['serve'].options.port
);