UNPKG

@nx/angular

Version:

The Nx Plugin for Angular contains executors, generators, and utilities for managing Angular applications and libraries within an Nx workspace. It provides: - Integration with libraries such as Storybook, Jest, ESLint, Tailwind CSS, Playwright and Cypre

521 lines (520 loc) • 23.5 kB
"use strict"; 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;