fix(angular): keep extra target metadata when needed in convert-to-rspack generator (#31309)

## Current Behavior

When converting an Angular project to use Rspack with the
`@nx/angular:convert-to-rspack` generator, some target top-level options
can be lost (e.g. custom `dependsOn`, `outputs`, etc.).

## Expected Behavior

When converting an Angular project to use Rspack with the
`@nx/angular:convert-to-rspack` generator, relevant target top-level
options that wouldn't be inferred need to be kept in the converted
project.
This commit is contained in:
Leosvel Pérez Espinosa 2025-06-02 12:58:41 +02:00 committed by GitHub
parent f02cc49b06
commit 2cf519a654
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 702 additions and 34 deletions

View File

@ -743,4 +743,468 @@ describe('convert-to-rspack', () => {
" "
`); `);
}); });
describe('top-level target options', () => {
describe('build target', () => {
it('should remove the target when there are no relevant top-level options', async () => {
const tree = createTreeWithEmptyWorkspace();
addProjectConfiguration(tree, 'app', {
root: 'apps/app',
sourceRoot: 'apps/app/src',
projectType: 'application',
targets: {
build: {
executor: '@angular-devkit/build-angular:browser',
options: {
outputPath: 'dist/apps/app',
index: 'apps/app/src/index.html',
main: 'apps/app/src/main.ts',
tsConfig: 'apps/app/tsconfig.app.json',
},
},
},
});
writeJson(tree, 'apps/app/tsconfig.json', {});
await convertToRspack(tree, { project: 'app' });
const updatedProject = readProjectConfiguration(tree, 'app');
expect(updatedProject.targets.build).not.toBeDefined();
});
it('should remove the target when all the top-level options match what would be inferred', async () => {
const tree = createTreeWithEmptyWorkspace();
updateJson(tree, 'nx.json', (json) => {
json.namedInputs = {
...json.namedInputs,
production: [
'default',
'!{projectRoot}/**/?(*.)+(spec|test).[jt]s?(x)?(.snap)',
],
};
return json;
});
addProjectConfiguration(tree, 'app', {
root: 'apps/app',
sourceRoot: 'apps/app/src',
projectType: 'application',
targets: {
build: {
dependsOn: ['^build'],
cache: true,
inputs: ['production', '^production'],
outputs: ['{options.outputPath}'],
syncGenerators: ['@nx/js:typescript-sync'],
executor: '@angular-devkit/build-angular:browser',
options: {
outputPath: 'dist/apps/app',
index: 'apps/app/src/index.html',
main: 'apps/app/src/main.ts',
tsConfig: 'apps/app/tsconfig.app.json',
},
},
},
});
writeJson(tree, 'apps/app/tsconfig.json', {});
await convertToRspack(tree, { project: 'app' });
const updatedProject = readProjectConfiguration(tree, 'app');
expect(updatedProject.targets.build).not.toBeDefined();
});
it('should remove the target when the normalized output matches what would be inferred', async () => {
const tree = createTreeWithEmptyWorkspace();
addProjectConfiguration(tree, 'app', {
root: 'apps/app',
sourceRoot: 'apps/app/src',
projectType: 'application',
targets: {
build: {
outputs: ['{workspaceRoot}/dist/{projectRoot}'],
executor: '@angular-devkit/build-angular:browser',
options: {
outputPath: 'dist/apps/app',
index: 'apps/app/src/index.html',
main: 'apps/app/src/main.ts',
tsConfig: 'apps/app/tsconfig.app.json',
},
},
},
});
writeJson(tree, 'apps/app/tsconfig.json', {});
await convertToRspack(tree, { project: 'app' });
const updatedProject = readProjectConfiguration(tree, 'app');
expect(updatedProject.targets.build).not.toBeDefined();
});
it('should remove the target when the transformed output matches what would be inferred', async () => {
const tree = createTreeWithEmptyWorkspace();
addProjectConfiguration(tree, 'app', {
root: 'apps/app',
sourceRoot: 'apps/app/src',
projectType: 'application',
targets: {
build: {
outputs: ['{workspaceRoot}/dist/{projectRoot}/browser'],
executor: '@angular-devkit/build-angular:browser',
options: {
outputPath: 'dist/apps/app/browser',
index: 'apps/app/src/index.html',
main: 'apps/app/src/main.ts',
tsConfig: 'apps/app/tsconfig.app.json',
},
},
},
});
writeJson(tree, 'apps/app/tsconfig.json', {});
await convertToRspack(tree, { project: 'app' });
const updatedProject = readProjectConfiguration(tree, 'app');
expect(updatedProject.targets.build).not.toBeDefined();
});
it('should keep the target with updated outputs when they would not match what would be inferred', async () => {
const tree = createTreeWithEmptyWorkspace();
addProjectConfiguration(tree, 'app', {
root: 'apps/app',
sourceRoot: 'apps/app/src',
projectType: 'application',
targets: {
build: {
outputs: [
// will be replaced with a explicit output path because the
// inferred task won't have an outputPath option
'{options.outputPath}',
'{workspaceRoot}/some-other-output',
],
executor: '@angular-devkit/build-angular:browser',
options: {
outputPath: 'dist/apps/app/browser',
index: 'apps/app/src/index.html',
main: 'apps/app/src/main.ts',
tsConfig: 'apps/app/tsconfig.app.json',
},
},
},
});
writeJson(tree, 'apps/app/tsconfig.json', {});
await convertToRspack(tree, { project: 'app' });
const updatedProject = readProjectConfiguration(tree, 'app');
expect(updatedProject.targets.build).toStrictEqual({
outputs: [
'{workspaceRoot}/dist/apps/app',
'{workspaceRoot}/some-other-output',
],
});
});
it('should remove the target when the dependsOn option matches what would be inferred', async () => {
const tree = createTreeWithEmptyWorkspace();
addProjectConfiguration(tree, 'app', {
root: 'apps/app',
sourceRoot: 'apps/app/src',
projectType: 'application',
targets: {
build: {
dependsOn: ['^build'],
executor: '@angular-devkit/build-angular:browser',
options: {
outputPath: 'dist/apps/app',
index: 'apps/app/src/index.html',
main: 'apps/app/src/main.ts',
tsConfig: 'apps/app/tsconfig.app.json',
},
},
},
});
writeJson(tree, 'apps/app/tsconfig.json', {});
await convertToRspack(tree, { project: 'app' });
const updatedProject = readProjectConfiguration(tree, 'app');
expect(updatedProject.targets.build).not.toBeDefined();
});
it('should keep the target with dependsOn when they would not match what would be inferred', async () => {
const tree = createTreeWithEmptyWorkspace();
addProjectConfiguration(tree, 'app', {
root: 'apps/app',
sourceRoot: 'apps/app/src',
projectType: 'application',
targets: {
build: {
dependsOn: ['pre-build', '^build'],
executor: '@angular-devkit/build-angular:browser',
options: {
outputPath: 'dist/apps/app/browser',
index: 'apps/app/src/index.html',
main: 'apps/app/src/main.ts',
tsConfig: 'apps/app/tsconfig.app.json',
},
},
},
});
writeJson(tree, 'apps/app/tsconfig.json', {});
await convertToRspack(tree, { project: 'app' });
const updatedProject = readProjectConfiguration(tree, 'app');
expect(updatedProject.targets.build).toStrictEqual({
dependsOn: ['pre-build', '^build'],
});
});
it('should remove the target when the syncGenerators option matches what would be inferred', async () => {
const tree = createTreeWithEmptyWorkspace();
addProjectConfiguration(tree, 'app', {
root: 'apps/app',
sourceRoot: 'apps/app/src',
projectType: 'application',
targets: {
build: {
syncGenerators: ['@nx/js:typescript-sync'],
executor: '@angular-devkit/build-angular:browser',
options: {
outputPath: 'dist/apps/app',
index: 'apps/app/src/index.html',
main: 'apps/app/src/main.ts',
tsConfig: 'apps/app/tsconfig.app.json',
},
},
},
});
writeJson(tree, 'apps/app/tsconfig.json', {});
await convertToRspack(tree, { project: 'app' });
const updatedProject = readProjectConfiguration(tree, 'app');
expect(updatedProject.targets.build).not.toBeDefined();
});
it('should keep the target with syncGenerators when they would not match what would be inferred', async () => {
const tree = createTreeWithEmptyWorkspace();
addProjectConfiguration(tree, 'app', {
root: 'apps/app',
sourceRoot: 'apps/app/src',
projectType: 'application',
targets: {
build: {
syncGenerators: ['@foo/bar:baz'],
executor: '@angular-devkit/build-angular:browser',
options: {
outputPath: 'dist/apps/app/browser',
index: 'apps/app/src/index.html',
main: 'apps/app/src/main.ts',
tsConfig: 'apps/app/tsconfig.app.json',
},
},
},
});
writeJson(tree, 'apps/app/tsconfig.json', {});
await convertToRspack(tree, { project: 'app' });
const updatedProject = readProjectConfiguration(tree, 'app');
expect(updatedProject.targets.build).toStrictEqual({
syncGenerators: ['@foo/bar:baz', '@nx/js:typescript-sync'],
});
});
it('should keep the target with any other extra top-level option that would not be inferred', async () => {
const tree = createTreeWithEmptyWorkspace();
addProjectConfiguration(tree, 'app', {
root: 'apps/app',
sourceRoot: 'apps/app/src',
projectType: 'application',
targets: {
build: {
parallelism: false,
executor: '@angular-devkit/build-angular:browser',
options: {
outputPath: 'dist/apps/app/browser',
index: 'apps/app/src/index.html',
main: 'apps/app/src/main.ts',
tsConfig: 'apps/app/tsconfig.app.json',
},
},
},
});
writeJson(tree, 'apps/app/tsconfig.json', {});
await convertToRspack(tree, { project: 'app' });
const updatedProject = readProjectConfiguration(tree, 'app');
expect(updatedProject.targets.build).toStrictEqual({
parallelism: false,
});
});
});
describe('serve target', () => {
it('should remove the target when there are no relevant top-level options', async () => {
const tree = createTreeWithEmptyWorkspace();
addProjectConfiguration(tree, 'app', {
root: 'apps/app',
sourceRoot: 'apps/app/src',
projectType: 'application',
targets: {
build: {
executor: '@angular-devkit/build-angular:browser',
options: {
outputPath: 'dist/apps/app',
index: 'apps/app/src/index.html',
main: 'apps/app/src/main.ts',
tsConfig: 'apps/app/tsconfig.app.json',
},
},
serve: {
executor: '@angular-devkit/build-angular:dev-server',
options: {},
},
},
});
writeJson(tree, 'apps/app/tsconfig.json', {});
await convertToRspack(tree, { project: 'app' });
const updatedProject = readProjectConfiguration(tree, 'app');
expect(updatedProject.targets.serve).not.toBeDefined();
});
it('should remove the target when all the top-level options match what would be inferred', async () => {
const tree = createTreeWithEmptyWorkspace();
addProjectConfiguration(tree, 'app', {
root: 'apps/app',
sourceRoot: 'apps/app/src',
projectType: 'application',
targets: {
build: {
executor: '@angular-devkit/build-angular:browser',
options: {
outputPath: 'dist/apps/app',
index: 'apps/app/src/index.html',
main: 'apps/app/src/main.ts',
tsConfig: 'apps/app/tsconfig.app.json',
},
},
serve: {
continuous: true,
syncGenerators: ['@nx/js:typescript-sync'],
executor: '@angular-devkit/build-angular:dev-server',
options: {},
},
},
});
writeJson(tree, 'apps/app/tsconfig.json', {});
await convertToRspack(tree, { project: 'app' });
const updatedProject = readProjectConfiguration(tree, 'app');
expect(updatedProject.targets.serve).not.toBeDefined();
});
it('should remove the target when the syncGenerators option matches what would be inferred', async () => {
const tree = createTreeWithEmptyWorkspace();
addProjectConfiguration(tree, 'app', {
root: 'apps/app',
sourceRoot: 'apps/app/src',
projectType: 'application',
targets: {
build: {
executor: '@angular-devkit/build-angular:browser',
options: {
outputPath: 'dist/apps/app',
index: 'apps/app/src/index.html',
main: 'apps/app/src/main.ts',
tsConfig: 'apps/app/tsconfig.app.json',
},
},
serve: {
syncGenerators: ['@nx/js:typescript-sync'],
executor: '@angular-devkit/build-angular:dev-server',
options: {},
},
},
});
writeJson(tree, 'apps/app/tsconfig.json', {});
await convertToRspack(tree, { project: 'app' });
const updatedProject = readProjectConfiguration(tree, 'app');
expect(updatedProject.targets.serve).not.toBeDefined();
});
it('should keep the target with syncGenerators when they would not match what would be inferred', async () => {
const tree = createTreeWithEmptyWorkspace();
addProjectConfiguration(tree, 'app', {
root: 'apps/app',
sourceRoot: 'apps/app/src',
projectType: 'application',
targets: {
build: {
executor: '@angular-devkit/build-angular:browser',
options: {
outputPath: 'dist/apps/app/browser',
index: 'apps/app/src/index.html',
main: 'apps/app/src/main.ts',
tsConfig: 'apps/app/tsconfig.app.json',
},
},
serve: {
syncGenerators: ['@foo/bar:baz'],
executor: '@angular-devkit/build-angular:dev-server',
options: {},
},
},
});
writeJson(tree, 'apps/app/tsconfig.json', {});
await convertToRspack(tree, { project: 'app' });
const updatedProject = readProjectConfiguration(tree, 'app');
expect(updatedProject.targets.serve).toStrictEqual({
syncGenerators: ['@foo/bar:baz', '@nx/js:typescript-sync'],
});
});
it('should keep the target with any other extra top-level option that would not be inferred', async () => {
const tree = createTreeWithEmptyWorkspace();
addProjectConfiguration(tree, 'app', {
root: 'apps/app',
sourceRoot: 'apps/app/src',
projectType: 'application',
targets: {
build: {
executor: '@angular-devkit/build-angular:browser',
options: {
outputPath: 'dist/apps/app/browser',
index: 'apps/app/src/index.html',
main: 'apps/app/src/main.ts',
tsConfig: 'apps/app/tsconfig.app.json',
},
},
serve: {
parallelism: false,
executor: '@angular-devkit/build-angular:dev-server',
options: {},
},
},
});
writeJson(tree, 'apps/app/tsconfig.json', {});
await convertToRspack(tree, { project: 'app' });
const updatedProject = readProjectConfiguration(tree, 'app');
expect(updatedProject.targets.serve).toStrictEqual({
parallelism: false,
});
});
});
});
}); });

