feat(angular): add support for rspack module federation (#31231)
## Current Behavior We currently have no method for generating Angular Rspack Module Federation applications ## Expected Behavior Update the `host` and `remote` generators to support a `--bundler` flag to allow users to select Rspack as their bundler method
This commit is contained in:
parent
a52a4356df
commit
43a20e2ecc
@ -37,6 +37,12 @@
|
||||
"x-priority": "important",
|
||||
"alias": "producers"
|
||||
},
|
||||
"bundler": {
|
||||
"type": "string",
|
||||
"description": "The bundler to use for the host application.",
|
||||
"default": "webpack",
|
||||
"enum": ["webpack", "rspack"]
|
||||
},
|
||||
"dynamic": {
|
||||
"type": "boolean",
|
||||
"description": "Should the host application use dynamic federation?",
|
||||
|
||||
@ -42,6 +42,12 @@
|
||||
"type": "number",
|
||||
"description": "The port on which this app should be served."
|
||||
},
|
||||
"bundler": {
|
||||
"type": "string",
|
||||
"description": "The bundler to use for the remote application.",
|
||||
"default": "webpack",
|
||||
"enum": ["webpack", "rspack"]
|
||||
},
|
||||
"style": {
|
||||
"description": "The file extension to be used for style files.",
|
||||
"type": "string",
|
||||
|
||||
178
e2e/angular/src/module-federation.rspack.test.ts
Normal file
178
e2e/angular/src/module-federation.rspack.test.ts
Normal file
@ -0,0 +1,178 @@
|
||||
import { names } from '@nx/devkit';
|
||||
import {
|
||||
checkFilesExist,
|
||||
cleanupProject,
|
||||
killPorts,
|
||||
killProcessAndPorts,
|
||||
newProject,
|
||||
readFile,
|
||||
readJson,
|
||||
runCLI,
|
||||
runCommandUntil,
|
||||
runE2ETests,
|
||||
uniq,
|
||||
updateFile,
|
||||
updateJson,
|
||||
} from '@nx/e2e/utils';
|
||||
import { join } from 'path';
|
||||
|
||||
describe('Angular Module Federation', () => {
|
||||
let proj: string;
|
||||
let oldVerboseLoggingValue: string;
|
||||
|
||||
beforeAll(() => {
|
||||
proj = newProject({ packages: ['@nx/angular'] });
|
||||
oldVerboseLoggingValue = process.env.NX_E2E_VERBOSE_LOGGING;
|
||||
process.env.NX_E2E_VERBOSE_LOGGING = 'true';
|
||||
});
|
||||
afterAll(() => {
|
||||
cleanupProject();
|
||||
process.env.NX_E2E_VERBOSE_LOGGING = oldVerboseLoggingValue;
|
||||
});
|
||||
|
||||
it('should generate valid host and remote apps', async () => {
|
||||
const hostApp = uniq('app');
|
||||
const remoteApp1 = uniq('remote');
|
||||
const sharedLib = uniq('shared-lib');
|
||||
const wildcardLib = uniq('wildcard-lib');
|
||||
const secondaryEntry = uniq('secondary');
|
||||
const hostPort = 4300;
|
||||
const remotePort = 4301;
|
||||
|
||||
// generate host app
|
||||
runCLI(
|
||||
`generate @nx/angular:host ${hostApp} --style=css --bundler=rspack --no-standalone --no-interactive`
|
||||
);
|
||||
let rspackConfigFileContents = readFile(join(hostApp, 'rspack.config.ts'));
|
||||
let updatedConfigFileContents = rspackConfigFileContents.replace(
|
||||
`maximumError: '1mb'`,
|
||||
`maximumError: '11mb'`
|
||||
);
|
||||
updateFile(join(hostApp, 'rspack.config.ts'), updatedConfigFileContents);
|
||||
|
||||
// generate remote app
|
||||
runCLI(
|
||||
`generate @nx/angular:remote ${remoteApp1} --host=${hostApp} --bundler=rspack --port=${remotePort} --style=css --no-standalone --no-interactive`
|
||||
);
|
||||
rspackConfigFileContents = readFile(join(remoteApp1, 'rspack.config.ts'));
|
||||
updatedConfigFileContents = rspackConfigFileContents.replace(
|
||||
`maximumError: '1mb'`,
|
||||
`maximumError: '11mb'`
|
||||
);
|
||||
updateFile(join(remoteApp1, 'rspack.config.ts'), updatedConfigFileContents);
|
||||
|
||||
// check files are generated without the layout directory ("apps/")
|
||||
checkFilesExist(
|
||||
`${hostApp}/src/app/app.module.ts`,
|
||||
`${remoteApp1}/src/app/app.module.ts`
|
||||
);
|
||||
|
||||
// check default generated host is built successfully
|
||||
const buildOutput = runCLI(`build ${hostApp}`);
|
||||
expect(buildOutput).toContain('Successfully ran target build');
|
||||
|
||||
// generate a shared lib with a seconary entry point
|
||||
runCLI(
|
||||
`generate @nx/angular:library ${sharedLib} --buildable --no-standalone --no-interactive`
|
||||
);
|
||||
runCLI(
|
||||
`generate @nx/angular:library-secondary-entry-point --library=${sharedLib} --name=${secondaryEntry} --no-interactive`
|
||||
);
|
||||
|
||||
// Add a library that will be accessed via a wildcard in tspath mappings
|
||||
runCLI(
|
||||
`generate @nx/angular:library ${wildcardLib} --buildable --no-standalone --no-interactive`
|
||||
);
|
||||
|
||||
updateJson('tsconfig.base.json', (json) => {
|
||||
delete json.compilerOptions.paths[`@${proj}/${wildcardLib}`];
|
||||
json.compilerOptions.paths[`@${proj}/${wildcardLib}/*`] = [
|
||||
`${wildcardLib}/src/lib/*`,
|
||||
];
|
||||
return json;
|
||||
});
|
||||
|
||||
// update host & remote files to use shared library
|
||||
updateFile(
|
||||
`${hostApp}/src/app/app.module.ts`,
|
||||
`import { NgModule } from '@angular/core';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { ${
|
||||
names(wildcardLib).className
|
||||
}Module } from '@${proj}/${wildcardLib}/${
|
||||
names(secondaryEntry).fileName
|
||||
}.module';
|
||||
import { ${
|
||||
names(sharedLib).className
|
||||
}Module } from '@${proj}/${sharedLib}';
|
||||
import { ${
|
||||
names(secondaryEntry).className
|
||||
}Module } from '@${proj}/${sharedLib}/${secondaryEntry}';
|
||||
import { AppComponent } from './app.component';
|
||||
import { NxWelcomeComponent } from './nx-welcome.component';
|
||||
import { RouterModule } from '@angular/router';
|
||||
|
||||
@NgModule({
|
||||
declarations: [AppComponent, NxWelcomeComponent],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
${names(sharedLib).className}Module,
|
||||
${names(wildcardLib).className}Module,
|
||||
RouterModule.forRoot(
|
||||
[
|
||||
{
|
||||
path: '${remoteApp1}',
|
||||
loadChildren: () =>
|
||||
import('${remoteApp1}/Module').then(
|
||||
(m) => m.RemoteEntryModule
|
||||
),
|
||||
},
|
||||
],
|
||||
{ initialNavigation: 'enabledBlocking' }
|
||||
),
|
||||
],
|
||||
providers: [],
|
||||
bootstrap: [AppComponent],
|
||||
})
|
||||
export class AppModule {}
|
||||
`
|
||||
);
|
||||
updateFile(
|
||||
`${remoteApp1}/src/app/remote-entry/entry.module.ts`,
|
||||
`import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { ${names(sharedLib).className}Module } from '@${proj}/${sharedLib}';
|
||||
import { ${
|
||||
names(secondaryEntry).className
|
||||
}Module } from '@${proj}/${sharedLib}/${secondaryEntry}';
|
||||
import { RemoteEntryComponent } from './entry.component';
|
||||
import { NxWelcomeComponent } from './nx-welcome.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [RemoteEntryComponent, NxWelcomeComponent],
|
||||
imports: [
|
||||
CommonModule,
|
||||
${names(sharedLib).className}Module,
|
||||
RouterModule.forChild([
|
||||
{
|
||||
path: '',
|
||||
component: RemoteEntryComponent,
|
||||
},
|
||||
]),
|
||||
],
|
||||
providers: [],
|
||||
})
|
||||
export class RemoteEntryModule {}
|
||||
`
|
||||
);
|
||||
|
||||
const processSwc = await runCommandUntil(
|
||||
`serve ${remoteApp1}`,
|
||||
(output) =>
|
||||
!output.includes(`Remote '${remoteApp1}' failed to serve correctly`) &&
|
||||
output.includes(`Build at:`)
|
||||
);
|
||||
await killProcessAndPorts(processSwc.pid, remotePort);
|
||||
}, 20_000_000);
|
||||
});
|
||||
@ -17,6 +17,7 @@ import {
|
||||
angularRspackVersion,
|
||||
nxVersion,
|
||||
tsNodeVersion,
|
||||
webpackMergeVersion,
|
||||
} from '../../utils/versions';
|
||||
import { createConfig } from './lib/create-config';
|
||||
import { getCustomWebpackConfig } from './lib/get-custom-webpack-config';
|
||||
@ -47,12 +48,7 @@ const RENAMED_OPTIONS = {
|
||||
|
||||
const DEFAULT_PORT = 4200;
|
||||
|
||||
const REMOVED_OPTIONS = [
|
||||
'buildOptimizer',
|
||||
'buildTarget',
|
||||
'browserTarget',
|
||||
'publicHost',
|
||||
];
|
||||
const REMOVED_OPTIONS = ['buildOptimizer', 'buildTarget', 'browserTarget'];
|
||||
|
||||
function normalizeFromProjectRoot(
|
||||
tree: Tree,
|
||||
@ -506,6 +502,7 @@ export async function convertToRspack(
|
||||
{},
|
||||
{
|
||||
'@nx/angular-rspack': angularRspackVersion,
|
||||
'webpack-merge': webpackMergeVersion,
|
||||
'ts-node': tsNodeVersion,
|
||||
}
|
||||
);
|
||||
|
||||
@ -16,7 +16,7 @@ describe('convertconvertWebpackConfigToUseNxModuleFederationPlugin', () => {
|
||||
// ASSERT
|
||||
expect(newWebpackConfigContents).toMatchInlineSnapshot(`
|
||||
"
|
||||
import { NxModuleFederationPlugin, NxModuleFederationDevServerPlugin } from '@nx/module-federation/rspack';
|
||||
import { NxModuleFederationPlugin, NxModuleFederationDevServerPlugin } from '@nx/module-federation/angular';
|
||||
import config from './module-federation.config';
|
||||
|
||||
|
||||
|
||||
@ -56,7 +56,7 @@ export function convertWebpackConfigToUseNxModuleFederationPlugin(
|
||||
newWebpackConfigContents = `${webpackConfigContents.slice(
|
||||
0,
|
||||
withModuleFederationImportNode.getStart()
|
||||
)}import { NxModuleFederationPlugin, NxModuleFederationDevServerPlugin } from '@nx/module-federation/rspack';${webpackConfigContents.slice(
|
||||
)}import { NxModuleFederationPlugin, NxModuleFederationDevServerPlugin } from '@nx/module-federation/angular';${webpackConfigContents.slice(
|
||||
withModuleFederationImportNode.getEnd()
|
||||
)}`;
|
||||
|
||||
|
||||
@ -2,8 +2,10 @@ import {
|
||||
formatFiles,
|
||||
getProjects,
|
||||
joinPathFragments,
|
||||
readProjectConfiguration,
|
||||
runTasksInSerial,
|
||||
Tree,
|
||||
updateProjectConfiguration,
|
||||
} from '@nx/devkit';
|
||||
import {
|
||||
determineProjectNameAndRootOptions,
|
||||
@ -18,9 +20,19 @@ import { setupMf } from '../setup-mf/setup-mf';
|
||||
import { addMfEnvToTargetDefaultInputs } from '../utils/add-mf-env-to-inputs';
|
||||
import { updateSsrSetup } from './lib';
|
||||
import type { Schema } from './schema';
|
||||
import { assertRspackIsCSR } from '../utils/assert-mf-utils';
|
||||
import convertToRspack from '../convert-to-rspack/convert-to-rspack';
|
||||
|
||||
export async function host(tree: Tree, schema: Schema) {
|
||||
assertNotUsingTsSolutionSetup(tree, 'angular', 'host');
|
||||
// TODO: Replace with Rspack when confidence is high enough
|
||||
schema.bundler ??= 'webpack';
|
||||
const isRspack = schema.bundler === 'rspack';
|
||||
assertRspackIsCSR(
|
||||
schema.bundler,
|
||||
schema.ssr ?? false,
|
||||
schema.serverRouting ?? false
|
||||
);
|
||||
|
||||
const { typescriptConfiguration = true, ...options }: Schema = schema;
|
||||
options.standalone = options.standalone ?? true;
|
||||
@ -100,7 +112,8 @@ export async function host(tree: Tree, schema: Schema) {
|
||||
installTasks.push(ssrInstallTask);
|
||||
}
|
||||
|
||||
for (const remote of remotesToGenerate) {
|
||||
for (let i = 0; i < remotesToGenerate.length; i++) {
|
||||
const remote = remotesToGenerate[i];
|
||||
const remoteDirectory = options.directory
|
||||
? joinPathFragments(options.directory, '..', remote)
|
||||
: appRoot === '.'
|
||||
@ -111,6 +124,7 @@ export async function host(tree: Tree, schema: Schema) {
|
||||
name: remote,
|
||||
directory: remoteDirectory,
|
||||
host: hostProjectName,
|
||||
port: isRspack ? 4200 + i + 1 : undefined,
|
||||
skipFormat: true,
|
||||
standalone: options.standalone,
|
||||
typescriptConfiguration,
|
||||
@ -119,6 +133,20 @@ export async function host(tree: Tree, schema: Schema) {
|
||||
|
||||
addMfEnvToTargetDefaultInputs(tree);
|
||||
|
||||
if (isRspack) {
|
||||
await convertToRspack(tree, {
|
||||
project: hostProjectName,
|
||||
skipInstall: options.skipPackageJson,
|
||||
skipFormat: true,
|
||||
});
|
||||
}
|
||||
|
||||
const project = readProjectConfiguration(tree, hostProjectName);
|
||||
project.targets.serve ??= {};
|
||||
project.targets.serve.options ??= {};
|
||||
project.targets.serve.options.port = 4200;
|
||||
updateProjectConfiguration(tree, hostProjectName, project);
|
||||
|
||||
if (!options.skipFormat) {
|
||||
await formatFiles(tree);
|
||||
}
|
||||
|
||||
@ -5,6 +5,7 @@ import type { Styles } from '../utils/types';
|
||||
export interface Schema {
|
||||
directory: string;
|
||||
name?: string;
|
||||
bundler?: 'webpack' | 'rspack';
|
||||
remotes?: string[];
|
||||
dynamic?: boolean;
|
||||
setParserOptionsProject?: boolean;
|
||||
|
||||
@ -37,6 +37,12 @@
|
||||
"x-priority": "important",
|
||||
"alias": "producers"
|
||||
},
|
||||
"bundler": {
|
||||
"type": "string",
|
||||
"description": "The bundler to use for the host application.",
|
||||
"default": "webpack",
|
||||
"enum": ["webpack", "rspack"]
|
||||
},
|
||||
"dynamic": {
|
||||
"type": "boolean",
|
||||
"description": "Should the host application use dynamic federation?",
|
||||
|
||||
@ -2,9 +2,11 @@ import {
|
||||
addDependenciesToPackageJson,
|
||||
formatFiles,
|
||||
getProjects,
|
||||
readProjectConfiguration,
|
||||
runTasksInSerial,
|
||||
stripIndents,
|
||||
Tree,
|
||||
updateProjectConfiguration,
|
||||
} from '@nx/devkit';
|
||||
import {
|
||||
determineProjectNameAndRootOptions,
|
||||
@ -18,9 +20,19 @@ import { setupMf } from '../setup-mf/setup-mf';
|
||||
import { addMfEnvToTargetDefaultInputs } from '../utils/add-mf-env-to-inputs';
|
||||
import { findNextAvailablePort, updateSsrSetup } from './lib';
|
||||
import type { Schema } from './schema';
|
||||
import { assertRspackIsCSR } from '../utils/assert-mf-utils';
|
||||
import convertToRspack from '../convert-to-rspack/convert-to-rspack';
|
||||
|
||||
export async function remote(tree: Tree, schema: Schema) {
|
||||
assertNotUsingTsSolutionSetup(tree, 'angular', 'remote');
|
||||
// TODO: Replace with Rspack when confidence is high enough
|
||||
schema.bundler ??= 'webpack';
|
||||
const isRspack = schema.bundler === 'rspack';
|
||||
assertRspackIsCSR(
|
||||
schema.bundler,
|
||||
schema.ssr ?? false,
|
||||
schema.serverRouting ?? false
|
||||
);
|
||||
|
||||
const { typescriptConfiguration = true, ...options }: Schema = schema;
|
||||
options.standalone = options.standalone ?? true;
|
||||
@ -105,6 +117,24 @@ export async function remote(tree: Tree, schema: Schema) {
|
||||
|
||||
addMfEnvToTargetDefaultInputs(tree);
|
||||
|
||||
if (isRspack) {
|
||||
await convertToRspack(tree, {
|
||||
project: remoteProjectName,
|
||||
skipInstall: options.skipPackageJson,
|
||||
skipFormat: true,
|
||||
});
|
||||
}
|
||||
|
||||
const project = readProjectConfiguration(tree, remoteProjectName);
|
||||
project.targets.serve ??= {};
|
||||
project.targets.serve.options ??= {};
|
||||
if (options.host) {
|
||||
project.targets.serve.dependsOn ??= [];
|
||||
project.targets.serve.dependsOn.push(`${options.host}:serve`);
|
||||
}
|
||||
project.targets.serve.options.port = port;
|
||||
updateProjectConfiguration(tree, remoteProjectName, project);
|
||||
|
||||
if (!options.skipFormat) {
|
||||
await formatFiles(tree);
|
||||
}
|
||||
|
||||
@ -5,6 +5,7 @@ import type { Styles } from '../utils/types';
|
||||
export interface Schema {
|
||||
directory: string;
|
||||
name?: string;
|
||||
bundler?: 'webpack' | 'rspack';
|
||||
host?: string;
|
||||
port?: number;
|
||||
setParserOptionsProject?: boolean;
|
||||
|
||||
@ -42,6 +42,12 @@
|
||||
"type": "number",
|
||||
"description": "The port on which this app should be served."
|
||||
},
|
||||
"bundler": {
|
||||
"type": "string",
|
||||
"description": "The bundler to use for the remote application.",
|
||||
"default": "webpack",
|
||||
"enum": ["webpack", "rspack"]
|
||||
},
|
||||
"style": {
|
||||
"description": "The file extension to be used for style files.",
|
||||
"type": "string",
|
||||
|
||||
16
packages/angular/src/generators/utils/assert-mf-utils.ts
Normal file
16
packages/angular/src/generators/utils/assert-mf-utils.ts
Normal file
@ -0,0 +1,16 @@
|
||||
export function assertRspackIsCSR(
|
||||
bundler: 'webpack' | 'rspack',
|
||||
ssr: boolean,
|
||||
serverRouting: boolean
|
||||
) {
|
||||
if (bundler === 'rspack' && serverRouting) {
|
||||
throw new Error(
|
||||
'Server Routing is not currently supported for Angular Rspack Module Federation. Please use webpack instead.'
|
||||
);
|
||||
}
|
||||
if (bundler === 'rspack' && ssr) {
|
||||
throw new Error(
|
||||
'SSR is not currently supported for Angular Rspack Module Federation. Please use webpack instead.'
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -51,6 +51,7 @@ export const backwardCompatibleVersions: VersionMap = {
|
||||
typesNodeVersion: '18.16.9',
|
||||
jasmineMarblesVersion: '^0.9.2',
|
||||
jsoncEslintParserVersion: '^2.1.0',
|
||||
webpackMergeVersion: '^5.8.0',
|
||||
},
|
||||
angularV18: {
|
||||
angularVersion: '~18.2.0',
|
||||
@ -80,5 +81,6 @@ export const backwardCompatibleVersions: VersionMap = {
|
||||
typesNodeVersion: '18.16.9',
|
||||
jasmineMarblesVersion: '^0.9.2',
|
||||
jsoncEslintParserVersion: '^2.1.0',
|
||||
webpackMergeVersion: '^5.8.0',
|
||||
},
|
||||
};
|
||||
|
||||
@ -17,6 +17,7 @@ export const typesExpressVersion = '^4.17.21';
|
||||
export const browserSyncVersion = '^3.0.0';
|
||||
export const moduleFederationNodeVersion = '^2.6.26';
|
||||
export const moduleFederationEnhancedVersion = '^0.9.0';
|
||||
export const webpackMergeVersion = '^5.8.0';
|
||||
|
||||
export const angularEslintVersion = '^19.2.0';
|
||||
export const typescriptEslintVersion = '^7.16.0';
|
||||
|
||||
@ -1,2 +1,5 @@
|
||||
export * from './src/with-module-federation/angular/with-module-federation';
|
||||
export * from './src/with-module-federation/angular/with-module-federation-ssr';
|
||||
export * from './src/plugins/nx-module-federation-plugin/angular/nx-module-federation-plugin';
|
||||
export * from './src/plugins/nx-module-federation-plugin/angular/nx-module-federation-dev-server-plugin';
|
||||
export * from './src/plugins/nx-module-federation-plugin/angular/nx-module-federation-ssr-dev-server-plugin';
|
||||
|
||||
@ -0,0 +1,142 @@
|
||||
import {
|
||||
Compilation,
|
||||
Compiler,
|
||||
DefinePlugin,
|
||||
RspackPluginInstance,
|
||||
} from '@rspack/core';
|
||||
import * as pc from 'picocolors';
|
||||
import {
|
||||
logger,
|
||||
readCachedProjectGraph,
|
||||
readProjectsConfigurationFromProjectGraph,
|
||||
workspaceRoot,
|
||||
} from '@nx/devkit';
|
||||
import { ModuleFederationConfig } from '../../../utils/models';
|
||||
import { extname, join } from 'path';
|
||||
import { existsSync } from 'fs';
|
||||
import {
|
||||
buildStaticRemotes,
|
||||
getDynamicMfManifestFile,
|
||||
getRemotes,
|
||||
getStaticRemotes,
|
||||
parseRemotesConfig,
|
||||
startRemoteProxies,
|
||||
startStaticRemotesFileServer,
|
||||
} from '../../utils';
|
||||
import { NxModuleFederationDevServerConfig } from '../../models';
|
||||
|
||||
const PLUGIN_NAME = 'NxModuleFederationDevServerPlugin';
|
||||
|
||||
export class NxModuleFederationDevServerPlugin implements RspackPluginInstance {
|
||||
private nxBin = require.resolve('nx/bin/nx');
|
||||
|
||||
constructor(
|
||||
private _options: {
|
||||
config: ModuleFederationConfig;
|
||||
devServerConfig?: NxModuleFederationDevServerConfig;
|
||||
}
|
||||
) {
|
||||
this._options.devServerConfig ??= {
|
||||
host: 'localhost',
|
||||
};
|
||||
}
|
||||
|
||||
apply(compiler: Compiler) {
|
||||
const isDevServer = process.env['WEBPACK_SERVE'];
|
||||
if (!isDevServer) {
|
||||
return;
|
||||
}
|
||||
compiler.hooks.watchRun.tapAsync(
|
||||
PLUGIN_NAME,
|
||||
async (compiler, callback) => {
|
||||
compiler.hooks.beforeCompile.tapAsync(
|
||||
PLUGIN_NAME,
|
||||
async (params, callback) => {
|
||||
const staticRemotesConfig = await this.setup();
|
||||
|
||||
logger.info(
|
||||
`NX Starting module federation dev-server for ${pc.bold(
|
||||
this._options.config.name
|
||||
)} with ${Object.keys(staticRemotesConfig).length} remotes`
|
||||
);
|
||||
|
||||
const mappedLocationOfRemotes = await buildStaticRemotes(
|
||||
staticRemotesConfig,
|
||||
this._options.devServerConfig,
|
||||
this.nxBin
|
||||
);
|
||||
startStaticRemotesFileServer(
|
||||
staticRemotesConfig,
|
||||
workspaceRoot,
|
||||
this._options.devServerConfig.staticRemotesPort
|
||||
);
|
||||
startRemoteProxies(staticRemotesConfig, mappedLocationOfRemotes, {
|
||||
pathToCert: this._options.devServerConfig.sslCert,
|
||||
pathToKey: this._options.devServerConfig.sslCert,
|
||||
});
|
||||
|
||||
new DefinePlugin({
|
||||
'process.env.NX_MF_DEV_REMOTES': process.env.NX_MF_DEV_REMOTES,
|
||||
}).apply(compiler);
|
||||
|
||||
callback();
|
||||
}
|
||||
);
|
||||
callback();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private async setup() {
|
||||
const projectGraph = readCachedProjectGraph();
|
||||
const { projects: workspaceProjects } =
|
||||
readProjectsConfigurationFromProjectGraph(projectGraph);
|
||||
const project = workspaceProjects[this._options.config.name];
|
||||
if (!this._options.devServerConfig.pathToManifestFile) {
|
||||
this._options.devServerConfig.pathToManifestFile =
|
||||
getDynamicMfManifestFile(project, workspaceRoot);
|
||||
} else {
|
||||
const userPathToManifestFile = join(
|
||||
workspaceRoot,
|
||||
this._options.devServerConfig.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(this._options.devServerConfig.pathToManifestFile) !== '.json'
|
||||
) {
|
||||
throw new Error(
|
||||
`The Module Federation manifest file must be a JSON. Please ensure the file at ${userPathToManifestFile} is a JSON.`
|
||||
);
|
||||
}
|
||||
|
||||
this._options.devServerConfig.pathToManifestFile = userPathToManifestFile;
|
||||
}
|
||||
|
||||
const { remotes, staticRemotePort } = getRemotes(
|
||||
this._options.config,
|
||||
projectGraph,
|
||||
this._options.devServerConfig.pathToManifestFile
|
||||
);
|
||||
this._options.devServerConfig.staticRemotesPort ??= staticRemotePort;
|
||||
|
||||
const remotesConfig = parseRemotesConfig(
|
||||
remotes,
|
||||
workspaceRoot,
|
||||
projectGraph
|
||||
);
|
||||
const staticRemotesConfig = await getStaticRemotes(
|
||||
remotesConfig.config ?? {},
|
||||
this._options.devServerConfig?.devRemoteFindOptions,
|
||||
this._options.devServerConfig?.host
|
||||
);
|
||||
const devRemotes = remotes.filter((r) => !staticRemotesConfig[r]);
|
||||
process.env.NX_MF_DEV_REMOTES = JSON.stringify([
|
||||
...(devRemotes.length > 0 ? devRemotes : []),
|
||||
project.name,
|
||||
]);
|
||||
return staticRemotesConfig ?? {};
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,87 @@
|
||||
import { Compiler, RspackPluginInstance } from '@rspack/core';
|
||||
import {
|
||||
ModuleFederationConfig,
|
||||
NxModuleFederationConfigOverride,
|
||||
} from '../../../utils/models';
|
||||
import { getModuleFederationConfigSync } from '../../../with-module-federation/angular/utils';
|
||||
|
||||
export class NxModuleFederationPlugin implements RspackPluginInstance {
|
||||
constructor(
|
||||
private _options: {
|
||||
config: ModuleFederationConfig;
|
||||
isServer?: boolean;
|
||||
},
|
||||
private configOverride?: NxModuleFederationConfigOverride
|
||||
) {}
|
||||
|
||||
apply(compiler: Compiler) {
|
||||
if (global.NX_GRAPH_CREATION) {
|
||||
return;
|
||||
}
|
||||
|
||||
// This is required to ensure Module Federation will build the project correctly
|
||||
compiler.options.optimization ??= {};
|
||||
compiler.options.optimization.runtimeChunk = false;
|
||||
compiler.options.output.publicPath = !compiler.options.output.publicPath
|
||||
? 'auto'
|
||||
: compiler.options.output.publicPath;
|
||||
compiler.options.output.uniqueName = this._options.config.name;
|
||||
if (compiler.options.output.scriptType === 'module') {
|
||||
compiler.options.output.scriptType = undefined;
|
||||
compiler.options.output.module = undefined;
|
||||
}
|
||||
if (this._options.isServer) {
|
||||
compiler.options.target = 'async-node';
|
||||
compiler.options.output.library ??= {
|
||||
type: 'commonjs-module',
|
||||
};
|
||||
compiler.options.output.library.type = 'commonjs-module';
|
||||
}
|
||||
|
||||
const config = getModuleFederationConfigSync(
|
||||
this._options.config,
|
||||
{
|
||||
isServer: this._options.isServer,
|
||||
},
|
||||
true
|
||||
);
|
||||
const sharedLibraries = config.sharedLibraries;
|
||||
const sharedDependencies = config.sharedDependencies;
|
||||
const mappedRemotes = config.mappedRemotes;
|
||||
|
||||
const runtimePlugins = [];
|
||||
if (this.configOverride?.runtimePlugins) {
|
||||
runtimePlugins.push(...(this.configOverride.runtimePlugins ?? []));
|
||||
}
|
||||
if (this._options.isServer) {
|
||||
runtimePlugins.push(
|
||||
require.resolve('@module-federation/node/runtimePlugin')
|
||||
);
|
||||
}
|
||||
|
||||
new (require('@module-federation/enhanced/rspack').ModuleFederationPlugin)({
|
||||
name: this._options.config.name.replace(/-/g, '_'),
|
||||
filename: 'remoteEntry.js',
|
||||
exposes: this._options.config.exposes,
|
||||
remotes: mappedRemotes,
|
||||
shared: {
|
||||
...(sharedDependencies ?? {}),
|
||||
},
|
||||
...(this._options.isServer
|
||||
? {
|
||||
library: {
|
||||
type: 'commonjs-module',
|
||||
},
|
||||
remoteType: 'script',
|
||||
}
|
||||
: {}),
|
||||
...(this.configOverride ? this.configOverride : {}),
|
||||
runtimePlugins,
|
||||
virtualRuntimeEntry: true,
|
||||
}).apply(compiler);
|
||||
|
||||
if (sharedLibraries) {
|
||||
sharedLibraries.getReplacementPlugin().apply(compiler as any);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,191 @@
|
||||
import {
|
||||
Compilation,
|
||||
Compiler,
|
||||
DefinePlugin,
|
||||
RspackPluginInstance,
|
||||
} from '@rspack/core';
|
||||
import * as pc from 'picocolors';
|
||||
import {
|
||||
logger,
|
||||
readCachedProjectGraph,
|
||||
readProjectsConfigurationFromProjectGraph,
|
||||
workspaceRoot,
|
||||
} from '@nx/devkit';
|
||||
import { ModuleFederationConfig } from '../../../utils/models';
|
||||
import { dirname, extname, join } from 'path';
|
||||
import { existsSync } from 'fs';
|
||||
import {
|
||||
buildStaticRemotes,
|
||||
getDynamicMfManifestFile,
|
||||
getRemotes,
|
||||
getStaticRemotes,
|
||||
parseRemotesConfig,
|
||||
startRemoteProxies,
|
||||
startStaticRemotesFileServer,
|
||||
} from '../../utils';
|
||||
import { NxModuleFederationDevServerConfig } from '../../models';
|
||||
import { ChildProcess, fork } from 'node:child_process';
|
||||
|
||||
const PLUGIN_NAME = 'NxModuleFederationSSRDevServerPlugin';
|
||||
|
||||
export class NxModuleFederationSSRDevServerPlugin
|
||||
implements RspackPluginInstance
|
||||
{
|
||||
private devServerProcess: ChildProcess | undefined;
|
||||
private nxBin = require.resolve('nx/bin/nx');
|
||||
|
||||
constructor(
|
||||
private _options: {
|
||||
config: ModuleFederationConfig;
|
||||
devServerConfig?: NxModuleFederationDevServerConfig;
|
||||
}
|
||||
) {
|
||||
this._options.devServerConfig ??= {
|
||||
host: 'localhost',
|
||||
};
|
||||
}
|
||||
|
||||
apply(compiler: Compiler) {
|
||||
const isDevServer = process.env['WEBPACK_SERVE'];
|
||||
if (!isDevServer) {
|
||||
return;
|
||||
}
|
||||
compiler.hooks.watchRun.tapAsync(
|
||||
PLUGIN_NAME,
|
||||
async (compiler, callback) => {
|
||||
compiler.hooks.beforeCompile.tapAsync(
|
||||
PLUGIN_NAME,
|
||||
async (params, callback) => {
|
||||
const staticRemotesConfig = await this.setup(compiler);
|
||||
|
||||
logger.info(
|
||||
`NX Starting module federation dev-server for ${pc.bold(
|
||||
this._options.config.name
|
||||
)} with ${Object.keys(staticRemotesConfig).length} remotes`
|
||||
);
|
||||
|
||||
const mappedLocationOfRemotes = await buildStaticRemotes(
|
||||
staticRemotesConfig,
|
||||
this._options.devServerConfig,
|
||||
this.nxBin
|
||||
);
|
||||
startStaticRemotesFileServer(
|
||||
staticRemotesConfig,
|
||||
workspaceRoot,
|
||||
this._options.devServerConfig.staticRemotesPort
|
||||
);
|
||||
startRemoteProxies(
|
||||
staticRemotesConfig,
|
||||
mappedLocationOfRemotes,
|
||||
{
|
||||
pathToCert: this._options.devServerConfig.sslCert,
|
||||
pathToKey: this._options.devServerConfig.sslCert,
|
||||
},
|
||||
true
|
||||
);
|
||||
|
||||
new DefinePlugin({
|
||||
'process.env.NX_MF_DEV_REMOTES': process.env.NX_MF_DEV_REMOTES,
|
||||
}).apply(compiler);
|
||||
|
||||
await this.startServer(compiler);
|
||||
|
||||
callback();
|
||||
}
|
||||
);
|
||||
callback();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private async startServer(compiler: Compiler) {
|
||||
compiler.hooks.done.tapAsync(PLUGIN_NAME, async (_, callback) => {
|
||||
const serverPath = join(
|
||||
compiler.options.output.path,
|
||||
(compiler.options.output.filename as string) ?? 'server.js'
|
||||
);
|
||||
if (this.devServerProcess) {
|
||||
await new Promise<void>((res) => {
|
||||
this.devServerProcess.on('exit', () => {
|
||||
res();
|
||||
});
|
||||
this.devServerProcess.kill('SIGKILL');
|
||||
this.devServerProcess = undefined;
|
||||
});
|
||||
}
|
||||
|
||||
if (!existsSync(serverPath)) {
|
||||
for (let retries = 0; retries < 10; retries++) {
|
||||
await new Promise<void>((res) => setTimeout(res, 200));
|
||||
if (existsSync(serverPath)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!existsSync(serverPath)) {
|
||||
throw new Error(`Could not find server bundle at ${serverPath}.`);
|
||||
}
|
||||
}
|
||||
|
||||
this.devServerProcess = fork(serverPath);
|
||||
process.on('exit', () => {
|
||||
this.devServerProcess?.kill('SIGKILL');
|
||||
});
|
||||
process.on('SIGINT', () => {
|
||||
this.devServerProcess?.kill('SIGKILL');
|
||||
});
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
private async setup(compiler: Compiler) {
|
||||
const projectGraph = readCachedProjectGraph();
|
||||
const { projects: workspaceProjects } =
|
||||
readProjectsConfigurationFromProjectGraph(projectGraph);
|
||||
const project = workspaceProjects[this._options.config.name];
|
||||
if (!this._options.devServerConfig.pathToManifestFile) {
|
||||
this._options.devServerConfig.pathToManifestFile =
|
||||
getDynamicMfManifestFile(project, workspaceRoot);
|
||||
} else {
|
||||
const userPathToManifestFile = join(
|
||||
workspaceRoot,
|
||||
this._options.devServerConfig.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(this._options.devServerConfig.pathToManifestFile) !== '.json'
|
||||
) {
|
||||
throw new Error(
|
||||
`The Module Federation manifest file must be a JSON. Please ensure the file at ${userPathToManifestFile} is a JSON.`
|
||||
);
|
||||
}
|
||||
|
||||
this._options.devServerConfig.pathToManifestFile = userPathToManifestFile;
|
||||
}
|
||||
|
||||
const { remotes, staticRemotePort } = getRemotes(
|
||||
this._options.config,
|
||||
projectGraph,
|
||||
this._options.devServerConfig.pathToManifestFile
|
||||
);
|
||||
this._options.devServerConfig.staticRemotesPort ??= staticRemotePort;
|
||||
|
||||
const remotesConfig = parseRemotesConfig(
|
||||
remotes,
|
||||
workspaceRoot,
|
||||
projectGraph,
|
||||
true
|
||||
);
|
||||
const staticRemotesConfig = await getStaticRemotes(
|
||||
remotesConfig.config ?? {}
|
||||
);
|
||||
const devRemotes = remotes.filter((r) => !staticRemotesConfig[r]);
|
||||
process.env.NX_MF_DEV_REMOTES = JSON.stringify([
|
||||
...(devRemotes.length > 0 ? devRemotes : []),
|
||||
project.name,
|
||||
]);
|
||||
return staticRemotesConfig ?? {};
|
||||
}
|
||||
}
|
||||
@ -128,7 +128,9 @@ export class NxModuleFederationDevServerPlugin implements RspackPluginInstance {
|
||||
projectGraph
|
||||
);
|
||||
const staticRemotesConfig = await getStaticRemotes(
|
||||
remotesConfig.config ?? {}
|
||||
remotesConfig.config ?? {},
|
||||
this._options.devServerConfig?.devRemoteFindOptions,
|
||||
this._options.devServerConfig?.host
|
||||
);
|
||||
const devRemotes = remotes.filter((r) => !staticRemotesConfig[r]);
|
||||
process.env.NX_MF_DEV_REMOTES = JSON.stringify([
|
||||
|
||||
@ -18,7 +18,7 @@ export async function buildStaticRemotes(
|
||||
const mappedLocationOfRemotes: Record<string, string> = {};
|
||||
for (const app of remotes) {
|
||||
mappedLocationOfRemotes[app] = `http${options.ssl ? 's' : ''}://${
|
||||
options.host
|
||||
options.host ?? 'localhost'
|
||||
}:${options.staticRemotesPort}/${staticRemotesConfig[app].urlSegment}`;
|
||||
}
|
||||
|
||||
|
||||
@ -4,7 +4,8 @@ import { DevRemoteFindOptions } from '../models';
|
||||
|
||||
export async function getStaticRemotes(
|
||||
remotesConfig: Record<string, StaticRemoteConfig>,
|
||||
devRemoteFindOptions?: DevRemoteFindOptions
|
||||
devRemoteFindOptions?: DevRemoteFindOptions,
|
||||
host: string = '127.0.0.1'
|
||||
) {
|
||||
const remotes = Object.keys(remotesConfig);
|
||||
const findStaticRemotesPromises: Promise<string | undefined>[] = [];
|
||||
@ -14,6 +15,7 @@ export async function getStaticRemotes(
|
||||
waitForPortOpen(remotesConfig[remote].port, {
|
||||
retries: devRemoteFindOptions?.retries ?? 3,
|
||||
retryDelay: devRemoteFindOptions?.retryDelay ?? 1000,
|
||||
host,
|
||||
}).then(
|
||||
(res) => {
|
||||
resolve(undefined);
|
||||
|
||||
@ -18,11 +18,23 @@ import {
|
||||
import { readCachedProjectConfiguration } from 'nx/src/project-graph/project-graph';
|
||||
|
||||
export function applyDefaultEagerPackages(
|
||||
sharedConfig: Record<string, SharedLibraryConfig>
|
||||
sharedConfig: Record<string, SharedLibraryConfig>,
|
||||
useRspack = false
|
||||
) {
|
||||
const DEFAULT_PACKAGES_TO_LOAD_EAGERLY = [
|
||||
'@angular/localize',
|
||||
'@angular/localize/init',
|
||||
...(useRspack
|
||||
? [
|
||||
'@angular/core',
|
||||
'@angular/core/primitives/signals',
|
||||
'@angular/core/event-dispatch',
|
||||
'@angular/core/rxjs-interop',
|
||||
'@angular/common',
|
||||
'@angular/common/http',
|
||||
'@angular/platform-browser',
|
||||
]
|
||||
: []),
|
||||
];
|
||||
for (const pkg of DEFAULT_PACKAGES_TO_LOAD_EAGERLY) {
|
||||
if (!sharedConfig[pkg]) {
|
||||
@ -37,6 +49,7 @@ export const DEFAULT_NPM_PACKAGES_TO_AVOID = [
|
||||
'zone.js',
|
||||
'@nx/angular/mf',
|
||||
'@nrwl/angular/mf',
|
||||
'@nx/angular-rspack',
|
||||
];
|
||||
export const DEFAULT_ANGULAR_PACKAGES_TO_SHARE = [
|
||||
'@angular/core',
|
||||
@ -44,9 +57,16 @@ export const DEFAULT_ANGULAR_PACKAGES_TO_SHARE = [
|
||||
'@angular/common',
|
||||
];
|
||||
|
||||
export function getFunctionDeterminateRemoteUrl(isServer: boolean = false) {
|
||||
export function getFunctionDeterminateRemoteUrl(
|
||||
isServer: boolean = false,
|
||||
useRspack = false
|
||||
) {
|
||||
const target = 'serve';
|
||||
const remoteEntry = isServer ? 'server/remoteEntry.js' : 'remoteEntry.mjs';
|
||||
const remoteEntry = isServer
|
||||
? 'server/remoteEntry.js'
|
||||
: useRspack
|
||||
? 'remoteEntry.js'
|
||||
: 'remoteEntry.mjs';
|
||||
|
||||
return function (remote: string) {
|
||||
const mappedStaticRemotesFromEnv = process.env
|
||||
@ -78,7 +98,7 @@ export function getFunctionDeterminateRemoteUrl(isServer: boolean = false) {
|
||||
serveTarget.options?.host ??
|
||||
`http${serveTarget.options.ssl ? 's' : ''}://localhost`;
|
||||
const port = serveTarget.options?.port ?? 4201;
|
||||
return `${
|
||||
return `${useRspack ? `${remote}@` : ''}${
|
||||
host.endsWith('/') ? host.slice(0, -1) : host
|
||||
}:${port}/${remoteEntry}`;
|
||||
};
|
||||
@ -164,3 +184,83 @@ export async function getModuleFederationConfig(
|
||||
: mapRemotesFunction(mfConfig.remotes, 'mjs', determineRemoteUrlFn);
|
||||
return { sharedLibraries, sharedDependencies, mappedRemotes };
|
||||
}
|
||||
|
||||
export function getModuleFederationConfigSync(
|
||||
mfConfig: ModuleFederationConfig,
|
||||
options: {
|
||||
isServer: boolean;
|
||||
determineRemoteUrl?: (remote: string) => string;
|
||||
} = { isServer: false },
|
||||
useRspack = false
|
||||
) {
|
||||
const projectGraph: ProjectGraph = readCachedProjectGraph();
|
||||
|
||||
if (!projectGraph.nodes[mfConfig.name]?.data) {
|
||||
throw Error(
|
||||
`Cannot find project "${mfConfig.name}". Check that the name is correct in module-federation.config.js`
|
||||
);
|
||||
}
|
||||
|
||||
const dependencies = getDependentPackagesForProject(
|
||||
projectGraph,
|
||||
mfConfig.name
|
||||
);
|
||||
|
||||
if (mfConfig.shared) {
|
||||
dependencies.workspaceLibraries = dependencies.workspaceLibraries.filter(
|
||||
(lib) => mfConfig.shared(lib.importKey, {}) !== false
|
||||
);
|
||||
dependencies.npmPackages = dependencies.npmPackages.filter(
|
||||
(pkg) => mfConfig.shared(pkg, {}) !== false
|
||||
);
|
||||
}
|
||||
|
||||
const sharedLibraries = shareWorkspaceLibraries(
|
||||
dependencies.workspaceLibraries
|
||||
);
|
||||
|
||||
const npmPackages = sharePackages(
|
||||
Array.from(
|
||||
new Set([
|
||||
...dependencies.npmPackages.filter(
|
||||
(pkg) => !DEFAULT_NPM_PACKAGES_TO_AVOID.includes(pkg)
|
||||
),
|
||||
])
|
||||
)
|
||||
);
|
||||
|
||||
DEFAULT_NPM_PACKAGES_TO_AVOID.forEach((pkgName) => {
|
||||
if (pkgName in npmPackages) {
|
||||
delete npmPackages[pkgName];
|
||||
}
|
||||
});
|
||||
|
||||
const sharedDependencies = {
|
||||
...sharedLibraries.getLibraries(
|
||||
projectGraph.nodes[mfConfig.name].data.root
|
||||
),
|
||||
...npmPackages,
|
||||
};
|
||||
|
||||
applyDefaultEagerPackages(sharedDependencies, useRspack);
|
||||
applySharedFunction(sharedDependencies, mfConfig.shared);
|
||||
applyAdditionalShared(
|
||||
sharedDependencies,
|
||||
mfConfig.additionalShared,
|
||||
projectGraph
|
||||
);
|
||||
const determineRemoteUrlFn =
|
||||
options.determineRemoteUrl ||
|
||||
getFunctionDeterminateRemoteUrl(options.isServer, useRspack);
|
||||
|
||||
const mapRemotesFunction = options.isServer ? mapRemotesForSSR : mapRemotes;
|
||||
const mappedRemotes =
|
||||
!mfConfig.remotes || mfConfig.remotes.length === 0
|
||||
? {}
|
||||
: mapRemotesFunction(
|
||||
mfConfig.remotes,
|
||||
useRspack ? 'js' : 'mjs',
|
||||
determineRemoteUrlFn
|
||||
);
|
||||
return { sharedLibraries, sharedDependencies, mappedRemotes };
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user