feat(react): add module federation static server (#27802)
<!-- 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 --> We can serve each application in a module federation setup statically, but only individually. ## Expected Behavior <!-- This is the behavior we should expect with the changes in this PR --> Add a serve static executor for module federation hosts which will also spin up the remotes statically ## Related Issue(s) <!-- Please link the issue being fixed so it gets closed when this is merged. --> Fixes #
This commit is contained in:
parent
24025c86fb
commit
2bc7d4e6e9
@ -9052,6 +9052,14 @@
|
|||||||
"children": [],
|
"children": [],
|
||||||
"isExternal": false,
|
"isExternal": false,
|
||||||
"disableCollapsible": false
|
"disableCollapsible": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "module-federation-static-server",
|
||||||
|
"path": "/nx-api/react/executors/module-federation-static-server",
|
||||||
|
"name": "module-federation-static-server",
|
||||||
|
"children": [],
|
||||||
|
"isExternal": false,
|
||||||
|
"disableCollapsible": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"isExternal": false,
|
"isExternal": false,
|
||||||
|
|||||||
@ -2232,6 +2232,15 @@
|
|||||||
"originalFilePath": "/packages/react/src/executors/module-federation-ssr-dev-server/schema.json",
|
"originalFilePath": "/packages/react/src/executors/module-federation-ssr-dev-server/schema.json",
|
||||||
"path": "/nx-api/react/executors/module-federation-ssr-dev-server",
|
"path": "/nx-api/react/executors/module-federation-ssr-dev-server",
|
||||||
"type": "executor"
|
"type": "executor"
|
||||||
|
},
|
||||||
|
"/nx-api/react/executors/module-federation-static-server": {
|
||||||
|
"description": "Serve a host and its remotes statically.",
|
||||||
|
"file": "generated/packages/react/executors/module-federation-static-server.json",
|
||||||
|
"hidden": false,
|
||||||
|
"name": "module-federation-static-server",
|
||||||
|
"originalFilePath": "/packages/react/src/executors/module-federation-static-server/schema.json",
|
||||||
|
"path": "/nx-api/react/executors/module-federation-static-server",
|
||||||
|
"type": "executor"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"generators": {
|
"generators": {
|
||||||
|
|||||||
@ -2206,6 +2206,15 @@
|
|||||||
"originalFilePath": "/packages/react/src/executors/module-federation-ssr-dev-server/schema.json",
|
"originalFilePath": "/packages/react/src/executors/module-federation-ssr-dev-server/schema.json",
|
||||||
"path": "react/executors/module-federation-ssr-dev-server",
|
"path": "react/executors/module-federation-ssr-dev-server",
|
||||||
"type": "executor"
|
"type": "executor"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Serve a host and its remotes statically.",
|
||||||
|
"file": "generated/packages/react/executors/module-federation-static-server.json",
|
||||||
|
"hidden": false,
|
||||||
|
"name": "module-federation-static-server",
|
||||||
|
"originalFilePath": "/packages/react/src/executors/module-federation-static-server/schema.json",
|
||||||
|
"path": "react/executors/module-federation-static-server",
|
||||||
|
"type": "executor"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"generators": [
|
"generators": [
|
||||||
|
|||||||
@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"name": "module-federation-static-server",
|
||||||
|
"implementation": "/packages/react/src/executors/module-federation-static-server/module-federation-static-server.impl.ts",
|
||||||
|
"schema": {
|
||||||
|
"version": 2,
|
||||||
|
"outputCapture": "direct-nodejs",
|
||||||
|
"title": "Module Federation Static Dev Server",
|
||||||
|
"description": "Serve a host application statically along with it's remotes.",
|
||||||
|
"cli": "nx",
|
||||||
|
"type": "object",
|
||||||
|
"properties": { "serveTarget": { "type": "string" } },
|
||||||
|
"required": ["serveTarget"],
|
||||||
|
"presets": []
|
||||||
|
},
|
||||||
|
"description": "Serve a host and its remotes statically.",
|
||||||
|
"aliases": [],
|
||||||
|
"hidden": false,
|
||||||
|
"path": "/packages/react/src/executors/module-federation-static-server/schema.json",
|
||||||
|
"type": "executor"
|
||||||
|
}
|
||||||
@ -596,6 +596,7 @@
|
|||||||
- [executors](/nx-api/react/executors)
|
- [executors](/nx-api/react/executors)
|
||||||
- [module-federation-dev-server](/nx-api/react/executors/module-federation-dev-server)
|
- [module-federation-dev-server](/nx-api/react/executors/module-federation-dev-server)
|
||||||
- [module-federation-ssr-dev-server](/nx-api/react/executors/module-federation-ssr-dev-server)
|
- [module-federation-ssr-dev-server](/nx-api/react/executors/module-federation-ssr-dev-server)
|
||||||
|
- [module-federation-static-server](/nx-api/react/executors/module-federation-static-server)
|
||||||
- [generators](/nx-api/react/generators)
|
- [generators](/nx-api/react/generators)
|
||||||
- [init](/nx-api/react/generators/init)
|
- [init](/nx-api/react/generators/init)
|
||||||
- [application](/nx-api/react/generators/application)
|
- [application](/nx-api/react/generators/application)
|
||||||
|
|||||||
@ -125,6 +125,72 @@ describe('React Rspack Module Federation', () => {
|
|||||||
},
|
},
|
||||||
500_000
|
500_000
|
||||||
);
|
);
|
||||||
|
it('should generate host and remote apps and use playwright for e2es', async () => {
|
||||||
|
const shell = uniq('shell');
|
||||||
|
const remote1 = uniq('remote1');
|
||||||
|
const remote2 = uniq('remote2');
|
||||||
|
const remote3 = uniq('remote3');
|
||||||
|
|
||||||
|
runCLI(
|
||||||
|
`generate @nx/react:host ${shell} --remotes=${remote1},${remote2},${remote3} --bundler=rspack --e2eTestRunner=playwright --style=css --no-interactive --skipFormat`
|
||||||
|
);
|
||||||
|
|
||||||
|
checkFilesExist(`apps/${shell}/module-federation.config.ts`);
|
||||||
|
checkFilesExist(`apps/${remote1}/module-federation.config.ts`);
|
||||||
|
checkFilesExist(`apps/${remote2}/module-federation.config.ts`);
|
||||||
|
checkFilesExist(`apps/${remote3}/module-federation.config.ts`);
|
||||||
|
|
||||||
|
await expect(runCLIAsync(`test ${shell}`)).resolves.toMatchObject({
|
||||||
|
combinedOutput: expect.stringContaining(
|
||||||
|
'Test Suites: 1 passed, 1 total'
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
updateFile(
|
||||||
|
`apps/${shell}-e2e/src/example.spec.ts`,
|
||||||
|
stripIndents`
|
||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
test('should display welcome message', async ({page}) => {
|
||||||
|
await page.goto("/");
|
||||||
|
expect(await page.locator('h1').innerText()).toContain('Welcome');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should load remote 1', async ({page}) => {
|
||||||
|
await page.goto("/${remote1}");
|
||||||
|
expect(await page.locator('h1').innerText()).toContain('${remote1}');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should load remote 2', async ({page}) => {
|
||||||
|
await page.goto("/${remote2}");
|
||||||
|
expect(await page.locator('h1').innerText()).toContain('${remote2}');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should load remote 3', async ({page}) => {
|
||||||
|
await page.goto("/${remote3}");
|
||||||
|
expect(await page.locator('h1').innerText()).toContain('${remote3}');
|
||||||
|
});
|
||||||
|
`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (runE2ETests()) {
|
||||||
|
const e2eResultsSwc = await runCommandUntil(
|
||||||
|
`e2e ${shell}-e2e`,
|
||||||
|
(output) => output.includes('Successfully ran target e2e for project')
|
||||||
|
);
|
||||||
|
|
||||||
|
await killProcessAndPorts(e2eResultsSwc.pid, readPort(shell));
|
||||||
|
|
||||||
|
const e2eResultsTsNode = await runCommandUntil(
|
||||||
|
`e2e ${shell}-e2e`,
|
||||||
|
(output) =>
|
||||||
|
output.includes('Successfully ran target e2e for project'),
|
||||||
|
{
|
||||||
|
env: { NX_PREFER_TS_NODE: 'true' },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
await killProcessAndPorts(e2eResultsTsNode.pid, readPort(shell));
|
||||||
|
}
|
||||||
|
}, 500_000);
|
||||||
|
|
||||||
it('should have interop between webpack host and rspack remote', async () => {
|
it('should have interop between webpack host and rspack remote', async () => {
|
||||||
const shell = uniq('shell');
|
const shell = uniq('shell');
|
||||||
@ -1049,7 +1115,7 @@ describe('React Rspack Module Federation', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
afterAll(() => cleanupProject());
|
afterAll(() => cleanupProject());
|
||||||
it('ttt should load remote dynamic module', async () => {
|
it('should load remote dynamic module', async () => {
|
||||||
const shell = uniq('shell');
|
const shell = uniq('shell');
|
||||||
const remote = uniq('remote');
|
const remote = uniq('remote');
|
||||||
const remotePort = 4205;
|
const remotePort = 4205;
|
||||||
|
|||||||
@ -126,6 +126,73 @@ describe('React Module Federation', () => {
|
|||||||
500_000
|
500_000
|
||||||
);
|
);
|
||||||
|
|
||||||
|
it('should generate host and remote apps and use playwright for e2es', async () => {
|
||||||
|
const shell = uniq('shell');
|
||||||
|
const remote1 = uniq('remote1');
|
||||||
|
const remote2 = uniq('remote2');
|
||||||
|
const remote3 = uniq('remote3');
|
||||||
|
|
||||||
|
runCLI(
|
||||||
|
`generate @nx/react:host ${shell} --remotes=${remote1},${remote2},${remote3} --bundler=webpack --e2eTestRunner=playwright --style=css --no-interactive --skipFormat`
|
||||||
|
);
|
||||||
|
|
||||||
|
checkFilesExist(`apps/${shell}/module-federation.config.ts`);
|
||||||
|
checkFilesExist(`apps/${remote1}/module-federation.config.ts`);
|
||||||
|
checkFilesExist(`apps/${remote2}/module-federation.config.ts`);
|
||||||
|
checkFilesExist(`apps/${remote3}/module-federation.config.ts`);
|
||||||
|
|
||||||
|
await expect(runCLIAsync(`test ${shell}`)).resolves.toMatchObject({
|
||||||
|
combinedOutput: expect.stringContaining(
|
||||||
|
'Test Suites: 1 passed, 1 total'
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
updateFile(
|
||||||
|
`apps/${shell}-e2e/src/example.spec.ts`,
|
||||||
|
stripIndents`
|
||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
test('should display welcome message', async ({page}) => {
|
||||||
|
await page.goto("/");
|
||||||
|
expect(await page.locator('h1').innerText()).toContain('Welcome');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should load remote 1', async ({page}) => {
|
||||||
|
await page.goto("/${remote1}");
|
||||||
|
expect(await page.locator('h1').innerText()).toContain('${remote1}');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should load remote 2', async ({page}) => {
|
||||||
|
await page.goto("/${remote2}");
|
||||||
|
expect(await page.locator('h1').innerText()).toContain('${remote2}');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should load remote 3', async ({page}) => {
|
||||||
|
await page.goto("/${remote3}");
|
||||||
|
expect(await page.locator('h1').innerText()).toContain('${remote3}');
|
||||||
|
});
|
||||||
|
`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (runE2ETests()) {
|
||||||
|
const e2eResultsSwc = await runCommandUntil(
|
||||||
|
`e2e ${shell}-e2e`,
|
||||||
|
(output) => output.includes('Successfully ran target e2e for project')
|
||||||
|
);
|
||||||
|
|
||||||
|
await killProcessAndPorts(e2eResultsSwc.pid, readPort(shell));
|
||||||
|
|
||||||
|
const e2eResultsTsNode = await runCommandUntil(
|
||||||
|
`e2e ${shell}-e2e`,
|
||||||
|
(output) =>
|
||||||
|
output.includes('Successfully ran target e2e for project'),
|
||||||
|
{
|
||||||
|
env: { NX_PREFER_TS_NODE: 'true' },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
await killProcessAndPorts(e2eResultsTsNode.pid, readPort(shell));
|
||||||
|
}
|
||||||
|
}, 500_000);
|
||||||
|
|
||||||
describe('ssr', () => {
|
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');
|
||||||
|
|||||||
@ -9,6 +9,11 @@
|
|||||||
"implementation": "./src/executors/module-federation-ssr-dev-server/module-federation-ssr-dev-server.impl",
|
"implementation": "./src/executors/module-federation-ssr-dev-server/module-federation-ssr-dev-server.impl",
|
||||||
"schema": "./src/executors/module-federation-ssr-dev-server/schema.json",
|
"schema": "./src/executors/module-federation-ssr-dev-server/schema.json",
|
||||||
"description": "Serve a host application along with it's known remotes."
|
"description": "Serve a host application along with it's known remotes."
|
||||||
|
},
|
||||||
|
"module-federation-static-server": {
|
||||||
|
"implementation": "./src/executors/module-federation-static-server/module-federation-static-server.impl",
|
||||||
|
"schema": "./src/executors/module-federation-static-server/schema.json",
|
||||||
|
"description": "Serve a host and its remotes statically."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -42,7 +42,9 @@
|
|||||||
"@nx/devkit": "file:../devkit",
|
"@nx/devkit": "file:../devkit",
|
||||||
"@nx/js": "file:../js",
|
"@nx/js": "file:../js",
|
||||||
"@nx/eslint": "file:../eslint",
|
"@nx/eslint": "file:../eslint",
|
||||||
"@nx/web": "file:../web"
|
"@nx/web": "file:../web",
|
||||||
|
"express": "^4.19.2",
|
||||||
|
"http-proxy-middleware": "^3.0.0"
|
||||||
},
|
},
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public"
|
"access": "public"
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import {
|
|||||||
} 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';
|
||||||
import { WebDevServerOptions } from '@nx/webpack/src/executors/dev-server/schema';
|
import { ModuleFederationDevServerOptions } from './schema';
|
||||||
import {
|
import {
|
||||||
getModuleFederationConfig,
|
getModuleFederationConfig,
|
||||||
getRemotes,
|
getRemotes,
|
||||||
@ -18,31 +18,14 @@ import {
|
|||||||
createAsyncIterable,
|
createAsyncIterable,
|
||||||
} 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 { workspaceDataDirectory } from 'nx/src/utils/cache-directory';
|
import { cpSync, existsSync } from 'fs';
|
||||||
import { fork } from 'node:child_process';
|
import { extname, join } from 'path';
|
||||||
import { cpSync, existsSync, createWriteStream } from 'fs';
|
|
||||||
import { join, 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,
|
||||||
type StaticRemotesConfig,
|
type StaticRemotesConfig,
|
||||||
} from '@nx/webpack/src/utils/module-federation/parse-static-remotes-config';
|
} from '@nx/webpack/src/utils/module-federation/parse-static-remotes-config';
|
||||||
|
import { buildStaticRemotes } from '../../utils/build-static.remotes';
|
||||||
type ModuleFederationDevServerOptions = WebDevServerOptions & {
|
|
||||||
devRemotes?: (
|
|
||||||
| string
|
|
||||||
| {
|
|
||||||
remoteName: string;
|
|
||||||
configuration: string;
|
|
||||||
}
|
|
||||||
)[];
|
|
||||||
skipRemotes?: string[];
|
|
||||||
static?: boolean;
|
|
||||||
isInitialHost?: boolean;
|
|
||||||
parallel?: number;
|
|
||||||
staticRemotesPort?: number;
|
|
||||||
pathToManifestFile?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
function getBuildOptions(buildTarget: string, context: ExecutorContext) {
|
function getBuildOptions(buildTarget: string, context: ExecutorContext) {
|
||||||
const target = parseTargetString(buildTarget, context);
|
const target = parseTargetString(buildTarget, context);
|
||||||
@ -170,93 +153,6 @@ async function startRemotes(
|
|||||||
return remoteIters;
|
return remoteIters;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function buildStaticRemotes(
|
|
||||||
staticRemotesConfig: StaticRemotesConfig,
|
|
||||||
nxBin,
|
|
||||||
context: ExecutorContext,
|
|
||||||
options: ModuleFederationDevServerOptions
|
|
||||||
) {
|
|
||||||
if (!staticRemotesConfig.remotes.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
logger.info(
|
|
||||||
`NX Building ${staticRemotesConfig.remotes.length} static remotes...`
|
|
||||||
);
|
|
||||||
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>((res, rej) => {
|
|
||||||
const staticProcess = fork(
|
|
||||||
nxBin,
|
|
||||||
[
|
|
||||||
'run-many',
|
|
||||||
`--target=build`,
|
|
||||||
`--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 build')) {
|
|
||||||
staticProcess.stdout.removeAllListeners('data');
|
|
||||||
logger.info(
|
|
||||||
`NX Built ${staticRemotesConfig.remotes.length} static remotes`
|
|
||||||
);
|
|
||||||
res();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
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) {
|
|
||||||
rej(
|
|
||||||
`Remote failed to start. A complete log can be found in: ${remoteBuildLogFile}`
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
res();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
process.on('SIGTERM', () => staticProcess.kill('SIGTERM'));
|
|
||||||
process.on('exit', () => staticProcess.kill('SIGTERM'));
|
|
||||||
});
|
|
||||||
|
|
||||||
return mappedLocationOfRemotes;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default async function* moduleFederationDevServer(
|
export default async function* moduleFederationDevServer(
|
||||||
options: ModuleFederationDevServerOptions,
|
options: ModuleFederationDevServerOptions,
|
||||||
context: ExecutorContext
|
context: ExecutorContext
|
||||||
|
|||||||
17
packages/react/src/executors/module-federation-dev-server/schema.d.ts
vendored
Normal file
17
packages/react/src/executors/module-federation-dev-server/schema.d.ts
vendored
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { WebDevServerOptions } from '@nx/webpack';
|
||||||
|
|
||||||
|
export type ModuleFederationDevServerOptions = WebDevServerOptions & {
|
||||||
|
devRemotes?: (
|
||||||
|
| string
|
||||||
|
| {
|
||||||
|
remoteName: string;
|
||||||
|
configuration: string;
|
||||||
|
}
|
||||||
|
)[];
|
||||||
|
skipRemotes?: string[];
|
||||||
|
static?: boolean;
|
||||||
|
isInitialHost?: boolean;
|
||||||
|
parallel?: number;
|
||||||
|
staticRemotesPort?: number;
|
||||||
|
pathToManifestFile?: string;
|
||||||
|
};
|
||||||
@ -0,0 +1,392 @@
|
|||||||
|
import { ModuleFederationStaticServerSchema } from './schema';
|
||||||
|
import { ModuleFederationDevServerOptions } from '../module-federation-dev-server/schema';
|
||||||
|
import { ExecutorContext } from 'nx/src/config/misc-interfaces';
|
||||||
|
import { basename, extname, join } from 'path';
|
||||||
|
import {
|
||||||
|
logger,
|
||||||
|
parseTargetString,
|
||||||
|
readTargetOptions,
|
||||||
|
Target,
|
||||||
|
workspaceRoot,
|
||||||
|
} from '@nx/devkit';
|
||||||
|
import { cpSync, existsSync, readFileSync, rmSync } from 'fs';
|
||||||
|
import {
|
||||||
|
getModuleFederationConfig,
|
||||||
|
getRemotes,
|
||||||
|
} from '@nx/webpack/src/utils/module-federation';
|
||||||
|
import {
|
||||||
|
parseStaticRemotesConfig,
|
||||||
|
StaticRemotesConfig,
|
||||||
|
} from '@nx/webpack/src/utils/module-federation/parse-static-remotes-config';
|
||||||
|
import { buildStaticRemotes } from '../../utils/build-static.remotes';
|
||||||
|
import { fork } from 'child_process';
|
||||||
|
import type { WebpackExecutorOptions } from '@nx/webpack';
|
||||||
|
import * as process from 'node:process';
|
||||||
|
import fileServerExecutor from '@nx/web/src/executors/file-server/file-server.impl';
|
||||||
|
import type { Express } from 'express';
|
||||||
|
import {
|
||||||
|
combineAsyncIterables,
|
||||||
|
createAsyncIterable,
|
||||||
|
} from '@nx/devkit/src/utils/async-iterable';
|
||||||
|
import { waitForPortOpen } from '@nx/web/src/utils/wait-for-port-open';
|
||||||
|
|
||||||
|
function getBuildAndServeOptionsFromServeTarget(
|
||||||
|
serveTarget: string,
|
||||||
|
context: ExecutorContext
|
||||||
|
) {
|
||||||
|
const target = parseTargetString(serveTarget, context);
|
||||||
|
|
||||||
|
const serveOptions: ModuleFederationDevServerOptions = readTargetOptions(
|
||||||
|
target,
|
||||||
|
context
|
||||||
|
);
|
||||||
|
const buildTarget = parseTargetString(serveOptions.buildTarget, context);
|
||||||
|
|
||||||
|
const buildOptions: WebpackExecutorOptions = readTargetOptions(
|
||||||
|
buildTarget,
|
||||||
|
context
|
||||||
|
);
|
||||||
|
|
||||||
|
let pathToManifestFile = join(
|
||||||
|
context.root,
|
||||||
|
context.projectGraph.nodes[context.projectName].data.sourceRoot,
|
||||||
|
'assets/module-federation.manifest.json'
|
||||||
|
);
|
||||||
|
if (serveOptions.pathToManifestFile) {
|
||||||
|
const userPathToManifestFile = join(
|
||||||
|
context.root,
|
||||||
|
serveOptions.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(serveOptions.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
buildTarget,
|
||||||
|
buildOptions,
|
||||||
|
serveOptions,
|
||||||
|
pathToManifestFile,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function buildHost(
|
||||||
|
nxBin: string,
|
||||||
|
buildTarget: Target,
|
||||||
|
context: ExecutorContext
|
||||||
|
) {
|
||||||
|
await new Promise<void>((res, rej) => {
|
||||||
|
const staticProcess = fork(
|
||||||
|
nxBin,
|
||||||
|
[
|
||||||
|
`run`,
|
||||||
|
`${buildTarget.project}:${buildTarget.target}${
|
||||||
|
buildTarget.configuration
|
||||||
|
? `:${buildTarget.configuration}`
|
||||||
|
: context.configurationName
|
||||||
|
? `:${context.configurationName}`
|
||||||
|
: ''
|
||||||
|
}`,
|
||||||
|
],
|
||||||
|
{
|
||||||
|
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, '');
|
||||||
|
|
||||||
|
// 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 build')) {
|
||||||
|
staticProcess.stdout.removeAllListeners('data');
|
||||||
|
logger.info(`NX Built host`);
|
||||||
|
res();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
staticProcess.stderr.on('data', (data) => logger.info(data.toString()));
|
||||||
|
staticProcess.once('exit', (code) => {
|
||||||
|
staticProcess.stdout.removeAllListeners('data');
|
||||||
|
staticProcess.stderr.removeAllListeners('data');
|
||||||
|
if (code !== 0) {
|
||||||
|
rej(`Host failed to build. See above for details.`);
|
||||||
|
} else {
|
||||||
|
res();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on('SIGTERM', () => staticProcess.kill('SIGTERM'));
|
||||||
|
process.on('exit', () => staticProcess.kill('SIGTERM'));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveToTmpDirectory(
|
||||||
|
staticRemotesConfig: StaticRemotesConfig,
|
||||||
|
hostOutputPath: string,
|
||||||
|
hostUrlSegment: string
|
||||||
|
) {
|
||||||
|
const commonOutputDirectory = join(
|
||||||
|
workspaceRoot,
|
||||||
|
'tmp/static-module-federation'
|
||||||
|
);
|
||||||
|
for (const app of staticRemotesConfig.remotes) {
|
||||||
|
const remoteConfig = staticRemotesConfig.config[app];
|
||||||
|
cpSync(
|
||||||
|
remoteConfig.outputPath,
|
||||||
|
join(commonOutputDirectory, remoteConfig.urlSegment),
|
||||||
|
{
|
||||||
|
force: true,
|
||||||
|
recursive: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
cpSync(hostOutputPath, join(commonOutputDirectory, hostUrlSegment), {
|
||||||
|
force: true,
|
||||||
|
recursive: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const cleanup = () => {
|
||||||
|
rmSync(commonOutputDirectory, { force: true, recursive: true });
|
||||||
|
};
|
||||||
|
process.on('SIGTERM', () => {
|
||||||
|
cleanup();
|
||||||
|
});
|
||||||
|
process.on('exit', () => {
|
||||||
|
cleanup();
|
||||||
|
});
|
||||||
|
|
||||||
|
return commonOutputDirectory;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function startProxies(
|
||||||
|
staticRemotesConfig: StaticRemotesConfig,
|
||||||
|
hostServeOptions: ModuleFederationDevServerOptions,
|
||||||
|
mappedLocationOfHost: string,
|
||||||
|
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();
|
||||||
|
expressProxy.use(
|
||||||
|
createProxyMiddleware({
|
||||||
|
target: mappedLocationsOfRemotes[app],
|
||||||
|
changeOrigin: true,
|
||||||
|
secure: sslCert ? false : undefined,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
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 Static remotes proxies started successfully`);
|
||||||
|
logger.info(`NX Starting static host proxy...`);
|
||||||
|
const expressProxy: Express = express();
|
||||||
|
expressProxy.use(
|
||||||
|
createProxyMiddleware({
|
||||||
|
target: mappedLocationOfHost,
|
||||||
|
changeOrigin: true,
|
||||||
|
secure: sslCert ? false : undefined,
|
||||||
|
pathRewrite: (path) => {
|
||||||
|
let pathRewrite = path;
|
||||||
|
for (const app of staticRemotesConfig.remotes) {
|
||||||
|
if (path.endsWith(app)) {
|
||||||
|
pathRewrite = '/';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pathRewrite;
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
const proxyServer = (sslCert ? https : http)
|
||||||
|
.createServer({ cert: sslCert, key: sslKey }, expressProxy)
|
||||||
|
.listen(hostServeOptions.port);
|
||||||
|
process.on('SIGTERM', () => proxyServer.close());
|
||||||
|
process.on('exit', () => proxyServer.close());
|
||||||
|
logger.info('NX Static host proxy started successfully');
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function* moduleFederationStaticServer(
|
||||||
|
schema: ModuleFederationStaticServerSchema,
|
||||||
|
context: ExecutorContext
|
||||||
|
) {
|
||||||
|
// Force Node to resolve to look for the nx binary that is inside node_modules
|
||||||
|
const nxBin = require.resolve('nx/bin/nx');
|
||||||
|
|
||||||
|
// Get the remotes from the module federation config
|
||||||
|
const p = context.projectsConfigurations.projects[context.projectName];
|
||||||
|
const options = getBuildAndServeOptionsFromServeTarget(
|
||||||
|
schema.serveTarget,
|
||||||
|
context
|
||||||
|
);
|
||||||
|
|
||||||
|
const moduleFederationConfig = getModuleFederationConfig(
|
||||||
|
options.buildOptions.tsConfig,
|
||||||
|
context.root,
|
||||||
|
p.root,
|
||||||
|
'react'
|
||||||
|
);
|
||||||
|
|
||||||
|
const remotes = getRemotes(
|
||||||
|
[],
|
||||||
|
options.serveOptions.skipRemotes,
|
||||||
|
moduleFederationConfig,
|
||||||
|
{
|
||||||
|
projectName: context.projectName,
|
||||||
|
projectGraph: context.projectGraph,
|
||||||
|
root: context.root,
|
||||||
|
},
|
||||||
|
options.pathToManifestFile
|
||||||
|
);
|
||||||
|
|
||||||
|
const staticRemotesConfig = parseStaticRemotesConfig(
|
||||||
|
[...remotes.staticRemotes, ...remotes.dynamicRemotes],
|
||||||
|
context
|
||||||
|
);
|
||||||
|
|
||||||
|
options.serveOptions.staticRemotesPort ??= remotes.staticRemotePort;
|
||||||
|
const mappedLocationsOfStaticRemotes = await buildStaticRemotes(
|
||||||
|
staticRemotesConfig,
|
||||||
|
nxBin,
|
||||||
|
context,
|
||||||
|
options.serveOptions
|
||||||
|
);
|
||||||
|
|
||||||
|
// Build the host
|
||||||
|
const hostUrlSegment = basename(options.buildOptions.outputPath);
|
||||||
|
const mappedLocationOfHost = `http${options.serveOptions.ssl ? 's' : ''}://${
|
||||||
|
options.serveOptions.host
|
||||||
|
}:${options.serveOptions.staticRemotesPort}/${hostUrlSegment}`;
|
||||||
|
await buildHost(nxBin, options.buildTarget, context);
|
||||||
|
|
||||||
|
// Move to a temporary directory
|
||||||
|
const commonOutputDirectory = moveToTmpDirectory(
|
||||||
|
staticRemotesConfig,
|
||||||
|
options.buildOptions.outputPath,
|
||||||
|
hostUrlSegment
|
||||||
|
);
|
||||||
|
|
||||||
|
// File Serve the temporary directory
|
||||||
|
const staticFileServerIter = fileServerExecutor(
|
||||||
|
{
|
||||||
|
cors: true,
|
||||||
|
watch: false,
|
||||||
|
staticFilePath: commonOutputDirectory,
|
||||||
|
parallel: false,
|
||||||
|
spa: false,
|
||||||
|
withDeps: false,
|
||||||
|
host: options.serveOptions.host,
|
||||||
|
port: options.serveOptions.staticRemotesPort,
|
||||||
|
ssl: options.serveOptions.ssl,
|
||||||
|
sslCert: options.serveOptions.sslCert,
|
||||||
|
sslKey: options.serveOptions.sslKey,
|
||||||
|
cacheSeconds: -1,
|
||||||
|
},
|
||||||
|
context
|
||||||
|
);
|
||||||
|
|
||||||
|
// express proxy all of it
|
||||||
|
startProxies(
|
||||||
|
staticRemotesConfig,
|
||||||
|
options.serveOptions,
|
||||||
|
mappedLocationOfHost,
|
||||||
|
mappedLocationsOfStaticRemotes,
|
||||||
|
options.serveOptions.ssl
|
||||||
|
? {
|
||||||
|
pathToCert: join(workspaceRoot, options.serveOptions.sslCert),
|
||||||
|
pathToKey: join(workspaceRoot, options.serveOptions.sslKey),
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
return yield* combineAsyncIterables(
|
||||||
|
staticFileServerIter,
|
||||||
|
createAsyncIterable<{ success: true; baseUrl: string }>(
|
||||||
|
async ({ next, done }) => {
|
||||||
|
const host = options.serveOptions.host ?? 'localhost';
|
||||||
|
const baseUrl = `http${options.serveOptions.ssl ? 's' : ''}://${host}:${
|
||||||
|
options.serveOptions.port
|
||||||
|
}`;
|
||||||
|
|
||||||
|
if (remotes.remotePorts.length === 0) {
|
||||||
|
const portsToWaitFor = [options.serveOptions.staticRemotesPort];
|
||||||
|
await Promise.all(
|
||||||
|
portsToWaitFor.map((port) =>
|
||||||
|
waitForPortOpen(port, {
|
||||||
|
retries: 480,
|
||||||
|
retryDelay: 2500,
|
||||||
|
host: host,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.info(`NX Server ready at ${baseUrl}`);
|
||||||
|
next({ success: true, baseUrl: baseUrl });
|
||||||
|
done();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const portsToWaitFor = staticFileServerIter
|
||||||
|
? [options.serveOptions.staticRemotesPort, ...remotes.remotePorts]
|
||||||
|
: [...remotes.remotePorts];
|
||||||
|
await Promise.all(
|
||||||
|
portsToWaitFor.map((port) =>
|
||||||
|
waitForPortOpen(port, {
|
||||||
|
retries: 480,
|
||||||
|
retryDelay: 2500,
|
||||||
|
host: host,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.info(`NX Server ready at ${baseUrl}`);
|
||||||
|
next({ success: true, baseUrl: baseUrl });
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error(`Failed to start. Check above for any errors.`, {
|
||||||
|
cause: err,
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
3
packages/react/src/executors/module-federation-static-server/schema.d.ts
vendored
Normal file
3
packages/react/src/executors/module-federation-static-server/schema.d.ts
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export interface ModuleFederationStaticServerSchema {
|
||||||
|
serveTarget: string;
|
||||||
|
}
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"version": 2,
|
||||||
|
"outputCapture": "direct-nodejs",
|
||||||
|
"title": "Module Federation Static Dev Server",
|
||||||
|
"description": "Serve a host application statically along with it's remotes.",
|
||||||
|
"cli": "nx",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"serveTarget": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["serveTarget"]
|
||||||
|
}
|
||||||
@ -20,7 +20,7 @@ export function updateModuleFederationProject(
|
|||||||
dynamic?: boolean;
|
dynamic?: boolean;
|
||||||
bundler?: 'rspack' | 'webpack';
|
bundler?: 'rspack' | 'webpack';
|
||||||
}
|
}
|
||||||
): GeneratorCallback {
|
) {
|
||||||
const projectConfig = readProjectConfiguration(host, options.projectName);
|
const projectConfig = readProjectConfiguration(host, options.projectName);
|
||||||
|
|
||||||
if (options.bundler === 'rspack') {
|
if (options.bundler === 'rspack') {
|
||||||
@ -101,25 +101,25 @@ export function updateModuleFederationProject(
|
|||||||
projectConfig.targets.serve.options.port = options.devServerPort;
|
projectConfig.targets.serve.options.port = options.devServerPort;
|
||||||
|
|
||||||
// `serve-static` for remotes that don't need to be in development mode
|
// `serve-static` for remotes that don't need to be in development mode
|
||||||
|
const serveStaticExecutor =
|
||||||
|
options.bundler === 'rspack'
|
||||||
|
? '@nx/rspack:module-federation-static-server'
|
||||||
|
: '@nx/react:module-federation-static-server';
|
||||||
projectConfig.targets['serve-static'] = {
|
projectConfig.targets['serve-static'] = {
|
||||||
executor: '@nx/web:file-server',
|
executor: serveStaticExecutor,
|
||||||
defaultConfiguration: 'production',
|
defaultConfiguration: 'production',
|
||||||
options: {
|
options: {
|
||||||
buildTarget: `${options.projectName}:build`,
|
serveTarget: `${options.projectName}:serve`,
|
||||||
watch: false,
|
|
||||||
port: options.devServerPort,
|
|
||||||
},
|
},
|
||||||
configurations: {
|
configurations: {
|
||||||
development: {
|
development: {
|
||||||
buildTarget: `${options.projectName}:build:development`,
|
serveTarget: `${options.projectName}:serve:development`,
|
||||||
},
|
},
|
||||||
production: {
|
production: {
|
||||||
buildTarget: `${options.projectName}:build:production`,
|
serveTarget: `${options.projectName}:serve:production`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
updateProjectConfiguration(host, options.projectName, projectConfig);
|
updateProjectConfiguration(host, options.projectName, projectConfig);
|
||||||
|
|
||||||
return addDependenciesToPackageJson(host, {}, { '@nx/web': nxVersion });
|
|
||||||
}
|
}
|
||||||
|
|||||||
95
packages/react/src/utils/build-static.remotes.ts
Normal file
95
packages/react/src/utils/build-static.remotes.ts
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
import type { StaticRemotesConfig } from '@nx/webpack/src/utils/module-federation/parse-static-remotes-config';
|
||||||
|
import { ExecutorContext } from '@nx/devkit';
|
||||||
|
import { ModuleFederationDevServerOptions } from '../executors/module-federation-dev-server/schema';
|
||||||
|
import { logger } from 'nx/src/utils/logger';
|
||||||
|
import { fork } from 'node:child_process';
|
||||||
|
import { join } from 'path';
|
||||||
|
import { workspaceDataDirectory } from 'nx/src/utils/cache-directory';
|
||||||
|
import { createWriteStream } from 'fs';
|
||||||
|
|
||||||
|
export async function buildStaticRemotes(
|
||||||
|
staticRemotesConfig: StaticRemotesConfig,
|
||||||
|
nxBin,
|
||||||
|
context: ExecutorContext,
|
||||||
|
options: ModuleFederationDevServerOptions
|
||||||
|
) {
|
||||||
|
if (!staticRemotesConfig.remotes.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
logger.info(
|
||||||
|
`NX Building ${staticRemotesConfig.remotes.length} static remotes...`
|
||||||
|
);
|
||||||
|
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>((res, rej) => {
|
||||||
|
const staticProcess = fork(
|
||||||
|
nxBin,
|
||||||
|
[
|
||||||
|
'run-many',
|
||||||
|
`--target=build`,
|
||||||
|
`--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 build')) {
|
||||||
|
staticProcess.stdout.removeAllListeners('data');
|
||||||
|
logger.info(
|
||||||
|
`NX Built ${staticRemotesConfig.remotes.length} static remotes`
|
||||||
|
);
|
||||||
|
res();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
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) {
|
||||||
|
rej(
|
||||||
|
`Remote failed to start. A complete log can be found in: ${remoteBuildLogFile}`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
res();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
process.on('SIGTERM', () => staticProcess.kill('SIGTERM'));
|
||||||
|
process.on('exit', () => staticProcess.kill('SIGTERM'));
|
||||||
|
});
|
||||||
|
|
||||||
|
return mappedLocationOfRemotes;
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user