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",
|
"x-priority": "important",
|
||||||
"alias": "producers"
|
"alias": "producers"
|
||||||
},
|
},
|
||||||
|
"bundler": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The bundler to use for the host application.",
|
||||||
|
"default": "webpack",
|
||||||
|
"enum": ["webpack", "rspack"]
|
||||||
|
},
|
||||||
"dynamic": {
|
"dynamic": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"description": "Should the host application use dynamic federation?",
|
"description": "Should the host application use dynamic federation?",
|
||||||
|
|||||||
@ -42,6 +42,12 @@
|
|||||||
"type": "number",
|
"type": "number",
|
||||||
"description": "The port on which this app should be served."
|
"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": {
|
"style": {
|
||||||
"description": "The file extension to be used for style files.",
|
"description": "The file extension to be used for style files.",
|
||||||
"type": "string",
|
"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,
|
angularRspackVersion,
|
||||||
nxVersion,
|
nxVersion,
|
||||||
tsNodeVersion,
|
tsNodeVersion,
|
||||||
|
webpackMergeVersion,
|
||||||
} from '../../utils/versions';
|
} from '../../utils/versions';
|
||||||
import { createConfig } from './lib/create-config';
|
import { createConfig } from './lib/create-config';
|
||||||
import { getCustomWebpackConfig } from './lib/get-custom-webpack-config';
|
import { getCustomWebpackConfig } from './lib/get-custom-webpack-config';
|
||||||
@ -47,12 +48,7 @@ const RENAMED_OPTIONS = {
|
|||||||
|
|
||||||
const DEFAULT_PORT = 4200;
|
const DEFAULT_PORT = 4200;
|
||||||
|
|
||||||
const REMOVED_OPTIONS = [
|
const REMOVED_OPTIONS = ['buildOptimizer', 'buildTarget', 'browserTarget'];
|
||||||
'buildOptimizer',
|
|
||||||
'buildTarget',
|
|
||||||
'browserTarget',
|
|
||||||
'publicHost',
|
|
||||||
];
|
|
||||||
|
|
||||||
function normalizeFromProjectRoot(
|
function normalizeFromProjectRoot(
|
||||||
tree: Tree,
|
tree: Tree,
|
||||||
@ -506,6 +502,7 @@ export async function convertToRspack(
|
|||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
'@nx/angular-rspack': angularRspackVersion,
|
'@nx/angular-rspack': angularRspackVersion,
|
||||||
|
'webpack-merge': webpackMergeVersion,
|
||||||
'ts-node': tsNodeVersion,
|
'ts-node': tsNodeVersion,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@ -16,7 +16,7 @@ describe('convertconvertWebpackConfigToUseNxModuleFederationPlugin', () => {
|
|||||||
// ASSERT
|
// ASSERT
|
||||||
expect(newWebpackConfigContents).toMatchInlineSnapshot(`
|
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';
|
import config from './module-federation.config';
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -56,7 +56,7 @@ export function convertWebpackConfigToUseNxModuleFederationPlugin(
|
|||||||
newWebpackConfigContents = `${webpackConfigContents.slice(
|
newWebpackConfigContents = `${webpackConfigContents.slice(
|
||||||
0,
|
0,
|
||||||
withModuleFederationImportNode.getStart()
|
withModuleFederationImportNode.getStart()
|
||||||
)}import { NxModuleFederationPlugin, NxModuleFederationDevServerPlugin } from '@nx/module-federation/rspack';${webpackConfigContents.slice(
|
)}import { NxModuleFederationPlugin, NxModuleFederationDevServerPlugin } from '@nx/module-federation/angular';${webpackConfigContents.slice(
|
||||||
withModuleFederationImportNode.getEnd()
|
withModuleFederationImportNode.getEnd()
|
||||||
)}`;
|
)}`;
|
||||||
|
|
||||||
|
|||||||
@ -2,8 +2,10 @@ import {
|
|||||||
formatFiles,
|
formatFiles,
|
||||||
getProjects,
|
getProjects,
|
||||||
joinPathFragments,
|
joinPathFragments,
|
||||||
|
readProjectConfiguration,
|
||||||
runTasksInSerial,
|
runTasksInSerial,
|
||||||
Tree,
|
Tree,
|
||||||
|
updateProjectConfiguration,
|
||||||
} from '@nx/devkit';
|
} from '@nx/devkit';
|
||||||
import {
|
import {
|
||||||
determineProjectNameAndRootOptions,
|
determineProjectNameAndRootOptions,
|
||||||
@ -18,9 +20,19 @@ import { setupMf } from '../setup-mf/setup-mf';
|
|||||||
import { addMfEnvToTargetDefaultInputs } from '../utils/add-mf-env-to-inputs';
|
import { addMfEnvToTargetDefaultInputs } from '../utils/add-mf-env-to-inputs';
|
||||||
import { updateSsrSetup } from './lib';
|
import { updateSsrSetup } from './lib';
|
||||||
import type { Schema } from './schema';
|
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) {
|
export async function host(tree: Tree, schema: Schema) {
|
||||||
assertNotUsingTsSolutionSetup(tree, 'angular', 'host');
|
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;
|
const { typescriptConfiguration = true, ...options }: Schema = schema;
|
||||||
options.standalone = options.standalone ?? true;
|
options.standalone = options.standalone ?? true;
|
||||||
@ -100,7 +112,8 @@ export async function host(tree: Tree, schema: Schema) {
|
|||||||
installTasks.push(ssrInstallTask);
|
installTasks.push(ssrInstallTask);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const remote of remotesToGenerate) {
|
for (let i = 0; i < remotesToGenerate.length; i++) {
|
||||||
|
const remote = remotesToGenerate[i];
|
||||||
const remoteDirectory = options.directory
|
const remoteDirectory = options.directory
|
||||||
? joinPathFragments(options.directory, '..', remote)
|
? joinPathFragments(options.directory, '..', remote)
|
||||||
: appRoot === '.'
|
: appRoot === '.'
|
||||||
@ -111,6 +124,7 @@ export async function host(tree: Tree, schema: Schema) {
|
|||||||
name: remote,
|
name: remote,
|
||||||
directory: remoteDirectory,
|
directory: remoteDirectory,
|
||||||
host: hostProjectName,
|
host: hostProjectName,
|
||||||
|
port: isRspack ? 4200 + i + 1 : undefined,
|
||||||
skipFormat: true,
|
skipFormat: true,
|
||||||
standalone: options.standalone,
|
standalone: options.standalone,
|
||||||
typescriptConfiguration,
|
typescriptConfiguration,
|
||||||
@ -119,6 +133,20 @@ export async function host(tree: Tree, schema: Schema) {
|
|||||||
|
|
||||||
addMfEnvToTargetDefaultInputs(tree);
|
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) {
|
if (!options.skipFormat) {
|
||||||
await formatFiles(tree);
|
await formatFiles(tree);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import type { Styles } from '../utils/types';
|
|||||||
export interface Schema {
|
export interface Schema {
|
||||||
directory: string;
|
directory: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
|
bundler?: 'webpack' | 'rspack';
|
||||||
remotes?: string[];
|
remotes?: string[];
|
||||||
dynamic?: boolean;
|
dynamic?: boolean;
|
||||||
setParserOptionsProject?: boolean;
|
setParserOptionsProject?: boolean;
|
||||||
|
|||||||
@ -37,6 +37,12 @@
|
|||||||
"x-priority": "important",
|
"x-priority": "important",
|
||||||
"alias": "producers"
|
"alias": "producers"
|
||||||
},
|
},
|
||||||
|
"bundler": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The bundler to use for the host application.",
|
||||||
|
"default": "webpack",
|
||||||
|
"enum": ["webpack", "rspack"]
|
||||||
|
},
|
||||||
"dynamic": {
|
"dynamic": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"description": "Should the host application use dynamic federation?",
|
"description": "Should the host application use dynamic federation?",
|
||||||
|
|||||||
@ -2,9 +2,11 @@ import {
|
|||||||
addDependenciesToPackageJson,
|
addDependenciesToPackageJson,
|
||||||
formatFiles,
|
formatFiles,
|
||||||
getProjects,
|
getProjects,
|
||||||
|
readProjectConfiguration,
|
||||||
runTasksInSerial,
|
runTasksInSerial,
|
||||||
stripIndents,
|
stripIndents,
|
||||||
Tree,
|
Tree,
|
||||||
|
updateProjectConfiguration,
|
||||||
} from '@nx/devkit';
|
} from '@nx/devkit';
|
||||||
import {
|
import {
|
||||||
determineProjectNameAndRootOptions,
|
determineProjectNameAndRootOptions,
|
||||||
@ -18,9 +20,19 @@ import { setupMf } from '../setup-mf/setup-mf';
|
|||||||
import { addMfEnvToTargetDefaultInputs } from '../utils/add-mf-env-to-inputs';
|
import { addMfEnvToTargetDefaultInputs } from '../utils/add-mf-env-to-inputs';
|
||||||
import { findNextAvailablePort, updateSsrSetup } from './lib';
|
import { findNextAvailablePort, updateSsrSetup } from './lib';
|
||||||
import type { Schema } from './schema';
|
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) {
|
export async function remote(tree: Tree, schema: Schema) {
|
||||||
assertNotUsingTsSolutionSetup(tree, 'angular', 'remote');
|
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;
|
const { typescriptConfiguration = true, ...options }: Schema = schema;
|
||||||
options.standalone = options.standalone ?? true;
|
options.standalone = options.standalone ?? true;
|
||||||
@ -105,6 +117,24 @@ export async function remote(tree: Tree, schema: Schema) {
|
|||||||
|
|
||||||
addMfEnvToTargetDefaultInputs(tree);
|
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) {
|
if (!options.skipFormat) {
|
||||||
await formatFiles(tree);
|
await formatFiles(tree);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import type { Styles } from '../utils/types';
|
|||||||
export interface Schema {
|
export interface Schema {
|
||||||
directory: string;
|
directory: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
|
bundler?: 'webpack' | 'rspack';
|
||||||
host?: string;
|
host?: string;
|
||||||
port?: number;
|
port?: number;
|
||||||
setParserOptionsProject?: boolean;
|
setParserOptionsProject?: boolean;
|
||||||
|
|||||||
@ -42,6 +42,12 @@
|
|||||||
"type": "number",
|
"type": "number",
|
||||||
"description": "The port on which this app should be served."
|
"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": {
|
"style": {
|
||||||
"description": "The file extension to be used for style files.",
|
"description": "The file extension to be used for style files.",
|
||||||
"type": "string",
|
"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',
|
typesNodeVersion: '18.16.9',
|
||||||
jasmineMarblesVersion: '^0.9.2',
|
jasmineMarblesVersion: '^0.9.2',
|
||||||
jsoncEslintParserVersion: '^2.1.0',
|
jsoncEslintParserVersion: '^2.1.0',
|
||||||
|
webpackMergeVersion: '^5.8.0',
|
||||||
},
|
},
|
||||||
angularV18: {
|
angularV18: {
|
||||||
angularVersion: '~18.2.0',
|
angularVersion: '~18.2.0',
|
||||||
@ -80,5 +81,6 @@ export const backwardCompatibleVersions: VersionMap = {
|
|||||||
typesNodeVersion: '18.16.9',
|
typesNodeVersion: '18.16.9',
|
||||||
jasmineMarblesVersion: '^0.9.2',
|
jasmineMarblesVersion: '^0.9.2',
|
||||||
jsoncEslintParserVersion: '^2.1.0',
|
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 browserSyncVersion = '^3.0.0';
|
||||||
export const moduleFederationNodeVersion = '^2.6.26';
|
export const moduleFederationNodeVersion = '^2.6.26';
|
||||||
export const moduleFederationEnhancedVersion = '^0.9.0';
|
export const moduleFederationEnhancedVersion = '^0.9.0';
|
||||||
|
export const webpackMergeVersion = '^5.8.0';
|
||||||
|
|
||||||
export const angularEslintVersion = '^19.2.0';
|
export const angularEslintVersion = '^19.2.0';
|
||||||
export const typescriptEslintVersion = '^7.16.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';
|
||||||
export * from './src/with-module-federation/angular/with-module-federation-ssr';
|
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
|
projectGraph
|
||||||
);
|
);
|
||||||
const staticRemotesConfig = await getStaticRemotes(
|
const staticRemotesConfig = await getStaticRemotes(
|
||||||
remotesConfig.config ?? {}
|
remotesConfig.config ?? {},
|
||||||
|
this._options.devServerConfig?.devRemoteFindOptions,
|
||||||
|
this._options.devServerConfig?.host
|
||||||
);
|
);
|
||||||
const devRemotes = remotes.filter((r) => !staticRemotesConfig[r]);
|
const devRemotes = remotes.filter((r) => !staticRemotesConfig[r]);
|
||||||
process.env.NX_MF_DEV_REMOTES = JSON.stringify([
|
process.env.NX_MF_DEV_REMOTES = JSON.stringify([
|
||||||
|
|||||||
@ -18,7 +18,7 @@ export async function buildStaticRemotes(
|
|||||||
const mappedLocationOfRemotes: Record<string, string> = {};
|
const mappedLocationOfRemotes: Record<string, string> = {};
|
||||||
for (const app of remotes) {
|
for (const app of remotes) {
|
||||||
mappedLocationOfRemotes[app] = `http${options.ssl ? 's' : ''}://${
|
mappedLocationOfRemotes[app] = `http${options.ssl ? 's' : ''}://${
|
||||||
options.host
|
options.host ?? 'localhost'
|
||||||
}:${options.staticRemotesPort}/${staticRemotesConfig[app].urlSegment}`;
|
}:${options.staticRemotesPort}/${staticRemotesConfig[app].urlSegment}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -4,7 +4,8 @@ import { DevRemoteFindOptions } from '../models';
|
|||||||
|
|
||||||
export async function getStaticRemotes(
|
export async function getStaticRemotes(
|
||||||
remotesConfig: Record<string, StaticRemoteConfig>,
|
remotesConfig: Record<string, StaticRemoteConfig>,
|
||||||
devRemoteFindOptions?: DevRemoteFindOptions
|
devRemoteFindOptions?: DevRemoteFindOptions,
|
||||||
|
host: string = '127.0.0.1'
|
||||||
) {
|
) {
|
||||||
const remotes = Object.keys(remotesConfig);
|
const remotes = Object.keys(remotesConfig);
|
||||||
const findStaticRemotesPromises: Promise<string | undefined>[] = [];
|
const findStaticRemotesPromises: Promise<string | undefined>[] = [];
|
||||||
@ -14,6 +15,7 @@ export async function getStaticRemotes(
|
|||||||
waitForPortOpen(remotesConfig[remote].port, {
|
waitForPortOpen(remotesConfig[remote].port, {
|
||||||
retries: devRemoteFindOptions?.retries ?? 3,
|
retries: devRemoteFindOptions?.retries ?? 3,
|
||||||
retryDelay: devRemoteFindOptions?.retryDelay ?? 1000,
|
retryDelay: devRemoteFindOptions?.retryDelay ?? 1000,
|
||||||
|
host,
|
||||||
}).then(
|
}).then(
|
||||||
(res) => {
|
(res) => {
|
||||||
resolve(undefined);
|
resolve(undefined);
|
||||||
|
|||||||
@ -18,11 +18,23 @@ import {
|
|||||||
import { readCachedProjectConfiguration } from 'nx/src/project-graph/project-graph';
|
import { readCachedProjectConfiguration } from 'nx/src/project-graph/project-graph';
|
||||||
|
|
||||||
export function applyDefaultEagerPackages(
|
export function applyDefaultEagerPackages(
|
||||||
sharedConfig: Record<string, SharedLibraryConfig>
|
sharedConfig: Record<string, SharedLibraryConfig>,
|
||||||
|
useRspack = false
|
||||||
) {
|
) {
|
||||||
const DEFAULT_PACKAGES_TO_LOAD_EAGERLY = [
|
const DEFAULT_PACKAGES_TO_LOAD_EAGERLY = [
|
||||||
'@angular/localize',
|
'@angular/localize',
|
||||||
'@angular/localize/init',
|
'@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) {
|
for (const pkg of DEFAULT_PACKAGES_TO_LOAD_EAGERLY) {
|
||||||
if (!sharedConfig[pkg]) {
|
if (!sharedConfig[pkg]) {
|
||||||
@ -37,6 +49,7 @@ export const DEFAULT_NPM_PACKAGES_TO_AVOID = [
|
|||||||
'zone.js',
|
'zone.js',
|
||||||
'@nx/angular/mf',
|
'@nx/angular/mf',
|
||||||
'@nrwl/angular/mf',
|
'@nrwl/angular/mf',
|
||||||
|
'@nx/angular-rspack',
|
||||||
];
|
];
|
||||||
export const DEFAULT_ANGULAR_PACKAGES_TO_SHARE = [
|
export const DEFAULT_ANGULAR_PACKAGES_TO_SHARE = [
|
||||||
'@angular/core',
|
'@angular/core',
|
||||||
@ -44,9 +57,16 @@ export const DEFAULT_ANGULAR_PACKAGES_TO_SHARE = [
|
|||||||
'@angular/common',
|
'@angular/common',
|
||||||
];
|
];
|
||||||
|
|
||||||
export function getFunctionDeterminateRemoteUrl(isServer: boolean = false) {
|
export function getFunctionDeterminateRemoteUrl(
|
||||||
|
isServer: boolean = false,
|
||||||
|
useRspack = false
|
||||||
|
) {
|
||||||
const target = 'serve';
|
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) {
|
return function (remote: string) {
|
||||||
const mappedStaticRemotesFromEnv = process.env
|
const mappedStaticRemotesFromEnv = process.env
|
||||||
@ -78,7 +98,7 @@ export function getFunctionDeterminateRemoteUrl(isServer: boolean = false) {
|
|||||||
serveTarget.options?.host ??
|
serveTarget.options?.host ??
|
||||||
`http${serveTarget.options.ssl ? 's' : ''}://localhost`;
|
`http${serveTarget.options.ssl ? 's' : ''}://localhost`;
|
||||||
const port = serveTarget.options?.port ?? 4201;
|
const port = serveTarget.options?.port ?? 4201;
|
||||||
return `${
|
return `${useRspack ? `${remote}@` : ''}${
|
||||||
host.endsWith('/') ? host.slice(0, -1) : host
|
host.endsWith('/') ? host.slice(0, -1) : host
|
||||||
}:${port}/${remoteEntry}`;
|
}:${port}/${remoteEntry}`;
|
||||||
};
|
};
|
||||||
@ -164,3 +184,83 @@ export async function getModuleFederationConfig(
|
|||||||
: mapRemotesFunction(mfConfig.remotes, 'mjs', determineRemoteUrlFn);
|
: mapRemotesFunction(mfConfig.remotes, 'mjs', determineRemoteUrlFn);
|
||||||
return { sharedLibraries, sharedDependencies, mappedRemotes };
|
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