@nx/angular
Version:
521 lines (520 loc) • 23.5 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.convertToRspack = convertToRspack;
const devkit_1 = require("@nx/devkit");
const executor_options_utils_1 = require("@nx/devkit/src/generators/executor-options-utils");
const get_named_inputs_1 = require("@nx/devkit/src/utils/get-named-inputs");
const enquirer_1 = require("enquirer");
const path_1 = require("path");
const posix_1 = require("path/posix");
const versions_1 = require("../../utils/versions");
const version_utils_1 = require("../utils/version-utils");
const create_config_1 = require("./lib/create-config");
const get_custom_webpack_config_1 = require("./lib/get-custom-webpack-config");
const update_tsconfig_1 = require("./lib/update-tsconfig");
const validate_supported_executor_1 = require("./lib/validate-supported-executor");
const SUPPORTED_EXECUTORS = [
'@angular-devkit/build-angular:browser',
'@angular-devkit/build-angular:dev-server',
'@angular-devkit/build-angular:server',
'@angular-devkit/build-angular:prerender',
'@angular-devkit/build-angular:app-shell',
'@nx/angular:webpack-browser',
'@nx/angular:webpack-server',
'@nx/angular:dev-server',
'@nx/angular:module-federation-dev-server',
];
const RENAMED_OPTIONS = {
main: 'browser',
ngswConfigPath: 'serviceWorker',
};
const DEFAULT_PORT = 4200;
const REMOVED_OPTIONS = ['buildOptimizer', 'buildTarget', 'browserTarget'];
function normalizeFromProjectRoot(tree, path, projectRoot) {
if (projectRoot === '.') {
if (!path.startsWith('./')) {
return `./${path}`;
}
else {
return path;
}
}
else if (path.startsWith(projectRoot)) {
return path.replace(projectRoot, '.');
}
else if (!path.startsWith('./')) {
if (tree.exists(path)) {
const pathWithWorkspaceRoot = (0, devkit_1.joinPathFragments)(devkit_1.workspaceRoot, path);
const projectRootWithWorkspaceRoot = (0, devkit_1.joinPathFragments)(devkit_1.workspaceRoot, projectRoot);
return (0, path_1.relative)(projectRootWithWorkspaceRoot, pathWithWorkspaceRoot);
}
return `./${path}`;
}
return path;
}
const defaultNormalizer = (tree, path, root) => normalizeFromProjectRoot(tree, path, root);
const PATH_NORMALIZER = {
index: (tree, path, root) => {
if (typeof path === 'string') {
return normalizeFromProjectRoot(tree, path, root);
}
return {
input: normalizeFromProjectRoot(tree, path.input, root),
output: path.output ?? 'index.html',
};
},
indexHtmlTransformer: defaultNormalizer,
main: defaultNormalizer,
server: defaultNormalizer,
tsConfig: defaultNormalizer,
outputPath: (tree, path, root) => {
const relativePathFromWorkspaceRoot = (0, path_1.relative)((0, devkit_1.joinPathFragments)(devkit_1.workspaceRoot, root), devkit_1.workspaceRoot);
return (0, devkit_1.joinPathFragments)(relativePathFromWorkspaceRoot, path);
},
proxyConfig: defaultNormalizer,
polyfills: (tree, paths, root) => {
const normalizedPaths = [];
const normalizeFn = (path) => {
if (path.startsWith('zone.js')) {
normalizedPaths.push(path);
return;
}
try {
const resolvedPath = require.resolve(path, {
paths: [(0, posix_1.join)(devkit_1.workspaceRoot, 'node_modules')],
});
normalizedPaths.push(path);
}
catch {
normalizedPaths.push(normalizeFromProjectRoot(tree, path, root));
}
};
if (typeof paths === 'string') {
normalizeFn(paths);
}
else {
for (const path of paths) {
normalizeFn(path);
}
}
return normalizedPaths;
},
styles: (tree, paths, root) => {
const normalizedPaths = [];
for (const path of paths) {
if (typeof path === 'string') {
normalizedPaths.push(normalizeFromProjectRoot(tree, path, root));
}
else {
normalizedPaths.push({
input: normalizeFromProjectRoot(tree, path.input, root),
bundleName: path.bundleName,
inject: path.inject ?? true,
});
}
}
return normalizedPaths;
},
scripts: (tree, paths, root) => {
const normalizedPaths = [];
for (const path of paths) {
if (typeof path === 'string') {
normalizedPaths.push(normalizeFromProjectRoot(tree, path, root));
}
else {
normalizedPaths.push({
input: normalizeFromProjectRoot(tree, path.input, root),
bundleName: path.bundleName,
inject: path.inject ?? true,
});
}
}
return normalizedPaths;
},
assets: (tree, paths, root) => {
const normalizedPaths = [];
for (const path of paths) {
if (typeof path === 'string') {
normalizedPaths.push(normalizeFromProjectRoot(tree, path, root));
}
else {
normalizedPaths.push({
...path,
input: normalizeFromProjectRoot(tree, path.input, root),
});
}
}
return normalizedPaths;
},
fileReplacements: (tree, paths, root) => {
const normalizedPaths = [];
for (const path of paths) {
normalizedPaths.push({
replace: normalizeFromProjectRoot(tree, 'src' in path ? path.src : path.replace, root),
with: normalizeFromProjectRoot(tree, 'replaceWith' in path ? path.replaceWith : path.with, root),
});
}
return normalizedPaths;
},
};
function handleBuildTargetOptions(tree, options, newConfigurationOptions, root) {
let customWebpackConfigPath;
if (!options || Object.keys(options).length === 0) {
return customWebpackConfigPath;
}
if (options.customWebpackConfig) {
customWebpackConfigPath = options.customWebpackConfig.path;
delete options.customWebpackConfig;
}
for (const [key, value] of Object.entries(options)) {
let optionName = key;
let optionValue = key in PATH_NORMALIZER ? PATH_NORMALIZER[key](tree, value, root) : value;
if (REMOVED_OPTIONS.includes(key)) {
continue;
}
if (key in RENAMED_OPTIONS) {
optionName = RENAMED_OPTIONS[key];
}
newConfigurationOptions[optionName] = optionValue;
}
if (typeof newConfigurationOptions.polyfills === 'string') {
newConfigurationOptions.polyfills = [newConfigurationOptions.polyfills];
}
let outputPath = newConfigurationOptions.outputPath;
if (typeof outputPath === 'string') {
if (!/\/browser\/?$/.test(outputPath)) {
console.warn(`The output location of the browser build has been updated from "${outputPath}" to ` +
`"${(0, posix_1.join)(outputPath, 'browser')}". ` +
'You might need to adjust your deployment pipeline or, as an alternative, ' +
'set outputPath.browser to "" in order to maintain the previous functionality.');
}
else {
outputPath = outputPath.replace(/\/browser\/?$/, '');
}
newConfigurationOptions['outputPath'] = {
base: outputPath,
};
if (typeof newConfigurationOptions.resourcesOutputPath === 'string') {
const media = newConfigurationOptions.resourcesOutputPath.replaceAll('/', '');
if (media && media !== 'media') {
newConfigurationOptions['outputPath'] = {
base: outputPath,
media,
};
}
}
}
return customWebpackConfigPath;
}
function handleDevServerTargetOptions(tree, options, newConfigurationOptions, root) {
for (const [key, value] of Object.entries(options)) {
let optionName = key;
let optionValue = key in PATH_NORMALIZER ? PATH_NORMALIZER[key](tree, value, root) : value;
if (REMOVED_OPTIONS.includes(key)) {
continue;
}
if (key in RENAMED_OPTIONS) {
optionName = RENAMED_OPTIONS[key];
}
newConfigurationOptions[optionName] = optionValue;
}
}
async function getProjectToConvert(tree) {
const projects = new Set();
for (const executor of SUPPORTED_EXECUTORS) {
(0, executor_options_utils_1.forEachExecutorOptions)(tree, executor, (_, project) => {
projects.add(project);
});
}
const { project } = await (0, enquirer_1.prompt)({
type: 'select',
name: 'project',
message: 'Which project would you like to convert to rspack?',
choices: Array.from(projects),
});
return project;
}
async function convertToRspack(tree, schema) {
let { project: projectName } = schema;
if (!projectName) {
projectName = await getProjectToConvert(tree);
}
const project = (0, devkit_1.readProjectConfiguration)(tree, projectName);
const tasks = [];
const createConfigOptions = {
root: project.root,
};
const configurationOptions = {};
let buildTarget;
let serveTarget;
const targetsToRemove = [];
let customWebpackConfigPath;
(0, validate_supported_executor_1.validateSupportedBuildExecutor)(Object.values(project.targets));
let projectServePort = DEFAULT_PORT;
const projectServeConfigurationOptions = {};
for (const [targetName, target] of Object.entries(project.targets)) {
if (target.executor === '@angular-devkit/build-angular:browser' ||
target.executor === '@nx/angular:webpack-browser') {
customWebpackConfigPath = handleBuildTargetOptions(tree, target.options, createConfigOptions, project.root);
if (target.configurations) {
for (const [configurationName, configuration] of Object.entries(target.configurations)) {
configurationOptions[configurationName] = {};
handleBuildTargetOptions(tree, configuration, configurationOptions[configurationName], project.root);
}
}
buildTarget = { name: targetName, config: target };
targetsToRemove.push(targetName);
}
else if (target.executor === '@angular-devkit/build-angular:server' ||
target.executor === '@nx/angular:webpack-server') {
createConfigOptions.ssr ??= {};
createConfigOptions.ssr.entry ??= normalizeFromProjectRoot(tree, target.options.main, project.root);
createConfigOptions.server = './src/main.server.ts';
targetsToRemove.push(targetName);
}
else if (target.executor === '@angular-devkit/build-angular:dev-server' ||
target.executor === '@nx/angular:dev-server' ||
target.executor === '@nx/angular:module-federation-dev-server') {
createConfigOptions.devServer = {};
if (target.options) {
handleDevServerTargetOptions(tree, target.options, createConfigOptions.devServer, project.root);
if (target.options.port && target.options.port !== DEFAULT_PORT) {
projectServePort = target.options.port;
}
}
if (target.configurations) {
for (const [configurationName, configuration] of Object.entries(target.configurations)) {
configurationOptions[configurationName] ??= {};
configurationOptions[configurationName].devServer ??= {};
handleDevServerTargetOptions(tree, configuration, configurationOptions[configurationName].devServer, project.root);
if (configuration.port && configuration.port !== DEFAULT_PORT) {
projectServeConfigurationOptions[configurationName] ??= {};
projectServeConfigurationOptions[configurationName].port =
configuration.port;
}
}
}
serveTarget = { name: targetName, config: target };
targetsToRemove.push(targetName);
}
else if (target.executor === '@angular-devkit/build-angular:prerender') {
if (target.options) {
const prerenderOptions = {
routesFile: target.options.routesFile,
discoverRoutes: target.options.discoverRoutes ?? true,
routes: target.options.routes ?? [],
};
createConfigOptions.prerender = prerenderOptions;
if (target.configurations) {
for (const [configurationName, configuration] of Object.entries(target.configurations)) {
configurationOptions[configurationName] ??= {};
configurationOptions[configurationName].prerender ??= {
routesFile: configuration.routesFile,
discoverRoutes: configuration.discoverRoutes ?? true,
routes: configuration.routes ?? [],
};
}
}
}
targetsToRemove.push(targetName);
}
else if (target.executor === '@angular-devkit/build-angular:app-shell') {
createConfigOptions.appShell = true;
targetsToRemove.push(targetName);
}
}
const customWebpackConfigInfo = customWebpackConfigPath
? await (0, get_custom_webpack_config_1.getCustomWebpackConfig)(tree, project.root, customWebpackConfigPath)
: undefined;
(0, create_config_1.createConfig)(tree, createConfigOptions, configurationOptions, customWebpackConfigInfo?.normalizedPathToCustomWebpackConfig, customWebpackConfigInfo?.isWebpackConfigFunction);
(0, update_tsconfig_1.updateTsconfig)(tree, project.root);
for (const targetName of targetsToRemove) {
delete project.targets[targetName];
}
(0, devkit_1.updateProjectConfiguration)(tree, projectName, project);
// ensure plugin is registered
const { rspackInitGenerator } = (0, devkit_1.ensurePackage)('@nx/rspack', versions_1.nxVersion);
await rspackInitGenerator(tree, {
addPlugin: true,
framework: 'angular',
});
// find the inferred target names
const nxJson = (0, devkit_1.readNxJson)(tree);
let inferredBuildTargetName = 'build';
let inferredServeTargetName = 'serve';
const pluginRegistration = nxJson.plugins.find((p) => typeof p === 'string' ? false : p.plugin === '@nx/rspack/plugin');
if (pluginRegistration) {
inferredBuildTargetName =
pluginRegistration.options.buildTargetName ?? inferredBuildTargetName;
inferredServeTargetName =
pluginRegistration.options.serveTargetName ?? inferredServeTargetName;
}
if (buildTarget) {
// these are all replaced by the inferred task
delete buildTarget.config.options;
delete buildTarget.config.configurations;
delete buildTarget.config.defaultConfiguration;
delete buildTarget.config.executor;
const shouldOverrideInputs = (inputs) => {
if (!inputs?.length) {
return false;
}
if (inputs.length === 2) {
// check whether the existing inputs would match the inferred task
// inputs with the exception of the @rspack/cli external dependency
// which webpack tasks wouldn't have
const namedInputs = (0, get_named_inputs_1.getNamedInputs)(project.root, {
nxJsonConfiguration: nxJson,
workspaceRoot: devkit_1.workspaceRoot,
});
if ('production' in namedInputs) {
return !['production', '^production'].every((input) => inputs.includes(input));
}
return !['default', '^default'].every((input) => inputs.includes(input));
}
return true;
};
if (shouldOverrideInputs(buildTarget.config.inputs)) {
// keep existing inputs and add the @rspack/cli external dependency
buildTarget.config.inputs = [
...buildTarget.config.inputs,
{ externalDependencies: ['@rspack/cli'] },
];
}
else {
delete buildTarget.config.inputs;
}
if (buildTarget.config.cache) {
delete buildTarget.config.cache;
}
if (buildTarget.config.dependsOn?.length === 1 &&
buildTarget.config.dependsOn[0] === `^${buildTarget.name}`) {
delete buildTarget.config.dependsOn;
}
else if (buildTarget.config.dependsOn) {
buildTarget.config.dependsOn = buildTarget.config.dependsOn.map((dep) => dep === `^${buildTarget.name}` ? `^${inferredBuildTargetName}` : dep);
}
const newOutputPath = (0, devkit_1.joinPathFragments)(project.root, createConfigOptions.outputPath.base);
const shouldOverrideOutputs = (outputs) => {
if (!outputs?.length) {
// this means the target was wrongly configured, so, we don't override
// anything and let the inferred outputs be used
return false;
}
if (outputs.length === 1) {
if (outputs[0] === '{options.outputPath}') {
// the inferred task output is created after the createConfig
// outputPath option, so we don't need to keep this
return false;
}
const normalizedOutputPath = outputs[0]
.replace('{workspaceRoot}/', '')
.replace('{projectRoot}', project.root)
.replace('{projectName}', '');
if (normalizedOutputPath === newOutputPath ||
normalizedOutputPath.replace(/\/browser\/?$/, '') === newOutputPath) {
return false;
}
}
return true;
};
const normalizeOutput = (path, workspaceRoot, projectRoot) => {
const fullProjectRoot = (0, path_1.resolve)(workspaceRoot, projectRoot);
const fullPath = (0, path_1.resolve)(workspaceRoot, path);
const pathRelativeToProjectRoot = (0, devkit_1.normalizePath)((0, path_1.relative)(fullProjectRoot, fullPath));
if (pathRelativeToProjectRoot.startsWith('..')) {
return (0, devkit_1.joinPathFragments)('{workspaceRoot}', (0, path_1.relative)(workspaceRoot, fullPath));
}
return (0, devkit_1.joinPathFragments)('{projectRoot}', pathRelativeToProjectRoot);
};
if (shouldOverrideOutputs(buildTarget.config.outputs)) {
buildTarget.config.outputs = buildTarget.config.outputs.map((output) => {
if (output === '{options.outputPath}') {
// the target won't have an outputPath option, so we replace it with the new output path
return normalizeOutput(newOutputPath, devkit_1.workspaceRoot, project.root);
}
const normalizedOutputPath = output
.replace('{workspaceRoot}/', '')
.replace('{projectRoot}', project.root)
.replace('{projectName}', '');
if (/\/browser\/?$/.test(normalizedOutputPath) &&
normalizedOutputPath.replace(/\/browser\/?$/, '') === newOutputPath) {
return normalizeOutput(newOutputPath, devkit_1.workspaceRoot, project.root);
}
return output;
});
}
else {
delete buildTarget.config.outputs;
}
if (buildTarget.config.syncGenerators?.length === 1 &&
buildTarget.config.syncGenerators[0] === '@nx/js:typescript-sync') {
delete buildTarget.config.syncGenerators;
}
else if (buildTarget.config.syncGenerators?.length) {
buildTarget.config.syncGenerators = Array.from(new Set([
...buildTarget.config.syncGenerators,
'@nx/js:typescript-sync',
]));
}
if (Object.keys(buildTarget.config).length) {
// there's extra target metadata left that wouldn't be inferred, we keep it
project.targets[inferredBuildTargetName] = buildTarget.config;
}
}
if (serveTarget) {
delete serveTarget.config.options;
delete serveTarget.config.configurations;
delete serveTarget.config.defaultConfiguration;
delete serveTarget.config.executor;
if (serveTarget.config.continuous) {
delete serveTarget.config.continuous;
}
if (serveTarget.config.syncGenerators?.length === 1 &&
serveTarget.config.syncGenerators[0] === '@nx/js:typescript-sync') {
delete serveTarget.config.syncGenerators;
}
else if (serveTarget.config.syncGenerators?.length) {
serveTarget.config.syncGenerators = Array.from(new Set([
...serveTarget.config.syncGenerators,
'@nx/js:typescript-sync',
]));
}
if (projectServePort !== DEFAULT_PORT) {
serveTarget.config.options = {};
serveTarget.config.options.port = projectServePort;
}
if (Object.keys(projectServeConfigurationOptions).length > 0) {
serveTarget.config.configurations = {};
for (const [configurationName, options] of Object.entries(projectServeConfigurationOptions)) {
serveTarget.config.configurations[configurationName] = options;
}
}
if (Object.keys(serveTarget.config).length) {
// there's extra target metadata left that wouldn't be inferred, we keep it
project.targets[inferredServeTargetName] = serveTarget.config;
}
}
(0, devkit_1.updateProjectConfiguration)(tree, projectName, project);
// This is needed to prevent a circular execution of the build target
const rootPkgJson = (0, devkit_1.readJson)(tree, 'package.json');
if (rootPkgJson.scripts?.build === 'nx build') {
delete rootPkgJson.scripts.build;
(0, devkit_1.writeJson)(tree, 'package.json', rootPkgJson);
}
if (!schema.skipInstall) {
const { webpackMergeVersion, tsNodeVersion } = (0, version_utils_1.versions)(tree);
const angularRspackVersion = (0, version_utils_1.getAngularRspackVersion)(tree);
const installTask = (0, devkit_1.addDependenciesToPackageJson)(tree, {}, {
'@nx/angular-rspack': angularRspackVersion,
'webpack-merge': webpackMergeVersion,
'ts-node': tsNodeVersion,
});
tasks.push(installTask);
}
if (!schema.skipFormat) {
await (0, devkit_1.formatFiles)(tree);
}
return (0, devkit_1.runTasksInSerial)(...tasks);
}
exports.default = convertToRspack;