Skip to content

Commit

Permalink
feat(angular): switch default to typescript configuration for module …
Browse files Browse the repository at this point in the history
…federation
  • Loading branch information
Coly010 committed Sep 19, 2023
1 parent 64c410d commit b53ffbb
Show file tree
Hide file tree
Showing 35 changed files with 1,792 additions and 42 deletions.
5 changes: 5 additions & 0 deletions docs/generated/packages/angular/generators/host.json
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,11 @@
"type": "boolean",
"default": false,
"x-priority": "important"
},
"typescriptConfiguration": {
"type": "boolean",
"description": "Whether the module federation configuration and webpack configuration files should use TS.",
"default": true
}
},
"additionalProperties": false,
Expand Down
5 changes: 5 additions & 0 deletions docs/generated/packages/angular/generators/remote.json
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,11 @@
"description": "Whether to configure SSR for the remote application to be consumed by a host application using SSR.",
"type": "boolean",
"default": false
},
"typescriptConfiguration": {
"type": "boolean",
"description": "Whether the module federation configuration and webpack configuration files should use TS.",
"default": true
}
},
"additionalProperties": false,
Expand Down
5 changes: 5 additions & 0 deletions docs/generated/packages/angular/generators/setup-mf.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,11 @@
"type": "boolean",
"description": "Whether the application is a standalone application. _Note: This is only supported in Angular versions >= 14.1.0_",
"default": false
},
"typescriptConfiguration": {
"type": "boolean",
"description": "Whether the module federation configuration and webpack configuration files should use TS.",
"default": true
}
},
"required": ["appName", "mfType"],
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import type { Schema } from './schema';
import { logger, readCachedProjectGraph, workspaceRoot } from '@nx/devkit';
import { scheduleTarget } from 'nx/src/adapter/ngcli-adapter';
import { executeWebpackDevServerBuilder } from '../webpack-dev-server/webpack-dev-server.impl';
import { readProjectsConfigurationFromProjectGraph } from 'nx/src/project-graph/project-graph';
import { getExecutorInformation } from 'nx/src/command-line/run/executor-utils';
Expand All @@ -12,11 +11,15 @@ import {
import { existsSync } from 'fs';
import { extname, join } from 'path';
import { findMatchingProjects } from 'nx/src/utils/find-matching-projects';
import { fork } from 'child_process';
import { waitForPortOpen } from '../utilities/wait-for-port';
import { from, switchMap } from 'rxjs';

export function executeModuleFederationDevServerBuilder(
schema: Schema,
context: import('@angular-devkit/architect').BuilderContext
): ReturnType<typeof executeWebpackDevServerBuilder> {
const nxBin = require.resolve('nx');
const { ...options } = schema;
const projectGraph = readCachedProjectGraph();
const { projects: workspaceProjects } =
Expand Down Expand Up @@ -78,9 +81,14 @@ export function executeModuleFederationDevServerBuilder(
? findMatchingProjects(options.devRemotes, projectGraph.nodes)
: findMatchingProjects([options.devRemotes], projectGraph.nodes);

let isCollectingStaticRemoteOutput = true;
const remotePorts: Set<number> = new Set();
for (const remote of remotes) {
const isDev = devServeRemotes.includes(remote);
const target = isDev ? 'serve' : 'serve-static';
remotePorts.add(
projectGraph.nodes[remote].data.targets[target].options.port
);

if (!workspaceProjects[remote].targets?.[target]) {
throw new Error(
Expand All @@ -107,25 +115,71 @@ export function executeModuleFederationDevServerBuilder(
}
}

scheduleTarget(
context.workspaceRoot,
let outWithErr: null | string[] = [];
const remoteProcess = fork(
nxBin,
[
'run',
`${remote}:${target}${
context.target.configuration ? `:${context.target.configuration}` : ''
}`,
...(runOptions.verbose ? [`--verbose`] : []),
],
{
project: remote,
target,
configuration: context.target.configuration,
runOptions,
},
options.verbose
).then((obs) => {
obs.toPromise().catch((err) => {
throw new Error(
`Remote '${remote}' failed to serve correctly due to the following: \r\n${err.toString()}`
);
});
cwd: context.workspaceRoot,
stdio: ['ignore', 'pipe', 'pipe', 'ipc'],
}
);

remoteProcess.stdout.on('data', (data) => {
if (isCollectingStaticRemoteOutput) {
outWithErr.push(data.toString());
} else {
outWithErr = null;
remoteProcess.stdout.removeAllListeners('data');
}
});
remoteProcess.stderr.on('data', (data) => logger.info(data.toString()));
remoteProcess.on('exit', (code) => {
if (code !== 0) {
logger.info(outWithErr.join(''));
throw new Error(`Remote failed to start. See above for errors.`);
}
});
process.on('SIGTERM', () => remoteProcess.kill('SIGTERM'));
process.on('exit', () => remoteProcess.kill('SIGTERM'));
}

return executeWebpackDevServerBuilder(options, context);
const waitForRemotes = async () => {
if (remotePorts.size === 0) {
return true;
}
try {
await Promise.all(
[...remotePorts.values()].map((port) =>
// Allow 20 minutes for each remote to start, which is plenty of time but we can tweak it later if needed.
// Most remotes should start in under 1 minute.
waitForPortOpen(port, {
retries: 480,
retryDelay: 2500,
host: 'localhost',
})
)
);
isCollectingStaticRemoteOutput = false;
logger.info(`NX All remotes started`);

return true;
} catch {
throw new Error(
`Timed out waiting for remote to start. Check above for any errors.`
);
}
};

return from(waitForRemotes()).pipe(
switchMap(() => executeWebpackDevServerBuilder(options, context))
);
}

export default require('@angular-devkit/architect').createBuilder(
Expand Down
40 changes: 40 additions & 0 deletions packages/angular/src/builders/utilities/wait-for-port.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import * as net from 'net';

export function waitForPortOpen(
port: number,
options: { host?: string; retries?: number; retryDelay?: number } = {}
): Promise<void> {
const allowedErrorCodes = ['ECONNREFUSED', 'ECONNRESET'];

return new Promise((resolve, reject) => {
const checkPort = (retries = options.retries ?? 120) => {
const client = new net.Socket();
const cleanupClient = () => {
client.removeAllListeners('connect');
client.removeAllListeners('error');
client.end();
client.destroy();
client.unref();
};
client.once('connect', () => {
cleanupClient();
resolve();
});

client.once('error', (err) => {
if (retries === 0 || !allowedErrorCodes.includes(err['code'])) {
cleanupClient();
reject(err);
} else {
setTimeout(() => checkPort(retries - 1), options.retryDelay ?? 1000);
}
});

// Node will use IPv6 if it is available, but this can cause issues if the server is only listening on IPv4.
// Hard-coding to look on 127.0.0.1 to avoid using the IPv6 loopback address "::1".
client.connect({ port, host: options.host ?? '127.0.0.1' });
};

checkPort();
});
}
Loading

0 comments on commit b53ffbb

Please sign in to comment.