feat(module-federation): add ssr support to rspack crystal plugin (#30437)
## Current Behavior The current `NxModuleFederationPlugin` does not support SSR ## Expected Behavior The current `NxModuleFederationPlugin` supports SSR
This commit is contained in:
parent
32f0acab42
commit
487aa6fa78
@ -2,3 +2,4 @@ export * from './src/with-module-federation/rspack/with-module-federation';
|
||||
export * from './src/with-module-federation/rspack/with-module-federation-ssr';
|
||||
export * from './src/plugins/nx-module-federation-plugin/rspack/nx-module-federation-plugin';
|
||||
export * from './src/plugins/nx-module-federation-plugin/rspack/nx-module-federation-dev-server-plugin';
|
||||
export * from './src/plugins/nx-module-federation-plugin/rspack/nx-module-federation-ssr-dev-server-plugin';
|
||||
|
||||
@ -24,24 +24,30 @@ import {
|
||||
startStaticRemotesFileServer,
|
||||
} from '../../utils';
|
||||
import { NxModuleFederationDevServerConfig } from '../../models';
|
||||
import { ChildProcess, fork } from 'node:child_process';
|
||||
|
||||
const PLUGIN_NAME = 'NxModuleFederationDevServerPlugin';
|
||||
|
||||
export class NxModuleFederationDevServerPlugin implements RspackPluginInstance {
|
||||
private devServerProcess: ChildProcess | undefined;
|
||||
private nxBin = require.resolve('nx/bin/nx');
|
||||
|
||||
constructor(
|
||||
private _options: {
|
||||
config: ModuleFederationConfig;
|
||||
devServerConfig: NxModuleFederationDevServerConfig;
|
||||
devServerConfig?: NxModuleFederationDevServerConfig;
|
||||
}
|
||||
) {
|
||||
this._options.devServerConfig ??= {
|
||||
host: 'localhost',
|
||||
};
|
||||
}
|
||||
) {}
|
||||
|
||||
apply(compiler: Compiler) {
|
||||
compiler.hooks.beforeCompile.tapAsync(
|
||||
PLUGIN_NAME,
|
||||
async (params, callback) => {
|
||||
const staticRemotesConfig = await this.setup();
|
||||
const staticRemotesConfig = await this.setup(compiler);
|
||||
|
||||
logger.info(
|
||||
`NX Starting module federation dev-server for ${pc.bold(
|
||||
@ -67,12 +73,13 @@ export class NxModuleFederationDevServerPlugin implements RspackPluginInstance {
|
||||
new DefinePlugin({
|
||||
'process.env.NX_MF_DEV_REMOTES': process.env.NX_MF_DEV_REMOTES,
|
||||
}).apply(compiler);
|
||||
|
||||
callback();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private async setup() {
|
||||
private async setup(compiler: Compiler) {
|
||||
const projectGraph = readCachedProjectGraph();
|
||||
const { projects: workspaceProjects } =
|
||||
readProjectsConfigurationFromProjectGraph(projectGraph);
|
||||
|
||||
@ -12,6 +12,7 @@ export class NxModuleFederationPlugin implements RspackPluginInstance {
|
||||
private _options: {
|
||||
config: ModuleFederationConfig;
|
||||
devServerConfig?: NxModuleFederationDevServerConfig;
|
||||
isServer?: boolean;
|
||||
},
|
||||
private configOverride?: NxModuleFederationConfigOverride
|
||||
) {}
|
||||
@ -23,15 +24,32 @@ export class NxModuleFederationPlugin implements RspackPluginInstance {
|
||||
|
||||
// This is required to ensure Module Federation will build the project correctly
|
||||
compiler.options.optimization.runtimeChunk = false;
|
||||
compiler.options.output.uniqueName = this._options.config.name;
|
||||
if (this._options.isServer) {
|
||||
compiler.options.target = 'async-node';
|
||||
compiler.options.output.library ??= {
|
||||
type: 'commonjs-module',
|
||||
};
|
||||
compiler.options.output.library.type = 'commonjs-module';
|
||||
}
|
||||
|
||||
const isDevServer = !!process.env['WEBPACK_SERVE'];
|
||||
|
||||
// TODO(colum): Add support for SSR
|
||||
const config = getModuleFederationConfig(this._options.config);
|
||||
const config = getModuleFederationConfig(this._options.config, {
|
||||
isServer: this._options.isServer,
|
||||
});
|
||||
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',
|
||||
@ -40,25 +58,21 @@ export class NxModuleFederationPlugin implements RspackPluginInstance {
|
||||
shared: {
|
||||
...(sharedDependencies ?? {}),
|
||||
},
|
||||
...(this._options.isServer
|
||||
? {
|
||||
library: {
|
||||
type: 'commonjs-module',
|
||||
},
|
||||
remoteType: 'script',
|
||||
}
|
||||
: {}),
|
||||
...(this.configOverride ? this.configOverride : {}),
|
||||
runtimePlugins: this.configOverride
|
||||
? this.configOverride.runtimePlugins ?? []
|
||||
: [],
|
||||
runtimePlugins,
|
||||
virtualRuntimeEntry: true,
|
||||
}).apply(compiler);
|
||||
|
||||
if (sharedLibraries) {
|
||||
sharedLibraries.getReplacementPlugin().apply(compiler as any);
|
||||
}
|
||||
|
||||
if (isDevServer) {
|
||||
new NxModuleFederationDevServerPlugin({
|
||||
config: this._options.config,
|
||||
devServerConfig: {
|
||||
...(this._options.devServerConfig ?? {}),
|
||||
host: this._options.devServerConfig?.host ?? 'localhost',
|
||||
},
|
||||
}).apply(compiler);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,187 @@
|
||||
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) {
|
||||
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.afterEmit.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();
|
||||
this.devServerProcess = undefined;
|
||||
});
|
||||
}
|
||||
|
||||
if (!existsSync(serverPath)) {
|
||||
for (let retries = 0; retries < 10; retries++) {
|
||||
await new Promise<void>((res) => setTimeout(res, 100));
|
||||
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();
|
||||
});
|
||||
process.on('SIGINT', () => {
|
||||
this.devServerProcess?.kill();
|
||||
});
|
||||
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 ?? {};
|
||||
}
|
||||
}
|
||||
@ -6,7 +6,8 @@ import { StaticRemoteConfig } from '../../utils';
|
||||
export function parseRemotesConfig(
|
||||
remotes: string[] | undefined,
|
||||
workspaceRoot: string,
|
||||
projectGraph: ProjectGraph
|
||||
projectGraph: ProjectGraph,
|
||||
isServer?: boolean
|
||||
) {
|
||||
if (!remotes?.length) {
|
||||
return { remotes: [], config: undefined };
|
||||
@ -32,7 +33,12 @@ export function parseRemotesConfig(
|
||||
const basePath = dirname(outputPath);
|
||||
const urlSegment = app;
|
||||
const port = projectGraph.nodes[app].data.targets?.['serve']?.options.port;
|
||||
config[app] = { basePath, outputPath, urlSegment, port };
|
||||
config[app] = {
|
||||
basePath,
|
||||
outputPath: isServer ? dirname(outputPath) : outputPath,
|
||||
urlSegment,
|
||||
port,
|
||||
};
|
||||
}
|
||||
|
||||
return { remotes, config };
|
||||
|
||||
@ -8,7 +8,8 @@ export function startRemoteProxies(
|
||||
sslOptions?: {
|
||||
pathToCert: string;
|
||||
pathToKey: string;
|
||||
}
|
||||
},
|
||||
isServer?: boolean
|
||||
) {
|
||||
const { createProxyMiddleware } = require('http-proxy-middleware');
|
||||
const express = require('express');
|
||||
@ -40,6 +41,15 @@ export function startRemoteProxies(
|
||||
target: mappedLocationsOfRemotes[app],
|
||||
changeOrigin: true,
|
||||
secure: sslCert ? false : undefined,
|
||||
pathRewrite: isServer
|
||||
? (path) => {
|
||||
if (path.includes('/server')) {
|
||||
return path;
|
||||
} else {
|
||||
return `browser/${path}`;
|
||||
}
|
||||
}
|
||||
: undefined,
|
||||
})
|
||||
);
|
||||
const proxyServer = (sslCert ? https : http)
|
||||
|
||||
@ -34,6 +34,14 @@ export class NxAppRspackPlugin {
|
||||
this.options.target = target;
|
||||
}
|
||||
|
||||
if (
|
||||
compiler.options.entry &&
|
||||
compiler.options.entry['main'] &&
|
||||
typeof compiler.options.entry['main'] === 'object' &&
|
||||
Object.keys(compiler.options.entry['main']).length === 0
|
||||
) {
|
||||
compiler.options.entry = {};
|
||||
}
|
||||
applyBaseConfig(this.options, compiler.options, {
|
||||
useNormalizedEntry: true,
|
||||
});
|
||||
|
||||
@ -65,11 +65,11 @@ function applyNxIndependentConfig(
|
||||
process.env.NODE_ENV === 'production' || options.mode === 'production';
|
||||
const hashFormat = getOutputHashFormat(options.outputHashing as string);
|
||||
config.context = path.join(options.root, options.projectRoot);
|
||||
config.target ??= options.target as 'node' | 'web';
|
||||
config.target ??= options.target as 'async-node' | 'node' | 'web';
|
||||
config.node = false;
|
||||
config.mode =
|
||||
// When the target is Node avoid any optimizations, such as replacing `process.env.NODE_ENV` with build time value.
|
||||
config.target === 'node'
|
||||
config.target === 'node' || config.target === 'async-node'
|
||||
? 'none'
|
||||
: // Otherwise, make sure it matches `process.env.NODE_ENV`.
|
||||
// When mode is development or production, rspack will automatically
|
||||
@ -86,7 +86,11 @@ function applyNxIndependentConfig(
|
||||
: 'none');
|
||||
// When target is Node, the Webpack mode will be set to 'none' which disables in memory caching and causes a full rebuild on every change.
|
||||
// So to mitigate this we enable in memory caching when target is Node and in watch mode.
|
||||
config.cache = options.target === 'node' && options.watch ? true : undefined;
|
||||
config.cache =
|
||||
(options.target === 'node' || options.target === 'async-node') &&
|
||||
options.watch
|
||||
? true
|
||||
: undefined;
|
||||
|
||||
config.devtool =
|
||||
options.sourceMap === true ? 'source-map' : options.sourceMap;
|
||||
@ -94,8 +98,11 @@ function applyNxIndependentConfig(
|
||||
config.output = {
|
||||
...(config.output ?? {}),
|
||||
libraryTarget:
|
||||
(config as Configuration).output?.libraryTarget ??
|
||||
(options.target === 'node' ? 'commonjs' : undefined),
|
||||
options.target === 'node'
|
||||
? 'commonjs'
|
||||
: options.target === 'async-node'
|
||||
? 'commonjs-module'
|
||||
: undefined,
|
||||
path:
|
||||
config.output?.path ??
|
||||
(options.outputPath
|
||||
@ -333,7 +340,10 @@ function applyNxDependentConfig(
|
||||
}
|
||||
|
||||
const externals = [];
|
||||
if (options.target === 'node' && options.externalDependencies === 'all') {
|
||||
if (
|
||||
(options.target === 'node' || options.target === 'async-node') &&
|
||||
options.externalDependencies === 'all'
|
||||
) {
|
||||
const modulesDir = `${options.root}/node_modules`;
|
||||
externals.push(nodeExternals({ modulesDir }));
|
||||
} else if (Array.isArray(options.externalDependencies)) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user