diff --git a/lib/packages.ts b/lib/packages.ts index 76befa1d010d..1417354a9b01 100644 --- a/lib/packages.ts +++ b/lib/packages.ts @@ -84,7 +84,7 @@ function loadPackageJson(p: string) { case 'engines': pkg['engines'] = { 'node': '>= 10.13.0', - 'npm': '^6.11.0', + 'npm': '^6.11.0 || ^7.5.6', 'yarn': '>= 1.13.0', }; break; diff --git a/packages/angular/cli/commands/new-impl.ts b/packages/angular/cli/commands/new-impl.ts index b13f42b81be9..032923138992 100644 --- a/packages/angular/cli/commands/new-impl.ts +++ b/packages/angular/cli/commands/new-impl.ts @@ -24,8 +24,6 @@ export class NewCommand extends SchematicCommand { } public async run(options: NewCommandSchema & Arguments) { - await ensureCompatibleNpm(this.workspace.root); - // Register the version of the CLI in the registry. const packageJson = require('../package.json'); const version = packageJson.version; diff --git a/packages/angular/cli/models/schematic-command.ts b/packages/angular/cli/models/schematic-command.ts index e2c7803cb968..b19f95b479cf 100644 --- a/packages/angular/cli/models/schematic-command.ts +++ b/packages/angular/cli/models/schematic-command.ts @@ -40,7 +40,7 @@ import { getWorkspaceRaw, } from '../utilities/config'; import { parseJsonSchemaToOptions } from '../utilities/json-schema'; -import { getPackageManager } from '../utilities/package-manager'; +import { ensureCompatibleNpm, getPackageManager } from '../utilities/package-manager'; import { isTTY } from '../utilities/tty'; import { isPackageNameSafeForAnalytics } from './analytics'; import { BaseCommandOptions, Command } from './command'; @@ -534,6 +534,16 @@ export abstract class SchematicCommand< } }); + // Temporary compatibility check for NPM 7 + if (collectionName === '@schematics/angular' && schematicName === 'ng-new') { + if ( + !input.skipInstall && + (input.packageManager === undefined || input.packageManager === 'npm') + ) { + await ensureCompatibleNpm(this.workspace.root); + } + } + return new Promise(resolve => { workflow .execute({ diff --git a/packages/angular/cli/utilities/package-manager.ts b/packages/angular/cli/utilities/package-manager.ts index 3789c56ea23b..8c53bdf97d20 100644 --- a/packages/angular/cli/utilities/package-manager.ts +++ b/packages/angular/cli/utilities/package-manager.ts @@ -8,6 +8,7 @@ import { execSync } from 'child_process'; import { existsSync } from 'fs'; import { join } from 'path'; +import { satisfies, valid } from 'semver'; import { PackageManager } from '../lib/config/schema'; import { getConfiguredPackageManager } from './config'; @@ -56,7 +57,7 @@ export async function getPackageManager(root: string): Promise { } /** - * Checks if the npm version is version 6.x. If not, display a message and exit. + * Checks if the npm version is a supported 7.x version. If not, display a warning. */ export async function ensureCompatibleNpm(root: string): Promise { if ((await getPackageManager(root)) !== PackageManager.Npm) { @@ -64,19 +65,19 @@ export async function ensureCompatibleNpm(root: string): Promise { } try { - const version = execSync('npm --version', {encoding: 'utf8', stdio: 'pipe'}).trim(); - const major = Number(version.match(/^(\d+)\./)?.[1]); - if (major === 6) { + const versionText = execSync('npm --version', {encoding: 'utf8', stdio: 'pipe'}).trim(); + const version = valid(versionText); + if (!version) { return; } - // tslint:disable-next-line: no-console - console.error( - `npm version ${version} detected.\n` + - 'The Angular CLI currently requires npm version 6.\n\n' + - 'Please install a compatible version to proceed (`npm install --global npm@6`).\n', - ); - process.exit(3); + if (satisfies(version, '>=7 <7.5.6')) { + // tslint:disable-next-line: no-console + console.warn( + `npm version ${version} detected.` + + ' When using npm 7 with the Angular CLI, npm version 7.5.6 or higher is recommended.', + ); + } } catch { // npm is not installed } diff --git a/tests/legacy-cli/e2e/tests/misc/npm-7.ts b/tests/legacy-cli/e2e/tests/misc/npm-7.ts index 38c3b4f8e76d..6a2a2c9d7eb2 100644 --- a/tests/legacy-cli/e2e/tests/misc/npm-7.ts +++ b/tests/legacy-cli/e2e/tests/misc/npm-7.ts @@ -1,7 +1,8 @@ +import { rimraf } from '../../utils/fs'; import { ng, npm } from '../../utils/process'; import { expectToFail } from '../../utils/utils'; -const errorText = 'The Angular CLI currently requires npm version 6.'; +const warningText = 'npm version 7.5.6 or higher is recommended'; export default async function() { // Windows CI fails with permission errors when trying to replace npm @@ -11,29 +12,66 @@ export default async function() { const currentDirectory = process.cwd(); try { - // Install version 7.x - await npm('install', '--global', 'npm@7'); + // Install version >=7.5.6 + await npm('install', '--global', 'npm@>=7.5.6'); - // Ensure `ng add` exits and shows npm error + // Ensure `ng update` does not show npm warning + const { stderr: stderrUpdate1 } = await ng('update'); + if (stderrUpdate1.includes(warningText)) { + throw new Error('ng update expected to not show npm version warning.'); + } + + // Install version <7.5.6 + await npm('install', '--global', 'npm@7.4.0'); + + // Ensure `ng add` shows npm warning const { message: stderrAdd } = await expectToFail(() => ng('add')); - if (!stderrAdd.includes(errorText)) { - throw new Error('ng add expected to show npm version error.'); + if (!stderrAdd.includes(warningText)) { + throw new Error('ng add expected to show npm version warning.'); } - // Ensure `ng update` exits and shows npm error - const { message: stderrUpdate } = await expectToFail(() => ng('update')); - if (!stderrUpdate.includes(errorText)) { - throw new Error('ng update expected to show npm version error.'); + // Ensure `ng update` shows npm warning + const { stderr: stderrUpdate2 } = await ng('update'); + if (!stderrUpdate2.includes(warningText)) { + throw new Error('ng update expected to show npm version warning.'); } - // Ensure `ng new` exits and shows npm error + // Ensure `ng build` executes successfully + const { stderr: stderrBuild } = await ng('build'); + if (stderrBuild.includes(warningText)) { + throw new Error('ng build expected to not show npm version warning.'); + } + + // Ensure `ng new` shows npm warning // Must be outside the project for `ng new` process.chdir('..'); const { message: stderrNew } = await expectToFail(() => ng('new')); - if (!stderrNew.includes(errorText)) { - throw new Error('ng new expected to show npm version error.'); + if (!stderrNew.includes(warningText)) { + throw new Error('ng new expected to show npm version warning.'); + } + + // Ensure `ng new --package-manager=npm` shows npm warning + const { message: stderrNewNpm } = await expectToFail(() => ng('new', '--package-manager=npm')); + if (!stderrNewNpm.includes(warningText)) { + throw new Error('ng new expected to show npm version warning.'); + } + + // Ensure `ng new --skip-install` executes successfully + const { stderr: stderrNewSkipInstall } = await ng('new', 'npm-seven-skip', '--skip-install'); + if (stderrNewSkipInstall.includes(warningText)) { + throw new Error('ng new --skip-install expected to not show npm version warning.'); + } + + // Ensure `ng new --package-manager=yarn` executes successfully + const { stderr: stderrNewYarn } = await ng('new', 'npm-seven-yarn', '--package-manager=yarn'); + if (stderrNewYarn.includes(warningText)) { + throw new Error('ng new --package-manager=yarn expected to not show npm version warning.'); } } finally { + // Cleanup extra test projects + await rimraf('npm-seven-skip'); + await rimraf('npm-seven-yarn'); + // Change directory back process.chdir(currentDirectory);