feat(module-federation): Update SSR enabling static serving for remotes (#27345)
<!-- Please make sure you have read the submission guidelines before posting an PR --> <!-- https://github.com/nrwl/nx/blob/master/CONTRIBUTING.md#-submitting-a-pr --> <!-- Please make sure that your commit message follows our format --> <!-- Example: `fix(nx): must begin with lowercase` --> <!-- If this is a particularly complex change or feature addition, you can request a dedicated Nx release for this pull request branch. Mention someone from the Nx team or the `@nrwl/nx-pipelines-reviewers` and they will confirm if the PR warrants its own release for testing purposes, and generate it for you if appropriate. --> ## Current Behavior <!-- This is the behavior we have today --> Currently, the default for remotes is to server them as development. Which means a separate node process for each remote that is inside the workspace. This does not scale well and can lead to out of memory exceptions. ## Expected Behavior <!-- This is the behavior we should expect with the changes in this PR --> Remotes will start as static by default, which allows for better scaling as the remotes increase. ## Related Issue(s) <!-- Please link the issue being fixed so it gets closed when this is merged. --> TODO - [ ] Migrations
This commit is contained in:
parent
225a8e019c
commit
ab162ebb54
@ -6755,6 +6755,14 @@
|
|||||||
"isExternal": false,
|
"isExternal": false,
|
||||||
"disableCollapsible": false
|
"disableCollapsible": false
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "module-federation-dev-ssr",
|
||||||
|
"path": "/nx-api/angular/executors/module-federation-dev-ssr",
|
||||||
|
"name": "module-federation-dev-ssr",
|
||||||
|
"children": [],
|
||||||
|
"isExternal": false,
|
||||||
|
"disableCollapsible": false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "application",
|
"id": "application",
|
||||||
"path": "/nx-api/angular/executors/application",
|
"path": "/nx-api/angular/executors/application",
|
||||||
@ -6794,14 +6802,6 @@
|
|||||||
"children": [],
|
"children": [],
|
||||||
"isExternal": false,
|
"isExternal": false,
|
||||||
"disableCollapsible": false
|
"disableCollapsible": false
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "module-federation-dev-ssr",
|
|
||||||
"path": "/nx-api/angular/executors/module-federation-dev-ssr",
|
|
||||||
"name": "module-federation-dev-ssr",
|
|
||||||
"children": [],
|
|
||||||
"isExternal": false,
|
|
||||||
"disableCollapsible": false
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"isExternal": false,
|
"isExternal": false,
|
||||||
|
|||||||
@ -98,6 +98,15 @@
|
|||||||
"path": "/nx-api/angular/executors/module-federation-dev-server",
|
"path": "/nx-api/angular/executors/module-federation-dev-server",
|
||||||
"type": "executor"
|
"type": "executor"
|
||||||
},
|
},
|
||||||
|
"/nx-api/angular/executors/module-federation-dev-ssr": {
|
||||||
|
"description": "The module-federation-ssr-dev-server executor is reserved exclusively for use with host SSR Module Federation applications. It allows the user to specify which remote applications should be served with the host.",
|
||||||
|
"file": "generated/packages/angular/executors/module-federation-dev-ssr.json",
|
||||||
|
"hidden": false,
|
||||||
|
"name": "module-federation-dev-ssr",
|
||||||
|
"originalFilePath": "/packages/angular/src/executors/module-federation-ssr-dev-server/schema.json",
|
||||||
|
"path": "/nx-api/angular/executors/module-federation-dev-ssr",
|
||||||
|
"type": "executor"
|
||||||
|
},
|
||||||
"/nx-api/angular/executors/application": {
|
"/nx-api/angular/executors/application": {
|
||||||
"description": "Builds an Angular application using [esbuild](https://esbuild.github.io/) with integrated SSR and prerendering capabilities. _Note: this is only supported in Angular versions >= 17.0.0_.",
|
"description": "Builds an Angular application using [esbuild](https://esbuild.github.io/) with integrated SSR and prerendering capabilities. _Note: this is only supported in Angular versions >= 17.0.0_.",
|
||||||
"file": "generated/packages/angular/executors/application.json",
|
"file": "generated/packages/angular/executors/application.json",
|
||||||
@ -142,15 +151,6 @@
|
|||||||
"originalFilePath": "/packages/angular/src/builders/webpack-server/schema.json",
|
"originalFilePath": "/packages/angular/src/builders/webpack-server/schema.json",
|
||||||
"path": "/nx-api/angular/executors/webpack-server",
|
"path": "/nx-api/angular/executors/webpack-server",
|
||||||
"type": "executor"
|
"type": "executor"
|
||||||
},
|
|
||||||
"/nx-api/angular/executors/module-federation-dev-ssr": {
|
|
||||||
"description": "Serves host [Module Federation](https://module-federation.io/) applications ([webpack](https://webpack.js.org/)-based) that use SSR allowing to specify which remote applications should be served with the host.",
|
|
||||||
"file": "generated/packages/angular/executors/module-federation-dev-ssr.json",
|
|
||||||
"hidden": false,
|
|
||||||
"name": "module-federation-dev-ssr",
|
|
||||||
"originalFilePath": "/packages/angular/src/builders/module-federation-dev-ssr/schema.json",
|
|
||||||
"path": "/nx-api/angular/executors/module-federation-dev-ssr",
|
|
||||||
"type": "executor"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"generators": {
|
"generators": {
|
||||||
|
|||||||
@ -93,6 +93,15 @@
|
|||||||
"path": "angular/executors/module-federation-dev-server",
|
"path": "angular/executors/module-federation-dev-server",
|
||||||
"type": "executor"
|
"type": "executor"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"description": "The module-federation-ssr-dev-server executor is reserved exclusively for use with host SSR Module Federation applications. It allows the user to specify which remote applications should be served with the host.",
|
||||||
|
"file": "generated/packages/angular/executors/module-federation-dev-ssr.json",
|
||||||
|
"hidden": false,
|
||||||
|
"name": "module-federation-dev-ssr",
|
||||||
|
"originalFilePath": "/packages/angular/src/executors/module-federation-ssr-dev-server/schema.json",
|
||||||
|
"path": "angular/executors/module-federation-dev-ssr",
|
||||||
|
"type": "executor"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "Builds an Angular application using [esbuild](https://esbuild.github.io/) with integrated SSR and prerendering capabilities. _Note: this is only supported in Angular versions >= 17.0.0_.",
|
"description": "Builds an Angular application using [esbuild](https://esbuild.github.io/) with integrated SSR and prerendering capabilities. _Note: this is only supported in Angular versions >= 17.0.0_.",
|
||||||
"file": "generated/packages/angular/executors/application.json",
|
"file": "generated/packages/angular/executors/application.json",
|
||||||
@ -137,15 +146,6 @@
|
|||||||
"originalFilePath": "/packages/angular/src/builders/webpack-server/schema.json",
|
"originalFilePath": "/packages/angular/src/builders/webpack-server/schema.json",
|
||||||
"path": "angular/executors/webpack-server",
|
"path": "angular/executors/webpack-server",
|
||||||
"type": "executor"
|
"type": "executor"
|
||||||
},
|
|
||||||
{
|
|
||||||
"description": "Serves host [Module Federation](https://module-federation.io/) applications ([webpack](https://webpack.js.org/)-based) that use SSR allowing to specify which remote applications should be served with the host.",
|
|
||||||
"file": "generated/packages/angular/executors/module-federation-dev-ssr.json",
|
|
||||||
"hidden": false,
|
|
||||||
"name": "module-federation-dev-ssr",
|
|
||||||
"originalFilePath": "/packages/angular/src/builders/module-federation-dev-ssr/schema.json",
|
|
||||||
"path": "angular/executors/module-federation-dev-ssr",
|
|
||||||
"type": "executor"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"generators": [
|
"generators": [
|
||||||
|
|||||||
@ -1,10 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "module-federation-dev-ssr",
|
"name": "module-federation-dev-ssr",
|
||||||
"implementation": "/packages/angular/src/builders/module-federation-dev-ssr/module-federation-dev-ssr.impl.ts",
|
"implementation": "/packages/angular/src/executors/module-federation-ssr-dev-server/module-federation-ssr-dev-server.impl.ts",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$schema": "http://json-schema.org/draft-07/schema",
|
"$schema": "http://json-schema.org/draft-07/schema",
|
||||||
"title": "Module Federation SSR Dev Server Target",
|
"title": "Module Federation SSR Dev Server Target",
|
||||||
"description": "Serves host [Module Federation](https://module-federation.io/) applications ([webpack](https://webpack.js.org/)-based) that use SSR allowing to specify which remote applications should be served with the host.",
|
"outputCapture": "direct-nodejs",
|
||||||
|
"description": "The module-federation-ssr-dev-server executor is reserved exclusively for use with host SSR Module Federation applications. It allows the user to specify which remote applications should be served with the host.",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"browserTarget": {
|
"browserTarget": {
|
||||||
@ -65,7 +66,20 @@
|
|||||||
},
|
},
|
||||||
"devRemotes": {
|
"devRemotes": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": { "type": "string" },
|
"items": {
|
||||||
|
"oneOf": [
|
||||||
|
{ "type": "string" },
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"remoteName": { "type": "string" },
|
||||||
|
"configuration": { "type": "string" }
|
||||||
|
},
|
||||||
|
"required": ["remoteName"],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"description": "List of remote applications to run in development mode (i.e. using serve target).",
|
"description": "List of remote applications to run in development mode (i.e. using serve target).",
|
||||||
"x-priority": "important"
|
"x-priority": "important"
|
||||||
},
|
},
|
||||||
@ -82,15 +96,29 @@
|
|||||||
"pathToManifestFile": {
|
"pathToManifestFile": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Path to a Module Federation manifest file (e.g. `my/path/to/module-federation.manifest.json`) containing the dynamic remote applications relative to the workspace root."
|
"description": "Path to a Module Federation manifest file (e.g. `my/path/to/module-federation.manifest.json`) containing the dynamic remote applications relative to the workspace root."
|
||||||
|
},
|
||||||
|
"isInitialHost": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Whether the host that is running this executor is the first in the project tree to do so.",
|
||||||
|
"default": true,
|
||||||
|
"x-priority": "internal"
|
||||||
|
},
|
||||||
|
"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,
|
"additionalProperties": false,
|
||||||
"required": ["browserTarget", "serverTarget"],
|
"required": ["browserTarget", "serverTarget"],
|
||||||
"presets": []
|
"presets": []
|
||||||
},
|
},
|
||||||
"description": "Serves host [Module Federation](https://module-federation.io/) applications ([webpack](https://webpack.js.org/)-based) that use SSR allowing to specify which remote applications should be served with the host.",
|
"description": "The module-federation-ssr-dev-server executor is reserved exclusively for use with host SSR Module Federation applications. It allows the user to specify which remote applications should be served with the host.",
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"hidden": false,
|
"hidden": false,
|
||||||
"path": "/packages/angular/src/builders/module-federation-dev-ssr/schema.json",
|
"path": "/packages/angular/src/executors/module-federation-ssr-dev-server/schema.json",
|
||||||
"type": "executor"
|
"type": "executor"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -41,6 +41,33 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Host to listen on.",
|
"description": "Host to listen on.",
|
||||||
"default": "localhost"
|
"default": "localhost"
|
||||||
|
},
|
||||||
|
"staticRemotesPort": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "The port at which to serve the file-server for the static remotes."
|
||||||
|
},
|
||||||
|
"pathToManifestFile": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Path to a Module Federation manifest file (e.g. `my/path/to/module-federation.manifest.json`) containing the dynamic remote applications relative to the workspace root."
|
||||||
|
},
|
||||||
|
"ssl": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Serve using HTTPS.",
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"sslKey": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "SSL key to use for serving HTTPS."
|
||||||
|
},
|
||||||
|
"sslCert": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "SSL certificate to use for serving HTTPS."
|
||||||
|
},
|
||||||
|
"isInitialHost": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Whether the host that is running this executor is the first in the project tree to do so.",
|
||||||
|
"default": true,
|
||||||
|
"x-priority": "internal"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["browserTarget", "serverTarget"],
|
"required": ["browserTarget", "serverTarget"],
|
||||||
|
|||||||
@ -327,12 +327,12 @@
|
|||||||
- [package](/nx-api/angular/executors/package)
|
- [package](/nx-api/angular/executors/package)
|
||||||
- [browser-esbuild](/nx-api/angular/executors/browser-esbuild)
|
- [browser-esbuild](/nx-api/angular/executors/browser-esbuild)
|
||||||
- [module-federation-dev-server](/nx-api/angular/executors/module-federation-dev-server)
|
- [module-federation-dev-server](/nx-api/angular/executors/module-federation-dev-server)
|
||||||
|
- [module-federation-dev-ssr](/nx-api/angular/executors/module-federation-dev-ssr)
|
||||||
- [application](/nx-api/angular/executors/application)
|
- [application](/nx-api/angular/executors/application)
|
||||||
- [extract-i18n](/nx-api/angular/executors/extract-i18n)
|
- [extract-i18n](/nx-api/angular/executors/extract-i18n)
|
||||||
- [webpack-browser](/nx-api/angular/executors/webpack-browser)
|
- [webpack-browser](/nx-api/angular/executors/webpack-browser)
|
||||||
- [dev-server](/nx-api/angular/executors/dev-server)
|
- [dev-server](/nx-api/angular/executors/dev-server)
|
||||||
- [webpack-server](/nx-api/angular/executors/webpack-server)
|
- [webpack-server](/nx-api/angular/executors/webpack-server)
|
||||||
- [module-federation-dev-ssr](/nx-api/angular/executors/module-federation-dev-ssr)
|
|
||||||
- [generators](/nx-api/angular/generators)
|
- [generators](/nx-api/angular/generators)
|
||||||
- [add-linting](/nx-api/angular/generators/add-linting)
|
- [add-linting](/nx-api/angular/generators/add-linting)
|
||||||
- [application](/nx-api/angular/generators/application)
|
- [application](/nx-api/angular/generators/application)
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { names } from '@nx/devkit';
|
|||||||
import {
|
import {
|
||||||
checkFilesExist,
|
checkFilesExist,
|
||||||
cleanupProject,
|
cleanupProject,
|
||||||
|
killPorts,
|
||||||
killProcessAndPorts,
|
killProcessAndPorts,
|
||||||
newProject,
|
newProject,
|
||||||
readJson,
|
readJson,
|
||||||
@ -230,48 +231,47 @@ describe('Angular Module Federation', () => {
|
|||||||
const remote2Port = readJson(join(remote2, 'project.json')).targets.serve
|
const remote2Port = readJson(join(remote2, 'project.json')).targets.serve
|
||||||
.options.port;
|
.options.port;
|
||||||
|
|
||||||
const processSwc = await runCommandUntil(
|
[host, remote1, remote2].forEach((app) => {
|
||||||
`serve-ssr ${host} --port=${hostPort}`,
|
checkFilesExist(
|
||||||
(output) =>
|
`${app}/module-federation.config.ts`,
|
||||||
output.includes(
|
`${app}/webpack.server.config.ts`
|
||||||
`Node Express server listening on http://localhost:${remote1Port}`
|
|
||||||
) &&
|
|
||||||
output.includes(
|
|
||||||
`Node Express server listening on http://localhost:${remote2Port}`
|
|
||||||
) &&
|
|
||||||
output.includes(
|
|
||||||
`Angular Universal Live Development Server is listening`
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
await killProcessAndPorts(
|
['build', 'server'].forEach((target) => {
|
||||||
processSwc.pid,
|
['development', 'production'].forEach(async (configuration) => {
|
||||||
hostPort,
|
const cliOutput = runCLI(`run ${app}:${target}:${configuration}`);
|
||||||
remote1Port,
|
expect(cliOutput).toContain('Successfully ran target');
|
||||||
remote2Port
|
|
||||||
);
|
|
||||||
|
|
||||||
const processTsNode = await runCommandUntil(
|
await killPorts(readPort(app));
|
||||||
`serve-ssr ${host} --port=${hostPort}`,
|
});
|
||||||
(output) =>
|
});
|
||||||
output.includes(
|
});
|
||||||
`Node Express server listening on http://localhost:${remote1Port}`
|
|
||||||
) &&
|
|
||||||
output.includes(
|
|
||||||
`Node Express server listening on http://localhost:${remote2Port}`
|
|
||||||
) &&
|
|
||||||
output.includes(
|
|
||||||
`Angular Universal Live Development Server is listening`
|
|
||||||
),
|
|
||||||
{ env: { NX_PREFER_TS_NODE: 'true' } }
|
|
||||||
);
|
|
||||||
|
|
||||||
await killProcessAndPorts(
|
const capitalize = (s: string) => s.charAt(0).toUpperCase() + s.slice(1);
|
||||||
processTsNode.pid,
|
|
||||||
hostPort,
|
updateFile(
|
||||||
remote1Port,
|
`${host}-e2e/src/example.spec.ts`,
|
||||||
remote2Port
|
(_) => `import { test, expect } from '@playwright/test';
|
||||||
|
test('renders remotes', async ({ page }) => {
|
||||||
|
await page.goto('/');
|
||||||
|
|
||||||
|
// Expect the page to contain a specific text.
|
||||||
|
// get ul li text
|
||||||
|
const items = page.locator('ul li');
|
||||||
|
|
||||||
|
await items.nth(2).waitFor()
|
||||||
|
expect(await items.count()).toEqual(3);
|
||||||
|
expect(await items.nth(0).innerText()).toContain('Home');
|
||||||
|
expect(await items.nth(1).innerText()).toContain('${capitalize(remote1)}');
|
||||||
|
expect(await items.nth(2).innerText()).toContain('${capitalize(remote2)}');
|
||||||
|
});`
|
||||||
);
|
);
|
||||||
|
if (runE2ETests()) {
|
||||||
|
const e2eProcess = await runCommandUntil(`e2e ${host}-e2e`, (output) =>
|
||||||
|
output.includes(`Successfully ran target e2e for project ${host}-e2e`)
|
||||||
|
);
|
||||||
|
await killProcessAndPorts(e2eProcess.pid);
|
||||||
|
}
|
||||||
}, 20_000_000);
|
}, 20_000_000);
|
||||||
|
|
||||||
it('should should support generating host and remote apps with --project-name-and-root-format=derived', async () => {
|
it('should should support generating host and remote apps with --project-name-and-root-format=derived', async () => {
|
||||||
@ -497,3 +497,13 @@ describe('Angular Module Federation', () => {
|
|||||||
}
|
}
|
||||||
}, 500_000);
|
}, 500_000);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function readPort(appName: string): number {
|
||||||
|
let config;
|
||||||
|
try {
|
||||||
|
config = readJson(join('apps', appName, 'project.json'));
|
||||||
|
} catch {
|
||||||
|
config = readJson(join(appName, 'project.json'));
|
||||||
|
}
|
||||||
|
return config.targets.serve.options.port;
|
||||||
|
}
|
||||||
|
|||||||
@ -22,7 +22,7 @@ import { createTreeWithEmptyWorkspace } from 'nx/src/devkit-testing-exports';
|
|||||||
describe('React Module Federation', () => {
|
describe('React Module Federation', () => {
|
||||||
describe('Default Configuration', () => {
|
describe('Default Configuration', () => {
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
newProject({ packages: ['@nx/react'] });
|
newProject({ packages: ['@nx/react', '@nx/webpack'] });
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(() => cleanupProject());
|
afterAll(() => cleanupProject());
|
||||||
@ -126,6 +126,7 @@ describe('React Module Federation', () => {
|
|||||||
500_000
|
500_000
|
||||||
);
|
);
|
||||||
|
|
||||||
|
describe('ssr', () => {
|
||||||
it('should generate host and remote apps with ssr', async () => {
|
it('should generate host and remote apps with ssr', async () => {
|
||||||
const shell = uniq('shell');
|
const shell = uniq('shell');
|
||||||
const remote1 = uniq('remote1');
|
const remote1 = uniq('remote1');
|
||||||
@ -157,6 +158,68 @@ describe('React Module Federation', () => {
|
|||||||
});
|
});
|
||||||
}, 500_000);
|
}, 500_000);
|
||||||
|
|
||||||
|
it('should serve remotes as static when running the host by default', async () => {
|
||||||
|
const shell = uniq('shell');
|
||||||
|
const remote1 = uniq('remote1');
|
||||||
|
const remote2 = uniq('remote2');
|
||||||
|
const remote3 = uniq('remote3');
|
||||||
|
|
||||||
|
await runCLIAsync(
|
||||||
|
`generate @nx/react:host ${shell} --ssr --remotes=${remote1},${remote2},${remote3} --style=css --e2eTestRunner=cypress --no-interactive --projectNameAndRootFormat=derived --skipFormat`
|
||||||
|
);
|
||||||
|
|
||||||
|
const serveResult = await runCommandUntil(`serve ${shell}`, (output) =>
|
||||||
|
output.includes(`Nx SSR Static remotes proxies started successfully`)
|
||||||
|
);
|
||||||
|
|
||||||
|
await killProcessAndPorts(serveResult.pid);
|
||||||
|
}, 500_000);
|
||||||
|
|
||||||
|
it('should serve remotes as static and they should be able to be accessed from the host', async () => {
|
||||||
|
const shell = uniq('shell');
|
||||||
|
const remote1 = uniq('remote1');
|
||||||
|
const remote2 = uniq('remote2');
|
||||||
|
const remote3 = uniq('remote3');
|
||||||
|
|
||||||
|
await runCLIAsync(
|
||||||
|
`generate @nx/react:host ${shell} --ssr --remotes=${remote1},${remote2},${remote3} --style=css --e2eTestRunner=cypress --no-interactive --projectNameAndRootFormat=derived --skipFormat`
|
||||||
|
);
|
||||||
|
|
||||||
|
const capitalize = (s: string) =>
|
||||||
|
s.charAt(0).toUpperCase() + s.slice(1);
|
||||||
|
|
||||||
|
updateFile(`apps/${shell}-e2e/src/e2e/app.cy.ts`, (content) => {
|
||||||
|
return `
|
||||||
|
describe('${shell}-e2e', () => {
|
||||||
|
beforeEach(() => cy.visit('/'));
|
||||||
|
|
||||||
|
it('should display welcome message', () => {
|
||||||
|
expect(cy.get('ul li').should('have.length', 4));
|
||||||
|
expect(cy.get('ul li').eq(0).should('have.text', 'Home'));
|
||||||
|
expect(cy.get('ul li').eq(1).should('have.text', '${capitalize(
|
||||||
|
remote1
|
||||||
|
)}'));
|
||||||
|
expect(cy.get('ul li').eq(2).should('have.text', '${capitalize(
|
||||||
|
remote2
|
||||||
|
)}'));
|
||||||
|
expect(cy.get('ul li').eq(3).should('have.text', '${capitalize(
|
||||||
|
remote3
|
||||||
|
)}'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (runE2ETests()) {
|
||||||
|
const hostE2eResults = await runCommandUntil(
|
||||||
|
`e2e ${shell}-e2e --no-watch --verbose`,
|
||||||
|
(output) => output.includes('All specs passed!')
|
||||||
|
);
|
||||||
|
await killProcessAndPorts(hostE2eResults.pid);
|
||||||
|
}
|
||||||
|
}, 600_000);
|
||||||
|
});
|
||||||
|
|
||||||
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');
|
||||||
|
|||||||
@ -25,6 +25,11 @@
|
|||||||
"schema": "./src/executors/module-federation-dev-server/schema.json",
|
"schema": "./src/executors/module-federation-dev-server/schema.json",
|
||||||
"description": "Serves host [Module Federation](https://module-federation.io/) applications ([webpack](https://webpack.js.org/)-based) allowing to specify which remote applications should be served with the host."
|
"description": "Serves host [Module Federation](https://module-federation.io/) applications ([webpack](https://webpack.js.org/)-based) allowing to specify which remote applications should be served with the host."
|
||||||
},
|
},
|
||||||
|
"module-federation-dev-ssr": {
|
||||||
|
"implementation": "./src/executors/module-federation-ssr-dev-server/module-federation-ssr-dev-server.impl",
|
||||||
|
"schema": "./src/executors/module-federation-ssr-dev-server/schema.json",
|
||||||
|
"description": "The module-federation-ssr-dev-server executor is reserved exclusively for use with host SSR Module Federation applications. It allows the user to specify which remote applications should be served with the host."
|
||||||
|
},
|
||||||
"application": {
|
"application": {
|
||||||
"implementation": "./src/executors/application/application.impl",
|
"implementation": "./src/executors/application/application.impl",
|
||||||
"schema": "./src/executors/application/schema.json",
|
"schema": "./src/executors/application/schema.json",
|
||||||
@ -51,11 +56,6 @@
|
|||||||
"implementation": "./src/builders/webpack-server/webpack-server.impl",
|
"implementation": "./src/builders/webpack-server/webpack-server.impl",
|
||||||
"schema": "./src/builders/webpack-server/schema.json",
|
"schema": "./src/builders/webpack-server/schema.json",
|
||||||
"description": "Builds a server Angular application using [webpack](https://webpack.js.org/). This executor is a drop-in replacement for the `@angular-devkit/build-angular:server` builder provided by the Angular CLI. It is usually used in tandem with the `@nx/angular:webpack-browser` executor when your Angular application uses a custom webpack configuration."
|
"description": "Builds a server Angular application using [webpack](https://webpack.js.org/). This executor is a drop-in replacement for the `@angular-devkit/build-angular:server` builder provided by the Angular CLI. It is usually used in tandem with the `@nx/angular:webpack-browser` executor when your Angular application uses a custom webpack configuration."
|
||||||
},
|
|
||||||
"module-federation-dev-ssr": {
|
|
||||||
"implementation": "./src/builders/module-federation-dev-ssr/module-federation-dev-ssr.impl",
|
|
||||||
"schema": "./src/builders/module-federation-dev-ssr/schema.json",
|
|
||||||
"description": "Serves host [Module Federation](https://module-federation.io/) applications ([webpack](https://webpack.js.org/)-based) that use SSR allowing to specify which remote applications should be served with the host."
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
export * from './src/builders/module-federation-dev-ssr/module-federation-dev-ssr.impl';
|
|
||||||
export * from './src/builders/webpack-browser/webpack-browser.impl';
|
export * from './src/builders/webpack-browser/webpack-browser.impl';
|
||||||
export * from './src/builders/webpack-server/webpack-server.impl';
|
export * from './src/builders/webpack-server/webpack-server.impl';
|
||||||
export * from './src/executors/module-federation-dev-server/module-federation-dev-server.impl';
|
export * from './src/executors/module-federation-dev-server/module-federation-dev-server.impl';
|
||||||
@ -8,6 +7,7 @@ export * from './src/executors/package/package.impl';
|
|||||||
export * from './src/executors/browser-esbuild/browser-esbuild.impl';
|
export * from './src/executors/browser-esbuild/browser-esbuild.impl';
|
||||||
export * from './src/executors/application/application.impl';
|
export * from './src/executors/application/application.impl';
|
||||||
export * from './src/executors/extract-i18n/extract-i18n.impl';
|
export * from './src/executors/extract-i18n/extract-i18n.impl';
|
||||||
|
export * from './src/executors/module-federation-ssr-dev-server/module-federation-ssr-dev-server.impl';
|
||||||
|
|
||||||
import { executeDevServerBuilder } from './src/builders/dev-server/dev-server.impl';
|
import { executeDevServerBuilder } from './src/builders/dev-server/dev-server.impl';
|
||||||
|
|
||||||
|
|||||||
@ -1,181 +0,0 @@
|
|||||||
import {
|
|
||||||
getPackageManagerCommand,
|
|
||||||
readCachedProjectGraph,
|
|
||||||
workspaceRoot,
|
|
||||||
} from '@nx/devkit';
|
|
||||||
import { execSync, 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 { from } from 'rxjs';
|
|
||||||
import { switchMap, tap } from 'rxjs/operators';
|
|
||||||
import { getInstalledAngularVersionInfo } from '../../executors/utilities/angular-version-utils';
|
|
||||||
import {
|
|
||||||
getDynamicMfManifestFile,
|
|
||||||
getDynamicRemotes,
|
|
||||||
getStaticRemotes,
|
|
||||||
validateDevRemotes,
|
|
||||||
} from '../utilities/module-federation';
|
|
||||||
import type { Schema } from './schema';
|
|
||||||
|
|
||||||
export function executeModuleFederationDevSSRBuilder(
|
|
||||||
schema: Schema,
|
|
||||||
context: import('@angular-devkit/architect').BuilderContext
|
|
||||||
) {
|
|
||||||
const { ...options } = schema;
|
|
||||||
const projectGraph = readCachedProjectGraph();
|
|
||||||
const { projects: workspaceProjects } =
|
|
||||||
readProjectsConfigurationFromProjectGraph(projectGraph);
|
|
||||||
const project = workspaceProjects[context.target.project];
|
|
||||||
|
|
||||||
let pathToManifestFile: string;
|
|
||||||
if (options.pathToManifestFile) {
|
|
||||||
const userPathToManifestFile = join(
|
|
||||||
context.workspaceRoot,
|
|
||||||
options.pathToManifestFile
|
|
||||||
);
|
|
||||||
if (!existsSync(userPathToManifestFile)) {
|
|
||||||
throw new Error(
|
|
||||||
`The provided Module Federation manifest file path does not exist. Please check the file exists at "${userPathToManifestFile}".`
|
|
||||||
);
|
|
||||||
} else if (extname(options.pathToManifestFile) !== '.json') {
|
|
||||||
throw new Error(
|
|
||||||
`The Module Federation manifest file must be a JSON. Please ensure the file at ${userPathToManifestFile} is a JSON.`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
pathToManifestFile = userPathToManifestFile;
|
|
||||||
} else {
|
|
||||||
pathToManifestFile = getDynamicMfManifestFile(
|
|
||||||
project,
|
|
||||||
context.workspaceRoot
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const devServeRemotes = !options.devRemotes
|
|
||||||
? []
|
|
||||||
: Array.isArray(options.devRemotes)
|
|
||||||
? options.devRemotes
|
|
||||||
: [options.devRemotes];
|
|
||||||
|
|
||||||
// Set NX_MF_DEV_REMOTES for the Nx Runtime Library Control Plugin
|
|
||||||
process.env.NX_MF_DEV_REMOTES = JSON.stringify(devServeRemotes);
|
|
||||||
|
|
||||||
validateDevRemotes({ devRemotes: devServeRemotes }, workspaceProjects);
|
|
||||||
|
|
||||||
const remotesToSkip = new Set(options.skipRemotes ?? []);
|
|
||||||
const staticRemotes = getStaticRemotes(
|
|
||||||
project,
|
|
||||||
context,
|
|
||||||
workspaceProjects,
|
|
||||||
remotesToSkip
|
|
||||||
);
|
|
||||||
const dynamicRemotes = getDynamicRemotes(
|
|
||||||
project,
|
|
||||||
context,
|
|
||||||
workspaceProjects,
|
|
||||||
remotesToSkip,
|
|
||||||
pathToManifestFile
|
|
||||||
);
|
|
||||||
const remotes = [...staticRemotes, ...dynamicRemotes];
|
|
||||||
|
|
||||||
const remoteProcessPromises = [];
|
|
||||||
for (const remote of remotes) {
|
|
||||||
const isDev = devServeRemotes.includes(remote);
|
|
||||||
const target = isDev ? 'serve-ssr' : 'static-server';
|
|
||||||
|
|
||||||
if (!workspaceProjects[remote].targets?.[target]) {
|
|
||||||
throw new Error(
|
|
||||||
`Could not find "${target}" target in "${remote}" project.`
|
|
||||||
);
|
|
||||||
} else if (!workspaceProjects[remote].targets?.[target].executor) {
|
|
||||||
throw new Error(
|
|
||||||
`Could not find executor for "${target}" target in "${remote}" project.`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const runOptions: { verbose?: boolean } = {};
|
|
||||||
if (options.verbose) {
|
|
||||||
const [collection, executor] =
|
|
||||||
workspaceProjects[remote].targets[target].executor.split(':');
|
|
||||||
const { schema } = getExecutorInformation(
|
|
||||||
collection,
|
|
||||||
executor,
|
|
||||||
workspaceRoot,
|
|
||||||
workspaceProjects
|
|
||||||
);
|
|
||||||
|
|
||||||
if (schema.additionalProperties || 'verbose' in schema.properties) {
|
|
||||||
runOptions.verbose = options.verbose;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const remotePromise = new Promise<void>((res, rej) => {
|
|
||||||
if (target === 'static-server') {
|
|
||||||
const remoteProject = workspaceProjects[remote];
|
|
||||||
const remoteServerOutput = join(
|
|
||||||
workspaceRoot,
|
|
||||||
remoteProject.targets.server.options.outputPath,
|
|
||||||
'main.js'
|
|
||||||
);
|
|
||||||
const pm = getPackageManagerCommand();
|
|
||||||
execSync(
|
|
||||||
`${pm.exec} nx run ${remote}:server${
|
|
||||||
context.target.configuration
|
|
||||||
? `:${context.target.configuration}`
|
|
||||||
: ''
|
|
||||||
}`,
|
|
||||||
{ stdio: 'inherit' }
|
|
||||||
);
|
|
||||||
const child = fork(remoteServerOutput, {
|
|
||||||
env: { PORT: remoteProject.targets['serve-ssr'].options.port },
|
|
||||||
});
|
|
||||||
child.on('message', (msg) => {
|
|
||||||
if (msg === 'nx.server.ready') {
|
|
||||||
res();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (target === 'serve-ssr') {
|
|
||||||
scheduleTarget(
|
|
||||||
context.workspaceRoot,
|
|
||||||
{
|
|
||||||
project: remote,
|
|
||||||
target,
|
|
||||||
configuration: context.target.configuration,
|
|
||||||
runOptions,
|
|
||||||
projects: workspaceProjects,
|
|
||||||
},
|
|
||||||
options.verbose
|
|
||||||
).then((obs) =>
|
|
||||||
obs
|
|
||||||
.pipe(
|
|
||||||
tap((result) => {
|
|
||||||
result.success && res();
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.toPromise()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
remoteProcessPromises.push(remotePromise);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { major: angularMajorVersion } = getInstalledAngularVersionInfo();
|
|
||||||
const { executeSSRDevServerBuilder } =
|
|
||||||
angularMajorVersion >= 17
|
|
||||||
? require('@angular-devkit/build-angular')
|
|
||||||
: require('@nguniversal/builders');
|
|
||||||
|
|
||||||
return from(Promise.all(remoteProcessPromises)).pipe(
|
|
||||||
switchMap(() => executeSSRDevServerBuilder(options, context))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default require('@angular-devkit/architect').createBuilder(
|
|
||||||
executeModuleFederationDevSSRBuilder
|
|
||||||
);
|
|
||||||
@ -0,0 +1,91 @@
|
|||||||
|
import type { Schema } from '../schema';
|
||||||
|
import { type ExecutorContext, logger } from '@nx/devkit';
|
||||||
|
import { workspaceDataDirectory } from 'nx/src/utils/cache-directory';
|
||||||
|
import { fork } from 'node:child_process';
|
||||||
|
import { join } from 'node:path';
|
||||||
|
import { createWriteStream } from 'node:fs';
|
||||||
|
import type { StaticRemotesConfig } from '@nx/webpack/src/utils/module-federation/parse-static-remotes-config';
|
||||||
|
|
||||||
|
export async function buildStaticRemotes(
|
||||||
|
staticRemotesConfig: StaticRemotesConfig,
|
||||||
|
nxBin,
|
||||||
|
context: ExecutorContext,
|
||||||
|
options: Schema
|
||||||
|
) {
|
||||||
|
if (!staticRemotesConfig.remotes.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const mappedLocationOfRemotes: Record<string, string> = {};
|
||||||
|
for (const app of staticRemotesConfig.remotes) {
|
||||||
|
mappedLocationOfRemotes[app] = `http${options.ssl ? 's' : ''}://${
|
||||||
|
options.host
|
||||||
|
}:${options.staticRemotesPort}/${
|
||||||
|
staticRemotesConfig.config[app].urlSegment
|
||||||
|
}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
logger.info(
|
||||||
|
`NX Building ${staticRemotesConfig.remotes.length} static remotes...`
|
||||||
|
);
|
||||||
|
const staticProcess = fork(
|
||||||
|
nxBin,
|
||||||
|
[
|
||||||
|
'run-many',
|
||||||
|
`--target=server`,
|
||||||
|
`--projects=${staticRemotesConfig.remotes.join(',')}`,
|
||||||
|
...(context.configurationName
|
||||||
|
? [`--configuration=${context.configurationName}`]
|
||||||
|
: []),
|
||||||
|
...(options.parallel ? [`--parallel=${options.parallel}`] : []),
|
||||||
|
],
|
||||||
|
{
|
||||||
|
cwd: context.root,
|
||||||
|
stdio: ['ignore', 'pipe', 'pipe', 'ipc'],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
// File to debug build failures e.g. 2024-01-01T00_00_0_0Z-build.log'
|
||||||
|
const remoteBuildLogFile = join(
|
||||||
|
workspaceDataDirectory,
|
||||||
|
`${new Date().toISOString().replace(/[:\.]/g, '_')}-build.log`
|
||||||
|
);
|
||||||
|
const stdoutStream = createWriteStream(remoteBuildLogFile);
|
||||||
|
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, '');
|
||||||
|
stdoutStream.write(stdoutString);
|
||||||
|
|
||||||
|
// in addition to writing into the stdout stream, also show error directly in console
|
||||||
|
// so the error is easily discoverable. 'ERROR in' is the key word to search in webpack output.
|
||||||
|
if (stdoutString.includes('ERROR in')) {
|
||||||
|
logger.log(stdoutString);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stdoutString.includes('Successfully ran target server')) {
|
||||||
|
staticProcess.stdout.removeAllListeners('data');
|
||||||
|
logger.info(
|
||||||
|
`NX Built ${staticRemotesConfig.remotes.length} static remotes`
|
||||||
|
);
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
staticProcess.stderr.on('data', (data) => logger.info(data.toString()));
|
||||||
|
staticProcess.once('exit', (code) => {
|
||||||
|
stdoutStream.end();
|
||||||
|
staticProcess.stdout.removeAllListeners('data');
|
||||||
|
staticProcess.stderr.removeAllListeners('data');
|
||||||
|
if (code !== 0) {
|
||||||
|
reject(
|
||||||
|
`Remote failed to start. A complete log can be found in: ${remoteBuildLogFile}`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
process.on('SIGTERM', () => staticProcess.kill('SIGTERM'));
|
||||||
|
process.on('exit', () => staticProcess.kill('SIGTERM'));
|
||||||
|
});
|
||||||
|
|
||||||
|
return mappedLocationOfRemotes;
|
||||||
|
}
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
import { workspaceRoot } from '@nx/devkit';
|
||||||
|
import type { Schema } from '../schema';
|
||||||
|
import { join } from 'path';
|
||||||
|
|
||||||
|
export function normalizeOptions(options: Schema): Schema {
|
||||||
|
const devServeRemotes = !options.devRemotes
|
||||||
|
? []
|
||||||
|
: Array.isArray(options.devRemotes)
|
||||||
|
? options.devRemotes
|
||||||
|
: [options.devRemotes];
|
||||||
|
|
||||||
|
return {
|
||||||
|
...options,
|
||||||
|
devRemotes: devServeRemotes,
|
||||||
|
ssl: options.ssl ?? false,
|
||||||
|
sslCert: options.sslCert ? join(workspaceRoot, options.sslCert) : undefined,
|
||||||
|
sslKey: options.sslKey ? join(workspaceRoot, options.sslKey) : undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -0,0 +1,58 @@
|
|||||||
|
import { type Schema } from '../schema';
|
||||||
|
import {
|
||||||
|
type ExecutorContext,
|
||||||
|
type ProjectConfiguration,
|
||||||
|
runExecutor,
|
||||||
|
} from '@nx/devkit';
|
||||||
|
|
||||||
|
export async function startRemotes(
|
||||||
|
remotes: string[],
|
||||||
|
workspaceProjects: Record<string, ProjectConfiguration>,
|
||||||
|
options: Schema,
|
||||||
|
context: ExecutorContext
|
||||||
|
) {
|
||||||
|
const target = 'serve-ssr';
|
||||||
|
const remoteIters: AsyncIterable<{ success: boolean }>[] = [];
|
||||||
|
for (const app of remotes) {
|
||||||
|
if (!workspaceProjects[app].targets?.[target]) {
|
||||||
|
throw new Error(`Could not find "${target}" target in "${app}" project.`);
|
||||||
|
} else if (!workspaceProjects[app].targets?.[target].executor) {
|
||||||
|
throw new Error(
|
||||||
|
`Could not find executor for "${target}" target in "${app}" project.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [_, executor] =
|
||||||
|
workspaceProjects[app].targets[target].executor.split(':');
|
||||||
|
const isUsingModuleFederationSsrDevServerExecutor = executor.includes(
|
||||||
|
'module-federation-dev-ssr'
|
||||||
|
);
|
||||||
|
|
||||||
|
const configurationOverride = options.devRemotes.find(
|
||||||
|
(
|
||||||
|
r
|
||||||
|
): r is {
|
||||||
|
remoteName: string;
|
||||||
|
configuration: string;
|
||||||
|
} => typeof r !== 'string' && r.remoteName === app
|
||||||
|
)?.configuration;
|
||||||
|
|
||||||
|
remoteIters.push(
|
||||||
|
await runExecutor(
|
||||||
|
{
|
||||||
|
project: app,
|
||||||
|
target,
|
||||||
|
configuration: configurationOverride ?? context.configurationName,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...{ verbose: options.verbose ?? false },
|
||||||
|
...(isUsingModuleFederationSsrDevServerExecutor
|
||||||
|
? { isInitialHost: false }
|
||||||
|
: {}),
|
||||||
|
},
|
||||||
|
context
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return remoteIters;
|
||||||
|
}
|
||||||
@ -0,0 +1,50 @@
|
|||||||
|
import { type ExecutorContext, workspaceRoot } from '@nx/devkit';
|
||||||
|
import { type Schema } from '../schema';
|
||||||
|
import fileServerExecutor from '@nx/web/src/executors/file-server/file-server.impl';
|
||||||
|
import { join } from 'path';
|
||||||
|
import { cpSync, rmSync } from 'fs';
|
||||||
|
import type { StaticRemotesConfig } from '@nx/webpack/src/utils/module-federation/parse-static-remotes-config';
|
||||||
|
|
||||||
|
export function startStaticRemotes(
|
||||||
|
ssrStaticRemotesConfig: StaticRemotesConfig,
|
||||||
|
context: ExecutorContext,
|
||||||
|
options: Schema
|
||||||
|
) {
|
||||||
|
if (ssrStaticRemotesConfig.remotes.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The directories are usually generated with /browser and /server suffixes so we need to copy them to a common directory
|
||||||
|
const commonOutputDirectory = join(workspaceRoot, 'tmp/static-remotes');
|
||||||
|
for (const app of ssrStaticRemotesConfig.remotes) {
|
||||||
|
const remoteConfig = ssrStaticRemotesConfig.config[app];
|
||||||
|
|
||||||
|
cpSync(
|
||||||
|
remoteConfig.outputPath,
|
||||||
|
join(commonOutputDirectory, remoteConfig.urlSegment),
|
||||||
|
{
|
||||||
|
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,
|
||||||
|
cacheSeconds: -1,
|
||||||
|
},
|
||||||
|
context
|
||||||
|
);
|
||||||
|
return staticRemotesIter;
|
||||||
|
}
|
||||||
@ -0,0 +1,196 @@
|
|||||||
|
import { type ExecutorContext, logger } from '@nx/devkit';
|
||||||
|
import { existsSync } from 'fs';
|
||||||
|
import { readProjectsConfigurationFromProjectGraph } from 'nx/src/project-graph/project-graph';
|
||||||
|
import { extname, join } from 'path';
|
||||||
|
import {
|
||||||
|
getDynamicMfManifestFile,
|
||||||
|
validateDevRemotes,
|
||||||
|
} from '../../builders/utilities/module-federation';
|
||||||
|
import type { Schema } from './schema';
|
||||||
|
import {
|
||||||
|
getModuleFederationConfig,
|
||||||
|
getRemotes,
|
||||||
|
} from '@nx/webpack/src/utils/module-federation';
|
||||||
|
import { parseStaticSsrRemotesConfig } from '@nx/webpack/src/utils/module-federation/parse-static-remotes-config';
|
||||||
|
import { buildStaticRemotes } from './lib/build-static-remotes';
|
||||||
|
import { startRemotes } from './lib/start-dev-remotes';
|
||||||
|
import { startStaticRemotes } from './lib/start-static-remotes';
|
||||||
|
import {
|
||||||
|
combineAsyncIterables,
|
||||||
|
createAsyncIterable,
|
||||||
|
mapAsyncIterable,
|
||||||
|
} from '@nx/devkit/src/utils/async-iterable';
|
||||||
|
import { eachValueFrom } from '@nx/devkit/src/utils/rxjs-for-await';
|
||||||
|
import { createBuilderContext } from 'nx/src/adapter/ngcli-adapter';
|
||||||
|
import { normalizeOptions } from './lib/normalize-options';
|
||||||
|
import { waitForPortOpen } from '@nx/web/src/utils/wait-for-port-open';
|
||||||
|
import { startSsrRemoteProxies } from '@nx/webpack/src/utils/module-federation/start-ssr-remote-proxies';
|
||||||
|
import { executeDevServerBuilder } from '../../builders/dev-server/dev-server.impl';
|
||||||
|
|
||||||
|
export async function* moduleFederationSsrDevServerExecutor(
|
||||||
|
schema: Schema,
|
||||||
|
context: ExecutorContext
|
||||||
|
) {
|
||||||
|
const nxBin = require.resolve('nx/bin/nx');
|
||||||
|
const options = normalizeOptions(schema);
|
||||||
|
|
||||||
|
const currIter = eachValueFrom(
|
||||||
|
executeDevServerBuilder(
|
||||||
|
options,
|
||||||
|
await createBuilderContext(
|
||||||
|
{
|
||||||
|
builderName: '@nx/angular:webpack-server',
|
||||||
|
description: 'Build a ssr application',
|
||||||
|
optionSchema: require('../../builders/webpack-server/schema.json'),
|
||||||
|
},
|
||||||
|
context
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (options.isInitialHost === false) {
|
||||||
|
return yield* currIter;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { projects: workspaceProjects } =
|
||||||
|
readProjectsConfigurationFromProjectGraph(context.projectGraph);
|
||||||
|
const project = workspaceProjects[context.projectName];
|
||||||
|
|
||||||
|
let pathToManifestFile: string;
|
||||||
|
if (options.pathToManifestFile) {
|
||||||
|
const userPathToManifestFile = join(
|
||||||
|
context.root,
|
||||||
|
options.pathToManifestFile
|
||||||
|
);
|
||||||
|
if (!existsSync(userPathToManifestFile)) {
|
||||||
|
throw new Error(
|
||||||
|
`The provided Module Federation manifest file path does not exist. Please check the file exists at "${userPathToManifestFile}".`
|
||||||
|
);
|
||||||
|
} else if (extname(options.pathToManifestFile) !== '.json') {
|
||||||
|
throw new Error(
|
||||||
|
`The Module Federation manifest file must be a JSON. Please ensure the file at ${userPathToManifestFile} is a JSON.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pathToManifestFile = userPathToManifestFile;
|
||||||
|
} else {
|
||||||
|
pathToManifestFile = getDynamicMfManifestFile(project, context.root);
|
||||||
|
}
|
||||||
|
|
||||||
|
validateDevRemotes({ devRemotes: options.devRemotes }, workspaceProjects);
|
||||||
|
|
||||||
|
const moduleFederationConfig = getModuleFederationConfig(
|
||||||
|
project.targets.build.options.tsConfig,
|
||||||
|
context.root,
|
||||||
|
project.root,
|
||||||
|
'angular'
|
||||||
|
);
|
||||||
|
|
||||||
|
const remoteNames = options.devRemotes.map((r) =>
|
||||||
|
typeof r === 'string' ? r : r.remoteName
|
||||||
|
);
|
||||||
|
|
||||||
|
const remotes = getRemotes(
|
||||||
|
remoteNames,
|
||||||
|
options.skipRemotes,
|
||||||
|
moduleFederationConfig,
|
||||||
|
{
|
||||||
|
projectName: project.name,
|
||||||
|
projectGraph: context.projectGraph,
|
||||||
|
root: context.root,
|
||||||
|
},
|
||||||
|
pathToManifestFile
|
||||||
|
);
|
||||||
|
|
||||||
|
options.staticRemotesPort ??= remotes.staticRemotePort;
|
||||||
|
|
||||||
|
const staticRemotesConfig = parseStaticSsrRemotesConfig(
|
||||||
|
[...remotes.staticRemotes, ...remotes.dynamicRemotes],
|
||||||
|
context
|
||||||
|
);
|
||||||
|
|
||||||
|
const mappedLocationsOfStaticRemotes = await buildStaticRemotes(
|
||||||
|
staticRemotesConfig,
|
||||||
|
nxBin,
|
||||||
|
context,
|
||||||
|
options
|
||||||
|
);
|
||||||
|
|
||||||
|
// Set NX_MF_DEV_REMOTES for the Nx Runtime Library Control Plugin
|
||||||
|
process.env.NX_MF_DEV_REMOTES = JSON.stringify(options.devRemotes);
|
||||||
|
|
||||||
|
const devRemotes = await startRemotes(
|
||||||
|
remotes.devRemotes,
|
||||||
|
workspaceProjects,
|
||||||
|
options,
|
||||||
|
context
|
||||||
|
);
|
||||||
|
|
||||||
|
const staticRemotes = startStaticRemotes(
|
||||||
|
staticRemotesConfig,
|
||||||
|
context,
|
||||||
|
options
|
||||||
|
);
|
||||||
|
|
||||||
|
startSsrRemoteProxies(
|
||||||
|
staticRemotesConfig,
|
||||||
|
mappedLocationsOfStaticRemotes,
|
||||||
|
options.ssl
|
||||||
|
? { pathToCert: options.sslCert, pathToKey: options.sslKey }
|
||||||
|
: undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
const removeBaseUrlEmission = (iter: AsyncIterable<unknown>) =>
|
||||||
|
mapAsyncIterable(iter, (v) => ({
|
||||||
|
...v,
|
||||||
|
baseUrl: undefined,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return yield* combineAsyncIterables(
|
||||||
|
removeBaseUrlEmission(currIter),
|
||||||
|
...devRemotes.map(removeBaseUrlEmission),
|
||||||
|
...(staticRemotes ? [removeBaseUrlEmission(staticRemotes)] : []),
|
||||||
|
createAsyncIterable<{ success: true; baseUrl: string }>(
|
||||||
|
async ({ next, done }) => {
|
||||||
|
if (!options.isInitialHost) {
|
||||||
|
done();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (remotes.remotePorts.length) {
|
||||||
|
logger.info(
|
||||||
|
`Nx All remotes started, server ready at http://localhost:${options.port}`
|
||||||
|
);
|
||||||
|
|
||||||
|
next({ success: true, baseUrl: `http://localhost:${options.port}` });
|
||||||
|
done();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const portsToWaitFor = staticRemotes
|
||||||
|
? [options.staticRemotesPort, ...remotes.remotePorts]
|
||||||
|
: [...remotes.remotePorts];
|
||||||
|
await Promise.all(
|
||||||
|
portsToWaitFor.map((port) =>
|
||||||
|
waitForPortOpen(port, {
|
||||||
|
retries: 480,
|
||||||
|
retryDelay: 2500,
|
||||||
|
host: 'localhost',
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(
|
||||||
|
`Failed to start remotes. Check above for any errors.`,
|
||||||
|
{
|
||||||
|
cause: error,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default moduleFederationSsrDevServerExecutor;
|
||||||
@ -1,3 +1,5 @@
|
|||||||
|
import { type DevRemoteDefinition } from '../../builders/utilities/module-federation';
|
||||||
|
|
||||||
export interface Schema {
|
export interface Schema {
|
||||||
browserTarget: string;
|
browserTarget: string;
|
||||||
serverTarget: string;
|
serverTarget: string;
|
||||||
@ -10,8 +12,12 @@ export interface Schema {
|
|||||||
sslKey?: string;
|
sslKey?: string;
|
||||||
sslCert?: string;
|
sslCert?: string;
|
||||||
proxyConfig?: string;
|
proxyConfig?: string;
|
||||||
devRemotes?: string[];
|
devRemotes?: DevRemoteDefinition[];
|
||||||
skipRemotes?: string[];
|
skipRemotes?: string[];
|
||||||
verbose: boolean;
|
verbose: boolean;
|
||||||
pathToManifestFile?: string;
|
pathToManifestFile?: string;
|
||||||
|
parallel?: number;
|
||||||
|
staticRemotesPort?: number;
|
||||||
|
parallel?: number;
|
||||||
|
isInitialHost?: boolean;
|
||||||
}
|
}
|
||||||
@ -1,7 +1,8 @@
|
|||||||
{
|
{
|
||||||
"$schema": "http://json-schema.org/draft-07/schema",
|
"$schema": "http://json-schema.org/draft-07/schema",
|
||||||
"title": "Module Federation SSR Dev Server Target",
|
"title": "Module Federation SSR Dev Server Target",
|
||||||
"description": "Serves host [Module Federation](https://module-federation.io/) applications ([webpack](https://webpack.js.org/)-based) that use SSR allowing to specify which remote applications should be served with the host.",
|
"outputCapture": "direct-nodejs",
|
||||||
|
"description": "The module-federation-ssr-dev-server executor is reserved exclusively for use with host SSR Module Federation applications. It allows the user to specify which remote applications should be served with the host.",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"browserTarget": {
|
"browserTarget": {
|
||||||
@ -63,8 +64,25 @@
|
|||||||
"devRemotes": {
|
"devRemotes": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"remoteName": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"configuration": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["remoteName"],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"description": "List of remote applications to run in development mode (i.e. using serve target).",
|
"description": "List of remote applications to run in development mode (i.e. using serve target).",
|
||||||
"x-priority": "important"
|
"x-priority": "important"
|
||||||
},
|
},
|
||||||
@ -83,6 +101,20 @@
|
|||||||
"pathToManifestFile": {
|
"pathToManifestFile": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Path to a Module Federation manifest file (e.g. `my/path/to/module-federation.manifest.json`) containing the dynamic remote applications relative to the workspace root."
|
"description": "Path to a Module Federation manifest file (e.g. `my/path/to/module-federation.manifest.json`) containing the dynamic remote applications relative to the workspace root."
|
||||||
|
},
|
||||||
|
"isInitialHost": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Whether the host that is running this executor is the first in the project tree to do so.",
|
||||||
|
"default": true,
|
||||||
|
"x-priority": "internal"
|
||||||
|
},
|
||||||
|
"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,
|
"additionalProperties": false,
|
||||||
@ -20,10 +20,8 @@ import {
|
|||||||
import { waitForPortOpen } from '@nx/web/src/utils/wait-for-port-open';
|
import { waitForPortOpen } from '@nx/web/src/utils/wait-for-port-open';
|
||||||
import { workspaceDataDirectory } from 'nx/src/utils/cache-directory';
|
import { workspaceDataDirectory } from 'nx/src/utils/cache-directory';
|
||||||
import { fork } from 'node:child_process';
|
import { fork } from 'node:child_process';
|
||||||
import { join } from 'node:path';
|
import { cpSync, existsSync, createWriteStream } from 'fs';
|
||||||
import { cpSync, createWriteStream } from 'node:fs';
|
import { join, extname } from 'path';
|
||||||
import { existsSync } from 'fs';
|
|
||||||
import { extname } from 'path';
|
|
||||||
import { startRemoteProxies } from '@nx/webpack/src/utils/module-federation/start-remote-proxies';
|
import { startRemoteProxies } from '@nx/webpack/src/utils/module-federation/start-remote-proxies';
|
||||||
import {
|
import {
|
||||||
parseStaticRemotesConfig,
|
parseStaticRemotesConfig,
|
||||||
@ -111,6 +109,7 @@ function startStaticRemotesFileServer(
|
|||||||
},
|
},
|
||||||
context
|
context
|
||||||
);
|
);
|
||||||
|
|
||||||
return staticRemotesIter;
|
return staticRemotesIter;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -193,10 +192,6 @@ async function buildStaticRemotes(
|
|||||||
}`;
|
}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
process.env.NX_MF_DEV_SERVER_STATIC_REMOTES = JSON.stringify(
|
|
||||||
mappedLocationOfRemotes
|
|
||||||
);
|
|
||||||
|
|
||||||
await new Promise<void>((res, rej) => {
|
await new Promise<void>((res, rej) => {
|
||||||
const staticProcess = fork(
|
const staticProcess = fork(
|
||||||
nxBin,
|
nxBin,
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
ExecutorContext,
|
ExecutorContext,
|
||||||
getPackageManagerCommand,
|
|
||||||
logger,
|
logger,
|
||||||
parseTargetString,
|
parseTargetString,
|
||||||
readTargetOptions,
|
readTargetOptions,
|
||||||
@ -9,24 +8,60 @@ import {
|
|||||||
} from '@nx/devkit';
|
} from '@nx/devkit';
|
||||||
import ssrDevServerExecutor from '@nx/webpack/src/executors/ssr-dev-server/ssr-dev-server.impl';
|
import ssrDevServerExecutor from '@nx/webpack/src/executors/ssr-dev-server/ssr-dev-server.impl';
|
||||||
import { WebSsrDevServerOptions } from '@nx/webpack/src/executors/ssr-dev-server/schema';
|
import { WebSsrDevServerOptions } from '@nx/webpack/src/executors/ssr-dev-server/schema';
|
||||||
import { join } from 'path';
|
import { extname, join } from 'path';
|
||||||
import * as chalk from 'chalk';
|
import {
|
||||||
|
getModuleFederationConfig,
|
||||||
|
getRemotes,
|
||||||
|
} from '@nx/webpack/src/utils/module-federation';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
combineAsyncIterables,
|
combineAsyncIterables,
|
||||||
createAsyncIterable,
|
createAsyncIterable,
|
||||||
mapAsyncIterable,
|
|
||||||
tapAsyncIterable,
|
|
||||||
} from '@nx/devkit/src/utils/async-iterable';
|
} from '@nx/devkit/src/utils/async-iterable';
|
||||||
import { execSync, fork } from 'child_process';
|
import { fork } from 'child_process';
|
||||||
import { existsSync } from 'fs';
|
import { cpSync, createWriteStream, existsSync } from 'fs';
|
||||||
import { registerTsProject } from '@nx/js/src/internal';
|
|
||||||
|
import {
|
||||||
|
parseStaticSsrRemotesConfig,
|
||||||
|
type StaticRemotesConfig,
|
||||||
|
} from '@nx/webpack/src/utils/module-federation/parse-static-remotes-config';
|
||||||
|
|
||||||
|
import fileServerExecutor from '@nx/web/src/executors/file-server/file-server.impl';
|
||||||
|
import { workspaceDataDirectory } from 'nx/src/utils/cache-directory';
|
||||||
|
import { startSsrRemoteProxies } from '@nx/webpack/src/utils/module-federation/start-ssr-remote-proxies';
|
||||||
|
import { waitForPortOpen } from '@nx/web/src/utils/wait-for-port-open';
|
||||||
|
|
||||||
|
type ModuleFederationSsrDevServerOptions = WebSsrDevServerOptions & {
|
||||||
|
devRemotes?: (
|
||||||
|
| string
|
||||||
|
| {
|
||||||
|
remoteName: string;
|
||||||
|
configuration: string;
|
||||||
|
}
|
||||||
|
)[];
|
||||||
|
|
||||||
type ModuleFederationDevServerOptions = WebSsrDevServerOptions & {
|
|
||||||
devRemotes?: string | string[];
|
|
||||||
skipRemotes?: string[];
|
skipRemotes?: string[];
|
||||||
host: string;
|
host: string;
|
||||||
|
pathToManifestFile?: string;
|
||||||
|
staticRemotesPort?: number;
|
||||||
|
parallel?: number;
|
||||||
|
ssl?: boolean;
|
||||||
|
sslKey?: string;
|
||||||
|
sslCert?: string;
|
||||||
|
isInitialHost?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function normalizeOptions(
|
||||||
|
options: ModuleFederationSsrDevServerOptions
|
||||||
|
): ModuleFederationSsrDevServerOptions {
|
||||||
|
return {
|
||||||
|
...options,
|
||||||
|
ssl: options.ssl ?? false,
|
||||||
|
sslCert: options.sslCert ? join(workspaceRoot, options.sslCert) : undefined,
|
||||||
|
sslKey: options.sslKey ? join(workspaceRoot, options.sslKey) : undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function getBuildOptions(buildTarget: string, context: ExecutorContext) {
|
function getBuildOptions(buildTarget: string, context: ExecutorContext) {
|
||||||
const target = parseTargetString(buildTarget, context);
|
const target = parseTargetString(buildTarget, context);
|
||||||
|
|
||||||
@ -37,151 +72,333 @@ function getBuildOptions(buildTarget: string, context: ExecutorContext) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function getModuleFederationConfig(
|
function startSsrStaticRemotesFileServer(
|
||||||
tsconfigPath: string,
|
ssrStaticRemotesConfig: StaticRemotesConfig,
|
||||||
workspaceRoot: string,
|
context: ExecutorContext,
|
||||||
projectRoot: string
|
options: ModuleFederationSsrDevServerOptions
|
||||||
) {
|
) {
|
||||||
const moduleFederationConfigPathJS = join(
|
if (ssrStaticRemotesConfig.remotes.length === 0) {
|
||||||
workspaceRoot,
|
return;
|
||||||
projectRoot,
|
|
||||||
'module-federation.config.js'
|
|
||||||
);
|
|
||||||
|
|
||||||
const moduleFederationConfigPathTS = join(
|
|
||||||
workspaceRoot,
|
|
||||||
projectRoot,
|
|
||||||
'module-federation.config.ts'
|
|
||||||
);
|
|
||||||
|
|
||||||
let moduleFederationConfigPath = moduleFederationConfigPathJS;
|
|
||||||
|
|
||||||
const fullTSconfigPath = tsconfigPath.startsWith(workspaceRoot)
|
|
||||||
? tsconfigPath
|
|
||||||
: join(workspaceRoot, tsconfigPath);
|
|
||||||
// create a no-op so this can be called with issue
|
|
||||||
let cleanupTranspiler = () => {};
|
|
||||||
if (existsSync(moduleFederationConfigPathTS)) {
|
|
||||||
cleanupTranspiler = registerTsProject(fullTSconfigPath);
|
|
||||||
moduleFederationConfigPath = moduleFederationConfigPathTS;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
// The directories are usually generated with /browser and /server suffixes so we need to copy them to a common directory
|
||||||
const config = require(moduleFederationConfigPath);
|
const commonOutputDirectory = join(workspaceRoot, 'tmp/static-remotes');
|
||||||
cleanupTranspiler();
|
for (const app of ssrStaticRemotesConfig.remotes) {
|
||||||
|
const remoteConfig = ssrStaticRemotesConfig.config[app];
|
||||||
|
|
||||||
return config.default || config;
|
cpSync(
|
||||||
} catch {
|
remoteConfig.outputPath,
|
||||||
throw new Error(
|
join(commonOutputDirectory, remoteConfig.urlSegment),
|
||||||
`Could not load ${moduleFederationConfigPath}. Was this project generated with "@nx/react:host"?\nSee: https://nx.dev/concepts/more-concepts/faster-builds-with-module-federation`
|
{
|
||||||
|
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,
|
||||||
|
cacheSeconds: -1,
|
||||||
|
},
|
||||||
|
context
|
||||||
|
);
|
||||||
|
|
||||||
|
return staticRemotesIter;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startRemotes(
|
||||||
|
remotes: string[],
|
||||||
|
context: ExecutorContext,
|
||||||
|
options: ModuleFederationSsrDevServerOptions
|
||||||
|
) {
|
||||||
|
const remoteIters: AsyncIterable<{ success: boolean }>[] = [];
|
||||||
|
const target = 'serve';
|
||||||
|
for (const app of remotes) {
|
||||||
|
const remoteProjectServeTarget =
|
||||||
|
context.projectGraph.nodes[app].data.targets[target];
|
||||||
|
const isUsingModuleFederationSsrDevServerExecutor =
|
||||||
|
remoteProjectServeTarget.executor.includes(
|
||||||
|
'module-federation-ssr-dev-server'
|
||||||
|
);
|
||||||
|
|
||||||
|
const configurationOverride = options.devRemotes?.find(
|
||||||
|
(remote): remote is { remoteName: string; configuration: string } =>
|
||||||
|
typeof remote !== 'string' && remote.remoteName === app
|
||||||
|
)?.configuration;
|
||||||
|
{
|
||||||
|
const defaultOverrides = {
|
||||||
|
...(options.host ? { host: options.host } : {}),
|
||||||
|
...(options.ssl ? { ssl: options.ssl } : {}),
|
||||||
|
...(options.sslCert ? { sslCert: options.sslCert } : {}),
|
||||||
|
...(options.sslKey ? { sslKey: options.sslKey } : {}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const overrides = {
|
||||||
|
watch: true,
|
||||||
|
...defaultOverrides,
|
||||||
|
...(isUsingModuleFederationSsrDevServerExecutor
|
||||||
|
? { isInitialHost: false }
|
||||||
|
: {}),
|
||||||
|
};
|
||||||
|
|
||||||
|
remoteIters.push(
|
||||||
|
await runExecutor(
|
||||||
|
{
|
||||||
|
project: app,
|
||||||
|
target,
|
||||||
|
configuration: configurationOverride ?? context.configurationName,
|
||||||
|
},
|
||||||
|
overrides,
|
||||||
|
context
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return remoteIters;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function buildSsrStaticRemotes(
|
||||||
|
staticRemotesConfig: StaticRemotesConfig,
|
||||||
|
nxBin,
|
||||||
|
context: ExecutorContext,
|
||||||
|
options: ModuleFederationSsrDevServerOptions
|
||||||
|
) {
|
||||||
|
if (!staticRemotesConfig.remotes.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
`Nx is building ${staticRemotesConfig.remotes.length} static remotes...`
|
||||||
|
);
|
||||||
|
const mapLocationOfRemotes: Record<string, string> = {};
|
||||||
|
|
||||||
|
for (const remoteApp of staticRemotesConfig.remotes) {
|
||||||
|
mapLocationOfRemotes[remoteApp] = `http${options.ssl ? 's' : ''}://${
|
||||||
|
options.host
|
||||||
|
}:${options.staticRemotesPort}/${
|
||||||
|
staticRemotesConfig.config[remoteApp].urlSegment
|
||||||
|
}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
await new Promise<void>((resolve) => {
|
||||||
|
const childProcess = fork(
|
||||||
|
nxBin,
|
||||||
|
[
|
||||||
|
'run-many',
|
||||||
|
'--target=server',
|
||||||
|
'--projects',
|
||||||
|
staticRemotesConfig.remotes.join(','),
|
||||||
|
...(context.configurationName
|
||||||
|
? [`--configuration=${context.configurationName}`]
|
||||||
|
: []),
|
||||||
|
...(options.parallel ? [`--parallel=${options.parallel}`] : []),
|
||||||
|
],
|
||||||
|
{
|
||||||
|
cwd: context.root,
|
||||||
|
stdio: ['ignore', 'pipe', 'pipe', 'ipc'],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add a listener to the child process to capture the build log
|
||||||
|
const remoteBuildLogFile = join(
|
||||||
|
workspaceDataDirectory,
|
||||||
|
`${new Date().toISOString().replace(/[:\.]/g, '_')}-build.log`
|
||||||
|
);
|
||||||
|
|
||||||
|
const remoteBuildLogStream = createWriteStream(remoteBuildLogFile);
|
||||||
|
|
||||||
|
childProcess.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, '');
|
||||||
|
remoteBuildLogStream.write(stdoutString);
|
||||||
|
|
||||||
|
// in addition to writing into the stdout stream, also show error directly in console
|
||||||
|
// so the error is easily discoverable. 'ERROR in' is the key word to search in webpack output.
|
||||||
|
if (stdoutString.includes('ERROR in')) {
|
||||||
|
logger.log(stdoutString);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stdoutString.includes('Successfully ran target server')) {
|
||||||
|
childProcess.stdout.removeAllListeners('data');
|
||||||
|
logger.info(
|
||||||
|
`Nx Built ${staticRemotesConfig.remotes.length} static remotes.`
|
||||||
|
);
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on('SIGTERM', () => childProcess.kill('SIGTERM'));
|
||||||
|
process.on('exit', () => childProcess.kill('SIGTERM'));
|
||||||
|
});
|
||||||
|
return mapLocationOfRemotes;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function* moduleFederationSsrDevServer(
|
export default async function* moduleFederationSsrDevServer(
|
||||||
options: ModuleFederationDevServerOptions,
|
ssrDevServerOptions: ModuleFederationSsrDevServerOptions,
|
||||||
context: ExecutorContext
|
context: ExecutorContext
|
||||||
) {
|
) {
|
||||||
|
const options = normalizeOptions(ssrDevServerOptions);
|
||||||
|
// Force Node to resolve to look for the nx binary that is inside node_modules
|
||||||
|
const nxBin = require.resolve('nx/bin/nx');
|
||||||
let iter: any = ssrDevServerExecutor(options, context);
|
let iter: any = ssrDevServerExecutor(options, context);
|
||||||
const p = context.projectsConfigurations.projects[context.projectName];
|
const projectConfig =
|
||||||
|
context.projectsConfigurations.projects[context.projectName];
|
||||||
const buildOptions = getBuildOptions(options.browserTarget, context);
|
const buildOptions = getBuildOptions(options.browserTarget, context);
|
||||||
|
|
||||||
|
let pathToManifestFile = join(
|
||||||
|
context.root,
|
||||||
|
projectConfig.sourceRoot,
|
||||||
|
'assets/module-federation.manifest.json'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (options.pathToManifestFile) {
|
||||||
|
const userPathToManifestFile = join(
|
||||||
|
context.root,
|
||||||
|
options.pathToManifestFile
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!existsSync(userPathToManifestFile)) {
|
||||||
|
throw new Error(
|
||||||
|
`The provided Module Federation manifest file path does not exist. Please check the file exists at "${userPathToManifestFile}".`
|
||||||
|
);
|
||||||
|
} else if (extname(userPathToManifestFile) !== '.json') {
|
||||||
|
throw new Error(
|
||||||
|
`The Module Federation manifest file must be a JSON. Please ensure the file at ${userPathToManifestFile} is a JSON.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
pathToManifestFile = userPathToManifestFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!options.isInitialHost) {
|
||||||
|
return yield* iter;
|
||||||
|
}
|
||||||
|
|
||||||
const moduleFederationConfig = getModuleFederationConfig(
|
const moduleFederationConfig = getModuleFederationConfig(
|
||||||
buildOptions.tsConfig,
|
buildOptions.tsConfig,
|
||||||
context.root,
|
context.root,
|
||||||
p.root
|
projectConfig.root,
|
||||||
|
'react'
|
||||||
);
|
);
|
||||||
|
|
||||||
const remotesToSkip = new Set(options.skipRemotes ?? []);
|
const remoteNames = options.devRemotes?.map((remote) =>
|
||||||
const remotesNotInWorkspace: string[] = [];
|
typeof remote === 'string' ? remote : remote.remoteName
|
||||||
const knownRemotes = (moduleFederationConfig.remotes ?? []).filter((r) => {
|
|
||||||
const validRemote = Array.isArray(r) ? r[0] : r;
|
|
||||||
|
|
||||||
if (remotesToSkip.has(validRemote)) {
|
|
||||||
return false;
|
|
||||||
} else if (!context.projectGraph.nodes[validRemote]) {
|
|
||||||
remotesNotInWorkspace.push(validRemote);
|
|
||||||
return false;
|
|
||||||
} else {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (remotesNotInWorkspace.length > 0) {
|
|
||||||
logger.warn(
|
|
||||||
`Skipping serving ${remotesNotInWorkspace.join(
|
|
||||||
', '
|
|
||||||
)} as they could not be found in the workspace. Ensure they are served correctly.`
|
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
const devServeApps = !options.devRemotes
|
const remotes = getRemotes(
|
||||||
? []
|
remoteNames,
|
||||||
: Array.isArray(options.devRemotes)
|
options.skipRemotes,
|
||||||
? options.devRemotes
|
moduleFederationConfig,
|
||||||
: [options.devRemotes];
|
|
||||||
|
|
||||||
// Set NX_MF_DEV_REMOTES for the Nx Runtime Library Control Plugin
|
|
||||||
process.env.NX_MF_DEV_REMOTES = JSON.stringify(devServeApps);
|
|
||||||
|
|
||||||
for (const app of knownRemotes) {
|
|
||||||
const [appName] = Array.isArray(app) ? app : [app];
|
|
||||||
const isDev = devServeApps.includes(appName);
|
|
||||||
const remoteServeIter = isDev
|
|
||||||
? await runExecutor(
|
|
||||||
{
|
{
|
||||||
project: appName,
|
projectName: context.projectName,
|
||||||
target: 'serve',
|
projectGraph: context.projectGraph,
|
||||||
configuration: context.configurationName,
|
root: context.root,
|
||||||
},
|
|
||||||
{
|
|
||||||
watch: isDev,
|
|
||||||
},
|
},
|
||||||
|
pathToManifestFile
|
||||||
|
);
|
||||||
|
|
||||||
|
options.staticRemotesPort ??= remotes.staticRemotePort;
|
||||||
|
|
||||||
|
process.env.NX_MF_DEV_REMOTES = JSON.stringify(
|
||||||
|
remotes.devRemotes.map((r) => (typeof r === 'string' ? r : r.remoteName))
|
||||||
|
);
|
||||||
|
|
||||||
|
const staticRemotesConfig = parseStaticSsrRemotesConfig(
|
||||||
|
[...remotes.staticRemotes, ...remotes.dynamicRemotes],
|
||||||
context
|
context
|
||||||
)
|
|
||||||
: mapAsyncIterable(
|
|
||||||
createAsyncIterable(async ({ next, done }) => {
|
|
||||||
const remoteProject =
|
|
||||||
context.projectsConfigurations.projects[appName];
|
|
||||||
const remoteServerOutput = join(
|
|
||||||
workspaceRoot,
|
|
||||||
remoteProject.targets.server.options.outputPath,
|
|
||||||
remoteProject.targets.server.options.outputFileName
|
|
||||||
);
|
);
|
||||||
const pm = getPackageManagerCommand();
|
|
||||||
execSync(
|
|
||||||
`${pm.exec} nx run ${appName}:server${
|
|
||||||
context.configurationName ? `:${context.configurationName}` : ''
|
|
||||||
}`,
|
|
||||||
{ stdio: 'inherit' }
|
|
||||||
);
|
|
||||||
const child = fork(remoteServerOutput, {
|
|
||||||
env: {
|
|
||||||
PORT: remoteProject.targets['serve-browser'].options.port,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
child.on('message', (msg) => {
|
const mappedLocationsOfStaticRemotes = await buildSsrStaticRemotes(
|
||||||
if (msg === 'nx.server.ready') {
|
staticRemotesConfig,
|
||||||
next(true);
|
nxBin,
|
||||||
|
context,
|
||||||
|
options
|
||||||
|
);
|
||||||
|
|
||||||
|
const devRemoteIters = await startRemotes(
|
||||||
|
remotes.devRemotes,
|
||||||
|
context,
|
||||||
|
options
|
||||||
|
);
|
||||||
|
|
||||||
|
const staticRemotesIter = startSsrStaticRemotesFileServer(
|
||||||
|
staticRemotesConfig,
|
||||||
|
context,
|
||||||
|
options
|
||||||
|
);
|
||||||
|
|
||||||
|
startSsrRemoteProxies(
|
||||||
|
staticRemotesConfig,
|
||||||
|
mappedLocationsOfStaticRemotes,
|
||||||
|
options.ssl
|
||||||
|
? {
|
||||||
|
pathToCert: options.sslCert,
|
||||||
|
pathToKey: options.sslKey,
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
return yield* combineAsyncIterables(
|
||||||
|
iter,
|
||||||
|
...devRemoteIters,
|
||||||
|
...(staticRemotesIter ? [staticRemotesIter] : []),
|
||||||
|
createAsyncIterable<{ success: true; baseUrl: string }>(
|
||||||
|
async ({ next, done }) => {
|
||||||
|
if (!options.isInitialHost) {
|
||||||
|
done();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remotes.remotePorts.length === 0) {
|
||||||
|
done();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const host = options.host ?? 'localhost';
|
||||||
|
const baseUrl = `http${options.ssl ? 's' : ''}://${host}:${
|
||||||
|
options.port
|
||||||
|
}`;
|
||||||
|
const portsToWaitFor = staticRemotesIter
|
||||||
|
? [options.staticRemotesPort, ...remotes.remotePorts]
|
||||||
|
: [...remotes.remotePorts];
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
portsToWaitFor.map((port) =>
|
||||||
|
waitForPortOpen(port, {
|
||||||
|
retries: 480,
|
||||||
|
retryDelay: 2500,
|
||||||
|
host,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
`Nx all ssr remotes have started, server ready at ${baseUrl}`
|
||||||
|
);
|
||||||
|
next({ success: true, baseUrl });
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(
|
||||||
|
`Nx failed to start ssr remotes. Check above for errors.`,
|
||||||
|
{
|
||||||
|
cause: error,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
done();
|
done();
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}),
|
|
||||||
(x) => x
|
|
||||||
);
|
|
||||||
|
|
||||||
iter = combineAsyncIterables(iter, remoteServeIter);
|
|
||||||
}
|
}
|
||||||
|
)
|
||||||
let numAwaiting = knownRemotes.length + 1; // remotes + host
|
|
||||||
return yield* tapAsyncIterable(iter, (x) => {
|
|
||||||
numAwaiting--;
|
|
||||||
if (numAwaiting === 0) {
|
|
||||||
logger.info(
|
|
||||||
`[ ${chalk.green('ready')} ] http://${options.host ?? 'localhost'}:${
|
|
||||||
options.port ?? 4200
|
|
||||||
}`
|
|
||||||
);
|
);
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -42,6 +42,33 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Host to listen on.",
|
"description": "Host to listen on.",
|
||||||
"default": "localhost"
|
"default": "localhost"
|
||||||
|
},
|
||||||
|
"staticRemotesPort": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "The port at which to serve the file-server for the static remotes."
|
||||||
|
},
|
||||||
|
"pathToManifestFile": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Path to a Module Federation manifest file (e.g. `my/path/to/module-federation.manifest.json`) containing the dynamic remote applications relative to the workspace root."
|
||||||
|
},
|
||||||
|
"ssl": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Serve using HTTPS.",
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"sslKey": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "SSL key to use for serving HTTPS."
|
||||||
|
},
|
||||||
|
"sslCert": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "SSL certificate to use for serving HTTPS."
|
||||||
|
},
|
||||||
|
"isInitialHost": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "Whether the host that is running this executor is the first in the project tree to do so.",
|
||||||
|
"default": true,
|
||||||
|
"x-priority": "internal"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["browserTarget", "serverTarget"]
|
"required": ["browserTarget", "serverTarget"]
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import cors from 'cors';
|
|||||||
|
|
||||||
import { handleRequest } from './src/main.server';
|
import { handleRequest } from './src/main.server';
|
||||||
|
|
||||||
const port = process.env['PORT'] || 4200;
|
const port = process.env['PORT'] || <%= port %>;
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
const browserDist = path.join(process.cwd(), '<%= browserBuildOutputPath %>');
|
const browserDist = path.join(process.cwd(), '<%= browserBuildOutputPath %>');
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import cors from 'cors';
|
|||||||
|
|
||||||
import { handleRequest } from './src/main.server';
|
import { handleRequest } from './src/main.server';
|
||||||
|
|
||||||
const port = process.env['PORT'] || 4200;
|
const port = process.env['PORT'] || <%= port %>;
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
const browserDist = path.join(process.cwd(), '<%= browserBuildOutputPath %>');
|
const browserDist = path.join(process.cwd(), '<%= browserBuildOutputPath %>');
|
||||||
|
|||||||
@ -34,6 +34,7 @@ export async function setupSsrForHost(
|
|||||||
{
|
{
|
||||||
...options,
|
...options,
|
||||||
static: !options?.dynamic,
|
static: !options?.dynamic,
|
||||||
|
port: Number(options?.devServerPort) || 4200,
|
||||||
remotes: defaultRemoteManifest.map(({ name, port }) => {
|
remotes: defaultRemoteManifest.map(({ name, port }) => {
|
||||||
return {
|
return {
|
||||||
...names(name),
|
...names(name),
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import cors from 'cors';
|
|||||||
|
|
||||||
import { handleRequest } from './src/main.server';
|
import { handleRequest } from './src/main.server';
|
||||||
|
|
||||||
const port = process.env['PORT'] || 4200;
|
const port = process.env['PORT'] || <%= port %>;
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
const browserDist = path.join(process.cwd(), '<%= browserBuildOutputPath %>');
|
const browserDist = path.join(process.cwd(), '<%= browserBuildOutputPath %>');
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import cors from 'cors';
|
|||||||
|
|
||||||
import { handleRequest } from './src/main.server';
|
import { handleRequest } from './src/main.server';
|
||||||
|
|
||||||
const port = process.env['PORT'] || 4200;
|
const port = process.env['PORT'] || <%= port %>;
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
const browserDist = path.join(process.cwd(), '<%= browserBuildOutputPath %>');
|
const browserDist = path.join(process.cwd(), '<%= browserBuildOutputPath %>');
|
||||||
|
|||||||
@ -30,6 +30,7 @@ export async function setupSsrForRemote(
|
|||||||
project.root,
|
project.root,
|
||||||
{
|
{
|
||||||
...options,
|
...options,
|
||||||
|
port: Number(options?.devServerPort) || 4200,
|
||||||
appName,
|
appName,
|
||||||
tmpl: '',
|
tmpl: '',
|
||||||
browserBuildOutputPath: project.targets.build.options.outputPath,
|
browserBuildOutputPath: project.targets.build.options.outputPath,
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import cors from 'cors';
|
|||||||
|
|
||||||
import { handleRequest } from './src/main.server';
|
import { handleRequest } from './src/main.server';
|
||||||
|
|
||||||
const port = process.env['PORT'] || 4200;
|
const port = process.env['PORT'] || <%= port %>;
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
const browserDist = path.join(process.cwd(), '<%= browserBuildOutputPath %>');
|
const browserDist = path.join(process.cwd(), '<%= browserBuildOutputPath %>');
|
||||||
|
|||||||
@ -216,6 +216,7 @@ export async function setupSsrGenerator(tree: Tree, options: Schema) {
|
|||||||
|
|
||||||
generateFiles(tree, join(__dirname, 'files'), projectRoot, {
|
generateFiles(tree, join(__dirname, 'files'), projectRoot, {
|
||||||
tmpl: '',
|
tmpl: '',
|
||||||
|
port: Number(options?.serverPort) || 4200,
|
||||||
extraInclude:
|
extraInclude:
|
||||||
options.extraInclude?.length > 0
|
options.extraInclude?.length > 0
|
||||||
? `"${options.extraInclude.join('", "')}",`
|
? `"${options.extraInclude.join('", "')}",`
|
||||||
|
|||||||
@ -3,7 +3,6 @@ import {
|
|||||||
ExecutorContext,
|
ExecutorContext,
|
||||||
parseTargetString,
|
parseTargetString,
|
||||||
readTargetOptions,
|
readTargetOptions,
|
||||||
targetToTargetString,
|
|
||||||
} from '@nx/devkit';
|
} from '@nx/devkit';
|
||||||
|
|
||||||
import { eachValueFrom } from '@nx/devkit/src/utils/rxjs-for-await';
|
import { eachValueFrom } from '@nx/devkit/src/utils/rxjs-for-await';
|
||||||
|
|||||||
@ -51,8 +51,6 @@ export async function* ssrDevServerExecutor(
|
|||||||
let nodeStarted = false;
|
let nodeStarted = false;
|
||||||
const combined = combineAsyncIterables(runBrowser, runServer);
|
const combined = combineAsyncIterables(runBrowser, runServer);
|
||||||
|
|
||||||
process.env['PORT'] = `${options.port}`;
|
|
||||||
|
|
||||||
for await (const output of combined) {
|
for await (const output of combined) {
|
||||||
if (!output.success) throw new Error('Could not build application');
|
if (!output.success) throw new Error('Could not build application');
|
||||||
if (output.options?.target === 'node') {
|
if (output.options?.target === 'node') {
|
||||||
|
|||||||
@ -139,7 +139,8 @@ export function getRemotes(
|
|||||||
context.projectGraph.nodes[r].data.targets['serve'].options.port
|
context.projectGraph.nodes[r].data.targets['serve'].options.port
|
||||||
),
|
),
|
||||||
] as number[])
|
] as number[])
|
||||||
) + 1;
|
) +
|
||||||
|
(remotesToSkip.size + 1);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
staticRemotes,
|
staticRemotes,
|
||||||
|
|||||||
@ -7,7 +7,6 @@ export type StaticRemoteConfig = {
|
|||||||
urlSegment: string;
|
urlSegment: string;
|
||||||
port: number;
|
port: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type StaticRemotesConfig = {
|
export type StaticRemotesConfig = {
|
||||||
remotes: string[];
|
remotes: string[];
|
||||||
config: Record<string, StaticRemoteConfig> | undefined;
|
config: Record<string, StaticRemoteConfig> | undefined;
|
||||||
@ -34,3 +33,25 @@ export function parseStaticRemotesConfig(
|
|||||||
|
|
||||||
return { remotes: staticRemotes, config };
|
return { remotes: staticRemotes, config };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function parseStaticSsrRemotesConfig(
|
||||||
|
staticRemotes: string[] | undefined,
|
||||||
|
context: ExecutorContext
|
||||||
|
): StaticRemotesConfig {
|
||||||
|
if (!staticRemotes?.length) {
|
||||||
|
return { remotes: [], config: undefined };
|
||||||
|
}
|
||||||
|
const config: Record<string, StaticRemoteConfig> = {};
|
||||||
|
for (const app of staticRemotes) {
|
||||||
|
const outputPath = dirname(
|
||||||
|
context.projectGraph.nodes[app].data.targets['build'].options.outputPath // dist/checkout/browser -> checkout
|
||||||
|
) as string;
|
||||||
|
const basePath = dirname(outputPath); // dist/checkout -> dist
|
||||||
|
const urlSegment = basename(outputPath); // dist/checkout -> checkout
|
||||||
|
const port =
|
||||||
|
context.projectGraph.nodes[app].data.targets['serve'].options.port;
|
||||||
|
config[app] = { basePath, outputPath, urlSegment, port };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { remotes: staticRemotes, config };
|
||||||
|
}
|
||||||
|
|||||||
@ -0,0 +1,65 @@
|
|||||||
|
import type { Express } from 'express';
|
||||||
|
import { logger } from '@nx/devkit';
|
||||||
|
import type { StaticRemotesConfig } from './parse-static-remotes-config';
|
||||||
|
import { existsSync, readFileSync } from 'fs';
|
||||||
|
|
||||||
|
export function startSsrRemoteProxies(
|
||||||
|
staticRemotesConfig: StaticRemotesConfig,
|
||||||
|
mappedLocationsOfRemotes: Record<string, string>,
|
||||||
|
sslOptions?: { pathToCert: string; pathToKey: string }
|
||||||
|
) {
|
||||||
|
const { createProxyMiddleware } = require('http-proxy-middleware');
|
||||||
|
const express = require('express');
|
||||||
|
|
||||||
|
let sslCert: Buffer;
|
||||||
|
let sslKey: Buffer;
|
||||||
|
if (sslOptions && sslOptions.pathToCert && sslOptions.pathToKey) {
|
||||||
|
if (existsSync(sslOptions.pathToCert) && existsSync(sslOptions.pathToKey)) {
|
||||||
|
sslCert = readFileSync(sslOptions.pathToCert);
|
||||||
|
sslKey = readFileSync(sslOptions.pathToKey);
|
||||||
|
} else {
|
||||||
|
logger.warn(
|
||||||
|
`Encountered SSL options in project.json, however, the certificate files do not exist in the filesystem. Using http.`
|
||||||
|
);
|
||||||
|
logger.warn(
|
||||||
|
`Attempted to find '${sslOptions.pathToCert}' and '${sslOptions.pathToKey}'.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const http = require('http');
|
||||||
|
const https = require('https');
|
||||||
|
|
||||||
|
logger.info(`NX Starting static remotes proxies...`);
|
||||||
|
for (const app of staticRemotesConfig.remotes) {
|
||||||
|
const expressProxy: Express = express();
|
||||||
|
/**
|
||||||
|
* SSR remotes have two output paths: one for the browser and one for the server.
|
||||||
|
* We need to handle paths for both of them.
|
||||||
|
* The browser output path is used to serve the client-side code.
|
||||||
|
* The server output path is used to serve the server-side code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
expressProxy.use(
|
||||||
|
createProxyMiddleware({
|
||||||
|
target: `${mappedLocationsOfRemotes[app]}`,
|
||||||
|
secure: sslCert ? false : undefined,
|
||||||
|
changeOrigin: true,
|
||||||
|
pathRewrite: (path) => {
|
||||||
|
if (path.includes('/server')) {
|
||||||
|
return path;
|
||||||
|
} else {
|
||||||
|
return `browser/${path}`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const proxyServer = (sslCert ? https : http)
|
||||||
|
.createServer({ cert: sslCert, key: sslKey }, expressProxy)
|
||||||
|
.listen(staticRemotesConfig.config[app].port);
|
||||||
|
process.on('SIGTERM', () => proxyServer.close());
|
||||||
|
process.on('exit', () => proxyServer.close());
|
||||||
|
}
|
||||||
|
logger.info(`Nx SSR Static remotes proxies started successfully`);
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user