@nx/rspack
Version:
561 lines (551 loc) • 23.1 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.findExistingTargetsInProject = findExistingTargetsInProject;
exports.addOrChangeBuildTarget = addOrChangeBuildTarget;
exports.addOrChangeServeTarget = addOrChangeServeTarget;
exports.writeRspackConfigFile = writeRspackConfigFile;
exports.deleteWebpackConfig = deleteWebpackConfig;
exports.moveAndEditIndexHtml = moveAndEditIndexHtml;
exports.normalizeViteConfigFilePathWithTree = normalizeViteConfigFilePathWithTree;
exports.getViteConfigPathForProject = getViteConfigPathForProject;
exports.handleUnsupportedUserProvidedTargets = handleUnsupportedUserProvidedTargets;
exports.handleUnknownExecutors = handleUnknownExecutors;
exports.determineFrameworkAndTarget = determineFrameworkAndTarget;
exports.determineMain = determineMain;
exports.determineTsConfig = determineTsConfig;
const devkit_1 = require("@nx/devkit");
const ensure_typescript_1 = require("@nx/js/src/utils/typescript/ensure-typescript");
const ts_solution_setup_1 = require("@nx/js/src/utils/typescript/ts-solution-setup");
const has_plugin_1 = require("./has-plugin");
function findExistingTargetsInProject(targets, userProvidedTargets) {
const output = {
validFoundTargetName: {},
projectContainsUnsupportedExecutor: false,
userProvidedTargetIsUnsupported: {},
alreadyHasNxRspackTargets: {},
};
const supportedExecutors = {
build: [
'@nxext/vite:build',
'@nrwl/webpack:webpack',
'@nrwl/rollup:rollup',
'@nrwl/web:rollup',
'@nrwl/vite:build',
'@nx/webpack:webpack',
'@nx/rollup:rollup',
'@nx/web:rollup',
'@nx/vite:build',
],
serve: [
'@nxext/vite:dev',
'@nrwl/webpack:dev-server',
'@nrwl/vite:dev-server',
'@nx/webpack:dev-server',
'@nx/vite:dev-server',
],
};
const unsupportedExecutors = [
'@nx/js:babel',
'@nx/js:node',
'@nx/js:swc',
'@nx/react-native:run-ios',
'@nx/react-native:start',
'@nx/react-native:run-android',
'@nx/react-native:bundle',
'@nx/react-native:build-android',
'@nx/react-native:bundle',
'@nx/next:build',
'@nx/next:server',
'@nx/js:tsc',
'@nx/angular:ng-packagr-lite',
'@nx/angular:package',
'@nx/angular:webpack-browser',
'@nx/esbuild:esbuild',
'@nrwl/js:babel',
'@nrwl/js:node',
'@nrwl/js:swc',
'@nrwl/react-native:run-ios',
'@nrwl/react-native:start',
'@nrwl/react-native:run-android',
'@nrwl/react-native:bundle',
'@nrwl/react-native:build-android',
'@nrwl/react-native:bundle',
'@nrwl/next:build',
'@nrwl/next:server',
'@nrwl/js:tsc',
'@nrwl/angular:ng-packagr-lite',
'@nrwl/angular:package',
'@nrwl/angular:webpack-browser',
'@nrwl/esbuild:esbuild',
'@angular-devkit/build-angular:browser',
'@angular-devkit/build-angular:dev-server',
];
// First, we check if the user has provided a target
// If they have, we check if the executor the target is using is supported
// If it's not supported, then we set the unsupported flag to true for that target
function checkUserProvidedTarget(target) {
if (userProvidedTargets?.[target]) {
if (supportedExecutors[target].includes(targets[userProvidedTargets[target]]?.executor)) {
output.validFoundTargetName[target] = userProvidedTargets[target];
}
else {
output.userProvidedTargetIsUnsupported[target] = true;
}
}
}
checkUserProvidedTarget('build');
checkUserProvidedTarget('serve');
// Returns early when we have a build, serve, and test targets.
if (output.validFoundTargetName.build && output.validFoundTargetName.serve) {
return output;
}
// We try to find the targets that are using the supported executors
// for build, serve and test, since these are the ones we will be converting
for (const target in targets) {
const executorName = targets[target].executor;
const hasRspackTargets = output.alreadyHasNxRspackTargets;
hasRspackTargets.build ||= executorName === '@nx/rspack:rspack';
hasRspackTargets.serve ||= executorName === '@nx/rspack:dev-server';
const foundTargets = output.validFoundTargetName;
if (!foundTargets.build &&
supportedExecutors.build.includes(executorName)) {
foundTargets.build = target;
}
if (!foundTargets.serve &&
supportedExecutors.serve.includes(executorName)) {
foundTargets.serve = target;
}
output.projectContainsUnsupportedExecutor ||=
unsupportedExecutors.includes(executorName);
}
return output;
}
function addOrChangeBuildTarget(tree, options, target) {
const project = (0, devkit_1.readProjectConfiguration)(tree, options.project);
const assets = [];
if (options.target === 'web' &&
tree.exists((0, devkit_1.joinPathFragments)(project.root, 'src/favicon.ico'))) {
assets.push((0, devkit_1.joinPathFragments)(project.root, 'src/favicon.ico'));
}
if (tree.exists((0, devkit_1.joinPathFragments)(project.root, 'src/assets'))) {
assets.push((0, devkit_1.joinPathFragments)(project.root, 'src/assets'));
}
const isTsSolutionSetup = (0, ts_solution_setup_1.isUsingTsSolutionSetup)(tree);
const buildOptions = {
target: options.target ?? 'web',
outputPath: isTsSolutionSetup
? (0, devkit_1.joinPathFragments)(project.root, 'dist')
: (0, devkit_1.joinPathFragments)('dist',
// If standalone project then use the project's name in dist.
project.root === '.' ? project.name : project.root),
index: (0, devkit_1.joinPathFragments)(project.root, 'src/index.html'),
main: determineMain(tree, options),
tsConfig: determineTsConfig(tree, options),
rspackConfig: (0, devkit_1.joinPathFragments)(project.root, 'rspack.config.js'),
assets,
};
const existingProjectConfigurations = {};
const buildTarget = project.targets.build;
if (buildTarget && buildTarget.configurations) {
for (const [configurationName, configuration] of Object.entries(buildTarget.configurations)) {
existingProjectConfigurations[configurationName] = configuration;
}
}
project.targets ??= {};
project.targets[target] = {
executor: '@nx/rspack:rspack',
outputs: ['{options.outputPath}'],
defaultConfiguration: 'production',
options: buildOptions,
configurations: {
development: {
...(existingProjectConfigurations['development'] ?? {}),
mode: 'development',
},
production: {
...(existingProjectConfigurations['production'] ?? {}),
mode: 'production',
optimization: options.target === 'web' ? true : undefined,
sourceMap: false,
},
},
};
(0, devkit_1.updateProjectConfiguration)(tree, options.project, project);
}
function addOrChangeServeTarget(tree, options, target) {
const project = (0, devkit_1.readProjectConfiguration)(tree, options.project);
project.targets ??= {};
project.targets[target] = {
executor: '@nx/rspack:dev-server',
options: {
buildTarget: `${options.project}:build:development`,
},
configurations: {
development: {},
production: {
buildTarget: `${options.project}:build:production`,
},
},
};
(0, devkit_1.updateProjectConfiguration)(tree, options.project, project);
}
function writeRspackConfigFile(tree, options, stylePreprocessorOptions) {
const project = (0, devkit_1.readProjectConfiguration)(tree, options.project);
tree.write((0, devkit_1.joinPathFragments)(project.root, 'rspack.config.js'), createConfig(tree, { ...options, stylePreprocessorOptions }));
}
function createConfig(tree, options) {
const project = (0, devkit_1.readProjectConfiguration)(tree, options.project);
const buildOptions = createBuildOptions(tree, options, project);
const defaultConfig = generateDefaultConfig(project, buildOptions);
if (options.framework === 'react') {
return generateReactConfig(options);
}
else if (isWebFramework(options)) {
return generateWebConfig(tree, options, defaultConfig);
}
else if (options.framework === 'nest') {
return generateNestConfig(tree, options, project, buildOptions);
}
else {
return generateGenericConfig(tree, options, defaultConfig);
}
}
function createBuildOptions(tree, options, project) {
return {
target: options.target ?? 'web',
outputPath: (0, devkit_1.joinPathFragments)('dist', project.root === '.' ? project.name : project.root),
main: determineMain(tree, options),
tsConfig: determineTsConfig(tree, options),
rspackConfig: (0, devkit_1.joinPathFragments)(project.root, 'rspack.config.js'),
};
}
function generateDefaultConfig(project, buildOptions) {
return `
const { NxAppRspackPlugin } = require('@nx/rspack/app-plugin');
const { join } = require('path');
module.exports = {
output: {
path: join(__dirname, '${(0, devkit_1.offsetFromRoot)(project.root)}${buildOptions.outputPath}'),
},
plugins: [
new NxAppRspackPlugin({
target: '${buildOptions.target}',
tsConfig: '${buildOptions.tsConfig}',
main: '${buildOptions.main}',
outputHashing: '${buildOptions.target !== 'web' ? 'none' : 'all'}',
})
]
}`;
}
function isWebFramework(options) {
return options.framework === 'web' || options.target === 'web';
}
function generateWebConfig(tree, options, defaultConfig) {
if ((0, has_plugin_1.hasPlugin)(tree)) {
return defaultConfig;
}
return `
const { composePlugins, withNx, withWeb } = require('@nx/rspack');
module.exports = composePlugins(withNx(), withWeb(${options.stylePreprocessorOptions
? `
{
stylePreprocessorOptions: ${JSON.stringify(options.stylePreprocessorOptions)},
}
`
: ''}), (config) => {
return config;
});
`;
}
function generateReactConfig({ stylePreprocessorOptions, }) {
return `
const { composePlugins, withNx, withReact } = require('@nx/rspack');
module.exports = composePlugins(withNx(), withReact(${stylePreprocessorOptions
? `
{
stylePreprocessorOptions: ${JSON.stringify(stylePreprocessorOptions)},
}
`
: ''}), (config) => {
return config;
});
`;
}
function generateNestConfig(tree, options, project, buildOptions) {
if ((0, has_plugin_1.hasPlugin)(tree) && options.addPlugin !== false) {
return `
const { NxAppRspackPlugin } = require('@nx/rspack/app-plugin');
const rspack = require('@rspack/core');
const { join } = require('path');
module.exports = {
output: {
path: join(__dirname, '${(0, devkit_1.offsetFromRoot)(project.root)}${buildOptions.outputPath}'),
},
optimization: {
minimizer: [
new rspack.SwcJsMinimizerRspackPlugin({
minimizerOptions: {
compress: {
keep_classnames: true,
keep_fnames: true,
},
mangle: {
keep_classnames: true,
keep_fnames: true,
},
},
}),
],
},
plugins: [
new NxAppRspackPlugin({
target: '${buildOptions.target}',
tsConfig: '${buildOptions.tsConfig}',
main: '${buildOptions.main}',
outputHashing: '${buildOptions.target !== 'web' ? 'none' : 'all'}',
})
]
}`;
}
return `
const { composePlugins, withNx } = require('@nx/rspack');
const rspack = require('@rspack/core');
module.exports = composePlugins(withNx(), (config) => {
config.optimization = {
minimizer: [
new rspack.SwcJsMinimizerRspackPlugin({
minimizerOptions: {
compress: {
keep_classnames: true,
keep_fnames: true,
},
mangle: {
keep_classnames: true,
keep_fnames: true,
},
},
}),
],
};
return config;
});
`;
}
function generateGenericConfig(tree, options, defaultConfig) {
if ((0, has_plugin_1.hasPlugin)(tree)) {
return defaultConfig;
}
return `
const { composePlugins, withNx${options.stylePreprocessorOptions ? ', withWeb' : ''} } = require('@nx/rspack');
module.exports = composePlugins(withNx()${options.stylePreprocessorOptions
? `,
withWeb({
stylePreprocessorOptions: ${JSON.stringify(options.stylePreprocessorOptions)},
})`
: ''}, (config) => {
return config;
});
`;
}
function deleteWebpackConfig(tree, projectRoot, webpackConfigFilePath) {
const webpackConfigPath = webpackConfigFilePath && tree.exists(webpackConfigFilePath)
? webpackConfigFilePath
: tree.exists(`${projectRoot}/webpack.config.js`)
? `${projectRoot}/webpack.config.js`
: tree.exists(`${projectRoot}/webpack.config.ts`)
? `${projectRoot}/webpack.config.ts`
: null;
if (webpackConfigPath) {
tree.delete(webpackConfigPath);
}
}
// Maybe add delete vite config?
function moveAndEditIndexHtml(tree, options, buildTarget) {
const projectConfig = (0, devkit_1.readProjectConfiguration)(tree, options.project);
let indexHtmlPath = projectConfig.targets?.[buildTarget]?.options?.index ??
`${projectConfig.root}/src/index.html`;
let mainPath = projectConfig.targets?.[buildTarget]?.options?.main ??
`${projectConfig.root}/src/main.ts${options.framework === 'react' ? 'x' : ''}`;
if (projectConfig.root !== '.') {
mainPath = mainPath.replace(projectConfig.root, '');
}
if (!tree.exists(indexHtmlPath) &&
tree.exists(`${projectConfig.root}/index.html`)) {
indexHtmlPath = `${projectConfig.root}/index.html`;
}
if (tree.exists(indexHtmlPath)) {
const indexHtmlContent = tree.read(indexHtmlPath, 'utf8');
if (!indexHtmlContent.includes(`<script type="module" src="${mainPath}"></script>`)) {
tree.write(`${projectConfig.root}/index.html`, indexHtmlContent.replace('</body>', `<script type="module" src="${mainPath}"></script>
</body>`));
if (tree.exists(`${projectConfig.root}/src/index.html`)) {
tree.delete(`${projectConfig.root}/src/index.html`);
}
}
}
else {
tree.write(`${projectConfig.root}/index.html`, `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="${mainPath}"></script>
</body>
</html>`);
}
}
function normalizeViteConfigFilePathWithTree(tree, projectRoot, configFile) {
return configFile && tree.exists(configFile)
? configFile
: tree.exists((0, devkit_1.joinPathFragments)(`${projectRoot}/rspack.config.ts`))
? (0, devkit_1.joinPathFragments)(`${projectRoot}/rspack.config.ts`)
: tree.exists((0, devkit_1.joinPathFragments)(`${projectRoot}/rspack.config.js`))
? (0, devkit_1.joinPathFragments)(`${projectRoot}/rspack.config.js`)
: undefined;
}
function getViteConfigPathForProject(tree, projectName, target) {
let viteConfigPath;
const { targets, root } = (0, devkit_1.readProjectConfiguration)(tree, projectName);
if (target) {
viteConfigPath = targets?.[target]?.options?.configFile;
}
else {
const config = Object.values(targets).find((config) => config.executor === '@nx/rspack:build');
viteConfigPath = config?.options?.configFile;
}
return normalizeViteConfigFilePathWithTree(tree, root, viteConfigPath);
}
async function handleUnsupportedUserProvidedTargets(userProvidedTargetIsUnsupported, userProvidedTargetName, validFoundTargetName, framework) {
if (userProvidedTargetIsUnsupported.build && validFoundTargetName.build) {
await handleUnsupportedUserProvidedTargetsErrors(userProvidedTargetName.build, validFoundTargetName.build, 'build', 'rspack');
}
if (framework !== 'nest' &&
userProvidedTargetIsUnsupported.serve &&
validFoundTargetName.serve) {
await handleUnsupportedUserProvidedTargetsErrors(userProvidedTargetName.serve, validFoundTargetName.serve, 'serve', 'dev-server');
}
}
async function handleUnsupportedUserProvidedTargetsErrors(userProvidedTargetName, validFoundTargetName, target, executor) {
devkit_1.logger.warn(`The custom ${target} target you provided (${userProvidedTargetName}) cannot be converted to use the @nx/rspack:${executor} executor.
However, we found the following ${target} target in your project that can be converted: ${validFoundTargetName}
Please note that converting a potentially non-compatible project to use Vite may result in unexpected behavior. Always commit
your changes before converting a project to use Vite, and test the converted project thoroughly before deploying it.
`);
const { Confirm } = require('enquirer');
const prompt = new Confirm({
name: 'question',
message: `Should we convert the ${validFoundTargetName} target to use the @nx/rspack:${executor} executor?`,
initial: true,
});
const shouldConvert = await prompt.run();
if (!shouldConvert) {
throw new Error(`The ${target} target ${userProvidedTargetName} cannot be converted to use the @nx/rspack:${executor} executor.
Please try again, either by providing a different ${target} target or by not providing a target at all (Nx will
convert the first one it finds, most probably this one: ${validFoundTargetName})
Please note that converting a potentially non-compatible project to use Vite may result in unexpected behavior. Always commit
your changes before converting a project to use Vite, and test the converted project thoroughly before deploying it.
`);
}
}
async function handleUnknownExecutors(projectName) {
devkit_1.logger.warn(`
We could not find any targets in project ${projectName} that use executors which
can be converted to the @nx/rspack executors.
This either means that your project may not have a target
for building, serving, or testing at all, or that your targets are
using executors that are not known to Nx.
If you still want to convert your project to use the @nx/rspack executors,
please make sure to commit your changes before running this generator.
`);
const { Confirm } = require('enquirer');
const prompt = new Confirm({
name: 'question',
message: `Should Nx convert your project to use the @nx/rspack executors?`,
initial: true,
});
const shouldConvert = await prompt.run();
if (!shouldConvert) {
throw new Error(`
Nx could not verify that the executors you are using can be converted to the @nx/rspack executors.
Please try again with a different project.
`);
}
}
function determineFrameworkAndTarget(tree, options, projectRoot, targets) {
(0, ensure_typescript_1.ensureTypescript)();
const { ast, query } = require('@phenomnomnominal/tsquery');
// First try to infer if the target is node
if (options.target !== 'node') {
// Try to infer from jest config if the env is node
let jestConfigPath;
if (targets?.test?.executor !== '@nx/jest:jest' &&
targets?.test?.options?.jestConfig) {
jestConfigPath = targets?.test?.options?.jestConfig;
}
else {
jestConfigPath = (0, devkit_1.joinPathFragments)(projectRoot, 'jest.config.ts');
}
if (!tree.exists(jestConfigPath)) {
return { target: options.target, framework: options.framework };
}
const appFileContent = tree.read(jestConfigPath, 'utf-8');
const file = ast(appFileContent);
// find testEnvironment: 'node' in jest config
const testEnvironment = query(file, `PropertyAssignment:has(Identifier[name="testEnvironment"]) > StringLiteral[value="node"]`);
if (testEnvironment.length > 0) {
return { target: 'node', framework: options.framework };
}
if (tree.exists((0, devkit_1.joinPathFragments)(projectRoot, 'src/main.ts'))) {
const appFileContent = tree.read((0, devkit_1.joinPathFragments)(projectRoot, 'src/main.ts'), 'utf-8');
const file = ast(appFileContent);
const hasNestJsDependency = query(file, `ImportDeclaration:has(StringLiteral[value="@nestjs/common"])`);
if (hasNestJsDependency?.length > 0) {
return { target: 'node', framework: 'nest' };
}
}
}
if (options.framework === 'nest') {
return { target: 'node', framework: 'nest' };
}
if (options.framework !== 'react' && options.target === 'web') {
// Look if React is used in the project
let tsConfigPath = (0, devkit_1.joinPathFragments)(projectRoot, 'tsconfig.json');
if (!tree.exists(tsConfigPath)) {
tsConfigPath = determineTsConfig(tree, options);
}
const tsConfig = JSON.parse(tree.read(tsConfigPath).toString());
if (tsConfig?.compilerOptions?.jsx?.includes('react')) {
return { target: 'web', framework: 'react' };
}
else {
return { target: options.target, framework: options.framework };
}
}
return { target: options.target, framework: options.framework };
}
function determineMain(tree, options) {
if (options.main)
return options.main;
const project = (0, devkit_1.readProjectConfiguration)(tree, options.project);
const mainTsx = (0, devkit_1.joinPathFragments)(project.root, 'src/main.tsx');
if (tree.exists(mainTsx))
return mainTsx;
return (0, devkit_1.joinPathFragments)(project.root, 'src/main.ts');
}
function determineTsConfig(tree, options) {
if (options.tsConfig)
return options.tsConfig;
const project = (0, devkit_1.readProjectConfiguration)(tree, options.project);
const appJson = (0, devkit_1.joinPathFragments)(project.root, 'tsconfig.app.json');
if (tree.exists(appJson))
return appJson;
const libJson = (0, devkit_1.joinPathFragments)(project.root, 'tsconfig.lib.json');
if (tree.exists(libJson))
return libJson;
return (0, devkit_1.joinPathFragments)(project.root, 'tsconfig.json');
}