## Current Behavior Continuous tasks are not set up for React Rspack Module Federation Remote projects. This is important because `--dev-remotes` is no longer supported with Crystal Module Federation usage. ## Expected Behavior Add Continuous Tasks support for React Rspack Module Federation Remote Projects. This replaces the need for `nx serve shell --dev-remotes=remote1`. Instead, the command is simply `nx serve remote1` and continuous tasks means that the `shell:serve` task is executed correctly.
266 lines
7.9 KiB
TypeScript
266 lines
7.9 KiB
TypeScript
import { join } from 'path';
|
|
import {
|
|
addDependenciesToPackageJson,
|
|
formatFiles,
|
|
generateFiles,
|
|
GeneratorCallback,
|
|
joinPathFragments,
|
|
names,
|
|
offsetFromRoot,
|
|
readProjectConfiguration,
|
|
runTasksInSerial,
|
|
stripIndents,
|
|
Tree,
|
|
updateProjectConfiguration,
|
|
} from '@nx/devkit';
|
|
|
|
import { normalizeOptions } from '../application/lib/normalize-options';
|
|
import applicationGenerator from '../application/application';
|
|
import { NormalizedSchema } from '../application/schema';
|
|
import { updateHostWithRemote } from './lib/update-host-with-remote';
|
|
import { updateModuleFederationProject } from '../../rules/update-module-federation-project';
|
|
import { Schema } from './schema';
|
|
import setupSsrGenerator from '../setup-ssr/setup-ssr';
|
|
import { setupSsrForRemote } from './lib/setup-ssr-for-remote';
|
|
import { setupTspathForRemote } from './lib/setup-tspath-for-remote';
|
|
import { addRemoteToDynamicHost } from './lib/add-remote-to-dynamic-host';
|
|
import { addMfEnvToTargetDefaultInputs } from '../../utils/add-mf-env-to-inputs';
|
|
import { maybeJs } from '../../utils/maybe-js';
|
|
import { isValidVariable } from '@nx/js';
|
|
import {
|
|
moduleFederationEnhancedVersion,
|
|
nxVersion,
|
|
} from '../../utils/versions';
|
|
import { ensureRootProjectName } from '@nx/devkit/src/generators/project-name-and-root-utils';
|
|
import {
|
|
createNxRspackPluginOptions,
|
|
getDefaultTemplateVariables,
|
|
} from '../application/lib/create-application-files';
|
|
|
|
export function addModuleFederationFiles(
|
|
host: Tree,
|
|
options: NormalizedSchema<Schema>
|
|
) {
|
|
const templateVariables =
|
|
options.bundler === 'rspack'
|
|
? {
|
|
...getDefaultTemplateVariables(host, options),
|
|
rspackPluginOptions: {
|
|
...createNxRspackPluginOptions(
|
|
options,
|
|
offsetFromRoot(options.appProjectRoot),
|
|
false
|
|
),
|
|
mainServer: `./server.ts`,
|
|
},
|
|
}
|
|
: {
|
|
...names(options.projectName),
|
|
...options,
|
|
tmpl: '',
|
|
};
|
|
|
|
generateFiles(
|
|
host,
|
|
join(
|
|
__dirname,
|
|
`./files/${
|
|
options.js
|
|
? options.bundler === 'rspack'
|
|
? 'rspack-common'
|
|
: 'common'
|
|
: 'common-ts'
|
|
}`
|
|
),
|
|
options.appProjectRoot,
|
|
templateVariables
|
|
);
|
|
|
|
const pathToModuleFederationFiles = options.typescriptConfiguration
|
|
? `${
|
|
options.bundler === 'rspack' ? 'rspack-' : 'webpack-'
|
|
}module-federation-ts`
|
|
: `${
|
|
options.bundler === 'rspack' ? 'rspack-' : 'webpack-'
|
|
}module-federation`;
|
|
|
|
generateFiles(
|
|
host,
|
|
join(__dirname, `./files/${pathToModuleFederationFiles}`),
|
|
options.appProjectRoot,
|
|
templateVariables
|
|
);
|
|
|
|
if (options.typescriptConfiguration) {
|
|
const pathToBundlerConfig = joinPathFragments(
|
|
options.appProjectRoot,
|
|
options.bundler === 'rspack' ? 'rspack.config.js' : 'webpack.config.js'
|
|
);
|
|
const pathToWebpackProdConfig = joinPathFragments(
|
|
options.appProjectRoot,
|
|
options.bundler === 'rspack'
|
|
? 'rspack.config.prod.js'
|
|
: 'webpack.config.prod.js'
|
|
);
|
|
if (host.exists(pathToBundlerConfig)) {
|
|
host.delete(pathToBundlerConfig);
|
|
}
|
|
if (host.exists(pathToWebpackProdConfig)) {
|
|
host.delete(pathToWebpackProdConfig);
|
|
}
|
|
}
|
|
}
|
|
|
|
export async function remoteGenerator(host: Tree, schema: Schema) {
|
|
const tasks: GeneratorCallback[] = [];
|
|
const options: NormalizedSchema<Schema> = {
|
|
...(await normalizeOptions<Schema>(host, {
|
|
...schema,
|
|
useProjectJson: true,
|
|
})),
|
|
// when js is set to true, we want to use the js configuration
|
|
js: schema.js ?? false,
|
|
typescriptConfiguration: schema.js
|
|
? false
|
|
: schema.typescriptConfiguration ?? true,
|
|
dynamic: schema.dynamic ?? false,
|
|
// TODO(colum): remove when Webpack MF works with Crystal
|
|
addPlugin: !schema.bundler || schema.bundler === 'rspack' ? true : false,
|
|
bundler: schema.bundler ?? 'rspack',
|
|
};
|
|
|
|
if (options.dynamic) {
|
|
// Dynamic remotes generate with library { type: 'var' } by default.
|
|
// We need to ensure that the remote name is a valid variable name.
|
|
const isValidRemote = isValidVariable(options.projectName);
|
|
if (!isValidRemote.isValid) {
|
|
throw new Error(
|
|
`Invalid remote name provided: ${options.projectName}. ${isValidRemote.message}`
|
|
);
|
|
}
|
|
}
|
|
|
|
await ensureRootProjectName(options, 'application');
|
|
const REMOTE_NAME_REGEX = '^[a-zA-Z_$][a-zA-Z_$0-9]*$';
|
|
const remoteNameRegex = new RegExp(REMOTE_NAME_REGEX);
|
|
if (!remoteNameRegex.test(options.projectName)) {
|
|
throw new Error(
|
|
stripIndents`Invalid remote name: ${options.projectName}. Remote project names must:
|
|
- Start with a letter, dollar sign ($) or underscore (_)
|
|
- Followed by any valid character (letters, digits, underscores, or dollar signs)
|
|
The regular expression used is ${REMOTE_NAME_REGEX}.`
|
|
);
|
|
}
|
|
const initAppTask = await applicationGenerator(host, {
|
|
...options,
|
|
name: options.projectName,
|
|
skipFormat: true,
|
|
useProjectJson: true,
|
|
});
|
|
tasks.push(initAppTask);
|
|
|
|
if (options.host) {
|
|
updateHostWithRemote(host, options.host, options.projectName);
|
|
}
|
|
|
|
// Module federation requires bootstrap code to be dynamically imported.
|
|
// Renaming original entry file so we can use `import(./bootstrap)` in
|
|
// new entry file.
|
|
host.rename(
|
|
join(
|
|
options.appProjectRoot,
|
|
maybeJs(
|
|
{ js: options.js, useJsx: options.bundler === 'rspack' },
|
|
'src/main.tsx'
|
|
)
|
|
),
|
|
join(
|
|
options.appProjectRoot,
|
|
maybeJs(
|
|
{ js: options.js, useJsx: options.bundler === 'rspack' },
|
|
'src/bootstrap.tsx'
|
|
)
|
|
)
|
|
);
|
|
|
|
addModuleFederationFiles(host, options);
|
|
updateModuleFederationProject(host, options);
|
|
setupTspathForRemote(host, options);
|
|
|
|
if (options.ssr) {
|
|
if (options.bundler !== 'rspack') {
|
|
const setupSsrTask = await setupSsrGenerator(host, {
|
|
project: options.projectName,
|
|
serverPort: options.devServerPort,
|
|
skipFormat: true,
|
|
bundler: options.bundler,
|
|
});
|
|
tasks.push(setupSsrTask);
|
|
}
|
|
|
|
const setupSsrForRemoteTask = await setupSsrForRemote(
|
|
host,
|
|
options,
|
|
options.projectName
|
|
);
|
|
tasks.push(setupSsrForRemoteTask);
|
|
|
|
const projectConfig = readProjectConfiguration(host, options.projectName);
|
|
if (options.bundler !== 'rspack') {
|
|
projectConfig.targets.server.options.webpackConfig = joinPathFragments(
|
|
projectConfig.root,
|
|
`webpack.server.config.${options.typescriptConfiguration ? 'ts' : 'js'}`
|
|
);
|
|
updateProjectConfiguration(host, options.projectName, projectConfig);
|
|
}
|
|
}
|
|
if (!options.setParserOptionsProject) {
|
|
host.delete(
|
|
joinPathFragments(options.appProjectRoot, 'tsconfig.lint.json')
|
|
);
|
|
}
|
|
|
|
if (options.host && options.bundler === 'rspack') {
|
|
const projectConfig = readProjectConfiguration(host, options.projectName);
|
|
projectConfig.targets.serve ??= {};
|
|
projectConfig.targets.serve.dependsOn ??= [];
|
|
projectConfig.targets.serve.dependsOn.push(`${options.host}:serve`);
|
|
updateProjectConfiguration(host, options.projectName, projectConfig);
|
|
}
|
|
|
|
if (options.host && options.dynamic) {
|
|
const hostConfig = readProjectConfiguration(host, schema.host);
|
|
const pathToMFManifest = joinPathFragments(
|
|
hostConfig.sourceRoot,
|
|
'assets/module-federation.manifest.json'
|
|
);
|
|
addRemoteToDynamicHost(
|
|
host,
|
|
options.projectName,
|
|
options.devServerPort,
|
|
pathToMFManifest
|
|
);
|
|
}
|
|
|
|
addMfEnvToTargetDefaultInputs(host, options.bundler);
|
|
|
|
const installTask = addDependenciesToPackageJson(
|
|
host,
|
|
{},
|
|
{
|
|
'@module-federation/enhanced': moduleFederationEnhancedVersion,
|
|
'@nx/web': nxVersion,
|
|
'@nx/module-federation': nxVersion,
|
|
}
|
|
);
|
|
tasks.push(installTask);
|
|
|
|
if (!options.skipFormat) {
|
|
await formatFiles(host);
|
|
}
|
|
|
|
return runTasksInSerial(...tasks);
|
|
}
|
|
|
|
export default remoteGenerator;
|