From 5ec27db6bce122d0bb1980be2e28eb679ccceca4 Mon Sep 17 00:00:00 2001 From: Filipe Silva Date: Thu, 4 Apr 2019 09:38:00 +0100 Subject: [PATCH] feat(@angular/cli): verify Angular version is supported --- package.json | 4 +- packages/angular/cli/commands/build-impl.ts | 4 -- packages/angular/cli/commands/serve-impl.ts | 4 -- .../angular/cli/models/architect-command.ts | 4 ++ packages/angular/cli/upgrade/version.ts | 38 ++++++++++++++++--- .../assets/1.0-project/src/app/app.module.ts | 2 - .../legacy-cli/e2e/tests/basic/update-1.0.ts | 2 +- .../e2e/tests/basic/update-1.7-longhand.ts | 2 +- .../legacy-cli/e2e/tests/basic/update-1.7.ts | 2 +- .../e2e/tests/misc/supported-angular.ts | 38 +++++++++++++++++++ yarn.lock | 8 ++-- 11 files changed, 84 insertions(+), 24 deletions(-) create mode 100644 tests/legacy-cli/e2e/tests/misc/supported-angular.ts diff --git a/package.json b/package.json index b9c74ac46a25..893335ecff3d 100644 --- a/package.json +++ b/package.json @@ -95,7 +95,7 @@ "@types/minimist": "^1.2.0", "@types/node": "10.9.4", "@types/request": "^2.47.1", - "@types/semver": "^5.5.0", + "@types/semver": "^6.0.0", "@types/source-map": "0.5.2", "@types/webpack": "^4.4.11", "@types/webpack-dev-server": "^3.1.0", @@ -122,7 +122,7 @@ "pidtree": "^0.3.0", "pidusage": "^2.0.17", "rxjs": "~6.4.0", - "semver": "^5.3.0", + "semver": "6.0.0", "source-map": "^0.5.6", "source-map-support": "^0.5.0", "spdx-satisfies": "^4.0.0", diff --git a/packages/angular/cli/commands/build-impl.ts b/packages/angular/cli/commands/build-impl.ts index fe1fde26fd46..081588c2b9cb 100644 --- a/packages/angular/cli/commands/build-impl.ts +++ b/packages/angular/cli/commands/build-impl.ts @@ -8,16 +8,12 @@ import { analytics } from '@angular-devkit/core'; import { ArchitectCommand, ArchitectCommandOptions } from '../models/architect-command'; import { Arguments } from '../models/interface'; -import { Version } from '../upgrade/version'; import { Schema as BuildCommandSchema } from './build'; export class BuildCommand extends ArchitectCommand { public readonly target = 'build'; public async run(options: ArchitectCommandOptions & Arguments) { - // Check Angular version. - Version.assertCompatibleAngularVersion(this.workspace.root); - return this.runArchitectTarget(options); } diff --git a/packages/angular/cli/commands/serve-impl.ts b/packages/angular/cli/commands/serve-impl.ts index 22e104b1010f..dff1855e6e7a 100644 --- a/packages/angular/cli/commands/serve-impl.ts +++ b/packages/angular/cli/commands/serve-impl.ts @@ -8,7 +8,6 @@ import { analytics } from '@angular-devkit/core'; import { ArchitectCommand, ArchitectCommandOptions } from '../models/architect-command'; import { Arguments } from '../models/interface'; -import { Version } from '../upgrade/version'; import { Schema as BuildCommandSchema } from './build'; import { Schema as ServeCommandSchema } from './serve'; @@ -16,9 +15,6 @@ export class ServeCommand extends ArchitectCommand { public readonly target = 'serve'; public validate(_options: ArchitectCommandOptions & Arguments) { - // Check Angular versions. - Version.assertCompatibleAngularVersion(this.workspace.root); - return true; } diff --git a/packages/angular/cli/models/architect-command.ts b/packages/angular/cli/models/architect-command.ts index ee1df7dc085e..5871bd0ccb69 100644 --- a/packages/angular/cli/models/architect-command.ts +++ b/packages/angular/cli/models/architect-command.ts @@ -9,6 +9,7 @@ import { Architect, Target } from '@angular-devkit/architect'; import { WorkspaceNodeModulesArchitectHost } from '@angular-devkit/architect/node'; import { experimental, json, schema, tags } from '@angular-devkit/core'; import { NodeJsSyncHost } from '@angular-devkit/core/node'; +import { Version } from '../upgrade/version'; import { BepJsonWriter } from '../utilities/bep'; import { parseJsonSchemaToOptions } from '../utilities/json-schema'; import { isPackageNameSafeForAnalytics } from './analytics'; @@ -274,6 +275,9 @@ export abstract class ArchitectCommand< protected async runArchitectTarget( options: ArchitectCommandOptions & Arguments, ): Promise { + // Check Angular version. + Version.assertCompatibleAngularVersion(this.workspace.root); + const extra = options['--'] || []; try { diff --git a/packages/angular/cli/upgrade/version.ts b/packages/angular/cli/upgrade/version.ts index 53670738ebe6..774678663b99 100644 --- a/packages/angular/cli/upgrade/version.ts +++ b/packages/angular/cli/upgrade/version.ts @@ -8,7 +8,7 @@ import { tags, terminal } from '@angular-devkit/core'; import * as path from 'path'; -import { SemVer } from 'semver'; +import { SemVer, satisfies } from 'semver'; export class Version { @@ -26,6 +26,12 @@ export class Version { isGreaterThanOrEqualTo(other: SemVer) { return this._semver !== null && this._semver.compare(other) >= 0; } + satisfies(other: string) { + // This comparison includes pre-releases (like betas and rcs), and considers them to be + // before the release proper. + // e.g. '9.0.0-beta.1' will satisfy '>=7.0.0 <9.0.0', but '9.0.0' will not. + return this._semver !== null && satisfies(this._semver, other, { includePrerelease: true }); + } get major() { return this._semver ? this._semver.major : 0; } get minor() { return this._semver ? this._semver.minor : 0; } @@ -36,11 +42,12 @@ export class Version { toString() { return this._version; } static assertCompatibleAngularVersion(projectRoot: string) { + let angularCliPkgJson; let angularPkgJson; let rxjsPkgJson; + const resolveOptions = { paths: [projectRoot] }; try { - const resolveOptions = { paths: [projectRoot] }; const angularPackagePath = require.resolve('@angular/core/package.json', resolveOptions); const rxjsPackagePath = require.resolve('rxjs/package.json', resolveOptions); @@ -61,6 +68,26 @@ export class Version { process.exit(2); } + try { + const angularCliPkgPath = require.resolve('@angular/cli/package.json', resolveOptions); + angularCliPkgJson = require(angularCliPkgPath); + if (!(angularCliPkgJson && angularCliPkgJson['version'])) { + throw new Error(); + } + } catch (error) { + console.error(terminal.bold(terminal.red(tags.stripIndents` + Cannot determine versions of "@angular/cli". + This likely means your local installation is broken. Please reinstall your packages. + `))); + process.exit(2); + } + + const cliMajor = new Version(angularCliPkgJson['version']).major; + // e.g. CLI 8.0 supports '>=8.0.0 <9.0.0', including pre-releases (betas, rcs, snapshots) + // of both 8 and 9. + const supportedAngularSemver = `^${cliMajor}.0.0-beta || ` + + `>=${cliMajor}.0.0 <${cliMajor + 1}.0.0`; + const angularVersion = new Version(angularPkgJson['version']); const rxjsVersion = new Version(rxjsPkgJson['version']); @@ -70,9 +97,10 @@ export class Version { return; } - if (!angularVersion.isGreaterThanOrEqualTo(new SemVer('5.0.0'))) { + if (!angularVersion.satisfies(supportedAngularSemver)) { console.error(terminal.bold(terminal.red(tags.stripIndents` - This version of CLI is only compatible with Angular version 5.0.0 or higher. + This version of CLI is only compatible with Angular versions ${supportedAngularSemver}, + but Angular version ${angularVersion} was found instead. Please visit the link below to find instructions on how to update Angular. https://angular-update-guide.firebaseapp.com/ @@ -84,7 +112,7 @@ export class Version { && !rxjsVersion.isGreaterThanOrEqualTo(new SemVer('6.0.0-beta.0')) ) { console.error(terminal.bold(terminal.red(tags.stripIndents` - This project uses version ${rxjsVersion} of RxJs, which is not supported by Angular v6. + This project uses version ${rxjsVersion} of RxJs, which is not supported by Angular v6+. The official RxJs version that is supported is 5.6.0-forward-compat.0 and greater. Please visit the link below to find instructions on how to update RxJs. diff --git a/tests/legacy-cli/e2e/assets/1.0-project/src/app/app.module.ts b/tests/legacy-cli/e2e/assets/1.0-project/src/app/app.module.ts index 67ae49119baa..d39d998af88f 100644 --- a/tests/legacy-cli/e2e/assets/1.0-project/src/app/app.module.ts +++ b/tests/legacy-cli/e2e/assets/1.0-project/src/app/app.module.ts @@ -1,7 +1,6 @@ import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { FormsModule } from '@angular/forms'; -import { HttpModule } from '@angular/http'; import { AppComponent } from './app.component'; @@ -12,7 +11,6 @@ import { AppComponent } from './app.component'; imports: [ BrowserModule, FormsModule, - HttpModule ], providers: [], bootstrap: [AppComponent] diff --git a/tests/legacy-cli/e2e/tests/basic/update-1.0.ts b/tests/legacy-cli/e2e/tests/basic/update-1.0.ts index e5a670c427b4..71342bca898f 100644 --- a/tests/legacy-cli/e2e/tests/basic/update-1.0.ts +++ b/tests/legacy-cli/e2e/tests/basic/update-1.0.ts @@ -7,7 +7,7 @@ import { expectToFail } from '../../utils/utils'; export default async function () { - const extraUpdateArgs = await isPrereleaseCli() ? ['--next'] : []; + const extraUpdateArgs = await isPrereleaseCli() ? ['--next', '--force'] : []; await createProjectFromAsset('1.0-project'); await useCIChrome('.'); diff --git a/tests/legacy-cli/e2e/tests/basic/update-1.7-longhand.ts b/tests/legacy-cli/e2e/tests/basic/update-1.7-longhand.ts index 397d126c6b38..2ca3a22d9745 100644 --- a/tests/legacy-cli/e2e/tests/basic/update-1.7-longhand.ts +++ b/tests/legacy-cli/e2e/tests/basic/update-1.7-longhand.ts @@ -5,7 +5,7 @@ import { expectToFail } from '../../utils/utils'; export default async function () { - const extraUpdateArgs = await isPrereleaseCli() ? ['--next'] : []; + const extraUpdateArgs = await isPrereleaseCli() ? ['--next', '--force'] : []; await createProjectFromAsset('1.7-project'); await expectToFail(() => ng('build')); diff --git a/tests/legacy-cli/e2e/tests/basic/update-1.7.ts b/tests/legacy-cli/e2e/tests/basic/update-1.7.ts index 977e2badf074..6abafddee0fd 100644 --- a/tests/legacy-cli/e2e/tests/basic/update-1.7.ts +++ b/tests/legacy-cli/e2e/tests/basic/update-1.7.ts @@ -7,7 +7,7 @@ import { expectToFail } from '../../utils/utils'; export default async function () { - const extraUpdateArgs = await isPrereleaseCli() ? ['--next'] : []; + const extraUpdateArgs = await isPrereleaseCli() ? ['--next', '--force'] : []; await createProjectFromAsset('1.7-project'); await useCIChrome('.'); diff --git a/tests/legacy-cli/e2e/tests/misc/supported-angular.ts b/tests/legacy-cli/e2e/tests/misc/supported-angular.ts new file mode 100644 index 000000000000..58cd5fad4872 --- /dev/null +++ b/tests/legacy-cli/e2e/tests/misc/supported-angular.ts @@ -0,0 +1,38 @@ +import { SemVer } from 'semver'; +import { getGlobalVariable } from '../../utils/env'; +import { readFile, writeFile } from '../../utils/fs'; +import { ng } from '../../utils/process'; +import { expectToFail } from '../../utils/utils'; + + +export default async function () { + if (getGlobalVariable('argv')['ng-snapshots'] || getGlobalVariable('argv')['ivy']) { + // Don't run this test in snapshots or Ivy test jobs. + // The snapshots job won't work correctly because it doesn't use semver for Angular, and the + // Ivy job will fail because Ivy wasn't stable in 7. + return; + } + + const angularCliPkgJson = JSON.parse(await readFile('node_modules/@angular/cli/package.json')); + const cliMajor = new SemVer(angularCliPkgJson.version as string).major; + const angularCorePkgPath = 'node_modules/@angular/core/package.json'; + const originalAngularCorePkgJson = await readFile(angularCorePkgPath); + + // Fake version by writing over the @angular/core version, since that's what the CLI checks. + const fakeCoreVersion = async (newMajor: number) => { + const tmpPkgJson = JSON.parse(originalAngularCorePkgJson); + tmpPkgJson.version = `${newMajor}.0.0`; + await writeFile(angularCorePkgPath, JSON.stringify(tmpPkgJson)); + }; + + // Major should succeed, but we don't need to test it here since it's tested everywhere else. + // Major+1 and -1 should fail architect commands, but allow other commands. + await fakeCoreVersion(cliMajor + 1); + await expectToFail(() => ng('build'), 'Should fail Major+1'); + await ng('version'); + await fakeCoreVersion(cliMajor - 1); + await ng('version'); + + // Restore the original core package.json. + await writeFile(angularCorePkgPath, originalAngularCorePkgJson); +} diff --git a/yarn.lock b/yarn.lock index 1c692123e003..483eb9ae4e17 100644 --- a/yarn.lock +++ b/yarn.lock @@ -548,10 +548,10 @@ resolved "https://registry.yarnpkg.com/@types/selenium-webdriver/-/selenium-webdriver-3.0.12.tgz#6affe5aed1ba379175075a889adbe2bc3aa62159" integrity sha512-hYn+eoOehVUIdMwp5h34ZsGAO1ydja10GDup4BwyoFCdcH5MQ35nQq+AInSaBMEMopD5hEooFCyKo2Pajbe1ag== -"@types/semver@^5.5.0": - version "5.5.0" - resolved "https://registry.yarnpkg.com/@types/semver/-/semver-5.5.0.tgz#146c2a29ee7d3bae4bf2fcb274636e264c813c45" - integrity sha512-41qEJgBH/TWgo5NFSvBCJ1qkoi3Q6ONSF2avrHq1LVEZfYpdHmj0y9SuTK+u9ZhG1sYQKBL1AWXKyLWP4RaUoQ== +"@types/semver@^6.0.0": + version "6.0.0" + resolved "https://registry.yarnpkg.com/@types/semver/-/semver-6.0.0.tgz#86ba89f02a414e39c68d02b351872e4ed31bd773" + integrity sha512-OO0srjOGH99a4LUN2its3+r6CBYcplhJ466yLqs+zvAWgphCpS8hYZEZ797tRDP/QKcqTdb/YCN6ifASoAWkrQ== "@types/serve-static@*": version "1.13.2"