View File

@ -1,18 +1,27 @@
import { import {
type Tree,
readProjectConfiguration,
addDependenciesToPackageJson, addDependenciesToPackageJson,
formatFiles,
GeneratorCallback,
runTasksInSerial,
ensurePackage, ensurePackage,
formatFiles,
joinPathFragments,
normalizePath,
readJson,
readNxJson,
readProjectConfiguration,
runTasksInSerial,
updateProjectConfiguration, updateProjectConfiguration,
workspaceRoot, workspaceRoot,
joinPathFragments,
readJson,
writeJson, writeJson,
type ExpandedPluginConfiguration,
type GeneratorCallback,
type TargetConfiguration,
type Tree,
} from '@nx/devkit'; } from '@nx/devkit';
import type { ConvertToRspackSchema } from './schema'; import { forEachExecutorOptions } from '@nx/devkit/src/generators/executor-options-utils';
import { getNamedInputs } from '@nx/devkit/src/utils/get-named-inputs';
import type { RspackPluginOptions } from '@nx/rspack/plugins/plugin';
import { prompt } from 'enquirer';
import { relative, resolve } from 'path';
import { join } from 'path/posix';
import { import {
angularRspackVersion, angularRspackVersion,
nxVersion, nxVersion,
@ -23,11 +32,7 @@ import { createConfig } from './lib/create-config';
import { getCustomWebpackConfig } from './lib/get-custom-webpack-config'; import { getCustomWebpackConfig } from './lib/get-custom-webpack-config';
import { updateTsconfig } from './lib/update-tsconfig'; import { updateTsconfig } from './lib/update-tsconfig';
import { validateSupportedBuildExecutor } from './lib/validate-supported-executor'; import { validateSupportedBuildExecutor } from './lib/validate-supported-executor';
import { join } from 'path/posix'; import type { ConvertToRspackSchema } from './schema';
import { relative } from 'path';
import { existsSync } from 'fs';
import { forEachExecutorOptions } from '@nx/devkit/src/generators/executor-options-utils';
import { prompt } from 'enquirer';
const SUPPORTED_EXECUTORS = [ const SUPPORTED_EXECUTORS = [
'@angular-devkit/build-angular:browser', '@angular-devkit/build-angular:browser',
@ -242,11 +247,6 @@ function handleBuildTargetOptions(
delete options.customWebpackConfig; delete options.customWebpackConfig;
} }
if (options.outputs) {
// handled by the Rspack inference plugin
delete options.outputs;
}
for (const [key, value] of Object.entries(options)) { for (const [key, value] of Object.entries(options)) {
let optionName = key; let optionName = key;
let optionValue = let optionValue =
@ -348,8 +348,9 @@ export async function convertToRspack(
root: project.root, root: project.root,
}; };
const configurationOptions: Record<string, Record<string, any>> = {}; const configurationOptions: Record<string, Record<string, any>> = {};
const buildTargetNames: string[] = []; let buildTarget: { name: string; config: TargetConfiguration } | undefined;
const serveTargetNames: string[] = []; let serveTarget: { name: string; config: TargetConfiguration } | undefined;
const targetsToRemove: string[] = [];
let customWebpackConfigPath: string | undefined; let customWebpackConfigPath: string | undefined;
validateSupportedBuildExecutor(Object.values(project.targets)); validateSupportedBuildExecutor(Object.values(project.targets));
@ -380,7 +381,8 @@ export async function convertToRspack(
); );
} }
} }
buildTargetNames.push(targetName); buildTarget = { name: targetName, config: target };
targetsToRemove.push(targetName);
} else if ( } else if (
target.executor === '@angular-devkit/build-angular:server' || target.executor === '@angular-devkit/build-angular:server' ||
target.executor === '@nx/angular:webpack-server' target.executor === '@nx/angular:webpack-server'
@ -392,7 +394,7 @@ export async function convertToRspack(
project.root project.root
); );
createConfigOptions.server = './src/main.server.ts'; createConfigOptions.server = './src/main.server.ts';
buildTargetNames.push(targetName); targetsToRemove.push(targetName);
} else if ( } else if (
target.executor === '@angular-devkit/build-angular:dev-server' || target.executor === '@angular-devkit/build-angular:dev-server' ||
target.executor === '@nx/angular:dev-server' || target.executor === '@nx/angular:dev-server' ||
@ -407,7 +409,7 @@ export async function convertToRspack(
project.root project.root
); );
if (target.options.port !== DEFAULT_PORT) { if (target.options.port && target.options.port !== DEFAULT_PORT) {
projectServePort = target.options.port; projectServePort = target.options.port;
} }
} }
@ -425,7 +427,8 @@ export async function convertToRspack(
); );
} }
} }
serveTargetNames.push(targetName); serveTarget = { name: targetName, config: target };
targetsToRemove.push(targetName);
} else if (target.executor === '@angular-devkit/build-angular:prerender') { } else if (target.executor === '@angular-devkit/build-angular:prerender') {
if (target.options) { if (target.options) {
const prerenderOptions = { const prerenderOptions = {
@ -447,10 +450,10 @@ export async function convertToRspack(
} }
} }
} }
buildTargetNames.push(targetName); targetsToRemove.push(targetName);
} else if (target.executor === '@angular-devkit/build-angular:app-shell') { } else if (target.executor === '@angular-devkit/build-angular:app-shell') {
createConfigOptions.appShell = true; createConfigOptions.appShell = true;
buildTargetNames.push(targetName); targetsToRemove.push(targetName);
} }
} }
@ -467,28 +470,229 @@ export async function convertToRspack(
); );
updateTsconfig(tree, project.root); updateTsconfig(tree, project.root);
for (const targetName of [...buildTargetNames, ...serveTargetNames]) { for (const targetName of targetsToRemove) {
delete project.targets[targetName]; delete project.targets[targetName];
} }
if (projectServePort !== DEFAULT_PORT) {
project.targets.serve ??= {};
project.targets.serve.options ??= {};
project.targets.serve.options.port = projectServePort;
}
updateProjectConfiguration(tree, projectName, project); updateProjectConfiguration(tree, projectName, project);
// ensure plugin is registered
const { rspackInitGenerator } = ensurePackage<typeof import('@nx/rspack')>( const { rspackInitGenerator } = ensurePackage<typeof import('@nx/rspack')>(
'@nx/rspack', '@nx/rspack',
nxVersion nxVersion
); );
await rspackInitGenerator(tree, { await rspackInitGenerator(tree, {
addPlugin: true, addPlugin: true,
framework: 'angular', framework: 'angular',
}); });
// find the inferred target names
const nxJson = readNxJson(tree);
let inferredBuildTargetName = 'build';
let inferredServeTargetName = 'serve';
const pluginRegistration = nxJson.plugins.find(
(p): p is ExpandedPluginConfiguration<RspackPluginOptions> =>
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: TargetConfiguration['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 = getNamedInputs(project.root, {
nxJsonConfiguration: nxJson,
configFiles: [],
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 = joinPathFragments(
project.root,
createConfigOptions.outputPath.base
);
const shouldOverrideOutputs = (outputs: TargetConfiguration['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: string,
workspaceRoot: string,
projectRoot: string
) => {
const fullProjectRoot = resolve(workspaceRoot, projectRoot);
const fullPath = resolve(workspaceRoot, path);
const pathRelativeToProjectRoot = normalizePath(
relative(fullProjectRoot, fullPath)
);
if (pathRelativeToProjectRoot.startsWith('..')) {
return joinPathFragments(
'{workspaceRoot}',
relative(workspaceRoot, fullPath)
);
}
return 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, 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, 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(serveTarget.config).length) {
// there's extra target metadata left that wouldn't be inferred, we keep it
project.targets[inferredServeTargetName] = serveTarget.config;
}
}
updateProjectConfiguration(tree, projectName, project);
// This is needed to prevent a circular execution of the build target // This is needed to prevent a circular execution of the build target
const rootPkgJson = readJson(tree, 'package.json'); const rootPkgJson = readJson(tree, 'package.json');
if (rootPkgJson.scripts?.build === 'nx build') { if (rootPkgJson.scripts?.build === 'nx build') {