diff --git a/docs/generated/packages/angular/generators/setup-ssr.json b/docs/generated/packages/angular/generators/setup-ssr.json index d4a94b5a1225b..60469d53069cf 100644 --- a/docs/generated/packages/angular/generators/setup-ssr.json +++ b/docs/generated/packages/angular/generators/setup-ssr.json @@ -61,6 +61,12 @@ "type": "boolean", "description": "Skip formatting the workspace after the generator completes.", "x-priority": "internal" + }, + "skipPackageJson": { + "type": "boolean", + "default": false, + "description": "Do not add dependencies to `package.json`.", + "x-priority": "internal" } }, "required": ["project"], diff --git a/packages/angular/src/generators/add-linting/add-linting.spec.ts b/packages/angular/src/generators/add-linting/add-linting.spec.ts index 1ddcf0ee0e05d..a2ad38741af00 100644 --- a/packages/angular/src/generators/add-linting/add-linting.spec.ts +++ b/packages/angular/src/generators/add-linting/add-linting.spec.ts @@ -3,6 +3,7 @@ import { Tree, addProjectConfiguration, readJson, + updateJson, } from '@nx/devkit'; import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; import * as linter from '@nx/eslint'; @@ -64,4 +65,26 @@ describe('addLinting generator', () => { const eslintConfig = readJson(tree, `${appProjectRoot}/.eslintrc.json`); expect(eslintConfig).toMatchSnapshot(); }); + + it('should not touch the package.json when run with `--skipPackageJson`', async () => { + let initialPackageJson; + updateJson(tree, 'package.json', (json) => { + json.dependencies = {}; + json.devDependencies = {}; + initialPackageJson = json; + + return json; + }); + + await addLintingGenerator(tree, { + prefix: 'myOrg', + projectName: appProjectName, + projectRoot: appProjectRoot, + skipFormat: true, + skipPackageJson: true, + }); + + const packageJson = readJson(tree, 'package.json'); + expect(packageJson).toEqual(initialPackageJson); + }); }); diff --git a/packages/angular/src/generators/add-linting/add-linting.ts b/packages/angular/src/generators/add-linting/add-linting.ts index 8fe088b4e99f4..2448d6f81805b 100755 --- a/packages/angular/src/generators/add-linting/add-linting.ts +++ b/packages/angular/src/generators/add-linting/add-linting.ts @@ -38,6 +38,7 @@ export async function addLintingGenerator( rootProject: rootProject, addPlugin: false, addExplicitTargets: true, + skipPackageJson: options.skipPackageJson, }); tasks.push(lintTask); diff --git a/packages/angular/src/generators/application/application.spec.ts b/packages/angular/src/generators/application/application.spec.ts index 0e58f6287fccf..f9a00cb4a3ace 100644 --- a/packages/angular/src/generators/application/application.spec.ts +++ b/packages/angular/src/generators/application/application.spec.ts @@ -87,6 +87,22 @@ describe('app', () => { expect(tsConfig).toMatchSnapshot(); }); + it('should not touch the package.json when run with `--skipPackageJson`', async () => { + let initialPackageJson; + updateJson(appTree, 'package.json', (json) => { + json.dependencies = {}; + json.devDependencies = {}; + initialPackageJson = json; + + return json; + }); + + await generateApp(appTree, 'my-app', { skipPackageJson: true }); + + const packageJson = readJson(appTree, 'package.json'); + expect(packageJson).toEqual(initialPackageJson); + }); + describe('not nested', () => { it('should create project configs', async () => { // ACT @@ -1182,58 +1198,19 @@ describe('app', () => { })); }); - it('should add angular dependencies', async () => { - // ACT + it('should add angular peer dependencies when not installed', async () => { await generateApp(appTree, 'my-app'); - // ASSERT - const { dependencies, devDependencies } = readJson( - appTree, - 'package.json' - ); - - expect(dependencies['@angular/animations']).toEqual( - backwardCompatibleVersions.angularV15.angularVersion - ); - expect(dependencies['@angular/common']).toEqual( - backwardCompatibleVersions.angularV15.angularVersion - ); - expect(dependencies['@angular/compiler']).toEqual( - backwardCompatibleVersions.angularV15.angularVersion - ); - expect(dependencies['@angular/core']).toEqual( - backwardCompatibleVersions.angularV15.angularVersion - ); - expect(dependencies['@angular/platform-browser']).toEqual( - backwardCompatibleVersions.angularV15.angularVersion - ); - expect(dependencies['@angular/platform-browser-dynamic']).toEqual( - backwardCompatibleVersions.angularV15.angularVersion - ); - expect(dependencies['@angular/router']).toEqual( - backwardCompatibleVersions.angularV15.angularVersion - ); - expect(dependencies['rxjs']).toEqual( - backwardCompatibleVersions.angularV15.rxjsVersion - ); - expect(dependencies['zone.js']).toEqual( - backwardCompatibleVersions.angularV15.zoneJsVersion - ); - expect(devDependencies['@angular/cli']).toEqual( + const { devDependencies } = readJson(appTree, 'package.json'); + expect(devDependencies['@angular-devkit/build-angular']).toEqual( backwardCompatibleVersions.angularV15.angularDevkitVersion ); - expect(devDependencies['@angular/compiler-cli']).toEqual( + expect(devDependencies['@angular-devkit/schematics']).toEqual( backwardCompatibleVersions.angularV15.angularDevkitVersion ); - expect(devDependencies['@angular/language-service']).toEqual( - backwardCompatibleVersions.angularV15.angularVersion - ); - expect(devDependencies['@angular-devkit/build-angular']).toEqual( + expect(devDependencies['@schematics/angular']).toEqual( backwardCompatibleVersions.angularV15.angularDevkitVersion ); - - // codelyzer should no longer be there by default - expect(devDependencies['codelyzer']).toBeUndefined(); }); it('should import "ApplicationConfig" from "@angular/platform-browser"', async () => { diff --git a/packages/angular/src/generators/application/application.ts b/packages/angular/src/generators/application/application.ts index c79fee0b29851..ce318a46cb1d3 100644 --- a/packages/angular/src/generators/application/application.ts +++ b/packages/angular/src/generators/application/application.ts @@ -55,7 +55,10 @@ export async function applicationGeneratorInternal( ...options, skipFormat: true, }); - ensureAngularDependencies(tree); + + if (!options.skipPackageJson) { + ensureAngularDependencies(tree); + } createProject(tree, options); @@ -95,6 +98,7 @@ export async function applicationGeneratorInternal( await setupSsr(tree, { project: options.name, standalone: options.standalone, + skipPackageJson: options.skipPackageJson, }); } diff --git a/packages/angular/src/generators/application/lib/add-e2e.ts b/packages/angular/src/generators/application/lib/add-e2e.ts index 390a7e30068d7..08fa9b2863df0 100644 --- a/packages/angular/src/generators/application/lib/add-e2e.ts +++ b/packages/angular/src/generators/application/lib/add-e2e.ts @@ -79,7 +79,9 @@ function addFileServerTarget( options: NormalizedSchema, targetName: string ) { - addDependenciesToPackageJson(tree, {}, { '@nx/web': nxVersion }); + if (!options.skipPackageJson) { + addDependenciesToPackageJson(tree, {}, { '@nx/web': nxVersion }); + } const { major: angularMajorVersion } = getInstalledAngularVersionInfo(tree); const isUsingApplicationBuilder = diff --git a/packages/angular/src/generators/host/host.spec.ts b/packages/angular/src/generators/host/host.spec.ts index 429eec388733e..183b9c260d01c 100644 --- a/packages/angular/src/generators/host/host.spec.ts +++ b/packages/angular/src/generators/host/host.spec.ts @@ -1,4 +1,4 @@ -import { updateJson } from '@nx/devkit'; +import { readJson, updateJson } from '@nx/devkit'; import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; import { getProjects, @@ -608,4 +608,26 @@ describe('Host App Generator', () => { ).toContain(`'remote1','foo-remote2','foo-remote3'`); }); }); + + it('should not touch the package.json when run with `--skipPackageJson`', async () => { + const tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' }); + let initialPackageJson; + updateJson(tree, 'package.json', (json) => { + json.dependencies = {}; + json.devDependencies = {}; + initialPackageJson = json; + + return json; + }); + + await generateTestHostApplication(tree, { + name: 'test', + ssr: true, + skipFormat: true, + skipPackageJson: true, + }); + + const packageJson = readJson(tree, 'package.json'); + expect(packageJson).toEqual(initialPackageJson); + }); }); diff --git a/packages/angular/src/generators/host/lib/update-ssr-setup.ts b/packages/angular/src/generators/host/lib/update-ssr-setup.ts index 68635e822d9fe..8b2e35f5e3dd3 100644 --- a/packages/angular/src/generators/host/lib/update-ssr-setup.ts +++ b/packages/angular/src/generators/host/lib/update-ssr-setup.ts @@ -76,16 +76,18 @@ export async function updateSsrSetup( updateProjectConfiguration(tree, appName, project); - const installTask = addDependenciesToPackageJson( - tree, - { - cors: corsVersion, - '@module-federation/node': moduleFederationNodeVersion, - }, - { - '@types/cors': typesCorsVersion, - } - ); + if (!options.skipPackageJson) { + return addDependenciesToPackageJson( + tree, + { + cors: corsVersion, + '@module-federation/node': moduleFederationNodeVersion, + }, + { + '@types/cors': typesCorsVersion, + } + ); + } - return installTask; + return () => {}; } diff --git a/packages/angular/src/generators/library/library.spec.ts b/packages/angular/src/generators/library/library.spec.ts index 00b57d2af5549..19dd1eb31eca1 100644 --- a/packages/angular/src/generators/library/library.spec.ts +++ b/packages/angular/src/generators/library/library.spec.ts @@ -94,6 +94,22 @@ describe('lib', () => { expect(devDependencies['codelyzer']).toBeUndefined(); }); + it('should not touch the package.json when run with `--skipPackageJson`', async () => { + let initialPackageJson; + updateJson(tree, 'package.json', (json) => { + json.dependencies = {}; + json.devDependencies = {}; + initialPackageJson = json; + + return json; + }); + + await runLibraryGeneratorWithOpts({ skipPackageJson: true }); + + const packageJson = readJson(tree, 'package.json'); + expect(packageJson).toEqual(initialPackageJson); + }); + describe('not nested', () => { it('should update ng-package.json', async () => { // ACT diff --git a/packages/angular/src/generators/library/library.ts b/packages/angular/src/generators/library/library.ts index 87769e830db0b..8c11d21980cda 100644 --- a/packages/angular/src/generators/library/library.ts +++ b/packages/angular/src/generators/library/library.ts @@ -1,4 +1,5 @@ import { + addDependenciesToPackageJson, formatFiles, GeneratorCallback, installPackagesTask, @@ -10,10 +11,7 @@ import { addTsConfigPath, initGenerator as jsInitGenerator } from '@nx/js'; import init from '../../generators/init/init'; import addLintingGenerator from '../add-linting/add-linting'; import setupTailwindGenerator from '../setup-tailwind/setup-tailwind'; -import { - addDependenciesToPackageJsonIfDontExist, - versions, -} from '../utils/version-utils'; +import { versions } from '../utils/version-utils'; import { addBuildableLibrariesPostCssDependencies } from '../utils/dependencies'; import { addModule } from './lib/add-module'; import { addStandaloneComponent } from './lib/add-standalone-component'; @@ -77,7 +75,10 @@ export async function libraryGeneratorInternal( skipFormat: true, }); await init(tree, { ...libraryOptions, skipFormat: true }); - ensureAngularDependencies(tree); + + if (!libraryOptions.skipPackageJson) { + ensureAngularDependencies(tree); + } const project = addProject(tree, libraryOptions); @@ -104,13 +105,18 @@ export async function libraryGeneratorInternal( }); } - if (libraryOptions.buildable || libraryOptions.publishable) { - addDependenciesToPackageJsonIfDontExist( + if ( + (libraryOptions.buildable || libraryOptions.publishable) && + !libraryOptions.skipPackageJson + ) { + addDependenciesToPackageJson( tree, {}, { 'ng-packagr': pkgVersions.ngPackagrVersion, - } + }, + undefined, + true ); addBuildableLibrariesPostCssDependencies(tree); } diff --git a/packages/angular/src/generators/remote/lib/update-ssr-setup.ts b/packages/angular/src/generators/remote/lib/update-ssr-setup.ts index fa21f80d6d7b8..f02d7322a6432 100644 --- a/packages/angular/src/generators/remote/lib/update-ssr-setup.ts +++ b/packages/angular/src/generators/remote/lib/update-ssr-setup.ts @@ -21,11 +21,13 @@ export async function updateSsrSetup( port, standalone, typescriptConfiguration, + skipPackageJson, }: { appName: string; port: number; standalone: boolean; typescriptConfiguration: boolean; + skipPackageJson?: boolean; } ) { let project = readProjectConfiguration(tree, appName); @@ -116,16 +118,18 @@ export async function updateSsrSetup( updateProjectConfiguration(tree, appName, project); - const installTask = addDependenciesToPackageJson( - tree, - { - cors: corsVersion, - '@module-federation/node': moduleFederationNodeVersion, - }, - { - '@types/cors': typesCorsVersion, - } - ); + if (!skipPackageJson) { + return addDependenciesToPackageJson( + tree, + { + cors: corsVersion, + '@module-federation/node': moduleFederationNodeVersion, + }, + { + '@types/cors': typesCorsVersion, + } + ); + } - return installTask; + return () => {}; } diff --git a/packages/angular/src/generators/remote/remote.spec.ts b/packages/angular/src/generators/remote/remote.spec.ts index 3a1c01883784e..a60faeb6501ab 100644 --- a/packages/angular/src/generators/remote/remote.spec.ts +++ b/packages/angular/src/generators/remote/remote.spec.ts @@ -455,4 +455,27 @@ describe('MF Remote App Generator', () => { ); }); }); + + it('should not touch the package.json when run with `--skipPackageJson`', async () => { + const tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' }); + let initialPackageJson; + updateJson(tree, 'package.json', (json) => { + json.dependencies = {}; + json.devDependencies = {}; + initialPackageJson = json; + + return json; + }); + + await generateTestRemoteApplication(tree, { + name: 'test', + port: 4201, + ssr: true, + skipFormat: true, + skipPackageJson: true, + }); + + const packageJson = readJson(tree, 'package.json'); + expect(packageJson).toEqual(initialPackageJson); + }); }); diff --git a/packages/angular/src/generators/remote/remote.ts b/packages/angular/src/generators/remote/remote.ts index 598682aa9b91b..5eaf73879ecd4 100644 --- a/packages/angular/src/generators/remote/remote.ts +++ b/packages/angular/src/generators/remote/remote.ts @@ -72,21 +72,25 @@ export async function remoteInternal(tree: Tree, schema: Schema) { setParserOptionsProject: options.setParserOptionsProject, }); - const installSwcHelpersTask = addDependenciesToPackageJson( - tree, - {}, - { - '@swc/helpers': swcHelpersVersion, - } - ); + const installTasks = [appInstallTask]; + if (!options.skipPackageJson) { + const installSwcHelpersTask = addDependenciesToPackageJson( + tree, + {}, + { + '@swc/helpers': swcHelpersVersion, + } + ); + installTasks.push(installSwcHelpersTask); + } - let installTasks = [appInstallTask, installSwcHelpersTask]; if (options.ssr) { let ssrInstallTask = await updateSsrSetup(tree, { appName: remoteProjectName, port, typescriptConfiguration, standalone: options.standalone, + skipPackageJson: options.skipPackageJson, }); installTasks.push(ssrInstallTask); } diff --git a/packages/angular/src/generators/setup-mf/setup-mf.spec.ts b/packages/angular/src/generators/setup-mf/setup-mf.spec.ts index 9d7d24ef8f6da..8513828a9f182 100644 --- a/packages/angular/src/generators/setup-mf/setup-mf.spec.ts +++ b/packages/angular/src/generators/setup-mf/setup-mf.spec.ts @@ -1,4 +1,9 @@ -import { readJson, readProjectConfiguration, Tree } from '@nx/devkit'; +import { + readJson, + readProjectConfiguration, + Tree, + updateJson, +} from '@nx/devkit'; import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; import { generateTestApplication } from '../utils/testing'; import { setupMf } from './setup-mf'; @@ -678,4 +683,25 @@ describe('Init MF', () => { }); expect(tree.read('app1/src/app/app.routes.ts', 'utf-8')).toMatchSnapshot(); }); + + it('should not touch the package.json when run with `--skipPackageJson`', async () => { + let initialPackageJson; + updateJson(tree, 'package.json', (json) => { + json.dependencies = {}; + json.devDependencies = {}; + initialPackageJson = json; + + return json; + }); + + await setupMf(tree, { + appName: 'app1', + mfType: 'host', + skipFormat: true, + skipPackageJson: true, + }); + + const packageJson = readJson(tree, 'package.json'); + expect(packageJson).toEqual(initialPackageJson); + }); }); diff --git a/packages/angular/src/generators/setup-mf/setup-mf.ts b/packages/angular/src/generators/setup-mf/setup-mf.ts index 5d79e4450fe98..bf3f547c27b4e 100644 --- a/packages/angular/src/generators/setup-mf/setup-mf.ts +++ b/packages/angular/src/generators/setup-mf/setup-mf.ts @@ -39,11 +39,13 @@ export async function setupMf(tree: Tree, rawOptions: Schema) { addRemoteEntry(tree, options, projectConfig.root); removeDeadCodeFromRemote(tree, options); setupTspathForRemote(tree, options); - installTask = addDependenciesToPackageJson( - tree, - {}, - { '@nx/web': nxVersion, '@nx/webpack': nxVersion } - ); + if (!options.skipPackageJson) { + installTask = addDependenciesToPackageJson( + tree, + {}, + { '@nx/web': nxVersion, '@nx/webpack': nxVersion } + ); + } } const remotesWithPorts = getRemotesWithPorts(tree, options); @@ -67,11 +69,13 @@ export async function setupMf(tree: Tree, rawOptions: Schema) { port, }); } - installTask = addDependenciesToPackageJson( - tree, - {}, - { '@nx/webpack': nxVersion } - ); + if (!options.skipPackageJson) { + installTask = addDependenciesToPackageJson( + tree, + {}, + { '@nx/webpack': nxVersion } + ); + } } if (!options.skipE2E) { diff --git a/packages/angular/src/generators/setup-ssr/schema.d.ts b/packages/angular/src/generators/setup-ssr/schema.d.ts index af319e5a9e917..5b16df69d5080 100644 --- a/packages/angular/src/generators/setup-ssr/schema.d.ts +++ b/packages/angular/src/generators/setup-ssr/schema.d.ts @@ -9,4 +9,5 @@ export interface Schema { standalone?: boolean; hydration?: boolean; skipFormat?: boolean; + skipPackageJson?: boolean; } diff --git a/packages/angular/src/generators/setup-ssr/schema.json b/packages/angular/src/generators/setup-ssr/schema.json index 48650bf32b4da..294874ed65cef 100644 --- a/packages/angular/src/generators/setup-ssr/schema.json +++ b/packages/angular/src/generators/setup-ssr/schema.json @@ -61,6 +61,12 @@ "type": "boolean", "description": "Skip formatting the workspace after the generator completes.", "x-priority": "internal" + }, + "skipPackageJson": { + "type": "boolean", + "default": false, + "description": "Do not add dependencies to `package.json`.", + "x-priority": "internal" } }, "required": ["project"], diff --git a/packages/angular/src/generators/setup-ssr/setup-ssr.spec.ts b/packages/angular/src/generators/setup-ssr/setup-ssr.spec.ts index 81bbca4b8a4ce..a49a6433afb66 100644 --- a/packages/angular/src/generators/setup-ssr/setup-ssr.spec.ts +++ b/packages/angular/src/generators/setup-ssr/setup-ssr.spec.ts @@ -380,6 +380,28 @@ describe('setupSSR', () => { expect(devDependencies['@nguniversal/builders']).toBeUndefined(); }); + it('should not touch the package.json when run with `--skipPackageJson`', async () => { + const tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' }); + await generateTestApplication(tree, { name: 'app1', skipFormat: true }); + let initialPackageJson; + updateJson(tree, 'package.json', (json) => { + json.dependencies = {}; + json.devDependencies = {}; + initialPackageJson = json; + + return json; + }); + + await setupSsr(tree, { + project: 'app1', + skipFormat: true, + skipPackageJson: true, + }); + + const packageJson = readJson(tree, 'package.json'); + expect(packageJson).toEqual(initialPackageJson); + }); + it('should add hydration correctly for NgModule apps', async () => { // ARRANGE const tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' }); diff --git a/packages/angular/src/generators/setup-ssr/setup-ssr.ts b/packages/angular/src/generators/setup-ssr/setup-ssr.ts index 7de23cbd33815..14916e371d2b9 100644 --- a/packages/angular/src/generators/setup-ssr/setup-ssr.ts +++ b/packages/angular/src/generators/setup-ssr/setup-ssr.ts @@ -30,7 +30,9 @@ export async function setupSsr(tree: Tree, schema: Schema) { targets.build.executor === '@angular-devkit/build-angular:application' || targets.build.executor === '@nx/angular:application'; - addDependencies(tree, isUsingApplicationBuilder); + if (!schema.skipPackageJson) { + addDependencies(tree, isUsingApplicationBuilder); + } generateSSRFiles(tree, options, isUsingApplicationBuilder); if (!options.standalone) { diff --git a/packages/angular/src/generators/utils/add-jest.ts b/packages/angular/src/generators/utils/add-jest.ts index 5261bdf375e03..68eb5b219b202 100644 --- a/packages/angular/src/generators/utils/add-jest.ts +++ b/packages/angular/src/generators/utils/add-jest.ts @@ -1,6 +1,10 @@ -import { ensurePackage, joinPathFragments, type Tree } from '@nx/devkit'; +import { + addDependenciesToPackageJson, + ensurePackage, + joinPathFragments, + type Tree, +} from '@nx/devkit'; import { jestPresetAngularVersion, nxVersion } from '../../utils/versions'; -import { addDependenciesToPackageJsonIfDontExist } from './version-utils'; export type AddJestOptions = { name: string; @@ -16,10 +20,12 @@ export async function addJest( if (!options.skipPackageJson) { process.env.npm_config_legacy_peer_deps ??= 'true'; - addDependenciesToPackageJsonIfDontExist( + addDependenciesToPackageJson( tree, {}, - { 'jest-preset-angular': jestPresetAngularVersion } + { 'jest-preset-angular': jestPresetAngularVersion }, + undefined, + true ); } diff --git a/packages/angular/src/generators/utils/ensure-angular-dependencies.spec.ts b/packages/angular/src/generators/utils/ensure-angular-dependencies.spec.ts index eda935c73dae2..88ef971efc5c6 100644 --- a/packages/angular/src/generators/utils/ensure-angular-dependencies.spec.ts +++ b/packages/angular/src/generators/utils/ensure-angular-dependencies.spec.ts @@ -10,7 +10,7 @@ describe('ensureAngularDependencies', () => { tree = createTreeWithEmptyWorkspace(); }); - it('should add angular dependencies', () => { + it('should add angular dependencies when not installed', () => { // ACT ensureAngularDependencies(tree); @@ -35,12 +35,13 @@ describe('ensureAngularDependencies', () => { expect(devDependencies['@angular-devkit/build-angular']).toBe( angularDevkitVersion ); - - // codelyzer should no longer be there by default - expect(devDependencies['codelyzer']).toBeUndefined(); + expect(devDependencies['@angular-devkit/schematics']).toBe( + angularDevkitVersion + ); + expect(devDependencies['@schematics/angular']).toBe(angularDevkitVersion); }); - it('should add angular dependencies respecting base packages versions', () => { + it('should add peer dependencies respecting the @angular/devkit installed version', () => { // ARRANGE updateJson(tree, 'package.json', (json) => ({ ...json, @@ -58,22 +59,10 @@ describe('ensureAngularDependencies', () => { ensureAngularDependencies(tree); // ASSERT - const { dependencies, devDependencies } = readJson(tree, 'package.json'); - - expect(dependencies['@angular/animations']).toBe('~15.0.0'); - expect(dependencies['@angular/common']).toBe('~15.0.0'); - expect(dependencies['@angular/compiler']).toBe('~15.0.0'); - expect(dependencies['@angular/core']).toBe('~15.0.0'); - expect(dependencies['@angular/platform-browser']).toBe('~15.0.0'); - expect(dependencies['@angular/platform-browser-dynamic']).toBe('~15.0.0'); - expect(dependencies['@angular/router']).toBe('~15.0.0'); - expect(dependencies['rxjs']).toBeDefined(); - expect(dependencies['tslib']).toBeDefined(); - expect(dependencies['zone.js']).toBeDefined(); - expect(devDependencies['@angular/cli']).toBe('~15.0.0'); - expect(devDependencies['@angular/compiler-cli']).toBe('~15.0.0'); - expect(devDependencies['@angular/language-service']).toBe('~15.0.0'); + const { devDependencies } = readJson(tree, 'package.json'); expect(devDependencies['@angular-devkit/build-angular']).toBe('~15.0.0'); + expect(devDependencies['@angular-devkit/schematics']).toBe('~15.0.0'); + expect(devDependencies['@schematics/angular']).toBe('~15.0.0'); }); it('should not overwrite already installed dependencies', () => { @@ -85,15 +74,48 @@ describe('ensureAngularDependencies', () => { '@angular/animations': '~15.0.1', '@angular/core': '~15.0.0', }, + devDependencies: { + ...json.devDependencies, + '@angular-devkit/build-angular': '~15.0.1', + }, })); // ACT ensureAngularDependencies(tree); // ASSERT - const { dependencies } = readJson(tree, 'package.json'); + const { dependencies, devDependencies } = readJson(tree, 'package.json'); expect(dependencies['@angular/animations']).toBe('~15.0.1'); expect(dependencies['@angular/core']).toBe('~15.0.0'); + expect(devDependencies['@angular-devkit/build-angular']).toBe('~15.0.1'); + }); + + it('should not add extra runtime dependencies when `@angular/core` is already installed', () => { + // ARRANGE + updateJson(tree, 'package.json', (json) => ({ + ...json, + dependencies: { + ...json.dependencies, + '@angular/core': '~15.0.0', + }, + })); + + // ACT + ensureAngularDependencies(tree); + + // ASSERT + const { dependencies, devDependencies } = readJson(tree, 'package.json'); + + expect(dependencies['@angular/core']).toBe('~15.0.0'); + expect(dependencies['@angular/animations']).toBeUndefined(); + expect(dependencies['@angular/common']).toBeUndefined(); + expect(dependencies['@angular/compiler']).toBeUndefined(); + expect(dependencies['@angular/platform-browser']).toBeUndefined(); + expect(dependencies['@angular/platform-browser-dynamic']).toBeUndefined(); + expect(dependencies['@angular/router']).toBeUndefined(); + expect(dependencies['rxjs']).toBeUndefined(); + expect(dependencies['tslib']).toBeUndefined(); + expect(dependencies['zone.js']).toBeUndefined(); }); }); diff --git a/packages/angular/src/generators/utils/ensure-angular-dependencies.ts b/packages/angular/src/generators/utils/ensure-angular-dependencies.ts index d966e9f81bf92..9b24c8649c894 100644 --- a/packages/angular/src/generators/utils/ensure-angular-dependencies.ts +++ b/packages/angular/src/generators/utils/ensure-angular-dependencies.ts @@ -1,48 +1,74 @@ -import type { GeneratorCallback, Tree } from '@nx/devkit'; import { - addDependenciesToPackageJsonIfDontExist, - getInstalledPackageVersion, - versions, -} from './version-utils'; + addDependenciesToPackageJson, + type GeneratorCallback, + type Tree, +} from '@nx/devkit'; +import { getInstalledPackageVersion, versions } from './version-utils'; export function ensureAngularDependencies(tree: Tree): GeneratorCallback { + const dependencies: Record = {}; + const devDependencies: Record = {}; const pkgVersions = versions(tree); - const angularVersion = - getInstalledPackageVersion(tree, '@angular/core') ?? - pkgVersions.angularVersion; + const installedAngularCoreVersion = getInstalledPackageVersion( + tree, + '@angular/core' + ); + if (!installedAngularCoreVersion) { + /** + * If @angular/core is already installed, we assume the workspace was already + * initialized with Angular dependencies and we don't want to re-install them. + * This is to avoid re-installing Angular runtime dependencies that the user + * might have removed. + */ + const angularVersion = pkgVersions.angularVersion; + const rxjsVersion = + getInstalledPackageVersion(tree, 'rxjs') ?? pkgVersions.rxjsVersion; + const tsLibVersion = + getInstalledPackageVersion(tree, 'tslib') ?? pkgVersions.tsLibVersion; + const zoneJsVersion = + getInstalledPackageVersion(tree, 'zone.js') ?? pkgVersions.zoneJsVersion; + + dependencies['@angular/animations'] = angularVersion; + dependencies['@angular/common'] = angularVersion; + dependencies['@angular/compiler'] = angularVersion; + dependencies['@angular/core'] = angularVersion; + dependencies['@angular/forms'] = angularVersion; + dependencies['@angular/platform-browser'] = angularVersion; + dependencies['@angular/platform-browser-dynamic'] = angularVersion; + dependencies['@angular/router'] = angularVersion; + dependencies.rxjs = rxjsVersion; + dependencies.tslib = tsLibVersion; + dependencies['zone.js'] = zoneJsVersion; + } + + const installedAngularDevkitVersion = getInstalledPackageVersion( + tree, + '@angular-devkit/build-angular' + ); + if (!installedAngularDevkitVersion) { + /** + * If `@angular-devkit/build-angular` is already installed, we assume the workspace + * was already initialized with Angular and we don't want to re-install the tooling. + * This is to avoid re-installing Angular tooling that the user might have removed. + */ + devDependencies['@angular/cli'] = pkgVersions.angularDevkitVersion; + devDependencies['@angular/compiler-cli'] = pkgVersions.angularVersion; + devDependencies['@angular/language-service'] = pkgVersions.angularVersion; + } + + // Ensure the `@nx/angular` peer dependencies are always installed. const angularDevkitVersion = - getInstalledPackageVersion(tree, '@angular-devkit/build-angular') ?? - pkgVersions.angularDevkitVersion; - const rxjsVersion = - getInstalledPackageVersion(tree, 'rxjs') ?? pkgVersions.rxjsVersion; - const tsLibVersion = - getInstalledPackageVersion(tree, 'tslib') ?? pkgVersions.tsLibVersion; - const zoneJsVersion = - getInstalledPackageVersion(tree, 'zone.js') ?? pkgVersions.zoneJsVersion; + installedAngularDevkitVersion ?? pkgVersions.angularDevkitVersion; + devDependencies['@angular-devkit/build-angular'] = angularDevkitVersion; + devDependencies['@angular-devkit/schematics'] = angularDevkitVersion; + devDependencies['@schematics/angular'] = angularDevkitVersion; - return addDependenciesToPackageJsonIfDontExist( + return addDependenciesToPackageJson( tree, - { - '@angular/animations': angularVersion, - '@angular/common': angularVersion, - '@angular/compiler': angularVersion, - '@angular/core': angularVersion, - '@angular/forms': angularVersion, - '@angular/platform-browser': angularVersion, - '@angular/platform-browser-dynamic': angularVersion, - '@angular/router': angularVersion, - rxjs: rxjsVersion, - tslib: tsLibVersion, - 'zone.js': zoneJsVersion, - }, - { - '@angular/cli': angularDevkitVersion, - '@angular/compiler-cli': angularVersion, - '@angular/language-service': angularVersion, - '@angular-devkit/build-angular': angularDevkitVersion, - '@angular-devkit/schematics': angularDevkitVersion, - '@schematics/angular': angularDevkitVersion, - } + dependencies, + devDependencies, + undefined, + true ); } diff --git a/packages/angular/src/generators/utils/testing.ts b/packages/angular/src/generators/utils/testing.ts index 5210bc24c4474..a081c0d571ac0 100644 --- a/packages/angular/src/generators/utils/testing.ts +++ b/packages/angular/src/generators/utils/testing.ts @@ -1,9 +1,8 @@ import type { Tree } from '@nx/devkit'; -import { names, readProjectConfiguration, updateJson } from '@nx/devkit'; +import { names, readProjectConfiguration } from '@nx/devkit'; import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; import { Linter } from '@nx/eslint'; import { UnitTestRunner } from '../../utils/test-runners'; -import { angularDevkitVersion } from '../../utils/versions'; import { applicationGenerator } from '../application/application'; import type { Schema as ApplicationOptions } from '../application/schema'; import { componentGenerator } from '../component/component'; @@ -18,7 +17,6 @@ export async function generateTestApplication( tree: Tree, options: ApplicationOptions ): Promise { - addAngularPluginPeerDeps(tree); tree.write('.gitignore', ''); await applicationGenerator(tree, { projectNameAndRootFormat: 'as-provided', @@ -30,7 +28,6 @@ export async function generateTestHostApplication( tree: Tree, options: HostOptions ): Promise { - addAngularPluginPeerDeps(tree); tree.write('.gitignore', ''); await host(tree, { projectNameAndRootFormat: 'as-provided', ...options }); } @@ -39,7 +36,6 @@ export async function generateTestRemoteApplication( tree: Tree, options: RemoteOptions ): Promise { - addAngularPluginPeerDeps(tree); tree.write('.gitignore', ''); await remote(tree, { projectNameAndRootFormat: 'as-provided', ...options }); } @@ -48,7 +44,6 @@ export async function generateTestLibrary( tree: Tree, options: LibraryOptions ): Promise { - addAngularPluginPeerDeps(tree); tree.write('.gitignore', ''); await libraryGenerator(tree, { projectNameAndRootFormat: 'as-provided', @@ -60,7 +55,6 @@ export async function createStorybookTestWorkspaceForLib( libName: string ): Promise { let tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' }); - addAngularPluginPeerDeps(tree); tree.write('.gitignore', ''); await libraryGenerator(tree, { @@ -310,18 +304,6 @@ export class StaticMemberDeclarationsModule { return tree; } -function addAngularPluginPeerDeps(tree: Tree): void { - updateJson(tree, 'package.json', (json) => ({ - ...json, - devDependencies: { - ...json.devDependencies, - '@angular-devkit/core': angularDevkitVersion, - '@angular-devkit/schematics': angularDevkitVersion, - '@schematics/angular': angularDevkitVersion, - }, - })); -} - function generateModule( tree: Tree, options: { name: string; project: string; path?: string } diff --git a/packages/angular/src/generators/utils/version-utils.ts b/packages/angular/src/generators/utils/version-utils.ts index 442d8674d1ccc..45b120640be7a 100644 --- a/packages/angular/src/generators/utils/version-utils.ts +++ b/packages/angular/src/generators/utils/version-utils.ts @@ -1,5 +1,4 @@ -import type { GeneratorCallback, Tree } from '@nx/devkit'; -import { addDependenciesToPackageJson, readJson } from '@nx/devkit'; +import { readJson, type Tree } from '@nx/devkit'; import { clean, coerce, major } from 'semver'; import { backwardCompatibleVersions, @@ -56,36 +55,6 @@ export function getInstalledPackageVersionInfo(tree: Tree, pkgName: string) { return version ? { major: major(coerce(version)), version } : null; } -export function addDependenciesToPackageJsonIfDontExist( - tree: Tree, - dependencies: Record, - devDependencies: Record, - packageJsonPath: string = 'package.json' -): GeneratorCallback { - const packageJson = readJson(tree, packageJsonPath); - - function filterExisting( - deps: Record - ): Record { - return Object.keys(deps) - .filter( - (d) => - !packageJson.dependencies?.[d] && !packageJson.devDependencies?.[d] - ) - .reduce((acc, d) => ({ ...acc, [d]: deps[d] }), {}); - } - - const depsToAdd = filterExisting(dependencies); - const devDepsToAdd = filterExisting(devDependencies); - - return addDependenciesToPackageJson( - tree, - depsToAdd, - devDepsToAdd, - packageJsonPath - ); -} - export function versions( tree: Tree ): PackageLatestVersions | PackageCompatVersions {