feat(module-federation): use single file-server for static remotes (#20006)
This commit is contained in:
parent
e8e8f94f7a
commit
a73e9fd562
@ -132,6 +132,10 @@
|
|||||||
"parallel": {
|
"parallel": {
|
||||||
"type": "number",
|
"type": "number",
|
||||||
"description": "Max number of parallel processes for building static remotes"
|
"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,
|
"additionalProperties": false,
|
||||||
|
|||||||
@ -104,6 +104,10 @@
|
|||||||
"parallel": {
|
"parallel": {
|
||||||
"type": "number",
|
"type": "number",
|
||||||
"description": "Max number of parallel processes for building static remotes"
|
"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": []
|
"presets": []
|
||||||
|
|||||||
@ -93,7 +93,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"required": ["buildTarget"],
|
|
||||||
"presets": []
|
"presets": []
|
||||||
},
|
},
|
||||||
"description": "Serve a web application from a folder.",
|
"description": "Serve a web application from a folder.",
|
||||||
|
|||||||
@ -316,6 +316,8 @@ describe('Angular Module Federation', () => {
|
|||||||
const module = uniq('module');
|
const module = uniq('module');
|
||||||
const host = uniq('host');
|
const host = uniq('host');
|
||||||
|
|
||||||
|
const hostPort = 4200;
|
||||||
|
|
||||||
runCLI(
|
runCLI(
|
||||||
`generate @nx/angular:host ${host} --remotes=${remote} --no-interactive --projectNameAndRootFormat=as-provided`
|
`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`,
|
`e2e ${host}-e2e --no-watch --verbose`,
|
||||||
(output) => output.includes('All specs passed!')
|
(output) => output.includes('All specs passed!')
|
||||||
);
|
);
|
||||||
await killProcessAndPorts(hostE2eResults.pid);
|
await killProcessAndPorts(hostE2eResults.pid, hostPort, hostPort + 1);
|
||||||
}
|
}
|
||||||
}, 500_000);
|
}, 500_000);
|
||||||
|
|
||||||
@ -393,6 +395,7 @@ describe('Angular Module Federation', () => {
|
|||||||
const childRemote = uniq('childremote');
|
const childRemote = uniq('childremote');
|
||||||
const module = uniq('module');
|
const module = uniq('module');
|
||||||
const host = uniq('host');
|
const host = uniq('host');
|
||||||
|
const hostPort = 4200;
|
||||||
|
|
||||||
runCLI(
|
runCLI(
|
||||||
`generate @nx/angular:host ${host} --remotes=${remote} --no-interactive --projectNameAndRootFormat=as-provided`
|
`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`,
|
`e2e ${host}-e2e --no-watch --verbose`,
|
||||||
(output) => output.includes('All specs passed!')
|
(output) => output.includes('All specs passed!')
|
||||||
);
|
);
|
||||||
await killProcessAndPorts(hostE2eResults.pid);
|
await killProcessAndPorts(hostE2eResults.pid, hostPort, hostPort + 1);
|
||||||
}
|
}
|
||||||
}, 500_000);
|
}, 500_000);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -114,21 +114,33 @@ describe('React Module Federation', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (runE2ETests()) {
|
if (runE2ETests()) {
|
||||||
const e2eResultsSwc = runCLI(`e2e ${shell}-e2e --no-watch --verbose`);
|
const e2eResultsSwc = await runCommandUntil(
|
||||||
expect(e2eResultsSwc).toContain('All specs passed!');
|
`e2e ${shell}-e2e --no-watch --verbose`,
|
||||||
await killPorts(readPort(shell));
|
(output) => output.includes('All specs passed!')
|
||||||
await killPorts(readPort(remote1));
|
);
|
||||||
await killPorts(readPort(remote2));
|
|
||||||
await killPorts(readPort(remote3));
|
|
||||||
|
|
||||||
const e2eResultsTsNode = runCLI(`e2e ${shell}-e2e --no-watch --verbose`, {
|
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' },
|
env: { NX_PREFER_TS_NODE: 'true' },
|
||||||
});
|
}
|
||||||
expect(e2eResultsTsNode).toContain('All specs passed!');
|
);
|
||||||
await killPorts(readPort(shell));
|
await killProcessAndPorts(
|
||||||
await killPorts(readPort(remote1));
|
e2eResultsTsNode.pid,
|
||||||
await killPorts(readPort(remote2));
|
readPort(shell),
|
||||||
await killPorts(readPort(remote3));
|
readPort(remote1),
|
||||||
|
readPort(remote2),
|
||||||
|
readPort(remote3)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}, 500_000);
|
}, 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 () => {
|
it('should should support generating host and remote apps with the new name and root format', async () => {
|
||||||
const shell = uniq('shell');
|
const shell = uniq('shell');
|
||||||
const remote = uniq('remote');
|
const remote = uniq('remote');
|
||||||
const shellPort = 4200;
|
|
||||||
|
|
||||||
runCLI(
|
runCLI(
|
||||||
`generate @nx/react:host ${shell} --project-name-and-root-format=as-provided --no-interactive`
|
`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`
|
`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
|
// check files are generated without the layout directory ("apps/") and
|
||||||
// using the project name as the directory when no directory is provided
|
// using the project name as the directory when no directory is provided
|
||||||
checkFilesExist(`${shell}/module-federation.config.ts`);
|
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(
|
const shellProcessTsNode = await runCommandUntil(
|
||||||
`serve ${shell} --devRemotes=${remote} --verbose`,
|
`serve ${shell} --devRemotes=${remote} --verbose`,
|
||||||
@ -209,256 +228,25 @@ describe('React Module Federation', () => {
|
|||||||
env: { NX_PREFER_TS_NODE: 'true' },
|
env: { NX_PREFER_TS_NODE: 'true' },
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
await killProcessAndPorts(shellProcessTsNode.pid, shellPort);
|
await killProcessAndPorts(
|
||||||
}, 500_000);
|
shellProcessTsNode.pid,
|
||||||
|
shellPort,
|
||||||
it('should support different versions workspace libs for host and remote', async () => {
|
remotePort + 1,
|
||||||
const shell = uniq('shell');
|
remotePort
|
||||||
const remote = uniq('remote');
|
|
||||||
const lib = uniq('lib');
|
|
||||||
|
|
||||||
runCLI(
|
|
||||||
`generate @nx/react:host ${shell} --remotes=${remote} --no-interactive --projectNameAndRootFormat=as-provided`
|
|
||||||
);
|
);
|
||||||
|
|
||||||
runCLI(
|
|
||||||
`generate @nx/js:lib ${lib} --importPath=@acme/${lib} --publishable=true --no-interactive --projectNameAndRootFormat=as-provided`
|
|
||||||
);
|
|
||||||
|
|
||||||
updateFile(
|
|
||||||
`${lib}/src/lib/${lib}.ts`,
|
|
||||||
stripIndents`
|
|
||||||
export const version = '0.0.1';
|
|
||||||
`
|
|
||||||
);
|
|
||||||
|
|
||||||
updateJson(`${lib}/package.json`, (json) => {
|
|
||||||
return {
|
|
||||||
...json,
|
|
||||||
version: '0.0.1',
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update host to use the lib
|
|
||||||
updateFile(
|
|
||||||
`${shell}/src/app/app.tsx`,
|
|
||||||
`
|
|
||||||
import * as React from 'react';
|
|
||||||
|
|
||||||
import NxWelcome from './nx-welcome';
|
|
||||||
import { version } from '@acme/${lib}';
|
|
||||||
import { Link, Route, Routes } from 'react-router-dom';
|
|
||||||
|
|
||||||
const About = React.lazy(() => import('${remote}/Module'));
|
|
||||||
|
|
||||||
export function App() {
|
|
||||||
return (
|
|
||||||
<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);
|
}, 500_000);
|
||||||
|
|
||||||
// Federate Module
|
// Federate Module
|
||||||
describe('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 () => {
|
it('should federate a module from a library and update an existing remote', async () => {
|
||||||
const lib = uniq('lib');
|
const lib = uniq('lib');
|
||||||
const remote = uniq('remote');
|
const remote = uniq('remote');
|
||||||
@ -535,6 +323,9 @@ describe('React Module Federation', () => {
|
|||||||
`
|
`
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const hostPort = readPort(host);
|
||||||
|
const remotePort = readPort(remote);
|
||||||
|
|
||||||
// Build host and remote
|
// Build host and remote
|
||||||
const buildOutput = runCLI(`build ${host}`);
|
const buildOutput = runCLI(`build ${host}`);
|
||||||
const remoteOutput = runCLI(`build ${remote}`);
|
const remoteOutput = runCLI(`build ${remote}`);
|
||||||
@ -543,9 +334,16 @@ describe('React Module Federation', () => {
|
|||||||
expect(remoteOutput).toContain('Successfully ran target build');
|
expect(remoteOutput).toContain('Successfully ran target build');
|
||||||
|
|
||||||
if (runE2ETests()) {
|
if (runE2ETests()) {
|
||||||
const hostE2eResults = runCLI(`e2e ${host}-e2e --no-watch --verbose`);
|
const hostE2eResults = await runCommandUntil(
|
||||||
|
`e2e ${host}-e2e --no-watch --verbose`,
|
||||||
expect(hostE2eResults).toContain('All specs passed!');
|
(output) => output.includes('All specs passed!')
|
||||||
|
);
|
||||||
|
await killProcessAndPorts(
|
||||||
|
hostE2eResults.pid,
|
||||||
|
hostPort,
|
||||||
|
hostPort + 1,
|
||||||
|
remotePort
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}, 500_000);
|
}, 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
|
// Build host and remote
|
||||||
const buildOutput = runCLI(`build ${host}`);
|
const buildOutput = runCLI(`build ${host}`);
|
||||||
const remoteOutput = runCLI(`build ${remote}`);
|
const remoteOutput = runCLI(`build ${remote}`);
|
||||||
@ -626,13 +428,32 @@ describe('React Module Federation', () => {
|
|||||||
expect(remoteOutput).toContain('Successfully ran target build');
|
expect(remoteOutput).toContain('Successfully ran target build');
|
||||||
|
|
||||||
if (runE2ETests()) {
|
if (runE2ETests()) {
|
||||||
const hostE2eResults = runCLI(`e2e ${host}-e2e --no-watch --verbose`);
|
const hostE2eResults = await runCommandUntil(
|
||||||
|
`e2e ${host}-e2e --no-watch --verbose`,
|
||||||
expect(hostE2eResults).toContain('All specs passed!');
|
(output) => output.includes('All specs passed!')
|
||||||
|
);
|
||||||
|
await killProcessAndPorts(
|
||||||
|
hostE2eResults.pid,
|
||||||
|
hostPort,
|
||||||
|
hostPort + 1,
|
||||||
|
remotePort,
|
||||||
|
childRemotePort
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}, 500_000);
|
}, 500_000);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Independent Deployability', () => {
|
||||||
|
let proj: string;
|
||||||
|
let tree: Tree;
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
tree = createTreeWithEmptyWorkspace();
|
||||||
|
proj = newProject();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => cleanupProject());
|
||||||
|
|
||||||
describe('Promised based remotes', () => {
|
|
||||||
it('should support promised based remotes', async () => {
|
it('should support promised based remotes', async () => {
|
||||||
const remote = uniq('remote');
|
const remote = uniq('remote');
|
||||||
const host = uniq('host');
|
const host = uniq('host');
|
||||||
@ -740,6 +561,9 @@ describe('React Module Federation', () => {
|
|||||||
`
|
`
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const hostPort = readPort(host);
|
||||||
|
const remotePort = readPort(remote);
|
||||||
|
|
||||||
// Build host and remote
|
// Build host and remote
|
||||||
const buildOutput = runCLI(`build ${host}`);
|
const buildOutput = runCLI(`build ${host}`);
|
||||||
const remoteOutput = runCLI(`build ${remote}`);
|
const remoteOutput = runCLI(`build ${remote}`);
|
||||||
@ -754,18 +578,302 @@ describe('React Module Federation', () => {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
const hostE2eResults = runCLI(`e2e ${host}-e2e --no-watch --verbose`);
|
const hostE2eResults = await runCommandUntil(
|
||||||
expect(hostE2eResults).toContain('All specs passed!');
|
`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);
|
||||||
|
|
||||||
remoteProcess.kill('SIGKILL');
|
it('should support different versions workspace libs for host and remote', async () => {
|
||||||
await killProcessAndPorts(remoteProcess.pid, 4201);
|
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
|
||||||
|
);
|
||||||
|
|
||||||
|
const remoteE2eResultsSwc = await runCommandUntil(
|
||||||
|
`e2e ${remote}-e2e --no-watch --verbose`,
|
||||||
|
(output) => output.includes('All specs passed!')
|
||||||
|
);
|
||||||
|
|
||||||
|
await killProcessAndPorts(remoteE2eResultsSwc.pid, remotePort);
|
||||||
|
|
||||||
|
const hostE2eResultsTsNode = await runCommandUntil(
|
||||||
|
`e2e ${shell}-e2e --no-watch --verbose`,
|
||||||
|
(output) => output.includes('All specs passed!'),
|
||||||
|
{ env: { NX_PREFER_TS_NODE: 'true' } }
|
||||||
|
);
|
||||||
|
|
||||||
|
await killProcessAndPorts(
|
||||||
|
hostE2eResultsTsNode.pid,
|
||||||
|
shellPort,
|
||||||
|
shellPort + 1,
|
||||||
|
remotePort
|
||||||
|
);
|
||||||
|
|
||||||
|
const remoteE2eResultsTsNode = await runCommandUntil(
|
||||||
|
`e2e ${remote}-e2e --no-watch --verbose`,
|
||||||
|
(output) => output.includes('All specs passed!'),
|
||||||
|
{ env: { NX_PREFER_TS_NODE: 'true' } }
|
||||||
|
);
|
||||||
|
|
||||||
|
await killProcessAndPorts(remoteE2eResultsTsNode.pid, remotePort);
|
||||||
}
|
}
|
||||||
}, 500_000);
|
}, 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;
|
||||||
|
}
|
||||||
|
|||||||
@ -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 {
|
import type {
|
||||||
NormalizedSchema,
|
NormalizedSchema,
|
||||||
Schema,
|
Schema,
|
||||||
SchemaWithBrowserTarget,
|
SchemaWithBrowserTarget,
|
||||||
SchemaWithBuildTarget,
|
SchemaWithBuildTarget,
|
||||||
} from './schema';
|
} 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(
|
export function executeModuleFederationDevServerBuilder(
|
||||||
schema: Schema,
|
schema: Schema,
|
||||||
@ -31,6 +245,8 @@ export function executeModuleFederationDevServerBuilder(
|
|||||||
// Force Node to resolve to look for the nx binary that is inside node_modules
|
// Force Node to resolve to look for the nx binary that is inside node_modules
|
||||||
const nxBin = require.resolve('nx/bin/nx');
|
const nxBin = require.resolve('nx/bin/nx');
|
||||||
const options = normalizeOptions(schema);
|
const options = normalizeOptions(schema);
|
||||||
|
options.staticRemotesPort ??= options.port + 1;
|
||||||
|
|
||||||
const projectGraph = readCachedProjectGraph();
|
const projectGraph = readCachedProjectGraph();
|
||||||
const { projects: workspaceProjects } =
|
const { projects: workspaceProjects } =
|
||||||
readProjectsConfigurationFromProjectGraph(projectGraph);
|
readProjectsConfigurationFromProjectGraph(projectGraph);
|
||||||
@ -122,150 +338,47 @@ export function executeModuleFederationDevServerBuilder(
|
|||||||
pathToManifestFile
|
pathToManifestFile
|
||||||
);
|
);
|
||||||
|
|
||||||
const staticRemoteBuildPromise = new Promise<void>((res) => {
|
if (remotes.devRemotes.length > 0 && !schema.staticRemotesPort) {
|
||||||
logger.info(
|
options.staticRemotesPort = options.devRemotes.reduce((portToUse, r) => {
|
||||||
`NX Building ${remotes.staticRemotes.length} static remotes...`
|
const remotePort =
|
||||||
);
|
projectGraph.nodes[r].data.targets['serve'].options.port;
|
||||||
const staticProcess = fork(
|
if (remotePort >= portToUse) {
|
||||||
|
return remotePort + 1;
|
||||||
|
}
|
||||||
|
}, options.staticRemotesPort);
|
||||||
|
}
|
||||||
|
|
||||||
|
const staticRemoteBuildPromise = buildStaticRemotes(
|
||||||
|
remotes,
|
||||||
nxBin,
|
nxBin,
|
||||||
[
|
context,
|
||||||
'run-many',
|
options
|
||||||
`--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 from(staticRemoteBuildPromise).pipe(
|
return from(staticRemoteBuildPromise).pipe(
|
||||||
concatMap(() => {
|
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 devRemotes$ = startDevRemotes(
|
||||||
const remoteProjectServeTarget =
|
remotes,
|
||||||
projectGraph.nodes[app].data.targets['serve-static'];
|
workspaceProjects,
|
||||||
const isUsingModuleFederationDevServerExecutor =
|
options,
|
||||||
remoteProjectServeTarget.executor.includes(
|
context
|
||||||
'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$);
|
|
||||||
}
|
|
||||||
|
|
||||||
return devRemotes$.length > 0
|
return devRemotes$.length > 0
|
||||||
? combineLatest([...devRemotes$]).pipe(concatMap(() => currExecutor))
|
? combineLatest([...devRemotes$, staticRemotesIter$]).pipe(
|
||||||
: currExecutor;
|
concatMap(() => currExecutor)
|
||||||
|
)
|
||||||
|
: from(staticRemotesIter$).pipe(concatMap(() => currExecutor));
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,6 +22,7 @@ interface BaseSchema {
|
|||||||
static?: boolean;
|
static?: boolean;
|
||||||
isInitialHost?: boolean;
|
isInitialHost?: boolean;
|
||||||
parallel?: number;
|
parallel?: number;
|
||||||
|
staticRemotesPort?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SchemaWithBrowserTarget = BaseSchema & {
|
export type SchemaWithBrowserTarget = BaseSchema & {
|
||||||
|
|||||||
@ -142,6 +142,10 @@
|
|||||||
"parallel": {
|
"parallel": {
|
||||||
"type": "number",
|
"type": "number",
|
||||||
"description": "Max number of parallel processes for building static remotes"
|
"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,
|
"additionalProperties": false,
|
||||||
|
|||||||
@ -48,6 +48,14 @@ export function getFunctionDeterminateRemoteUrl(isServer: boolean = false) {
|
|||||||
const remoteEntry = isServer ? 'server/remoteEntry.js' : 'remoteEntry.mjs';
|
const remoteEntry = isServer ? 'server/remoteEntry.js' : 'remoteEntry.mjs';
|
||||||
|
|
||||||
return function (remote: string) {
|
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;
|
let remoteConfiguration = null;
|
||||||
try {
|
try {
|
||||||
remoteConfiguration = readCachedProjectConfiguration(remote);
|
remoteConfiguration = readCachedProjectConfiguration(remote);
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import {
|
|||||||
parseTargetString,
|
parseTargetString,
|
||||||
readTargetOptions,
|
readTargetOptions,
|
||||||
runExecutor,
|
runExecutor,
|
||||||
|
workspaceRoot,
|
||||||
} from '@nx/devkit';
|
} from '@nx/devkit';
|
||||||
import devServerExecutor from '@nx/webpack/src/executors/dev-server/dev-server.impl';
|
import devServerExecutor from '@nx/webpack/src/executors/dev-server/dev-server.impl';
|
||||||
import fileServerExecutor from '@nx/web/src/executors/file-server/file-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';
|
} from '@nx/devkit/src/utils/async-iterable';
|
||||||
import { waitForPortOpen } from '@nx/web/src/utils/wait-for-port-open';
|
import { waitForPortOpen } from '@nx/web/src/utils/wait-for-port-open';
|
||||||
import { fork } from 'child_process';
|
import { fork } from 'child_process';
|
||||||
|
import { dirname, join } from 'path';
|
||||||
|
import { cpSync } from 'fs';
|
||||||
|
|
||||||
type ModuleFederationDevServerOptions = WebDevServerOptions & {
|
type ModuleFederationDevServerOptions = WebDevServerOptions & {
|
||||||
devRemotes?: string[];
|
devRemotes?: string[];
|
||||||
@ -25,6 +28,7 @@ type ModuleFederationDevServerOptions = WebDevServerOptions & {
|
|||||||
static?: boolean;
|
static?: boolean;
|
||||||
isInitialHost?: boolean;
|
isInitialHost?: boolean;
|
||||||
parallel?: number;
|
parallel?: number;
|
||||||
|
staticRemotesPort?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
function getBuildOptions(buildTarget: string, context: ExecutorContext) {
|
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(
|
export default async function* moduleFederationDevServer(
|
||||||
options: ModuleFederationDevServerOptions,
|
options: ModuleFederationDevServerOptions,
|
||||||
context: ExecutorContext
|
context: ExecutorContext
|
||||||
): AsyncIterableIterator<{ success: boolean; baseUrl?: string }> {
|
): 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
|
// Force Node to resolve to look for the nx binary that is inside node_modules
|
||||||
const nxBin = require.resolve('nx/bin/nx');
|
const nxBin = require.resolve('nx/bin/nx');
|
||||||
const currIter = options.static
|
const currIter = options.static
|
||||||
@ -81,118 +241,29 @@ export default async function* moduleFederationDevServer(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
logger.info(`NX Building ${remotes.staticRemotes.length} static remotes...`);
|
if (remotes.devRemotes.length > 0 && !initialStaticRemotesPorts) {
|
||||||
await new Promise<void>((res) => {
|
options.staticRemotesPort = options.devRemotes.reduce((portToUse, r) => {
|
||||||
const staticProcess = fork(
|
const remotePort =
|
||||||
nxBin,
|
context.projectGraph.nodes[r].data.targets['serve'].options.port;
|
||||||
[
|
if (remotePort >= portToUse) {
|
||||||
'run-many',
|
return remotePort + 1;
|
||||||
`--target=build`,
|
|
||||||
`--projects=${remotes.staticRemotes.join(',')}`,
|
|
||||||
...(context.configurationName
|
|
||||||
? [`--configuration=${context.configurationName}`]
|
|
||||||
: []),
|
|
||||||
...(options.parallel ? [`--parallel=${options.parallel}`] : []),
|
|
||||||
],
|
|
||||||
{
|
|
||||||
cwd: context.root,
|
|
||||||
stdio: ['ignore', 'pipe', 'pipe', 'ipc'],
|
|
||||||
}
|
}
|
||||||
);
|
}, options.staticRemotesPort);
|
||||||
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;
|
await buildStaticRemotes(remotes, nxBin, context, options);
|
||||||
const devRemoteIters: AsyncIterable<{ success: boolean }>[] = [];
|
|
||||||
|
|
||||||
for (const app of remotes.devRemotes) {
|
const devRemoteIters = await startDevRemotes(remotes, context);
|
||||||
const remoteProjectServeTarget =
|
|
||||||
context.projectGraph.nodes[app].data.targets['serve'];
|
|
||||||
const isUsingModuleFederationDevServerExecutor =
|
|
||||||
remoteProjectServeTarget.executor.includes(
|
|
||||||
'module-federation-dev-server'
|
|
||||||
);
|
|
||||||
|
|
||||||
devRemoteIters.push(
|
const staticRemotesIter =
|
||||||
await runExecutor(
|
remotes.staticRemotes.length > 0
|
||||||
{
|
? startStaticRemotesFileServer(remotes, context, options)
|
||||||
project: app,
|
: undefined;
|
||||||
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'));
|
|
||||||
}
|
|
||||||
|
|
||||||
return yield* combineAsyncIterables(
|
return yield* combineAsyncIterables(
|
||||||
currIter,
|
currIter,
|
||||||
...devRemoteIters,
|
...devRemoteIters,
|
||||||
|
...(staticRemotesIter ? [staticRemotesIter] : []),
|
||||||
createAsyncIterable<{ success: true; baseUrl: string }>(
|
createAsyncIterable<{ success: true; baseUrl: string }>(
|
||||||
async ({ next, done }) => {
|
async ({ next, done }) => {
|
||||||
if (!options.isInitialHost) {
|
if (!options.isInitialHost) {
|
||||||
@ -204,10 +275,11 @@ export default async function* moduleFederationDevServer(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
const portsToWaitFor = staticRemotesIter
|
||||||
|
? [options.staticRemotesPort, ...remotes.remotePorts]
|
||||||
|
: [...remotes.remotePorts];
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
remotes.remotePorts.map((port) =>
|
portsToWaitFor.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.
|
|
||||||
waitForPortOpen(port, {
|
waitForPortOpen(port, {
|
||||||
retries: 480,
|
retries: 480,
|
||||||
retryDelay: 2500,
|
retryDelay: 2500,
|
||||||
@ -215,7 +287,7 @@ export default async function* moduleFederationDevServer(
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
isCollectingStaticRemoteOutput = false;
|
|
||||||
logger.info(
|
logger.info(
|
||||||
`NX All remotes started, server ready at http://localhost:${options.port}`
|
`NX All remotes started, server ready at http://localhost:${options.port}`
|
||||||
);
|
);
|
||||||
|
|||||||
@ -105,6 +105,10 @@
|
|||||||
"parallel": {
|
"parallel": {
|
||||||
"type": "number",
|
"type": "number",
|
||||||
"description": "Max number of parallel processes for building static remotes"
|
"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."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,6 +21,14 @@ export function getFunctionDeterminateRemoteUrl(isServer: boolean = false) {
|
|||||||
const remoteEntry = isServer ? 'server/remoteEntry.js' : 'remoteEntry.js';
|
const remoteEntry = isServer ? 'server/remoteEntry.js' : 'remoteEntry.js';
|
||||||
|
|
||||||
return function (remote: string) {
|
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;
|
let remoteConfiguration = null;
|
||||||
try {
|
try {
|
||||||
remoteConfiguration = readCachedProjectConfiguration(remote);
|
remoteConfiguration = readCachedProjectConfiguration(remote);
|
||||||
|
|||||||
@ -113,8 +113,20 @@ export default async function* fileServerExecutor(
|
|||||||
options: Schema,
|
options: Schema,
|
||||||
context: ExecutorContext
|
context: ExecutorContext
|
||||||
) {
|
) {
|
||||||
let running = false;
|
if (!options.buildTarget && !options.staticFilePath) {
|
||||||
|
throw new Error("You must set either 'buildTarget' or 'staticFilePath'.");
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = () => {
|
const run = () => {
|
||||||
if (!running) {
|
if (!running) {
|
||||||
running = true;
|
running = true;
|
||||||
@ -133,7 +145,6 @@ export default async function* fileServerExecutor(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let disposeWatch: () => void;
|
|
||||||
if (options.watch) {
|
if (options.watch) {
|
||||||
const projectRoot =
|
const projectRoot =
|
||||||
context.projectsConfigurations.projects[context.projectName].root;
|
context.projectsConfigurations.projects[context.projectName].root;
|
||||||
@ -142,6 +153,7 @@ export default async function* fileServerExecutor(
|
|||||||
|
|
||||||
// perform initial run
|
// perform initial run
|
||||||
run();
|
run();
|
||||||
|
}
|
||||||
|
|
||||||
const outputPath = getBuildTargetOutputPath(options, context);
|
const outputPath = getBuildTargetOutputPath(options, context);
|
||||||
|
|
||||||
|
|||||||
@ -5,7 +5,7 @@ export interface Schema {
|
|||||||
sslKey?: string;
|
sslKey?: string;
|
||||||
sslCert?: string;
|
sslCert?: string;
|
||||||
proxyUrl?: string;
|
proxyUrl?: string;
|
||||||
buildTarget: string;
|
buildTarget?: string;
|
||||||
parallel: boolean;
|
parallel: boolean;
|
||||||
maxParallel?: number;
|
maxParallel?: number;
|
||||||
withDeps: boolean;
|
withDeps: boolean;
|
||||||
|
|||||||
@ -94,6 +94,5 @@
|
|||||||
"default": -1
|
"default": -1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"additionalProperties": false,
|
"additionalProperties": false
|
||||||
"required": ["buildTarget"]
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -114,7 +114,7 @@ export function getRemotes(
|
|||||||
|
|
||||||
const staticRemotes = knownRemotes.filter((r) => !devServeApps.has(r));
|
const staticRemotes = knownRemotes.filter((r) => !devServeApps.has(r));
|
||||||
const devServeRemotes = 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
|
(r) => context.projectGraph.nodes[r].data.targets['serve'].options.port
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user