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 {
type Tree,
readProjectConfiguration,
addDependenciesToPackageJson,
formatFiles,
GeneratorCallback,
runTasksInSerial,
ensurePackage,
formatFiles,
joinPathFragments,
normalizePath,
readJson,
readNxJson,
readProjectConfiguration,
runTasksInSerial,
updateProjectConfiguration,
workspaceRoot,
joinPathFragments,
readJson,
writeJson,
type ExpandedPluginConfiguration,
type GeneratorCallback,
type TargetConfiguration,
type Tree,
} 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 {
angularRspackVersion,
nxVersion,
@ -23,11 +32,7 @@ import { createConfig } from './lib/create-config';
import { getCustomWebpackConfig } from './lib/get-custom-webpack-config';
import { updateTsconfig } from './lib/update-tsconfig';
import { validateSupportedBuildExecutor } from './lib/validate-supported-executor';
import { join } from 'path/posix';
import { relative } from 'path';
import { existsSync } from 'fs';
import { forEachExecutorOptions } from '@nx/devkit/src/generators/executor-options-utils';
import { prompt } from 'enquirer';
import type { ConvertToRspackSchema } from './schema';
const SUPPORTED_EXECUTORS = [
'@angular-devkit/build-angular:browser',
@ -242,11 +247,6 @@ function handleBuildTargetOptions(
delete options.customWebpackConfig;
}
if (options.outputs) {
// handled by the Rspack inference plugin
delete options.outputs;
}
for (const [key, value] of Object.entries(options)) {
let optionName = key;
let optionValue =
@ -348,8 +348,9 @@ export async function convertToRspack(
root: project.root,
};
const configurationOptions: Record<string, Record<string, any>> = {};
const buildTargetNames: string[] = [];
const serveTargetNames: string[] = [];
let buildTarget: { name: string; config: TargetConfiguration } | undefined;
let serveTarget: { name: string; config: TargetConfiguration } | undefined;
const targetsToRemove: string[] = [];
let customWebpackConfigPath: string | undefined;
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 (
target.executor === '@angular-devkit/build-angular:server' ||
target.executor === '@nx/angular:webpack-server'
@ -392,7 +394,7 @@ export async function convertToRspack(
project.root
);
createConfigOptions.server = './src/main.server.ts';
buildTargetNames.push(targetName);
targetsToRemove.push(targetName);
} else if (
target.executor === '@angular-devkit/build-angular:dev-server' ||
target.executor === '@nx/angular:dev-server' ||
@ -407,7 +409,7 @@ export async function convertToRspack(
project.root
);
if (target.options.port !== DEFAULT_PORT) {
if (target.options.port && target.options.port !== DEFAULT_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') {
if (target.options) {
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') {
createConfigOptions.appShell = true;
buildTargetNames.push(targetName);
targetsToRemove.push(targetName);
}
}
@ -467,28 +470,229 @@ export async function convertToRspack(
);
updateTsconfig(tree, project.root);
for (const targetName of [...buildTargetNames, ...serveTargetNames]) {
for (const targetName of targetsToRemove) {
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);
// ensure plugin is registered
const { rspackInitGenerator } = ensurePackage<typeof import('@nx/rspack')>(
'@nx/rspack',
nxVersion
);
await rspackInitGenerator(tree, {
addPlugin: true,
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
const rootPkgJson = readJson(tree, 'package.json');
if (rootPkgJson.scripts?.build === 'nx build') {