From 03c4296c1b2984338e382a0b002657be1bc53773 Mon Sep 17 00:00:00 2001 From: Eaton Date: Thu, 23 Aug 2018 16:54:32 -0400 Subject: [PATCH 0001/1703] docs: fix broken links to angular.io (#11811) docs: fix broken links to angular.io --- packages/schematics/angular/application/files/src/polyfills.ts | 2 +- .../build_angular/hello-world-app/src/polyfills.ts | 2 +- tests/angular_devkit/build_webpack/angular-app/src/polyfills.ts | 2 +- tests/legacy-cli/e2e/assets/1.0-project/src/polyfills.ts | 2 +- tests/legacy-cli/e2e/assets/1.7-project/src/polyfills.ts | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/schematics/angular/application/files/src/polyfills.ts b/packages/schematics/angular/application/files/src/polyfills.ts index d310405a6817..8fceb45d3b0a 100644 --- a/packages/schematics/angular/application/files/src/polyfills.ts +++ b/packages/schematics/angular/application/files/src/polyfills.ts @@ -11,7 +11,7 @@ * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. * - * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html + * Learn more in https://angular.io/guide/browser-support */ /*************************************************************************************************** diff --git a/tests/angular_devkit/build_angular/hello-world-app/src/polyfills.ts b/tests/angular_devkit/build_angular/hello-world-app/src/polyfills.ts index 1e5ba9f031b6..d7fd9a7ba789 100644 --- a/tests/angular_devkit/build_angular/hello-world-app/src/polyfills.ts +++ b/tests/angular_devkit/build_angular/hello-world-app/src/polyfills.ts @@ -18,7 +18,7 @@ * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. * - * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html + * Learn more in https://angular.io/guide/browser-support */ /*************************************************************************************************** diff --git a/tests/angular_devkit/build_webpack/angular-app/src/polyfills.ts b/tests/angular_devkit/build_webpack/angular-app/src/polyfills.ts index 1e5ba9f031b6..d7fd9a7ba789 100644 --- a/tests/angular_devkit/build_webpack/angular-app/src/polyfills.ts +++ b/tests/angular_devkit/build_webpack/angular-app/src/polyfills.ts @@ -18,7 +18,7 @@ * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. * - * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html + * Learn more in https://angular.io/guide/browser-support */ /*************************************************************************************************** diff --git a/tests/legacy-cli/e2e/assets/1.0-project/src/polyfills.ts b/tests/legacy-cli/e2e/assets/1.0-project/src/polyfills.ts index 53bdaf1b8642..a8f13561140d 100644 --- a/tests/legacy-cli/e2e/assets/1.0-project/src/polyfills.ts +++ b/tests/legacy-cli/e2e/assets/1.0-project/src/polyfills.ts @@ -11,7 +11,7 @@ * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. * - * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html + * Learn more in https://angular.io/guide/browser-support */ /*************************************************************************************************** diff --git a/tests/legacy-cli/e2e/assets/1.7-project/src/polyfills.ts b/tests/legacy-cli/e2e/assets/1.7-project/src/polyfills.ts index af84770782bb..e17a0b1ea437 100644 --- a/tests/legacy-cli/e2e/assets/1.7-project/src/polyfills.ts +++ b/tests/legacy-cli/e2e/assets/1.7-project/src/polyfills.ts @@ -11,7 +11,7 @@ * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. * - * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html + * Learn more in https://angular.io/guide/browser-support */ /*************************************************************************************************** From 0535e2b7389f594a68905c7acfd23e477207c51e Mon Sep 17 00:00:00 2001 From: Yi Qi Date: Thu, 23 Aug 2018 15:02:15 -0700 Subject: [PATCH 0002/1703] feat(@angular/cli): allow schematic command to specify the default colletion. --- .../angular/cli/commands/generate-impl.ts | 3 +- packages/angular/cli/commands/new-impl.ts | 4 +-- .../angular/cli/models/schematic-command.ts | 35 +++++++++++++++++-- packages/angular/cli/utilities/config.ts | 32 +---------------- 4 files changed, 36 insertions(+), 38 deletions(-) diff --git a/packages/angular/cli/commands/generate-impl.ts b/packages/angular/cli/commands/generate-impl.ts index 812e5fe2ed24..204bee6d48db 100644 --- a/packages/angular/cli/commands/generate-impl.ts +++ b/packages/angular/cli/commands/generate-impl.ts @@ -9,7 +9,6 @@ // tslint:disable:no-global-tslint-disable no-any import { tags, terminal } from '@angular-devkit/core'; import { SchematicCommand } from '../models/schematic-command'; -import { getDefaultSchematicCollection } from '../utilities/config'; export class GenerateCommand extends SchematicCommand { @@ -61,7 +60,7 @@ export class GenerateCommand extends SchematicCommand { } private parseSchematicInfo(options: any) { - let collectionName = getDefaultSchematicCollection(); + let collectionName = this.getDefaultSchematicCollection(); let schematicName: string = options.schematic; diff --git a/packages/angular/cli/commands/new-impl.ts b/packages/angular/cli/commands/new-impl.ts index 0057fdcfbef5..b8663199c342 100644 --- a/packages/angular/cli/commands/new-impl.ts +++ b/packages/angular/cli/commands/new-impl.ts @@ -8,8 +8,6 @@ // tslint:disable:no-global-tslint-disable no-any import { SchematicCommand } from '../models/schematic-command'; -import { getDefaultSchematicCollection } from '../utilities/config'; - export class NewCommand extends SchematicCommand { public readonly allowMissingWorkspace = true; @@ -65,7 +63,7 @@ export class NewCommand extends SchematicCommand { } private parseCollectionName(options: any): string { - const collectionName = options.collection || options.c || getDefaultSchematicCollection(); + const collectionName = options.collection || options.c || this.getDefaultSchematicCollection(); return collectionName; } diff --git a/packages/angular/cli/models/schematic-command.ts b/packages/angular/cli/models/schematic-command.ts index 81594ac651d2..9f78d5b450ed 100644 --- a/packages/angular/cli/models/schematic-command.ts +++ b/packages/angular/cli/models/schematic-command.ts @@ -40,9 +40,10 @@ import { import { take } from 'rxjs/operators'; import { WorkspaceLoader } from '../models/workspace-loader'; import { - getDefaultSchematicCollection, getPackageManager, + getProjectByCwd, getSchematicDefaults, + getWorkspace, } from '../utilities/config'; import { ArgumentStrategy, Command, CommandContext, Option } from './command'; @@ -188,6 +189,36 @@ export abstract class SchematicCommand extends Command { return this._workflow; } + protected getDefaultSchematicCollection(): string { + let workspace = getWorkspace('local'); + + if (workspace) { + const project = getProjectByCwd(workspace); + if (project && workspace.getProjectCli(project)) { + const value = workspace.getProjectCli(project)['defaultCollection']; + if (typeof value == 'string') { + return value; + } + } + if (workspace.getCli()) { + const value = workspace.getCli()['defaultCollection']; + if (typeof value == 'string') { + return value; + } + } + } + + workspace = getWorkspace('global'); + if (workspace && workspace.getCli()) { + const value = workspace.getCli()['defaultCollection']; + if (typeof value == 'string') { + return value; + } + } + + return '@schematics/angular'; + } + protected runSchematic(options: RunSchematicOptions) { const {collectionName, schematicName, debug, dryRun} = options; let schematicOptions = this.removeCoreOptions(options.schematicOptions); @@ -347,7 +378,7 @@ export abstract class SchematicCommand extends Command { // Make a copy. this._originalOptions = [...this.options]; - const collectionName = options.collectionName || getDefaultSchematicCollection(); + const collectionName = options.collectionName || this.getDefaultSchematicCollection(); const collection = this.getCollection(collectionName); diff --git a/packages/angular/cli/utilities/config.ts b/packages/angular/cli/utilities/config.ts index d9fdb26e9aaf..abad429d7b1f 100644 --- a/packages/angular/cli/utilities/config.ts +++ b/packages/angular/cli/utilities/config.ts @@ -140,7 +140,7 @@ export function validateWorkspace(json: JsonObject) { return true; } -function getProjectByCwd(workspace: experimental.workspace.Workspace): string | null { +export function getProjectByCwd(workspace: experimental.workspace.Workspace): string | null { try { return workspace.getProjectByPath(normalize(process.cwd())); } catch (e) { @@ -265,36 +265,6 @@ function getLegacyPackageManager(): string | null { return null; } -export function getDefaultSchematicCollection(): string { - let workspace = getWorkspace('local'); - - if (workspace) { - const project = getProjectByCwd(workspace); - if (project && workspace.getProjectCli(project)) { - const value = workspace.getProjectCli(project)['defaultCollection']; - if (typeof value == 'string') { - return value; - } - } - if (workspace.getCli()) { - const value = workspace.getCli()['defaultCollection']; - if (typeof value == 'string') { - return value; - } - } - } - - workspace = getWorkspace('global'); - if (workspace && workspace.getCli()) { - const value = workspace.getCli()['defaultCollection']; - if (typeof value == 'string') { - return value; - } - } - - return '@schematics/angular'; -} - export function getSchematicDefaults( collection: string, schematic: string, From c66f831a078736ce01f47d50bb7b2f74dc6ebfed Mon Sep 17 00:00:00 2001 From: ajspera Date: Mon, 27 Aug 2018 11:24:26 -0600 Subject: [PATCH 0003/1703] fix(@angular-devkit/build-angular): load style source maps inline so they work (#11729) without this, the way styles are into the DOM breaks the default sourcemap option postcss-loader option docs https://github.com/postcss/postcss-loader#inline fixes #9099 --- .../src/angular-cli-files/models/webpack-configs/styles.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/styles.ts b/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/styles.ts index 09a267b2b173..b8d067df5163 100644 --- a/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/styles.ts +++ b/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/styles.ts @@ -258,7 +258,7 @@ export function getStylesConfig(wco: WebpackConfigOptions) { options: { ident: 'embedded', plugins: postcssPluginCreator, - sourceMap: cssSourceMap + sourceMap: cssSourceMap ? 'inline' : false } }, ...(use as webpack.Loader[]) @@ -281,7 +281,7 @@ export function getStylesConfig(wco: WebpackConfigOptions) { options: { ident: buildOptions.extractCss ? 'extracted' : 'embedded', plugins: postcssPluginCreator, - sourceMap: cssSourceMap + sourceMap: cssSourceMap && !buildOptions.extractCss ? 'inline' : cssSourceMap } }, ...(use as webpack.Loader[]) From 907fedc0b8bebc227e7f70b3740babcaa4678d4a Mon Sep 17 00:00:00 2001 From: Filipe Silva Date: Mon, 27 Aug 2018 18:26:30 +0100 Subject: [PATCH 0004/1703] feat(@angular-devkit/build-angular): use terser instead of uglify-es (#11996) Should help bring down prod build times, @SanderElias reported a ~60% reduction on his project. Should fix #9340. --- .../angular_devkit/build_angular/package.json | 2 +- .../models/webpack-configs/common.ts | 8 +- yarn.lock | 107 +++++++++++++++++- 3 files changed, 110 insertions(+), 7 deletions(-) diff --git a/packages/angular_devkit/build_angular/package.json b/packages/angular_devkit/build_angular/package.json index 558574ab3afd..35bffdbf66fa 100644 --- a/packages/angular_devkit/build_angular/package.json +++ b/packages/angular_devkit/build_angular/package.json @@ -46,7 +46,7 @@ "stylus": "^0.54.5", "stylus-loader": "^3.0.2", "tree-kill": "^1.2.0", - "uglifyjs-webpack-plugin": "^1.2.5", + "terser-webpack-plugin": "^1.0.2", "url-loader": "^1.0.1", "webpack": "^4.15.1", "webpack-dev-middleware": "^3.1.3", diff --git a/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/common.ts b/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/common.ts index cab614154b63..c65d78fa07e5 100644 --- a/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/common.ts +++ b/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/common.ts @@ -24,7 +24,7 @@ import { normalizeExtraEntryPoints } from './utils'; const ProgressPlugin = require('webpack/lib/ProgressPlugin'); const CircularDependencyPlugin = require('circular-dependency-plugin'); -const UglifyJSPlugin = require('uglifyjs-webpack-plugin'); +const TerserPlugin = require('terser-webpack-plugin'); const StatsPlugin = require('stats-webpack-plugin'); /** @@ -208,7 +208,7 @@ export function getCommonConfig(wco: WebpackConfigOptions) { const isIvyEnabled = wco.tsConfig.raw.angularCompilerOptions && wco.tsConfig.raw.angularCompilerOptions.enableIvy; - const uglifyOptions = { + const terserOptions = { ecma: wco.supportES2015 ? 6 : 5, warnings: !!buildOptions.verbose, safari10: true, @@ -318,11 +318,11 @@ export function getCommonConfig(wco: WebpackConfigOptions) { // component styles retain their original file name test: (file) => /\.(?:css|scss|sass|less|styl)$/.test(file), }), - new UglifyJSPlugin({ + new TerserPlugin({ sourceMap: buildOptions.sourceMap, parallel: true, cache: true, - uglifyOptions, + terserOptions, }), ], }, diff --git a/yarn.lock b/yarn.lock index cf0ab04b672a..d04f305387cd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -500,6 +500,10 @@ agent-base@^4.1.0: dependencies: es6-promisify "^5.0.0" +ajv-errors@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/ajv-errors/-/ajv-errors-1.0.0.tgz#ecf021fa108fd17dfb5e6b383f2dd233e31ffc59" + ajv-keywords@^3.1.0: version "3.2.0" resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.2.0.tgz#e86b819c602cf8821ad637413698f1dec021847a" @@ -1187,6 +1191,25 @@ cacache@^10.0.4: unique-filename "^1.1.0" y18n "^4.0.0" +cacache@^11.0.2: + version "11.2.0" + resolved "https://registry.yarnpkg.com/cacache/-/cacache-11.2.0.tgz#617bdc0b02844af56310e411c0878941d5739965" + dependencies: + bluebird "^3.5.1" + chownr "^1.0.1" + figgy-pudding "^3.1.0" + glob "^7.1.2" + graceful-fs "^4.1.11" + lru-cache "^4.1.3" + mississippi "^3.0.0" + mkdirp "^0.5.1" + move-concurrently "^1.0.1" + promise-inflight "^1.0.1" + rimraf "^2.6.2" + ssri "^6.0.0" + unique-filename "^1.1.0" + y18n "^4.0.0" + cache-base@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2" @@ -2697,6 +2720,10 @@ fecha@^2.3.3: version "2.3.3" resolved "https://registry.yarnpkg.com/fecha/-/fecha-2.3.3.tgz#948e74157df1a32fd1b12c3a3c3cdcb6ec9d96cd" +figgy-pudding@^3.1.0: + version "3.4.1" + resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.4.1.tgz#af66da1991fa2f94ff7f33b545a38ea4b3869696" + file-loader@^1.1.11: version "1.1.11" resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-1.1.11.tgz#6fe886449b0f2a936e43cabaac0cdbfb369506f8" @@ -2766,6 +2793,14 @@ find-cache-dir@^1.0.0: make-dir "^1.0.0" pkg-dir "^2.0.0" +find-cache-dir@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-2.0.0.tgz#4c1faed59f45184530fb9d7fa123a4d04a98472d" + dependencies: + commondir "^1.0.1" + make-dir "^1.0.0" + pkg-dir "^3.0.0" + find-parent-dir@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/find-parent-dir/-/find-parent-dir-0.3.0.tgz#33c44b429ab2b2f0646299c5f9f718f376ff8d54" @@ -4454,7 +4489,7 @@ lru-cache@2.2.x: version "2.2.4" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-2.2.4.tgz#6c658619becf14031d0d0b594b16042ce4dc063d" -lru-cache@^4.0.1, lru-cache@^4.1.1: +lru-cache@^4.0.1, lru-cache@^4.1.1, lru-cache@^4.1.3: version "4.1.3" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.3.tgz#a1175cf3496dfc8436c156c334b4955992bce69c" dependencies: @@ -4710,6 +4745,21 @@ mississippi@^2.0.0: stream-each "^1.1.0" through2 "^2.0.0" +mississippi@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/mississippi/-/mississippi-3.0.0.tgz#ea0a3291f97e0b5e8776b363d5f0a12d94c67022" + dependencies: + concat-stream "^1.5.0" + duplexify "^3.4.2" + end-of-stream "^1.1.0" + flush-write-stream "^1.0.0" + from2 "^2.1.0" + parallel-transform "^1.1.0" + pump "^3.0.0" + pumpify "^1.3.3" + stream-each "^1.1.0" + through2 "^2.0.0" + mixin-deep@^1.2.0: version "1.3.1" resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.1.tgz#a49e7268dce1a0d9698e45326c5626df3543d0fe" @@ -5441,6 +5491,12 @@ pkg-dir@^2.0.0: dependencies: find-up "^2.1.0" +pkg-dir@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-3.0.0.tgz#2749020f239ed990881b1f71210d51eb6523bea3" + dependencies: + find-up "^3.0.0" + popper.js@^1.14.1: version "1.14.4" resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.14.4.tgz#8eec1d8ff02a5a3a152dd43414a15c7b79fd69b6" @@ -5635,6 +5691,13 @@ pump@^2.0.0, pump@^2.0.1: end-of-stream "^1.1.0" once "^1.3.1" +pump@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + pumpify@^1.3.3: version "1.5.1" resolved "https://registry.yarnpkg.com/pumpify/-/pumpify-1.5.1.tgz#36513be246ab27570b1a374a5ce278bfd74370ce" @@ -6195,6 +6258,14 @@ schema-utils@^0.4.0, schema-utils@^0.4.3, schema-utils@^0.4.4, schema-utils@^0.4 ajv "^6.1.0" ajv-keywords "^3.1.0" +schema-utils@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-1.0.0.tgz#0b79a93204d7b600d4b2850d1f66c2a34951c770" + dependencies: + ajv "^6.1.0" + ajv-errors "^1.0.0" + ajv-keywords "^3.1.0" + scss-tokenizer@^0.2.3: version "0.2.3" resolved "https://registry.yarnpkg.com/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz#8eb06db9a9723333824d3f5530641149847ce5d1" @@ -6503,6 +6574,13 @@ source-map-support@~0.4.0: dependencies: source-map "^0.5.6" +source-map-support@~0.5.6: + version "0.5.9" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.9.tgz#41bc953b2534267ea2d605bccfa7bfa3111ced5f" + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + source-map-url@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3" @@ -6660,6 +6738,10 @@ ssri@^5.2.4: dependencies: safe-buffer "^5.1.1" +ssri@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/ssri/-/ssri-6.0.0.tgz#fc21bfc90e03275ac3e23d5a42e38b8a1cbc130d" + stack-trace@0.0.x: version "0.0.10" resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0" @@ -6884,6 +6966,27 @@ term-size@^1.2.0: dependencies: execa "^0.7.0" +terser-webpack-plugin@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-1.0.2.tgz#b62dfdc4e59b0b5093665a765b234645b598d1a5" + dependencies: + cacache "^11.0.2" + find-cache-dir "^2.0.0" + schema-utils "^1.0.0" + serialize-javascript "^1.4.0" + source-map "^0.6.1" + terser "^3.8.1" + webpack-sources "^1.1.0" + worker-farm "^1.5.2" + +terser@^3.8.1: + version "3.8.1" + resolved "https://registry.yarnpkg.com/terser/-/terser-3.8.1.tgz#cb70070ac9e0a71add169dfb63c0a64fca2738ac" + dependencies: + commander "~2.16.0" + source-map "~0.6.1" + source-map-support "~0.5.6" + text-extensions@^1.0.0: version "1.7.0" resolved "https://registry.yarnpkg.com/text-extensions/-/text-extensions-1.7.0.tgz#faaaba2625ed746d568a23e4d0aacd9bf08a8b39" @@ -7143,7 +7246,7 @@ uglify-to-browserify@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz#6e0924d6bda6b5afe349e39a6d632850a0f882b7" -uglifyjs-webpack-plugin@^1.2.4, uglifyjs-webpack-plugin@^1.2.5: +uglifyjs-webpack-plugin@^1.2.4: version "1.2.7" resolved "https://registry.yarnpkg.com/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-1.2.7.tgz#57638dd99c853a1ebfe9d97b42160a8a507f9d00" dependencies: From d55deb928218089ea2eb291ae572dd59c63d07ae Mon Sep 17 00:00:00 2001 From: clydin <19598772+clydin@users.noreply.github.com> Date: Mon, 27 Aug 2018 13:27:44 -0400 Subject: [PATCH 0005/1703] build: add bazel TS library for node tasks (#12008) --- packages/angular_devkit/schematics/BUILD | 29 ++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/packages/angular_devkit/schematics/BUILD b/packages/angular_devkit/schematics/BUILD index db9835277204..613a672ff023 100644 --- a/packages/angular_devkit/schematics/BUILD +++ b/packages/angular_devkit/schematics/BUILD @@ -65,6 +65,7 @@ ts_library( srcs = glob( include = ["tasks/**/*.ts"], exclude = [ + "tasks/node/**/*.ts", "tasks/**/*_spec.ts", "tasks/**/*_benchmark.ts", "tasks/tslint-fix/test/**/*", @@ -84,6 +85,32 @@ ts_library( ], ) +# @angular-devkit/schematics/tasks/node + +ts_library( + name = "tasks_node", + srcs = glob( + include = ["tasks/node/**/*.ts"], + exclude = [ + "tasks/node/**/*_spec.ts", + "tasks/node/**/*_benchmark.ts", + ], + ), + module_name = "@angular-devkit/schematics/tasks/node", + module_root = "tasks/node/index.d.ts", + deps = [ + ":schematics", + ":tasks", + "//packages/angular_devkit/core", + "//packages/angular_devkit/core:node", + "@rxjs", + "@rxjs//operators", + # @typings: node + # @typings: tslint + # @typings: typescript + ], +) + ts_library( name = "tasks_test_lib", srcs = glob( @@ -135,6 +162,7 @@ ts_library( deps = [ ":schematics", ":tasks", + ":tasks_node", "//packages/angular_devkit/core", "//packages/angular_devkit/core:node", "@rxjs", @@ -185,6 +213,7 @@ ts_library( deps = [ ":schematics", ":tasks", + ":tasks_node", ":tools", "//packages/angular_devkit/core", "@rxjs", From 02bfde50d65c76900ab6885d5f0dc855b3e299ec Mon Sep 17 00:00:00 2001 From: Alex Eagle Date: Mon, 27 Aug 2018 10:28:21 -0700 Subject: [PATCH 0006/1703] docs: add install instructions to pull a snapshot build (#11988) Would like to point users here when they ask why their bug was closed but the problem is still visible in `@latest` --- docs/documentation/home.md | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/docs/documentation/home.md b/docs/documentation/home.md index 83762c301ab0..c5674d45ce4f 100644 --- a/docs/documentation/home.md +++ b/docs/documentation/home.md @@ -2,7 +2,7 @@ # Angular CLI -NOTE: this documentation is for Angular CLI 6. For Angular CLI 1.x go [here](1-x/home) instead. +NOTE: this documentation is for Angular CLI 6. For Angular CLI 1.x go [here](1-x-home) instead. ### Overview The Angular CLI is a tool to initialize, develop, scaffold and maintain [Angular](https://angular.io) applications @@ -13,6 +13,10 @@ To install the Angular CLI: npm install -g @angular/cli ``` +> If you get an error installing the CLI, this is an issue with your local npm setup on your machine, not a problem in Angular CLI. +> Please have a look at the [fixing npm permissions page](https://docs.npmjs.com/getting-started/fixing-npm-permissions), [common errors page](https://docs.npmjs.com/troubleshooting/common-errors), [npm issue tracker](https://github.com/npm/npm/issues), or open a new issue if the problem you are experiencing isn't known. +> To install a different version, see below. + Generating and serving an Angular project via a development server [Create](new) and [run](serve) a new project: ``` @@ -63,3 +67,12 @@ End-to-end tests are run via [Protractor](http://www.protractortest.org/). ### Additional Information There are several [stories](stories) which will walk you through setting up additional aspects of Angular applications. + +### Installing a specific version +The CLI is installed both globally (the command above with the `-g` argument to `npm install`) and also within the project. To install a different version of the CLI, you can just update the version locally within your project. The globally installed package will always delegate to the local one. + +There are several different versions available at any time: +- Install a previous version, maybe because of a bug in the latest version. For example to get 6.0.2: `npm install @angular/cli@6.0.2` +- Install the pre-release of a newer minor/major version, to try new features. For example to get 7.0.0-beta.3: `npm install @angular/cli@next`. Note that the `@next` version is only useful during beta periods. +- Install a snapshot build from the latest passing run of our CI (angular-cli/master). This is useful if an issue was just fixed or a new feature just landed on master, but is not yet released: `npm install @angular/cli@github:angular/cli-builds` (or maybe better, find the particular SHA that you want to try: +`npm install @angular/cli@github:angular/cli-builds#0123456789abcdef`) From 62e72fea38108de31e412c6068cf55062298b4d4 Mon Sep 17 00:00:00 2001 From: Hans Date: Mon, 27 Aug 2018 18:11:07 -0700 Subject: [PATCH 0007/1703] fix(@angular-devkit/architect): explicitly type functions Closes: #11992 --- .../angular_devkit/architect/testing/test-project-host.ts | 3 ++- packages/angular_devkit/build_angular/test/utils.ts | 4 ++-- packages/angular_devkit/core/src/virtual-fs/host/test.ts | 6 +++--- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/angular_devkit/architect/testing/test-project-host.ts b/packages/angular_devkit/architect/testing/test-project-host.ts index 4980f0a35bd9..82b75036cf56 100644 --- a/packages/angular_devkit/architect/testing/test-project-host.ts +++ b/packages/angular_devkit/architect/testing/test-project-host.ts @@ -8,6 +8,7 @@ import { Path, + PathFragment, basename, dirname, join, @@ -128,7 +129,7 @@ export class TestProjectHost extends NodeJsSyncHost { virtualFs.stringToFileBuffer(content.concat(str))); } - fileMatchExists(dir: string, regex: RegExp) { + fileMatchExists(dir: string, regex: RegExp): PathFragment | undefined { const [fileName] = this.scopedSync().list(normalize(dir)).filter(name => name.match(regex)); return fileName || undefined; diff --git a/packages/angular_devkit/build_angular/test/utils.ts b/packages/angular_devkit/build_angular/test/utils.ts index 826c48524d3a..72a11bd0a605 100644 --- a/packages/angular_devkit/build_angular/test/utils.ts +++ b/packages/angular_devkit/build_angular/test/utils.ts @@ -7,13 +7,13 @@ */ import { TestProjectHost } from '@angular-devkit/architect/testing'; -import { join, normalize } from '@angular-devkit/core'; +import { Path, join, normalize } from '@angular-devkit/core'; const devkitRoot = normalize((global as any)._DevKitRoot); // tslint:disable-line:no-any const workspaceRoot = join(devkitRoot, 'tests/angular_devkit/build_angular/hello-world-app/'); export const host = new TestProjectHost(workspaceRoot); -export const outputPath = normalize('dist'); +export const outputPath: Path = normalize('dist'); export const browserTargetSpec = { project: 'app', target: 'build' }; export const devServerTargetSpec = { project: 'app', target: 'serve' }; diff --git a/packages/angular_devkit/core/src/virtual-fs/host/test.ts b/packages/angular_devkit/core/src/virtual-fs/host/test.ts index eb44fa557133..4efdae796f4b 100644 --- a/packages/angular_devkit/core/src/virtual-fs/host/test.ts +++ b/packages/angular_devkit/core/src/virtual-fs/host/test.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ import { Observable } from 'rxjs'; -import { Path, join, normalize } from '../path'; +import { Path, PathFragment, join, normalize } from '../path'; import { fileBufferToString, stringToFileBuffer } from './buffer'; import { FileBuffer, HostWatchEvent, HostWatchOptions, Stats } from './interface'; import { SimpleMemoryHost, SimpleMemoryHostStats } from './memory'; @@ -96,7 +96,7 @@ export class TestHost extends SimpleMemoryHost { return super._rename(from, to); } - protected _list(path: Path) { + protected _list(path: Path): PathFragment[] { this._records.push({ kind: 'list', path }); return super._list(path); @@ -135,7 +135,7 @@ export class TestHost extends SimpleMemoryHost { return fileBufferToString(super._read(normalize(path))); } - $list(path: string) { + $list(path: string): PathFragment[] { return super._list(normalize(path)); } From 1730c79b99e1744350f05585f20ab3a4fc220c55 Mon Sep 17 00:00:00 2001 From: Alan Agius Date: Tue, 28 Aug 2018 03:12:04 +0200 Subject: [PATCH 0008/1703] =?UTF-8?q?fix(@angular-devkit/build-angular):?= =?UTF-8?q?=20exclude=20`.map`=20files=20from=20budget=20=E2=80=A6=20(#120?= =?UTF-8?q?12)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(@angular-devkit/build-angular): exclude `.map` files from budget calculations Closes #11999 --- .../utilities/bundle-calculator.ts | 7 ++-- .../test/browser/bundle-budgets_spec_large.ts | 35 +++++++++++++++++++ 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/packages/angular_devkit/build_angular/src/angular-cli-files/utilities/bundle-calculator.ts b/packages/angular_devkit/build_angular/src/angular-cli-files/utilities/bundle-calculator.ts index 810899e0d868..a396b2f3801d 100644 --- a/packages/angular_devkit/build_angular/src/angular-cli-files/utilities/bundle-calculator.ts +++ b/packages/angular_devkit/build_angular/src/angular-cli-files/utilities/bundle-calculator.ts @@ -64,6 +64,7 @@ class InitialCalculator extends Calculator { const initialChunks = this.compilation.chunks.filter(chunk => chunk.isOnlyInitial()); const size: number = initialChunks .reduce((files, chunk) => [...files, ...chunk.files], []) + .filter((file: string) => !file.endsWith('.map')) .map((file: string) => this.compilation.assets[file].size()) .reduce((total: number, size: number) => total + size, 0); return [{size, label: 'initial'}]; @@ -76,7 +77,7 @@ class InitialCalculator extends Calculator { class AllScriptCalculator extends Calculator { calculate() { const size: number = Object.keys(this.compilation.assets) - .filter(key => /\.js$/.test(key)) + .filter(key => key.endsWith('.js')) .map(key => this.compilation.assets[key]) .map(asset => asset.size()) .reduce((total: number, size: number) => total + size, 0); @@ -90,6 +91,7 @@ class AllScriptCalculator extends Calculator { class AllCalculator extends Calculator { calculate() { const size: number = Object.keys(this.compilation.assets) + .filter(key => !key.endsWith('.map')) .map(key => this.compilation.assets[key].size()) .reduce((total: number, size: number) => total + size, 0); return [{size, label: 'total'}]; @@ -102,7 +104,7 @@ class AllCalculator extends Calculator { class AnyScriptCalculator extends Calculator { calculate() { return Object.keys(this.compilation.assets) - .filter(key => /\.js$/.test(key)) + .filter(key => key.endsWith('.js')) .map(key => { const asset = this.compilation.assets[key]; return { @@ -119,6 +121,7 @@ class AnyScriptCalculator extends Calculator { class AnyCalculator extends Calculator { calculate() { return Object.keys(this.compilation.assets) + .filter(key => !key.endsWith('.map')) .map(key => { const asset = this.compilation.assets[key]; return { diff --git a/packages/angular_devkit/build_angular/test/browser/bundle-budgets_spec_large.ts b/packages/angular_devkit/build_angular/test/browser/bundle-budgets_spec_large.ts index 918e691ba72b..56eb019a5e79 100644 --- a/packages/angular_devkit/build_angular/test/browser/bundle-budgets_spec_large.ts +++ b/packages/angular_devkit/build_angular/test/browser/bundle-budgets_spec_large.ts @@ -54,4 +54,39 @@ describe('Browser Builder bundle budgets', () => { tap(() => expect(logger.includes('WARNING')).toBe(true)), ).toPromise().then(done, done.fail); }); + + describe(`should ignore '.map' files`, () => { + it(`when 'intial' budget`, (done) => { + const overrides = { + optimization: true, + budgets: [{ type: 'initial', maximumError: '1mb' }], + }; + + runTargetSpec(host, browserTargetSpec, overrides, DefaultTimeout * 2).pipe( + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + ).toPromise().then(done, done.fail); + }); + + it(`when 'all' budget`, (done) => { + const overrides = { + optimization: true, + budgets: [{ type: 'all', maximumError: '1mb' }], + }; + + runTargetSpec(host, browserTargetSpec, overrides, DefaultTimeout * 2).pipe( + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + ).toPromise().then(done, done.fail); + }); + + it(`when 'any' budget`, (done) => { + const overrides = { + optimization: true, + budgets: [{ type: 'any', maximumError: '1mb' }], + }; + + runTargetSpec(host, browserTargetSpec, overrides, DefaultTimeout * 2).pipe( + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + ).toPromise().then(done, done.fail); + }); + }); }); From 8b7fa047d75c95d760903fa7dab8b7b71d5c90a5 Mon Sep 17 00:00:00 2001 From: Dmitry Teplov Date: Tue, 28 Aug 2018 04:12:30 +0300 Subject: [PATCH 0009/1703] docs: fix generate library skip-install desc (#11998) --- docs/documentation/generate/library.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/documentation/generate/library.md b/docs/documentation/generate/library.md index 2d62e7985834..2215d2c69db3 100644 --- a/docs/documentation/generate/library.md +++ b/docs/documentation/generate/library.md @@ -57,7 +57,7 @@ Generate a library project for Angular. --skip-install

- Do not add dependencies to package.json. + Skip installing dependency packages.

From 3f94b2d8ba5fe58d71a19e8808628302794a6b81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Exbrayat?= Date: Tue, 28 Aug 2018 22:10:18 +0200 Subject: [PATCH 0010/1703] feat(@schematics/angular): remove useless import for Ivy (#11874) Using cli `6.2.0-beta.2` with the new `experimentalIvy` flag leads to: ERROR in src/app/app.module.ts(1,1): error TS6133: 'BrowserModule' is declared but its value is never read. --- .../schematics/angular/application/other-files/app.module.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/schematics/angular/application/other-files/app.module.ts b/packages/schematics/angular/application/other-files/app.module.ts index 281482250cfa..4ef3c6e953a1 100644 --- a/packages/schematics/angular/application/other-files/app.module.ts +++ b/packages/schematics/angular/application/other-files/app.module.ts @@ -1,6 +1,6 @@ -import { BrowserModule } from '@angular/platform-browser'; +<% if (!experimentalIvy) { %>import { BrowserModule } from '@angular/platform-browser';<% } %> import { NgModule } from '@angular/core'; -<% if (routing) { %> +<% if (routing && !experimentalIvy) { %> import { AppRoutingModule } from './app-routing.module';<% } %> import { AppComponent } from './app.component'; From 7f67c64be1b84d4927bce882cb37cf323688dccc Mon Sep 17 00:00:00 2001 From: Alan Agius Date: Tue, 28 Aug 2018 22:10:43 +0200 Subject: [PATCH 0011/1703] fix(@angular-devkit/build-angular): fixes cors issues with karma (#11970) Checking the stack trace in the issue one can noticed that the paths for the components are being served via the `webpack://` protocol which is causing a `cors` issue between `http` and `webpack` protocol This PR removed the `webpack` protocol from the `devtoolModuleFilenameTemplate` Closes #11966 --- .../build_angular/src/angular-cli-files/plugins/karma.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/angular_devkit/build_angular/src/angular-cli-files/plugins/karma.ts b/packages/angular_devkit/build_angular/src/angular-cli-files/plugins/karma.ts index 71060d02a597..92f4776ae7b6 100644 --- a/packages/angular_devkit/build_angular/src/angular-cli-files/plugins/karma.ts +++ b/packages/angular_devkit/build_angular/src/angular-cli-files/plugins/karma.ts @@ -144,6 +144,7 @@ const init: any = (config: any, emitter: any, customFileHandlers: any) => { // Files need to be served from a custom path for Karma. webpackConfig.output.path = '/_karma_webpack_/'; webpackConfig.output.publicPath = '/_karma_webpack_/'; + webpackConfig.output.devtoolModuleFilenameTemplate = '[namespace]/[resource-path]?[loaders]'; let compiler: any; try { From 7eb080362ffdb137c00cb0572f50a7664db807de Mon Sep 17 00:00:00 2001 From: Alan Agius Date: Tue, 28 Aug 2018 22:50:25 +0200 Subject: [PATCH 0012/1703] fix(@angular-devkit/build-angular): only collect coverage from files under `sourceRoot` (#11974) In some cases when having libraries within the workspace this is causing a `Maximum call stack size exceeded`, Also for libraries coverage should be collected with the respective `ng test` Closes #11934 --- .../angular-cli-files/models/build-options.ts | 1 + .../models/webpack-configs/test.ts | 6 ++- .../build_angular/src/karma/index.ts | 6 ++- .../test/karma/code-coverage_spec_large.ts | 48 +++++++++++++++++++ 4 files changed, 58 insertions(+), 3 deletions(-) diff --git a/packages/angular_devkit/build_angular/src/angular-cli-files/models/build-options.ts b/packages/angular_devkit/build_angular/src/angular-cli-files/models/build-options.ts index a81e728e45de..4dac5449818a 100644 --- a/packages/angular_devkit/build_angular/src/angular-cli-files/models/build-options.ts +++ b/packages/angular_devkit/build_angular/src/angular-cli-files/models/build-options.ts @@ -74,6 +74,7 @@ export interface WebpackTestOptions extends BuildOptions { export interface WebpackConfigOptions { root: string; projectRoot: string; + sourceRoot?: string; buildOptions: T; tsConfig: ts.ParsedCommandLine; tsConfigPath: string; diff --git a/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/test.ts b/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/test.ts index 9a2c6bf10945..4c8b05aa6008 100644 --- a/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/test.ts +++ b/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/test.ts @@ -23,7 +23,7 @@ import { WebpackConfigOptions, WebpackTestOptions } from '../build-options'; export function getTestConfig( wco: WebpackConfigOptions, ): webpack.Configuration { - const { root, buildOptions } = wco; + const { root, buildOptions, sourceRoot: include } = wco; const extraRules: webpack.Rule[] = []; const extraPlugins: webpack.Plugin[] = []; @@ -46,10 +46,12 @@ export function getTestConfig( } extraRules.push({ - test: /\.(js|ts)$/, loader: 'istanbul-instrumenter-loader', + test: /\.(js|ts)$/, + loader: 'istanbul-instrumenter-loader', options: { esModules: true }, enforce: 'post', exclude, + include, }); } diff --git a/packages/angular_devkit/build_angular/src/karma/index.ts b/packages/angular_devkit/build_angular/src/karma/index.ts index f12b29015c5a..df1138edc85a 100644 --- a/packages/angular_devkit/build_angular/src/karma/index.ts +++ b/packages/angular_devkit/build_angular/src/karma/index.ts @@ -71,11 +71,13 @@ export class KarmaBuilder implements Builder { karmaOptions.browsers = options.browsers.split(','); } + const sourceRoot = builderConfig.sourceRoot && resolve(root, builderConfig.sourceRoot); + karmaOptions.buildWebpack = { root: getSystemPath(root), projectRoot: getSystemPath(projectRoot), options: options as NormalizedKarmaBuilderSchema, - webpackConfig: this._buildWebpackConfig(root, projectRoot, host, + webpackConfig: this._buildWebpackConfig(root, projectRoot, sourceRoot, host, options as NormalizedKarmaBuilderSchema), // Pass onto Karma to emit BuildEvents. successCb: () => obs.next({ success: true }), @@ -108,6 +110,7 @@ export class KarmaBuilder implements Builder { private _buildWebpackConfig( root: Path, projectRoot: Path, + sourceRoot: Path | undefined, host: virtualFs.Host, options: NormalizedKarmaBuilderSchema, ) { @@ -130,6 +133,7 @@ export class KarmaBuilder implements Builder { wco = { root: getSystemPath(root), projectRoot: getSystemPath(projectRoot), + sourceRoot: sourceRoot && getSystemPath(sourceRoot), // TODO: use only this.options, it contains all flags and configs items already. buildOptions: compatOptions, tsConfig, diff --git a/packages/angular_devkit/build_angular/test/karma/code-coverage_spec_large.ts b/packages/angular_devkit/build_angular/test/karma/code-coverage_spec_large.ts index b67d3d41108e..8440e1143420 100644 --- a/packages/angular_devkit/build_angular/test/karma/code-coverage_spec_large.ts +++ b/packages/angular_devkit/build_angular/test/karma/code-coverage_spec_large.ts @@ -56,4 +56,52 @@ describe('Karma Builder code coverage', () => { }), ).toPromise().then(done, done.fail); }, 120000); + + it(`should collect coverage from paths in 'sourceRoot'`, (done) => { + const overrides: Partial = { codeCoverage: true }; + + const files: { [path: string]: string } = { + './dist/my-lib/index.d.ts': ` + export declare const title = 'app'; + `, + './dist/my-lib/index.js': ` + export const title = 'app'; + `, + './src/app/app.component.ts': ` + import { Component } from '@angular/core'; + import { title } from 'my-lib'; + + @Component({ + selector: 'app-root', + templateUrl: './app.component.html', + styleUrls: ['./app.component.css'] + }) + export class AppComponent { + title = title; + } + `, + }; + + host.writeMultipleFiles(files); + + host.replaceInFile('tsconfig.json', /"baseUrl": ".\/",/, ` + "baseUrl": "./", + "paths": { + "my-lib": [ + "./dist/my-lib" + ] + }, + `); + + runTargetSpec(host, karmaTargetSpec, overrides).pipe( + // It seems like the coverage files take a while being written to disk, so we wait 500ms here. + debounceTime(500), + tap(buildEvent => { + expect(buildEvent.success).toBe(true); + expect(host.scopedSync().exists(coverageFilePath)).toBe(true); + const content = virtualFs.fileBufferToString(host.scopedSync().read(coverageFilePath)); + expect(content).not.toContain('my-lib'); + }), + ).toPromise().then(done, done.fail); + }, 120000); }); From 2dcf0b9901af011c02a7d425e23e4d4c04ba1100 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Wed, 8 Aug 2018 11:39:38 -0400 Subject: [PATCH 0013/1703] refactor(@angular-devkit/core): cleanup ajv initialization --- .../core/src/json/schema/registry.ts | 23 ++++++++----------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/packages/angular_devkit/core/src/json/schema/registry.ts b/packages/angular_devkit/core/src/json/schema/registry.ts index 0965910a6273..530ea4d14888 100644 --- a/packages/angular_devkit/core/src/json/schema/registry.ts +++ b/packages/angular_devkit/core/src/json/schema/registry.ts @@ -7,7 +7,7 @@ */ import * as ajv from 'ajv'; import * as http from 'http'; -import { Observable, from, of as observableOf } from 'rxjs'; +import { Observable, from, of as observableOf, throwError } from 'rxjs'; import { concatMap, map, switchMap, tap } from 'rxjs/operators'; import { BaseException } from '../../exception/exception'; import { PartiallyOrderedSet, isObservable } from '../../utils'; @@ -176,7 +176,7 @@ export class CoreSchemaRegistry implements SchemaRegistry { validate = (validate.refVal as any)[(validate.refs as any)['#' + refHash]]; } - return { context: validate, schema: validate && validate.schema as JsonObject }; + return { context: validate, schema: validate.schema as JsonObject }; } compile(schema: JsonObject): Observable { @@ -186,23 +186,18 @@ export class CoreSchemaRegistry implements SchemaRegistry { // in synchronous (if available). let validator: Observable; try { - const maybeFnValidate = this._ajv.compile(schema); - validator = observableOf(maybeFnValidate); + validator = observableOf(this._ajv.compile(schema)); } catch (e) { // Propagate the error. if (!(e instanceof (ajv.MissingRefError as {} as Function))) { - throw e; + return throwError(e); } - validator = new Observable(obs => { - this._ajv.compileAsync(schema) - .then(validate => { - obs.next(validate); - obs.complete(); - }, err => { - obs.error(err); - }); - }); + try { + validator = from(this._ajv.compileAsync(schema)); + } catch (e) { + return throwError(e); + } } return validator From 4207a15c4f68dccc89feba5c93dca948f71f51d9 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Fri, 10 Aug 2018 14:37:55 -0400 Subject: [PATCH 0014/1703] fix(@angular-devkit/core): make smart default work with default/required --- packages/angular_devkit/core/src/json/schema/registry.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/angular_devkit/core/src/json/schema/registry.ts b/packages/angular_devkit/core/src/json/schema/registry.ts index 530ea4d14888..5ad00609e965 100644 --- a/packages/angular_devkit/core/src/json/schema/registry.ts +++ b/packages/angular_devkit/core/src/json/schema/registry.ts @@ -208,6 +208,7 @@ export class CoreSchemaRegistry implements SchemaRegistry { return visitJson(data, visitor, schema, this._resolver, validate); })), ).pipe( + switchMap(updateData => this._applySmartDefaults(updateData)), switchMap(updatedData => { const result = validate(updatedData); @@ -227,8 +228,8 @@ export class CoreSchemaRegistry implements SchemaRegistry { }), switchMap(([data, valid]) => { if (valid) { - return this._applySmartDefaults(data).pipe( - ...[...this._post].map(visitor => concatMap(data => { + return observableOf(data).pipe( + ...[...this._post].map(visitor => concatMap((data: JsonValue) => { return visitJson(data as JsonValue, visitor, schema, this._resolver, validate); })), ).pipe( From 516f52e3ddb5828d9f792fd740164fe894e4fdc1 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Thu, 9 Aug 2018 14:18:41 -0400 Subject: [PATCH 0015/1703] feat(@angular-devkit/core): initial prompt provider json schema support --- packages/angular_devkit/core/package.json | 1 + .../core/src/json/schema/interface.ts | 25 +- .../core/src/json/schema/prompt_spec.ts | 364 ++++++++++++++++++ .../core/src/json/schema/registry.ts | 311 ++++++++++++--- .../core/src/json/schema/registry_spec.ts | 1 + yarn.lock | 6 +- 6 files changed, 647 insertions(+), 61 deletions(-) create mode 100644 packages/angular_devkit/core/src/json/schema/prompt_spec.ts diff --git a/packages/angular_devkit/core/package.json b/packages/angular_devkit/core/package.json index 1c310e7cb05d..b63e953adeec 100644 --- a/packages/angular_devkit/core/package.json +++ b/packages/angular_devkit/core/package.json @@ -10,6 +10,7 @@ "dependencies": { "ajv": "~6.4.0", "chokidar": "^2.0.3", + "fast-json-stable-stringify": "^2.0.0", "source-map": "^0.5.6", "rxjs": "^6.0.0" } diff --git a/packages/angular_devkit/core/src/json/schema/interface.ts b/packages/angular_devkit/core/src/json/schema/interface.ts index 99af07451d46..6f0f092629c3 100644 --- a/packages/angular_devkit/core/src/json/schema/interface.ts +++ b/packages/angular_devkit/core/src/json/schema/interface.ts @@ -5,7 +5,7 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ -import { Observable } from 'rxjs'; +import { Observable, SubscribableOrPromise } from 'rxjs'; import { JsonArray, JsonObject, JsonValue } from '../interface'; export type JsonPointer = string & { @@ -57,9 +57,13 @@ export interface SchemaValidatorResult { errors?: SchemaValidatorError[]; } +export interface SchemaValidatorOptions { + withPrompts: boolean; +} + export interface SchemaValidator { // tslint:disable-next-line:no-any - (data: any): Observable; + (data: any, options?: Partial): Observable; } export interface SchemaFormatter { @@ -90,8 +94,25 @@ export interface SchemaKeywordValidator { ): boolean | Observable; } +export interface PromptDefinition { + id: string; + type: string; + message: string; + default?: string | number | boolean | null; + priority: number; + validator?: (value: string) => boolean | string | Promise; + + items?: Array; + + raw?: string | JsonObject; +} + +export type PromptProvider = (definitions: Array) + => SubscribableOrPromise<{ [id: string]: JsonValue }>; + export interface SchemaRegistry { compile(schema: Object): Observable; addFormat(format: SchemaFormat): void; addSmartDefaultProvider(source: string, provider: SmartDefaultProvider): void; + usePromptProvider(provider: PromptProvider): void; } diff --git a/packages/angular_devkit/core/src/json/schema/prompt_spec.ts b/packages/angular_devkit/core/src/json/schema/prompt_spec.ts new file mode 100644 index 000000000000..40d5f89543c9 --- /dev/null +++ b/packages/angular_devkit/core/src/json/schema/prompt_spec.ts @@ -0,0 +1,364 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +// tslint:disable:no-any no-big-function +import { map, mergeMap } from 'rxjs/operators'; +import { CoreSchemaRegistry } from './registry'; + + +describe('Prompt Provider', () => { + it('sets properties with answer', done => { + const registry = new CoreSchemaRegistry(); + const data: any = {}; + + registry.usePromptProvider(async definitions => { + return { [definitions[0].id]: true }; + }); + + registry + .compile({ + properties: { + test: { + type: 'boolean', + 'x-prompt': 'test-message', + }, + }, + }) + .pipe( + mergeMap(validator => validator(data)), + map(() => { + expect(data.test).toBe(true); + }), + ) + .toPromise().then(done, done.fail); + }); + + it('supports mixed schema references', done => { + const registry = new CoreSchemaRegistry(); + const data: any = {}; + + registry.usePromptProvider(async definitions => { + return { + '\'bool\'': true, + '\'test\'': 'two', + '\'obj\'/\'deep\'/\'three\'': 'test3-answer', + }; + }); + + registry + .compile({ + properties: { + bool: { + $ref: '#/definitions/example', + }, + test: { + type: 'string', + enum: [ + 'one', + 'two', + 'three', + ], + 'x-prompt': { + type: 'list', + 'message': 'other-message', + }, + }, + obj: { + properties: { + deep: { + properties: { + three: { + $ref: '#/definitions/test3', + }, + }, + }, + }, + }, + }, + definitions: { + example: { + type: 'boolean', + 'x-prompt': 'example-message', + }, + test3: { + type: 'string', + 'x-prompt': 'test3-message', + }, + }, + }) + .pipe( + mergeMap(validator => validator(data)), + map(result => { + expect(result.success).toBe(true); + expect(data.bool).toBe(true); + expect(data.test).toBe('two'); + expect(data.obj.deep.three).toEqual('test3-answer'); + }), + ) + .toPromise().then(done, done.fail); + }); + + describe('with shorthand', () => { + it('supports message value', done => { + const registry = new CoreSchemaRegistry(); + const data: any = {}; + + registry.usePromptProvider(async definitions => { + expect(definitions.length).toBe(1); + expect(definitions[0].message).toBe('test-message'); + + return {}; + }); + + registry + .compile({ + properties: { + test: { + type: 'string', + 'x-prompt': 'test-message', + }, + }, + }) + .pipe( + mergeMap(validator => validator(data)), + ) + .toPromise().then(done, done.fail); + }); + + it('analyzes enums', done => { + const registry = new CoreSchemaRegistry(); + const data: any = {}; + + registry.usePromptProvider(async definitions => { + expect(definitions.length).toBe(1); + expect(definitions[0].type).toBe('list'); + expect(definitions[0].items).toEqual([ + 'one', + 'two', + 'three', + ]); + + return {}; + }); + + registry + .compile({ + properties: { + test: { + type: 'string', + enum: [ + 'one', + 'two', + 'three', + ], + 'x-prompt': 'test-message', + }, + }, + }) + .pipe( + mergeMap(validator => validator(data)), + ) + .toPromise().then(done, done.fail); + }); + + it('analyzes boolean properties', done => { + const registry = new CoreSchemaRegistry(); + const data: any = {}; + + registry.usePromptProvider(async definitions => { + expect(definitions.length).toBe(1); + expect(definitions[0].type).toBe('confirmation'); + expect(definitions[0].items).toBeUndefined(); + + return {}; + }); + + registry + .compile({ + properties: { + test: { + type: 'boolean', + 'x-prompt': 'test-message', + }, + }, + }) + .pipe( + mergeMap(validator => validator(data)), + ) + .toPromise().then(done, done.fail); + }); + + }); + + describe('with longhand', () => { + it('supports message option', done => { + const registry = new CoreSchemaRegistry(); + const data: any = {}; + + registry.usePromptProvider(async definitions => { + expect(definitions.length).toBe(1); + expect(definitions[0].message).toBe('test-message'); + + return {}; + }); + + registry + .compile({ + properties: { + test: { + type: 'string', + 'x-prompt': { + 'message': 'test-message', + }, + }, + }, + }) + .pipe( + mergeMap(validator => validator(data)), + ) + .toPromise().then(done, done.fail); + }); + + it('analyzes enums WITH explicit list type', done => { + const registry = new CoreSchemaRegistry(); + const data: any = {}; + + registry.usePromptProvider(async definitions => { + expect(definitions.length).toBe(1); + expect(definitions[0].type).toBe('list'); + expect(definitions[0].items).toEqual([ + 'one', + 'two', + 'three', + ]); + + return { [definitions[0].id]: 'one' }; + }); + + registry + .compile({ + properties: { + test: { + type: 'string', + enum: [ + 'one', + 'two', + 'three', + ], + 'x-prompt': { + 'type': 'list', + 'message': 'test-message', + }, + }, + }, + }) + .pipe( + mergeMap(validator => validator(data)), + ) + .toPromise().then(done, done.fail); + }); + + it('analyzes enums WITHOUT explicit list type', done => { + const registry = new CoreSchemaRegistry(); + const data: any = {}; + + registry.usePromptProvider(async definitions => { + expect(definitions.length).toBe(1); + expect(definitions[0].type).toBe('list'); + expect(definitions[0].items).toEqual([ + 'one', + 'two', + 'three', + ]); + + return {}; + }); + + registry + .compile({ + properties: { + test: { + type: 'string', + enum: [ + 'one', + 'two', + 'three', + ], + 'x-prompt': { + 'message': 'test-message', + }, + }, + }, + }) + .pipe( + mergeMap(validator => validator(data)), + ) + .toPromise().then(done, done.fail); + }); + + it('analyzes boolean properties', done => { + const registry = new CoreSchemaRegistry(); + const data: any = {}; + + registry.usePromptProvider(async definitions => { + expect(definitions.length).toBe(1); + expect(definitions[0].type).toBe('confirmation'); + expect(definitions[0].items).toBeUndefined(); + + return {}; + }); + + registry + .compile({ + properties: { + test: { + type: 'boolean', + 'x-prompt': { + 'message': 'test-message', + }, + }, + }, + }) + .pipe( + mergeMap(validator => validator(data)), + ) + .toPromise().then(done, done.fail); + }); + + it('allows prompt type override', done => { + const registry = new CoreSchemaRegistry(); + const data: any = {}; + + registry.usePromptProvider(async definitions => { + expect(definitions.length).toBe(1); + expect(definitions[0].type).toBe('input'); + expect(definitions[0].items).toBeUndefined(); + + return {}; + }); + + registry + .compile({ + properties: { + test: { + type: 'boolean', + 'x-prompt': { + 'type': 'input', + 'message': 'test-message', + }, + }, + }, + }) + .pipe( + mergeMap(validator => validator(data)), + ) + .toPromise().then(done, done.fail); + }); + + }); + +}); diff --git a/packages/angular_devkit/core/src/json/schema/registry.ts b/packages/angular_devkit/core/src/json/schema/registry.ts index 5ad00609e965..27e82270ad69 100644 --- a/packages/angular_devkit/core/src/json/schema/registry.ts +++ b/packages/angular_devkit/core/src/json/schema/registry.ts @@ -13,17 +13,21 @@ import { BaseException } from '../../exception/exception'; import { PartiallyOrderedSet, isObservable } from '../../utils'; import { JsonObject, JsonValue } from '../interface'; import { + PromptDefinition, + PromptProvider, SchemaFormat, SchemaFormatter, SchemaRegistry, SchemaValidator, SchemaValidatorError, + SchemaValidatorOptions, SchemaValidatorResult, SmartDefaultProvider, } from './interface'; import { addUndefinedDefaults } from './transforms'; import { JsonVisitor, visitJson } from './visitor'; +const serialize = require('fast-json-stable-stringify'); // This interface should be exported from ajv, but they only export the class and not the type. interface AjvValidationError { @@ -69,15 +73,22 @@ export class SchemaValidationException extends BaseException { } } +interface SchemaInfo { + smartDefaultRecord: Map; + promptDefinitions: Array; +} + export class CoreSchemaRegistry implements SchemaRegistry { private _ajv: ajv.Ajv; private _uriCache = new Map(); private _pre = new PartiallyOrderedSet(); private _post = new PartiallyOrderedSet(); + private _currentCompilationSchemaInfo?: SchemaInfo; + private _validatorCache = new Map(); private _smartDefaultKeyword = false; + private _promptProvider?: PromptProvider; private _sourceMap = new Map>(); - private _smartDefaultRecord = new Map(); constructor(formats: SchemaFormat[] = []) { /** @@ -91,10 +102,10 @@ export class CoreSchemaRegistry implements SchemaRegistry { } this._ajv = ajv({ - useDefaults: true, formats: formatsObj, loadSchema: (uri: string) => this._fetch(uri), schemaId: 'auto', + passContext: true, }); this._ajv.addMetaSchema(require('ajv/lib/refs/json-schema-draft-04.json')); @@ -176,16 +187,28 @@ export class CoreSchemaRegistry implements SchemaRegistry { validate = (validate.refVal as any)[(validate.refs as any)['#' + refHash]]; } - return { context: validate, schema: validate.schema as JsonObject }; + return { context: validate, schema: validate && validate.schema as JsonObject }; } compile(schema: JsonObject): Observable { + const schemaKey = serialize(schema); + const existingValidator = this._validatorCache.get(schemaKey); + if (existingValidator) { + return observableOf(existingValidator); + } + + const schemaInfo: SchemaInfo = { + smartDefaultRecord: new Map(), + promptDefinitions: [], + }; + // Supports both synchronous and asynchronous compilation, by trying the synchronous // version first, then if refs are missing this will fails. // We also add any refs from external fetched schemas so that those will also be used // in synchronous (if available). let validator: Observable; try { + this._currentCompilationSchemaInfo = schemaInfo; validator = observableOf(this._ajv.compile(schema)); } catch (e) { // Propagate the error. @@ -202,15 +225,26 @@ export class CoreSchemaRegistry implements SchemaRegistry { return validator .pipe( - map(validate => (data: JsonValue): Observable => { + map(validate => (data, options) => { + const validationOptions: SchemaValidatorOptions = { + withPrompts: true, + ...options, + }; + const validationContext = { + promptFieldsWithValue: new Set(), + }; + return observableOf(data).pipe( ...[...this._pre].map(visitor => concatMap((data: JsonValue) => { return visitJson(data, visitor, schema, this._resolver, validate); })), ).pipe( - switchMap(updateData => this._applySmartDefaults(updateData)), - switchMap(updatedData => { - const result = validate(updatedData); + switchMap(updateData => this._applySmartDefaults( + updateData, + schemaInfo.smartDefaultRecord, + )), + switchMap((updatedData: JsonValue) => { + const result = validate.call(validationContext, updatedData); return typeof result == 'boolean' ? observableOf([updatedData, result]) @@ -226,6 +260,22 @@ export class CoreSchemaRegistry implements SchemaRegistry { return Promise.reject(err); })); }), + switchMap(([data, valid]) => { + if (!validationOptions.withPrompts) { + return observableOf([data, valid]); + } + + const definitions = schemaInfo.promptDefinitions + .filter(def => !validationContext.promptFieldsWithValue.has(def.id)); + + if (valid && this._promptProvider && definitions.length > 0) { + return from(this._applyPrompts(data, definitions)).pipe( + map(data => [data, valid]), + ); + } else { + return observableOf([data, valid]); + } + }), switchMap(([data, valid]) => { if (valid) { return observableOf(data).pipe( @@ -252,6 +302,7 @@ export class CoreSchemaRegistry implements SchemaRegistry { }), ); }), + tap(v => this._validatorCache.set(schemaKey, v)), ); } @@ -289,10 +340,15 @@ export class CoreSchemaRegistry implements SchemaRegistry { errors: false, valid: true, compile: (schema, _parentSchema, it) => { + const compilationSchemInfo = this._currentCompilationSchemaInfo; + if (!compilationSchemInfo) { + throw new Error('Invalid JSON schema compilation state'); + } + // We cheat, heavily. - this._smartDefaultRecord.set( + compilationSchemInfo.smartDefaultRecord.set( // tslint:disable-next-line:no-any - JSON.stringify((it as any).dataPathArr.slice(1, (it as any).dataLevel + 1) as string[]), + JSON.stringify((it as any).dataPathArr.slice(1, it.dataLevel + 1) as string[]), schema, ); @@ -310,70 +366,209 @@ export class CoreSchemaRegistry implements SchemaRegistry { } } - // tslint:disable-next-line:no-any - private _applySmartDefaults(data: any): Observable { - function _set( - // tslint:disable-next-line:no-any - data: any, - fragments: string[], - value: {}, - // tslint:disable-next-line:no-any - parent: any | null = null, - parentProperty?: string, - ): void { - for (let i = 0; i < fragments.length; i++) { - const f = fragments[i]; - - if (f[0] == 'i') { - if (!Array.isArray(data)) { - return; + usePromptProvider(provider: PromptProvider) { + const isSetup = !!this._promptProvider; + + this._promptProvider = provider; + + if (isSetup) { + return; + } + + this._ajv.addKeyword('x-prompt', { + errors: false, + valid: true, + compile: (schema, parentSchema: JsonObject, it) => { + const compilationSchemInfo = this._currentCompilationSchemaInfo; + if (!compilationSchemInfo) { + throw new Error('Invalid JSON schema compilation state'); + } + + // tslint:disable-next-line:no-any + const pathArray = ((it as any).dataPathArr as string[]).slice(1, it.dataLevel + 1); + const path = pathArray.join('/'); + + let type: string | undefined; + let items: Array | undefined; + let message: string; + if (typeof schema == 'string') { + message = schema; + } else { + message = schema.message; + type = schema.type; + items = schema.items; + } + + if (!type) { + if (parentSchema.type === 'boolean') { + type = 'confirmation'; + } else if (Array.isArray(parentSchema.enum)) { + type = 'list'; + } else { + type = 'input'; } + } - for (let j = 0; j < data.length; j++) { - _set(data[j], fragments.slice(i + 1), value, data, '' + j); + if (type === 'list' && !items) { + if (Array.isArray(parentSchema.enum)) { + type = 'list'; + items = []; + for (const value of parentSchema.enum) { + if (typeof value == 'string') { + items.push(value); + } else if (typeof value == 'object') { + // Invalid + } else { + items.push({ label: value.toString(), value }); + } + } } + } - return; - } else if (f.startsWith('key')) { - if (typeof data !== 'object') { - return; + const definition: PromptDefinition = { + id: path, + type, + message, + priority: 0, + raw: schema, + items, + default: typeof parentSchema.default == 'object' ? undefined : parentSchema.default, + async validator(data: string) { + const result = it.self.validate(parentSchema, data); + if (typeof result === 'boolean') { + return result; + } else { + try { + await result; + + return true; + } catch { + return false; + } + } + }, + }; + + compilationSchemInfo.promptDefinitions.push(definition); + + return function(this: { promptFieldsWithValue: Set }) { + if (this) { + this.promptFieldsWithValue.add(path); } - Object.getOwnPropertyNames(data).forEach(property => { - _set(data[property], fragments.slice(i + 1), value, data, property); - }); + return true; + }; + }, + metaSchema: { + oneOf: [ + { type: 'string' }, + { + type: 'object', + properties: { + 'type': { type: 'string' }, + 'message': { type: 'string' }, + }, + additionalProperties: true, + required: [ 'message' ], + }, + ], + }, + }); + } + + private _applyPrompts(data: T, prompts: Array): Observable { + const provider = this._promptProvider; + if (!provider) { + return observableOf(data); + } + + prompts.sort((a, b) => b.priority - a.priority); + + return from(provider(prompts)).pipe( + map(answers => { + for (const path in answers) { + CoreSchemaRegistry._set( + data, + path.split('/'), + answers[path] as {}, + null, + undefined, + true, + ); + } + + return data; + }), + ); + } + private static _set( + // tslint:disable-next-line:no-any + data: any, + fragments: string[], + value: {}, + // tslint:disable-next-line:no-any + parent: any | null = null, + parentProperty?: string, + force?: boolean, + ): void { + for (let i = 0; i < fragments.length; i++) { + const f = fragments[i]; + + if (f[0] == 'i') { + if (!Array.isArray(data)) { return; - } else if (f.startsWith('\'') && f[f.length - 1] == '\'') { - const property = f - .slice(1, -1) - .replace(/\\'/g, '\'') - .replace(/\\n/g, '\n') - .replace(/\\r/g, '\r') - .replace(/\\f/g, '\f') - .replace(/\\t/g, '\t'); - - // We know we need an object because the fragment is a property key. - if (!data && parent !== null && parentProperty) { - data = parent[parentProperty] = {}; - } - parent = data; - parentProperty = property; + } - data = data[property]; - } else { + for (let j = 0; j < data.length; j++) { + CoreSchemaRegistry._set(data[j], fragments.slice(i + 1), value, data, '' + j); + } + + return; + } else if (f.startsWith('key')) { + if (typeof data !== 'object') { return; } - } - if (parent && parentProperty && parent[parentProperty] === undefined) { - parent[parentProperty] = value; + Object.getOwnPropertyNames(data).forEach(property => { + CoreSchemaRegistry._set(data[property], fragments.slice(i + 1), value, data, property); + }); + + return; + } else if (f.startsWith('\'') && f[f.length - 1] == '\'') { + const property = f + .slice(1, -1) + .replace(/\\'/g, '\'') + .replace(/\\n/g, '\n') + .replace(/\\r/g, '\r') + .replace(/\\f/g, '\f') + .replace(/\\t/g, '\t'); + + // We know we need an object because the fragment is a property key. + if (!data && parent !== null && parentProperty) { + data = parent[parentProperty] = {}; + } + parent = data; + parentProperty = property; + + data = data[property]; + } else { + return; } } + if (parent && parentProperty && (force || parent[parentProperty] === undefined)) { + parent[parentProperty] = value; + } + } + + private _applySmartDefaults( + data: T, + smartDefaults: Map, + ): Observable { return observableOf(data).pipe( - ...[...this._smartDefaultRecord.entries()].map(([pointer, schema]) => { - return concatMap(data => { + ...[...smartDefaults.entries()].map(([pointer, schema]) => { + return concatMap(data => { const fragments = JSON.parse(pointer); const source = this._sourceMap.get((schema as JsonObject).$source as string); @@ -385,7 +580,7 @@ export class CoreSchemaRegistry implements SchemaRegistry { return (value as Observable<{}>).pipe( // Synchronously set the new data at the proper JsonSchema path. - tap(x => _set(data, fragments, x)), + tap(x => CoreSchemaRegistry._set(data, fragments, x)), // But return the data object. map(() => data), ); diff --git a/packages/angular_devkit/core/src/json/schema/registry_spec.ts b/packages/angular_devkit/core/src/json/schema/registry_spec.ts index 4b47be28b2d8..773a87a479bf 100644 --- a/packages/angular_devkit/core/src/json/schema/registry_spec.ts +++ b/packages/angular_devkit/core/src/json/schema/registry_spec.ts @@ -424,4 +424,5 @@ describe('CoreSchemaRegistry', () => { ) .toPromise().then(done, done.fail); }); + }); diff --git a/yarn.lock b/yarn.lock index d04f305387cd..d5e23db55b07 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3813,6 +3813,10 @@ is-primitive@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/is-primitive/-/is-primitive-2.0.0.tgz#207bab91638499c07b2adf240a41a87210034575" +is-promise@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.1.0.tgz#79a2a9ece7f096e80f36d2b2f3bc16c1ff4bf3fa" + is-redirect@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-redirect/-/is-redirect-1.0.0.tgz#1d03dded53bd8db0f30c26e4f95d36fc7c87dc24" @@ -4408,7 +4412,7 @@ lodash.templatesettings@^4.0.0: dependencies: lodash._reinterpolate "~3.0.0" -lodash@^4.0.0, lodash@^4.17.10, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.1, lodash@^4.5.0, lodash@~4.17.10: +lodash@^4.0.0, lodash@^4.17.10, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.1, lodash@^4.3.0, lodash@^4.5.0, lodash@~4.17.10: version "4.17.10" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.10.tgz#1b7793cf7259ea38fb3661d4d38b3260af8ae4e7" From 489f0e905576f3c4ad821d4385d771b0f4bcb31d Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Thu, 9 Aug 2018 14:19:25 -0400 Subject: [PATCH 0016/1703] feat(@angular/cli): initialize a console prompt provider for schematics --- package.json | 1 + .../angular/cli/commands/generate-impl.ts | 10 +- packages/angular/cli/commands/generate.json | 5 + packages/angular/cli/commands/new-impl.ts | 6 +- packages/angular/cli/commands/new.json | 5 + .../angular/cli/models/schematic-command.ts | 45 ++++ packages/angular/cli/package.json | 2 +- yarn.lock | 192 +++++++++++++++++- 8 files changed, 255 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index 4936df3a1966..8b881cb9159e 100644 --- a/package.json +++ b/package.json @@ -69,6 +69,7 @@ "@types/copy-webpack-plugin": "^4.4.1", "@types/express": "^4.16.0", "@types/glob": "^5.0.35", + "@types/inquirer": "^0.0.42", "@types/istanbul": "^0.4.30", "@types/jasmine": "^2.8.8", "@types/loader-utils": "^1.1.3", diff --git a/packages/angular/cli/commands/generate-impl.ts b/packages/angular/cli/commands/generate-impl.ts index 204bee6d48db..7e2580751137 100644 --- a/packages/angular/cli/commands/generate-impl.ts +++ b/packages/angular/cli/commands/generate-impl.ts @@ -52,10 +52,11 @@ export class GenerateCommand extends SchematicCommand { return this.runSchematic({ collectionName, schematicName, - schematicOptions: options, + schematicOptions: this.removeLocalOptions(options), debug: options.debug, dryRun: options.dryRun, force: options.force, + interactive: options.interactive, }); } @@ -95,4 +96,11 @@ export class GenerateCommand extends SchematicCommand { this.logger.info(terminal.cyan(` ng generate --help`)); } } + + private removeLocalOptions(options: any): any { + const opts = Object.assign({}, options); + delete opts.interactive; + + return opts; + } } diff --git a/packages/angular/cli/commands/generate.json b/packages/angular/cli/commands/generate.json index 95c6c8a46e90..3a784e3d47b9 100644 --- a/packages/angular/cli/commands/generate.json +++ b/packages/angular/cli/commands/generate.json @@ -30,6 +30,11 @@ "default": false, "aliases": ["f"], "description": "Forces overwriting of files." + }, + "interactive": { + "type": "boolean", + "default": "true", + "description": "Disables interactive inputs (i.e., prompts)." } }, "required": [ diff --git a/packages/angular/cli/commands/new-impl.ts b/packages/angular/cli/commands/new-impl.ts index b8663199c342..c9229a2e1b37 100644 --- a/packages/angular/cli/commands/new-impl.ts +++ b/packages/angular/cli/commands/new-impl.ts @@ -50,15 +50,14 @@ export class NewCommand extends SchematicCommand { // Ensure skipGit has a boolean value. options.skipGit = options.skipGit === undefined ? false : options.skipGit; - options = this.removeLocalOptions(options); - return this.runSchematic({ collectionName: collectionName, schematicName: this.schematicName, - schematicOptions: options, + schematicOptions: this.removeLocalOptions(options), debug: options.debug, dryRun: options.dryRun, force: options.force, + interactive: options.interactive, }); } @@ -72,6 +71,7 @@ export class NewCommand extends SchematicCommand { const opts = Object.assign({}, options); delete opts.verbose; delete opts.collection; + delete opts.interactive; return opts; } diff --git a/packages/angular/cli/commands/new.json b/packages/angular/cli/commands/new.json index cf8104029909..b1f8db9f577b 100644 --- a/packages/angular/cli/commands/new.json +++ b/packages/angular/cli/commands/new.json @@ -33,6 +33,11 @@ "default": false, "aliases": ["v"], "description": "Adds more details to output logging." + }, + "interactive": { + "type": "boolean", + "default": "true", + "description": "Disables interactive inputs (i.e., prompts)." } }, "required": [] diff --git a/packages/angular/cli/models/schematic-command.ts b/packages/angular/cli/models/schematic-command.ts index 9f78d5b450ed..75714606c250 100644 --- a/packages/angular/cli/models/schematic-command.ts +++ b/packages/angular/cli/models/schematic-command.ts @@ -37,6 +37,7 @@ import { NodeWorkflow, validateOptionsWithSchema, } from '@angular-devkit/schematics/tools'; +import * as inquirer from 'inquirer'; import { take } from 'rxjs/operators'; import { WorkspaceLoader } from '../models/workspace-loader'; import { @@ -60,6 +61,7 @@ export interface RunSchematicOptions { dryRun: boolean; force: boolean; showNothingDone?: boolean; + interactive?: boolean; } export interface GetOptionsOptions { @@ -279,6 +281,49 @@ export abstract class SchematicCommand extends Command { return undefined; }); + if (options.interactive !== false && process.stdout.isTTY) { + workflow.registry.usePromptProvider((definitions: Array) => { + const questions: inquirer.Questions = definitions.map(definition => { + const question: inquirer.Question = { + name: definition.id, + message: definition.message, + default: definition.default, + }; + + const validator = definition.validator; + if (validator) { + question.validate = input => validator(input); + } + + switch (definition.type) { + case 'confirmation': + question.type = 'confirm'; + break; + case 'list': + question.type = 'list'; + question.choices = definition.items && definition.items.map(item => { + if (typeof item == 'string') { + return item; + } else { + return { + name: item.label, + value: item.value, + }; + } + }); + break; + default: + question.type = definition.type; + break; + } + + return question; + }); + + return inquirer.prompt(questions); + }); + } + workflow.reporter.subscribe((event: DryRunEvent) => { nothingDone = false; diff --git a/packages/angular/cli/package.json b/packages/angular/cli/package.json index 38b778a03d0d..6c658e474a02 100644 --- a/packages/angular/cli/package.json +++ b/packages/angular/cli/package.json @@ -35,9 +35,9 @@ "@angular-devkit/schematics": "0.0.0", "@schematics/angular": "0.0.0", "@schematics/update": "0.0.0", + "inquirer": "^6.1.0", "json-schema-traverse": "^0.4.1", "opn": "^5.3.0", - "json-schema-traverse": "^0.4.1", "rxjs": "^6.0.0", "semver": "^5.1.0", "symbol-observable": "^1.2.0", diff --git a/yarn.lock b/yarn.lock index d5e23db55b07..88e1463881ab 100644 --- a/yarn.lock +++ b/yarn.lock @@ -181,6 +181,13 @@ "@types/events" "*" "@types/node" "*" +"@types/inquirer@^0.0.42": + version "0.0.42" + resolved "https://registry.yarnpkg.com/@types/inquirer/-/inquirer-0.0.42.tgz#2310fd8623053bead72247adae416aa32bacdf0c" + dependencies: + "@types/rx" "*" + "@types/through" "*" + "@types/istanbul@^0.4.30": version "0.4.30" resolved "https://registry.yarnpkg.com/@types/istanbul/-/istanbul-0.4.30.tgz#073159320ab3296b2cfeb481f756a1f8f4c9c8e4" @@ -237,6 +244,94 @@ "@types/node" "*" "@types/tough-cookie" "*" +"@types/rx-core-binding@*": + version "4.0.4" + resolved "https://registry.yarnpkg.com/@types/rx-core-binding/-/rx-core-binding-4.0.4.tgz#d969d32f15a62b89e2862c17b3ee78fe329818d3" + dependencies: + "@types/rx-core" "*" + +"@types/rx-core@*": + version "4.0.3" + resolved "https://registry.yarnpkg.com/@types/rx-core/-/rx-core-4.0.3.tgz#0b3354b1238cedbe2b74f6326f139dbc7a591d60" + +"@types/rx-lite-aggregates@*": + version "4.0.3" + resolved "https://registry.yarnpkg.com/@types/rx-lite-aggregates/-/rx-lite-aggregates-4.0.3.tgz#6efb2b7f3d5f07183a1cb2bd4b1371d7073384c2" + dependencies: + "@types/rx-lite" "*" + +"@types/rx-lite-async@*": + version "4.0.2" + resolved "https://registry.yarnpkg.com/@types/rx-lite-async/-/rx-lite-async-4.0.2.tgz#27fbf0caeff029f41e2d2aae638b05e91ceb600c" + dependencies: + "@types/rx-lite" "*" + +"@types/rx-lite-backpressure@*": + version "4.0.3" + resolved "https://registry.yarnpkg.com/@types/rx-lite-backpressure/-/rx-lite-backpressure-4.0.3.tgz#05abb19bdf87cc740196c355e5d0b37bb50b5d56" + dependencies: + "@types/rx-lite" "*" + +"@types/rx-lite-coincidence@*": + version "4.0.3" + resolved "https://registry.yarnpkg.com/@types/rx-lite-coincidence/-/rx-lite-coincidence-4.0.3.tgz#80bd69acc4054a15cdc1638e2dc8843498cd85c0" + dependencies: + "@types/rx-lite" "*" + +"@types/rx-lite-experimental@*": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@types/rx-lite-experimental/-/rx-lite-experimental-4.0.1.tgz#c532f5cbdf3f2c15da16ded8930d1b2984023cbd" + dependencies: + "@types/rx-lite" "*" + +"@types/rx-lite-joinpatterns@*": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@types/rx-lite-joinpatterns/-/rx-lite-joinpatterns-4.0.1.tgz#f70fe370518a8432f29158cc92ffb56b4e4afc3e" + dependencies: + "@types/rx-lite" "*" + +"@types/rx-lite-testing@*": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@types/rx-lite-testing/-/rx-lite-testing-4.0.1.tgz#21b19d11f4dfd6ffef5a9d1648e9c8879bfe21e9" + dependencies: + "@types/rx-lite-virtualtime" "*" + +"@types/rx-lite-time@*": + version "4.0.3" + resolved "https://registry.yarnpkg.com/@types/rx-lite-time/-/rx-lite-time-4.0.3.tgz#0eda65474570237598f3448b845d2696f2dbb1c4" + dependencies: + "@types/rx-lite" "*" + +"@types/rx-lite-virtualtime@*": + version "4.0.3" + resolved "https://registry.yarnpkg.com/@types/rx-lite-virtualtime/-/rx-lite-virtualtime-4.0.3.tgz#4b30cacd0fe2e53af29f04f7438584c7d3959537" + dependencies: + "@types/rx-lite" "*" + +"@types/rx-lite@*": + version "4.0.5" + resolved "https://registry.yarnpkg.com/@types/rx-lite/-/rx-lite-4.0.5.tgz#b3581525dff69423798daa9a0d33c1e66a5e8c4c" + dependencies: + "@types/rx-core" "*" + "@types/rx-core-binding" "*" + +"@types/rx@*": + version "4.1.1" + resolved "https://registry.yarnpkg.com/@types/rx/-/rx-4.1.1.tgz#598fc94a56baed975f194574e0f572fd8e627a48" + dependencies: + "@types/rx-core" "*" + "@types/rx-core-binding" "*" + "@types/rx-lite" "*" + "@types/rx-lite-aggregates" "*" + "@types/rx-lite-async" "*" + "@types/rx-lite-backpressure" "*" + "@types/rx-lite-coincidence" "*" + "@types/rx-lite-experimental" "*" + "@types/rx-lite-joinpatterns" "*" + "@types/rx-lite-testing" "*" + "@types/rx-lite-time" "*" + "@types/rx-lite-virtualtime" "*" + "@types/selenium-webdriver@^3.0.0": version "3.0.10" resolved "https://registry.yarnpkg.com/@types/selenium-webdriver/-/selenium-webdriver-3.0.10.tgz#e98cc6f05b4b436277671c784ee2f9d05a634f9b" @@ -270,6 +365,12 @@ version "1.0.4" resolved "https://registry.yarnpkg.com/@types/tapable/-/tapable-1.0.4.tgz#b4ffc7dc97b498c969b360a41eee247f82616370" +"@types/through@*": + version "0.0.29" + resolved "https://registry.yarnpkg.com/@types/through/-/through-0.0.29.tgz#72943aac922e179339c651fa34a4428a4d722f93" + dependencies: + "@types/node" "*" + "@types/tough-cookie@*": version "2.3.3" resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-2.3.3.tgz#7f226d67d654ec9070e755f46daebf014628e9d9" @@ -553,6 +654,10 @@ ansi-align@^2.0.0: dependencies: string-width "^2.0.0" +ansi-escapes@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.1.0.tgz#f73207bb81207d75fd6c83f125af26eea378ca30" + ansi-html@0.0.7: version "0.0.7" resolved "https://registry.yarnpkg.com/ansi-html/-/ansi-html-0.0.7.tgz#813584021962a9e9e6fd039f940d12f56ca7859e" @@ -1307,7 +1412,7 @@ chalk@^1.1.1, chalk@^1.1.3: strip-ansi "^3.0.0" supports-color "^2.0.0" -chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.0, chalk@^2.3.1, chalk@^2.3.2, chalk@^2.4.1: +chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.0, chalk@^2.3.1, chalk@^2.3.2, chalk@^2.4.1: version "2.4.1" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.1.tgz#18c49ab16a037b6eb0152cc83e3471338215b66e" dependencies: @@ -1315,6 +1420,10 @@ chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.0, chalk@^2.3.1, chalk@^2.3.2, chalk@^2.4 escape-string-regexp "^1.0.5" supports-color "^5.3.0" +chardet@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.5.0.tgz#fe3ac73c00c3d865ffcc02a0682e2c20b6a06029" + chokidar@^1.4.2: version "1.7.0" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-1.7.0.tgz#798e689778151c8076b4b360e5edd28cda2bb468" @@ -1403,6 +1512,16 @@ cli-boxes@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-1.0.0.tgz#4fa917c3e59c94a004cd61f8ee509da651687143" +cli-cursor@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-2.1.0.tgz#b35dac376479facc3e94747d41d0d0f5238ffcb5" + dependencies: + restore-cursor "^2.0.0" + +cli-width@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.0.tgz#ff19ede8a9a5e579324147b0c11f0fbcbabed639" + cliui@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/cliui/-/cliui-2.1.0.tgz#4b475760ff80264c762c3a1719032e91c7fea0d1" @@ -2653,6 +2772,14 @@ extend@^3.0.0, extend@~3.0.1: version "3.0.2" resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" +external-editor@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.0.1.tgz#fc9638c4d7cde4f0bb82b12307a1a23912c492e3" + dependencies: + chardet "^0.5.0" + iconv-lite "^0.4.22" + tmp "^0.0.33" + extglob@^0.3.1: version "0.3.2" resolved "https://registry.yarnpkg.com/extglob/-/extglob-0.3.2.tgz#2e18ff3d2f49ab2765cec9023f011daa8d8349a1" @@ -2724,6 +2851,12 @@ figgy-pudding@^3.1.0: version "3.4.1" resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.4.1.tgz#af66da1991fa2f94ff7f33b545a38ea4b3869696" +figures@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/figures/-/figures-2.0.0.tgz#3ab1a2d2a62c8bfb431a0c94cb797a2fce27c962" + dependencies: + escape-string-regexp "^1.0.5" + file-loader@^1.1.11: version "1.1.11" resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-1.1.11.tgz#6fe886449b0f2a936e43cabaac0cdbfb369506f8" @@ -3463,6 +3596,12 @@ iconv-lite@0.4.23, iconv-lite@^0.4.4: dependencies: safer-buffer ">= 2.1.2 < 3" +iconv-lite@^0.4.22: + version "0.4.24" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + dependencies: + safer-buffer ">= 2.1.2 < 3" + ieee754@^1.1.11, ieee754@^1.1.4: version "1.1.12" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.12.tgz#50bf24e5b9c8bb98af4964c941cdb0918da7b60b" @@ -3561,6 +3700,24 @@ injection-js@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/injection-js/-/injection-js-2.2.1.tgz#a8d6a085b2f0b8d8650f6f4487f6abb8cc0d67ce" +inquirer@^6.1.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-6.2.0.tgz#51adcd776f661369dc1e894859c2560a224abdd8" + dependencies: + ansi-escapes "^3.0.0" + chalk "^2.0.0" + cli-cursor "^2.1.0" + cli-width "^2.0.0" + external-editor "^3.0.0" + figures "^2.0.0" + lodash "^4.17.10" + mute-stream "0.0.7" + run-async "^2.2.0" + rxjs "^6.1.0" + string-width "^2.1.0" + strip-ansi "^4.0.0" + through "^2.3.6" + internal-ip@1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/internal-ip/-/internal-ip-1.2.0.tgz#ae9fbf93b984878785d50a8de1b356956058cf5c" @@ -4412,7 +4569,7 @@ lodash.templatesettings@^4.0.0: dependencies: lodash._reinterpolate "~3.0.0" -lodash@^4.0.0, lodash@^4.17.10, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.1, lodash@^4.3.0, lodash@^4.5.0, lodash@~4.17.10: +lodash@^4.0.0, lodash@^4.17.10, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.1, lodash@^4.5.0, lodash@~4.17.10: version "4.17.10" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.10.tgz#1b7793cf7259ea38fb3661d4d38b3260af8ae4e7" @@ -4818,6 +4975,10 @@ multicast-dns@^6.0.1: dns-packet "^1.3.1" thunky "^1.0.2" +mute-stream@0.0.7: + version "0.0.7" + resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" + nan@^2.10.0, nan@^2.9.2: version "2.10.0" resolved "https://registry.yarnpkg.com/nan/-/nan-2.10.0.tgz#96d0cd610ebd58d4b4de9cc0c6828cda99c7548f" @@ -5193,6 +5354,12 @@ one-time@0.0.4: version "0.0.4" resolved "https://registry.yarnpkg.com/one-time/-/one-time-0.0.4.tgz#f8cdf77884826fe4dff93e3a9cc37b1e4480742e" +onetime@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-2.0.1.tgz#067428230fd67443b2794b22bba528b6867962d4" + dependencies: + mimic-fn "^1.0.0" + opn@^5.1.0, opn@^5.3.0: version "5.3.0" resolved "https://registry.yarnpkg.com/opn/-/opn-5.3.0.tgz#64871565c863875f052cfdf53d3e3cb5adb53b1c" @@ -6116,6 +6283,13 @@ responselike@1.0.2: dependencies: lowercase-keys "^1.0.0" +restore-cursor@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-2.0.0.tgz#9f7ee287f82fd326d4fd162923d62129eee0dfaf" + dependencies: + onetime "^2.0.0" + signal-exit "^3.0.2" + ret@~0.1.10: version "0.1.15" resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" @@ -6185,13 +6359,19 @@ rollup@^0.64.0: "@types/estree" "0.0.39" "@types/node" "*" +run-async@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.3.0.tgz#0371ab4ae0bdd720d4166d7dfda64ff7a445a6c0" + dependencies: + is-promise "^2.1.0" + run-queue@^1.0.0, run-queue@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/run-queue/-/run-queue-1.0.3.tgz#e848396f057d223f24386924618e25694161ec47" dependencies: aproba "^1.1.1" -rxjs@6.0.0, rxjs@^6.0.0: +rxjs@6.0.0, rxjs@^6.0.0, rxjs@^6.1.0: version "6.0.0" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.0.0.tgz#d647e029b5854617f994c82fe57a4c6747b352da" dependencies: @@ -6830,7 +7010,7 @@ string-width@^1.0.1, string-width@^1.0.2: is-fullwidth-code-point "^1.0.0" strip-ansi "^3.0.0" -"string-width@^1.0.2 || 2", string-width@^2.0.0, string-width@^2.1.1: +"string-width@^1.0.2 || 2", string-width@^2.0.0, string-width@^2.1.0, string-width@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" dependencies: @@ -7010,7 +7190,7 @@ through2@^2.0.0, through2@^2.0.2, through2@^2.0.3: readable-stream "^2.1.5" xtend "~4.0.1" -through@2, "through@>=2.2.7 <3", through@X.X.X: +through@2, "through@>=2.2.7 <3", through@X.X.X, through@^2.3.6: version "2.3.8" resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" @@ -7034,7 +7214,7 @@ tmp@0.0.30: dependencies: os-tmpdir "~1.0.1" -tmp@0.0.33, tmp@0.0.x: +tmp@0.0.33, tmp@0.0.x, tmp@^0.0.33: version "0.0.33" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" dependencies: From cdbc1988f28ce4f2c2509efb9ebd10e727facbdd Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Fri, 17 Aug 2018 11:42:18 -0400 Subject: [PATCH 0017/1703] feat(@angular-devkit/schematics): allow schematic rules to control interactivity --- .../schematics/src/engine/engine.ts | 13 +++++++++++- .../schematics/src/engine/interface.ts | 9 ++++++++ .../schematics/src/engine/schematic.ts | 6 ++++-- .../schematics/src/rules/schematic.ts | 21 ++++++++++++------- .../schematics/tools/fallback-engine-host.ts | 9 ++++---- .../tools/file-system-engine-host-base.ts | 9 ++++++-- .../tools/schema-option-transform.ts | 12 ++++++++--- 7 files changed, 59 insertions(+), 20 deletions(-) diff --git a/packages/angular_devkit/schematics/src/engine/engine.ts b/packages/angular_devkit/schematics/src/engine/engine.ts index 7bcff545af15..4222a904ce11 100644 --- a/packages/angular_devkit/schematics/src/engine/engine.ts +++ b/packages/angular_devkit/schematics/src/engine/engine.ts @@ -18,6 +18,7 @@ import { CollectionDescription, Engine, EngineHost, + ExecutionOptions, Schematic, SchematicContext, SchematicDescription, @@ -215,12 +216,20 @@ export class SchematicEngine, parent?: Partial>, + executionOptions?: Partial, ): TypedSchematicContext { // Check for inconsistencies. if (parent && parent.engine && parent.engine !== this) { throw new SchematicEngineConflictingException(); } + let interactive = true; + if (executionOptions && executionOptions.interactive != undefined) { + interactive = executionOptions.interactive; + } else if (parent && parent.interactive != undefined) { + interactive = parent.interactive; + } + let context: TypedSchematicContext = { debug: parent && parent.debug || false, engine: this, @@ -229,6 +238,7 @@ export class SchematicEngine( schematic: Schematic, options: OptionT, + context?: TypedSchematicContext, ): Observable { - return this._host.transformOptions(schematic.description, options); + return this._host.transformOptions(schematic.description, options, context); } createSourceFromUrl(url: Url, context: TypedSchematicContext): Source { diff --git a/packages/angular_devkit/schematics/src/engine/interface.ts b/packages/angular_devkit/schematics/src/engine/interface.ts index 2bcb6b9a6c33..0e43788c2340 100644 --- a/packages/angular_devkit/schematics/src/engine/interface.ts +++ b/packages/angular_devkit/schematics/src/engine/interface.ts @@ -41,6 +41,10 @@ export interface TaskInfo { readonly context: SchematicContext; } +export interface ExecutionOptions { + interactive: boolean; +} + /** * The description (metadata) of a collection. This type contains every information the engine * needs to run. The CollectionMetadataT type parameter contains additional metadata that you @@ -92,6 +96,7 @@ export interface EngineHost( schematic: SchematicDescription, options: OptionT, + context?: TypedSchematicContext, ): Observable; transformContext( context: TypedSchematicContext, @@ -118,6 +123,7 @@ export interface Engine, parent?: Partial>, + executionOptions?: Partial, ): TypedSchematicContext; createSchematic( name: string, @@ -130,6 +136,7 @@ export interface Engine( schematic: Schematic, options: OptionT, + context?: TypedSchematicContext, ): Observable; executePostTasks(): Observable; @@ -166,6 +173,7 @@ export interface Schematic, parentContext?: Partial>, + executionOptions?: Partial, ): Observable; } @@ -181,6 +189,7 @@ export interface TypedSchematicContext; readonly strategy: MergeStrategy; + readonly interactive: boolean; addTask(task: TaskConfigurationGenerator, dependencies?: Array): TaskId; } diff --git a/packages/angular_devkit/schematics/src/engine/schematic.ts b/packages/angular_devkit/schematics/src/engine/schematic.ts index 932fdd76285a..fd3acbf774df 100644 --- a/packages/angular_devkit/schematics/src/engine/schematic.ts +++ b/packages/angular_devkit/schematics/src/engine/schematic.ts @@ -13,6 +13,7 @@ import { Tree } from '../tree/interface'; import { Collection, Engine, + ExecutionOptions, RuleFactory, Schematic, SchematicDescription, @@ -46,13 +47,14 @@ export class SchematicImpl, parentContext?: Partial>, + executionOptions?: Partial, ): Observable { - const context = this._engine.createContext(this, parentContext); + const context = this._engine.createContext(this, parentContext, executionOptions); return host .pipe( first(), - concatMap(tree => this._engine.transformOptions(this, options).pipe( + concatMap(tree => this._engine.transformOptions(this, options, context).pipe( map(o => [tree, o]), )), concatMap(([tree, transformedOptions]: [Tree, OptionT]) => { diff --git a/packages/angular_devkit/schematics/src/rules/schematic.ts b/packages/angular_devkit/schematics/src/rules/schematic.ts index 7d7a01cfd258..6885ad9bacb6 100644 --- a/packages/angular_devkit/schematics/src/rules/schematic.ts +++ b/packages/angular_devkit/schematics/src/rules/schematic.ts @@ -7,7 +7,7 @@ */ import { of as observableOf } from 'rxjs'; import { last, map } from 'rxjs/operators'; -import { Rule, SchematicContext } from '../engine/interface'; +import { ExecutionOptions, Rule, SchematicContext } from '../engine/interface'; import { MergeStrategy, Tree } from '../tree/interface'; import { branch } from '../tree/static'; @@ -19,14 +19,17 @@ import { branch } from '../tree/static'; * @param schematicName The name of the schematic to run. * @param options The options to pass as input to the RuleFactory. */ -export function externalSchematic(collectionName: string, - schematicName: string, - options: OptionT): Rule { +export function externalSchematic( + collectionName: string, + schematicName: string, + options: OptionT, + executionOptions?: Partial, +): Rule { return (input: Tree, context: SchematicContext) => { const collection = context.engine.createCollection(collectionName); const schematic = collection.createSchematic(schematicName); - return schematic.call(options, observableOf(branch(input)), context); + return schematic.call(options, observableOf(branch(input)), context, executionOptions); }; } @@ -37,12 +40,16 @@ export function externalSchematic(collectionName: string * @param schematicName The name of the schematic to run. * @param options The options to pass as input to the RuleFactory. */ -export function schematic(schematicName: string, options: OptionT): Rule { +export function schematic( + schematicName: string, + options: OptionT, + executionOptions?: Partial, +): Rule { return (input: Tree, context: SchematicContext) => { const collection = context.schematic.collection; const schematic = collection.createSchematic(schematicName, true); - return schematic.call(options, observableOf(branch(input)), context).pipe( + return schematic.call(options, observableOf(branch(input)), context, executionOptions).pipe( last(), map(x => { // We allow overwrite conflict here because they're the only merge conflict we particularly diff --git a/packages/angular_devkit/schematics/tools/fallback-engine-host.ts b/packages/angular_devkit/schematics/tools/fallback-engine-host.ts index a9c327cbc7e0..7dd9849bef24 100644 --- a/packages/angular_devkit/schematics/tools/fallback-engine-host.ts +++ b/packages/angular_devkit/schematics/tools/fallback-engine-host.ts @@ -31,10 +31,6 @@ export type FallbackSchematicDescription = { }; export type FallbackContext = TypedSchematicContext; -export declare type OptionTransform = ( - schematic: SchematicDescription, - options: T, -) => Observable; /** @@ -93,9 +89,12 @@ export class FallbackEngineHost implements EngineHost<{}, {}> { transformOptions( schematic: SchematicDescription, options: OptionT, + context?: FallbackContext, ): Observable { return (observableOf(options) - .pipe(...this._hosts.map(host => mergeMap(opt => host.transformOptions(schematic, opt)))) + .pipe(...this._hosts + .map(host => mergeMap(opt => host.transformOptions(schematic, opt, context))), + ) ) as {} as Observable; } diff --git a/packages/angular_devkit/schematics/tools/file-system-engine-host-base.ts b/packages/angular_devkit/schematics/tools/file-system-engine-host-base.ts index bc5d19fac843..7c3ed9c808d4 100644 --- a/packages/angular_devkit/schematics/tools/file-system-engine-host-base.ts +++ b/packages/angular_devkit/schematics/tools/file-system-engine-host-base.ts @@ -39,7 +39,11 @@ import { readJsonFile } from './file-system-utility'; export declare type OptionTransform - = (schematic: FileSystemSchematicDescription, options: T) => Observable; + = ( + schematic: FileSystemSchematicDescription, + options: T, + context?: FileSystemSchematicContext, + ) => Observable; export class CollectionCannotBeResolvedException extends BaseException { @@ -269,11 +273,12 @@ export abstract class FileSystemEngineHostBase implements transformOptions( schematic: FileSystemSchematicDesc, options: OptionT, + context?: FileSystemSchematicContext, ): Observable { return (observableOf(options) .pipe( ...this._transforms.map(tFn => mergeMap(opt => { - const newOptions = tFn(schematic, opt); + const newOptions = tFn(schematic, opt, context); if (isObservable(newOptions)) { return newOptions; } else { diff --git a/packages/angular_devkit/schematics/tools/schema-option-transform.ts b/packages/angular_devkit/schematics/tools/schema-option-transform.ts index 1827b1106f62..1c7491ecb60a 100644 --- a/packages/angular_devkit/schematics/tools/schema-option-transform.ts +++ b/packages/angular_devkit/schematics/tools/schema-option-transform.ts @@ -8,7 +8,7 @@ import { deepCopy, schema } from '@angular-devkit/core'; import { Observable, of as observableOf } from 'rxjs'; import { first, map, mergeMap } from 'rxjs/operators'; -import { FileSystemSchematicDescription } from './description'; +import { FileSystemSchematicContext, FileSystemSchematicDescription } from './description'; export class InvalidInputOptions extends schema.SchemaValidationException { constructor(options: T, errors: schema.SchemaValidatorError[]) { @@ -21,16 +21,22 @@ export class InvalidInputOptions extends schema.SchemaValidationExceptio // This can only be used in NodeJS. export function validateOptionsWithSchema(registry: schema.SchemaRegistry) { - return (schematic: FileSystemSchematicDescription, options: T): Observable => { + return ( + schematic: FileSystemSchematicDescription, + options: T, + context?: FileSystemSchematicContext, + ): Observable => { // Prevent a schematic from changing the options object by making a copy of it. options = deepCopy(options); + const withPrompts = context ? context.interactive : true; + if (schematic.schema && schematic.schemaJson) { // Make a deep copy of options. return registry .compile(schematic.schemaJson) .pipe( - mergeMap(validator => validator(options)), + mergeMap(validator => validator(options, { withPrompts })), first(), map(result => { if (!result.success) { From d299ef7ff710b5408dee0c1d0f80bf5c8660967a Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Tue, 21 Aug 2018 13:45:57 -0400 Subject: [PATCH 0018/1703] fix(@angular-devkit/core): correctly resolve schema references --- .../core/src/json/schema/registry.ts | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/packages/angular_devkit/core/src/json/schema/registry.ts b/packages/angular_devkit/core/src/json/schema/registry.ts index 27e82270ad69..b16d79495839 100644 --- a/packages/angular_devkit/core/src/json/schema/registry.ts +++ b/packages/angular_devkit/core/src/json/schema/registry.ts @@ -171,23 +171,21 @@ export class CoreSchemaRegistry implements SchemaRegistry { ref: string, validate: ajv.ValidateFunction, ): { context?: ajv.ValidateFunction, schema?: JsonObject } { - if (!validate) { + if (!validate || !validate.refs || !validate.refVal || !ref) { return {}; } - const refHash = ref.split('#', 2)[1]; - const refUrl = ref.startsWith('#') ? ref : ref.split('#', 1); - - if (!ref.startsWith('#')) { - // tslint:disable-next-line:no-any - validate = (validate.refVal as any)[(validate.refs as any)[refUrl[0]]]; - } - if (validate && refHash) { - // tslint:disable-next-line:no-any - validate = (validate.refVal as any)[(validate.refs as any)['#' + refHash]]; + // tslint:disable-next-line:no-any + const id = (validate.schema as any).$id || (validate.schema as any).id; + let fullReference = (ref[0] === '#' && id) ? id + ref : ref; + if (fullReference.endsWith('#')) { + fullReference = fullReference.slice(0, -1); } - return { context: validate, schema: validate && validate.schema as JsonObject }; + // tslint:disable-next-line:no-any + const context = validate.refVal[(validate.refs as any)[fullReference]]; + + return { context, schema: context && context.schema as JsonObject }; } compile(schema: JsonObject): Observable { From e177849f44621bb2fbbe96a19d32213c4050fedc Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Tue, 21 Aug 2018 21:31:06 -0400 Subject: [PATCH 0019/1703] fix(@angular/cli): correct update command packages option type --- packages/angular/cli/commands/update.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/angular/cli/commands/update.json b/packages/angular/cli/commands/update.json index dd66520136e0..7a47adf3c7bb 100644 --- a/packages/angular/cli/commands/update.json +++ b/packages/angular/cli/commands/update.json @@ -11,7 +11,10 @@ "type": "object", "properties": { "packages": { - "type": "string", + "type": "array", + "items": { + "type": "string" + }, "description": "The names of package(s) to update", "$default": { "$source": "argv" From e0882a754995af7ee1d4c875de4642f43504a79a Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Wed, 22 Aug 2018 12:28:24 -0400 Subject: [PATCH 0020/1703] fix(@angular-devkit/core): make workspace projects field not required It has a default which makes the required aspect void. Global files also do not have projects. --- .../angular_devkit/core/src/workspace/workspace-schema.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/angular_devkit/core/src/workspace/workspace-schema.json b/packages/angular_devkit/core/src/workspace/workspace-schema.json index 9fd692661630..a030a6753d55 100644 --- a/packages/angular_devkit/core/src/workspace/workspace-schema.json +++ b/packages/angular_devkit/core/src/workspace/workspace-schema.json @@ -48,8 +48,7 @@ }, "additionalProperties": false, "required": [ - "version", - "projects" + "version" ], "definitions": { "project": { From fd6bba38a008c6ff89584c3edf8ff5ab326f51d1 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Wed, 22 Aug 2018 13:03:32 -0400 Subject: [PATCH 0021/1703] fix(@angular/cli): properly type config command value option --- packages/angular/cli/commands/config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/angular/cli/commands/config.json b/packages/angular/cli/commands/config.json index 0caaa4f5415a..cdcce065041c 100644 --- a/packages/angular/cli/commands/config.json +++ b/packages/angular/cli/commands/config.json @@ -20,7 +20,7 @@ } }, "value": { - "type": "string", + "type": ["string", "number", "boolean"], "description": "The new value to be set.", "$default": { "$source": "argv", From 9af2368481bc3c869f138c942e0177fa9e2c2c5a Mon Sep 17 00:00:00 2001 From: clydin <19598772+clydin@users.noreply.github.com> Date: Wed, 29 Aug 2018 12:26:30 -0400 Subject: [PATCH 0022/1703] ci: continue even if lint or validate fail (#12011) --- .circleci/config.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index c02a15a576f5..bab56e835e84 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -140,12 +140,10 @@ workflows: - install - build: requires: - - lint - - validate + - install - build-bazel: requires: - - lint - - validate + - build - test: requires: - build @@ -167,6 +165,7 @@ workflows: requires: - test - build + - e2e-cli - snapshot_publish filters: tags: From 67e32a8f2536132e594a00f28ebc7aebb5b8949f Mon Sep 17 00:00:00 2001 From: clydin <19598772+clydin@users.noreply.github.com> Date: Wed, 29 Aug 2018 12:26:52 -0400 Subject: [PATCH 0023/1703] fix(@angular-devkit/build-angular): improve bundle size value parsing (#12026) Fixes #12013 --- .../plugins/bundle-budget.ts | 36 ++++----- .../utilities/bundle-calculator.ts | 74 +++++++++-------- .../utilities/bundle-calculator_spec.ts | 80 +++++++++++++++++++ 3 files changed, 133 insertions(+), 57 deletions(-) create mode 100644 packages/angular_devkit/build_angular/src/angular-cli-files/utilities/bundle-calculator_spec.ts diff --git a/packages/angular_devkit/build_angular/src/angular-cli-files/plugins/bundle-budget.ts b/packages/angular_devkit/build_angular/src/angular-cli-files/plugins/bundle-budget.ts index 6400ce9491b9..4ee676886846 100644 --- a/packages/angular_devkit/build_angular/src/angular-cli-files/plugins/bundle-budget.ts +++ b/packages/angular_devkit/build_angular/src/angular-cli-files/plugins/bundle-budget.ts @@ -1,6 +1,3 @@ -// tslint:disable -// TODO: cleanup this file, it's copied as is from Angular CLI. - /** * @license * Copyright Google Inc. All Rights Reserved. @@ -8,9 +5,9 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ - -import { Size, calculateBytes, calculateSizes } from '../utilities/bundle-calculator'; +import { Compiler, compilation } from 'webpack'; import { Budget } from '../../browser/schema'; +import { Size, calculateBytes, calculateSizes } from '../utilities/bundle-calculator'; import { formatSize } from '../utilities/stats'; interface Thresholds { @@ -31,19 +28,20 @@ export interface BundleBudgetPluginOptions { export class BundleBudgetPlugin { constructor(private options: BundleBudgetPluginOptions) { } - apply(compiler: any): void { + apply(compiler: Compiler): void { const { budgets } = this.options; - compiler.hooks.afterEmit.tap('BundleBudgetPlugin', (compilation: any) => { + compiler.hooks.afterEmit.tap('BundleBudgetPlugin', (compilation: compilation.Compilation) => { if (!budgets || budgets.length === 0) { return; } budgets.map(budget => { const thresholds = this.calculate(budget); + return { budget, thresholds, - sizes: calculateSizes(budget, compilation) + sizes: calculateSizes(budget, compilation), }; }) .forEach(budgetCheck => { @@ -62,7 +60,7 @@ export class BundleBudgetPlugin { }); } - private checkMinimum(threshold: number | undefined, size: Size, messages: any) { + private checkMinimum(threshold: number | undefined, size: Size, messages: string[]) { if (threshold) { if (threshold > size.size) { const sizeDifference = formatSize(threshold - size.size); @@ -72,7 +70,7 @@ export class BundleBudgetPlugin { } } - private checkMaximum(threshold: number | undefined, size: Size, messages: any) { + private checkMaximum(threshold: number | undefined, size: Size, messages: string[]) { if (threshold) { if (threshold < size.size) { const sizeDifference = formatSize(size.size - threshold); @@ -83,37 +81,37 @@ export class BundleBudgetPlugin { } private calculate(budget: Budget): Thresholds { - let thresholds: Thresholds = {}; + const thresholds: Thresholds = {}; if (budget.maximumWarning) { - thresholds.maximumWarning = calculateBytes(budget.maximumWarning, budget.baseline, 'pos'); + thresholds.maximumWarning = calculateBytes(budget.maximumWarning, budget.baseline, 1); } if (budget.maximumError) { - thresholds.maximumError = calculateBytes(budget.maximumError, budget.baseline, 'pos'); + thresholds.maximumError = calculateBytes(budget.maximumError, budget.baseline, 1); } if (budget.minimumWarning) { - thresholds.minimumWarning = calculateBytes(budget.minimumWarning, budget.baseline, 'neg'); + thresholds.minimumWarning = calculateBytes(budget.minimumWarning, budget.baseline, -1); } if (budget.minimumError) { - thresholds.minimumError = calculateBytes(budget.minimumError, budget.baseline, 'neg'); + thresholds.minimumError = calculateBytes(budget.minimumError, budget.baseline, -1); } if (budget.warning) { - thresholds.warningLow = calculateBytes(budget.warning, budget.baseline, 'neg'); + thresholds.warningLow = calculateBytes(budget.warning, budget.baseline, -1); } if (budget.warning) { - thresholds.warningHigh = calculateBytes(budget.warning, budget.baseline, 'pos'); + thresholds.warningHigh = calculateBytes(budget.warning, budget.baseline, 1); } if (budget.error) { - thresholds.errorLow = calculateBytes(budget.error, budget.baseline, 'neg'); + thresholds.errorLow = calculateBytes(budget.error, budget.baseline, -1); } if (budget.error) { - thresholds.errorHigh = calculateBytes(budget.error, budget.baseline, 'pos'); + thresholds.errorHigh = calculateBytes(budget.error, budget.baseline, 1); } return thresholds; diff --git a/packages/angular_devkit/build_angular/src/angular-cli-files/utilities/bundle-calculator.ts b/packages/angular_devkit/build_angular/src/angular-cli-files/utilities/bundle-calculator.ts index a396b2f3801d..ca20d6fb386c 100644 --- a/packages/angular_devkit/build_angular/src/angular-cli-files/utilities/bundle-calculator.ts +++ b/packages/angular_devkit/build_angular/src/angular-cli-files/utilities/bundle-calculator.ts @@ -1,6 +1,3 @@ -// tslint:disable -// TODO: cleanup this file, it's copied as is from Angular CLI. - /** * @license * Copyright Google Inc. All Rights Reserved. @@ -11,8 +8,8 @@ import { Budget } from '../../browser/schema'; export interface Compilation { - assets: any; - chunks: any[]; + assets: { [name: string]: { size: () => number } }; + chunks: { name: string, files: string[], isOnlyInitial: () => boolean }[]; warnings: string[]; errors: string[]; } @@ -33,6 +30,7 @@ export function calculateSizes(budget: Budget, compilation: Compilation): Size[] }; const ctor = calculatorMap[budget.type]; const calculator = new ctor(budget, compilation); + return calculator.calculate(); } @@ -52,6 +50,7 @@ class BundleCalculator extends Calculator { .reduce((files, chunk) => [...files, ...chunk.files], []) .map((file: string) => this.compilation.assets[file].size()) .reduce((total: number, size: number) => total + size, 0); + return [{size, label: this.budget.name}]; } } @@ -67,6 +66,7 @@ class InitialCalculator extends Calculator { .filter((file: string) => !file.endsWith('.map')) .map((file: string) => this.compilation.assets[file].size()) .reduce((total: number, size: number) => total + size, 0); + return [{size, label: 'initial'}]; } } @@ -81,6 +81,7 @@ class AllScriptCalculator extends Calculator { .map(key => this.compilation.assets[key]) .map(asset => asset.size()) .reduce((total: number, size: number) => total + size, 0); + return [{size, label: 'total scripts'}]; } } @@ -94,6 +95,7 @@ class AllCalculator extends Calculator { .filter(key => !key.endsWith('.map')) .map(key => this.compilation.assets[key].size()) .reduce((total: number, size: number) => total + size, 0); + return [{size, label: 'total'}]; } } @@ -107,9 +109,10 @@ class AnyScriptCalculator extends Calculator { .filter(key => key.endsWith('.js')) .map(key => { const asset = this.compilation.assets[key]; + return { size: asset.size(), - label: key + label: key, }; }); } @@ -124,9 +127,10 @@ class AnyCalculator extends Calculator { .filter(key => !key.endsWith('.map')) .map(key => { const asset = this.compilation.assets[key]; + return { size: asset.size(), - label: key + label: key, }; }); } @@ -135,39 +139,33 @@ class AnyCalculator extends Calculator { /** * Calculate the bytes given a string value. */ -export function calculateBytes(val: string, baseline?: string, factor?: ('pos' | 'neg')): number { - if (/^\d+$/.test(val)) { - return parseFloat(val); +export function calculateBytes( + input: string, + baseline?: string, + factor: 1 | -1 = 1, +): number { + const matches = input.match(/^\s*(\d+(?:\.\d+)?)\s*(%|(?:[mM]|[kK]|[gG])?[bB])?\s*$/); + if (!matches) { + return NaN; } - if (/^(\d+)%$/.test(val)) { - return calculatePercentBytes(val, baseline, factor); + const baselineBytes = baseline && calculateBytes(baseline) || 0; + + let value = Number(matches[1]); + switch (matches[2] && matches[2].toLowerCase()) { + case '%': + value = baselineBytes * value / 100 * factor; + break; + case 'kb': + value *= 1024; + break; + case 'mb': + value *= 1024 * 1024; + break; + case 'gb': + value *= 1024 * 1024 * 1024; + break; } - const multiplier = getMultiplier(val); - - const numberVal = parseFloat(val.replace(/((k|m|M|)b?)$/, '')); - const baselineVal = baseline ? parseFloat(baseline.replace(/((k|m|M|)b?)$/, '')) : 0; - const baselineMultiplier = baseline ? getMultiplier(baseline) : 1; - const factorMultiplier = factor ? (factor === 'pos' ? 1 : -1) : 1; - - return numberVal * multiplier + baselineVal * baselineMultiplier * factorMultiplier; -} - -function getMultiplier(val: string): number { - if (/^(\d+)b?$/.test(val)) { - return 1; - } else if (/^(\d+)kb$/.test(val)) { - return 1000; - } else if (/^(\d+)(m|M)b$/.test(val)) { - return 1000 * 1000; - } else { - return 1; - } -} - -function calculatePercentBytes(val: string, baseline?: string, factor?: ('pos' | 'neg')): number { - const baselineBytes = calculateBytes(baseline as string); - const percentage = parseFloat(val.replace(/%/g, '')); - return baselineBytes + baselineBytes * percentage / 100 * (factor === 'pos' ? 1 : -1); + return value + baselineBytes; } diff --git a/packages/angular_devkit/build_angular/src/angular-cli-files/utilities/bundle-calculator_spec.ts b/packages/angular_devkit/build_angular/src/angular-cli-files/utilities/bundle-calculator_spec.ts new file mode 100644 index 000000000000..c610706ebe0c --- /dev/null +++ b/packages/angular_devkit/build_angular/src/angular-cli-files/utilities/bundle-calculator_spec.ts @@ -0,0 +1,80 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import { calculateBytes } from './bundle-calculator'; + +describe('bundle-calculator', () => { + it('converts an integer with no postfix', () => { + expect(calculateBytes('0')).toBe(0); + expect(calculateBytes('5')).toBe(5); + expect(calculateBytes('190')).toBe(190); + expect(calculateBytes('92')).toBe(92); + }); + + it('converts a decimal with no postfix', () => { + expect(calculateBytes('3.14')).toBe(3.14); + expect(calculateBytes('0.25')).toBe(0.25); + expect(calculateBytes('90.5')).toBe(90.5); + expect(calculateBytes('25.0')).toBe(25); + }); + + it('converts an integer with kb postfix', () => { + expect(calculateBytes('0kb')).toBe(0); + expect(calculateBytes('5kb')).toBe(5 * 1024); + expect(calculateBytes('190KB')).toBe(190 * 1024); + expect(calculateBytes('92Kb')).toBe(92 * 1024); + expect(calculateBytes('25kB')).toBe(25 * 1024); + }); + + it('converts a decimal with kb postfix', () => { + expect(calculateBytes('3.14kb')).toBe(3.14 * 1024); + expect(calculateBytes('0.25KB')).toBe(0.25 * 1024); + expect(calculateBytes('90.5Kb')).toBe(90.5 * 1024); + expect(calculateBytes('25.0kB')).toBe(25 * 1024); + }); + + it('converts an integer with mb postfix', () => { + expect(calculateBytes('0mb')).toBe(0); + expect(calculateBytes('5mb')).toBe(5 * 1024 * 1024); + expect(calculateBytes('190MB')).toBe(190 * 1024 * 1024); + expect(calculateBytes('92Mb')).toBe(92 * 1024 * 1024); + expect(calculateBytes('25mB')).toBe(25 * 1024 * 1024); + }); + + it('converts a decimal with mb postfix', () => { + expect(calculateBytes('3.14mb')).toBe(3.14 * 1024 * 1024); + expect(calculateBytes('0.25MB')).toBe(0.25 * 1024 * 1024); + expect(calculateBytes('90.5Mb')).toBe(90.5 * 1024 * 1024); + expect(calculateBytes('25.0mB')).toBe(25 * 1024 * 1024); + }); + + it('converts an integer with gb postfix', () => { + expect(calculateBytes('0gb')).toBe(0); + expect(calculateBytes('5gb')).toBe(5 * 1024 * 1024 * 1024); + expect(calculateBytes('190GB')).toBe(190 * 1024 * 1024 * 1024); + expect(calculateBytes('92Gb')).toBe(92 * 1024 * 1024 * 1024); + expect(calculateBytes('25gB')).toBe(25 * 1024 * 1024 * 1024); + }); + + it('converts a decimal with gb postfix', () => { + expect(calculateBytes('3.14gb')).toBe(3.14 * 1024 * 1024 * 1024); + expect(calculateBytes('0.25GB')).toBe(0.25 * 1024 * 1024 * 1024); + expect(calculateBytes('90.5Gb')).toBe(90.5 * 1024 * 1024 * 1024); + expect(calculateBytes('25.0gB')).toBe(25 * 1024 * 1024 * 1024); + }); + + it ('converts a percentage with baseline', () => { + expect(calculateBytes('20%', '1mb')).toBe(1024 * 1024 * 1.2); + expect(calculateBytes('20%', '1mb', -1)).toBe(1024 * 1024 * 0.8); + }); + + it ('supports whitespace', () => { + expect(calculateBytes(' 5kb ')).toBe(5 * 1024); + expect(calculateBytes('0.25 MB')).toBe(0.25 * 1024 * 1024); + expect(calculateBytes(' 20 % ', ' 1 mb ')).toBe(1024 * 1024 * 1.2); + }); +}); From a2342b6a75fee3181397f7a6e78642a62812640e Mon Sep 17 00:00:00 2001 From: Alan Agius Date: Wed, 29 Aug 2018 18:27:21 +0200 Subject: [PATCH 0024/1703] feat(@angular-devkit/build-angular): remove inlining of assets in css (#12027) BREAKING CHANGE: Assets under 10Kib are not longer inlined in css --- .../models/webpack-configs/styles.ts | 41 +++++------- .../test/browser/styles_spec_large.ts | 66 ++---------------- .../e2e/tests/build/styles/inline-urls.ts | 67 ------------------- 3 files changed, 21 insertions(+), 153 deletions(-) delete mode 100644 tests/legacy-cli/e2e/tests/build/styles/inline-urls.ts diff --git a/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/styles.ts b/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/styles.ts index b8d067df5163..353e2b91ffc2 100644 --- a/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/styles.ts +++ b/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/styles.ts @@ -53,8 +53,6 @@ export function getStylesConfig(wco: WebpackConfigOptions) { const extraPlugins: any[] = []; const cssSourceMap = buildOptions.sourceMap; - // Maximum resource size to inline (KiB) - const maximumInlineSize = 10; // Determine hashing format. const hashFormat = getOutputHashFormat(buildOptions.outputHashing as string); // Convert absolute resource URLs to account for base-href and deploy-url. @@ -134,18 +132,7 @@ export function getStylesConfig(wco: WebpackConfigOptions) { return `/${baseHref}/${deployUrl}/${url}`.replace(/\/\/+/g, '/'); } } - }, - { - // TODO: inline .cur if not supporting IE (use browserslist to check) - filter: (asset: PostcssUrlAsset) => { - return maximumInlineSize > 0 && !asset.hash && !asset.absolutePath.endsWith('.cur'); - }, - url: 'inline', - // NOTE: maxSize is in KB - maxSize: maximumInlineSize, - fallback: 'rebase', - }, - { url: 'rebase' }, + } ]), PostcssCliResources({ deployUrl: loader.loaders[loader.loaderIndex].options.ident == 'extracted' ? '' : deployUrl, @@ -196,7 +183,7 @@ export function getStylesConfig(wco: WebpackConfigOptions) { if (chunkNames.length > 0) { // Add plugin to remove hashes from lazy styles. - extraPlugins.push(new RemoveHashPlugin({ chunkNames, hashFormat})); + extraPlugins.push(new RemoveHashPlugin({ chunkNames, hashFormat })); } } @@ -216,7 +203,8 @@ export function getStylesConfig(wco: WebpackConfigOptions) { const baseRules: webpack.Rule[] = [ { test: /\.css$/, use: [] }, { - test: /\.scss$|\.sass$/, use: [{ + test: /\.scss$|\.sass$/, + use: [{ loader: 'sass-loader', options: { implementation: dartSass, @@ -229,7 +217,8 @@ export function getStylesConfig(wco: WebpackConfigOptions) { }] }, { - test: /\.less$/, use: [{ + test: /\.less$/, + use: [{ loader: 'less-loader', options: { sourceMap: cssSourceMap, @@ -239,7 +228,8 @@ export function getStylesConfig(wco: WebpackConfigOptions) { }] }, { - test: /\.styl$/, use: [{ + test: /\.styl$/, + use: [{ loader: 'stylus-loader', options: { sourceMap: cssSourceMap, @@ -251,7 +241,9 @@ export function getStylesConfig(wco: WebpackConfigOptions) { // load component css as raw strings const rules: webpack.Rule[] = baseRules.map(({ test, use }) => ({ - exclude: globalStylePaths, test, use: [ + exclude: globalStylePaths, + test, + use: [ { loader: 'raw-loader' }, { loader: 'postcss-loader', @@ -306,16 +298,17 @@ export function getStylesConfig(wco: WebpackConfigOptions) { } if (buildOptions.extractCss) { - // extract global css from js files into own css file extraPlugins.push( - new MiniCssExtractPlugin({ filename: `[name]${hashFormat.extract}.css` })); - // suppress empty .js files in css only entry points - extraPlugins.push(new SuppressExtractedTextChunksWebpackPlugin()); + // extract global css from js files into own css file + new MiniCssExtractPlugin({ filename: `[name]${hashFormat.extract}.css` }), + // suppress empty .js files in css only entry points + new SuppressExtractedTextChunksWebpackPlugin(), + ); } return { entry: entryPoints, module: { rules }, - plugins: [].concat(extraPlugins as any) + plugins: extraPlugins }; } diff --git a/packages/angular_devkit/build_angular/test/browser/styles_spec_large.ts b/packages/angular_devkit/build_angular/test/browser/styles_spec_large.ts index e6bb8cf866ae..774e1ae96752 100644 --- a/packages/angular_devkit/build_angular/test/browser/styles_spec_large.ts +++ b/packages/angular_devkit/build_angular/test/browser/styles_spec_large.ts @@ -278,62 +278,6 @@ describe('Browser Builder styles', () => { }); }); - it('inlines resources', (done) => { - host.copyFile('src/spectrum.png', 'src/large.png'); - host.writeMultipleFiles({ - 'src/styles.scss': ` - h1 { background: url('./large.png'), - linear-gradient(to bottom, #0e40fa 25%, #0654f4 75%); } - h2 { background: url('./small.svg'); } - p { background: url(./small-id.svg#testID); } - `, - 'src/app/app.component.css': ` - h3 { background: url('../small.svg'); } - h4 { background: url("../large.png"); } - `, - 'src/small.svg': imgSvg, - 'src/small-id.svg': imgSvg, - }); - - const overrides = { - aot: true, - extractCss: true, - styles: [`src/styles.scss`], - }; - - runTargetSpec(host, browserTargetSpec, overrides).pipe( - tap((buildEvent) => expect(buildEvent.success).toBe(true)), - tap(() => { - const fileName = 'dist/styles.css'; - const content = virtualFs.fileBufferToString(host.scopedSync().read(normalize(fileName))); - // Large image should not be inlined, and gradient should be there. - expect(content).toMatch( - /url\(['"]?large\.png['"]?\),\s+linear-gradient\(to bottom, #0e40fa 25%, #0654f4 75%\);/); - // Small image should be inlined. - expect(content).toMatch(/url\(\\?['"]data:image\/svg\+xml/); - // Small image with param should not be inlined. - expect(content).toMatch(/url\(['"]?small-id\.svg#testID['"]?\)/); - }), - tap(() => { - const fileName = 'dist/main.js'; - const content = virtualFs.fileBufferToString(host.scopedSync().read(normalize(fileName))); - // Large image should not be inlined. - expect(content).toMatch(/url\((?:['"]|\\')?large\.png(?:['"]|\\')?\)/); - // Small image should be inlined. - expect(content).toMatch(/url\(\\?['"]data:image\/svg\+xml/); - }), - tap(() => { - expect(host.scopedSync().exists(normalize('dist/small.svg'))).toBe(false); - expect(host.scopedSync().exists(normalize('dist/large.png'))).toBe(true); - expect(host.scopedSync().exists(normalize('dist/small-id.svg'))).toBe(true); - }), - // TODO: find a way to check logger/output for warnings. - // if (stdout.match(/postcss-url: \.+: Can't read file '\.+', ignoring/)) { - // throw new Error('Expected no postcss-url file read warnings.'); - // } - ).toPromise().then(done, done.fail); - }); - it(`supports font-awesome imports`, (done) => { host.writeMultipleFiles({ 'src/styles.scss': ` @@ -407,8 +351,6 @@ describe('Browser Builder styles', () => { h3 { background: url('/assets/component-img-absolute.svg'); } h4 { background: url('../assets/component-img-relative.png'); } `, - // Use a small SVG for the absolute image to help validate that it is being referenced, - // because it is so small it would be inlined usually. 'src/assets/global-img-absolute.svg': imgSvg, 'src/assets/component-img-absolute.svg': imgSvg, }); @@ -427,12 +369,12 @@ describe('Browser Builder styles', () => { expect(styles).toContain(`url('global-img-relative.png')`); expect(main).toContain(`url('/assets/component-img-absolute.svg')`); expect(main).toContain(`url('component-img-relative.png')`); - expect(host.scopedSync().exists(normalize('dist/global-img-absolute.svg'))) - .toBe(false); + expect(host.scopedSync().exists(normalize('dist/assets/global-img-absolute.svg'))) + .toBe(true); expect(host.scopedSync().exists(normalize('dist/global-img-relative.png'))) .toBe(true); - expect(host.scopedSync().exists(normalize('dist/component-img-absolute.svg'))) - .toBe(false); + expect(host.scopedSync().exists(normalize('dist/assets/component-img-absolute.svg'))) + .toBe(true); expect(host.scopedSync().exists(normalize('dist/component-img-relative.png'))) .toBe(true); }), diff --git a/tests/legacy-cli/e2e/tests/build/styles/inline-urls.ts b/tests/legacy-cli/e2e/tests/build/styles/inline-urls.ts deleted file mode 100644 index a1ff323a1067..000000000000 --- a/tests/legacy-cli/e2e/tests/build/styles/inline-urls.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { ng, silentNpm } from '../../../utils/process'; -import { - expectFileToMatch, - expectFileToExist, - expectFileMatchToExist, - writeMultipleFiles -} from '../../../utils/fs'; -import { copyProjectAsset } from '../../../utils/assets'; -import { expectToFail } from '../../../utils/utils'; -import { updateJsonFile } from '../../../utils/project'; - -const imgSvg = ` - - - -`; - -export default function () { - // TODO(architect): Delete this test. It is now in devkit/build-angular. - - return Promise.resolve() - .then(() => silentNpm('install', 'font-awesome@4.7.0')) - .then(() => writeMultipleFiles({ - 'src/styles.scss': ` - $fa-font-path: "~font-awesome/fonts"; - @import "~font-awesome/scss/font-awesome"; - h1 { background: url('./assets/large.png'), - linear-gradient(to bottom, #0e40fa 25%, #0654f4 75%); } - h2 { background: url('./assets/small.svg'); } - p { background: url(./assets/small-id.svg#testID); } - `, - 'src/app/app.component.css': ` - h3 { background: url('../assets/small.svg'); } - h4 { background: url("../assets/large.png"); } - `, - 'src/assets/small.svg': imgSvg, - 'src/assets/small-id.svg': imgSvg - })) - .then(() => copyProjectAsset('images/spectrum.png', './src/assets/large.png')) - .then(() => updateJsonFile('angular.json', workspaceJson => { - const appArchitect = workspaceJson.projects['test-project'].targets; - appArchitect.build.options.styles = [ - { input: 'src/styles.scss' } - ]; - })) - .then(() => ng('build', '--extract-css', '--aot')) - .then(({ stdout }) => { - if (stdout.match(/postcss-url: \.+: Can't read file '\.+', ignoring/)) { - throw new Error('Expected no postcss-url file read warnings.'); - } - }) - // Check paths are correctly generated. - .then(() => expectFileToMatch('dist/test-project/styles.css', - /url\(['"]?large\.png['"]?\),\s+linear-gradient\(to bottom, #0e40fa 25%, #0654f4 75%\);/)) - .then(() => expectFileToMatch('dist/test-project/styles.css', - /url\(\\?['"]data:image\/svg\+xml/)) - .then(() => expectFileToMatch('dist/test-project/styles.css', - /url\(['"]?small-id\.svg#testID['"]?\)/)) - .then(() => expectFileToMatch('dist/test-project/main.js', - /url\(\\?['"]data:image\/svg\+xml/)) - .then(() => expectFileToMatch('dist/test-project/main.js', - /url\((?:['"]|\\')?large\.png(?:['"]|\\')?\)/)) - // Check files are correctly created. - .then(() => expectToFail(() => expectFileToExist('dist/test-project/small.svg'))) - .then(() => expectFileMatchToExist('./dist/test-project', /large\.png/)) - .then(() => expectFileMatchToExist('./dist/test-project', /small-id\.svg/)); -} From 41d20873b16ccb0e2e02db228bb4b3940ce8f3bd Mon Sep 17 00:00:00 2001 From: Filipe Silva Date: Wed, 29 Aug 2018 17:27:39 +0100 Subject: [PATCH 0025/1703] fix(@angular-devkit/build-angular): remove workaround for uglify-es (#12033) Fix https://github.com/angular/angular-cli/pull/11996#issuecomment-416514015 --- .../src/angular-cli-files/models/webpack-configs/common.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/common.ts b/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/common.ts index c65d78fa07e5..29eea7c418f7 100644 --- a/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/common.ts +++ b/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/common.ts @@ -229,10 +229,6 @@ export function getCommonConfig(wco: WebpackConfigOptions) { // PURE comments work best with 3 passes. // See https://github.com/webpack/webpack/issues/2899#issuecomment-317425926. passes: buildOptions.buildOptimizer ? 3 : 1, - // Workaround known uglify-es issue - // See https://github.com/mishoo/UglifyJS2/issues/2949#issuecomment-368070307 - inline: wco.supportES2015 ? 1 : 3, - global_defs: { ngDevMode: false, }, From b0897c0306c16f3332666b1e7b00ce784c309934 Mon Sep 17 00:00:00 2001 From: Alan Agius Date: Thu, 30 Aug 2018 22:46:52 +0200 Subject: [PATCH 0026/1703] fix(@schematics/angular): better error message when finding only routing modules (#11994) Closes #11961 --- .../schematics/angular/utility/find-module.ts | 23 +++++++++++++------ .../angular/utility/find-module_spec.ts | 12 ++++++++++ 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/packages/schematics/angular/utility/find-module.ts b/packages/schematics/angular/utility/find-module.ts index cf55bbd2a88f..6086523b6a12 100644 --- a/packages/schematics/angular/utility/find-module.ts +++ b/packages/schematics/angular/utility/find-module.ts @@ -28,7 +28,7 @@ export function findModuleFromOptions(host: Tree, options: ModuleOptions): Path if (!options.module) { const pathToCheck = (options.path || '') - + (options.flat ? '' : '/' + strings.dasherize(options.name)); + + (options.flat ? '' : '/' + strings.dasherize(options.name)); return normalize(findModule(host, pathToCheck)); } else { @@ -59,12 +59,17 @@ export function findModule(host: Tree, generateDir: string): Path { const moduleRe = /\.module\.ts$/; const routingModuleRe = /-routing\.module\.ts/; + let foundRoutingModule = false; + while (dir) { - const matches = dir.subfiles.filter(p => moduleRe.test(p) && !routingModuleRe.test(p)); + const allMatches = dir.subfiles.filter(p => moduleRe.test(p)); + const filteredMatches = allMatches.filter(p => !routingModuleRe.test(p)); + + foundRoutingModule = foundRoutingModule || allMatches.length !== filteredMatches.length; - if (matches.length == 1) { - return join(dir.path, matches[0]); - } else if (matches.length > 1) { + if (filteredMatches.length == 1) { + return join(dir.path, filteredMatches[0]); + } else if (filteredMatches.length > 1) { throw new Error('More than one module matches. Use skip-import option to skip importing ' + 'the component into the closest module.'); } @@ -72,8 +77,12 @@ export function findModule(host: Tree, generateDir: string): Path { dir = dir.parent; } - throw new Error('Could not find an NgModule. Use the skip-import ' - + 'option to skip importing in NgModule.'); + const errorMsg = foundRoutingModule ? 'Could not find a non Routing NgModule.' + + '\nModules with suffix \'-routing.module\' are strictly reserved for routing.' + + '\nUse the skip-import option to skip importing in NgModule.' + : 'Could not find an NgModule. Use the skip-import option to skip importing in NgModule.'; + + throw new Error(errorMsg); } /** diff --git a/packages/schematics/angular/utility/find-module_spec.ts b/packages/schematics/angular/utility/find-module_spec.ts index 8b92f4ebc54a..4caa1d1b1618 100644 --- a/packages/schematics/angular/utility/find-module_spec.ts +++ b/packages/schematics/angular/utility/find-module_spec.ts @@ -53,6 +53,18 @@ describe('find-module', () => { } }); + it('should throw if only routing modules were found', () => { + host = new EmptyTree(); + host.create('/foo/src/app/anything-routing.module.ts', 'anything routing module'); + + try { + findModule(host, 'foo/src/app/anything-routing'); + throw new Error('Succeeded, should have failed'); + } catch (err) { + expect(err.message).toMatch(/Could not find a non Routing NgModule/); + } + }); + it('should throw if two modules found', () => { try { host = new EmptyTree(); From 247b20d37a917b0540d16ca3c6ea09e50f75a644 Mon Sep 17 00:00:00 2001 From: Adrian Moos Date: Thu, 30 Aug 2018 22:47:15 +0200 Subject: [PATCH 0027/1703] style(@schematics/angular): fix typo and English in environment.ts (#12023) --- .../application/files/src/environments/environment.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/schematics/angular/application/files/src/environments/environment.ts b/packages/schematics/angular/application/files/src/environments/environment.ts index 600fdaf8a00a..7b4f817adb75 100644 --- a/packages/schematics/angular/application/files/src/environments/environment.ts +++ b/packages/schematics/angular/application/files/src/environments/environment.ts @@ -1,5 +1,5 @@ // This file can be replaced during build by using the `fileReplacements` array. -// `ng build ---prod` replaces `environment.ts` with `environment.prod.ts`. +// `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. // The list of file replacements can be found in `angular.json`. export const environment = { @@ -7,9 +7,10 @@ export const environment = { }; /* - * In development mode, for easier debugging, you can ignore zone related error - * stack frames such as `zone.run`/`zoneDelegate.invokeTask` by importing the - * below file. Don't forget to comment it out in production mode - * because it will have a performance impact when errors are thrown + * For easier debugging in development mode, you can import the following file + * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. + * + * This import should be commented out in production mode because it will have a negative impact + * on performance if an error is thrown. */ // import 'zone.js/dist/zone-error'; // Included with Angular CLI. From 7e63dd791b67ad49de4b0b2f55ecc386e4cad20e Mon Sep 17 00:00:00 2001 From: clydin <19598772+clydin@users.noreply.github.com> Date: Thu, 30 Aug 2018 18:33:31 -0400 Subject: [PATCH 0028/1703] build: limit rxjs to 6.2.x due to a defect in 6.3.0 (#12048) --- package.json | 4 ++-- packages/angular/cli/package.json | 2 +- packages/angular_devkit/architect/package.json | 2 +- packages/angular_devkit/architect/test/package.json | 2 +- packages/angular_devkit/architect_cli/package.json | 2 +- packages/angular_devkit/build_angular/package.json | 2 +- packages/angular_devkit/build_ng_packagr/package.json | 2 +- packages/angular_devkit/build_webpack/package.json | 2 +- packages/angular_devkit/core/package.json | 2 +- packages/angular_devkit/schematics/package.json | 2 +- packages/angular_devkit/schematics_cli/package.json | 2 +- packages/ngtools/webpack/package.json | 2 +- packages/schematics/angular/utility/latest-versions.ts | 2 +- packages/schematics/update/package.json | 2 +- .../build_angular/hello-world-app/package.json | 2 +- yarn.lock | 6 +++--- 16 files changed, 19 insertions(+), 19 deletions(-) diff --git a/package.json b/package.json index 8b881cb9159e..04bba652e809 100644 --- a/package.json +++ b/package.json @@ -93,7 +93,7 @@ "license-checker": "^20.1.0", "minimatch": "^3.0.4", "minimist": "^1.2.0", - "rxjs": "^6.0.0", + "rxjs": "~6.2.0", "semver": "^5.3.0", "source-map": "^0.5.6", "source-map-support": "^0.5.0", @@ -107,6 +107,6 @@ "resolutions": { "@types/webpack": "4.4.0", "@types/webpack-dev-server": "2.9.4", - "rxjs": "6.0.0" + "rxjs": "~6.2.0" } } diff --git a/packages/angular/cli/package.json b/packages/angular/cli/package.json index 6c658e474a02..ef122769ae77 100644 --- a/packages/angular/cli/package.json +++ b/packages/angular/cli/package.json @@ -38,7 +38,7 @@ "inquirer": "^6.1.0", "json-schema-traverse": "^0.4.1", "opn": "^5.3.0", - "rxjs": "^6.0.0", + "rxjs": "~6.2.0", "semver": "^5.1.0", "symbol-observable": "^1.2.0", "yargs-parser": "^10.0.0" diff --git a/packages/angular_devkit/architect/package.json b/packages/angular_devkit/architect/package.json index 496454e7d68e..442fdee61353 100644 --- a/packages/angular_devkit/architect/package.json +++ b/packages/angular_devkit/architect/package.json @@ -6,6 +6,6 @@ "typings": "src/index.d.ts", "dependencies": { "@angular-devkit/core": "0.0.0", - "rxjs": "^6.0.0" + "rxjs": "~6.2.0" } } \ No newline at end of file diff --git a/packages/angular_devkit/architect/test/package.json b/packages/angular_devkit/architect/test/package.json index b4617c03ea0d..f433fb9a3eca 100644 --- a/packages/angular_devkit/architect/test/package.json +++ b/packages/angular_devkit/architect/test/package.json @@ -1,6 +1,6 @@ { "builders": "builders.json", "dependencies": { - "rxjs": "^6.0.0" + "rxjs": "~6.2.0" } } diff --git a/packages/angular_devkit/architect_cli/package.json b/packages/angular_devkit/architect_cli/package.json index ec7373fd2699..9b54a4cbd89c 100644 --- a/packages/angular_devkit/architect_cli/package.json +++ b/packages/angular_devkit/architect_cli/package.json @@ -16,6 +16,6 @@ "@angular-devkit/architect": "0.0.0", "minimist": "^1.2.0", "symbol-observable": "^1.2.0", - "rxjs": "^6.0.0" + "rxjs": "~6.2.0" } } diff --git a/packages/angular_devkit/build_angular/package.json b/packages/angular_devkit/build_angular/package.json index 35bffdbf66fa..68284ad44e6d 100644 --- a/packages/angular_devkit/build_angular/package.json +++ b/packages/angular_devkit/build_angular/package.json @@ -36,7 +36,7 @@ "postcss-loader": "^2.1.5", "postcss-url": "^7.3.2", "raw-loader": "^0.5.1", - "rxjs": "^6.0.0", + "rxjs": "~6.2.0", "sass-loader": "^7.1.0", "semver": "^5.5.0", "source-map-support": "^0.5.0", diff --git a/packages/angular_devkit/build_ng_packagr/package.json b/packages/angular_devkit/build_ng_packagr/package.json index ef34febed104..a9eebffa6c25 100644 --- a/packages/angular_devkit/build_ng_packagr/package.json +++ b/packages/angular_devkit/build_ng_packagr/package.json @@ -8,7 +8,7 @@ "dependencies": { "@angular-devkit/architect": "0.0.0", "@angular-devkit/core": "0.0.0", - "rxjs": "^6.0.0", + "rxjs": "~6.2.0", "semver": "^5.3.0" }, "peerDependencies": { diff --git a/packages/angular_devkit/build_webpack/package.json b/packages/angular_devkit/build_webpack/package.json index 63c07c6c97a0..4f4b343eae32 100644 --- a/packages/angular_devkit/build_webpack/package.json +++ b/packages/angular_devkit/build_webpack/package.json @@ -8,7 +8,7 @@ "dependencies": { "@angular-devkit/architect": "0.0.0", "@angular-devkit/core": "0.0.0", - "rxjs": "^6.0.0" + "rxjs": "~6.2.0" }, "peerDependencies": { "webpack": "^4.6.0", diff --git a/packages/angular_devkit/core/package.json b/packages/angular_devkit/core/package.json index b63e953adeec..b48d40207cb5 100644 --- a/packages/angular_devkit/core/package.json +++ b/packages/angular_devkit/core/package.json @@ -12,6 +12,6 @@ "chokidar": "^2.0.3", "fast-json-stable-stringify": "^2.0.0", "source-map": "^0.5.6", - "rxjs": "^6.0.0" + "rxjs": "~6.2.0" } } diff --git a/packages/angular_devkit/schematics/package.json b/packages/angular_devkit/schematics/package.json index a964deec6899..44f474c8db8f 100644 --- a/packages/angular_devkit/schematics/package.json +++ b/packages/angular_devkit/schematics/package.json @@ -14,6 +14,6 @@ ], "dependencies": { "@angular-devkit/core": "0.0.0", - "rxjs": "^6.0.0" + "rxjs": "~6.2.0" } } diff --git a/packages/angular_devkit/schematics_cli/package.json b/packages/angular_devkit/schematics_cli/package.json index afe13e9cb173..609a07421c9d 100644 --- a/packages/angular_devkit/schematics_cli/package.json +++ b/packages/angular_devkit/schematics_cli/package.json @@ -23,6 +23,6 @@ "@schematics/schematics": "0.0.0", "minimist": "^1.2.0", "symbol-observable": "^1.2.0", - "rxjs": "^6.0.0" + "rxjs": "~6.2.0" } } diff --git a/packages/ngtools/webpack/package.json b/packages/ngtools/webpack/package.json index e7505a21e3d9..e49d0c2868f5 100644 --- a/packages/ngtools/webpack/package.json +++ b/packages/ngtools/webpack/package.json @@ -26,7 +26,7 @@ }, "dependencies": { "@angular-devkit/core": "0.0.0", - "rxjs": "^6.0.0", + "rxjs": "~6.2.0", "tree-kill": "^1.0.0", "webpack-sources": "^1.1.0" }, diff --git a/packages/schematics/angular/utility/latest-versions.ts b/packages/schematics/angular/utility/latest-versions.ts index 095b7cf1e6a1..ed46b138584e 100644 --- a/packages/schematics/angular/utility/latest-versions.ts +++ b/packages/schematics/angular/utility/latest-versions.ts @@ -9,7 +9,7 @@ export const latestVersions = { // These versions should be kept up to date with latest Angular peer dependencies. Angular: '^6.1.0', - RxJs: '^6.0.0', + RxJs: '~6.2.0', ZoneJs: '~0.8.26', TypeScript: '~2.9.2', // The versions below must be manually updated when making a new devkit release. diff --git a/packages/schematics/update/package.json b/packages/schematics/update/package.json index 7f5346566eea..9d8b40047fc5 100644 --- a/packages/schematics/update/package.json +++ b/packages/schematics/update/package.json @@ -15,6 +15,6 @@ "npm-registry-client": "^8.5.1", "semver": "^5.3.0", "semver-intersect": "^1.1.2", - "rxjs": "^6.0.0" + "rxjs": "~6.2.0" } } \ No newline at end of file diff --git a/tests/angular_devkit/build_angular/hello-world-app/package.json b/tests/angular_devkit/build_angular/hello-world-app/package.json index df5e3fd1255f..e45a8c5b0c7c 100644 --- a/tests/angular_devkit/build_angular/hello-world-app/package.json +++ b/tests/angular_devkit/build_angular/hello-world-app/package.json @@ -22,7 +22,7 @@ "@angular/platform-browser-dynamic": "^6.0.0-rc.0", "@angular/router": "^6.0.0-rc.0", "core-js": "^2.4.1", - "rxjs": "^6.0.0", + "rxjs": "~6.2.0", "zone.js": "^0.8.19" }, "devDependencies": { diff --git a/yarn.lock b/yarn.lock index 88e1463881ab..41f1c83ef417 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6371,9 +6371,9 @@ run-queue@^1.0.0, run-queue@^1.0.3: dependencies: aproba "^1.1.1" -rxjs@6.0.0, rxjs@^6.0.0, rxjs@^6.1.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.0.0.tgz#d647e029b5854617f994c82fe57a4c6747b352da" +rxjs@^6.0.0, rxjs@^6.1.0, rxjs@~6.2.0: + version "6.2.2" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.2.2.tgz#eb75fa3c186ff5289907d06483a77884586e1cf9" dependencies: tslib "^1.9.0" From 39b4cc030b0deba88627d659df01aaf3663f0265 Mon Sep 17 00:00:00 2001 From: Hans Larsen Date: Thu, 30 Aug 2018 16:34:02 -0700 Subject: [PATCH 0029/1703] build: fix RxJS invalid typings --- packages/angular_devkit/core/src/json/schema/registry.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/angular_devkit/core/src/json/schema/registry.ts b/packages/angular_devkit/core/src/json/schema/registry.ts index b16d79495839..89130f10d563 100644 --- a/packages/angular_devkit/core/src/json/schema/registry.ts +++ b/packages/angular_devkit/core/src/json/schema/registry.ts @@ -258,7 +258,7 @@ export class CoreSchemaRegistry implements SchemaRegistry { return Promise.reject(err); })); }), - switchMap(([data, valid]) => { + switchMap(([data, valid]: [JsonValue, boolean]) => { if (!validationOptions.withPrompts) { return observableOf([data, valid]); } @@ -274,7 +274,7 @@ export class CoreSchemaRegistry implements SchemaRegistry { return observableOf([data, valid]); } }), - switchMap(([data, valid]) => { + switchMap(([data, valid]: [JsonValue, boolean]) => { if (valid) { return observableOf(data).pipe( ...[...this._post].map(visitor => concatMap((data: JsonValue) => { @@ -287,7 +287,7 @@ export class CoreSchemaRegistry implements SchemaRegistry { return observableOf([data, valid]); } }), - map(([data, valid]) => { + map(([data, valid]: [JsonValue, boolean]) => { if (valid) { return { data, success: true } as SchemaValidatorResult; } From 189563d2778d4691ee2fe247465ec6c16a348c3b Mon Sep 17 00:00:00 2001 From: Hans Date: Tue, 4 Sep 2018 14:04:21 -0700 Subject: [PATCH 0030/1703] refactor(@angular/cli): rename getset command to deprecated --- packages/angular/cli/commands.json | 4 ++-- .../angular/cli/commands/deprecated-impl.ts | 21 +++++++++++++++++++ .../commands/{getset.json => deprecated.json} | 6 +++--- packages/angular/cli/commands/getset-impl.ts | 20 ------------------ 4 files changed, 26 insertions(+), 25 deletions(-) create mode 100644 packages/angular/cli/commands/deprecated-impl.ts rename packages/angular/cli/commands/{getset.json => deprecated.json} (66%) delete mode 100644 packages/angular/cli/commands/getset-impl.ts diff --git a/packages/angular/cli/commands.json b/packages/angular/cli/commands.json index a4a0b9360211..be69ff2dbe62 100644 --- a/packages/angular/cli/commands.json +++ b/packages/angular/cli/commands.json @@ -7,8 +7,8 @@ "make-this-awesome": "./commands/easter-egg.json", "eject": "./commands/eject.json", "generate": "./commands/generate.json", - "get": "./commands/getset.json", - "set": "./commands/getset.json", + "get": "./commands/deprecated.json", + "set": "./commands/deprecated.json", "help": "./commands/help.json", "lint": "./commands/lint.json", "new": "./commands/new.json", diff --git a/packages/angular/cli/commands/deprecated-impl.ts b/packages/angular/cli/commands/deprecated-impl.ts new file mode 100644 index 000000000000..85c8af440384 --- /dev/null +++ b/packages/angular/cli/commands/deprecated-impl.ts @@ -0,0 +1,21 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import { Command } from '../models/command'; + +export class DeprecatedCommand extends Command { + public async run() { + let message = 'The "${this.description.name}" command has been deprecated.'; + if (this.description.name == 'get' || this.description.name == 'set') { + message = 'get/set have been deprecated in favor of the config command.'; + } + + this.logger.error(message); + + return 0; + } +} diff --git a/packages/angular/cli/commands/getset.json b/packages/angular/cli/commands/deprecated.json similarity index 66% rename from packages/angular/cli/commands/getset.json rename to packages/angular/cli/commands/deprecated.json index 5d05f348a0ac..ee77bbd03d20 100644 --- a/packages/angular/cli/commands/getset.json +++ b/packages/angular/cli/commands/deprecated.json @@ -1,12 +1,12 @@ { "$schema": "http://json-schema.org/schema", - "id": "GetSetCommandOptions", + "$id": "ng-cli://commands/deprecated.json", "description": "Deprecated in favor of config command.", "$longDescription": "", - "$impl": "./getset-impl#GetSetCommand", + "$impl": "./deprecated-impl#DeprecatedCommand", "$hidden": true, "$type": "deprecated", "type": "object" -} \ No newline at end of file +} diff --git a/packages/angular/cli/commands/getset-impl.ts b/packages/angular/cli/commands/getset-impl.ts deleted file mode 100644 index 0e6c26795820..000000000000 --- a/packages/angular/cli/commands/getset-impl.ts +++ /dev/null @@ -1,20 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ - -import { Command } from '../models/command'; - -export interface Options { - keyword: string; - search?: boolean; -} - -export class GetSetCommand extends Command { - public async run(_options: Options) { - this.logger.warn('get/set have been deprecated in favor of the config command.'); - } -} From 4a25d7c9b4ebfabca2a4498c977003edbc792467 Mon Sep 17 00:00:00 2001 From: Hans Date: Tue, 4 Sep 2018 14:04:43 -0700 Subject: [PATCH 0031/1703] test: get-set deprecation should check stderr now --- .../e2e/tests/commands/config/get-set-deprecation.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/legacy-cli/e2e/tests/commands/config/get-set-deprecation.ts b/tests/legacy-cli/e2e/tests/commands/config/get-set-deprecation.ts index d5b898b82825..ad9d3f106d17 100644 --- a/tests/legacy-cli/e2e/tests/commands/config/get-set-deprecation.ts +++ b/tests/legacy-cli/e2e/tests/commands/config/get-set-deprecation.ts @@ -5,14 +5,14 @@ export default function() { const depRegEx = /get\/set have been deprecated in favor of the config command\./; return Promise.resolve() .then(() => ng('get')) - .then(({ stdout }) => { - if (!stdout.match(depRegEx)) { + .then(({ stderr }) => { + if (!stderr.match(depRegEx)) { throw new Error(`Expected deprecation warning.`); } }) .then(() => ng('set')) - .then(({ stdout }) => { - if (!stdout.match(depRegEx)) { + .then(({ stderr }) => { + if (!stderr.match(depRegEx)) { throw new Error(`Expected deprecation warning.`); } }); From 0b84c1d4b06c5ec43f59a10c54996521a149d920 Mon Sep 17 00:00:00 2001 From: Hans Date: Tue, 4 Sep 2018 14:15:37 -0700 Subject: [PATCH 0032/1703] feat(@angular-devkit/core): move findTypes in utility and export --- .../core/src/json/schema/transforms.ts | 54 ++------------- .../core/src/json/schema/utility.ts | 67 +++++++++++++++++++ 2 files changed, 71 insertions(+), 50 deletions(-) create mode 100644 packages/angular_devkit/core/src/json/schema/utility.ts diff --git a/packages/angular_devkit/core/src/json/schema/transforms.ts b/packages/angular_devkit/core/src/json/schema/transforms.ts index 513d7943b0d5..9c6694e4e32e 100644 --- a/packages/angular_devkit/core/src/json/schema/transforms.ts +++ b/packages/angular_devkit/core/src/json/schema/transforms.ts @@ -7,55 +7,7 @@ */ import { JsonObject, JsonValue, isJsonObject } from '../interface'; import { JsonPointer } from './interface'; - -const allTypes = ['string', 'integer', 'number', 'object', 'array', 'boolean', 'null']; - -function findTypes(schema: JsonObject): Set { - if (!schema) { - return new Set(); - } - - let potentials: Set; - if (typeof schema.type === 'string') { - potentials = new Set([schema.type]); - } else if (Array.isArray(schema.type)) { - potentials = new Set(schema.type as string[]); - } else { - potentials = new Set(allTypes); - } - - if (isJsonObject(schema.not)) { - const notTypes = findTypes(schema.not); - potentials = new Set([...potentials].filter(p => !notTypes.has(p))); - } - - if (Array.isArray(schema.allOf)) { - for (const sub of schema.allOf) { - const types = findTypes(sub as JsonObject); - potentials = new Set([...potentials].filter(p => types.has(p))); - } - } - - if (Array.isArray(schema.oneOf)) { - let options = new Set(); - for (const sub of schema.oneOf) { - const types = findTypes(sub as JsonObject); - options = new Set([...options, ...types]); - } - potentials = new Set([...potentials].filter(p => options.has(p))); - } - - if (Array.isArray(schema.anyOf)) { - let options = new Set(); - for (const sub of schema.anyOf) { - const types = findTypes(sub as JsonObject); - options = new Set([...options, ...types]); - } - potentials = new Set([...potentials].filter(p => options.has(p))); - } - - return potentials; -} +import { getTypesOfSchema } from './utility'; export function addUndefinedDefaults( value: JsonValue, @@ -66,7 +18,7 @@ export function addUndefinedDefaults( return value; } - const types = findTypes(schema); + const types = getTypesOfSchema(schema); if (types.size === 0) { return value; } @@ -110,6 +62,8 @@ export function addUndefinedDefaults( for (const propName of Object.getOwnPropertyNames(schema.properties)) { if (propName in newValue) { continue; + } else if (propName == '$schema') { + continue; } // TODO: Does not currently handle more complex schemas (oneOf/anyOf/etc.) diff --git a/packages/angular_devkit/core/src/json/schema/utility.ts b/packages/angular_devkit/core/src/json/schema/utility.ts new file mode 100644 index 000000000000..5e1608312ea8 --- /dev/null +++ b/packages/angular_devkit/core/src/json/schema/utility.ts @@ -0,0 +1,67 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import { JsonObject, isJsonObject } from '../interface'; + + +const allTypes = ['string', 'integer', 'number', 'object', 'array', 'boolean', 'null']; + +export function getTypesOfSchema(schema: JsonObject | true): Set { + if (!schema) { + return new Set(); + } + if (schema === true) { + return new Set(allTypes); + } + + let potentials: Set; + if (typeof schema.type === 'string') { + potentials = new Set([schema.type]); + } else if (Array.isArray(schema.type)) { + potentials = new Set(schema.type as string[]); + } else { + potentials = new Set(allTypes); + } + + if (isJsonObject(schema.not)) { + const notTypes = getTypesOfSchema(schema.not); + potentials = new Set([...potentials].filter(p => !notTypes.has(p))); + } + + if (Array.isArray(schema.allOf)) { + for (const sub of schema.allOf) { + const types = getTypesOfSchema(sub as JsonObject); + potentials = new Set([...potentials].filter(p => types.has(p))); + } + } + + if (Array.isArray(schema.oneOf)) { + let options = new Set(); + for (const sub of schema.oneOf) { + const types = getTypesOfSchema(sub as JsonObject); + options = new Set([...options, ...types]); + } + potentials = new Set([...potentials].filter(p => options.has(p))); + } + + if (Array.isArray(schema.anyOf)) { + let options = new Set(); + for (const sub of schema.anyOf) { + const types = getTypesOfSchema(sub as JsonObject); + options = new Set([...options, ...types]); + } + potentials = new Set([...potentials].filter(p => options.has(p))); + } + + if (schema.properties) { + potentials.add('object'); + } else if (schema.items) { + potentials.add('array'); + } + + return potentials; +} From 1b5c0082efb2224be80f3b8128a1c17a1f1bac99 Mon Sep 17 00:00:00 2001 From: Hans Date: Tue, 4 Sep 2018 14:18:14 -0700 Subject: [PATCH 0033/1703] feat(@angular-devkit/core): add levenshtein distance utility --- .../angular_devkit/core/src/utils/strings.ts | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/packages/angular_devkit/core/src/utils/strings.ts b/packages/angular_devkit/core/src/utils/strings.ts index 4743e9cb6555..ff3c0ce7cec3 100644 --- a/packages/angular_devkit/core/src/utils/strings.ts +++ b/packages/angular_devkit/core/src/utils/strings.ts @@ -127,3 +127,52 @@ export function underscore(str: string): string { export function capitalize(str: string): string { return str.charAt(0).toUpperCase() + str.substr(1); } + +/** + * Calculate the levenshtein distance of two strings. + * See https://en.wikipedia.org/wiki/Levenshtein_distance. + * Based off https://gist.github.com/andrei-m/982927 (for using the faster dynamic programming + * version). + * + * @param a String a. + * @param b String b. + * @returns A number that represents the distance between the two strings. The greater the number + * the more distant the strings are from each others. + */ +export function levenshtein(a: string, b: string): number { + if (a.length == 0) { + return b.length; + } + if (b.length == 0) { + return a.length; + } + + const matrix = []; + + // increment along the first column of each row + for (let i = 0; i <= b.length; i++) { + matrix[i] = [i]; + } + + // increment each column in the first row + for (let j = 0; j <= a.length; j++) { + matrix[0][j] = j; + } + + // Fill in the rest of the matrix + for (let i = 1; i <= b.length; i++) { + for (let j = 1; j <= a.length; j++) { + if (b.charAt(i - 1) == a.charAt(j - 1)) { + matrix[i][j] = matrix[i - 1][j - 1]; + } else { + matrix[i][j] = Math.min( + matrix[i - 1][j - 1] + 1, // substitution + matrix[i][j - 1] + 1, // insertion + matrix[i - 1][j] + 1, // deletion + ); + } + } + } + + return matrix[b.length][a.length]; +} From 75d682b2716a3d1203da1812cf3456bb6e924048 Mon Sep 17 00:00:00 2001 From: Hans Date: Tue, 4 Sep 2018 14:29:13 -0700 Subject: [PATCH 0034/1703] refactor(@angular/cli): use smart default instead of overwriting args --- packages/angular/cli/commands/new-impl.ts | 4 +++- packages/schematics/angular/ng-new/schema.json | 6 ++++-- packages/schematics/angular/workspace/schema.json | 5 ++++- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/packages/angular/cli/commands/new-impl.ts b/packages/angular/cli/commands/new-impl.ts index c9229a2e1b37..338d821d150f 100644 --- a/packages/angular/cli/commands/new-impl.ts +++ b/packages/angular/cli/commands/new-impl.ts @@ -44,11 +44,13 @@ export class NewCommand extends SchematicCommand { collectionName = this.parseCollectionName(options); } + // Register the version of the CLI in the registry. const packageJson = require('../package.json'); - options.version = packageJson.version; + const version = packageJson.version; // Ensure skipGit has a boolean value. options.skipGit = options.skipGit === undefined ? false : options.skipGit; + this._workflow.registry.addSmartDefaultProvider('ng-cli-version', () => version); return this.runSchematic({ collectionName: collectionName, diff --git a/packages/schematics/angular/ng-new/schema.json b/packages/schematics/angular/ng-new/schema.json index c4846c957fba..2c927a03f9bf 100644 --- a/packages/schematics/angular/ng-new/schema.json +++ b/packages/schematics/angular/ng-new/schema.json @@ -90,7 +90,10 @@ "version": { "type": "string", "description": "The version of the Angular CLI to use.", - "visible": false + "visible": false, + "$default": { + "$source": "ng-cli-version" + } }, "routing": { "type": "boolean", @@ -117,6 +120,5 @@ } }, "required": [ - "version" ] } diff --git a/packages/schematics/angular/workspace/schema.json b/packages/schematics/angular/workspace/schema.json index 7eac69c5d29a..c5f296fb06c7 100644 --- a/packages/schematics/angular/workspace/schema.json +++ b/packages/schematics/angular/workspace/schema.json @@ -69,7 +69,10 @@ "version": { "type": "string", "description": "The version of the Angular CLI to use.", - "visible": false + "visible": false, + "$default": { + "$source": "ng-cli-version" + } } }, "required": [ From fb99a490bfe8527edc7b3e0356a61d99c50b3912 Mon Sep 17 00:00:00 2001 From: Hans Date: Tue, 4 Sep 2018 14:29:54 -0700 Subject: [PATCH 0035/1703] feat(@angular-devkit/core): add a parseJsonFile that shows file path on error --- .../angular_devkit/core/src/json/parser.ts | 47 +++++++++++++++++-- 1 file changed, 43 insertions(+), 4 deletions(-) diff --git a/packages/angular_devkit/core/src/json/parser.ts b/packages/angular_devkit/core/src/json/parser.ts index 809b96c318c7..90a9e1b12497 100644 --- a/packages/angular_devkit/core/src/json/parser.ts +++ b/packages/angular_devkit/core/src/json/parser.ts @@ -25,15 +25,26 @@ import { Position, } from './interface'; +export class JsonException extends BaseException {} /** * A character was invalid in this context. */ -export class InvalidJsonCharacterException extends BaseException { +export class InvalidJsonCharacterException extends JsonException { + invalidChar: string; + line: number; + character: number; + offset: number; + constructor(context: JsonParserContext) { const pos = context.previous; - super(`Invalid JSON character: ${JSON.stringify(_peek(context))} ` - + `at ${pos.line}:${pos.character}.`); + const invalidChar = JSON.stringify(_peek(context)); + super(`Invalid JSON character: ${invalidChar} at ${pos.line}:${pos.character}.`); + + this.invalidChar = invalidChar; + this.line = pos.line; + this.offset = pos.offset; + this.character = pos.character; } } @@ -41,12 +52,20 @@ export class InvalidJsonCharacterException extends BaseException { /** * More input was expected, but we reached the end of the stream. */ -export class UnexpectedEndOfInputException extends BaseException { +export class UnexpectedEndOfInputException extends JsonException { constructor(_context: JsonParserContext) { super(`Unexpected end of file.`); } } +/** + * An error happened within a file. + */ +export class PathSpecificJsonException extends JsonException { + constructor(public path: string, public exception: JsonException) { + super(`An error happened at file path ${JSON.stringify(path)}: ${exception.message}`); + } +} /** * Context passed around the parser with information about where we currently are in the parse. @@ -860,3 +879,23 @@ export function parseJson(input: string, mode = JsonParseMode.Default): JsonValu return parseJsonAst(input, mode).value; } + +/** + * Parse a JSON string into its value. This discards the AST and only returns the value itself. + * It also absorbs JSON parsing errors and return a new error with the path in it. Useful for + * showing errors when parsing from a file. + * @param input The string to parse. + * @param mode The mode to parse the input with. {@see JsonParseMode}. + * @returns {JsonValue} The value represented by the JSON string. + */ +export function parseJsonFile(input: string, mode = JsonParseMode.Default, path: string) { + try { + return parseJson(input, mode); + } catch (e) { + if (e instanceof JsonException) { + throw new PathSpecificJsonException(path, e); + } else { + throw e; + } + } +} From ff1baab7fca4938ba717a87692b6869eb0ba8579 Mon Sep 17 00:00:00 2001 From: Hans Date: Tue, 4 Sep 2018 14:42:53 -0700 Subject: [PATCH 0036/1703] feat(@angular/cli): remove yargs-parser and implement parsing This is fully backward compatible. --- packages/angular/cli/custom-typings.d.ts | 38 -- packages/angular/cli/models/command-runner.ts | 500 ++++-------------- packages/angular/cli/models/command.ts | 200 ++++--- packages/angular/cli/models/parser.ts | 296 +++++++++++ packages/angular/cli/models/parser_spec.ts | 73 +++ packages/angular/cli/package.json | 4 +- yarn.lock | 6 - 7 files changed, 581 insertions(+), 536 deletions(-) delete mode 100644 packages/angular/cli/custom-typings.d.ts create mode 100644 packages/angular/cli/models/parser.ts create mode 100644 packages/angular/cli/models/parser_spec.ts diff --git a/packages/angular/cli/custom-typings.d.ts b/packages/angular/cli/custom-typings.d.ts deleted file mode 100644 index b33ea8011637..000000000000 --- a/packages/angular/cli/custom-typings.d.ts +++ /dev/null @@ -1,38 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ - -// tslint:disable:no-global-tslint-disable no-any -declare module 'yargs-parser' { - const parseOptions: any; - const yargsParser: (args: string | string[], options?: typeof parseOptions) => T; - export = yargsParser; -} - -declare module 'json-schema-traverse' { - import { JsonObject } from '@angular-devkit/core'; - interface TraverseOptions { - allKeys?: boolean; - } - type TraverseCallback = ( - schema: JsonObject, - jsonPointer: string, - rootSchema: string, - parentJsonPointer: string, - parentKeyword: string, - parentSchema: string, - property: string) => void; - - interface TraverseCallbacks { - pre?: TraverseCallback; - post?: TraverseCallback; - } - - const traverse: (schema: object, options: TraverseOptions, cbs: TraverseCallbacks) => void; - - export = traverse; -} diff --git a/packages/angular/cli/models/command-runner.ts b/packages/angular/cli/models/command-runner.ts index 8792328df6f1..b1cef78b87d6 100644 --- a/packages/angular/cli/models/command-runner.ts +++ b/packages/angular/cli/models/command-runner.ts @@ -5,454 +5,178 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ - -// tslint:disable:no-global-tslint-disable no-any import { - JsonObject, - deepCopy, + JsonParseMode, + isJsonObject, + json, logging, - parseJson, schema, - strings as coreStrings, + strings, tags, } from '@angular-devkit/core'; -import { ExportStringRef } from '@angular-devkit/schematics/tools'; import { readFileSync } from 'fs'; -import { dirname, join } from 'path'; -import { of, throwError } from 'rxjs'; -import { concatMap } from 'rxjs/operators'; -import * as yargsParser from 'yargs-parser'; -import { - Command, - CommandConstructor, - CommandContext, - CommandScope, - Option, -} from '../models/command'; +import { dirname, join, resolve } from 'path'; +import { of } from 'rxjs'; import { findUp } from '../utilities/find-up'; -import { insideProject } from '../utilities/project'; -import { convertSchemaToOptions, parseSchema } from './json-schema'; +import { parseJsonSchemaToCommandDescription } from '../utilities/json-schema'; +import { Command } from './command'; +import { + CommandDescription, + CommandDescriptionMap, + CommandProject, +} from './interface'; +import * as parser from './parser'; -interface CommandMap { +export interface CommandMapOptions { [key: string]: string; } -interface CommandMetadata { - description: string; - $aliases?: string[]; - $impl: string; - $scope?: 'in' | 'out'; - $type?: 'architect' | 'schematic'; - $hidden?: boolean; -} - -interface CommandLocation { - path: string; - text: string; - rawData: CommandMetadata; -} - -// Based off https://en.wikipedia.org/wiki/Levenshtein_distance -// No optimization, really. -function levenshtein(a: string, b: string): number { - /* base case: empty strings */ - if (a.length == 0) { - return b.length; - } - if (b.length == 0) { - return a.length; - } - - // Test if last characters of the strings match. - const cost = a[a.length - 1] == b[b.length - 1] ? 0 : 1; - - /* return minimum of delete char from s, delete char from t, and delete char from both */ - return Math.min( - levenshtein(a.slice(0, -1), b) + 1, - levenshtein(a, b.slice(0, -1)) + 1, - levenshtein(a.slice(0, -1), b.slice(0, -1)) + cost, - ); -} - /** * Run a command. * @param args Raw unparsed arguments. * @param logger The logger to use. - * @param context Execution context. + * @param project Project data structure containing project information. + * @param commands The map of supported commands. */ export async function runCommand( args: string[], logger: logging.Logger, - context: CommandContext, - commandMap?: CommandMap, + project: CommandProject, + commands?: CommandMapOptions, ): Promise { - - // if not args supplied, just run the help command. - if (!args || args.length === 0) { - args = ['help']; - } - const rawOptions = yargsParser(args, { alias: { help: ['h'] }, boolean: [ 'help' ] }); - let commandName = rawOptions._[0] || ''; - - // remove the command name - rawOptions._ = rawOptions._.slice(1); - const executionScope = insideProject() - ? CommandScope.inProject - : CommandScope.outsideProject; - - if (commandMap === undefined) { + if (commands === undefined) { const commandMapPath = findUp('commands.json', __dirname); if (commandMapPath === null) { - logger.fatal('Unable to find command map.'); - - return 1; + throw new Error('Unable to find command map.'); } const cliDir = dirname(commandMapPath); const commandsText = readFileSync(commandMapPath).toString('utf-8'); - const commandJson = JSON.parse(commandsText) as { [name: string]: string }; + const commandJson = json.parseJsonFile( + commandsText, + JsonParseMode.Loose, + commandMapPath, + ) as { [name: string]: string }; - commandMap = {}; + commands = {}; for (const commandName of Object.keys(commandJson)) { - commandMap[commandName] = join(cliDir, commandJson[commandName]); + commands[commandName] = resolve(cliDir, commandJson[commandName]); } } - let commandMetadata = commandName ? findCommand(commandMap, commandName) : null; - - if (!commandMetadata && (rawOptions.v || rawOptions.version)) { - commandName = 'version'; - commandMetadata = findCommand(commandMap, commandName); - } else if (!commandMetadata && rawOptions.help) { - commandName = 'help'; - commandMetadata = findCommand(commandMap, commandName); - } - - if (!commandMetadata) { - if (!commandName) { - logger.error(tags.stripIndent` - We could not find a command from the arguments and the help command seems to be disabled. - This is an issue with the CLI itself. If you see this comment, please report it and - provide your repository. - `); - - return 1; - } else { - const commandsDistance = {} as { [name: string]: number }; - const allCommands = Object.keys(commandMap).sort((a, b) => { - if (!(a in commandsDistance)) { - commandsDistance[a] = levenshtein(a, commandName); - } - if (!(b in commandsDistance)) { - commandsDistance[b] = levenshtein(b, commandName); - } - - return commandsDistance[a] - commandsDistance[b]; - }); - - logger.error(tags.stripIndent` - The specified command ("${commandName}") is invalid. For a list of available options, - run "ng help". - Did you mean "${allCommands[0]}"? - `); + // This registry is exclusively used for flattening schemas, and not for validating. + const registry = new schema.CoreSchemaRegistry([]); + registry.registerUriHandler((uri: string) => { + if (uri.startsWith('ng-cli://')) { + const content = readFileSync(join(__dirname, '..', uri.substr('ng-cli://'.length)), 'utf-8'); - return 1; + return of(JSON.parse(content)); } - } + }); - const command = await createCommand(commandMetadata, context, logger); - const metadataOptions = await convertSchemaToOptions(commandMetadata.text); - if (command === null) { - logger.error(tags.stripIndent`Command (${commandName}) failed to instantiate.`); + // Normalize the commandMap + const commandMap: CommandDescriptionMap = {}; + for (const name of Object.keys(commands)) { + const schemaPath = commands[name]; + const schemaContent = readFileSync(schemaPath, 'utf-8'); + const schema = json.parseJsonFile(schemaContent, JsonParseMode.Loose, schemaPath); + if (!isJsonObject(schema)) { + throw new Error('Invalid command JSON loaded from ' + JSON.stringify(schemaPath)); + } - return 1; + commandMap[name] = await parseJsonSchemaToCommandDescription( + name, + schemaPath, + registry, + schema, + ); } - // Add the options from the metadata to the command object. - command.addOptions(metadataOptions); - let options = parseOptions(args, metadataOptions); - args = await command.initializeRaw(args); - - const optionsCopy = deepCopy(options); - await processRegistry(optionsCopy, commandMetadata); - await command.initialize(optionsCopy); - // Reparse options after initializing the command. - options = parseOptions(args, command.options); + let commandName: string | undefined = undefined; + for (let i = 0; i < args.length; i++) { + const arg = args[i]; - if (commandName === 'help') { - options.commandInfo = getAllCommandInfo(commandMap); + if (arg in commandMap) { + commandName = arg; + args.splice(i, 1); + break; + } else if (!arg.startsWith('-')) { + commandName = arg; + args.splice(i, 1); + break; + } } - if (options.help) { - command.printHelp(commandName, commandMetadata.rawData.description, options); + // if no commands were found, use `help`. + if (commandName === undefined) { + commandName = 'help'; + } - return; - } else { - const commandScope = mapCommandScope(commandMetadata.rawData.$scope); - if (commandScope !== undefined && commandScope !== CommandScope.everywhere) { - if (commandScope !== executionScope) { - let errorMessage; - if (commandScope === CommandScope.inProject) { - errorMessage = `This command can only be run inside of a CLI project.`; - } else { - errorMessage = `This command can not be run inside of a CLI project.`; - } - logger.fatal(errorMessage); + let description: CommandDescription | null = null; - return 1; - } - if (commandScope === CommandScope.inProject) { - if (!context.project.configFile) { - logger.fatal('Invalid project: missing workspace file.'); - - return 1; + if (commandName !== undefined) { + if (commandMap[commandName]) { + description = commandMap[commandName]; + } else { + Object.keys(commandMap).forEach(name => { + const commandDescription = commandMap[name]; + const aliases = commandDescription.aliases; + + let found = false; + if (aliases) { + if (aliases.some(alias => alias === commandName)) { + found = true; + } } - if (['.angular-cli.json', 'angular-cli.json'].includes(context.project.configFile)) { - // -------------------------------------------------------------------------------- - // If changing this message, please update the same message in - // `packages/@angular/cli/bin/ng-update-message.js` - const message = tags.stripIndent` - The Angular CLI configuration format has been changed, and your existing configuration - can be updated automatically by running the following command: - - ng update @angular/cli - `; - - logger.warn(message); - - return 1; + if (found) { + if (description) { + throw new Error('Found multiple commands with the same alias.'); + } + commandName = name; + description = commandDescription; } - } + }); } } - delete options.h; - delete options.help; - await processRegistry(options, commandMetadata); - const isValid = await command.validate(options); - if (!isValid) { - logger.fatal(`Validation error. Invalid command options.`); + if (!commandName) { + logger.error(tags.stripIndent` + We could not find a command from the arguments and the help command seems to be disabled. + This is an issue with the CLI itself. If you see this comment, please report it and + provide your repository. + `); return 1; } - return command.run(options); -} - -async function processRegistry( - options: {_: (string | boolean | number)[]}, commandMetadata: CommandLocation) { - const rawArgs = options._; - const registry = new schema.CoreSchemaRegistry([]); - registry.addSmartDefaultProvider('argv', (schema: JsonObject) => { - if ('index' in schema) { - return rawArgs[Number(schema['index'])]; - } else { - return rawArgs; - } - }); - - const jsonSchema = parseSchema(commandMetadata.text); - if (jsonSchema === null) { - throw new Error(''); - } - await registry.compile(jsonSchema).pipe( - concatMap(validator => validator(options)), concatMap(validatorResult => { - if (validatorResult.success) { - return of(options); - } else { - return throwError(new schema.SchemaValidationException(validatorResult.errors)); + if (!description) { + const commandsDistance = {} as { [name: string]: number }; + const name = commandName; + const allCommands = Object.keys(commandMap).sort((a, b) => { + if (!(a in commandsDistance)) { + commandsDistance[a] = strings.levenshtein(a, name); } - })).toPromise(); -} - -export function parseOptions(args: string[], optionsAndArguments: Option[]) { - const parser = yargsParser; - - // filter out arguments - const options = optionsAndArguments - .filter(opt => { - let isOption = true; - if (opt.$default !== undefined && opt.$default.$source === 'argv') { - isOption = false; - } - - return isOption; - }); - - const aliases: { [key: string]: string[]; } = options - .reduce((aliases: { [key: string]: string; }, opt) => { - if (!opt || !opt.aliases || opt.aliases.length === 0) { - return aliases; - } - - aliases[opt.name] = (opt.aliases || []) - .filter(a => a.length === 1)[0]; - - return aliases; - }, {}); - - const booleans = options - .filter(o => o.type && o.type === 'boolean') - .map(o => o.name); - - const defaults = options - .filter(o => o.default !== undefined || booleans.indexOf(o.name) !== -1) - .reduce((defaults: {[key: string]: string | number | boolean | undefined }, opt: Option) => { - defaults[opt.name] = opt.default; - - return defaults; - }, {}); - - const strings = options - .filter(o => o.type === 'string') - .map(o => o.name); - - const numbers = options - .filter(o => o.type === 'number') - .map(o => o.name); - - - aliases.help = ['h']; - booleans.push('help'); - - const yargsOptions = { - alias: aliases, - boolean: booleans, - default: defaults, - string: strings, - number: numbers, - }; - - const parsedOptions = parser(args, yargsOptions); - - // Remove aliases. - options - .reduce((allAliases, option) => { - if (!option || !option.aliases || option.aliases.length === 0) { - return allAliases; + if (!(b in commandsDistance)) { + commandsDistance[b] = strings.levenshtein(b, name); } - return allAliases.concat([...option.aliases]); - }, [] as string[]) - .forEach((alias: string) => { - delete parsedOptions[alias]; + return commandsDistance[a] - commandsDistance[b]; }); - // Remove undefined booleans - booleans - .filter(b => parsedOptions[b] === undefined) - .map(b => coreStrings.camelize(b)) - .forEach(b => delete parsedOptions[b]); + logger.error(tags.stripIndent` + The specified command ("${commandName}") is invalid. For a list of available options, + run "ng help". - // remove options with dashes. - Object.keys(parsedOptions) - .filter(key => key.indexOf('-') !== -1) - .forEach(key => delete parsedOptions[key]); + Did you mean "${allCommands[0]}"? + `); - // remove the command name - parsedOptions._ = parsedOptions._.slice(1); - - return parsedOptions; -} - -// Find a command. -function findCommand(map: CommandMap, name: string): CommandLocation | null { - // let Cmd: CommandConstructor = map[name]; - let commandName = name; - - if (!map[commandName]) { - // find command via aliases - commandName = Object.keys(map) - .filter(key => { - // get aliases for the key - const metadataText = readFileSync(map[key]).toString('utf-8'); - const metadata = JSON.parse(metadataText); - const aliases = metadata['$aliases']; - if (!aliases) { - return false; - } - const foundAlias = aliases.filter((alias: string) => alias === name); - - return foundAlias.length > 0; - })[0]; - } - - const metadataPath = map[commandName]; - - if (!metadataPath) { - return null; - } - const metadataText = readFileSync(metadataPath).toString('utf-8'); - - const metadata = parseJson(metadataText) as any; - - return { - path: metadataPath, - text: metadataText, - rawData: metadata, - }; -} - -// Create an instance of a command. -async function createCommand(metadata: CommandLocation, - context: CommandContext, - logger: logging.Logger): Promise { - const schema = parseSchema(metadata.text); - if (schema === null) { - return null; - } - const implPath = schema.$impl; - if (typeof implPath !== 'string') { - throw new Error('Implementation path is incorrect'); - } - - const implRef = new ExportStringRef(implPath, dirname(metadata.path)); - - const ctor = implRef.ref as CommandConstructor; - - return new ctor(context, logger); -} - -function mapCommandScope(scope: 'in' | 'out' | undefined): CommandScope { - let commandScope = CommandScope.everywhere; - switch (scope) { - case 'in': - commandScope = CommandScope.inProject; - break; - case 'out': - commandScope = CommandScope.outsideProject; - break; + return 1; } - return commandScope; -} - -interface CommandInfo { - name: string; - description: string; - aliases: string[]; - hidden: boolean; -} -function getAllCommandInfo(map: CommandMap): CommandInfo[] { - return Object.keys(map) - .map(name => { - return { - name: name, - metadata: findCommand(map, name), - }; - }) - .map(info => { - if (info.metadata === null) { - return null; - } + const parsedOptions = parser.parseArguments(args, description.options); + Command.setCommandMap(commandMap); + const command = new description.impl({ project }, description, logger); - return { - name: info.name, - description: info.metadata.rawData.description, - aliases: info.metadata.rawData.$aliases || [], - hidden: info.metadata.rawData.$hidden || false, - }; - }) - .filter(info => info !== null) as CommandInfo[]; + return await command.validateAndRun(parsedOptions); } diff --git a/packages/angular/cli/models/command.ts b/packages/angular/cli/models/command.ts index 511abc6a813d..53a1e4307179 100644 --- a/packages/angular/cli/models/command.ts +++ b/packages/angular/cli/models/command.ts @@ -7,151 +7,149 @@ */ // tslint:disable:no-global-tslint-disable no-any -import { JsonValue, logging, terminal } from '@angular-devkit/core'; - -export interface CommandConstructor { - new(context: CommandContext, logger: logging.Logger): Command; - readonly name: string; - aliases: string[]; - scope: CommandScope; -} - -export enum CommandScope { - everywhere, - inProject, - outsideProject, -} - -export enum ArgumentStrategy { - MapToOptions, - Nothing, +import { logging, strings, tags, terminal } from '@angular-devkit/core'; +import { getWorkspace } from '../utilities/config'; +import { + Arguments, + CommandContext, + CommandDescription, + CommandDescriptionMap, + CommandProject, + CommandScope, + Option, +} from './interface'; + +export interface BaseCommandOptions { + help?: boolean; + help_json?: boolean; + '--': string[]; } -export abstract class Command { - protected _rawArgs: string[]; +export abstract class Command { public allowMissingWorkspace = false; + public project: CommandProject; - constructor(context: CommandContext, logger: logging.Logger) { - this.logger = logger; - if (context) { - this.project = context.project; - } + protected static commandMap: CommandDescriptionMap; + static setCommandMap(map: CommandDescriptionMap) { + this.commandMap = map; } - public addOptions(options: Option[]) { - this.options = (this.options || []).concat(options); + constructor( + context: CommandContext, + public readonly description: CommandDescription, + protected readonly logger: logging.Logger, + ) { + this.project = context.project; } - async initializeRaw(args: string[]): Promise { - this._rawArgs = args; - - return args; - } - async initialize(_options: any): Promise { + async initialize(options: T): Promise { return; } - validate(_options: T): boolean | Promise { - return true; - } + async printHelp(options: T): Promise { + await this.printHelpUsage(); + await this.printHelpOptions(); - printHelp(commandName: string, description: string, options: any): void { - if (description) { - this.logger.info(description); - } - this.printHelpUsage(commandName, this.options); - this.printHelpOptions(this.options); + return 0; } - private _getArguments(options: Option[]) { - function _getArgIndex(def: OptionSmartDefault | undefined): number { - if (def === undefined || def.$source !== 'argv' || typeof def.index !== 'number') { - // If there's no proper order, this argument is wonky. We will show it at the end only - // (after all other arguments). - return Infinity; - } - - return def.index; - } + async printJsonHelp(_options: T): Promise { + this.logger.info(JSON.stringify(this.description)); - return options - .filter(opt => this.isArgument(opt)) - .sort((a, b) => _getArgIndex(a.$default) - _getArgIndex(b.$default)); + return 0; } - protected printHelpUsage(name: string, options: Option[]) { - const args = this._getArguments(options); - const opts = options.filter(opt => !this.isArgument(opt)); + protected async printHelpUsage() { + this.logger.info(this.description.description); + + const name = this.description.name; + const args = this.description.options.filter(x => x.positional !== undefined); + const opts = this.description.options.filter(x => x.positional === undefined); + const argDisplay = args && args.length > 0 ? ' ' + args.map(a => `<${a.name}>`).join(' ') : ''; const optionsDisplay = opts && opts.length > 0 ? ` [options]` : ``; + this.logger.info(`usage: ng ${name}${argDisplay}${optionsDisplay}`); + this.logger.info(''); } - protected isArgument(option: Option) { - let isArg = false; - if (option.$default !== undefined && option.$default.$source === 'argv') { - isArg = true; - } - - return isArg; - } + protected async printHelpOptions(options: Option[] = this.description.options) { + const args = options.filter(opt => opt.positional !== undefined); + const opts = options.filter(opt => opt.positional === undefined); - protected printHelpOptions(options: Option[]) { - if (!options) { - return; - } - const args = options.filter(opt => this.isArgument(opt)); - const opts = options.filter(opt => !this.isArgument(opt)); if (args.length > 0) { this.logger.info(`arguments:`); args.forEach(o => { this.logger.info(` ${terminal.cyan(o.name)}`); - this.logger.info(` ${o.description}`); + if (o.description) { + this.logger.info(` ${o.description}`); + } }); } - if (this.options.length > 0) { + if (options.length > 0) { + if (args.length > 0) { + this.logger.info(''); + } this.logger.info(`options:`); opts .filter(o => !o.hidden) - .sort((a, b) => a.name >= b.name ? 1 : -1) + .sort((a, b) => a.name.localeCompare(b.name)) .forEach(o => { const aliases = o.aliases && o.aliases.length > 0 ? '(' + o.aliases.map(a => `-${a}`).join(' ') + ')' : ''; - this.logger.info(` ${terminal.cyan('--' + o.name)} ${aliases}`); - this.logger.info(` ${o.description}`); + this.logger.info(` ${terminal.cyan('--' + strings.dasherize(o.name))} ${aliases}`); + if (o.description) { + this.logger.info(` ${o.description}`); + } }); } } - abstract run(options: T): number | void | Promise; - public options: Option[]; - public additionalSchemas: string[] = []; - protected readonly logger: logging.Logger; - protected readonly project: any; -} - -export interface CommandContext { - project: any; -} + async validateScope(): Promise { + switch (this.description.scope) { + case CommandScope.OutProject: + if (this.project.configFile || getWorkspace('local') !== null) { + this.logger.fatal(tags.oneLine` + The ${this.description.name} command requires to be run outside of a project, but a + project definition was found at "${this.project.root}". + `); + throw 1; + } + break; + case CommandScope.InProject: + if (!this.project.configFile || getWorkspace('local') === null) { + this.logger.fatal(tags.oneLine` + The ${this.description.name} command requires to be run in an Angular project, but a + project definition could not be found. + `); + throw 1; + } + break; + case CommandScope.Everywhere: + // Can't miss this. + break; + } + } -export interface Option { - name: string; - description: string; - type: string; - default?: string | number | boolean; - required?: boolean; - aliases?: string[]; - format?: string; - hidden?: boolean; - $default?: OptionSmartDefault; -} + abstract async run(options: T & Arguments): Promise; -export interface OptionSmartDefault { - $source: string; - [key: string]: JsonValue; + async validateAndRun(options: T & Arguments): Promise { + if (!options.help && !options.help_json) { + await this.validateScope(); + } + await this.initialize(options); + + if (options.help) { + return this.printHelp(options); + } else if (options.help_json) { + return this.printJsonHelp(options); + } else { + return await this.run(options); + } + } } diff --git a/packages/angular/cli/models/parser.ts b/packages/angular/cli/models/parser.ts new file mode 100644 index 000000000000..1af0c368dd0c --- /dev/null +++ b/packages/angular/cli/models/parser.ts @@ -0,0 +1,296 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + * + */ +import { strings } from '@angular-devkit/core'; +import { Arguments, Option, Value } from './interface'; + + +function _coerceType(str: string | undefined, type: string, v?: Value): Value | undefined { + switch (type) { + case 'any': + if (Array.isArray(v)) { + return v.concat(str || ''); + } + + return _coerceType(str, 'boolean', v) !== undefined ? _coerceType(str, 'boolean', v) + : _coerceType(str, 'number', v) !== undefined ? _coerceType(str, 'number', v) + : _coerceType(str, 'string', v); + + case 'string': + return str || ''; + + case 'boolean': + switch (str) { + case 'false': + return false; + + case undefined: + case '': + case 'true': + return true; + + default: + return undefined; + } + + case 'number': + if (str === undefined) { + return 0; + } else if (Number.isFinite(+str)) { + return +str; + } else { + return undefined; + } + + case 'array': + return Array.isArray(v) ? v.concat(str || '') : [str || '']; + + default: + return undefined; + } +} + +function _coerce(str: string | undefined, o: Option | null, v?: Value): Value | undefined { + return _coerceType(str, o ? o.type : 'any', v); +} + + +function _getOptionFromName(name: string, options: Option[]): Option | undefined { + const cName = strings.camelize(name); + + for (const option of options) { + if (option.name == name || option.name == cName) { + return option; + } + + if (option.aliases.some(x => x == name || x == cName)) { + return option; + } + } + + return undefined; +} + + +function _assignOption( + arg: string, + args: string[], + options: Option[], + parsedOptions: Arguments, + _positionals: string[], + leftovers: string[], +) { + let key = arg.substr(2); + let option: Option | null = null; + let value = ''; + const i = arg.indexOf('='); + + // If flag is --no-abc AND there's no equal sign. + if (i == -1) { + if (key.startsWith('no-')) { + // Only use this key if the option matching the rest is a boolean. + const maybeOption = _getOptionFromName(key.substr(3), options); + if (maybeOption && maybeOption.type == 'boolean') { + value = 'false'; + option = maybeOption; + } + } else if (key.startsWith('no')) { + // Only use this key if the option matching the rest is a boolean. + const maybeOption = _getOptionFromName(key.substr(2), options); + if (maybeOption && maybeOption.type == 'boolean') { + value = 'false'; + option = maybeOption; + } + } + + if (option === null) { + // Set it to true if it's a boolean and the next argument doesn't match true/false. + const maybeOption = _getOptionFromName(key, options); + if (maybeOption) { + // Not of type boolean, consume the next value. + value = args[0]; + // Only absorb it if it leads to a value. + if (_coerce(value, maybeOption) !== undefined) { + args.shift(); + } else { + value = ''; + } + option = maybeOption; + } + } + } else { + key = arg.substring(0, i); + option = _getOptionFromName(key, options) || null; + if (option) { + value = arg.substring(i + 1); + if (option.type === 'boolean' && _coerce(value, option) === undefined) { + value = 'true'; + } + } + } + if (option === null) { + if (args[0] && !args[0].startsWith('--')) { + leftovers.push(arg, args[0]); + args.shift(); + } else { + leftovers.push(arg); + } + } else { + const v = _coerce(value, option, parsedOptions[option.name]); + if (v !== undefined) { + parsedOptions[option.name] = v; + } + } +} + + +/** + * Parse the arguments in a consistent way, but without having any option definition. This tries + * to assess what the user wants in a free form. For example, using `--name=false` will set the + * name properties to a boolean type. + * This should only be used when there's no schema available or if a schema is "true" (anything is + * valid). + * + * @param args Argument list to parse. + * @returns An object that contains a property per flags from the args. + */ +export function parseFreeFormArguments(args: string[]): Arguments { + const parsedOptions: Arguments = {}; + const leftovers = []; + + for (let arg = args.shift(); arg !== undefined; arg = args.shift()) { + if (arg == '--') { + leftovers.push(...args); + break; + } + + if (arg.startsWith('--')) { + const eqSign = arg.indexOf('='); + let name: string; + let value: string | undefined; + if (eqSign !== -1) { + name = arg.substring(2, eqSign); + value = arg.substring(eqSign + 1); + } else { + name = arg.substr(2); + value = args.shift(); + } + + const v = _coerce(value, null, parsedOptions[name]); + if (v !== undefined) { + parsedOptions[name] = v; + } + } else if (arg.startsWith('-')) { + arg.split('').forEach(x => parsedOptions[x] = true); + } else { + leftovers.push(arg); + } + } + + parsedOptions['--'] = leftovers; + + return parsedOptions; +} + + +/** + * Parse the arguments in a consistent way, from a list of standardized options. + * The result object will have a key per option name, with the `_` key reserved for positional + * arguments, and `--` will contain everything that did not match. Any key that don't have an + * option will be pushed back in `--` and removed from the object. If you need to validate that + * there's no additionalProperties, you need to check the `--` key. + * + * @param args The argument array to parse. + * @param options List of supported options. {@see Option}. + * @returns An object that contains a property per option. + */ +export function parseArguments(args: string[], options: Option[] | null): Arguments { + if (options === null) { + options = []; + } + + const leftovers: string[] = []; + const positionals: string[] = []; + const parsedOptions: Arguments = {}; + + for (let arg = args.shift(); arg !== undefined; arg = args.shift()) { + if (!arg) { + break; + } + + if (arg == '--') { + // If we find a --, we're done. + leftovers.push(...args); + break; + } + + if (arg.startsWith('--')) { + _assignOption(arg, args, options, parsedOptions, positionals, leftovers); + } else if (arg.startsWith('-')) { + // Argument is of form -abcdef. Starts at 1 because we skip the `-`. + for (let i = 1; i < arg.length; i++) { + const flag = arg[i]; + // Treat the last flag as `--a` (as if full flag but just one letter). We do this in + // the loop because it saves us a check to see if the arg is just `-`. + if (i == arg.length - 1) { + _assignOption('--' + flag, args, options, parsedOptions, positionals, leftovers); + } else { + const maybeOption = _getOptionFromName(flag, options); + if (maybeOption) { + const v = _coerce(undefined, maybeOption, parsedOptions[maybeOption.name]); + if (v !== undefined) { + parsedOptions[maybeOption.name] = v; + } + } + } + } + } else { + positionals.push(arg); + } + } + + // Deal with positionals. + if (positionals.length > 0) { + let pos = 0; + for (let i = 0; i < positionals.length;) { + let found = false; + let incrementPos = false; + let incrementI = true; + + // We do this with a found flag because more than 1 option could have the same positional. + for (const option of options) { + // If any option has this positional and no value, we need to remove it. + if (option.positional === pos) { + if (parsedOptions[option.name] === undefined) { + parsedOptions[option.name] = positionals[i]; + found = true; + } else { + incrementI = false; + } + incrementPos = true; + } + } + + if (found) { + positionals.splice(i--, 1); + } + if (incrementPos) { + pos++; + } + if (incrementI) { + i++; + } + } + } + + if (positionals.length > 0 || leftovers.length > 0) { + parsedOptions['--'] = [...positionals, ...leftovers]; + } + + return parsedOptions; +} diff --git a/packages/angular/cli/models/parser_spec.ts b/packages/angular/cli/models/parser_spec.ts new file mode 100644 index 000000000000..961f37952aae --- /dev/null +++ b/packages/angular/cli/models/parser_spec.ts @@ -0,0 +1,73 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + * + */ +import { Arguments, Option, OptionType } from './interface'; +import { parseArguments } from './parser'; + +describe('parseArguments', () => { + const options: Option[] = [ + { name: 'bool', aliases: [ 'b' ], type: OptionType.Boolean, description: '' }, + { name: 'num', aliases: [ 'n' ], type: OptionType.Number, description: '' }, + { name: 'str', aliases: [ 's' ], type: OptionType.String, description: '' }, + { name: 'helloWorld', aliases: [], type: OptionType.String, description: '' }, + { name: 'helloBool', aliases: [], type: OptionType.Boolean, description: '' }, + { name: 'arr', aliases: [ 'a' ], type: OptionType.Array, description: '' }, + { name: 'p1', positional: 0, aliases: [], type: OptionType.String, description: '' }, + { name: 'p2', positional: 1, aliases: [], type: OptionType.String, description: '' }, + ]; + + const tests: { [test: string]: Partial } = { + '--bool': { bool: true }, + '--bool=1': { bool: true }, + '--bool=true': { bool: true }, + '--bool=false': { bool: false }, + '--no-bool': { bool: false }, + '--no-bool=true': { '--': ['--no-bool=true'] }, + '--b=true': { bool: true }, + '--b=false': { bool: false }, + '--b true': { bool: true }, + '--b false': { bool: false }, + '--bool --num': { bool: true, num: 0 }, + '--bool --num=true': { bool: true }, + '--bool=true --num': { bool: true, num: 0 }, + '--bool true --num': { bool: true, num: 0 }, + '--bool=false --num': { bool: false, num: 0 }, + '--bool false --num': { bool: false, num: 0 }, + '--str false --num': { str: 'false', num: 0 }, + '--str=false --num': { str: 'false', num: 0 }, + '--str=false --num1': { str: 'false', '--': ['--num1'] }, + '--str=false val1 --num1': { str: 'false', p1: 'val1', '--': ['--num1'] }, + '--str=false val1 val2': { str: 'false', p1: 'val1', p2: 'val2' }, + '--str=false val1 val2 --num1': { str: 'false', p1: 'val1', p2: 'val2', '--': ['--num1'] }, + '--str=false val1 --num1 val2': { str: 'false', p1: 'val1', '--': ['--num1', 'val2'] }, + 'val1 --num=1 val2': { num: 1, p1: 'val1', p2: 'val2' }, + '--p1=val1 --num=1 val2': { num: 1, p1: 'val1', p2: 'val2' }, + '--p1=val1 --num=1 --p2=val2 val3': { num: 1, p1: 'val1', p2: 'val2', '--': ['val3'] }, + '--bool val1 --etc --num val2 --v': { bool: true, num: 0, p1: 'val1', p2: 'val2', + '--': ['--etc', '--v'] }, + '--arr=a --arr=b --arr c d': { arr: ['a', 'b', 'c'], p1: 'd' }, + '--arr=1 --arr --arr c d': { arr: ['1', '--arr'], p1: 'c', p2: 'd' }, + '--str=1': { str: '1' }, + '--hello-world=1': { helloWorld: '1' }, + '--hello-bool': { helloBool: true }, + '--helloBool': { helloBool: true }, + '--no-helloBool': { helloBool: false }, + '--noHelloBool': { helloBool: false }, + '-b': { bool: true }, + '-sb': { bool: true, str: '' }, + '-bs': { bool: true, str: '' }, + }; + + Object.entries(tests).forEach(([str, expected]) => { + it(`works for ${str}`, () => { + const actual = parseArguments(str.split(/\s+/), options); + + expect(actual).toEqual(expected as Arguments); + }); + }); +}); diff --git a/packages/angular/cli/package.json b/packages/angular/cli/package.json index ef122769ae77..0c44982c52a7 100644 --- a/packages/angular/cli/package.json +++ b/packages/angular/cli/package.json @@ -36,12 +36,10 @@ "@schematics/angular": "0.0.0", "@schematics/update": "0.0.0", "inquirer": "^6.1.0", - "json-schema-traverse": "^0.4.1", "opn": "^5.3.0", "rxjs": "~6.2.0", "semver": "^5.1.0", - "symbol-observable": "^1.2.0", - "yargs-parser": "^10.0.0" + "symbol-observable": "^1.2.0" }, "ng-update": { "migrations": "@schematics/angular/migrations/migration-collection.json" diff --git a/yarn.lock b/yarn.lock index 41f1c83ef417..00e64d738515 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7978,12 +7978,6 @@ yallist@^3.0.0, yallist@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.0.2.tgz#8452b4bb7e83c7c188d8041c1a837c773d6d8bb9" -yargs-parser@^10.0.0: - version "10.1.0" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-10.1.0.tgz#7202265b89f7e9e9f2e5765e0fe735a905edbaa8" - dependencies: - camelcase "^4.1.0" - yargs-parser@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-5.0.0.tgz#275ecf0d7ffe05c77e64e7c86e4cd94bf0e1228a" From 95176771587e12244e7ad15c2e35a85e38bfd706 Mon Sep 17 00:00:00 2001 From: Hans Date: Tue, 4 Sep 2018 14:45:15 -0700 Subject: [PATCH 0037/1703] feat(@angular-devkit/core): remove addUndefinedDefaults as default post transform And various bug fixes. --- .../core/src/json/schema/index.ts | 1 + .../core/src/json/schema/interface.ts | 42 +++- .../core/src/json/schema/registry.ts | 218 ++++++++++++++---- .../core/src/json/schema/registry_spec.ts | 10 +- .../core/src/json/schema/visitor.ts | 65 +++--- .../schematics/src/workflow/base.ts | 9 +- .../testing/schematic-test-runner.ts | 1 + .../schematics/tools/description.ts | 3 + .../tools/workflow/node-workflow.ts | 4 +- 9 files changed, 265 insertions(+), 88 deletions(-) diff --git a/packages/angular_devkit/core/src/json/schema/index.ts b/packages/angular_devkit/core/src/json/schema/index.ts index 862c127a6b06..e5ff6c4a63c5 100644 --- a/packages/angular_devkit/core/src/json/schema/index.ts +++ b/packages/angular_devkit/core/src/json/schema/index.ts @@ -9,6 +9,7 @@ export * from './interface'; export * from './pointer'; export * from './registry'; export * from './visitor'; +export * from './utility'; import * as transforms from './transforms'; diff --git a/packages/angular_devkit/core/src/json/schema/interface.ts b/packages/angular_devkit/core/src/json/schema/interface.ts index 6f0f092629c3..9861a30889ac 100644 --- a/packages/angular_devkit/core/src/json/schema/interface.ts +++ b/packages/angular_devkit/core/src/json/schema/interface.ts @@ -58,12 +58,13 @@ export interface SchemaValidatorResult { } export interface SchemaValidatorOptions { - withPrompts: boolean; + applyPreTransforms?: boolean; + applyPostTransforms?: boolean; + withPrompts?: boolean; } export interface SchemaValidator { - // tslint:disable-next-line:no-any - (data: any, options?: Partial): Observable; + (data: JsonValue, options?: SchemaValidatorOptions): Observable; } export interface SchemaFormatter { @@ -112,7 +113,42 @@ export type PromptProvider = (definitions: Array) export interface SchemaRegistry { compile(schema: Object): Observable; + flatten(schema: JsonObject | string): Observable; addFormat(format: SchemaFormat): void; addSmartDefaultProvider(source: string, provider: SmartDefaultProvider): void; usePromptProvider(provider: PromptProvider): void; + + /** + * Add a transformation step before the validation of any Json. + * @param {JsonVisitor} visitor The visitor to transform every value. + * @param {JsonVisitor[]} deps A list of other visitors to run before. + */ + addPreTransform(visitor: JsonVisitor, deps?: JsonVisitor[]): void; + + /** + * Add a transformation step after the validation of any Json. The JSON will not be validated + * after the POST, so if transformations are not compatible with the Schema it will not result + * in an error. + * @param {JsonVisitor} visitor The visitor to transform every value. + * @param {JsonVisitor[]} deps A list of other visitors to run before. + */ + addPostTransform(visitor: JsonVisitor, deps?: JsonVisitor[]): void; +} + +export interface JsonSchemaVisitor { + ( + current: JsonObject | JsonArray, + pointer: JsonPointer, + parentSchema?: JsonObject | JsonArray, + index?: string, + ): void; +} + +export interface JsonVisitor { + ( + value: JsonValue, + pointer: JsonPointer, + schema?: JsonObject, + root?: JsonObject | JsonArray, + ): Observable | JsonValue; } diff --git a/packages/angular_devkit/core/src/json/schema/registry.ts b/packages/angular_devkit/core/src/json/schema/registry.ts index 89130f10d563..785af9a7a7ac 100644 --- a/packages/angular_devkit/core/src/json/schema/registry.ts +++ b/packages/angular_devkit/core/src/json/schema/registry.ts @@ -7,12 +7,15 @@ */ import * as ajv from 'ajv'; import * as http from 'http'; -import { Observable, from, of as observableOf, throwError } from 'rxjs'; +import { Observable, from, of, throwError } from 'rxjs'; import { concatMap, map, switchMap, tap } from 'rxjs/operators'; +import * as Url from 'url'; import { BaseException } from '../../exception/exception'; -import { PartiallyOrderedSet, isObservable } from '../../utils'; -import { JsonObject, JsonValue } from '../interface'; +import { PartiallyOrderedSet, deepCopy, isObservable } from '../../utils'; +import { JsonArray, JsonObject, JsonValue, isJsonObject } from '../interface'; import { + JsonPointer, + JsonVisitor, PromptDefinition, PromptProvider, SchemaFormat, @@ -24,10 +27,7 @@ import { SchemaValidatorResult, SmartDefaultProvider, } from './interface'; -import { addUndefinedDefaults } from './transforms'; -import { JsonVisitor, visitJson } from './visitor'; - -const serialize = require('fast-json-stable-stringify'); +import { visitJson, visitJsonSchema } from './visitor'; // This interface should be exported from ajv, but they only export the class and not the type. interface AjvValidationError { @@ -37,6 +37,14 @@ interface AjvValidationError { validation: true; } +interface AjvRefMap { + refs: string[]; + refVal: any; // tslint:disable-line:no-any + schema: JsonObject; +} + +export type UriHandler = (uri: string) => Observable | null | undefined; + export class SchemaValidationException extends BaseException { public readonly errors: SchemaValidatorError[]; @@ -81,10 +89,11 @@ interface SchemaInfo { export class CoreSchemaRegistry implements SchemaRegistry { private _ajv: ajv.Ajv; private _uriCache = new Map(); + private _uriHandlers = new Set(); private _pre = new PartiallyOrderedSet(); private _post = new PartiallyOrderedSet(); + private _currentCompilationSchemaInfo?: SchemaInfo; - private _validatorCache = new Map(); private _smartDefaultKeyword = false; private _promptProvider?: PromptProvider; @@ -110,8 +119,6 @@ export class CoreSchemaRegistry implements SchemaRegistry { this._ajv.addMetaSchema(require('ajv/lib/refs/json-schema-draft-04.json')); this._ajv.addMetaSchema(require('ajv/lib/refs/json-schema-draft-06.json')); - - this.addPostTransform(addUndefinedDefaults); } private _fetch(uri: string): Promise { @@ -121,12 +128,24 @@ export class CoreSchemaRegistry implements SchemaRegistry { return Promise.resolve(maybeSchema); } + // Try all handlers, one after the other. + for (const maybeHandler of this._uriHandlers) { + const handler = maybeHandler(uri); + if (handler) { + // The AJV API only understands Promises. + return handler.pipe( + tap(json => this._uriCache.set(uri, json)), + ).toPromise(); + } + } + + // If none are found, handle using http client. return new Promise((resolve, reject) => { http.get(uri, res => { if (!res.statusCode || res.statusCode >= 300) { // Consume the rest of the data to free memory. res.resume(); - reject(`Request failed. Status Code: ${res.statusCode}`); + reject(new Error(`Request failed. Status Code: ${res.statusCode}`)); } else { res.setEncoding('utf8'); let data = ''; @@ -175,9 +194,22 @@ export class CoreSchemaRegistry implements SchemaRegistry { return {}; } - // tslint:disable-next-line:no-any - const id = (validate.schema as any).$id || (validate.schema as any).id; - let fullReference = (ref[0] === '#' && id) ? id + ref : ref; + let refMap = validate as AjvRefMap; + const rootRefMap = validate.root as AjvRefMap; + + // Resolve from the root if it's different. + if (validate.root && validate.schema !== rootRefMap.schema) { + refMap = rootRefMap; + } + + const schema = refMap.schema ? typeof refMap.schema == 'object' && refMap.schema : null; + const maybeId = schema ? (schema as JsonObject).id || (schema as JsonObject).$id : null; + + if (typeof maybeId == 'string') { + ref = Url.resolve(maybeId, ref); + } + + let fullReference = (ref[0] === '#' && maybeId) ? maybeId + ref : ref; if (fullReference.endsWith('#')) { fullReference = fullReference.slice(0, -1); } @@ -185,21 +217,95 @@ export class CoreSchemaRegistry implements SchemaRegistry { // tslint:disable-next-line:no-any const context = validate.refVal[(validate.refs as any)[fullReference]]; - return { context, schema: context && context.schema as JsonObject }; + if (typeof context == 'function') { + // Context will be a function if the schema isn't loaded yet, and an actual schema if it's + // synchronously available. + return { context, schema: context && context.schema as JsonObject }; + } else { + return { context: validate, schema: context as JsonObject }; + } } - compile(schema: JsonObject): Observable { - const schemaKey = serialize(schema); - const existingValidator = this._validatorCache.get(schemaKey); - if (existingValidator) { - return observableOf(existingValidator); + /** + * Flatten the Schema, resolving and replacing all the refs. Makes it into a synchronous schema + * that is also easier to traverse. Does not cache the result. + * + * @param schema The schema or URI to flatten. + * @returns An Observable of the flattened schema object. + */ + flatten(schema: JsonObject): Observable { + this._ajv.removeSchema(schema); + + // Supports both synchronous and asynchronous compilation, by trying the synchronous + // version first, then if refs are missing this will fails. + // We also add any refs from external fetched schemas so that those will also be used + // in synchronous (if available). + let validator: Observable; + try { + this._currentCompilationSchemaInfo = undefined; + validator = of(this._ajv.compile(schema)).pipe( + tap(() => this._currentCompilationSchemaInfo = undefined), + ); + } catch (e) { + // Propagate the error. + if (!(e instanceof (ajv.MissingRefError as {} as Function))) { + return throwError(e); + } + + this._currentCompilationSchemaInfo = undefined; + validator = from(this._ajv.compileAsync(schema)).pipe( + tap(() => this._currentCompilationSchemaInfo = undefined), + ); } + return validator.pipe( + switchMap(validate => { + const self = this; + + function visitor( + current: JsonObject | JsonArray, + pointer: JsonPointer, + parentSchema?: JsonObject | JsonArray, + index?: string, + ) { + if (current + && parentSchema + && index + && isJsonObject(current) + && current.hasOwnProperty('$ref') + && typeof current['$ref'] == 'string' + ) { + const resolved = self._resolver(current['$ref'] as string, validate); + + if (resolved.schema) { + (parentSchema as JsonObject)[index] = resolved.schema; + } + } + } + + const schema = deepCopy(validate.schema as JsonObject); + visitJsonSchema(schema, visitor); + + return of(schema); + }), + ); + } + + /** + * Compile and return a validation function for the Schema. + * + * @param schema The schema to validate. If a string, will fetch the schema before compiling it + * (using schema as a URI). + * @returns An Observable of the Validation function. + */ + compile(schema: JsonObject): Observable { const schemaInfo: SchemaInfo = { smartDefaultRecord: new Map(), promptDefinitions: [], }; + this._ajv.removeSchema(schema); + // Supports both synchronous and asynchronous compilation, by trying the synchronous // version first, then if refs are missing this will fails. // We also add any refs from external fetched schemas so that those will also be used @@ -207,7 +313,7 @@ export class CoreSchemaRegistry implements SchemaRegistry { let validator: Observable; try { this._currentCompilationSchemaInfo = schemaInfo; - validator = observableOf(this._ajv.compile(schema)); + validator = of(this._ajv.compile(schema)); } catch (e) { // Propagate the error. if (!(e instanceof (ajv.MissingRefError as {} as Function))) { @@ -223,29 +329,36 @@ export class CoreSchemaRegistry implements SchemaRegistry { return validator .pipe( - map(validate => (data, options) => { + map(validate => (data: JsonValue, options?: SchemaValidatorOptions) => { const validationOptions: SchemaValidatorOptions = { withPrompts: true, + applyPostTransforms: true, + applyPreTransforms: true, ...options, }; const validationContext = { promptFieldsWithValue: new Set(), }; - return observableOf(data).pipe( - ...[...this._pre].map(visitor => concatMap((data: JsonValue) => { - return visitJson(data, visitor, schema, this._resolver, validate); - })), - ).pipe( + let result = of(data); + if (validationOptions.applyPreTransforms) { + result = result.pipe( + ...[...this._pre].map(visitor => concatMap((data: JsonValue) => { + return visitJson(data, visitor, schema, this._resolver, validate); + })), + ); + } + + return result.pipe( switchMap(updateData => this._applySmartDefaults( updateData, schemaInfo.smartDefaultRecord, )), - switchMap((updatedData: JsonValue) => { + switchMap(updatedData => { const result = validate.call(validationContext, updatedData); return typeof result == 'boolean' - ? observableOf([updatedData, result]) + ? of([updatedData, result]) : from((result as Promise) .then(r => [updatedData, true]) .catch((err: Error | AjvValidationError) => { @@ -259,8 +372,8 @@ export class CoreSchemaRegistry implements SchemaRegistry { })); }), switchMap(([data, valid]: [JsonValue, boolean]) => { - if (!validationOptions.withPrompts) { - return observableOf([data, valid]); + if (validationOptions.withPrompts === false) { + return of([data, valid]); } const definitions = schemaInfo.promptDefinitions @@ -271,20 +384,26 @@ export class CoreSchemaRegistry implements SchemaRegistry { map(data => [data, valid]), ); } else { - return observableOf([data, valid]); + return of([data, valid]); } }), switchMap(([data, valid]: [JsonValue, boolean]) => { if (valid) { - return observableOf(data).pipe( - ...[...this._post].map(visitor => concatMap((data: JsonValue) => { - return visitJson(data as JsonValue, visitor, schema, this._resolver, validate); - })), - ).pipe( + let result = of(data); + + if (validationOptions.applyPostTransforms) { + result = result.pipe( + ...[...this._post].map(visitor => concatMap((data: JsonValue) => { + return visitJson(data, visitor, schema, this._resolver, validate); + })), + ); + } + + return result.pipe( map(data => [data, valid]), ); } else { - return observableOf([data, valid]); + return of([data, valid]); } }), map(([data, valid]: [JsonValue, boolean]) => { @@ -300,7 +419,6 @@ export class CoreSchemaRegistry implements SchemaRegistry { }), ); }), - tap(v => this._validatorCache.set(schemaKey, v)), ); } @@ -339,14 +457,14 @@ export class CoreSchemaRegistry implements SchemaRegistry { valid: true, compile: (schema, _parentSchema, it) => { const compilationSchemInfo = this._currentCompilationSchemaInfo; - if (!compilationSchemInfo) { - throw new Error('Invalid JSON schema compilation state'); + if (compilationSchemInfo === undefined) { + return () => true; } // We cheat, heavily. compilationSchemInfo.smartDefaultRecord.set( // tslint:disable-next-line:no-any - JSON.stringify((it as any).dataPathArr.slice(1, it.dataLevel + 1) as string[]), + JSON.stringify((it as any).dataPathArr.slice(1, (it as any).dataLevel + 1) as string[]), schema, ); @@ -364,6 +482,10 @@ export class CoreSchemaRegistry implements SchemaRegistry { } } + registerUriHandler(handler: UriHandler) { + this._uriHandlers.add(handler); + } + usePromptProvider(provider: PromptProvider) { const isSetup = !!this._promptProvider; @@ -378,8 +500,10 @@ export class CoreSchemaRegistry implements SchemaRegistry { valid: true, compile: (schema, parentSchema: JsonObject, it) => { const compilationSchemInfo = this._currentCompilationSchemaInfo; - if (!compilationSchemInfo) { + if (compilationSchemInfo === undefined) { throw new Error('Invalid JSON schema compilation state'); + } else if (compilationSchemInfo === null) { + return () => true; } // tslint:disable-next-line:no-any @@ -477,7 +601,7 @@ export class CoreSchemaRegistry implements SchemaRegistry { private _applyPrompts(data: T, prompts: Array): Observable { const provider = this._promptProvider; if (!provider) { - return observableOf(data); + return of(data); } prompts.sort((a, b) => b.priority - a.priority); @@ -564,16 +688,16 @@ export class CoreSchemaRegistry implements SchemaRegistry { data: T, smartDefaults: Map, ): Observable { - return observableOf(data).pipe( + return of(data).pipe( ...[...smartDefaults.entries()].map(([pointer, schema]) => { return concatMap(data => { const fragments = JSON.parse(pointer); const source = this._sourceMap.get((schema as JsonObject).$source as string); - let value = source ? source(schema) : observableOf(undefined); + let value = source ? source(schema) : of(undefined); if (!isObservable(value)) { - value = observableOf(value); + value = of(value); } return (value as Observable<{}>).pipe( diff --git a/packages/angular_devkit/core/src/json/schema/registry_spec.ts b/packages/angular_devkit/core/src/json/schema/registry_spec.ts index 773a87a479bf..b221ac9cde91 100644 --- a/packages/angular_devkit/core/src/json/schema/registry_spec.ts +++ b/packages/angular_devkit/core/src/json/schema/registry_spec.ts @@ -9,11 +9,13 @@ import { of as observableOf } from 'rxjs'; import { map, mergeMap } from 'rxjs/operators'; import { CoreSchemaRegistry } from './registry'; +import { addUndefinedDefaults } from './transforms'; describe('CoreSchemaRegistry', () => { it('works asynchronously', done => { const registry = new CoreSchemaRegistry(); + registry.addPostTransform(addUndefinedDefaults); const data: any = {}; // tslint:disable-line:no-any registry @@ -45,6 +47,7 @@ describe('CoreSchemaRegistry', () => { it('supports pre transforms', done => { const registry = new CoreSchemaRegistry(); + registry.addPostTransform(addUndefinedDefaults); const data: any = {}; // tslint:disable-line:no-any registry.addPreTransform((data, ptr) => { @@ -82,6 +85,7 @@ describe('CoreSchemaRegistry', () => { it('supports local references', done => { const registry = new CoreSchemaRegistry(); + registry.addPostTransform(addUndefinedDefaults); const data = { numbers: { one: 1 } }; registry @@ -108,6 +112,7 @@ describe('CoreSchemaRegistry', () => { it('fails on invalid additionalProperties', done => { const registry = new CoreSchemaRegistry(); + registry.addPostTransform(addUndefinedDefaults); const data = { notNum: 'foo' }; registry @@ -129,6 +134,7 @@ describe('CoreSchemaRegistry', () => { it('fails on invalid additionalProperties async', done => { const registry = new CoreSchemaRegistry(); + registry.addPostTransform(addUndefinedDefaults); const data = { notNum: 'foo' }; registry @@ -156,6 +162,7 @@ describe('CoreSchemaRegistry', () => { // (i.e. not relyign on the observable). it('works synchronously', done => { const registry = new CoreSchemaRegistry(); + registry.addPostTransform(addUndefinedDefaults); const data: any = {}; // tslint:disable-line:no-any let isDone = false; @@ -298,7 +305,7 @@ describe('CoreSchemaRegistry', () => { return schema['blue']; }); - registry.addSmartDefaultProvider('test3', (schema) => { + registry.addSmartDefaultProvider('test3', () => { return [ 1, 2, 3 ]; }); @@ -369,6 +376,7 @@ describe('CoreSchemaRegistry', () => { it('adds undefined properties', done => { const registry = new CoreSchemaRegistry(); + registry.addPostTransform(addUndefinedDefaults); const data: any = {}; // tslint:disable-line:no-any registry diff --git a/packages/angular_devkit/core/src/json/schema/visitor.ts b/packages/angular_devkit/core/src/json/schema/visitor.ts index 8091785cf06d..0708c3564c51 100644 --- a/packages/angular_devkit/core/src/json/schema/visitor.ts +++ b/packages/angular_devkit/core/src/json/schema/visitor.ts @@ -9,27 +9,9 @@ import { Observable, concat, from, of as observableOf } from 'rxjs'; import { concatMap, ignoreElements, mergeMap, tap } from 'rxjs/operators'; import { isObservable } from '../../utils'; import { JsonArray, JsonObject, JsonValue } from '../interface'; -import { JsonPointer } from './interface'; +import { JsonPointer, JsonSchemaVisitor, JsonVisitor } from './interface'; import { buildJsonPointer, joinJsonPointer } from './pointer'; -export interface JsonSchemaVisitor { - ( - current: JsonObject | JsonArray, - pointer: JsonPointer, - parentSchema?: JsonObject | JsonArray, - index?: string, - ): void; -} - -export interface JsonVisitor { - ( - value: JsonValue, - pointer: JsonPointer, - schema?: JsonObject, - root?: JsonObject | JsonArray, - ): Observable | JsonValue; -} - export interface ReferenceResolver { (ref: string, context?: ContextT): { context?: ContextT, schema?: JsonObject }; @@ -167,6 +149,13 @@ export function visitJsonSchema(schema: JsonObject, visitor: JsonSchemaVisitor) not: true, }; + const arrayKeywords = { + items: true, + allOf: true, + anyOf: true, + oneOf: true, + }; + const propsKeywords = { definitions: true, properties: true, @@ -188,23 +177,11 @@ export function visitJsonSchema(schema: JsonObject, visitor: JsonSchemaVisitor) for (const key of Object.keys(schema)) { const sch = schema[key]; - if (Array.isArray(sch)) { - if (key == 'items') { - for (let i = 0; i < sch.length; i++) { - _traverse( - sch[i] as JsonArray, - joinJsonPointer(jsonPtr, key, '' + i), - rootSchema, - schema, - '' + i, - ); - } - } - } else if (key in propsKeywords) { + if (key in propsKeywords) { if (sch && typeof sch == 'object') { for (const prop of Object.keys(sch)) { _traverse( - sch[prop] as JsonObject, + (sch as JsonObject)[prop] as JsonObject, joinJsonPointer(jsonPtr, key, prop), rootSchema, schema, @@ -214,6 +191,28 @@ export function visitJsonSchema(schema: JsonObject, visitor: JsonSchemaVisitor) } } else if (key in keywords) { _traverse(sch as JsonObject, joinJsonPointer(jsonPtr, key), rootSchema, schema, key); + } else if (key in arrayKeywords) { + if (Array.isArray(sch)) { + for (let i = 0; i < sch.length; i++) { + _traverse( + sch[i] as JsonArray, + joinJsonPointer(jsonPtr, key, '' + i), + rootSchema, + sch, + '' + i, + ); + } + } + } else if (Array.isArray(sch)) { + for (let i = 0; i < sch.length; i++) { + _traverse( + sch[i] as JsonArray, + joinJsonPointer(jsonPtr, key, '' + i), + rootSchema, + sch, + '' + i, + ); + } } } } diff --git a/packages/angular_devkit/schematics/src/workflow/base.ts b/packages/angular_devkit/schematics/src/workflow/base.ts index ae5ba62f8bfb..134cde1e61f6 100644 --- a/packages/angular_devkit/schematics/src/workflow/base.ts +++ b/packages/angular_devkit/schematics/src/workflow/base.ts @@ -62,7 +62,14 @@ export abstract class BaseWorkflow implements Workflow { constructor(options: BaseWorkflowOptions) { this._host = options.host; this._engineHost = options.engineHost; - this._registry = options.registry || new schema.CoreSchemaRegistry(standardFormats); + + if (options.registry) { + this._registry = options.registry; + } else { + this._registry = new schema.CoreSchemaRegistry(standardFormats); + this._registry.addPostTransform(schema.transforms.addUndefinedDefaults); + } + this._engine = new SchematicEngine(this._engineHost, this); this._context = []; diff --git a/packages/angular_devkit/schematics/testing/schematic-test-runner.ts b/packages/angular_devkit/schematics/testing/schematic-test-runner.ts index a105d260bede..1c0c06ac1ce8 100644 --- a/packages/angular_devkit/schematics/testing/schematic-test-runner.ts +++ b/packages/angular_devkit/schematics/testing/schematic-test-runner.ts @@ -57,6 +57,7 @@ export class SchematicTestRunner { this._logger = new logging.Logger('test'); const registry = new schema.CoreSchemaRegistry(formats.standardFormats); + registry.addPostTransform(schema.transforms.addUndefinedDefaults); this._engineHost.registerOptionsTransform(validateOptionsWithSchema(registry)); this._engineHost.registerTaskExecutor(BuiltinTaskExecutor.NodePackage); diff --git a/packages/angular_devkit/schematics/tools/description.ts b/packages/angular_devkit/schematics/tools/description.ts index faf18574635c..a6501af85375 100644 --- a/packages/angular_devkit/schematics/tools/description.ts +++ b/packages/angular_devkit/schematics/tools/description.ts @@ -10,6 +10,7 @@ import { Collection, CollectionDescription, RuleFactory, + Schematic, SchematicDescription, TypedSchematicContext, } from '../src'; @@ -45,6 +46,8 @@ export interface FileSystemSchematicDescription extends FileSystemSchematicJsonD */ export declare type FileSystemCollection = Collection; +export declare type FileSystemSchematic + = Schematic; export declare type FileSystemCollectionDesc = CollectionDescription; export declare type FileSystemSchematicDesc diff --git a/packages/angular_devkit/schematics/tools/workflow/node-workflow.ts b/packages/angular_devkit/schematics/tools/workflow/node-workflow.ts index 740f23e27a97..20ed4d9a9382 100644 --- a/packages/angular_devkit/schematics/tools/workflow/node-workflow.ts +++ b/packages/angular_devkit/schematics/tools/workflow/node-workflow.ts @@ -5,9 +5,8 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ -import { Path, schema, virtualFs } from '@angular-devkit/core'; +import { Path, virtualFs } from '@angular-devkit/core'; import { - formats, workflow, } from '@angular-devkit/schematics'; // tslint:disable-line:no-implicit-dependencies import { BuiltinTaskExecutor } from '../../tasks/node'; @@ -30,7 +29,6 @@ export class NodeWorkflow extends workflow.BaseWorkflow { const engineHost = new NodeModulesEngineHost(); super({ host: host, - registry: new schema.CoreSchemaRegistry(formats.standardFormats), engineHost: engineHost, force: options.force, From fd7bcd25fe1af58bf25c22968852aa2cbdb4dde1 Mon Sep 17 00:00:00 2001 From: Hans Date: Tue, 4 Sep 2018 14:45:59 -0700 Subject: [PATCH 0038/1703] fix(@angular-devkit/core): use architect key only if it exists And if target didnt exist. --- .../angular_devkit/core/src/workspace/workspace.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/angular_devkit/core/src/workspace/workspace.ts b/packages/angular_devkit/core/src/workspace/workspace.ts index a4252a9ee5b7..d03bdb42c898 100644 --- a/packages/angular_devkit/core/src/workspace/workspace.ts +++ b/packages/angular_devkit/core/src/workspace/workspace.ts @@ -63,6 +63,7 @@ export class Workspace { constructor(private _root: Path, private _host: virtualFs.Host<{}>) { this._registry = new schema.CoreSchemaRegistry(); + this._registry.addPostTransform(schema.transforms.addUndefinedDefaults); } loadWorkspaceFromJson(json: {}) { @@ -232,7 +233,9 @@ export class Workspace { let workspaceTool = this._workspace[toolName]; // Try falling back to 'architect' if 'targets' is not there or is empty. - if ((!workspaceTool || Object.keys(workspaceTool).length === 0) && toolName === 'targets') { + if ((!workspaceTool || Object.keys(workspaceTool).length === 0) + && toolName === 'targets' + && this._workspace['architect']) { workspaceTool = this._workspace['architect']; } @@ -257,11 +260,12 @@ export class Workspace { let projectTool = workspaceProject[toolName]; // Try falling back to 'architect' if 'targets' is not there or is empty. - if ((!projectTool || Object.keys(projectTool).length === 0) && toolName === 'targets') { + if ((!projectTool || Object.keys(projectTool).length === 0) + && workspaceProject['architect'] + && toolName === 'targets') { projectTool = workspaceProject['architect']; } - if (!projectTool) { throw new ProjectToolNotFoundException(toolName); } From de7ec8487299bc2fe4a9820fc52c5b1d9af2a711 Mon Sep 17 00:00:00 2001 From: Hans Date: Tue, 4 Sep 2018 14:54:37 -0700 Subject: [PATCH 0039/1703] feat(@angular/cli): add --helpJson (or --help-json) too all commands And other refactors. The interface for the JSON is available in command.ts (the CommandDescription). --- packages/angular/cli/commands/add-impl.ts | 57 +-- packages/angular/cli/commands/add.json | 31 +- packages/angular/cli/commands/build-impl.ts | 6 +- packages/angular/cli/commands/build.json | 28 +- packages/angular/cli/commands/config-impl.ts | 10 +- packages/angular/cli/commands/config.json | 4 +- .../angular/cli/commands/definitions.json | 64 +++ packages/angular/cli/commands/doc-impl.ts | 22 +- packages/angular/cli/commands/doc.json | 4 +- packages/angular/cli/commands/e2e.json | 21 +- .../angular/cli/commands/easter-egg-impl.ts | 2 +- packages/angular/cli/commands/easter-egg.json | 4 +- packages/angular/cli/commands/eject-impl.ts | 15 +- packages/angular/cli/commands/eject.json | 9 +- .../angular/cli/commands/generate-impl.ts | 104 ++-- packages/angular/cli/commands/generate.json | 49 +- packages/angular/cli/commands/help-impl.ts | 33 +- packages/angular/cli/commands/help.json | 5 +- packages/angular/cli/commands/lint.json | 41 +- packages/angular/cli/commands/new-impl.ts | 47 +- packages/angular/cli/commands/new.json | 53 +-- packages/angular/cli/commands/run.json | 39 +- packages/angular/cli/commands/serve.json | 27 +- packages/angular/cli/commands/test.json | 27 +- packages/angular/cli/commands/update-impl.ts | 86 ++-- packages/angular/cli/commands/update.json | 43 +- packages/angular/cli/commands/version-impl.ts | 2 +- packages/angular/cli/commands/version.json | 5 +- packages/angular/cli/commands/xi18n.json | 23 +- packages/angular/cli/lib/cli/index.ts | 5 +- .../angular/cli/models/architect-command.ts | 217 +++------ packages/angular/cli/models/interface.ts | 110 +++++ packages/angular/cli/models/json-schema.ts | 109 ----- .../angular/cli/models/schematic-command.ts | 450 ++++++++---------- packages/angular/cli/utilities/json-schema.ts | 201 ++++++++ packages/angular/cli/utilities/project.ts | 8 +- .../e2e/tests/basic/in-project-logic.ts | 15 +- .../e2e/tests/generate/help-output.ts | 6 +- tests/legacy-cli/e2e/utils/assets.ts | 2 +- tests/legacy-cli/e2e/utils/process.ts | 11 +- 40 files changed, 998 insertions(+), 997 deletions(-) create mode 100644 packages/angular/cli/commands/definitions.json create mode 100644 packages/angular/cli/models/interface.ts delete mode 100644 packages/angular/cli/models/json-schema.ts create mode 100644 packages/angular/cli/utilities/json-schema.ts diff --git a/packages/angular/cli/commands/add-impl.ts b/packages/angular/cli/commands/add-impl.ts index ba33b8bcc95f..e2492826ea57 100644 --- a/packages/angular/cli/commands/add-impl.ts +++ b/packages/angular/cli/commands/add-impl.ts @@ -9,44 +9,24 @@ // tslint:disable:no-global-tslint-disable no-any import { tags, terminal } from '@angular-devkit/core'; import { NodePackageDoesNotSupportSchematics } from '@angular-devkit/schematics/tools'; -import { parseOptions } from '../models/command-runner'; -import { SchematicCommand } from '../models/schematic-command'; +import { Arguments } from '../models/interface'; +import { BaseSchematicOptions, SchematicCommand } from '../models/schematic-command'; import { NpmInstall } from '../tasks/npm-install'; import { getPackageManager } from '../utilities/config'; +export interface AddCommandOptions extends BaseSchematicOptions { + collection: string; + help?: boolean; + help_json?: boolean; +} -export class AddCommand extends SchematicCommand { +export class AddCommand< + T extends AddCommandOptions = AddCommandOptions, +> extends SchematicCommand { readonly allowPrivateSchematics = true; - private async _parseSchematicOptions(collectionName: string): Promise { - const schematicOptions = await this.getOptions({ - schematicName: 'ng-add', - collectionName, - }); - this.addOptions(schematicOptions); - - return parseOptions(this._rawArgs, this.options); - } - - validate(options: any) { - const collectionName = options._[0]; - - if (!collectionName) { - this.logger.fatal( - `The "ng add" command requires a name argument to be specified eg. ` - + `${terminal.yellow('ng add [name] ')}. For more details, use "ng help".`, - ); - - return false; - } - - return true; - } - - async run(options: any) { - const firstArg = options._[0]; - - if (!firstArg) { + async run(options: AddCommandOptions & Arguments) { + if (!options.collection) { this.logger.fatal( `The "ng add" command requires a name argument to be specified eg. ` + `${terminal.yellow('ng add [name] ')}. For more details, use "ng help".`, @@ -59,16 +39,16 @@ export class AddCommand extends SchematicCommand { const npmInstall: NpmInstall = require('../tasks/npm-install').default; - const packageName = firstArg.startsWith('@') - ? firstArg.split('/', 2).join('/') - : firstArg.split('/', 1)[0]; + const packageName = options.collection.startsWith('@') + ? options.collection.split('/', 2).join('/') + : options.collection.split('/', 1)[0]; // Remove the tag/version from the package name. const collectionName = ( packageName.startsWith('@') ? packageName.split('@', 2).join('@') : packageName.split('@', 1).join('@') - ) + firstArg.slice(packageName.length); + ) + options.collection.slice(packageName.length); // We don't actually add the package to package.json, that would be the work of the package // itself. @@ -79,11 +59,8 @@ export class AddCommand extends SchematicCommand { this.project.root, ); - // Reparse the options with the new schematic accessible. - options = await this._parseSchematicOptions(collectionName); - const runOptions = { - schematicOptions: options, + schematicOptions: options['--'] || [], workingDir: this.project.root, collectionName, schematicName: 'ng-add', diff --git a/packages/angular/cli/commands/add.json b/packages/angular/cli/commands/add.json index 04a9f3a888ed..f61b1ac2b6e6 100644 --- a/packages/angular/cli/commands/add.json +++ b/packages/angular/cli/commands/add.json @@ -1,6 +1,6 @@ { "$schema": "http://json-schema.org/schema", - "id": "AddCommandOptions", + "$id": "ng-cli://commands/add.json", "description": "Add support for a library to your project.", "$longDescription": "", @@ -8,16 +8,23 @@ "$impl": "./add-impl#AddCommand", "type": "object", - "properties": { - "collection": { - "type": "string", - "description": "The package to be added.", - "$default": { - "$source": "argv", - "index": 0 - } + "allOf": [ + { + "properties": { + "collection": { + "type": "string", + "description": "The package to be added.", + "$default": { + "$source": "argv", + "index": 0 + } + } + }, + "required": [ + ] + }, + { + "$ref": "./definitions.json#/definitions/base" } - }, - "required": [ ] -} \ No newline at end of file +} diff --git a/packages/angular/cli/commands/build-impl.ts b/packages/angular/cli/commands/build-impl.ts index 60b82df73e29..46484827de12 100644 --- a/packages/angular/cli/commands/build-impl.ts +++ b/packages/angular/cli/commands/build-impl.ts @@ -12,15 +12,11 @@ import { Version } from '../upgrade/version'; export class BuildCommand extends ArchitectCommand { public readonly target = 'build'; - public validate(options: ArchitectCommandOptions) { + public async run(options: ArchitectCommandOptions) { // Check Angular and TypeScript versions. Version.assertCompatibleAngularVersion(this.project.root); Version.assertTypescriptVersion(this.project.root); - return super.validate(options); - } - - public async run(options: ArchitectCommandOptions) { return this.runArchitectTarget(options); } } diff --git a/packages/angular/cli/commands/build.json b/packages/angular/cli/commands/build.json index 247914d3ba3f..db73c39b0605 100644 --- a/packages/angular/cli/commands/build.json +++ b/packages/angular/cli/commands/build.json @@ -1,6 +1,6 @@ { "$schema": "http://json-schema.org/schema", - "id": "BuildCommandOptions", + "$id": "ng-cli://commands/build.json", "description": "Builds your app and places it into the output path (dist/ by default).", "$longDescription": "", @@ -9,26 +9,8 @@ "$type": "architect", "$impl": "./build-impl#BuildCommand", - "type": "object", - "properties": { - "project": { - "type": "string", - "description": "The name of the project to build.", - "$default": { - "$source": "argv", - "index": 0 - } - }, - "configuration": { - "description": "Specify the configuration to use.", - "type": "string", - "aliases": ["c"] - }, - "prod": { - "description": "Flag to set configuration to 'production'.", - "type": "boolean" - } - }, - "required": [ + "allOf": [ + { "$ref": "./definitions.json#/definitions/architect" }, + { "$ref": "./definitions.json#/definitions/base" } ] -} \ No newline at end of file +} diff --git a/packages/angular/cli/commands/config-impl.ts b/packages/angular/cli/commands/config-impl.ts index b1850b43999b..7499f542cc37 100644 --- a/packages/angular/cli/commands/config-impl.ts +++ b/packages/angular/cli/commands/config-impl.ts @@ -17,7 +17,7 @@ import { tags, } from '@angular-devkit/core'; import { writeFileSync } from 'fs'; -import { Command } from '../models/command'; +import { BaseCommandOptions, Command } from '../models/command'; import { getWorkspace, getWorkspaceRaw, @@ -26,7 +26,7 @@ import { } from '../utilities/config'; -export interface ConfigOptions { +export interface ConfigOptions extends BaseCommandOptions { jsonPath: string; value?: string; global?: boolean; @@ -178,8 +178,8 @@ function normalizeValue(value: string, path: string): JsonValue { return value; } -export class ConfigCommand extends Command { - public run(options: ConfigOptions) { +export class ConfigCommand extends Command { + public async run(options: T) { const level = options.global ? 'global' : 'local'; let config = @@ -210,7 +210,7 @@ export class ConfigCommand extends Command { } } - private get(config: experimental.workspace.WorkspaceSchema, options: ConfigOptions) { + private get(config: experimental.workspace.WorkspaceSchema, options: T) { let value; if (options.jsonPath) { value = getValueFromPath(config as {} as JsonObject, options.jsonPath); diff --git a/packages/angular/cli/commands/config.json b/packages/angular/cli/commands/config.json index cdcce065041c..35521ba50406 100644 --- a/packages/angular/cli/commands/config.json +++ b/packages/angular/cli/commands/config.json @@ -1,6 +1,6 @@ { "$schema": "http://json-schema.org/schema", - "id": "ConfigCommandOptions", + "$id": "ng-cli://commands/config.json", "description": "Get/set configuration values.", "$longDescription": "", @@ -36,4 +36,4 @@ }, "required": [ ] -} \ No newline at end of file +} diff --git a/packages/angular/cli/commands/definitions.json b/packages/angular/cli/commands/definitions.json new file mode 100644 index 000000000000..5dd2700d5321 --- /dev/null +++ b/packages/angular/cli/commands/definitions.json @@ -0,0 +1,64 @@ +{ + "$schema": "http://json-schema.org/schema", + "$id": "ng-cli://commands/definitions.json", + + "definitions": { + "architect": { + "properties": { + "project": { + "type": "string", + "description": "The name of the project to build.", + "$default": { + "$source": "argv", + "index": 0 + } + }, + "configuration": { + "description": "Specify the configuration to use.", + "type": "string", + "aliases": [ + "c" + ] + }, + "prod": { + "description": "Flag to set configuration to 'production'.", + "type": "boolean" + } + } + }, + "base": { + "type": "object", + "properties": { + "help": { + "type": "boolean", + "description": "Shows a help message." + }, + "helpJson": { + "type": "boolean", + "description": "Shows the metadata associated with each flags, in JSON format." + } + } + }, + "schematic": { + "properties": { + "dryRun": { + "type": "boolean", + "default": false, + "aliases": [ "d" ], + "description": "Run through without making any changes." + }, + "force": { + "type": "boolean", + "default": false, + "aliases": [ "f" ], + "description": "Forces overwriting of files." + }, + "interactive": { + "type": "boolean", + "default": "true", + "description": "Disables interactive inputs (i.e., prompts)." + } + } + } + } +} diff --git a/packages/angular/cli/commands/doc-impl.ts b/packages/angular/cli/commands/doc-impl.ts index b9f6c9451fe4..543c9ecca89a 100644 --- a/packages/angular/cli/commands/doc-impl.ts +++ b/packages/angular/cli/commands/doc-impl.ts @@ -6,31 +6,23 @@ * found in the LICENSE file at https://angular.io/license */ -import { Command } from '../models/command'; +import { BaseCommandOptions, Command } from '../models/command'; const opn = require('opn'); -export interface Options { +export interface DocCommandOptions extends BaseCommandOptions { keyword: string; search?: boolean; } -export class DocCommand extends Command { - public validate(options: Options) { - if (!options.keyword) { - this.logger.error(`keyword argument is required.`); - - return false; - } - - return true; - } - - public async run(options: Options) { +export class DocCommand extends Command { + public async run(options: T) { let searchUrl = `https://angular.io/api?query=${options.keyword}`; if (options.search) { searchUrl = `https://www.google.com/search?q=site%3Aangular.io+${options.keyword}`; } - return opn(searchUrl); + return opn(searchUrl, { + wait: false, + }); } } diff --git a/packages/angular/cli/commands/doc.json b/packages/angular/cli/commands/doc.json index d0fd2ce0c4fa..e3649a987960 100644 --- a/packages/angular/cli/commands/doc.json +++ b/packages/angular/cli/commands/doc.json @@ -1,6 +1,6 @@ { "$schema": "http://json-schema.org/schema", - "id": "DocCommandOptions", + "$id": "ng-cli://commands/doc.json", "description": "Opens the official Angular API documentation for a given keyword.", "$longDescription": "", @@ -27,4 +27,4 @@ }, "required": [ ] -} \ No newline at end of file +} diff --git a/packages/angular/cli/commands/e2e.json b/packages/angular/cli/commands/e2e.json index 8f659194cf7d..6e7b1079a46a 100644 --- a/packages/angular/cli/commands/e2e.json +++ b/packages/angular/cli/commands/e2e.json @@ -1,7 +1,7 @@ { "$schema": "http://json-schema.org/schema", - "id": "E2eCommandOptions", - "description": "", + "$id": "ng-cli://commands/e2e.json", + "description": "Runs the end-to-end tests.", "$longDescription": "", "$aliases": [ "e" ], @@ -10,17 +10,8 @@ "$impl": "./e2e-impl#E2eCommand", "type": "object", - "properties": { - "configuration": { - "description": "Specify the configuration to use.", - "type": "string", - "aliases": ["c"] - }, - "prod": { - "description": "Flag to set configuration to 'production'.", - "type": "boolean" - } - }, - "required": [ + "allOf": [ + { "$ref": "./definitions.json#/definitions/architect" }, + { "$ref": "./definitions.json#/definitions/base" } ] -} \ No newline at end of file +} diff --git a/packages/angular/cli/commands/easter-egg-impl.ts b/packages/angular/cli/commands/easter-egg-impl.ts index c91fd44fde6b..197bf9e675f4 100644 --- a/packages/angular/cli/commands/easter-egg-impl.ts +++ b/packages/angular/cli/commands/easter-egg-impl.ts @@ -14,7 +14,7 @@ function pickOne(of: string[]): string { } export class AwesomeCommand extends Command { - run() { + async run() { const phrase = pickOne([ `You're on it, there's nothing for me to do!`, `Let's take a look... nope, it's all good!`, diff --git a/packages/angular/cli/commands/easter-egg.json b/packages/angular/cli/commands/easter-egg.json index 18833da40982..04e7afa1e45b 100644 --- a/packages/angular/cli/commands/easter-egg.json +++ b/packages/angular/cli/commands/easter-egg.json @@ -1,6 +1,6 @@ { "$schema": "http://json-schema.org/schema", - "id": "EasterEggCommandOptions", + "$id": "ng-cli://commands/easter-egg.json", "description": "", "$longDescription": "", "$hidden": true, @@ -8,4 +8,4 @@ "$impl": "./easter-egg-impl#AwesomeCommand", "type": "object" -} \ No newline at end of file +} diff --git a/packages/angular/cli/commands/eject-impl.ts b/packages/angular/cli/commands/eject-impl.ts index cf755606eca2..cdfe324e2231 100644 --- a/packages/angular/cli/commands/eject-impl.ts +++ b/packages/angular/cli/commands/eject-impl.ts @@ -7,19 +7,12 @@ */ import { tags } from '@angular-devkit/core'; -import { Command, Option } from '../models/command'; +import { Command } from '../models/command'; export class EjectCommand extends Command { - public readonly name = 'eject'; - public readonly description = 'Temporarily disabled. Ejects your app and output the proper ' - + 'webpack configuration and scripts.'; - public readonly arguments: string[] = []; - public readonly options: Option[] = []; - public static aliases = []; - - run() { - this.logger.info(tags.stripIndents` + async run() { + this.logger.error(tags.stripIndents` The 'eject' command has been temporarily disabled, as it is not yet compatible with the new angular.json format. The new configuration format provides further flexibility to modify the configuration of your workspace without ejecting. Ejection will be re-enabled in a future @@ -27,5 +20,7 @@ export class EjectCommand extends Command { If you need to eject today, use CLI 1.7 to eject your project. `); + + return 1; } } diff --git a/packages/angular/cli/commands/eject.json b/packages/angular/cli/commands/eject.json index aa90f156c46b..d5c527d02166 100644 --- a/packages/angular/cli/commands/eject.json +++ b/packages/angular/cli/commands/eject.json @@ -1,6 +1,6 @@ { "$schema": "http://json-schema.org/schema", - "id": "CommandOptions", + "$id": "ng-cli://commands/eject.json", "description": "Temporarily disabled. Ejects your app and output the proper webpack configuration and scripts.", "$longDescription": "", @@ -8,5 +8,8 @@ "$scope": "in", "$impl": "./eject-impl#EjectCommand", - "type": "object" -} \ No newline at end of file + "type": "object", + "allOf": [ + { "$ref": "./definitions.json#/definitions/base" } + ] +} diff --git a/packages/angular/cli/commands/generate-impl.ts b/packages/angular/cli/commands/generate-impl.ts index 7e2580751137..7a58da674214 100644 --- a/packages/angular/cli/commands/generate-impl.ts +++ b/packages/angular/cli/commands/generate-impl.ts @@ -7,63 +7,64 @@ */ // tslint:disable:no-global-tslint-disable no-any -import { tags, terminal } from '@angular-devkit/core'; -import { SchematicCommand } from '../models/schematic-command'; +import { terminal } from '@angular-devkit/core'; +import { Option } from '../models/interface'; +import { BaseSchematicOptions, SchematicCommand } from '../models/schematic-command'; +import { parseJsonSchemaToOptions } from '../utilities/json-schema'; +export interface GenerateCommandOptions extends BaseSchematicOptions { + schematic?: string; +} -export class GenerateCommand extends SchematicCommand { - private initialized = false; - public async initialize(options: any) { - if (this.initialized) { - return; - } +export class GenerateCommand< + T extends GenerateCommandOptions = GenerateCommandOptions, +> extends SchematicCommand { + async initialize(options: T) { await super.initialize(options); - this.initialized = true; + // Fill up the schematics property of the command description. const [collectionName, schematicName] = this.parseSchematicInfo(options); - if (!!schematicName) { - const schematicOptions = await this.getOptions({ - schematicName, - collectionName, - }); - this.addOptions(schematicOptions); - } - } - validate(options: any): boolean | Promise { - if (!options.schematic) { - this.logger.error(tags.oneLine` - The "ng generate" command requires a - schematic name to be specified. - For more details, use "ng help".`); + const collection = this.getCollection(collectionName); + this.description.schematics = {}; - return false; - } + const schematicNames = schematicName ? [schematicName] : collection.listSchematicNames(); + + for (const name of schematicNames) { + const schematic = this.getSchematic(collection, name, true); + let options: Option[] = []; + if (schematic.description.schemaJson) { + options = await parseJsonSchemaToOptions( + this._workflow.registry, + schematic.description.schemaJson, + ); + } - return true; + this.description.schematics[`${collectionName}:${name}`] = options; + } } - public run(options: any) { + public async run(options: T) { const [collectionName, schematicName] = this.parseSchematicInfo(options); - // remove the schematic name from the options - delete options.schematic; + if (!schematicName || !collectionName) { + return this.printHelp(options); + } return this.runSchematic({ collectionName, schematicName, - schematicOptions: this.removeLocalOptions(options), - debug: options.debug, - dryRun: options.dryRun, - force: options.force, - interactive: options.interactive, + schematicOptions: options['--'] || [], + debug: !!options.debug || false, + dryRun: !!options.dryRun || false, + force: !!options.force || false, }); } - private parseSchematicInfo(options: any) { + private parseSchematicInfo(options: { schematic?: string }): [string, string | undefined] { let collectionName = this.getDefaultSchematicCollection(); - let schematicName: string = options.schematic; + let schematicName = options.schematic; if (schematicName) { if (schematicName.includes(':')) { @@ -74,33 +75,14 @@ export class GenerateCommand extends SchematicCommand { return [collectionName, schematicName]; } - public printHelp(_name: string, _description: string, options: any) { - const schematicName = options._[0]; - if (schematicName) { - const optsWithoutSchematic = this.options - .filter(o => !(o.name === 'schematic' && this.isArgument(o))); - this.printHelpUsage(`generate ${schematicName}`, optsWithoutSchematic); - this.printHelpOptions(this.options); - } else { - this.printHelpUsage('generate', this.options); - const engineHost = this.getEngineHost(); - const [collectionName] = this.parseSchematicInfo(options); - const collection = this.getCollection(collectionName); - const schematicNames: string[] = engineHost.listSchematicNames(collection.description); - this.logger.info('Available schematics:'); - schematicNames.forEach(schematicName => { - this.logger.info(` ${schematicName}`); - }); - - this.logger.warn(`\nTo see help for a schematic run:`); + public async printHelp(options: T) { + await super.printHelp(options); + + if (Object.keys(this.description.schematics || {}).length == 1) { + this.logger.info(`\nTo see help for a schematic run:`); this.logger.info(terminal.cyan(` ng generate --help`)); } - } - - private removeLocalOptions(options: any): any { - const opts = Object.assign({}, options); - delete opts.interactive; - return opts; + return 0; } } diff --git a/packages/angular/cli/commands/generate.json b/packages/angular/cli/commands/generate.json index 3a784e3d47b9..f8668058cc9f 100644 --- a/packages/angular/cli/commands/generate.json +++ b/packages/angular/cli/commands/generate.json @@ -1,6 +1,6 @@ { "$schema": "http://json-schema.org/schema", - "id": "GenerateCommandOptions", + "$id": "ng-cli://commands/generate.json", "description": "Generates and/or modifies files based on a schematic.", "$longDescription": "", @@ -9,34 +9,23 @@ "$type": "schematics", "$impl": "./generate-impl#GenerateCommand", - "type": "object", - "properties": { - "schematic": { - "type": "string", - "description": "The schematic or collection:schematic to generate.", - "$default": { - "$source": "argv", - "index": 0 - } + "allOf": [ + { + "type": "object", + "properties": { + "schematic": { + "type": "string", + "description": "The schematic or collection:schematic to generate.", + "$default": { + "$source": "argv", + "index": 0 + } + } + }, + "required": [ + ] }, - "dryRun": { - "type": "boolean", - "default": false, - "aliases": ["d"], - "description": "Run through without making any changes." - }, - "force": { - "type": "boolean", - "default": false, - "aliases": ["f"], - "description": "Forces overwriting of files." - }, - "interactive": { - "type": "boolean", - "default": "true", - "description": "Disables interactive inputs (i.e., prompts)." - } - }, - "required": [ + { "$ref": "./definitions.json#/definitions/base" }, + { "$ref": "./definitions.json#/definitions/schematic" } ] -} \ No newline at end of file +} diff --git a/packages/angular/cli/commands/help-impl.ts b/packages/angular/cli/commands/help-impl.ts index ac31379a27b8..a117a19fd209 100644 --- a/packages/angular/cli/commands/help-impl.ts +++ b/packages/angular/cli/commands/help-impl.ts @@ -5,36 +5,23 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ - -// tslint:disable:no-global-tslint-disable no-any import { terminal } from '@angular-devkit/core'; import { Command } from '../models/command'; -interface CommandInfo { - name: string; - description: string; - hidden: boolean; - aliases: string[]; -} - export class HelpCommand extends Command { - run(options: any) { + async run() { this.logger.info(`Available Commands:`); - options.commandInfo - .filter((cmd: CommandInfo) => !cmd.hidden) - .forEach((cmd: CommandInfo) => { - let aliasInfo = ''; - if (cmd.aliases.length > 0) { - aliasInfo = ` (${cmd.aliases.join(', ')})`; - } - this.logger.info(` ${terminal.cyan(cmd.name)}${aliasInfo} ${cmd.description}`); - }); + for (const name of Object.keys(Command.commandMap)) { + const cmd = Command.commandMap[name]; - this.logger.info(`\nFor more detailed help run "ng [command name] --help"`); - } + if (cmd.hidden) { + continue; + } - printHelp(_commandName: string, _description: string, options: any) { - return this.run(options); + const aliasInfo = cmd.aliases.length > 0 ? ` (${cmd.aliases.join(', ')})` : ''; + this.logger.info(` ${terminal.cyan(cmd.name)}${aliasInfo} ${cmd.description}`); + } + this.logger.info(`\nFor more detailed help run "ng [command name] --help"`); } } diff --git a/packages/angular/cli/commands/help.json b/packages/angular/cli/commands/help.json index 6ce3882e17aa..51c7adc53398 100644 --- a/packages/angular/cli/commands/help.json +++ b/packages/angular/cli/commands/help.json @@ -1,11 +1,12 @@ { "$schema": "http://json-schema.org/schema", - "id": "HelpCommandOptions", + "$id": "ng-cli://commands/help.json", "description": "Displays help for the Angular CLI.", "$longDescription": "", + "$scope": "all", "$aliases": [], "$impl": "./help-impl#HelpCommand", "type": "object" -} \ No newline at end of file +} diff --git a/packages/angular/cli/commands/lint.json b/packages/angular/cli/commands/lint.json index 6340ca1e3d4a..7d007122c28a 100644 --- a/packages/angular/cli/commands/lint.json +++ b/packages/angular/cli/commands/lint.json @@ -1,6 +1,6 @@ { "$schema": "http://json-schema.org/schema", - "id": "LintCommandOptions", + "$id": "ng-cli://commands/lint.json", "description": "Lints code in existing project.", "$longDescription": "", @@ -10,21 +10,30 @@ "$impl": "./lint-impl#LintCommand", "type": "object", - "properties": { - "project": { - "type": "string", - "description": "The name of the project to lint.", - "$default": { - "$source": "argv", - "index": 0 - } + "allOf": [ + { + "properties": { + "project": { + "type": "string", + "description": "The name of the project to lint.", + "$default": { + "$source": "argv", + "index": 0 + } + }, + "configuration": { + "description": "Specify the configuration to use.", + "type": "string", + "aliases": [ + "c" + ] + } + }, + "required": [ + ] }, - "configuration": { - "description": "Specify the configuration to use.", - "type": "string", - "aliases": ["c"] + { + "$ref": "./definitions.json#/definitions/base" } - }, - "required": [ ] -} \ No newline at end of file +} diff --git a/packages/angular/cli/commands/new-impl.ts b/packages/angular/cli/commands/new-impl.ts index 338d821d150f..94ae330611eb 100644 --- a/packages/angular/cli/commands/new-impl.ts +++ b/packages/angular/cli/commands/new-impl.ts @@ -7,32 +7,19 @@ */ // tslint:disable:no-global-tslint-disable no-any -import { SchematicCommand } from '../models/schematic-command'; +import { BaseSchematicOptions, SchematicCommand } from '../models/schematic-command'; + +export interface NewCommandOptions extends BaseSchematicOptions { + skipGit?: boolean; + collection?: string; +} + export class NewCommand extends SchematicCommand { public readonly allowMissingWorkspace = true; private schematicName = 'ng-new'; - private initialized = false; - public async initialize(options: any) { - if (this.initialized) { - return; - } - - await super.initialize(options); - - this.initialized = true; - - const collectionName = this.parseCollectionName(options); - - const schematicOptions = await this.getOptions({ - schematicName: this.schematicName, - collectionName, - }); - this.addOptions(this.options.concat(schematicOptions)); - } - - public async run(options: any) { + public async run(options: NewCommandOptions) { if (options.dryRun) { options.skipGit = true; } @@ -48,33 +35,19 @@ export class NewCommand extends SchematicCommand { const packageJson = require('../package.json'); const version = packageJson.version; - // Ensure skipGit has a boolean value. - options.skipGit = options.skipGit === undefined ? false : options.skipGit; this._workflow.registry.addSmartDefaultProvider('ng-cli-version', () => version); return this.runSchematic({ collectionName: collectionName, schematicName: this.schematicName, - schematicOptions: this.removeLocalOptions(options), + schematicOptions: options['--'] || [], debug: options.debug, dryRun: options.dryRun, force: options.force, - interactive: options.interactive, }); } private parseCollectionName(options: any): string { - const collectionName = options.collection || options.c || this.getDefaultSchematicCollection(); - - return collectionName; - } - - private removeLocalOptions(options: any): any { - const opts = Object.assign({}, options); - delete opts.verbose; - delete opts.collection; - delete opts.interactive; - - return opts; + return options.collection || this.getDefaultSchematicCollection(); } } diff --git a/packages/angular/cli/commands/new.json b/packages/angular/cli/commands/new.json index b1f8db9f577b..ed301b03a217 100644 --- a/packages/angular/cli/commands/new.json +++ b/packages/angular/cli/commands/new.json @@ -1,6 +1,6 @@ { "$schema": "http://json-schema.org/schema", - "id": "NewCommandOptions", + "$id": "ng-cli://commands/new.json", "description": "Creates a new directory and a new Angular app.", "$longDescription": "", @@ -10,35 +10,24 @@ "$impl": "./new-impl#NewCommand", "type": "object", - "properties": { - "collection": { - "type": "string", - "aliases": ["c"], - "description": "Schematics collection to use." + "allOf": [ + { + "properties": { + "collection": { + "type": "string", + "aliases": [ "c" ], + "description": "Schematics collection to use." + }, + "verbose": { + "type": "boolean", + "default": false, + "aliases": [ "v" ], + "description": "Adds more details to output logging." + } + }, + "required": [] }, - "dryRun": { - "type": "boolean", - "default": false, - "aliases": ["d"], - "description": "Run through without making any changes." - }, - "force": { - "type": "boolean", - "default": false, - "aliases": ["f"], - "description": "Forces overwriting of files." - }, - "verbose": { - "type": "boolean", - "default": false, - "aliases": ["v"], - "description": "Adds more details to output logging." - }, - "interactive": { - "type": "boolean", - "default": "true", - "description": "Disables interactive inputs (i.e., prompts)." - } - }, - "required": [] -} \ No newline at end of file + { "$ref": "./definitions.json#/definitions/base" }, + { "$ref": "./definitions.json#/definitions/schematic" } + ] +} diff --git a/packages/angular/cli/commands/run.json b/packages/angular/cli/commands/run.json index c46b8b3e72e1..51b74bbacf55 100644 --- a/packages/angular/cli/commands/run.json +++ b/packages/angular/cli/commands/run.json @@ -1,6 +1,6 @@ { "$schema": "http://json-schema.org/schema", - "id": "RunCommandOptions", + "$id": "ng-cli://commands/run.json", "description": "Runs Architect targets.", "$longDescription": "", @@ -10,21 +10,28 @@ "$impl": "./run-impl#RunCommand", "type": "object", - "properties": { - "target": { - "type": "string", - "description": "The target to run.", - "$default": { - "$source": "argv", - "index": 0 - } + "allOf": [ + { + "properties": { + "target": { + "type": "string", + "description": "The target to run.", + "$default": { + "$source": "argv", + "index": 0 + } + }, + "configuration": { + "description": "Specify the configuration to use.", + "type": "string", + "aliases": [ "c" ] + } + }, + "required": [ + ] }, - "configuration": { - "description": "Specify the configuration to use.", - "type": "string", - "aliases": ["c"] + { + "$ref": "./definitions.json#/definitions/base" } - }, - "required": [ ] -} \ No newline at end of file +} diff --git a/packages/angular/cli/commands/serve.json b/packages/angular/cli/commands/serve.json index d10ebccee6a9..9b01a5039cb5 100644 --- a/packages/angular/cli/commands/serve.json +++ b/packages/angular/cli/commands/serve.json @@ -1,6 +1,6 @@ { "$schema": "http://json-schema.org/schema", - "id": "ServeCommandOptions", + "$id": "ng-cli://commands/serve.json", "description": "Builds and serves your app, rebuilding on file changes.", "$longDescription": "", @@ -10,25 +10,8 @@ "$impl": "./serve-impl#ServeCommand", "type": "object", - "properties": { - "project": { - "type": "string", - "description": "The name of the project to serve.", - "$default": { - "$source": "argv", - "index": 0 - } - }, - "configuration": { - "description": "Specify the configuration to use.", - "type": "string", - "aliases": ["c"] - }, - "prod": { - "description": "Flag to set configuration to 'production'.", - "type": "boolean" - } - }, - "required": [ + "allOf": [ + { "$ref": "./definitions.json#/definitions/architect" }, + { "$ref": "./definitions.json#/definitions/base" } ] -} \ No newline at end of file +} diff --git a/packages/angular/cli/commands/test.json b/packages/angular/cli/commands/test.json index f987e7fc5cbd..79c5a0ef8c85 100644 --- a/packages/angular/cli/commands/test.json +++ b/packages/angular/cli/commands/test.json @@ -1,6 +1,6 @@ { "$schema": "http://json-schema.org/schema", - "id": "TestCommandOptions", + "$id": "ng-cli://commands/test.json", "description": "Run unit tests in existing project.", "$longDescription": "", @@ -10,25 +10,8 @@ "$impl": "./test-impl#TestCommand", "type": "object", - "properties": { - "project": { - "type": "string", - "description": "The name of the project to test.", - "$default": { - "$source": "argv", - "index": 0 - } - }, - "configuration": { - "description": "Specify the configuration to use.", - "type": "string", - "aliases": ["c"] - }, - "prod": { - "description": "Flag to set configuration to 'production'.", - "type": "boolean" - } - }, - "required": [ + "allOf": [ + { "$ref": "./definitions.json#/definitions/architect" }, + { "$ref": "./definitions.json#/definitions/base" } ] -} \ No newline at end of file +} diff --git a/packages/angular/cli/commands/update-impl.ts b/packages/angular/cli/commands/update-impl.ts index 0594d3c54921..4dbe42cd67f6 100644 --- a/packages/angular/cli/commands/update-impl.ts +++ b/packages/angular/cli/commands/update-impl.ts @@ -5,77 +5,83 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ - -// tslint:disable:no-global-tslint-disable no-any import { normalize } from '@angular-devkit/core'; -import { CommandScope, Option } from '../models/command'; -import { CoreSchematicOptions, SchematicCommand } from '../models/schematic-command'; +import { Arguments, Option } from '../models/interface'; +import { BaseSchematicOptions, SchematicCommand } from '../models/schematic-command'; import { findUp } from '../utilities/find-up'; +import { parseJsonSchemaToOptions } from '../utilities/json-schema'; -export interface UpdateOptions extends CoreSchematicOptions { +export interface UpdateOptions extends BaseSchematicOptions { next: boolean; schematic?: boolean; + dryRun: boolean; + force: boolean; } +type UpdateSchematicOptions = Arguments & { + migrateOnly?: boolean; + from?: string; + packages?: string | string[]; +}; + -export class UpdateCommand extends SchematicCommand { - public readonly name = 'update'; - public readonly description = 'Updates your application and its dependencies.'; - public static aliases: string[] = []; - public static scope = CommandScope.everywhere; - public arguments: string[] = [ 'packages' ]; - public options: Option[] = [ - // Remove the --force flag. - ...this.coreOptions.filter(option => option.name !== 'force'), - ]; +export class UpdateCommand extends SchematicCommand { public readonly allowMissingWorkspace = true; private collectionName = '@schematics/update'; private schematicName = 'update'; - private initialized = false; - public async initialize(options: any) { - if (this.initialized) { - return; - } - await super.initialize(options); - this.initialized = true; + async initialize(input: T) { + await super.initialize(input); - const schematicOptions = await this.getOptions({ - schematicName: this.schematicName, - collectionName: this.collectionName, - }); - this.addOptions(schematicOptions); + // Set the options. + const collection = this.getCollection(this.collectionName); + const schematic = this.getSchematic(collection, this.schematicName, true); + const options = await parseJsonSchemaToOptions( + this._workflow.registry, + schematic.description.schemaJson || {}, + ); + + this.description.options.push(...options); } - async validate(options: any) { - if (options._[0] == '@angular/cli' - && options.migrateOnly === undefined - && options.from === undefined) { + async parseArguments(schematicOptions: string[], schema: Option[]): Promise { + const args = await super.parseArguments(schematicOptions, schema) as UpdateSchematicOptions; + const maybeArgsLeftovers = args['--']; + + if (maybeArgsLeftovers + && maybeArgsLeftovers.length == 1 + && maybeArgsLeftovers[0] == '@angular/cli' + && args.migrateOnly === undefined + && args.from === undefined) { // Check for a 1.7 angular-cli.json file. const oldConfigFileNames = [ normalize('.angular-cli.json'), normalize('angular-cli.json'), ]; - const oldConfigFilePath = - findUp(oldConfigFileNames, process.cwd()) - || findUp(oldConfigFileNames, __dirname); + const oldConfigFilePath = findUp(oldConfigFileNames, process.cwd()) + || findUp(oldConfigFileNames, __dirname); if (oldConfigFilePath) { - options.migrateOnly = true; - options.from = '1.0.0'; + args.migrateOnly = true; + args.from = '1.0.0'; } } - return super.validate(options); - } + // Move `--` to packages. + if (args.packages == undefined && args['--']) { + args.packages = args['--']; + delete args['--']; + } + return args; + } - public async run(options: UpdateOptions) { + async run(options: UpdateOptions) { return this.runSchematic({ collectionName: this.collectionName, schematicName: this.schematicName, - schematicOptions: options, + schematicOptions: options['--'], dryRun: options.dryRun, force: false, showNothingDone: false, diff --git a/packages/angular/cli/commands/update.json b/packages/angular/cli/commands/update.json index 7a47adf3c7bb..0f179aa44cd8 100644 --- a/packages/angular/cli/commands/update.json +++ b/packages/angular/cli/commands/update.json @@ -1,32 +1,39 @@ { "$schema": "http://json-schema.org/schema", - "id": "UpdateCommandOptions", + "$id": "ng-cli://commands/update.json", "description": "Updates your application and its dependencies.", "$longDescription": "", + "$scope": "all", "$aliases": [], "$type": "schematics", "$impl": "./update-impl#UpdateCommand", "type": "object", - "properties": { - "packages": { - "type": "array", - "items": { - "type": "string" + "allOf": [ + { + "properties": { + "packages": { + "type": "array", + "description": "The names of package(s) to update", + "$default": { + "$source": "argv" + } + }, + "dryRun": { + "type": "boolean", + "default": false, + "aliases": [ + "d" + ], + "description": "Run through without making any changes." + } }, - "description": "The names of package(s) to update", - "$default": { - "$source": "argv" - } + "required": [ + ] }, - "dryRun": { - "type": "boolean", - "default": false, - "aliases": ["d"], - "description": "Run through without making any changes." + { + "$ref": "./definitions.json#/definitions/base" } - }, - "required": [ ] -} \ No newline at end of file +} diff --git a/packages/angular/cli/commands/version-impl.ts b/packages/angular/cli/commands/version-impl.ts index 2b9b740d6d58..64e10026ab35 100644 --- a/packages/angular/cli/commands/version-impl.ts +++ b/packages/angular/cli/commands/version-impl.ts @@ -17,7 +17,7 @@ import { findUp } from '../utilities/find-up'; export class VersionCommand extends Command { public static aliases = ['v']; - public run() { + async run() { const pkg = require(path.resolve(__dirname, '..', 'package.json')); let projPkg; try { diff --git a/packages/angular/cli/commands/version.json b/packages/angular/cli/commands/version.json index 8d8fda9f1c96..66ff3c3aa364 100644 --- a/packages/angular/cli/commands/version.json +++ b/packages/angular/cli/commands/version.json @@ -1,11 +1,12 @@ { "$schema": "http://json-schema.org/schema", - "id": "VersionCommandOptions", + "$id": "ng-cli://commands/version.json", "description": "Outputs Angular CLI version.", "$longDescription": "", "$aliases": [ "v" ], + "$scope": "all", "$impl": "./version-impl#VersionCommand", "type": "object" -} \ No newline at end of file +} diff --git a/packages/angular/cli/commands/xi18n.json b/packages/angular/cli/commands/xi18n.json index 0c55c95e5a1c..082ae7ca1ba2 100644 --- a/packages/angular/cli/commands/xi18n.json +++ b/packages/angular/cli/commands/xi18n.json @@ -1,6 +1,6 @@ { "$schema": "http://json-schema.org/schema", - "id": "Xi18nCommandOptions", + "$id": "ng-cli://commands/xi18n.json", "description": "Extracts i18n messages from source code.", "$longDescription": "", @@ -10,21 +10,8 @@ "$impl": "./xi18n-impl#Xi18nCommand", "type": "object", - "properties": { - "project": { - "type": "string", - "description": "The name of the project to extract.", - "$default": { - "$source": "argv", - "index": 0 - } - }, - "configuration": { - "description": "Specify the configuration to use.", - "type": "string", - "aliases": ["c"] - } - }, - "required": [ + "allOf": [ + { "$ref": "./definitions.json#/definitions/architect" }, + { "$ref": "./definitions.json#/definitions/base" } ] -} \ No newline at end of file +} diff --git a/packages/angular/cli/lib/cli/index.ts b/packages/angular/cli/lib/cli/index.ts index 6be758d2b05e..b6940090de15 100644 --- a/packages/angular/cli/lib/cli/index.ts +++ b/packages/angular/cli/lib/cli/index.ts @@ -25,12 +25,9 @@ export default async function(options: { testing?: boolean, cliArgs: string[] }) if (projectDetails === null) { projectDetails = { root: process.cwd() }; } - const context = { - project: projectDetails, - }; try { - const maybeExitCode = await runCommand(options.cliArgs, logger, context); + const maybeExitCode = await runCommand(options.cliArgs, logger, projectDetails); if (typeof maybeExitCode === 'number') { console.assert(Number.isInteger(maybeExitCode)); diff --git a/packages/angular/cli/models/architect-command.ts b/packages/angular/cli/models/architect-command.ts index aacacfe9a588..f1f61a90b103 100644 --- a/packages/angular/cli/models/architect-command.ts +++ b/packages/angular/cli/models/architect-command.ts @@ -5,95 +5,47 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ -import { - Architect, - BuildEvent, - BuilderDescription, - TargetSpecifier, -} from '@angular-devkit/architect'; -import { - JsonObject, - UnknownException, - experimental, - schema, - strings, - tags, -} from '@angular-devkit/core'; +import { Architect, BuildEvent, TargetSpecifier } from '@angular-devkit/architect'; +import { experimental, json, schema, tags } from '@angular-devkit/core'; import { NodeJsSyncHost, createConsoleLogger } from '@angular-devkit/core/node'; -import { from, of } from 'rxjs'; +import { from } from 'rxjs'; import { concatMap, map, tap, toArray } from 'rxjs/operators'; -import { Command, Option } from './command'; +import { parseJsonSchemaToOptions } from '../utilities/json-schema'; +import { BaseCommandOptions, Command } from './command'; +import { parseArguments } from './parser'; import { WorkspaceLoader } from './workspace-loader'; -export interface ProjectAndConfigurationOptions { +export interface ArchitectCommandOptions extends BaseCommandOptions { project?: string; configuration?: string; prod: boolean; -} - -export interface TargetOptions { target?: string; } -export type ArchitectCommandOptions = ProjectAndConfigurationOptions & TargetOptions & JsonObject; - export abstract class ArchitectCommand extends Command { - private _host = new NodeJsSyncHost(); - private _architect: Architect; - private _workspace: experimental.workspace.Workspace; - private _logger = createConsoleLogger(); - // If this command supports running multiple targets. - protected multiTarget = false; + protected _architect: Architect; + protected _workspace: experimental.workspace.Workspace; + protected _logger = createConsoleLogger(); - readonly Options: Option[] = [{ - name: 'configuration', - description: 'The configuration', - type: 'string', - aliases: ['c'], - }]; + protected _registry: json.schema.SchemaRegistry; - readonly arguments = ['project']; + // If this command supports running multiple targets. + protected multiTarget = false; target: string | undefined; public async initialize(options: ArchitectCommandOptions): Promise { - return this._loadWorkspaceAndArchitect().pipe( - concatMap(() => { - const targetSpec: TargetSpecifier = this._makeTargetSpecifier(options); - - if (this.target && !targetSpec.project) { - const projects = this.getProjectNamesByTarget(this.target); - - if (projects.length === 1) { - // If there is a single target, use it to parse overrides. - targetSpec.project = projects[0]; - } else { - // Multiple targets can have different, incompatible options. - // We only lookup options for single targets. - return of(null); - } - } - - if (!targetSpec.project || !targetSpec.target) { - throw new Error('Cannot determine project or target for Architect command.'); - } + await super.initialize(options); - const builderConfig = this._architect.getBuilderConfiguration(targetSpec); + this._registry = new json.schema.CoreSchemaRegistry(); + this._registry.addPostTransform(json.schema.transforms.addUndefinedDefaults); - return this._architect.getBuilderDescription(builderConfig).pipe( - tap(builderDesc => { this.mapArchitectOptions(builderDesc.schema); }), - ); - }), - ).toPromise() - .then(() => { }); - } + await this._loadWorkspaceAndArchitect().toPromise(); - public validate(options: ArchitectCommandOptions) { if (!options.project && this.target) { const projectNames = this.getProjectNamesByTarget(this.target); - const { overrides } = this._makeTargetSpecifier(options); - if (projectNames.length > 1 && Object.keys(overrides || {}).length > 0) { + if (projectNames.length > 1 && options['--'] && options['--'].length > 0) { // Verify that all builders are the same, otherwise error out (since the meaning of an // option could vary from builder to builder). @@ -120,91 +72,63 @@ export abstract class ArchitectCommand extends Command } } - return true; - } + const targetSpec: TargetSpecifier = this._makeTargetSpecifier(options); + + if (this.target && !targetSpec.project) { + const projects = this.getProjectNamesByTarget(this.target); - protected mapArchitectOptions(schema: JsonObject) { - const properties = schema.properties; - if (typeof properties != 'object' || properties === null || Array.isArray(properties)) { - throw new UnknownException('Invalid schema.'); + if (projects.length === 1) { + // If there is a single target, use it to parse overrides. + targetSpec.project = projects[0]; + } } - const keys = Object.keys(properties); - keys - .map(key => { - const value = properties[key]; - if (typeof value != 'object') { - throw new UnknownException('Invalid schema.'); - } - return { - ...value, - name: strings.dasherize(key), - } as any; // tslint:disable-line:no-any - }) - .map(opt => { - const types = ['string', 'boolean', 'integer', 'number']; - // Ignore arrays / objects. - if (types.indexOf(opt.type) === -1) { - return null; - } + if ((!targetSpec.project || !targetSpec.target) && !this.multiTarget) { + throw new Error('Cannot determine project or target for Architect command.'); + } + } - let aliases: string[] = []; - if (opt.alias) { - aliases = [...aliases, opt.alias]; - } - if (opt.aliases) { - aliases = [...aliases, ...opt.aliases]; - } - const schematicDefault = opt.default; - - return { - ...opt, - aliases, - default: undefined, // do not carry over schematics defaults - schematicDefault, - hidden: opt.visible === false, - }; - }) - .filter(x => x) - .forEach(option => this.addOptions(option)); + async run(options: ArchitectCommandOptions) { + return await this.runArchitectTarget(options); } - protected prodOption: Option = { - name: 'prod', - description: 'Flag to set configuration to "prod".', - type: 'boolean', - }; + protected async runArchitectTarget(options: ArchitectCommandOptions): Promise { + const runSingleTarget = async (targetSpec: TargetSpecifier) => { + // We need to build the builderSpec twice because architect does not understand + // overrides separately (getting the configuration builds the whole project, including + // overrides). + const builderConf = this._architect.getBuilderConfiguration(targetSpec); + const builderDesc = await this._architect.getBuilderDescription(builderConf).toPromise(); + const targetOptionArray = await parseJsonSchemaToOptions(this._registry, builderDesc.schema); + const overrides = parseArguments(options['--'] || [], targetOptionArray); + + if (overrides['--']) { + (overrides['--'] || []).forEach(additional => { + this.logger.warn(`Unknown option: '${additional.split(/=/)[0]}'`); + }); - protected configurationOption: Option = { - name: 'configuration', - description: 'Specify the configuration to use.', - type: 'string', - aliases: ['c'], - }; + return 1; + } + const realBuilderConf = this._architect.getBuilderConfiguration({ ...targetSpec, overrides }); - protected async runArchitectTarget(options: ArchitectCommandOptions): Promise { - delete options._; - const targetSpec = this._makeTargetSpecifier(options); - - const runSingleTarget = (targetSpec: TargetSpecifier) => this._architect.run( - this._architect.getBuilderConfiguration(targetSpec), - { logger: this._logger }, - ).pipe( - map((buildEvent: BuildEvent) => buildEvent.success ? 0 : 1), - ); + return this._architect.run(realBuilderConf, { logger: this._logger }).pipe( + map((buildEvent: BuildEvent) => buildEvent.success ? 0 : 1), + ).toPromise(); + }; try { + const targetSpec = this._makeTargetSpecifier(options); if (!targetSpec.project && this.target) { // This runs each target sequentially. // Running them in parallel would jumble the log messages. return await from(this.getProjectNamesByTarget(this.target)).pipe( - concatMap(project => runSingleTarget({ ...targetSpec, project })), + concatMap(project => from(runSingleTarget({ ...targetSpec, project }))), toArray(), map(results => results.every(res => res === 0) ? 0 : 1), ) .toPromise(); } else { - return await runSingleTarget(targetSpec).toPromise(); + return await runSingleTarget(targetSpec); } } catch (e) { if (e instanceof schema.SchemaValidationException) { @@ -268,32 +192,22 @@ export abstract class ArchitectCommand extends Command ); } - private _makeTargetSpecifier(options: ArchitectCommandOptions): TargetSpecifier { - let project, target, configuration, overrides; - - if (options.target) { - [project, target, configuration] = options.target.split(':'); + private _makeTargetSpecifier(commandOptions: ArchitectCommandOptions): TargetSpecifier { + let project, target, configuration; - overrides = { ...options }; - delete overrides.target; + if (commandOptions.target) { + [project, target, configuration] = commandOptions.target.split(':'); - if (overrides.configuration) { - configuration = overrides.configuration; - delete overrides.configuration; + if (commandOptions.configuration) { + configuration = commandOptions.configuration; } } else { - project = options.project; + project = commandOptions.project; target = this.target; - configuration = options.configuration; - if (!configuration && options.prod) { + configuration = commandOptions.configuration; + if (!configuration && commandOptions.prod) { configuration = 'production'; } - - overrides = { ...options }; - - delete overrides.configuration; - delete overrides.prod; - delete overrides.project; } if (!project) { @@ -307,7 +221,6 @@ export abstract class ArchitectCommand extends Command project, configuration, target, - overrides, }; } } diff --git a/packages/angular/cli/models/interface.ts b/packages/angular/cli/models/interface.ts new file mode 100644 index 000000000000..1bfdf7c1269b --- /dev/null +++ b/packages/angular/cli/models/interface.ts @@ -0,0 +1,110 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import { json, logging } from '@angular-devkit/core'; + +export type Value = number | string | boolean | (number | string | boolean)[]; + +export type Arguments = { + [argName: string]: Value; +} & { + '--'?: string[]; +} + +export interface CommandInterface { + initialize(options: T): Promise; + printHelp(options: T): Promise; + printJsonHelp(_options: T): Promise; + run(options: T & Arguments): Promise; + validateAndRun(options: T & Arguments): Promise; +} + +export interface CommandConstructor { + new( + context: CommandContext, + description: CommandDescription, + logger: logging.Logger, + ): CommandInterface; +} + +export interface CommandProject { + root: string; + configFile?: string; +} + +export interface CommandContext { + project: CommandProject; +} + +export enum OptionType { + String = 'string', + Number = 'number', + Boolean = 'boolean', + Array = 'array', + Any = 'any', +} + +export interface Option { + name: string; + description: string; + type: OptionType; + types?: OptionType[]; + aliases: string[]; + required?: boolean; + format?: string; + hidden?: boolean; + default?: string | number | boolean; + positional?: number; + $default?: OptionSmartDefault; +} + +export enum CommandScope { + InProject = 'in', + OutProject = 'out', + Everywhere = 'all', + + Default = InProject, +} + +export enum CommandType { + Custom = 'custom', + Architect = 'architect', + Schematic = 'schematics', + + Default = Custom, +} + +export interface CommandDescription { + name: string; + description: string; + options: Option[]; + + aliases: string[]; + scope: CommandScope; + + impl: CommandConstructor; + + hidden: boolean; + + type: CommandType; + + schematics?: { + [name: string]: Option[]; + }; + architect?: { + [name: string]: Option[]; + }; +} + +export interface OptionSmartDefault { + $source: string; + [key: string]: json.JsonValue; +} + +export interface CommandDescriptionMap { + [key: string]: CommandDescription; +} diff --git a/packages/angular/cli/models/json-schema.ts b/packages/angular/cli/models/json-schema.ts deleted file mode 100644 index 1dcb8aee16d3..000000000000 --- a/packages/angular/cli/models/json-schema.ts +++ /dev/null @@ -1,109 +0,0 @@ -/** - * @license - * Copyright Google Inc. All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -import { JsonObject, isJsonObject, parseJson } from '@angular-devkit/core'; -import * as jsonSchemaTraverse from 'json-schema-traverse'; -import { Option, OptionSmartDefault } from './command'; - -export async function convertSchemaToOptions(schema: string): Promise { - const options = await getOptions(schema); - - return options; -} - -function getOptions(schemaText: string, onlyRootProperties = true): Promise { - // TODO: Use devkit core's visitJsonSchema - return new Promise((resolve) => { - const fullSchema = parseJson(schemaText); - if (!isJsonObject(fullSchema)) { - return Promise.resolve([]); - } - const traverseOptions = {}; - const options: Option[] = []; - function postCallback(schema: JsonObject, - jsonPointer: string, - _rootSchema: string, - _parentJsonPointer: string, - parentKeyword: string, - _parentSchema: string, - property: string) { - if (parentKeyword === 'properties') { - let includeOption = true; - if (onlyRootProperties && isPropertyNested(jsonPointer)) { - includeOption = false; - } - const description = typeof schema.description == 'string' ? schema.description : ''; - const type = typeof schema.type == 'string' ? schema.type : ''; - let defaultValue: string | number | boolean | undefined = undefined; - if (schema.default !== null) { - if (typeof schema.default !== 'object') { - defaultValue = schema.default; - } - } - let $default: OptionSmartDefault | undefined = undefined; - if (schema.$default !== null && isJsonObject(schema.$default)) { - $default = schema.$default as OptionSmartDefault; - } - let required = false; - if (typeof schema.required === 'boolean') { - required = schema.required; - } - let aliases: string[] | undefined = undefined; - if (typeof schema.aliases === 'object' && Array.isArray(schema.aliases)) { - aliases = schema.aliases as string[]; - } - let format: string | undefined = undefined; - if (typeof schema.format === 'string') { - format = schema.format; - } - let hidden = false; - if (typeof schema.hidden === 'boolean') { - hidden = schema.hidden; - } - - const option: Option = { - name: property, - // ...schema, - - description, - type, - default: defaultValue, - $default, - required, - aliases, - format, - hidden, - }; - - if (includeOption) { - options.push(option); - } - } else if (schema === fullSchema) { - resolve(options); - } - } - - const callbacks = { post: postCallback }; - - jsonSchemaTraverse(fullSchema, traverseOptions, callbacks); - }); -} - -function isPropertyNested(jsonPath: string): boolean { - return jsonPath.split('/') - .filter(part => part == 'properties' || part == 'items') - .length > 1; -} - -export function parseSchema(schema: string): JsonObject | null { - const parsedSchema = parseJson(schema); - if (parsedSchema === null || !isJsonObject(parsedSchema)) { - return null; - } - - return parsedSchema; -} diff --git a/packages/angular/cli/models/schematic-command.ts b/packages/angular/cli/models/schematic-command.ts index 75714606c250..f04c262bd1c8 100644 --- a/packages/angular/cli/models/schematic-command.ts +++ b/packages/angular/cli/models/schematic-command.ts @@ -5,10 +5,7 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ - -// tslint:disable:no-global-tslint-disable no-any import { - JsonObject, experimental, logging, normalize, @@ -20,24 +17,24 @@ import { } from '@angular-devkit/core'; import { NodeJsSyncHost } from '@angular-devkit/core/node'; import { - Collection, DryRunEvent, Engine, - Schematic, SchematicEngine, UnsuccessfulWorkflowExecution, - formats, workflow, } from '@angular-devkit/schematics'; import { + FileSystemCollection, FileSystemCollectionDesc, FileSystemEngineHostBase, + FileSystemSchematic, FileSystemSchematicDesc, NodeModulesEngineHost, NodeWorkflow, validateOptionsWithSchema, } from '@angular-devkit/schematics/tools'; import * as inquirer from 'inquirer'; +import * as systemPath from 'path'; import { take } from 'rxjs/operators'; import { WorkspaceLoader } from '../models/workspace-loader'; import { @@ -46,33 +43,30 @@ import { getSchematicDefaults, getWorkspace, } from '../utilities/config'; -import { ArgumentStrategy, Command, CommandContext, Option } from './command'; +import { parseJsonSchemaToOptions } from '../utilities/json-schema'; +import { BaseCommandOptions, Command } from './command'; +import { Arguments, CommandContext, CommandDescription, Option } from './interface'; +import { parseArguments, parseFreeFormArguments } from './parser'; -export interface CoreSchematicOptions { - dryRun: boolean; - force: boolean; +export interface BaseSchematicOptions extends BaseCommandOptions { + debug?: boolean; + dryRun?: boolean; + force?: boolean; + interactive?: boolean; } export interface RunSchematicOptions { collectionName: string; schematicName: string; - schematicOptions: any; + + schematicOptions?: string[]; + debug?: boolean; - dryRun: boolean; - force: boolean; + dryRun?: boolean; + force?: boolean; showNothingDone?: boolean; - interactive?: boolean; -} - -export interface GetOptionsOptions { - collectionName: string; - schematicName: string; } -export interface GetOptionsResult { - options: Option[]; - arguments: Option[]; -} export class UnknownCollectionError extends Error { constructor(collectionName: string) { @@ -80,47 +74,101 @@ export class UnknownCollectionError extends Error { } } -export abstract class SchematicCommand extends Command { - readonly options: Option[] = []; +export abstract class SchematicCommand< + T extends BaseSchematicOptions = BaseSchematicOptions, +> extends Command { readonly allowPrivateSchematics: boolean = false; private _host = new NodeJsSyncHost(); private _workspace: experimental.workspace.Workspace; - private _deAliasedName: string; - private _originalOptions: Option[]; - private _engineHost: FileSystemEngineHostBase; - private _engine: Engine; - private _workflow: workflow.BaseWorkflow; - argStrategy = ArgumentStrategy.Nothing; + private readonly _engine: Engine; + protected _workflow: workflow.BaseWorkflow; constructor( - context: CommandContext, logger: logging.Logger, - engineHost: FileSystemEngineHostBase = new NodeModulesEngineHost()) { - super(context, logger); - this._engineHost = engineHost; + context: CommandContext, + description: CommandDescription, + logger: logging.Logger, + private readonly _engineHost: FileSystemEngineHostBase = new NodeModulesEngineHost(), + ) { + super(context, description, logger); this._engine = new SchematicEngine(this._engineHost); - const registry = new schema.CoreSchemaRegistry(formats.standardFormats); - this._engineHost.registerOptionsTransform( - validateOptionsWithSchema(registry)); } - protected readonly coreOptions: Option[] = [ - { - name: 'dryRun', - type: 'boolean', - default: false, - aliases: ['d'], - description: 'Run through without making any changes.', - }, - { - name: 'force', - type: 'boolean', - default: false, - aliases: ['f'], - description: 'Forces overwriting of files.', - }]; - - public async initialize(_options: any) { + public async initialize(options: T) { this._loadWorkspace(); + this.createWorkflow(options); + } + + public async printHelp(options: T) { + await super.printHelp(options); + const schematicNames = Object.keys(this.description.schematics || {}); + + await super.printHelpOptions(); + this.logger.info(''); + + if (this.description.schematics) { + + if (schematicNames.length > 1) { + this.logger.info('Available Schematics:'); + + const namesPerCollection: { [c: string]: string[] } = {}; + schematicNames.forEach(name => { + const [collectionName, schematicName] = name.split(/:/, 2); + + if (!namesPerCollection[collectionName]) { + namesPerCollection[collectionName] = []; + } + + namesPerCollection[collectionName].push(schematicName); + }); + + const defaultCollection = this.getDefaultSchematicCollection(); + Object.keys(namesPerCollection).forEach(collectionName => { + const isDefault = defaultCollection == collectionName; + this.logger.info( + ` Collection "${collectionName}"${isDefault ? ' (default)' : ''}:`, + ); + + namesPerCollection[collectionName].forEach(schematicName => { + this.logger.info(` ${schematicName}`); + }); + }); + } else if (schematicNames.length == 1) { + this.logger.info('Options for schematic ' + schematicNames[0]); + await this.printHelpOptions(this.description.schematics[schematicNames[0]]); + } + } + + return 0; + } + + async printHelpUsage() { + const schematicNames = Object.keys(this.description.schematics || {}); + if (this.description.schematics && schematicNames.length == 1) { + this.logger.info(this.description.description); + + const opts = this.description.options.filter(x => x.positional === undefined); + const [collectionName, schematicName] = schematicNames[0].split(/:/)[0]; + + // Display if this is not the default collectionName, + // otherwise just show the schematicName. + const displayName = collectionName == this.getDefaultSchematicCollection() + ? schematicName + : schematicNames[0]; + + const schematicOptions = this.description.schematics[schematicNames[0]]; + const schematicArgs = schematicOptions.filter(x => x.positional !== undefined); + const argDisplay = schematicArgs.length > 0 + ? ' ' + schematicArgs.map(a => `<${strings.dasherize(a.name)}>`).join(' ') + : ''; + + this.logger.info(tags.oneLine` + usage: ng ${this.description.name} ${displayName}${argDisplay} + ${opts.length > 0 ? `[options]` : ``} + `); + this.logger.info(''); + } else { + await super.printHelpUsage(); + } } protected getEngineHost() { @@ -131,7 +179,7 @@ export abstract class SchematicCommand extends Command { return this._engine; } - protected getCollection(collectionName: string): Collection { + protected getCollection(collectionName: string): FileSystemCollection { const engine = this.getEngine(); const collection = engine.createCollection(collectionName); @@ -143,127 +191,58 @@ export abstract class SchematicCommand extends Command { } protected getSchematic( - collection: Collection, schematicName: string, - allowPrivate?: boolean): Schematic { + collection: FileSystemCollection, + schematicName: string, + allowPrivate?: boolean, + ): FileSystemSchematic { return collection.createSchematic(schematicName, allowPrivate); } - protected setPathOptions(options: any, workingDir: string): any { + protected setPathOptions(options: Option[], workingDir: string) { if (workingDir === '') { return {}; } - return this.options + return options .filter(o => o.format === 'path') .map(o => o.name) - .filter(name => options[name] === undefined) - .reduce((acc: any, curr) => { + .reduce((acc, curr) => { acc[curr] = workingDir; return acc; - }, {}); + }, {} as { [name: string]: string }); } /* * Runtime hook to allow specifying customized workflow */ - protected getWorkflow(options: RunSchematicOptions): workflow.BaseWorkflow { + protected createWorkflow(options: BaseSchematicOptions): workflow.BaseWorkflow { + if (this._workflow) { + return this._workflow; + } + const {force, dryRun} = options; - const fsHost = new virtualFs.ScopedHost( - new NodeJsSyncHost(), normalize(this.project.root)); + const fsHost = new virtualFs.ScopedHost(new NodeJsSyncHost(), normalize(this.project.root)); - return new NodeWorkflow( - fsHost as any, + const workflow = new NodeWorkflow( + fsHost, { force, dryRun, packageManager: getPackageManager(), - root: this.project.root, + root: normalize(this.project.root), }, ); - } - - private _getWorkflow(options: RunSchematicOptions): workflow.BaseWorkflow { - if (!this._workflow) { - this._workflow = this.getWorkflow(options); - } - return this._workflow; - } - - protected getDefaultSchematicCollection(): string { - let workspace = getWorkspace('local'); + this._engineHost.registerOptionsTransform(validateOptionsWithSchema(workflow.registry)); - if (workspace) { - const project = getProjectByCwd(workspace); - if (project && workspace.getProjectCli(project)) { - const value = workspace.getProjectCli(project)['defaultCollection']; - if (typeof value == 'string') { - return value; - } - } - if (workspace.getCli()) { - const value = workspace.getCli()['defaultCollection']; - if (typeof value == 'string') { - return value; - } - } - } - - workspace = getWorkspace('global'); - if (workspace && workspace.getCli()) { - const value = workspace.getCli()['defaultCollection']; - if (typeof value == 'string') { - return value; - } - } - - return '@schematics/angular'; - } - - protected runSchematic(options: RunSchematicOptions) { - const {collectionName, schematicName, debug, dryRun} = options; - let schematicOptions = this.removeCoreOptions(options.schematicOptions); - let nothingDone = true; - let loggingQueue: string[] = []; - let error = false; - const workflow = this._getWorkflow(options); - - const workingDir = process.cwd().replace(this.project.root, '').replace(/\\/g, '/'); - const pathOptions = this.setPathOptions(schematicOptions, workingDir); - schematicOptions = { ...schematicOptions, ...pathOptions }; - const defaultOptions = this.readDefaults(collectionName, schematicName, schematicOptions); - schematicOptions = { ...schematicOptions, ...defaultOptions }; - - // Remove all of the original arguments which have already been parsed - - const argumentCount = this._originalOptions - .filter(opt => { - let isArgument = false; - if (opt.$default !== undefined && opt.$default.$source === 'argv') { - isArgument = true; - } - - return isArgument; - }) - .length; - - // Pass the rest of the arguments as the smart default "argv". Then delete it. - const rawArgs = schematicOptions._.slice(argumentCount); - workflow.registry.addSmartDefaultProvider('argv', (schema: JsonObject) => { - if ('index' in schema) { - return rawArgs[Number(schema['index'])]; - } else { - return rawArgs; - } - }); - delete schematicOptions._; + workflow.registry.addPostTransform(schema.transforms.addUndefinedDefaults); - workflow.registry.addSmartDefaultProvider('projectName', (_schema: JsonObject) => { + workflow.registry.addSmartDefaultProvider('projectName', () => { if (this._workspace) { try { - return this._workspace.getProjectByPath(normalize(process.cwd())) - || this._workspace.getDefaultProjectName(); + return this._workspace.getProjectByPath(normalize(process.cwd())) + || this._workspace.getDefaultProjectName(); } catch (e) { if (e instanceof experimental.workspace.AmbiguousProjectPathException) { this.logger.warn(tags.oneLine` @@ -324,6 +303,81 @@ export abstract class SchematicCommand extends Command { }); } + return this._workflow = workflow; + } + + protected getDefaultSchematicCollection(): string { + let workspace = getWorkspace('local'); + + if (workspace) { + const project = getProjectByCwd(workspace); + if (project && workspace.getProjectCli(project)) { + const value = workspace.getProjectCli(project)['defaultCollection']; + if (typeof value == 'string') { + return value; + } + } + if (workspace.getCli()) { + const value = workspace.getCli()['defaultCollection']; + if (typeof value == 'string') { + return value; + } + } + } + + workspace = getWorkspace('global'); + if (workspace && workspace.getCli()) { + const value = workspace.getCli()['defaultCollection']; + if (typeof value == 'string') { + return value; + } + } + + return '@schematics/angular'; + } + + protected async runSchematic(options: RunSchematicOptions) { + const { schematicOptions, debug, dryRun } = options; + let { collectionName, schematicName } = options; + + let nothingDone = true; + let loggingQueue: string[] = []; + let error = false; + + const workflow = this._workflow; + + const workingDir = normalize(systemPath.relative(this.project.root, process.cwd())); + + // Get the option object from the schematic schema. + const schematic = this.getSchematic( + this.getCollection(collectionName), + schematicName, + this.allowPrivateSchematics, + ); + // Update the schematic and collection name in case they're not the same as the ones we + // received in our options, e.g. after alias resolution or extension. + collectionName = schematic.collection.description.name; + schematicName = schematic.description.name; + + // Set the options of format "path". + let o: Option[] | null = null; + let args: Arguments; + + if (!schematic.description.schemaJson) { + args = await this.parseFreeFormArguments(schematicOptions || []); + } else { + o = await parseJsonSchemaToOptions(workflow.registry, schematic.description.schemaJson); + args = await this.parseArguments(schematicOptions || [], o); + } + + const pathOptions = o ? this.setPathOptions(o, workingDir) : {}; + let input = Object.assign(pathOptions, args); + + // Read the default values from the workspace. + const projectName = input.project !== undefined ? '' + input.project : null; + const defaults = getSchematicDefaults(collectionName, schematicName, projectName); + input = Object.assign<{}, {}, typeof input>({}, defaults, input); + workflow.reporter.subscribe((event: DryRunEvent) => { nothingDone = false; @@ -371,9 +425,9 @@ export abstract class SchematicCommand extends Command { workflow.execute({ collection: collectionName, schematic: schematicName, - options: schematicOptions, + options: input, debug: debug, - logger: this.logger as any, + logger: this.logger, allowPrivate: this.allowPrivateSchematics, }) .subscribe({ @@ -396,7 +450,7 @@ export abstract class SchematicCommand extends Command { this.logger.info('Nothing to be done.'); } if (dryRun) { - this.logger.warn(`\nNOTE: Run with "dry run" no changes were made.`); + this.logger.warn(`\nNOTE: The "dryRun" flag means no changes were made.`); } resolve(); }, @@ -404,68 +458,15 @@ export abstract class SchematicCommand extends Command { }); } - protected removeCoreOptions(options: any): any { - const opts = Object.assign({}, options); - if (this._originalOptions.find(option => option.name == 'dryRun')) { - delete opts.dryRun; - } - if (this._originalOptions.find(option => option.name == 'force')) { - delete opts.force; - } - if (this._originalOptions.find(option => option.name == 'debug')) { - delete opts.debug; - } - - return opts; + protected async parseFreeFormArguments(schematicOptions: string[]) { + return parseFreeFormArguments(schematicOptions); } - protected getOptions(options: GetOptionsOptions): Promise { - // Make a copy. - this._originalOptions = [...this.options]; - - const collectionName = options.collectionName || this.getDefaultSchematicCollection(); - - const collection = this.getCollection(collectionName); - - const schematic = this.getSchematic(collection, options.schematicName, - this.allowPrivateSchematics); - this._deAliasedName = schematic.description.name; - - if (!schematic.description.schemaJson) { - return Promise.resolve([]); - } - - const properties = schematic.description.schemaJson.properties; - const keys = Object.keys(properties); - const availableOptions = keys - .map(key => ({ ...properties[key], ...{ name: strings.dasherize(key) } })) - .map(opt => { - const types = ['string', 'boolean', 'integer', 'number']; - // Ignore arrays / objects. - if (types.indexOf(opt.type) === -1) { - return null; - } - - let aliases: string[] = []; - if (opt.alias) { - aliases = [...aliases, opt.alias]; - } - if (opt.aliases) { - aliases = [...aliases, ...opt.aliases]; - } - const schematicDefault = opt.default; - - return { - ...opt, - aliases, - default: undefined, // do not carry over schematics defaults - schematicDefault, - hidden: opt.visible === false, - }; - }) - .filter(x => x); - - return Promise.resolve(availableOptions); + protected async parseArguments( + schematicOptions: string[], + options: Option[] | null, + ): Promise { + return parseArguments(schematicOptions, options); } private _loadWorkspace() { @@ -489,36 +490,7 @@ export abstract class SchematicCommand extends Command { if (!this.allowMissingWorkspace) { // Ignore missing workspace throw err; - } - } } - - private _cleanDefaults(defaults: T, undefinedOptions: string[]): T { - (Object.keys(defaults) as K[]) - .filter(key => !undefinedOptions.map(strings.camelize).includes(key as string)) - .forEach(key => { - delete defaults[key]; - }); - - return defaults; - } - - private readDefaults(collectionName: string, schematicName: string, options: any): {} { - if (this._deAliasedName) { - schematicName = this._deAliasedName; } - - const projectName = options.project; - const defaults = getSchematicDefaults(collectionName, schematicName, projectName); - - // Get list of all undefined options. - const undefinedOptions = this.options - .filter(o => options[o.name] === undefined) - .map(o => o.name); - - // Delete any default that is not undefined. - this._cleanDefaults(defaults, undefinedOptions); - - return defaults; } } diff --git a/packages/angular/cli/utilities/json-schema.ts b/packages/angular/cli/utilities/json-schema.ts new file mode 100644 index 000000000000..41cabd96be88 --- /dev/null +++ b/packages/angular/cli/utilities/json-schema.ts @@ -0,0 +1,201 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import { json } from '@angular-devkit/core'; +import { ExportStringRef } from '@angular-devkit/schematics/tools'; +import { dirname } from 'path'; +import { + CommandConstructor, + CommandDescription, + CommandScope, + CommandType, + Option, + OptionType, +} from '../models/interface'; + +function _getEnumFromValue(v: json.JsonValue, e: E, d: T): T { + if (typeof v !== 'string') { + return d; + } + + if (Object.values(e).indexOf(v) !== -1) { + return v as T; + } + + return d; +} + +export async function parseJsonSchemaToCommandDescription( + name: string, + jsonPath: string, + registry: json.schema.SchemaRegistry, + schema: json.JsonObject, +): Promise { + // Before doing any work, let's validate the implementation. + if (typeof schema.$impl != 'string') { + throw new Error(`Command ${name} has an invalid implementation.`); + } + const ref = new ExportStringRef(schema.$impl, dirname(jsonPath)); + const impl = ref.ref; + + if (impl === undefined || typeof impl !== 'function') { + throw new Error(`Command ${name} has an invalid implementation.`); + } + + const options = await parseJsonSchemaToOptions(registry, schema); + + const aliases: string[] = []; + if (json.isJsonArray(schema.$aliases)) { + schema.$aliases.forEach(value => { + if (typeof value == 'string') { + aliases.push(value); + } + }); + } + + const scope = _getEnumFromValue(schema.$scope, CommandScope, CommandScope.Default); + const type = _getEnumFromValue(schema.$type, CommandType, CommandType.Default); + const description = '' + (schema.description === undefined ? '' : schema.description); + const hidden = !!schema.$hidden; + + return { name, description, hidden, type, options, aliases, scope, impl }; +} + +export async function parseJsonSchemaToOptions( + registry: json.schema.SchemaRegistry, + schema: json.JsonObject, +): Promise { + const options: Option[] = []; + + function visitor( + current: json.JsonObject | json.JsonArray, + pointer: json.schema.JsonPointer, + parentSchema?: json.JsonObject | json.JsonArray, + ) { + if (!parentSchema) { + // Ignore root. + return; + } else if (pointer.split(/properties|items/g).length > 2) { + // Ignore subitems (objects or arrays). + return; + } else if (json.isJsonArray(current)) { + return; + } + + if (pointer.indexOf('/not/') != -1) { + // We don't support anyOf/not. + throw new Error('The "not" keyword is not supported in JSON Schema.'); + } + + const ptr = json.schema.parseJsonPointer(pointer); + const name = ptr[ptr.length - 1]; + + if (ptr[ptr.length - 2] != 'properties') { + // Skip any non-property items. + return; + } + + const typeSet = json.schema.getTypesOfSchema(current); + + if (typeSet.size == 0) { + throw new Error('Cannot find type of schema.'); + } + + // We only support number, string or boolean (or array of those), so remove everything else. + const types = [...typeSet].filter(x => { + switch (x) { + case 'boolean': + case 'number': + case 'string': + return true; + + case 'array': + // Only include arrays if they're boolean, string or number. + if (json.isJsonObject(current.items) + && typeof current.items.type == 'string' + && ['boolean', 'number', 'string'].includes(current.items.type)) { + return true; + } + + return false; + + default: + return false; + } + }).map(x => _getEnumFromValue(x, OptionType, OptionType.String)); + + if (types.length == 0) { + // This means it's not usable on the command line. e.g. an Object. + return; + } + + let defaultValue: string | number | boolean | undefined = undefined; + if (schema.default !== undefined) { + switch (types[0]) { + case 'string': + if (typeof schema.default == 'string') { + defaultValue = schema.default; + } + break; + case 'number': + if (typeof schema.default == 'number') { + defaultValue = schema.default; + } + break; + case 'boolean': + if (typeof schema.default == 'boolean') { + defaultValue = schema.default; + } + break; + } + } + + const $default = current.$default; + const $defaultIndex = (json.isJsonObject($default) && $default['$source'] == 'argv') + ? $default['index'] : undefined; + const positional: number | undefined = typeof $defaultIndex == 'number' + ? $defaultIndex : undefined; + + const required = json.isJsonArray(current.required) + ? current.required.indexOf(name) != -1 : false; + const aliases = json.isJsonArray(current.aliases) ? [...current.aliases].map(x => '' + x) : []; + const format = typeof current.format == 'string' ? current.format : undefined; + const hidden = !!current.hidden; + + const option: Option = { + name, + description: '' + (current.description === undefined ? '' : current.description), + ...types.length == 1 ? { type: types[0] } : { type: types[0], types }, + ...defaultValue !== undefined ? { default: defaultValue } : {}, + required, + aliases, + ...format !== undefined ? { format } : {}, + hidden, + ...positional !== undefined ? { positional } : {}, + }; + + options.push(option); + } + + const flattenedSchema = await registry.flatten(schema).toPromise(); + json.schema.visitJsonSchema(flattenedSchema, visitor); + + // Sort by positional. + return options.sort((a, b) => { + if (a.positional) { + if (b.positional) { + return a.positional - b.positional; + } else { + return 1; + } + } else if (b.positional) { + return -1; + } else { + return 0; + } + }); +} diff --git a/packages/angular/cli/utilities/project.ts b/packages/angular/cli/utilities/project.ts index d6becfa363f1..e2be4245dda7 100644 --- a/packages/angular/cli/utilities/project.ts +++ b/packages/angular/cli/utilities/project.ts @@ -11,18 +11,14 @@ import { normalize } from '@angular-devkit/core'; import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; +import { CommandProject } from '../models/interface'; import { findUp } from './find-up'; export function insideProject(): boolean { return getProjectDetails() !== null; } -export interface ProjectDetails { - root: string; - configFile?: string; -} - -export function getProjectDetails(): ProjectDetails | null { +export function getProjectDetails(): CommandProject | null { const currentDir = process.cwd(); const possibleConfigFiles = [ 'angular.json', diff --git a/tests/legacy-cli/e2e/tests/basic/in-project-logic.ts b/tests/legacy-cli/e2e/tests/basic/in-project-logic.ts index fea6d3fd7a1c..5bf34c937a61 100644 --- a/tests/legacy-cli/e2e/tests/basic/in-project-logic.ts +++ b/tests/legacy-cli/e2e/tests/basic/in-project-logic.ts @@ -1,15 +1,20 @@ -import {writeFile, deleteFile} from '../../utils/fs'; -import {ng} from '../../utils/process'; import * as os from 'os'; -import {join} from 'path'; +import { join } from 'path'; +import { writeFile, deleteFile } from '../../utils/fs'; +import { ng } from '../../utils/process'; +import { expectToFail } from '../../utils/utils'; export default function() { const homedir = os.homedir(); - const globalConfigPath = join(homedir, '.angular.json'); + const globalConfigPath = join(homedir, '.angular-config.json'); return Promise.resolve() .then(() => writeFile(globalConfigPath, '{"version":1}')) .then(() => process.chdir(homedir)) .then(() => ng('new', 'proj-name', '--dry-run')) - .then(() => deleteFile(globalConfigPath)); + .then(() => deleteFile(globalConfigPath)) + // Test that we cannot create a project inside another project. + .then(() => writeFile(join(homedir, '.angular.json'), '{"version":1}')) + .then(() => expectToFail(() => ng('new', 'proj-name', '--dry-run'))) + .then(() => deleteFile(join(homedir, '.angular.json'))); } diff --git a/tests/legacy-cli/e2e/tests/generate/help-output.ts b/tests/legacy-cli/e2e/tests/generate/help-output.ts index 465448e14ced..c1b9f69c4b2f 100644 --- a/tests/legacy-cli/e2e/tests/generate/help-output.ts +++ b/tests/legacy-cli/e2e/tests/generate/help-output.ts @@ -74,7 +74,7 @@ export default function() { .then(() => ng('generate', 'fake-schematics:fake', '--help')) .then(({stdout}) => { if (!/ng generate fake-schematics:fake \[options\]/.test(stdout)) { - throw new Error('Help signature is wrong.'); + throw new Error('Help signature is wrong (1).'); } if (!/opt-a[\s\S]*opt-b[\s\S]*opt-c/.test(stdout)) { throw new Error('Help signature options are incorrect.'); @@ -88,8 +88,8 @@ export default function() { .then(() => ng('generate', 'fake', '--help')) // verify same output .then(({stdout}) => { - if (!/ng generate fake \[options\]/.test(stdout)) { - throw new Error('Help signature is wrong.'); + if (!/ng generate fake-schematics:fake \[options\]/.test(stdout)) { + throw new Error('Help signature is wrong (2).'); } if (!/opt-a[\s\S]*opt-b[\s\S]*opt-c/.test(stdout)) { throw new Error('Help signature options are incorrect.'); diff --git a/tests/legacy-cli/e2e/utils/assets.ts b/tests/legacy-cli/e2e/utils/assets.ts index 581350c6673e..83266c792b29 100644 --- a/tests/legacy-cli/e2e/utils/assets.ts +++ b/tests/legacy-cli/e2e/utils/assets.ts @@ -4,7 +4,7 @@ import {getGlobalVariable} from './env'; import {relative} from 'path'; import {copyFile} from './fs'; import {useBuiltPackages} from './project'; -import {silentNpm} from './process'; +import { git, silentNpm } from './process'; export function assetDir(assetName: string) { diff --git a/tests/legacy-cli/e2e/utils/process.ts b/tests/legacy-cli/e2e/utils/process.ts index 7da400f46797..31bd37a724ff 100644 --- a/tests/legacy-cli/e2e/utils/process.ts +++ b/tests/legacy-cli/e2e/utils/process.ts @@ -44,7 +44,11 @@ function _exec(options: ExecOptions, cmd: string, args: string[]): Promise `"${x}"`).join(' ')}\`${flags}...`)); console.log(terminal.blue(`CWD: ${cwd}`)); - const spawnOptions: SpawnOptions = {cwd, env}; + console.log(terminal.blue(`ENV: ${JSON.stringify(env)}`)); + const spawnOptions: SpawnOptions = { + cwd, + ...env ? { env } : {}, + }; if (process.platform.startsWith('win')) { args.unshift('/c', cmd); @@ -91,13 +95,14 @@ function _exec(options: ExecOptions, cmd: string, args: string[]): Promise { - if (data.toString().match(options.waitForMatch)) { + if (data.toString().match(match)) { resolve({ stdout, stderr }); } }); childProcess.stderr.on('data', (data: Buffer) => { - if (data.toString().match(options.waitForMatch)) { + if (data.toString().match(match)) { resolve({ stdout, stderr }); } }); From 7ffd3b303352f4909a850ebc64cd696d1925f588 Mon Sep 17 00:00:00 2001 From: Hans Date: Tue, 4 Sep 2018 15:26:47 -0700 Subject: [PATCH 0040/1703] refactor(@angular/cli): rename project to workspace and clean up Removed unnecessary fields in interfaces, added some comments. Overall QoL. --- packages/angular/cli/commands/add-impl.ts | 6 +- packages/angular/cli/commands/build-impl.ts | 4 +- .../angular/cli/commands/generate-impl.ts | 6 +- packages/angular/cli/commands/serve-impl.ts | 4 +- packages/angular/cli/commands/version-impl.ts | 4 +- packages/angular/cli/lib/cli/index.ts | 6 +- .../angular/cli/models/architect-command.ts | 5 +- packages/angular/cli/models/command-runner.ts | 8 +-- packages/angular/cli/models/command.ts | 21 ++++--- packages/angular/cli/models/interface.ts | 56 +++++++++++++------ .../angular/cli/models/schematic-command.ts | 21 ++++--- packages/angular/cli/utilities/project.ts | 8 +-- 12 files changed, 83 insertions(+), 66 deletions(-) diff --git a/packages/angular/cli/commands/add-impl.ts b/packages/angular/cli/commands/add-impl.ts index e2492826ea57..17e0bb23034a 100644 --- a/packages/angular/cli/commands/add-impl.ts +++ b/packages/angular/cli/commands/add-impl.ts @@ -16,8 +16,6 @@ import { getPackageManager } from '../utilities/config'; export interface AddCommandOptions extends BaseSchematicOptions { collection: string; - help?: boolean; - help_json?: boolean; } export class AddCommand< @@ -56,12 +54,12 @@ export class AddCommand< packageName, this.logger, packageManager, - this.project.root, + this.workspace.root, ); const runOptions = { schematicOptions: options['--'] || [], - workingDir: this.project.root, + workingDir: this.workspace.root, collectionName, schematicName: 'ng-add', allowPrivate: true, diff --git a/packages/angular/cli/commands/build-impl.ts b/packages/angular/cli/commands/build-impl.ts index 46484827de12..c6c94dcfbb9d 100644 --- a/packages/angular/cli/commands/build-impl.ts +++ b/packages/angular/cli/commands/build-impl.ts @@ -14,8 +14,8 @@ export class BuildCommand extends ArchitectCommand { public async run(options: ArchitectCommandOptions) { // Check Angular and TypeScript versions. - Version.assertCompatibleAngularVersion(this.project.root); - Version.assertTypescriptVersion(this.project.root); + Version.assertCompatibleAngularVersion(this.workspace.root); + Version.assertTypescriptVersion(this.workspace.root); return this.runArchitectTarget(options); } diff --git a/packages/angular/cli/commands/generate-impl.ts b/packages/angular/cli/commands/generate-impl.ts index 7a58da674214..81189872338e 100644 --- a/packages/angular/cli/commands/generate-impl.ts +++ b/packages/angular/cli/commands/generate-impl.ts @@ -26,7 +26,7 @@ export class GenerateCommand< const [collectionName, schematicName] = this.parseSchematicInfo(options); const collection = this.getCollection(collectionName); - this.description.schematics = {}; + this.description.suboptions = {}; const schematicNames = schematicName ? [schematicName] : collection.listSchematicNames(); @@ -40,7 +40,7 @@ export class GenerateCommand< ); } - this.description.schematics[`${collectionName}:${name}`] = options; + this.description.suboptions[`${collectionName}:${name}`] = options; } } @@ -78,7 +78,7 @@ export class GenerateCommand< public async printHelp(options: T) { await super.printHelp(options); - if (Object.keys(this.description.schematics || {}).length == 1) { + if (Object.keys(this.description.suboptions || {}).length == 1) { this.logger.info(`\nTo see help for a schematic run:`); this.logger.info(terminal.cyan(` ng generate --help`)); } diff --git a/packages/angular/cli/commands/serve-impl.ts b/packages/angular/cli/commands/serve-impl.ts index 6ff9b11e62c3..1e9175c00497 100644 --- a/packages/angular/cli/commands/serve-impl.ts +++ b/packages/angular/cli/commands/serve-impl.ts @@ -15,8 +15,8 @@ export class ServeCommand extends ArchitectCommand { public validate(_options: ArchitectCommandOptions) { // Check Angular and TypeScript versions. - Version.assertCompatibleAngularVersion(this.project.root); - Version.assertTypescriptVersion(this.project.root); + Version.assertCompatibleAngularVersion(this.workspace.root); + Version.assertTypescriptVersion(this.workspace.root); return true; } diff --git a/packages/angular/cli/commands/version-impl.ts b/packages/angular/cli/commands/version-impl.ts index 64e10026ab35..1b0367d1e18a 100644 --- a/packages/angular/cli/commands/version-impl.ts +++ b/packages/angular/cli/commands/version-impl.ts @@ -21,7 +21,7 @@ export class VersionCommand extends Command { const pkg = require(path.resolve(__dirname, '..', 'package.json')); let projPkg; try { - projPkg = require(path.resolve(this.project.root, 'package.json')); + projPkg = require(path.resolve(this.workspace.root, 'package.json')); } catch (exception) { projPkg = undefined; } @@ -39,7 +39,7 @@ export class VersionCommand extends Command { const maybeNodeModules = findUp('node_modules', __dirname); const packageRoot = projPkg - ? path.resolve(this.project.root, 'node_modules') + ? path.resolve(this.workspace.root, 'node_modules') : maybeNodeModules; const packageNames = [ diff --git a/packages/angular/cli/lib/cli/index.ts b/packages/angular/cli/lib/cli/index.ts index b6940090de15..fd55a21a6731 100644 --- a/packages/angular/cli/lib/cli/index.ts +++ b/packages/angular/cli/lib/cli/index.ts @@ -9,19 +9,17 @@ import { logging, terminal } from '@angular-devkit/core'; import { filter } from 'rxjs/operators'; import { runCommand } from '../../models/command-runner'; -import { getProjectDetails } from '../../utilities/project'; +import { getWorkspaceDetails } from '../../utilities/project'; export default async function(options: { testing?: boolean, cliArgs: string[] }) { - // const commands = await loadCommands(); - const logger = new logging.IndentLogger('cling'); let loggingSubscription; if (!options.testing) { loggingSubscription = initializeLogging(logger); } - let projectDetails = getProjectDetails(); + let projectDetails = getWorkspaceDetails(); if (projectDetails === null) { projectDetails = { root: process.cwd() }; } diff --git a/packages/angular/cli/models/architect-command.ts b/packages/angular/cli/models/architect-command.ts index f1f61a90b103..ae9979c99255 100644 --- a/packages/angular/cli/models/architect-command.ts +++ b/packages/angular/cli/models/architect-command.ts @@ -45,7 +45,8 @@ export abstract class ArchitectCommand extends Command if (!options.project && this.target) { const projectNames = this.getProjectNamesByTarget(this.target); - if (projectNames.length > 1 && options['--'] && options['--'].length > 0) { + const leftovers = options['--']; + if (projectNames.length > 1 && leftovers && leftovers.length > 0) { // Verify that all builders are the same, otherwise error out (since the meaning of an // option could vary from builder to builder). @@ -183,7 +184,7 @@ export abstract class ArchitectCommand extends Command private _loadWorkspaceAndArchitect() { const workspaceLoader = new WorkspaceLoader(this._host); - return workspaceLoader.loadWorkspace(this.project.root).pipe( + return workspaceLoader.loadWorkspace(this.workspace.root).pipe( tap((workspace: experimental.workspace.Workspace) => this._workspace = workspace), concatMap((workspace: experimental.workspace.Workspace) => { return new Architect(workspace).loadArchitect(); diff --git a/packages/angular/cli/models/command-runner.ts b/packages/angular/cli/models/command-runner.ts index b1cef78b87d6..d673be6cf166 100644 --- a/packages/angular/cli/models/command-runner.ts +++ b/packages/angular/cli/models/command-runner.ts @@ -23,7 +23,7 @@ import { Command } from './command'; import { CommandDescription, CommandDescriptionMap, - CommandProject, + CommandWorkspace, } from './interface'; import * as parser from './parser'; @@ -36,13 +36,13 @@ export interface CommandMapOptions { * Run a command. * @param args Raw unparsed arguments. * @param logger The logger to use. - * @param project Project data structure containing project information. + * @param workspace Workspace information. * @param commands The map of supported commands. */ export async function runCommand( args: string[], logger: logging.Logger, - project: CommandProject, + workspace: CommandWorkspace, commands?: CommandMapOptions, ): Promise { if (commands === undefined) { @@ -176,7 +176,7 @@ export async function runCommand( const parsedOptions = parser.parseArguments(args, description.options); Command.setCommandMap(commandMap); - const command = new description.impl({ project }, description, logger); + const command = new description.impl({ workspace }, description, logger); return await command.validateAndRun(parsedOptions); } diff --git a/packages/angular/cli/models/command.ts b/packages/angular/cli/models/command.ts index 53a1e4307179..932cd62ad4d5 100644 --- a/packages/angular/cli/models/command.ts +++ b/packages/angular/cli/models/command.ts @@ -14,20 +14,19 @@ import { CommandContext, CommandDescription, CommandDescriptionMap, - CommandProject, CommandScope, + CommandWorkspace, Option, } from './interface'; -export interface BaseCommandOptions { +export interface BaseCommandOptions extends Arguments { help?: boolean; - help_json?: boolean; - '--': string[]; + helpJson?: boolean; } export abstract class Command { public allowMissingWorkspace = false; - public project: CommandProject; + public workspace: CommandWorkspace; protected static commandMap: CommandDescriptionMap; static setCommandMap(map: CommandDescriptionMap) { @@ -39,7 +38,7 @@ export abstract class Command public readonly description: CommandDescription, protected readonly logger: logging.Logger, ) { - this.project = context.project; + this.workspace = context.workspace; } async initialize(options: T): Promise { @@ -113,16 +112,16 @@ export abstract class Command async validateScope(): Promise { switch (this.description.scope) { case CommandScope.OutProject: - if (this.project.configFile || getWorkspace('local') !== null) { + if (this.workspace.configFile || getWorkspace('local') !== null) { this.logger.fatal(tags.oneLine` The ${this.description.name} command requires to be run outside of a project, but a - project definition was found at "${this.project.root}". + project definition was found at "${this.workspace.root}". `); throw 1; } break; case CommandScope.InProject: - if (!this.project.configFile || getWorkspace('local') === null) { + if (!this.workspace.configFile || getWorkspace('local') === null) { this.logger.fatal(tags.oneLine` The ${this.description.name} command requires to be run in an Angular project, but a project definition could not be found. @@ -139,14 +138,14 @@ export abstract class Command abstract async run(options: T & Arguments): Promise; async validateAndRun(options: T & Arguments): Promise { - if (!options.help && !options.help_json) { + if (!options.help && !options.helpJson) { await this.validateScope(); } await this.initialize(options); if (options.help) { return this.printHelp(options); - } else if (options.help_json) { + } else if (options.helpJson) { return this.printJsonHelp(options); } else { return await this.run(options); diff --git a/packages/angular/cli/models/interface.ts b/packages/angular/cli/models/interface.ts index 1bfdf7c1269b..7744e7b9effe 100644 --- a/packages/angular/cli/models/interface.ts +++ b/packages/angular/cli/models/interface.ts @@ -7,22 +7,35 @@ */ import { json, logging } from '@angular-devkit/core'; +/** + * Value type of arguments. + */ export type Value = number | string | boolean | (number | string | boolean)[]; -export type Arguments = { - [argName: string]: Value; -} & { +/** + * An object representing parsed arguments from the command line. + */ +export interface Arguments { + [argName: string]: Value | undefined; + + /** + * Extra arguments that were not parsed. Will be omitted if all arguments were parsed. + */ '--'?: string[]; } -export interface CommandInterface { - initialize(options: T): Promise; +/** + * The base interface for Command, understood by the command runner. + */ +export interface CommandInterface { printHelp(options: T): Promise; - printJsonHelp(_options: T): Promise; - run(options: T & Arguments): Promise; - validateAndRun(options: T & Arguments): Promise; + printJsonHelp(options: T): Promise; + validateAndRun(options: T): Promise; } +/** + * Command constructor. + */ export interface CommandConstructor { new( context: CommandContext, @@ -31,15 +44,24 @@ export interface CommandConstructor { ): CommandInterface; } -export interface CommandProject { +/** + * A CLI workspace information. + */ +export interface CommandWorkspace { root: string; configFile?: string; } +/** + * A command runner context. + */ export interface CommandContext { - project: CommandProject; + workspace: CommandWorkspace; } +/** + * Value types of an Option. + */ export enum OptionType { String = 'string', Number = 'number', @@ -48,11 +70,16 @@ export enum OptionType { Any = 'any', } +/** + * An option description. This is exposed when using `ng --help-json`. + */ export interface Option { name: string; description: string; + type: OptionType; types?: OptionType[]; + aliases: string[]; required?: boolean; format?: string; @@ -85,17 +112,12 @@ export interface CommandDescription { aliases: string[]; scope: CommandScope; + type: CommandType; impl: CommandConstructor; hidden: boolean; - - type: CommandType; - - schematics?: { - [name: string]: Option[]; - }; - architect?: { + suboptions?: { [name: string]: Option[]; }; } diff --git a/packages/angular/cli/models/schematic-command.ts b/packages/angular/cli/models/schematic-command.ts index f04c262bd1c8..f3a7fd597adc 100644 --- a/packages/angular/cli/models/schematic-command.ts +++ b/packages/angular/cli/models/schematic-command.ts @@ -100,13 +100,12 @@ export abstract class SchematicCommand< public async printHelp(options: T) { await super.printHelp(options); - const schematicNames = Object.keys(this.description.schematics || {}); + const schematicNames = Object.keys(this.description.suboptions || {}); await super.printHelpOptions(); this.logger.info(''); - if (this.description.schematics) { - + if (this.description.suboptions) { if (schematicNames.length > 1) { this.logger.info('Available Schematics:'); @@ -134,7 +133,7 @@ export abstract class SchematicCommand< }); } else if (schematicNames.length == 1) { this.logger.info('Options for schematic ' + schematicNames[0]); - await this.printHelpOptions(this.description.schematics[schematicNames[0]]); + await this.printHelpOptions(this.description.suboptions[schematicNames[0]]); } } @@ -142,8 +141,8 @@ export abstract class SchematicCommand< } async printHelpUsage() { - const schematicNames = Object.keys(this.description.schematics || {}); - if (this.description.schematics && schematicNames.length == 1) { + const schematicNames = Object.keys(this.description.suboptions || {}); + if (this.description.suboptions && schematicNames.length == 1) { this.logger.info(this.description.description); const opts = this.description.options.filter(x => x.positional === undefined); @@ -155,7 +154,7 @@ export abstract class SchematicCommand< ? schematicName : schematicNames[0]; - const schematicOptions = this.description.schematics[schematicNames[0]]; + const schematicOptions = this.description.suboptions[schematicNames[0]]; const schematicArgs = schematicOptions.filter(x => x.positional !== undefined); const argDisplay = schematicArgs.length > 0 ? ' ' + schematicArgs.map(a => `<${strings.dasherize(a.name)}>`).join(' ') @@ -222,7 +221,7 @@ export abstract class SchematicCommand< } const {force, dryRun} = options; - const fsHost = new virtualFs.ScopedHost(new NodeJsSyncHost(), normalize(this.project.root)); + const fsHost = new virtualFs.ScopedHost(new NodeJsSyncHost(), normalize(this.workspace.root)); const workflow = new NodeWorkflow( fsHost, @@ -230,7 +229,7 @@ export abstract class SchematicCommand< force, dryRun, packageManager: getPackageManager(), - root: normalize(this.project.root), + root: normalize(this.workspace.root), }, ); @@ -346,7 +345,7 @@ export abstract class SchematicCommand< const workflow = this._workflow; - const workingDir = normalize(systemPath.relative(this.project.root, process.cwd())); + const workingDir = normalize(systemPath.relative(this.workspace.root, process.cwd())); // Get the option object from the schematic schema. const schematic = this.getSchematic( @@ -476,7 +475,7 @@ export abstract class SchematicCommand< const workspaceLoader = new WorkspaceLoader(this._host); try { - workspaceLoader.loadWorkspace(this.project.root).pipe(take(1)) + workspaceLoader.loadWorkspace(this.workspace.root).pipe(take(1)) .subscribe( (workspace: experimental.workspace.Workspace) => this._workspace = workspace, (err: Error) => { diff --git a/packages/angular/cli/utilities/project.ts b/packages/angular/cli/utilities/project.ts index e2be4245dda7..0ec8749d9886 100644 --- a/packages/angular/cli/utilities/project.ts +++ b/packages/angular/cli/utilities/project.ts @@ -11,14 +11,14 @@ import { normalize } from '@angular-devkit/core'; import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; -import { CommandProject } from '../models/interface'; +import { CommandWorkspace } from '../models/interface'; import { findUp } from './find-up'; -export function insideProject(): boolean { - return getProjectDetails() !== null; +export function insideWorkspace(): boolean { + return getWorkspaceDetails() !== null; } -export function getProjectDetails(): CommandProject | null { +export function getWorkspaceDetails(): CommandWorkspace | null { const currentDir = process.cwd(); const possibleConfigFiles = [ 'angular.json', From c8ddfbbe620bc43cb6d83114fde65db64d8e6bf2 Mon Sep 17 00:00:00 2001 From: Hans Date: Tue, 4 Sep 2018 16:30:37 -0700 Subject: [PATCH 0041/1703] fix(@angular/cli): add options in help for architect commands And fix a bug with json schema to Option array. --- packages/angular/cli/models/architect-command.ts | 6 ++++++ packages/angular/cli/utilities/json-schema.ts | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/angular/cli/models/architect-command.ts b/packages/angular/cli/models/architect-command.ts index ae9979c99255..d5f2184e663a 100644 --- a/packages/angular/cli/models/architect-command.ts +++ b/packages/angular/cli/models/architect-command.ts @@ -87,6 +87,12 @@ export abstract class ArchitectCommand extends Command if ((!targetSpec.project || !targetSpec.target) && !this.multiTarget) { throw new Error('Cannot determine project or target for Architect command.'); } + + const builderConf = this._architect.getBuilderConfiguration(targetSpec); + const builderDesc = await this._architect.getBuilderDescription(builderConf).toPromise(); + const targetOptionArray = await parseJsonSchemaToOptions(this._registry, builderDesc.schema); + + this.description.options.push(...targetOptionArray); } async run(options: ArchitectCommandOptions) { diff --git a/packages/angular/cli/utilities/json-schema.ts b/packages/angular/cli/utilities/json-schema.ts index 41cabd96be88..28de7bb59cec 100644 --- a/packages/angular/cli/utilities/json-schema.ts +++ b/packages/angular/cli/utilities/json-schema.ts @@ -79,7 +79,7 @@ export async function parseJsonSchemaToOptions( if (!parentSchema) { // Ignore root. return; - } else if (pointer.split(/properties|items/g).length > 2) { + } else if (pointer.split(/\/(?:properties|items|definitions)\//g).length > 2) { // Ignore subitems (objects or arrays). return; } else if (json.isJsonArray(current)) { From 37d1a43d716ab6610ee64b453fc761f7f6a8845c Mon Sep 17 00:00:00 2001 From: Hans Date: Tue, 4 Sep 2018 16:32:55 -0700 Subject: [PATCH 0042/1703] fix(@angular/cli): only print options once for schematic --help --- packages/angular/cli/models/schematic-command.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/angular/cli/models/schematic-command.ts b/packages/angular/cli/models/schematic-command.ts index f3a7fd597adc..bf87b7559ada 100644 --- a/packages/angular/cli/models/schematic-command.ts +++ b/packages/angular/cli/models/schematic-command.ts @@ -100,11 +100,10 @@ export abstract class SchematicCommand< public async printHelp(options: T) { await super.printHelp(options); - const schematicNames = Object.keys(this.description.suboptions || {}); - - await super.printHelpOptions(); this.logger.info(''); + const schematicNames = Object.keys(this.description.suboptions || {}); + if (this.description.suboptions) { if (schematicNames.length > 1) { this.logger.info('Available Schematics:'); From 66fbc59767a08a34771da854dc6a665053b2f475 Mon Sep 17 00:00:00 2001 From: Hans Date: Tue, 4 Sep 2018 16:48:54 -0700 Subject: [PATCH 0043/1703] feat(@angular/cli): add long description and suboption option type --- packages/angular/cli/commands/add.json | 2 +- packages/angular/cli/commands/add.md | 2 ++ .../angular/cli/commands/generate-impl.ts | 7 +++++++ packages/angular/cli/models/interface.ts | 15 +++++++++++++- packages/angular/cli/models/parser.ts | 20 +++++++++++++------ packages/angular/cli/utilities/json-schema.ts | 11 ++++++++-- 6 files changed, 47 insertions(+), 10 deletions(-) create mode 100644 packages/angular/cli/commands/add.md diff --git a/packages/angular/cli/commands/add.json b/packages/angular/cli/commands/add.json index f61b1ac2b6e6..4009d228304c 100644 --- a/packages/angular/cli/commands/add.json +++ b/packages/angular/cli/commands/add.json @@ -2,7 +2,7 @@ "$schema": "http://json-schema.org/schema", "$id": "ng-cli://commands/add.json", "description": "Add support for a library to your project.", - "$longDescription": "", + "$longDescription": "./add.md", "$scope": "in", "$impl": "./add-impl#AddCommand", diff --git a/packages/angular/cli/commands/add.md b/packages/angular/cli/commands/add.md new file mode 100644 index 000000000000..c120a4b9d98d --- /dev/null +++ b/packages/angular/cli/commands/add.md @@ -0,0 +1,2 @@ +Add support for a library in your project, for example adding `@angular/pwa` which would configure +your project for PWA support. diff --git a/packages/angular/cli/commands/generate-impl.ts b/packages/angular/cli/commands/generate-impl.ts index 81189872338e..d4c65730e210 100644 --- a/packages/angular/cli/commands/generate-impl.ts +++ b/packages/angular/cli/commands/generate-impl.ts @@ -42,6 +42,12 @@ export class GenerateCommand< this.description.suboptions[`${collectionName}:${name}`] = options; } + + this.description.options.forEach(option => { + if (option.name == 'schematic') { + option.type = 'suboption'; + } + }); } public async run(options: T) { @@ -78,6 +84,7 @@ export class GenerateCommand< public async printHelp(options: T) { await super.printHelp(options); + this.logger.info(''); if (Object.keys(this.description.suboptions || {}).length == 1) { this.logger.info(`\nTo see help for a schematic run:`); this.logger.info(terminal.cyan(` ng generate --help`)); diff --git a/packages/angular/cli/models/interface.ts b/packages/angular/cli/models/interface.ts index 7744e7b9effe..412d6f9c5fe6 100644 --- a/packages/angular/cli/models/interface.ts +++ b/packages/angular/cli/models/interface.ts @@ -74,10 +74,17 @@ export enum OptionType { * An option description. This is exposed when using `ng --help-json`. */ export interface Option { + /** + * The name of the option. + */ name: string; + + /** + * A short description of the option. + */ description: string; - type: OptionType; + type: OptionType | 'suboption'; types?: OptionType[]; aliases: string[]; @@ -108,6 +115,12 @@ export enum CommandType { export interface CommandDescription { name: string; description: string; + + /** + * A long description of the option, in Markdown format. + */ + longDescription: string; + options: Option[]; aliases: string[]; diff --git a/packages/angular/cli/models/parser.ts b/packages/angular/cli/models/parser.ts index 1af0c368dd0c..2bf7a89098e0 100644 --- a/packages/angular/cli/models/parser.ts +++ b/packages/angular/cli/models/parser.ts @@ -7,19 +7,21 @@ * */ import { strings } from '@angular-devkit/core'; -import { Arguments, Option, Value } from './interface'; +import { Arguments, Option, OptionType, Value } from './interface'; -function _coerceType(str: string | undefined, type: string, v?: Value): Value | undefined { +function _coerceType(str: string | undefined, type: OptionType, v?: Value): Value | undefined { switch (type) { case 'any': if (Array.isArray(v)) { return v.concat(str || ''); } - return _coerceType(str, 'boolean', v) !== undefined ? _coerceType(str, 'boolean', v) - : _coerceType(str, 'number', v) !== undefined ? _coerceType(str, 'number', v) - : _coerceType(str, 'string', v); + return _coerceType(str, OptionType.Boolean, v) !== undefined + ? _coerceType(str, OptionType.Boolean, v) + : _coerceType(str, OptionType.Number, v) !== undefined + ? _coerceType(str, OptionType.Number, v) + : _coerceType(str, OptionType.String, v); case 'string': return str || ''; @@ -56,7 +58,13 @@ function _coerceType(str: string | undefined, type: string, v?: Value): Value | } function _coerce(str: string | undefined, o: Option | null, v?: Value): Value | undefined { - return _coerceType(str, o ? o.type : 'any', v); + if (!o) { + return _coerceType(str, OptionType.Any, v); + } else if (o.type == 'suboption') { + return _coerceType(str, OptionType.String, v); + } else { + return _coerceType(str, o.type, v); + } } diff --git a/packages/angular/cli/utilities/json-schema.ts b/packages/angular/cli/utilities/json-schema.ts index 28de7bb59cec..d852772e71c9 100644 --- a/packages/angular/cli/utilities/json-schema.ts +++ b/packages/angular/cli/utilities/json-schema.ts @@ -7,7 +7,8 @@ */ import { json } from '@angular-devkit/core'; import { ExportStringRef } from '@angular-devkit/schematics/tools'; -import { dirname } from 'path'; +import { readFileSync } from 'fs'; +import { dirname, resolve } from 'path'; import { CommandConstructor, CommandDescription, @@ -57,12 +58,18 @@ export async function parseJsonSchemaToCommandDescription( }); } + let longDescription = ''; + if (typeof schema.$longDescription == 'string' && schema.$longDescription) { + const ldPath = resolve(dirname(jsonPath), schema.$longDescription); + longDescription = readFileSync(ldPath, 'utf-8'); + } + const scope = _getEnumFromValue(schema.$scope, CommandScope, CommandScope.Default); const type = _getEnumFromValue(schema.$type, CommandType, CommandType.Default); const description = '' + (schema.description === undefined ? '' : schema.description); const hidden = !!schema.$hidden; - return { name, description, hidden, type, options, aliases, scope, impl }; + return { name, description, longDescription, hidden, type, options, aliases, scope, impl }; } export async function parseJsonSchemaToOptions( From c72c45f9db5c09ed3099e62d7a74c6bc47c70ef6 Mon Sep 17 00:00:00 2001 From: Hans Date: Tue, 4 Sep 2018 17:22:16 -0700 Subject: [PATCH 0044/1703] fix(@angular/cli): only add options if theres only one builder configuration --- .../angular/cli/models/architect-command.ts | 35 ++++++++++++++++--- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/packages/angular/cli/models/architect-command.ts b/packages/angular/cli/models/architect-command.ts index d5f2184e663a..981bb8602e42 100644 --- a/packages/angular/cli/models/architect-command.ts +++ b/packages/angular/cli/models/architect-command.ts @@ -5,7 +5,12 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ -import { Architect, BuildEvent, TargetSpecifier } from '@angular-devkit/architect'; +import { + Architect, + BuildEvent, + BuilderConfiguration, + TargetSpecifier, +} from '@angular-devkit/architect'; import { experimental, json, schema, tags } from '@angular-devkit/core'; import { NodeJsSyncHost, createConsoleLogger } from '@angular-devkit/core/node'; import { from } from 'rxjs'; @@ -88,11 +93,31 @@ export abstract class ArchitectCommand extends Command throw new Error('Cannot determine project or target for Architect command.'); } - const builderConf = this._architect.getBuilderConfiguration(targetSpec); - const builderDesc = await this._architect.getBuilderDescription(builderConf).toPromise(); - const targetOptionArray = await parseJsonSchemaToOptions(this._registry, builderDesc.schema); + if (this.target) { + // Add options IF there's only one builder of this kind. + const projectNames = this.getProjectNamesByTarget(this.target); + const builderConfigurations: BuilderConfiguration[] = []; + for (const projectName of projectNames) { + const targetSpec: TargetSpecifier = this._makeTargetSpecifier(options); + const targetDesc = this._architect.getBuilderConfiguration({ + project: projectName, + target: targetSpec.target, + }); + + if (!builderConfigurations.find(b => b.builder === targetDesc.builder)) { + builderConfigurations.push(targetDesc); + } + } + + if (builderConfigurations.length == 1) { + const builderConf = builderConfigurations[0]; + const builderDesc = await this._architect.getBuilderDescription(builderConf).toPromise(); - this.description.options.push(...targetOptionArray); + this.description.options.push(...( + await parseJsonSchemaToOptions(this._registry, builderDesc.schema) + )); + } + } } async run(options: ArchitectCommandOptions) { From fefef0271eda1fac5a1866235aa3d87bf5142497 Mon Sep 17 00:00:00 2001 From: Hans Larsen Date: Wed, 5 Sep 2018 07:46:49 -0700 Subject: [PATCH 0045/1703] refactor(@angular/cli): remove parseJsonFile and add options to parseJson instead --- packages/angular/cli/models/command-runner.ts | 16 ++++-- .../angular_devkit/core/src/json/parser.ts | 54 +++++++++++-------- 2 files changed, 42 insertions(+), 28 deletions(-) diff --git a/packages/angular/cli/models/command-runner.ts b/packages/angular/cli/models/command-runner.ts index d673be6cf166..9bd185486ca0 100644 --- a/packages/angular/cli/models/command-runner.ts +++ b/packages/angular/cli/models/command-runner.ts @@ -52,15 +52,21 @@ export async function runCommand( } const cliDir = dirname(commandMapPath); const commandsText = readFileSync(commandMapPath).toString('utf-8'); - const commandJson = json.parseJsonFile( + const commandJson = json.parseJson( commandsText, JsonParseMode.Loose, - commandMapPath, - ) as { [name: string]: string }; + { path: commandMapPath }, + ); + if (!isJsonObject(commandJson)) { + throw Error('Invalid command.json'); + } commands = {}; for (const commandName of Object.keys(commandJson)) { - commands[commandName] = resolve(cliDir, commandJson[commandName]); + const commandValue = commandJson[commandName]; + if (typeof commandValue == 'string') { + commands[commandName] = resolve(cliDir, commandValue); + } } } @@ -79,7 +85,7 @@ export async function runCommand( for (const name of Object.keys(commands)) { const schemaPath = commands[name]; const schemaContent = readFileSync(schemaPath, 'utf-8'); - const schema = json.parseJsonFile(schemaContent, JsonParseMode.Loose, schemaPath); + const schema = json.parseJson(schemaContent, JsonParseMode.Loose, { path: schemaPath }); if (!isJsonObject(schema)) { throw new Error('Invalid command JSON loaded from ' + JSON.stringify(schemaPath)); } diff --git a/packages/angular_devkit/core/src/json/parser.ts b/packages/angular_devkit/core/src/json/parser.ts index 90a9e1b12497..b89b6ccc92dd 100644 --- a/packages/angular_devkit/core/src/json/parser.ts +++ b/packages/angular_devkit/core/src/json/parser.ts @@ -862,40 +862,48 @@ export function parseJsonAst(input: string, mode = JsonParseMode.Default): JsonA /** - * Parse a JSON string into its value. This discards the AST and only returns the value itself. - * @param input The string to parse. - * @param mode The mode to parse the input with. {@see JsonParseMode}. - * @returns {JsonValue} The value represented by the JSON string. + * Options for the parseJson() function. */ -export function parseJson(input: string, mode = JsonParseMode.Default): JsonValue { - // Try parsing for the fastest path available, if error, uses our own parser for better errors. - if (mode == JsonParseMode.Strict) { - try { - return JSON.parse(input); - } catch (err) { - return parseJsonAst(input, mode).value; - } - } - - return parseJsonAst(input, mode).value; +interface ParseJsonOptions { + /** + * If omitted, will only emit errors related to the content of the JSON. If specified, any + * JSON errors will also include the path of the file that caused the error. + */ + path?: string; } + /** * Parse a JSON string into its value. This discards the AST and only returns the value itself. - * It also absorbs JSON parsing errors and return a new error with the path in it. Useful for - * showing errors when parsing from a file. + * + * If a path option is pass, it also absorbs JSON parsing errors and return a new error with the + * path in it. Useful for showing errors when parsing from a file. + * * @param input The string to parse. * @param mode The mode to parse the input with. {@see JsonParseMode}. + * @param options Additional optinos for parsing. * @returns {JsonValue} The value represented by the JSON string. */ -export function parseJsonFile(input: string, mode = JsonParseMode.Default, path: string) { +export function parseJson( + input: string, + mode = JsonParseMode.Default, + options?: ParseJsonOptions, +): JsonValue { try { - return parseJson(input, mode); + // Try parsing for the fastest path available, if error, uses our own parser for better errors. + if (mode == JsonParseMode.Strict) { + try { + return JSON.parse(input); + } catch (err) { + return parseJsonAst(input, mode).value; + } + } + + return parseJsonAst(input, mode).value; } catch (e) { - if (e instanceof JsonException) { - throw new PathSpecificJsonException(path, e); - } else { - throw e; + if (options && options.path && e instanceof JsonException) { + throw new PathSpecificJsonException(options.path, e); } + throw e; } } From 0070ea46e6eaa25ad49d17c24fe7bed546dc1ec0 Mon Sep 17 00:00:00 2001 From: Hans Date: Wed, 5 Sep 2018 16:49:41 -0700 Subject: [PATCH 0046/1703] fix(@angular-devkit/schematics): fix task executor on Windows Before we did not denormalize the paths, but were expecting to get paths as options. --- .../schematics/tools/workflow/node-workflow.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/angular_devkit/schematics/tools/workflow/node-workflow.ts b/packages/angular_devkit/schematics/tools/workflow/node-workflow.ts index 20ed4d9a9382..e1f507f5b268 100644 --- a/packages/angular_devkit/schematics/tools/workflow/node-workflow.ts +++ b/packages/angular_devkit/schematics/tools/workflow/node-workflow.ts @@ -5,7 +5,7 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ -import { Path, virtualFs } from '@angular-devkit/core'; +import { Path, getSystemPath, virtualFs } from '@angular-devkit/core'; import { workflow, } from '@angular-devkit/schematics'; // tslint:disable-line:no-implicit-dependencies @@ -42,13 +42,13 @@ export class NodeWorkflow extends workflow.BaseWorkflow { { allowPackageManagerOverride: true, packageManager: options.packageManager, - rootDirectory: options.root, + rootDirectory: options.root && getSystemPath(options.root), }, ); engineHost.registerTaskExecutor( BuiltinTaskExecutor.RepositoryInitializer, { - rootDirectory: options.root, + rootDirectory: options.root && getSystemPath(options.root), }, ); engineHost.registerTaskExecutor(BuiltinTaskExecutor.RunSchematic); From 2962ede3e3004bf67e7efa7d179ec7f7c4657d8c Mon Sep 17 00:00:00 2001 From: Filipe Silva Date: Wed, 5 Sep 2018 11:28:25 +0100 Subject: [PATCH 0047/1703] test(@angular-devkit/build-optimizer): also check method metadata --- .../src/transforms/scrub-file_spec.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/packages/angular_devkit/build_optimizer/src/transforms/scrub-file_spec.ts b/packages/angular_devkit/build_optimizer/src/transforms/scrub-file_spec.ts index 20c079ead1ab..5d1140fc8b55 100644 --- a/packages/angular_devkit/build_optimizer/src/transforms/scrub-file_spec.ts +++ b/packages/angular_devkit/build_optimizer/src/transforms/scrub-file_spec.ts @@ -341,6 +341,13 @@ describe('scrub-file', () => { NotInput(), __metadata("design:type", Object) ], Clazz.prototype, "other", void 0); + Clazz.prototype.myMethod = function () { return 'bar'; }; + __decorate([ + myDecorator(), + __metadata("design:type", Function), + __metadata("design:paramtypes", []), + __metadata("design:returntype", void 0) + ], MyClass.prototype, "myMethod", null); return Clazz; }()); `; @@ -357,6 +364,13 @@ describe('scrub-file', () => { NotInput(), __metadata("design:type", Object) ], Clazz.prototype, "other", void 0); + Clazz.prototype.myMethod = function () { return 'bar'; }; + __decorate([ + myDecorator(), + __metadata("design:type", Function), + __metadata("design:paramtypes", []), + __metadata("design:returntype", void 0) + ], MyClass.prototype, "myMethod", null); return Clazz; }()); `; From 6076e16ebcba1947cda308b9f3a6e611768ee82e Mon Sep 17 00:00:00 2001 From: Filipe Silva Date: Wed, 5 Sep 2018 11:30:23 +0100 Subject: [PATCH 0048/1703] feat(@angular-devkit/build-optimizer): remove constructor __param --- .../src/transforms/scrub-file.ts | 17 +++++++++- .../src/transforms/scrub-file_spec.ts | 32 +++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/packages/angular_devkit/build_optimizer/src/transforms/scrub-file.ts b/packages/angular_devkit/build_optimizer/src/transforms/scrub-file.ts index 620b4815ff57..5e7ed7afa48b 100644 --- a/packages/angular_devkit/build_optimizer/src/transforms/scrub-file.ts +++ b/packages/angular_devkit/build_optimizer/src/transforms/scrub-file.ts @@ -410,6 +410,7 @@ function pickDecorateNodesToRemove( // Only remove constructor parameter metadata on non-whitelisted classes. if (platformWhitelist.indexOf(classId.text) === -1) { + // Remove __metadata calls of type 'design:paramtypes'. const metadataCalls = elements.filter((el) => { if (!isTslibHelper(el, '__metadata', tslibImports, checker)) { return false; @@ -427,7 +428,21 @@ function pickDecorateNodesToRemove( return true; }); - ngDecoratorCalls.push(...metadataCalls); + // Remove all __param calls. + const paramCalls = elements.filter((el) => { + if (!isTslibHelper(el, '__param', tslibImports, checker)) { + return false; + } + if (el.arguments.length != 2) { + return false; + } + if (el.arguments[0].kind !== ts.SyntaxKind.NumericLiteral) { + return false; + } + + return true; + }); + ngDecoratorCalls.push(...metadataCalls, ...paramCalls); } // If all decorators are metadata decorators then return the whole `Class = __decorate([...])'` diff --git a/packages/angular_devkit/build_optimizer/src/transforms/scrub-file_spec.ts b/packages/angular_devkit/build_optimizer/src/transforms/scrub-file_spec.ts index 5d1140fc8b55..f9bae119b78c 100644 --- a/packages/angular_devkit/build_optimizer/src/transforms/scrub-file_spec.ts +++ b/packages/angular_devkit/build_optimizer/src/transforms/scrub-file_spec.ts @@ -422,6 +422,38 @@ describe('scrub-file', () => { }); }); + describe('__param', () => { + it('removes all constructor parameters and their type metadata', () => { + const output = tags.stripIndent` + var MyClass = /** @class */ (function () { + function MyClass(myParam) { + this.myProp = 'foo'; + } + MyClass = __decorate([ + myDecorator() + ], MyClass); + return MyClass; + }()); + `; + const input = tags.stripIndent` + var MyClass = /** @class */ (function () { + function MyClass(myParam) { + this.myProp = 'foo'; + } + MyClass = __decorate([ + myDecorator(), + __param(0, myDecorator()), + __metadata("design:paramtypes", [Number]) + ], MyClass); + return MyClass; + }()); + `; + + expect(testScrubFile(input)).toBeTruthy(); + expect(tags.oneLine`${transform(input)}`).toEqual(tags.oneLine`${output}`); + }); + }); + describe('propDecorators', () => { it('removes top-level Angular propDecorators', () => { const output = tags.stripIndent` From 7c5178c6983c4b8ecfdbbbbcbadae39f36f0bbc0 Mon Sep 17 00:00:00 2001 From: Filipe Silva Date: Wed, 5 Sep 2018 11:50:42 +0100 Subject: [PATCH 0049/1703] feat(@angular-devkit/build-optimizer): correctly identify renamed enums --- .../src/transforms/wrap-enums.ts | 10 +++- .../src/transforms/wrap-enums_spec.ts | 60 ++++++++++++++++++- 2 files changed, 65 insertions(+), 5 deletions(-) diff --git a/packages/angular_devkit/build_optimizer/src/transforms/wrap-enums.ts b/packages/angular_devkit/build_optimizer/src/transforms/wrap-enums.ts index 0100b162aab4..ca943192a901 100644 --- a/packages/angular_devkit/build_optimizer/src/transforms/wrap-enums.ts +++ b/packages/angular_devkit/build_optimizer/src/transforms/wrap-enums.ts @@ -221,10 +221,14 @@ function findTs2_3EnumIife( } const parameter = expression.parameters[0]; - if (!ts.isIdentifier(parameter.name) || parameter.name.text !== name) { + if (!ts.isIdentifier(parameter.name)) { return null; } + // The name of the parameter can be different than the name of the enum if it was renamed + // due to scope hoisting. + const parameterName = parameter.name.text; + // In TS 2.3 enums, the IIFE contains only expressions with a certain format. // If we find any that is different, we ignore the whole thing. for (let bodyIndex = 0; bodyIndex < expression.body.statements.length; ++bodyIndex) { @@ -245,7 +249,7 @@ function findTs2_3EnumIife( return null; } - if (!ts.isIdentifier(assignment.expression) || assignment.expression.text !== name) { + if (!ts.isIdentifier(assignment.expression) || assignment.expression.text !== parameterName) { return null; } @@ -261,7 +265,7 @@ function findTs2_3EnumIife( } if (!ts.isIdentifier(memberArgument.left.expression) - || memberArgument.left.expression.text !== name) { + || memberArgument.left.expression.text !== parameterName) { return null; } diff --git a/packages/angular_devkit/build_optimizer/src/transforms/wrap-enums_spec.ts b/packages/angular_devkit/build_optimizer/src/transforms/wrap-enums_spec.ts index 1718dfa7838a..7f1319fde9cb 100644 --- a/packages/angular_devkit/build_optimizer/src/transforms/wrap-enums_spec.ts +++ b/packages/angular_devkit/build_optimizer/src/transforms/wrap-enums_spec.ts @@ -14,6 +14,7 @@ import { getWrapEnumsTransformer } from './wrap-enums'; const transform = (content: string) => transformJavascript( { content, getTransforms: [getWrapEnumsTransformer] }).content; +// tslint:disable-next-line:no-big-function describe('wrap-enums', () => { it('wraps ts 2.2 enums in IIFE', () => { const input = tags.stripIndent` @@ -37,7 +38,7 @@ describe('wrap-enums', () => { expect(tags.oneLine`${transform(input)}`).toEqual(tags.oneLine`${output}`); }); - it('wraps ts 2.3 - 2.6 enums in IIFE', () => { + it('wraps ts >2.3 enums in IIFE', () => { const input = tags.stripIndent` export var ChangeDetectionStrategy; (function (ChangeDetectionStrategy) { @@ -56,7 +57,7 @@ describe('wrap-enums', () => { expect(tags.oneLine`${transform(input)}`).toEqual(tags.oneLine`${output}`); }); - it('wraps ts 2.3 - 2.6 enums in IIFE, even if they have funny numbers', () => { + it('wraps ts >2.3 enums in IIFE, even if they have funny numbers', () => { const input = tags.stripIndent` export var AnimatorControlState; (function (AnimatorControlState) { @@ -79,6 +80,61 @@ describe('wrap-enums', () => { expect(tags.oneLine`${transform(input)}`).toEqual(tags.oneLine`${output}`); }); + it('wraps ts >2.3 enums in IIFE, even if they were renamed due to scope hoisting', () => { + const input = tags.stripIndent` + var TokenType$1; + (function (TokenType) { + TokenType[TokenType["TAG_OPEN_START"] = 0] = "TAG_OPEN_START"; + TokenType[TokenType["TAG_OPEN_END"] = 1] = "TAG_OPEN_END"; + TokenType[TokenType["TAG_OPEN_END_VOID"] = 2] = "TAG_OPEN_END_VOID"; + TokenType[TokenType["TAG_CLOSE"] = 3] = "TAG_CLOSE"; + TokenType[TokenType["TEXT"] = 4] = "TEXT"; + TokenType[TokenType["ESCAPABLE_RAW_TEXT"] = 5] = "ESCAPABLE_RAW_TEXT"; + TokenType[TokenType["RAW_TEXT"] = 6] = "RAW_TEXT"; + TokenType[TokenType["COMMENT_START"] = 7] = "COMMENT_START"; + TokenType[TokenType["COMMENT_END"] = 8] = "COMMENT_END"; + TokenType[TokenType["CDATA_START"] = 9] = "CDATA_START"; + TokenType[TokenType["CDATA_END"] = 10] = "CDATA_END"; + TokenType[TokenType["ATTR_NAME"] = 11] = "ATTR_NAME"; + TokenType[TokenType["ATTR_VALUE"] = 12] = "ATTR_VALUE"; + TokenType[TokenType["DOC_TYPE"] = 13] = "DOC_TYPE"; + TokenType[TokenType["EXPANSION_FORM_START"] = 14] = "EXPANSION_FORM_START"; + TokenType[TokenType["EXPANSION_CASE_VALUE"] = 15] = "EXPANSION_CASE_VALUE"; + TokenType[TokenType["EXPANSION_CASE_EXP_START"] = 16] = "EXPANSION_CASE_EXP_START"; + TokenType[TokenType["EXPANSION_CASE_EXP_END"] = 17] = "EXPANSION_CASE_EXP_END"; + TokenType[TokenType["EXPANSION_FORM_END"] = 18] = "EXPANSION_FORM_END"; + TokenType[TokenType["EOF"] = 19] = "EOF"; + })(TokenType$1 || (TokenType$1 = {})); + `; + const output = tags.stripIndent` + var TokenType$1 = /*@__PURE__*/ (function (TokenType) { + TokenType[TokenType["TAG_OPEN_START"] = 0] = "TAG_OPEN_START"; + TokenType[TokenType["TAG_OPEN_END"] = 1] = "TAG_OPEN_END"; + TokenType[TokenType["TAG_OPEN_END_VOID"] = 2] = "TAG_OPEN_END_VOID"; + TokenType[TokenType["TAG_CLOSE"] = 3] = "TAG_CLOSE"; + TokenType[TokenType["TEXT"] = 4] = "TEXT"; + TokenType[TokenType["ESCAPABLE_RAW_TEXT"] = 5] = "ESCAPABLE_RAW_TEXT"; + TokenType[TokenType["RAW_TEXT"] = 6] = "RAW_TEXT"; + TokenType[TokenType["COMMENT_START"] = 7] = "COMMENT_START"; + TokenType[TokenType["COMMENT_END"] = 8] = "COMMENT_END"; + TokenType[TokenType["CDATA_START"] = 9] = "CDATA_START"; + TokenType[TokenType["CDATA_END"] = 10] = "CDATA_END"; + TokenType[TokenType["ATTR_NAME"] = 11] = "ATTR_NAME"; + TokenType[TokenType["ATTR_VALUE"] = 12] = "ATTR_VALUE"; + TokenType[TokenType["DOC_TYPE"] = 13] = "DOC_TYPE"; + TokenType[TokenType["EXPANSION_FORM_START"] = 14] = "EXPANSION_FORM_START"; + TokenType[TokenType["EXPANSION_CASE_VALUE"] = 15] = "EXPANSION_CASE_VALUE"; + TokenType[TokenType["EXPANSION_CASE_EXP_START"] = 16] = "EXPANSION_CASE_EXP_START"; + TokenType[TokenType["EXPANSION_CASE_EXP_END"] = 17] = "EXPANSION_CASE_EXP_END"; + TokenType[TokenType["EXPANSION_FORM_END"] = 18] = "EXPANSION_FORM_END"; + TokenType[TokenType["EOF"] = 19] = "EOF"; + return TokenType; + })({}); + `; + + expect(tags.oneLine`${transform(input)}`).toEqual(tags.oneLine`${output}`); + }); + it('wraps tsickle enums in IIFE', () => { const input = tags.stripIndent` /** @enum {number} */ From 53e6d2d3e9fd6a5a1dfa3c9ff55595b13c03a3ca Mon Sep 17 00:00:00 2001 From: Filipe Silva Date: Wed, 5 Sep 2018 12:58:14 +0100 Subject: [PATCH 0050/1703] test(@angular/cli): add third party decorator prod test --- .../e2e/tests/misc/third-party-decorators.ts | 160 ++++++++++++++++++ 1 file changed, 160 insertions(+) create mode 100644 tests/legacy-cli/e2e/tests/misc/third-party-decorators.ts diff --git a/tests/legacy-cli/e2e/tests/misc/third-party-decorators.ts b/tests/legacy-cli/e2e/tests/misc/third-party-decorators.ts new file mode 100644 index 000000000000..1886318a051c --- /dev/null +++ b/tests/legacy-cli/e2e/tests/misc/third-party-decorators.ts @@ -0,0 +1,160 @@ +import { writeMultipleFiles } from '../../utils/fs'; +import { ng, npm } from '../../utils/process'; +import { updateJsonFile } from '../../utils/project'; + +export default function () { + return updateJsonFile('package.json', packageJson => { + // Install ngrx + packageJson['dependencies']['@ngrx/effects'] = '^6.1.0'; + packageJson['dependencies']['@ngrx/schematics'] = '^6.1.0'; + packageJson['dependencies']['@ngrx/store'] = '^6.1.0'; + packageJson['dependencies']['@ngrx/store-devtools'] = '^6.1.0'; + }) + .then(() => npm('install')) + // Create an app that uses ngrx decorators and has e2e tests. + .then(_ => writeMultipleFiles({ + './e2e/src/app.po.ts': ` + import { browser, by, element } from 'protractor'; + export class AppPage { + navigateTo() { return browser.get('/'); } + getIncrementButton() { return element(by.buttonText('Increment')); } + getDecrementButton() { return element(by.buttonText('Decrement')); } + getResetButton() { return element(by.buttonText('Reset Counter')); } + getCounter() { return element(by.xpath('/html/body/app-root/div/span')).getText(); } + } + `, + './e2e/src/app.e2e-spec.ts': ` + import { AppPage } from './app.po'; + + describe('workspace-project App', () => { + let page: AppPage; + + beforeEach(() => { + page = new AppPage(); + }); + + it('should operate counter', () => { + page.navigateTo(); + page.getIncrementButton().click(); + page.getIncrementButton().click(); + expect(page.getCounter()).toEqual('2'); + page.getDecrementButton().click(); + expect(page.getCounter()).toEqual('1'); + page.getResetButton().click(); + expect(page.getCounter()).toEqual('0'); + }); + }); + `, + './src/app/app.component.ts': ` + import { Component } from '@angular/core'; + import { Store, select } from '@ngrx/store'; + import { Observable } from 'rxjs'; + import { INCREMENT, DECREMENT, RESET } from './counter.reducer'; + + interface AppState { + count: number; + } + + @Component({ + selector: 'app-root', + template: \` + +
Current Count: {{ count$ | async }}
+ + + + \`, + }) + export class AppComponent { + count$: Observable; + + constructor(private store: Store) { + this.count$ = store.pipe(select(state => state.count)); + } + + increment() { + this.store.dispatch({ type: INCREMENT }); + } + + decrement() { + this.store.dispatch({ type: DECREMENT }); + } + + reset() { + this.store.dispatch({ type: RESET }); + } + } + `, + './src/app/app.effects.ts': ` + import { Injectable } from '@angular/core'; + import { Actions, Effect } from '@ngrx/effects'; + import { filter, map, tap } from 'rxjs/operators'; + + @Injectable() + export class AppEffects { + + @Effect() + mapper$ = this.actions$.pipe(map(() => ({ type: 'ANOTHER'})), filter(() => false)); + + @Effect({ dispatch: false }) + logger$ = this.actions$.pipe(tap(console.log)); + + constructor(private actions$: Actions) {} + } + `, + './src/app/app.module.ts': ` + import { BrowserModule } from '@angular/platform-browser'; + import { NgModule } from '@angular/core'; + + import { AppComponent } from './app.component'; + import { StoreModule } from '@ngrx/store'; + import { StoreDevtoolsModule } from '@ngrx/store-devtools'; + import { environment } from '../environments/environment'; + import { EffectsModule } from '@ngrx/effects'; + import { AppEffects } from './app.effects'; + import { counterReducer } from './counter.reducer'; + + @NgModule({ + declarations: [ + AppComponent + ], + imports: [ + BrowserModule, + StoreModule.forRoot({ count: counterReducer }), + !environment.production ? StoreDevtoolsModule.instrument() : [], + EffectsModule.forRoot([AppEffects]) + ], + providers: [], + bootstrap: [AppComponent] + }) + export class AppModule { } + `, + './src/app/counter.reducer.ts': ` + import { Action } from '@ngrx/store'; + + export const INCREMENT = 'INCREMENT'; + export const DECREMENT = 'DECREMENT'; + export const RESET = 'RESET'; + + const initialState = 0; + + export function counterReducer(state: number = initialState, action: Action) { + switch (action.type) { + case INCREMENT: + return state + 1; + + case DECREMENT: + return state - 1; + + case RESET: + return 0; + + default: + return state; + } + } + `, + })) + // Run the e2e tests against a prod build. + .then(() => ng('e2e', '--prod')); +} From cc6181e3c9e2ae532c178107dac3cd32a9e1f8de Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Tue, 4 Sep 2018 19:00:07 -0400 Subject: [PATCH 0051/1703] feat(@schematics/angular): add CLI 7.x migration schematic --- .../migrations/migration-collection.json | 5 +++ .../angular/migrations/update-7/index.ts | 31 +++++++++++++++++++ 2 files changed, 36 insertions(+) create mode 100644 packages/schematics/angular/migrations/update-7/index.ts diff --git a/packages/schematics/angular/migrations/migration-collection.json b/packages/schematics/angular/migrations/migration-collection.json index eaf1c496708d..adee986ffabe 100644 --- a/packages/schematics/angular/migrations/migration-collection.json +++ b/packages/schematics/angular/migrations/migration-collection.json @@ -4,6 +4,11 @@ "version": "6.0.0-beta.8", "factory": "./update-6", "description": "Update an Angular CLI project to version 6." + }, + "migration-02": { + "version": "7.0.0-beta.0", + "factory": "./update-7", + "description": "Update an Angular CLI project to version 7." } } } \ No newline at end of file diff --git a/packages/schematics/angular/migrations/update-7/index.ts b/packages/schematics/angular/migrations/update-7/index.ts new file mode 100644 index 000000000000..626d4a449ef1 --- /dev/null +++ b/packages/schematics/angular/migrations/update-7/index.ts @@ -0,0 +1,31 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import { Rule } from '@angular-devkit/schematics'; +import { NodePackageInstallTask } from '@angular-devkit/schematics/tasks'; +import { + NodeDependencyType, + addPackageJsonDependency, +} from '../../utility/dependencies'; +import { latestVersions } from '../../utility/latest-versions'; + + +export default function(): Rule { + return (tree, context) => { + addPackageJsonDependency( + tree, + { + type: NodeDependencyType.Dev, + name: '@angular-devkit/build-angular', + version: latestVersions.DevkitBuildAngular, + overwrite: true, + }, + ); + + context.addTask(new NodePackageInstallTask()); + }; +} From 58977a196985562db1b350af3344ba46c402d2ae Mon Sep 17 00:00:00 2001 From: Alan Agius Date: Thu, 30 Aug 2018 09:06:30 +0200 Subject: [PATCH 0052/1703] feat(@ngtools/webpack): add `typescript` version `3.0.x` as a peer dependency --- packages/ngtools/webpack/package.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/ngtools/webpack/package.json b/packages/ngtools/webpack/package.json index e49d0c2868f5..c3e3c72530ae 100644 --- a/packages/ngtools/webpack/package.json +++ b/packages/ngtools/webpack/package.json @@ -31,11 +31,10 @@ "webpack-sources": "^1.1.0" }, "peerDependencies": { - "typescript": "~2.4.0 || ~2.5.0 || ~2.6.0 || ~2.7.0 || ~2.8.0 || ~2.9.0", + "typescript": ">=2.4.0 < 3.1", "webpack": "^4.0.0" }, "devDependencies": { - "typescript": "~2.9.2", "webpack": "^4.0.0" } } From 88804b63dc4380be087b2b41c3372f69075640a6 Mon Sep 17 00:00:00 2001 From: Alan Agius Date: Thu, 30 Aug 2018 15:22:08 +0200 Subject: [PATCH 0053/1703] build: update repo to use `typescript` `3.0.x` Updated `@angular/` to `^7.0.0-beta.4` since we need this to be able to run the tests. Due to the `@angular/compiler` in `v6` doesn't support `typescript` `3.0.x`. --- package.json | 2 +- .../angular_devkit/build_angular/package.json | 22 ++--- .../build_ng_packagr/package.json | 4 +- packages/schematics/angular/package.json | 2 +- yarn.lock | 94 ++++++++++--------- 5 files changed, 66 insertions(+), 58 deletions(-) diff --git a/package.json b/package.json index 04bba652e809..7f1bcd60ea16 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "glob": "^7.0.3", "temp": "^0.8.3", "tslint": "^5.11.0", - "typescript": "~2.9.2" + "typescript": "~3.0.1" }, "devDependencies": { "@bazel/typescript": "0.16.1", diff --git a/packages/angular_devkit/build_angular/package.json b/packages/angular_devkit/build_angular/package.json index 68284ad44e6d..d7662c1e1ef5 100644 --- a/packages/angular_devkit/build_angular/package.json +++ b/packages/angular_devkit/build_angular/package.json @@ -59,19 +59,19 @@ "node-sass": "^4.9.3" }, "devDependencies": { - "@angular/animations": "^6.1.0", + "@angular/animations": "^7.0.0-beta.4", "@angular/cdk": "^6.1.0", - "@angular/common": "^6.1.0", - "@angular/compiler": "^6.1.0", - "@angular/compiler-cli": "^6.1.0", - "@angular/core": "^6.1.0", - "@angular/http": "^6.1.0", + "@angular/common": "^7.0.0-beta.4", + "@angular/compiler": "^7.0.0-beta.4", + "@angular/compiler-cli": "^7.0.0-beta.4", + "@angular/core": "^7.0.0-beta.4", + "@angular/http": "^7.0.0-beta.4", "@angular/material": "^6.1.0", - "@angular/platform-browser": "^6.1.0", - "@angular/platform-browser-dynamic": "^6.1.0", - "@angular/platform-server": "^6.1.0", - "@angular/router": "^6.1.0", - "@angular/service-worker": "^6.1.0", + "@angular/platform-browser": "^7.0.0-beta.4", + "@angular/platform-browser-dynamic": "^7.0.0-beta.4", + "@angular/platform-server": "^7.0.0-beta.4", + "@angular/router": "^7.0.0-beta.4", + "@angular/service-worker": "^7.0.0-beta.4", "codelyzer": "^4.2.1", "core-js": "^2.4.1", "bootstrap": "^4.0.0", diff --git a/packages/angular_devkit/build_ng_packagr/package.json b/packages/angular_devkit/build_ng_packagr/package.json index a9eebffa6c25..a14b3af53761 100644 --- a/packages/angular_devkit/build_ng_packagr/package.json +++ b/packages/angular_devkit/build_ng_packagr/package.json @@ -15,8 +15,8 @@ "ng-packagr": "^2.2.0 || ^3.0.0 || ^4.0.0" }, "devDependencies": { - "@angular/compiler": "^6.1.0", - "@angular/compiler-cli": "^6.1.0", + "@angular/compiler": "^7.0.0-beta.4", + "@angular/compiler-cli": "^7.0.0-beta.4", "ng-packagr": "^4.1.0", "tsickle": ">=0.27.3", "tslib": "^1.9.0" diff --git a/packages/schematics/angular/package.json b/packages/schematics/angular/package.json index 98c0fac997eb..49a9160cd022 100644 --- a/packages/schematics/angular/package.json +++ b/packages/schematics/angular/package.json @@ -11,6 +11,6 @@ "dependencies": { "@angular-devkit/core": "0.0.0", "@angular-devkit/schematics": "0.0.0", - "typescript": ">=2.6.2 <2.10" + "typescript": ">=2.6.2 <3.1" } } diff --git a/yarn.lock b/yarn.lock index 00e64d738515..3309a02b20ac 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,9 +2,9 @@ # yarn lockfile v1 -"@angular/animations@^6.1.0": - version "6.1.2" - resolved "https://registry.yarnpkg.com/@angular/animations/-/animations-6.1.2.tgz#31070ac85bca47e3feab887fed8872c147fcf63e" +"@angular/animations@^7.0.0-beta.4": + version "7.0.0-beta.4" + resolved "https://registry.yarnpkg.com/@angular/animations/-/animations-7.0.0-beta.4.tgz#98a8c709604f4023f888a33205e01790015953a0" dependencies: tslib "^1.9.0" @@ -14,36 +14,38 @@ dependencies: tslib "^1.7.1" -"@angular/common@^6.1.0": - version "6.1.2" - resolved "https://registry.yarnpkg.com/@angular/common/-/common-6.1.2.tgz#6e3bd4b16975dbb4ba67e2fc7589d0a036e5df12" +"@angular/common@^7.0.0-beta.4": + version "7.0.0-beta.4" + resolved "https://registry.yarnpkg.com/@angular/common/-/common-7.0.0-beta.4.tgz#e7d6526e1034fb7eb7926dcff0b8205284b40854" dependencies: tslib "^1.9.0" -"@angular/compiler-cli@^6.1.0": - version "6.1.2" - resolved "https://registry.yarnpkg.com/@angular/compiler-cli/-/compiler-cli-6.1.2.tgz#00829df848210a8a06c3641428f36525fdf41e53" +"@angular/compiler-cli@^7.0.0-beta.4": + version "7.0.0-beta.4" + resolved "https://registry.yarnpkg.com/@angular/compiler-cli/-/compiler-cli-7.0.0-beta.4.tgz#43ca2c585c903f5c219b7903b2ceabd3d4fb3d48" dependencies: chokidar "^1.4.2" + convert-source-map "^1.5.1" + magic-string "^0.25.0" minimist "^1.2.0" reflect-metadata "^0.1.2" - tsickle "^0.32.1" + source-map "^0.6.1" -"@angular/compiler@^6.1.0": - version "6.1.2" - resolved "https://registry.yarnpkg.com/@angular/compiler/-/compiler-6.1.2.tgz#56fada04cb7dc77a5d3f0b786be713130293d468" +"@angular/compiler@^7.0.0-beta.4": + version "7.0.0-beta.4" + resolved "https://registry.yarnpkg.com/@angular/compiler/-/compiler-7.0.0-beta.4.tgz#f61a1e15db3e895ea6e8be98a01b261924b87bd7" dependencies: tslib "^1.9.0" -"@angular/core@^6.1.0": - version "6.1.2" - resolved "https://registry.yarnpkg.com/@angular/core/-/core-6.1.2.tgz#412dd32a91ec1d5d6db508babbb36e9799235d8d" +"@angular/core@^7.0.0-beta.4": + version "7.0.0-beta.4" + resolved "https://registry.yarnpkg.com/@angular/core/-/core-7.0.0-beta.4.tgz#797edfc2c59bca443eac0a277cde328f3c704b91" dependencies: tslib "^1.9.0" -"@angular/http@^6.1.0": - version "6.1.2" - resolved "https://registry.yarnpkg.com/@angular/http/-/http-6.1.2.tgz#2e0e376686f4bf51ef043856b26679e88d7a92b9" +"@angular/http@^7.0.0-beta.4": + version "7.0.0-beta.4" + resolved "https://registry.yarnpkg.com/@angular/http/-/http-7.0.0-beta.4.tgz#72230abac0b97c32cb9d3e98a0855d26f332ee79" dependencies: tslib "^1.9.0" @@ -55,35 +57,35 @@ optionalDependencies: parse5 "^5.0.0" -"@angular/platform-browser-dynamic@^6.1.0": - version "6.1.2" - resolved "https://registry.yarnpkg.com/@angular/platform-browser-dynamic/-/platform-browser-dynamic-6.1.2.tgz#76d813e6ab577ce1ffdb31e88d37df9a5177f55c" +"@angular/platform-browser-dynamic@^7.0.0-beta.4": + version "7.0.0-beta.4" + resolved "https://registry.yarnpkg.com/@angular/platform-browser-dynamic/-/platform-browser-dynamic-7.0.0-beta.4.tgz#2d930f01aca9bd12fccb20754952df1af3acb02f" dependencies: tslib "^1.9.0" -"@angular/platform-browser@^6.1.0": - version "6.1.2" - resolved "https://registry.yarnpkg.com/@angular/platform-browser/-/platform-browser-6.1.2.tgz#9a2a498e27e2bcd0fc8d6c696340c258f8b3e322" +"@angular/platform-browser@^7.0.0-beta.4": + version "7.0.0-beta.4" + resolved "https://registry.yarnpkg.com/@angular/platform-browser/-/platform-browser-7.0.0-beta.4.tgz#bea6218c7d2b549a2d29bf5ba59df7d398f116bb" dependencies: tslib "^1.9.0" -"@angular/platform-server@^6.1.0": - version "6.1.2" - resolved "https://registry.yarnpkg.com/@angular/platform-server/-/platform-server-6.1.2.tgz#f8954c5720169c4a2433cb73678c8a8709b12271" +"@angular/platform-server@^7.0.0-beta.4": + version "7.0.0-beta.4" + resolved "https://registry.yarnpkg.com/@angular/platform-server/-/platform-server-7.0.0-beta.4.tgz#71702fd37182a5638ea13e9eb2c7312f54762ad1" dependencies: domino "^2.0.1" tslib "^1.9.0" xhr2 "^0.1.4" -"@angular/router@^6.1.0": - version "6.1.2" - resolved "https://registry.yarnpkg.com/@angular/router/-/router-6.1.2.tgz#5e9e12225b41940fe346675caf1ed15033738abc" +"@angular/router@^7.0.0-beta.4": + version "7.0.0-beta.4" + resolved "https://registry.yarnpkg.com/@angular/router/-/router-7.0.0-beta.4.tgz#becbdf90fae1be20da598cdb2ad69179d9e66901" dependencies: tslib "^1.9.0" -"@angular/service-worker@^6.1.0": - version "6.1.2" - resolved "https://registry.yarnpkg.com/@angular/service-worker/-/service-worker-6.1.2.tgz#11b1b93a040ae20218feb3ea45b2aca792c6e1c1" +"@angular/service-worker@^7.0.0-beta.4": + version "7.0.0-beta.4" + resolved "https://registry.yarnpkg.com/@angular/service-worker/-/service-worker-7.0.0-beta.4.tgz#c4c37f211a325c32c0d59031b63ca64822a46a9f" dependencies: tslib "^1.9.0" @@ -1914,7 +1916,7 @@ conventional-commits-parser@^3.0.0: through2 "^2.0.0" trim-off-newlines "^1.0.0" -convert-source-map@^1.5.0: +convert-source-map@^1.5.0, convert-source-map@^1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.5.1.tgz#b8278097b9bc229365de5c62cf5fcaed8b5599e5" @@ -4663,6 +4665,12 @@ magic-string@^0.22.4: dependencies: vlq "^0.2.2" +magic-string@^0.25.0: + version "0.25.0" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.0.tgz#1f3696f9931ff0a1ed4c132250529e19cad6759b" + dependencies: + sourcemap-codec "^1.4.1" + make-dir@^1.0.0: version "1.3.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.3.0.tgz#79c1033b80515bd6d24ec9933e860ca75ee27f0c" @@ -6795,6 +6803,10 @@ source-map@~0.2.0: dependencies: amdefine ">=0.0.4" +sourcemap-codec@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.1.tgz#c8fd92d91889e902a07aee392bdd2c5863958ba2" + spdx-compare@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/spdx-compare/-/spdx-compare-1.0.0.tgz#2c55f117362078d7409e6d7b08ce70a857cd3ed7" @@ -7311,7 +7323,7 @@ ts-node@^5.0.0: source-map-support "^0.5.3" yn "^2.0.0" -tsickle@0.32.1, tsickle@>=0.27.3, tsickle@^0.32.1: +tsickle@0.32.1, tsickle@>=0.27.3: version "0.32.1" resolved "https://registry.yarnpkg.com/tsickle/-/tsickle-0.32.1.tgz#f16e94ba80b32fc9ebe320dc94fbc2ca7f3521a5" dependencies: @@ -7395,13 +7407,9 @@ typedarray@^0.0.6: version "0.0.6" resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" -"typescript@>=2.6.2 <2.10", typescript@~2.9.2: - version "2.9.2" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.9.2.tgz#1cbf61d05d6b96269244eb6a3bce4bd914e0f00c" - -typescript@~2.6.2: - version "2.6.2" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.6.2.tgz#3c5b6fd7f6de0914269027f03c0946758f7673a4" +"typescript@>=2.6.2 <3.1", typescript@~3.0.1: + version "3.0.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.0.3.tgz#4853b3e275ecdaa27f78fda46dc273a7eb7fc1c8" uglify-es@^3.3.4: version "3.3.9" From a723310e94cff9eb7758cd6a176503b57133c80a Mon Sep 17 00:00:00 2001 From: Alan Agius Date: Fri, 31 Aug 2018 07:41:09 +0200 Subject: [PATCH 0054/1703] feat(@angular-devkit/build-optimizer): update `typescript` dependency to `3.0.x` --- .../build_angular/test/browser/build-optimizer_spec_large.ts | 2 +- packages/angular_devkit/build_optimizer/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/angular_devkit/build_angular/test/browser/build-optimizer_spec_large.ts b/packages/angular_devkit/build_angular/test/browser/build-optimizer_spec_large.ts index e91af8d67d14..4e1f1a0aca9e 100644 --- a/packages/angular_devkit/build_angular/test/browser/build-optimizer_spec_large.ts +++ b/packages/angular_devkit/build_angular/test/browser/build-optimizer_spec_large.ts @@ -21,7 +21,7 @@ describe('Browser Builder build optimizer', () => { it('works', (done) => { const overrides = { aot: true, buildOptimizer: true }; - runTargetSpec(host, browserTargetSpec, overrides).pipe( + runTargetSpec(host, browserTargetSpec, overrides, DefaultTimeout * 3).pipe( tap((buildEvent) => expect(buildEvent.success).toBe(true)), tap(() => { const content = virtualFs.fileBufferToString(host.scopedSync().read(normalize(fileName))); diff --git a/packages/angular_devkit/build_optimizer/package.json b/packages/angular_devkit/build_optimizer/package.json index 3ffe3d902374..461f626380f7 100644 --- a/packages/angular_devkit/build_optimizer/package.json +++ b/packages/angular_devkit/build_optimizer/package.json @@ -11,7 +11,7 @@ "dependencies": { "loader-utils": "^1.1.0", "source-map": "^0.5.6", - "typescript": "~2.9.2", + "typescript": "~3.0.1", "webpack-sources": "^1.1.0" } } From 509c0fa8afd3a076f8040609ae9c39fa1f017a0c Mon Sep 17 00:00:00 2001 From: Alan Agius Date: Fri, 31 Aug 2018 15:49:39 +0200 Subject: [PATCH 0055/1703] build: add `typescript` as `devDependencies` --- packages/ngtools/webpack/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/ngtools/webpack/package.json b/packages/ngtools/webpack/package.json index c3e3c72530ae..965b0ce2ad37 100644 --- a/packages/ngtools/webpack/package.json +++ b/packages/ngtools/webpack/package.json @@ -35,6 +35,7 @@ "webpack": "^4.0.0" }, "devDependencies": { + "typescript": "~3.0.1", "webpack": "^4.0.0" } } From bdebc9e0cb488b052cb3587f292420ac9c215417 Mon Sep 17 00:00:00 2001 From: Alan Agius Date: Sun, 12 Aug 2018 10:39:19 +0200 Subject: [PATCH 0056/1703] feat(@angular-devkit/build-angular): add `ignore` option to `assets` object You can now provide an array of globs to `ignore` from copying Closes #11850 --- .../stories/asset-configuration.md | 16 +++++++--- packages/angular/cli/lib/config/schema.json | 21 ++++++++++--- .../models/webpack-configs/common.ts | 1 + .../build_angular/src/browser/schema.d.ts | 5 ++++ .../build_angular/src/browser/schema.json | 7 +++++ .../build_angular/src/karma/schema.json | 7 +++++ .../test/browser/assets_spec_large.ts | 30 +++++++++++++++++++ 7 files changed, 79 insertions(+), 8 deletions(-) diff --git a/docs/documentation/stories/asset-configuration.md b/docs/documentation/stories/asset-configuration.md index 6d2f50556cd9..07eee9ed23bb 100644 --- a/docs/documentation/stories/asset-configuration.md +++ b/docs/documentation/stories/asset-configuration.md @@ -23,9 +23,10 @@ The array below does the same as the default one: ] ``` -`glob` is the a [node-glob](https://github.com/isaacs/node-glob) using `input` as base directory. -`input` is relative to the workspace root, while `output` is relative to `outDir` -(`dist/project-name` default). +- `glob` is the a [node-glob](https://github.com/isaacs/node-glob) using `input` as base directory. +- `input` is relative to the workspace root. +- `ignore` is a list of globs to ignore from copying. +- `output` is relative to `outDir` (`dist/project-name` default). You can use this extended configuration to copy assets from outside your project. For instance, you can copy assets from a node package: @@ -36,7 +37,14 @@ The array below does the same as the default one: ] ``` -The contents of `node_modules/some-package/images/` will be available in `dist/some-package/`. +You can ignore certain files from copying by using the `ignore` option: + ```json +"assets": [ + { "glob": "**/*", "input": "src/assets/", "ignore": ["**/*.svg"], "output": "/assets/" }, +] +``` + +The contents of `node_modules/some-package/images/` will be available in `dist/some-package/`. ## Writing assets outside of `dist/` diff --git a/packages/angular/cli/lib/config/schema.json b/packages/angular/cli/lib/config/schema.json index 4183c0aee9a8..b87f3b70a110 100644 --- a/packages/angular/cli/lib/config/schema.json +++ b/packages/angular/cli/lib/config/schema.json @@ -108,7 +108,7 @@ "default": false, "alias": "t" }, - "module": { + "module": { "type": "string", "description": "Allows specification of the declaring module.", "alias": "m" @@ -160,7 +160,7 @@ "description": "Flag to indicate if a dir is created.", "default": true }, - "module": { + "module": { "type": "string", "description": "Allows specification of the declaring module.", "alias": "m" @@ -219,7 +219,7 @@ "default": true, "visible": false }, - "module": { + "module": { "type": "string", "description": "Allows specification of the declaring module.", "alias": "m" @@ -778,6 +778,13 @@ "output": { "type": "string", "description": "Absolute path within the output." + }, + "ignore": { + "description": "An array of globs to ignore.", + "type": "array", + "items": { + "type": "string" + } } }, "additionalProperties": false, @@ -1241,6 +1248,13 @@ "output": { "type": "string", "description": "Absolute path within the output." + }, + "ignore": { + "description": "An array of globs to ignore.", + "type": "array", + "items": { + "type": "string" + } } }, "additionalProperties": false, @@ -1541,7 +1555,6 @@ } ] } - } }, "tslint": { diff --git a/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/common.ts b/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/common.ts index 29eea7c418f7..fe5ab77d2086 100644 --- a/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/common.ts +++ b/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/common.ts @@ -125,6 +125,7 @@ export function getCommonConfig(wco: WebpackConfigOptions) { context: asset.input, // Now we remove starting slash to make Webpack place it from the output root. to: asset.output.replace(/^\//, ''), + ignore: asset.ignore, from: { glob: asset.glob, dot: true diff --git a/packages/angular_devkit/build_angular/src/browser/schema.d.ts b/packages/angular_devkit/build_angular/src/browser/schema.d.ts index 0cd707918eb2..ca0fc891efe9 100644 --- a/packages/angular_devkit/build_angular/src/browser/schema.d.ts +++ b/packages/angular_devkit/build_angular/src/browser/schema.d.ts @@ -237,6 +237,11 @@ export interface AssetPatternObject { */ input: string; + /** + * An array of globs to ignore. + */ + ignore?: string[]; + /** * Absolute path within the output. */ diff --git a/packages/angular_devkit/build_angular/src/browser/schema.json b/packages/angular_devkit/build_angular/src/browser/schema.json index 61aab484cf21..0539ceb4b628 100644 --- a/packages/angular_devkit/build_angular/src/browser/schema.json +++ b/packages/angular_devkit/build_angular/src/browser/schema.json @@ -259,6 +259,13 @@ "type": "string", "description": "The input path dir in which to apply 'glob'. Defaults to the project root." }, + "ignore": { + "description": "An array of globs to ignore.", + "type": "array", + "items": { + "type": "string" + } + }, "output": { "type": "string", "description": "Absolute path within the output." diff --git a/packages/angular_devkit/build_angular/src/karma/schema.json b/packages/angular_devkit/build_angular/src/karma/schema.json index ce3414672ef6..4316a48ca246 100644 --- a/packages/angular_devkit/build_angular/src/karma/schema.json +++ b/packages/angular_devkit/build_angular/src/karma/schema.json @@ -174,6 +174,13 @@ "output": { "type": "string", "description": "Absolute path within the output." + }, + "ignore": { + "description": "An array of globs to ignore.", + "type": "array", + "items": { + "type": "string" + } } }, "additionalProperties": false, diff --git a/packages/angular_devkit/build_angular/test/browser/assets_spec_large.ts b/packages/angular_devkit/build_angular/test/browser/assets_spec_large.ts index 703e44a73060..8d7fee3205e7 100644 --- a/packages/angular_devkit/build_angular/test/browser/assets_spec_large.ts +++ b/packages/angular_devkit/build_angular/test/browser/assets_spec_large.ts @@ -59,6 +59,36 @@ describe('Browser Builder assets', () => { ).toPromise().then(done, done.fail); }); + it('works with ignored patterns', (done) => { + const assets: { [path: string]: string } = { + './src/folder/.gitkeep': '', + './src/folder/asset-ignored.txt': '', + './src/folder/asset.txt': '', + }; + + host.writeMultipleFiles(assets); + + const overrides = { + assets: [ + { + glob: '**/*', + ignore: ['asset-ignored.txt'], + input: 'src/folder', + output: '/folder', + }, + ], + }; + + runTargetSpec(host, browserTargetSpec, overrides).pipe( + tap((buildEvent) => expect(buildEvent.success).toBe(true)), + tap(() => { + expect(host.scopedSync().exists(normalize('./dist/folder/asset.txt'))).toBe(true); + expect(host.scopedSync().exists(normalize('./dist/folder/asset-ignored.txt'))).toBe(false); + expect(host.scopedSync().exists(normalize('./dist/folder/.gitkeep'))).toBe(false); + }), + ).toPromise().then(done, done.fail); + }); + it('fails with non-absolute output path', (done) => { const assets: { [path: string]: string } = { './node_modules/some-package/node_modules-asset.txt': 'node_modules-asset.txt', From 01beb520bcd6ec60182e7c514ba97d5ef3aa29b3 Mon Sep 17 00:00:00 2001 From: Alan Agius Date: Fri, 31 Aug 2018 15:50:52 +0200 Subject: [PATCH 0057/1703] feat(@angular-devkit/build-angular): add `ignore` option to `assets` object You can now provide an array of globs to `ignore` from copying Closes #11850 --- docs/documentation/stories/asset-configuration.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/documentation/stories/asset-configuration.md b/docs/documentation/stories/asset-configuration.md index 07eee9ed23bb..e8b05b4c9dc1 100644 --- a/docs/documentation/stories/asset-configuration.md +++ b/docs/documentation/stories/asset-configuration.md @@ -1,7 +1,8 @@ # Project assets You use the `assets` array inside the build target in `angular.json` to list files or folders -you want to copy as-is when building your project. +you want to copy as-is when building your project. if you think you need to exclude files, +consider not putting that thing in the assets By default, the `src/assets/` folder and `src/favicon.ico` are copied over. @@ -23,7 +24,7 @@ The array below does the same as the default one: ] ``` -- `glob` is the a [node-glob](https://github.com/isaacs/node-glob) using `input` as base directory. +- `glob` is a [node-glob](https://github.com/isaacs/node-glob) using `input` as base directory. - `input` is relative to the workspace root. - `ignore` is a list of globs to ignore from copying. - `output` is relative to `outDir` (`dist/project-name` default). From a94c826661fe122332fcaa17b0457cf48974a86b Mon Sep 17 00:00:00 2001 From: Bram Borggreve Date: Tue, 21 Aug 2018 22:40:12 +0200 Subject: [PATCH 0058/1703] feat(@schematics/angular): let Universal schematic use configurations from build architect --- .../schematics/angular/universal/index.ts | 20 +++++++++++++++++++ .../angular/universal/index_spec.ts | 7 +++++++ 2 files changed, 27 insertions(+) diff --git a/packages/schematics/angular/universal/index.ts b/packages/schematics/angular/universal/index.ts index 6db955cd11bf..09c8bae3127d 100644 --- a/packages/schematics/angular/universal/index.ts +++ b/packages/schematics/angular/universal/index.ts @@ -68,6 +68,16 @@ function getClientTargets( return projectTargets; } +// TODO: Add types for the Target +// tslint:disable-next-line:no-any +function getFileReplacements(target: any ) { + const configurations = target.configurations || {}; + const production = configurations.production || {}; + const fileReplacements = production.fileReplacements || []; + + return fileReplacements; +} + function updateConfigFile(options: UniversalOptions, tsConfigDirectory: Path): Rule { return (host: Tree) => { const workspace = getWorkspace(host); @@ -83,9 +93,19 @@ function updateConfigFile(options: UniversalOptions, tsConfigDirectory: Path): R main: `${clientProject.root}src/main.server.ts`, tsConfig: join(tsConfigDirectory, `${options.tsconfigFileName}.json`), }; + + // TODO: Add types for the TargetConfiguration + // tslint:disable-next-line:no-any + const builderConfigurations: any = { + production: { + fileReplacements: getFileReplacements(projectTargets.build), + }, + }; + const serverTarget: JsonObject = { builder: '@angular-devkit/build-angular:server', options: builderOptions, + configurations: builderConfigurations, }; projectTargets.server = serverTarget; diff --git a/packages/schematics/angular/universal/index_spec.ts b/packages/schematics/angular/universal/index_spec.ts index 7a5e3304172c..0ca8c034ac10 100644 --- a/packages/schematics/angular/universal/index_spec.ts +++ b/packages/schematics/angular/universal/index_spec.ts @@ -130,6 +130,13 @@ describe('Universal Schematic', () => { expect(opts.outputPath).toEqual('dist/bar-server'); expect(opts.main).toEqual('projects/bar/src/main.server.ts'); expect(opts.tsConfig).toEqual('projects/bar/tsconfig.server.json'); + const configurations = targets.server.configurations; + expect(configurations.production).toBeDefined(); + expect(configurations.production.fileReplacements).toBeDefined(); + const fileReplacements = targets.server.configurations.production.fileReplacements; + expect(fileReplacements.length).toEqual(1); + expect(fileReplacements[0].replace).toEqual('projects/bar/src/environments/environment.ts'); + expect(fileReplacements[0].with).toEqual('projects/bar/src/environments/environment.prod.ts'); }); it('should add a server transition to BrowerModule import', () => { From ee7603f597dda3e9d856b7eb238731a35bf0fa35 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Thu, 9 Aug 2018 14:19:59 -0400 Subject: [PATCH 0059/1703] feat(@schematics/angular): add several prompts to ng-new --- .../schematics/angular/ng-new/schema.json | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/packages/schematics/angular/ng-new/schema.json b/packages/schematics/angular/ng-new/schema.json index 2c927a03f9bf..61a32c3cf593 100644 --- a/packages/schematics/angular/ng-new/schema.json +++ b/packages/schematics/angular/ng-new/schema.json @@ -15,7 +15,8 @@ "$default": { "$source": "argv", "index": 0 - } + }, + "x-prompt": "What name would you like to use for the project?" }, "experimentalIvy": { "description": "EXPERIMENTAL: Specifies whether to create a new application which uses the Ivy rendering engine.", @@ -98,19 +99,32 @@ "routing": { "type": "boolean", "description": "Generates a routing module.", - "default": false + "default": false, + "x-prompt": "Would you like to generate a routing module?" }, "prefix": { "type": "string", "format": "html-selector", "description": "The prefix to apply to generated selectors.", + "minLength": 1, "default": "app", "alias": "p" }, "style": { "description": "The file extension to be used for style files.", "type": "string", - "default": "css" + "default": "css", + "x-prompt": { + "message": "Which stylesheet format would you like to use?", + "type": "list", + "items": [ + { "value": "css", "label": "CSS" }, + { "value": "scss", "label": "SCSS [ http://sass-lang.com ]" }, + { "value": "sass", "label": "SASS [ http://sass-lang.com ]" }, + { "value": "less", "label": "LESS [ http://lesscss.org ]" }, + { "value": "styl", "label": "Stylus [ http://stylus-lang.com ]" } + ] + } }, "skipTests": { "description": "Skip creating spec files.", From 5007a19ad9f3567b8a2193538b6946407cd43694 Mon Sep 17 00:00:00 2001 From: Alan Agius Date: Fri, 31 Aug 2018 17:11:18 +0200 Subject: [PATCH 0060/1703] refactor(@angular-devkit/build-angular): clean up style webpack config Clean up several parts of the styles config and also removed the dependency on `postcss-url` --- .../angular_devkit/build_angular/package.json | 1 - .../models/webpack-configs/styles.ts | 168 +++++------------- .../plugins/postcss-cli-resources.ts | 47 +++-- .../src/angular-cli-files/plugins/webpack.ts | 3 +- yarn.lock | 10 -- 5 files changed, 81 insertions(+), 148 deletions(-) diff --git a/packages/angular_devkit/build_angular/package.json b/packages/angular_devkit/build_angular/package.json index d7662c1e1ef5..16c5bf4e9cb8 100644 --- a/packages/angular_devkit/build_angular/package.json +++ b/packages/angular_devkit/build_angular/package.json @@ -34,7 +34,6 @@ "postcss": "^6.0.22", "postcss-import": "^11.1.0", "postcss-loader": "^2.1.5", - "postcss-url": "^7.3.2", "raw-loader": "^0.5.1", "rxjs": "~6.2.0", "sass-loader": "^7.1.0", diff --git a/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/styles.ts b/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/styles.ts index 353e2b91ffc2..f9677ac8eae4 100644 --- a/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/styles.ts +++ b/packages/angular_devkit/build_angular/src/angular-cli-files/models/webpack-configs/styles.ts @@ -5,24 +5,21 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ -// tslint:disable -// TODO: cleanup this file, it's copied as is from Angular CLI. -import * as webpack from 'webpack'; import * as path from 'path'; -import { SuppressExtractedTextChunksWebpackPlugin } from '../../plugins/webpack'; -import { getOutputHashFormat } from './utils'; +import * as webpack from 'webpack'; +import { + PostcssCliResources, + RawCssLoader, + RemoveHashPlugin, + SuppressExtractedTextChunksWebpackPlugin, +} from '../../plugins/webpack'; import { WebpackConfigOptions } from '../build-options'; -import { findUp } from '../../utilities/find-up'; -import { RawCssLoader } from '../../plugins/webpack'; -import { normalizeExtraEntryPoints } from './utils'; -import { RemoveHashPlugin } from '../../plugins/remove-hash-plugin'; +import { getOutputHashFormat, normalizeExtraEntryPoints } from './utils'; -const postcssUrl = require('postcss-url'); const autoprefixer = require('autoprefixer'); const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const postcssImports = require('postcss-import'); -const PostcssCliResources = require('../../plugins/webpack').PostcssCliResources; /** * Enumerate loaders and their dependencies from this file to let the dependency validator @@ -38,19 +35,11 @@ const PostcssCliResources = require('../../plugins/webpack').PostcssCliResources * require('sass-loader') */ -interface PostcssUrlAsset { - url: string; - hash: string; - absolutePath: string; -} - export function getStylesConfig(wco: WebpackConfigOptions) { - const { root, projectRoot, buildOptions } = wco; - - // const appRoot = path.resolve(projectRoot, appConfig.root); + const { root, buildOptions } = wco; const entryPoints: { [key: string]: string[] } = {}; const globalStylePaths: string[] = []; - const extraPlugins: any[] = []; + const extraPlugins = []; const cssSourceMap = buildOptions.sourceMap; // Determine hashing format. @@ -62,37 +51,13 @@ export function getStylesConfig(wco: WebpackConfigOptions) { const postcssPluginCreator = function (loader: webpack.loader.LoaderContext) { return [ postcssImports({ - resolve: (url: string, context: string) => { - return new Promise((resolve, reject) => { - let hadTilde = false; - if (url && url.startsWith('~')) { - url = url.substr(1); - hadTilde = true; - } - loader.resolve(context, (hadTilde ? '' : './') + url, (err: Error, result: string) => { - if (err) { - if (hadTilde) { - reject(err); - return; - } - loader.resolve(context, url, (err: Error, result: string) => { - if (err) { - reject(err); - } else { - resolve(result); - } - }); - } else { - resolve(result); - } - }); - }); - }, + resolve: (url: string) => url.startsWith('~') ? url.substr(1) : url, load: (filename: string) => { return new Promise((resolve, reject) => { loader.fs.readFile(filename, (err: Error, data: Buffer) => { if (err) { reject(err); + return; } @@ -100,42 +65,11 @@ export function getStylesConfig(wco: WebpackConfigOptions) { resolve(content); }); }); - } - }), - postcssUrl({ - filter: ({ url }: PostcssUrlAsset) => url.startsWith('~'), - url: ({ url }: PostcssUrlAsset) => { - // Note: This will only find the first node_modules folder. - const nodeModules = findUp('node_modules', projectRoot); - if (!nodeModules) { - throw new Error('Cannot locate node_modules directory.') - } - const fullPath = path.join(nodeModules, url.substr(1)); - return path.relative(loader.context, fullPath).replace(/\\/g, '/'); - } + }, }), - postcssUrl([ - { - // Only convert root relative URLs, which CSS-Loader won't process into require(). - filter: ({ url }: PostcssUrlAsset) => url.startsWith('/') && !url.startsWith('//'), - url: ({ url }: PostcssUrlAsset) => { - if (deployUrl.match(/:\/\//) || deployUrl.startsWith('/')) { - // If deployUrl is absolute or root relative, ignore baseHref & use deployUrl as is. - return `${deployUrl.replace(/\/$/, '')}${url}`; - } else if (baseHref.match(/:\/\//)) { - // If baseHref contains a scheme, include it as is. - return baseHref.replace(/\/$/, '') + - `/${deployUrl}/${url}`.replace(/\/\/+/g, '/'); - } else { - // Join together base-href, deploy-url and the original URL. - // Also dedupe multiple slashes into single ones. - return `/${baseHref}/${deployUrl}/${url}`.replace(/\/\/+/g, '/'); - } - } - } - ]), PostcssCliResources({ - deployUrl: loader.loaders[loader.loaderIndex].options.ident == 'extracted' ? '' : deployUrl, + baseHref, + deployUrl, loader, filename: `[name]${hashFormat.file}.[ext]`, }), @@ -164,12 +98,11 @@ export function getStylesConfig(wco: WebpackConfigOptions) { normalizeExtraEntryPoints(buildOptions.styles, 'styles').forEach(style => { const resolvedPath = path.resolve(root, style.input); - // Add style entry points. if (entryPoints[style.bundleName]) { - entryPoints[style.bundleName].push(resolvedPath) + entryPoints[style.bundleName].push(resolvedPath); } else { - entryPoints[style.bundleName] = [resolvedPath] + entryPoints[style.bundleName] = [resolvedPath]; } // Add lazy styles to the list. @@ -189,18 +122,20 @@ export function getStylesConfig(wco: WebpackConfigOptions) { let dartSass: {} | undefined; try { + // tslint:disable-next-line:no-implicit-dependencies dartSass = require('sass'); } catch { } let fiber: {} | undefined; if (dartSass) { try { + // tslint:disable-next-line:no-implicit-dependencies fiber = require('fibers'); } catch { } } // set base rules to derive final rules from - const baseRules: webpack.Rule[] = [ + const baseRules: webpack.RuleSetRule[] = [ { test: /\.css$/, use: [] }, { test: /\.scss$|\.sass$/, @@ -212,9 +147,9 @@ export function getStylesConfig(wco: WebpackConfigOptions) { sourceMap: cssSourceMap, // bootstrap-sass requires a minimum precision of 8 precision: 8, - includePaths - } - }] + includePaths, + }, + }], }, { test: /\.less$/, @@ -224,8 +159,8 @@ export function getStylesConfig(wco: WebpackConfigOptions) { sourceMap: cssSourceMap, javascriptEnabled: true, ...lessPathOptions, - } - }] + }, + }], }, { test: /\.styl$/, @@ -233,14 +168,14 @@ export function getStylesConfig(wco: WebpackConfigOptions) { loader: 'stylus-loader', options: { sourceMap: cssSourceMap, - paths: includePaths - } - }] - } + paths: includePaths, + }, + }], + }, ]; // load component css as raw strings - const rules: webpack.Rule[] = baseRules.map(({ test, use }) => ({ + const rules: webpack.RuleSetRule[] = baseRules.map(({ test, use }) => ({ exclude: globalStylePaths, test, use: [ @@ -250,50 +185,33 @@ export function getStylesConfig(wco: WebpackConfigOptions) { options: { ident: 'embedded', plugins: postcssPluginCreator, - sourceMap: cssSourceMap ? 'inline' : false - } + sourceMap: cssSourceMap ? 'inline' : false, + }, }, - ...(use as webpack.Loader[]) - ] + ...(use as webpack.Loader[]), + ], })); // load global css as css files if (globalStylePaths.length > 0) { rules.push(...baseRules.map(({ test, use }) => { - const extractTextPlugin = { + return { + include: globalStylePaths, + test, use: [ - // style-loader still has issues with relative url()'s with sourcemaps enabled; - // even with the convertToAbsoluteUrls options as it uses 'document.location' - // which breaks when used with routing. - // Once style-loader 1.0 is released the following conditional won't be necessary - // due to this 1.0 PR: https://github.com/webpack-contrib/style-loader/pull/219 - { loader: buildOptions.extractCss ? RawCssLoader : 'raw-loader' }, + buildOptions.extractCss ? MiniCssExtractPlugin.loader : 'style-loader', + RawCssLoader, { loader: 'postcss-loader', options: { ident: buildOptions.extractCss ? 'extracted' : 'embedded', plugins: postcssPluginCreator, - sourceMap: cssSourceMap && !buildOptions.extractCss ? 'inline' : cssSourceMap - } + sourceMap: cssSourceMap && !buildOptions.extractCss ? 'inline' : cssSourceMap, + }, }, - ...(use as webpack.Loader[]) + ...(use as webpack.Loader[]), ], - // publicPath needed as a workaround https://github.com/angular/angular-cli/issues/4035 - publicPath: '' - }; - const ret: any = { - include: globalStylePaths, - test, - use: [ - buildOptions.extractCss ? MiniCssExtractPlugin.loader : 'style-loader', - ...extractTextPlugin.use, - ] }; - // Save the original options as arguments for eject. - // if (buildOptions.extractCss) { - // ret[pluginArgs] = extractTextPlugin; - // } - return ret; })); } @@ -309,6 +227,6 @@ export function getStylesConfig(wco: WebpackConfigOptions) { return { entry: entryPoints, module: { rules }, - plugins: extraPlugins + plugins: extraPlugins, }; } diff --git a/packages/angular_devkit/build_angular/src/angular-cli-files/plugins/postcss-cli-resources.ts b/packages/angular_devkit/build_angular/src/angular-cli-files/plugins/postcss-cli-resources.ts index 7bf139cd662f..7192ca92efea 100644 --- a/packages/angular_devkit/build_angular/src/angular-cli-files/plugins/postcss-cli-resources.ts +++ b/packages/angular_devkit/build_angular/src/angular-cli-files/plugins/postcss-cli-resources.ts @@ -1,7 +1,3 @@ -// tslint:disable -// TODO: cleanup this file, it's copied as is from Angular CLI. - - /** * @license * Copyright Google Inc. All Rights Reserved. @@ -28,6 +24,7 @@ function wrapUrl(url: string): string { } export interface PostcssCliResourcesOptions { + baseHref?: string; deployUrl?: string; filename: string; loader: webpack.loader.LoaderContext; @@ -36,21 +33,28 @@ export interface PostcssCliResourcesOptions { async function resolve( file: string, base: string, - resolver: (file: string, base: string) => Promise + resolver: (file: string, base: string) => Promise, ): Promise { try { return await resolver('./' + file, base); - } catch (err) { + } catch { return resolver(file, base); } } export default postcss.plugin('postcss-cli-resources', (options: PostcssCliResourcesOptions) => { - const { deployUrl, filename, loader } = options; + const { + deployUrl = '', + baseHref = '', + filename, + loader, + } = options; + + const dedupeSlashes = (url: string) => url.replace(/\/\/+/g, '/'); const process = async (inputUrl: string, resourceCache: Map) => { // If root-relative or absolute, leave as is - if (inputUrl.match(/^(?:\w+:\/\/|data:|chrome:|#|\/)/)) { + if (inputUrl.match(/^(?:\w+:\/\/|data:|chrome:|#)/)) { return inputUrl; } // If starts with a caret, remove and return remainder @@ -64,11 +68,34 @@ export default postcss.plugin('postcss-cli-resources', (options: PostcssCliResou return cachedUrl; } + if (inputUrl.startsWith('~')) { + inputUrl = inputUrl.substr(1); + } + + if (inputUrl.startsWith('/') && !inputUrl.startsWith('//')) { + let outputUrl = ''; + if (deployUrl.match(/:\/\//) || deployUrl.startsWith('/')) { + // If deployUrl is absolute or root relative, ignore baseHref & use deployUrl as is. + outputUrl = `${deployUrl.replace(/\/$/, '')}${inputUrl}`; + } else if (baseHref.match(/:\/\//)) { + // If baseHref contains a scheme, include it as is. + outputUrl = baseHref.replace(/\/$/, '') + dedupeSlashes(`/${deployUrl}/${inputUrl}`); + } else { + // Join together base-href, deploy-url and the original URL. + outputUrl = dedupeSlashes(`/${baseHref}/${deployUrl}/${inputUrl}`); + } + + resourceCache.set(inputUrl, outputUrl); + + return outputUrl; + } + const { pathname, hash, search } = url.parse(inputUrl.replace(/\\/g, '/')); const resolver = (file: string, base: string) => new Promise((resolve, reject) => { loader.resolve(base, file, (err, result) => { if (err) { reject(err); + return; } resolve(result); @@ -81,6 +108,7 @@ export default postcss.plugin('postcss-cli-resources', (options: PostcssCliResou loader.fs.readFile(result, (err: Error, content: Buffer) => { if (err) { reject(err); + return; } @@ -98,12 +126,11 @@ export default postcss.plugin('postcss-cli-resources', (options: PostcssCliResou outputUrl = url.format({ pathname: outputUrl, hash, search }); } - if (deployUrl) { + if (deployUrl && loader.loaders[loader.loaderIndex].options.ident !== 'extracted') { outputUrl = url.resolve(deployUrl, outputUrl); } resourceCache.set(inputUrl, outputUrl); - resolve(outputUrl); }); }); diff --git a/packages/angular_devkit/build_angular/src/angular-cli-files/plugins/webpack.ts b/packages/angular_devkit/build_angular/src/angular-cli-files/plugins/webpack.ts index 0480230ad484..026e834641fd 100644 --- a/packages/angular_devkit/build_angular/src/angular-cli-files/plugins/webpack.ts +++ b/packages/angular_devkit/build_angular/src/angular-cli-files/plugins/webpack.ts @@ -5,8 +5,6 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ -// tslint:disable -// TODO: cleanup this file, it's copied as is from Angular CLI. // Exports the webpack plugins we use internally. export { BaseHrefWebpackPlugin } from '../lib/base-href-webpack/base-href-webpack-plugin'; @@ -14,6 +12,7 @@ export { CleanCssWebpackPlugin, CleanCssWebpackPluginOptions } from './cleancss- export { BundleBudgetPlugin, BundleBudgetPluginOptions } from './bundle-budget'; export { ScriptsWebpackPlugin, ScriptsWebpackPluginOptions } from './scripts-webpack-plugin'; export { SuppressExtractedTextChunksWebpackPlugin } from './suppress-entry-chunks-webpack-plugin'; +export { RemoveHashPlugin, RemoveHashPluginOptions } from './remove-hash-plugin'; export { default as PostcssCliResources, PostcssCliResourcesOptions, diff --git a/yarn.lock b/yarn.lock index 3309a02b20ac..20e684a651b0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5717,16 +5717,6 @@ postcss-loader@^2.1.5: postcss-load-config "^2.0.0" schema-utils "^0.4.0" -postcss-url@^7.3.2: - version "7.3.2" - resolved "https://registry.yarnpkg.com/postcss-url/-/postcss-url-7.3.2.tgz#5fea273807fb84b38c461c3c9a9e8abd235f7120" - dependencies: - mime "^1.4.1" - minimatch "^3.0.4" - mkdirp "^0.5.0" - postcss "^6.0.1" - xxhashjs "^0.2.1" - postcss-url@^8.0.0: version "8.0.0" resolved "https://registry.yarnpkg.com/postcss-url/-/postcss-url-8.0.0.tgz#7b10059bd12929cdbb1971c60f61a0e5af86b4ca" From 54f7ea2b1dcffa9cbe1e96d0c99acd8704d232ea Mon Sep 17 00:00:00 2001 From: Alan Agius Date: Sun, 2 Sep 2018 11:55:58 +0200 Subject: [PATCH 0061/1703] fix(@angular-devkit/schematics): throw `InvalidCollectionJsonException` when collection file is invalid Closes #11818 --- .../tools/file-system-engine-host-base.ts | 16 ++++++++++++++-- .../schematics/tools/node-module-engine-host.ts | 15 +++++++++++++-- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/packages/angular_devkit/schematics/tools/file-system-engine-host-base.ts b/packages/angular_devkit/schematics/tools/file-system-engine-host-base.ts index 7c3ed9c808d4..0ed3cc7dac98 100644 --- a/packages/angular_devkit/schematics/tools/file-system-engine-host-base.ts +++ b/packages/angular_devkit/schematics/tools/file-system-engine-host-base.ts @@ -7,7 +7,9 @@ */ import { BaseException, + InvalidJsonCharacterException, JsonObject, + UnexpectedEndOfInputException, isObservable, normalize, virtualFs, @@ -52,8 +54,18 @@ export class CollectionCannotBeResolvedException extends BaseException { } } export class InvalidCollectionJsonException extends BaseException { - constructor(_name: string, path: string) { - super(`Collection JSON at path ${JSON.stringify(path)} is invalid.`); + constructor( + _name: string, + path: string, + jsonException?: UnexpectedEndOfInputException | InvalidJsonCharacterException, + ) { + let msg = `Collection JSON at path ${JSON.stringify(path)} is invalid.`; + + if (jsonException) { + msg = `${msg} ${jsonException.message}`; + } + + super(msg); } } export class SchematicMissingFactoryException extends BaseException { diff --git a/packages/angular_devkit/schematics/tools/node-module-engine-host.ts b/packages/angular_devkit/schematics/tools/node-module-engine-host.ts index 93aa34dbafe0..2c0e681626bc 100644 --- a/packages/angular_devkit/schematics/tools/node-module-engine-host.ts +++ b/packages/angular_devkit/schematics/tools/node-module-engine-host.ts @@ -5,7 +5,11 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ -import { BaseException } from '@angular-devkit/core'; +import { + BaseException, + InvalidJsonCharacterException, + UnexpectedEndOfInputException, +} from '@angular-devkit/core'; import * as core from '@angular-devkit/core/node'; import { dirname, join, resolve as resolvePath } from 'path'; import { RuleFactory } from '../src'; @@ -18,6 +22,7 @@ import { CollectionCannotBeResolvedException, CollectionMissingSchematicsMapException, FileSystemEngineHostBase, + InvalidCollectionJsonException, SchematicMissingFieldsException, } from './file-system-engine-host-base'; import { readJsonFile } from './file-system-utility'; @@ -77,7 +82,7 @@ export class NodeModulesEngineHost extends FileSystemEngineHostBase { if (name.replace(/\\/, '/').split('/').length > (name[0] == '@' ? 2 : 1)) { try { collectionPath = this._resolvePath(name, process.cwd()); - } catch (_) { + } catch { } } @@ -102,7 +107,13 @@ export class NodeModulesEngineHost extends FileSystemEngineHostBase { return collectionPath; } } catch (e) { + if ( + e instanceof InvalidJsonCharacterException || e instanceof UnexpectedEndOfInputException + ) { + throw new InvalidCollectionJsonException(name, collectionPath, e); + } } + throw new CollectionCannotBeResolvedException(name); } From 65ede1fd6eb1dbbe0160336828485cc328e1c987 Mon Sep 17 00:00:00 2001 From: Alan Agius Date: Fri, 31 Aug 2018 09:06:27 +0200 Subject: [PATCH 0062/1703] docs: fix english --- packages/angular/cli/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/angular/cli/README.md b/packages/angular/cli/README.md index b462efe7e56b..1c076e8a40a2 100644 --- a/packages/angular/cli/README.md +++ b/packages/angular/cli/README.md @@ -103,7 +103,7 @@ Scaffold | Usage -angular-cli will add reference to `components`, `directives` and `pipes` automatically in the `app.module.ts`. If you need to add this references to another custom module, follow this steps: +angular-cli will add reference to `components`, `directives` and `pipes` automatically in the `app.module.ts`. If you need to add this references to another custom module, follow these steps: 1. `ng g module new-module` to create a new module 2. call `ng g component new-module/new-component` From e16c8bbe87487b45ab831b348b3137e607233462 Mon Sep 17 00:00:00 2001 From: Alan Agius Date: Fri, 31 Aug 2018 13:56:11 +0200 Subject: [PATCH 0063/1703] fix(@angular-devkit/build-angular): set the proper type to `fileReplacements` Closes #11294 --- .../build_angular/src/browser/schema.d.ts | 2 +- .../build_angular/src/extract-i18n/index.ts | 3 ++- .../build_angular/src/server/schema.d.ts | 11 +++++++---- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/packages/angular_devkit/build_angular/src/browser/schema.d.ts b/packages/angular_devkit/build_angular/src/browser/schema.d.ts index ca0fc891efe9..11a63aaa698d 100644 --- a/packages/angular_devkit/build_angular/src/browser/schema.d.ts +++ b/packages/angular_devkit/build_angular/src/browser/schema.d.ts @@ -49,7 +49,7 @@ export interface BrowserBuilderSchema { /** * Replace files with other files in the build. */ - fileReplacements: FileReplacements[]; + fileReplacements: FileReplacement[]; /** * Path where output will be placed. diff --git a/packages/angular_devkit/build_angular/src/extract-i18n/index.ts b/packages/angular_devkit/build_angular/src/extract-i18n/index.ts index ead3923d38f8..72752838f18f 100644 --- a/packages/angular_devkit/build_angular/src/extract-i18n/index.ts +++ b/packages/angular_devkit/build_angular/src/extract-i18n/index.ts @@ -85,7 +85,8 @@ export class ExtractI18nBuilder implements Builder { // Extracting i18n uses the browser target webpack config with some specific options. const webpackConfig = this.buildWebpackConfig(root, projectRoot, { - ...browserOptions, + // todo: remove this casting when 'CurrentFileReplacement' is changed to 'FileReplacement' + ...(browserOptions as NormalizedBrowserBuilderSchema), optimization: false, i18nLocale: options.i18nLocale, i18nFormat: options.i18nFormat, diff --git a/packages/angular_devkit/build_angular/src/server/schema.d.ts b/packages/angular_devkit/build_angular/src/server/schema.d.ts index 16beda1fd8fb..24b575422afd 100644 --- a/packages/angular_devkit/build_angular/src/server/schema.d.ts +++ b/packages/angular_devkit/build_angular/src/server/schema.d.ts @@ -5,6 +5,9 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ + +import { FileReplacement } from '../browser/schema'; + export interface BuildWebpackServerSchema { /** * The name of the TypeScript configuration file. @@ -80,7 +83,7 @@ export interface BuildWebpackServerSchema { /** * Replace files with other files in the build. */ - fileReplacements: FileReplacements[]; + fileReplacements: FileReplacement[]; /** * Define the output filename cache-busting hashing mode. */ @@ -125,9 +128,9 @@ export interface BuildWebpackServerSchema { * Run build when files change. */ watch?: boolean; - /** - * Enable and define the file watching poll time period in milliseconds. - */ + /** + * Enable and define the file watching poll time period in milliseconds. + */ poll?: number; } From 8caeb478248ffef219a9ace2386633f2d2e64281 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20H=C3=A4ni?= Date: Thu, 23 Aug 2018 01:21:31 +0200 Subject: [PATCH 0064/1703] fix(@angular-devkit/schematics): fix `generate` mangling files containing wide characters Executing a command like `ng generate component my-component` can sometimes lead to mangled Angular module files when inserting the component into `declaration` and adding the import. This happens if the file contains characters that are wider than one byte e.g. a copyright sign or an umlaut. Today it is expected to be able to use two byte long characters in code. The `UpdateBuffer` class operates using Buffer objects which use byte arrays internally. Using text node positions provided by the TypeScript library, these will not match up. This change looks up the textual position inside the Buffer and uses the correct index. Closes #7851, #7950 --- .../schematics/src/tree/recorder.ts | 6 +++--- .../schematics/src/utility/update-buffer.ts | 20 +++++++++++++++---- .../src/utility/update-buffer_spec.ts | 10 ++++++++++ 3 files changed, 29 insertions(+), 7 deletions(-) diff --git a/packages/angular_devkit/schematics/src/tree/recorder.ts b/packages/angular_devkit/schematics/src/tree/recorder.ts index be8e485e6f8b..8b18b8182e55 100644 --- a/packages/angular_devkit/schematics/src/tree/recorder.ts +++ b/packages/angular_devkit/schematics/src/tree/recorder.ts @@ -30,9 +30,9 @@ export class UpdateRecorderBase implements UpdateRecorder { if (c0 == 0xEF && c1 == 0xBB && c2 == 0xBF) { return new UpdateRecorderBom(entry); } else if (c0 === 0xFF && c1 == 0xFE) { - return new UpdateRecorderBom(entry, 2); + return new UpdateRecorderBom(entry); } else if (c0 === 0xFE && c1 == 0xFF) { - return new UpdateRecorderBom(entry, 2); + return new UpdateRecorderBom(entry); } return new UpdateRecorderBase(entry); @@ -70,7 +70,7 @@ export class UpdateRecorderBase implements UpdateRecorder { export class UpdateRecorderBom extends UpdateRecorderBase { - constructor(entry: FileEntry, private _delta = 3) { + constructor(entry: FileEntry, private _delta = 1) { super(entry); } diff --git a/packages/angular_devkit/schematics/src/utility/update-buffer.ts b/packages/angular_devkit/schematics/src/utility/update-buffer.ts index 44a9f620a914..165305ddabae 100644 --- a/packages/angular_devkit/schematics/src/utility/update-buffer.ts +++ b/packages/angular_devkit/schematics/src/utility/update-buffer.ts @@ -201,19 +201,31 @@ export class UpdateBuffer { } protected _slice(start: number): [Chunk, Chunk] { - this._assertIndex(start); + // If start is longer than the content, use start, otherwise determine exact position in string. + const index = start >= this._originalContent.length ? start : this._getTextPosition(start); + + this._assertIndex(index); // Find the chunk by going through the list. - const h = this._linkedList.find(chunk => start <= chunk.end); + const h = this._linkedList.find(chunk => index <= chunk.end); if (!h) { throw Error('Chunk cannot be found.'); } - if (start == h.end && h.next !== null) { + if (index == h.end && h.next !== null) { return [h, h.next]; } - return [h, h.slice(start)]; + return [h, h.slice(index)]; + } + + /** + * Gets the position in the content based on the position in the string. + * Some characters might be wider than one byte, thus we have to determine the position using + * string functions. + */ + protected _getTextPosition(index: number): number { + return Buffer.from(this._originalContent.toString().substring(0, index)).length; } get length(): number { diff --git a/packages/angular_devkit/schematics/src/utility/update-buffer_spec.ts b/packages/angular_devkit/schematics/src/utility/update-buffer_spec.ts index f6e7cc2aa271..64e650feeb29 100644 --- a/packages/angular_devkit/schematics/src/utility/update-buffer_spec.ts +++ b/packages/angular_devkit/schematics/src/utility/update-buffer_spec.ts @@ -58,6 +58,16 @@ describe('UpdateBuffer', () => { mb.insertLeft(6, Buffer.from('Awesome ')); expect(mb.toString()).toBe('Hello Great Awesome Beautiful World'); }); + + it('works with special characters', () => { + const mb = new UpdateBuffer(Buffer.from('Ülaut')); + + mb.insertLeft(1, Buffer.from('m')); + expect(mb.toString()).toBe('Ümlaut'); + + mb.insertLeft(0, Buffer.from('Hello ')); + expect(mb.toString()).toBe('Hello Ümlaut'); + }); }); describe('delete', () => { From 6afa6a31bbf1e6516b4ac2120ec431178c5a8e43 Mon Sep 17 00:00:00 2001 From: Jan Kuri Date: Mon, 3 Sep 2018 20:30:10 +0200 Subject: [PATCH 0065/1703] docs: update proxy.md with correct path --- docs/documentation/stories/proxy.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/documentation/stories/proxy.md b/docs/documentation/stories/proxy.md index c0cfd0e1cabc..17b301f17c30 100644 --- a/docs/documentation/stories/proxy.md +++ b/docs/documentation/stories/proxy.md @@ -26,7 +26,7 @@ We can then add the `proxyConfig` option to the serve target: "builder": "@angular-devkit/build-angular:dev-server", "options": { "browserTarget": "your-application-name:build", - "proxyConfig": "src/proxy.conf.json" + "proxyConfig": "proxy.conf.json" }, ``` @@ -119,7 +119,7 @@ Make sure to point to the right file (`.js` instead of `.json`): "builder": "@angular-devkit/build-angular:dev-server", "options": { "browserTarget": "your-application-name:build", - "proxyConfig": "src/proxy.conf.js" + "proxyConfig": "proxy.conf.js" }, ``` @@ -183,4 +183,4 @@ function setupForCorporateProxy(proxyConfig) { module.exports = setupForCorporateProxy(proxyConfig); ``` -This way if you have a `http_proxy` or `HTTP_PROXY` environment variable defined, an agent will automatically be added to pass calls through your corporate proxy when running `npm start`. \ No newline at end of file +This way if you have a `http_proxy` or `HTTP_PROXY` environment variable defined, an agent will automatically be added to pass calls through your corporate proxy when running `npm start`. From 616ffe70f8f1482b1aadf76810f704383947e1e6 Mon Sep 17 00:00:00 2001 From: Alan Agius Date: Tue, 4 Sep 2018 12:49:47 +0200 Subject: [PATCH 0066/1703] fix(@schematics/angular): app shell schematic does not use production configuration Closes: #11578 --- packages/schematics/angular/app-shell/index.ts | 5 +++++ packages/schematics/angular/app-shell/index_spec.ts | 1 + 2 files changed, 6 insertions(+) diff --git a/packages/schematics/angular/app-shell/index.ts b/packages/schematics/angular/app-shell/index.ts index 2cdf54a46cd0..a08c7f5c9875 100644 --- a/packages/schematics/angular/app-shell/index.ts +++ b/packages/schematics/angular/app-shell/index.ts @@ -196,6 +196,11 @@ function addAppShellConfigToWorkspace(options: AppShellOptions): Rule { serverTarget: `${options.clientProject}:server`, route: options.route, }, + configurations: { + production: { + browserTarget: `${options.clientProject}:build:production`, + }, + }, }; if (!workspace.projects[options.clientProject]) { diff --git a/packages/schematics/angular/app-shell/index_spec.ts b/packages/schematics/angular/app-shell/index_spec.ts index d7ceb96785be..e765c25352b4 100644 --- a/packages/schematics/angular/app-shell/index_spec.ts +++ b/packages/schematics/angular/app-shell/index_spec.ts @@ -68,6 +68,7 @@ describe('App Shell Schematic', () => { expect(target.options.browserTarget).toEqual('bar:build'); expect(target.options.serverTarget).toEqual('bar:server'); expect(target.options.route).toEqual('shell'); + expect(target.configurations.production.browserTarget).toEqual('bar:build:production'); }); it('should add router module to client app module', () => { From 36f8cd77baf7baff0df482a6b7bba17cd28e247e Mon Sep 17 00:00:00 2001 From: Alan Agius Date: Thu, 6 Sep 2018 12:51:01 +0200 Subject: [PATCH 0067/1703] fix(@schematics/angular): fix path for `ngswConfigPath` during migration `/` will cause an error during lookup `Error: Expected to find an `ngsw-config.json` configuration file --- packages/schematics/angular/migrations/update-6/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/schematics/angular/migrations/update-6/index.ts b/packages/schematics/angular/migrations/update-6/index.ts index 3509bb8849f4..be938fc4639b 100644 --- a/packages/schematics/angular/migrations/update-6/index.ts +++ b/packages/schematics/angular/migrations/update-6/index.ts @@ -308,7 +308,7 @@ function extractProjectsConfig( extractLicenses: true, vendorChunk: false, buildOptimizer: true, - ...(serviceWorker ? {serviceWorker: true, ngswConfigPath: '/src/ngsw-config.json'} : {}), + ...(serviceWorker ? {serviceWorker: true, ngswConfigPath: 'src/ngsw-config.json'} : {}), ...(app.budgets ? { budgets: app.budgets as JsonArray} : {}), }; From 0dc36f2848163c570921f84033c6e5450ae1cfb6 Mon Sep 17 00:00:00 2001 From: Filipe Silva Date: Wed, 5 Sep 2018 13:32:07 +0100 Subject: [PATCH 0068/1703] ci: update CI to node 10 --- .appveyor.yml | 9 ++++----- .circleci/config.yml | 23 ++++++++++++++++++++--- WORKSPACE | 8 +++++--- package.json | 6 +++--- 4 files changed, 32 insertions(+), 14 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index 7b237d6d1cf7..edc5ecbae89d 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -1,15 +1,14 @@ environment: - # Note: if updating to Node 10, use at least 10.5.0 to include a fix for - # https://github.com/nodejs/node/issues/20297 - nodejs_version: "8.9.2" # Same version as used in CircleCI. + nodejs_version: "10.9.0" # Same version as used in CircleCI. matrix: fast_finish: true install: - ps: Install-Product node $env:nodejs_version - - yarn install --frozen-lockfile - - npm run webdriver-update-appveyor + # --network-timeout is a workaround for https://github.com/yarnpkg/yarn/issues/6221 + - yarn --frozen-lockfile --network-timeout=500000 + - yarn webdriver-update-appveyor test_script: - node --version diff --git a/.circleci/config.yml b/.circleci/config.yml index bab56e835e84..5ffb9a5590e0 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -12,8 +12,9 @@ ## IMPORTANT # If you change the `docker_image` version, also change the `cache_key` suffix and the version of # `com_github_bazelbuild_buildtools` in the `/WORKSPACE` file. -var_1: &docker_image angular/ngcontainer:0.3.3 -var_2: &cache_key angular_devkit-{{ checksum "yarn.lock" }}-0.3.3-2 +var_1: &docker_image angular/ngcontainer:0.5.0 +var_2: &cache_key angular_devkit-{{ checksum "yarn.lock" }}-0.5.0 +var_3: &node_8_docker_image angular/ngcontainer:0.3.3 # Settings common to each job anchor_1: &defaults @@ -84,8 +85,21 @@ jobs: parallelism: 4 steps: - attach_workspace: *attach_options - - run: npm install --global npm@6 - run: xvfb-run -a node ./tests/legacy-cli/run_e2e --nb-shards=${CIRCLE_NODE_TOTAL} --shard=${CIRCLE_NODE_INDEX} + + e2e-node-8: + <<: *defaults + # Overwrite docker image to node 8. + docker: + - image: *node_8_docker_image + environment: + BASH_ENV: ~/.profile + resource_class: xlarge + parallelism: 4 + steps: + - attach_workspace: *attach_options + - run: npm install --global npm@6 + - run: xvfb-run -a node ./tests/legacy-cli/run_e2e --glob=tests/basic/* build: <<: *defaults @@ -153,6 +167,9 @@ workflows: - e2e-cli: requires: - build + - e2e-node-8: + requires: + - build - snapshot_publish: requires: - test diff --git a/WORKSPACE b/WORKSPACE index 3647c4e935d3..01a96e2ea061 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -29,9 +29,9 @@ go_register_toolchains() # We need a minimum of this version to include https://github.com/bazelbuild/rules_nodejs/pull/281. http_archive( name = "build_bazel_rules_nodejs", - url = "https://github.com/bazelbuild/rules_nodejs/archive/c75e3dd0571b0937e3ce0c4f0e6b6b50d90468f0.zip", - strip_prefix = "rules_nodejs-c75e3dd0571b0937e3ce0c4f0e6b6b50d90468f0", - sha256 = "b78506ddaed7c682027f873d2bd50086a28570b3187da9fa16fe1672eed3015e", + url = "https://github.com/bazelbuild/rules_nodejs/archive/0.12.4.zip", + strip_prefix = "rules_nodejs-0.12.4", + sha256 = "c482700e032b4df60425cb9a6f8f28152fb1c4c947a9d61e6132fc59ce332b16", ) # Load the TypeScript rules, its dependencies, and setup the workspace. @@ -58,6 +58,8 @@ check_bazel_version("0.15.0") node_repositories( package_json = ["//:package.json"], preserve_symlinks = True, + node_version = "10.3.0", + yarn_version = "1.6.0", ) local_repository( diff --git a/package.json b/package.json index 7f1bcd60ea16..9f6c4df624c2 100644 --- a/package.json +++ b/package.json @@ -35,15 +35,15 @@ "validate-commits": "./bin/devkit-admin validate-commits", "prepush": "node ./bin/devkit-admin hooks/pre-push", "webdriver-update-appveyor": "webdriver-manager update --standalone false --gecko false --versions.chrome 2.37", - "webdriver-update-circleci": "webdriver-manager update --standalone false --gecko false --versions.chrome 2.33" + "webdriver-update-circleci": "webdriver-manager update --standalone false --gecko false --versions.chrome $CHROMEDRIVER_VERSION_ARG " }, "repository": { "type": "git", "url": "https://github.com/angular/angular-cli.git" }, "engines": { - "node": ">= 8.9.0", - "npm": ">= 5.5.1" + "node": ">=10.3.0 <11.0.0", + "yarn": ">=1.6.0 <2.0.0" }, "author": "Angular Authors", "license": "MIT", From 6595490b8b32122d39edc3b4a6828f6604c64bfe Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Mon, 20 Aug 2018 11:55:19 -0400 Subject: [PATCH 0069/1703] feat(@angular/pwa): support customized workspace configurations with add --- packages/angular/pwa/BUILD | 5 +- packages/angular/pwa/package.json | 3 +- packages/angular/pwa/pwa/index.ts | 244 +++++++++++++++---------- packages/angular/pwa/pwa/index_spec.ts | 117 +++++++----- packages/angular/pwa/utility/config.ts | 30 --- yarn.lock | 17 ++ 6 files changed, 240 insertions(+), 176 deletions(-) delete mode 100644 packages/angular/pwa/utility/config.ts diff --git a/packages/angular/pwa/BUILD b/packages/angular/pwa/BUILD index 80cc6f59bedf..b72ac6d5d4db 100644 --- a/packages/angular/pwa/BUILD +++ b/packages/angular/pwa/BUILD @@ -24,11 +24,10 @@ ts_library( "**/*_spec_large.ts", ], ), - # Borrow the compile-time deps of the typescript compiler - # Just to avoid an extra npm install action. - node_modules = "@build_bazel_rules_typescript_tsc_wrapped_deps//:node_modules", deps = [ "//packages/angular_devkit/core", "//packages/angular_devkit/schematics", + "@rxjs", + # @typings: node ], ) diff --git a/packages/angular/pwa/package.json b/packages/angular/pwa/package.json index c419ac9b7b2a..6ac874ec90a8 100644 --- a/packages/angular/pwa/package.json +++ b/packages/angular/pwa/package.json @@ -12,6 +12,7 @@ "@angular-devkit/core": "0.0.0", "@angular-devkit/schematics": "0.0.0", "@schematics/angular": "0.0.0", - "typescript": "~2.6.2" + "parse5-html-rewriting-stream": "^5.1.0", + "rxjs": "^6.0.0" } } \ No newline at end of file diff --git a/packages/angular/pwa/pwa/index.ts b/packages/angular/pwa/pwa/index.ts index 53e9f87f7e56..9eb869c7c5cf 100644 --- a/packages/angular/pwa/pwa/index.ts +++ b/packages/angular/pwa/pwa/index.ts @@ -5,7 +5,14 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ -import { Path, join, normalize } from '@angular-devkit/core'; +import { + JsonParseMode, + experimental, + getSystemPath, + join, + normalize, + parseJson, +} from '@angular-devkit/core'; import { Rule, SchematicContext, @@ -19,153 +26,190 @@ import { template, url, } from '@angular-devkit/schematics'; -import { getWorkspace, getWorkspacePath } from '../utility/config'; +import { Observable } from 'rxjs'; +import { Readable, Writable } from 'stream'; import { Schema as PwaOptions } from './schema'; +const RewritingStream = require('parse5-html-rewriting-stream'); -function addServiceWorker(options: PwaOptions): Rule { - return (host: Tree, context: SchematicContext) => { - context.logger.debug('Adding service worker...'); - - const swOptions = { - ...options, - }; - delete swOptions.title; - - return externalSchematic('@schematics/angular', 'service-worker', swOptions); - }; -} -function getIndent(text: string): string { - let indent = ''; +function getWorkspace( + host: Tree, +): { path: string, workspace: experimental.workspace.WorkspaceSchema } { + const possibleFiles = [ '/angular.json', '/.angular.json' ]; + const path = possibleFiles.filter(path => host.exists(path))[0]; - for (const char of text) { - if (char === ' ' || char === '\t') { - indent += char; - } else { - break; - } + const configBuffer = host.read(path); + if (configBuffer === null) { + throw new SchematicsException(`Could not find (${path})`); } - - return indent; + const content = configBuffer.toString(); + + return { + path, + workspace: parseJson( + content, + JsonParseMode.Loose, + ) as {} as experimental.workspace.WorkspaceSchema, + }; } -function updateIndexFile(options: PwaOptions): Rule { - return (host: Tree, context: SchematicContext) => { - const workspace = getWorkspace(host); - const project = workspace.projects[options.project as string]; - let path: string; - const projectTargets = project.targets || project.architect; - if (project && projectTargets && projectTargets.build && projectTargets.build.options.index) { - path = projectTargets.build.options.index; - } else { - throw new SchematicsException('Could not find index file for the project'); - } +function updateIndexFile(path: string): Rule { + return (host: Tree) => { const buffer = host.read(path); if (buffer === null) { throw new SchematicsException(`Could not read index file: ${path}`); } - const content = buffer.toString(); - const lines = content.split('\n'); - let closingHeadTagLineIndex = -1; - let closingBodyTagLineIndex = -1; - lines.forEach((line, index) => { - if (closingHeadTagLineIndex === -1 && /<\/head>/.test(line)) { - closingHeadTagLineIndex = index; - } else if (closingBodyTagLineIndex === -1 && /<\/body>/.test(line)) { - closingBodyTagLineIndex = index; - } - }); - const headIndent = getIndent(lines[closingHeadTagLineIndex]) + ' '; - const itemsToAddToHead = [ - '', - '', - ]; + const rewriter = new RewritingStream(); - const bodyIndent = getIndent(lines[closingBodyTagLineIndex]) + ' '; - const itemsToAddToBody = [ - '', - ]; + let needsNoScript = true; + rewriter.on('startTag', (startTag: { tagName: string }) => { + if (startTag.tagName === 'noscript') { + needsNoScript = false; + } - const updatedIndex = [ - ...lines.slice(0, closingHeadTagLineIndex), - ...itemsToAddToHead.map(line => headIndent + line), - ...lines.slice(closingHeadTagLineIndex, closingBodyTagLineIndex), - ...itemsToAddToBody.map(line => bodyIndent + line), - ...lines.slice(closingBodyTagLineIndex), - ].join('\n'); + rewriter.emitStartTag(startTag); + }); - host.overwrite(path, updatedIndex); + rewriter.on('endTag', (endTag: { tagName: string }) => { + if (endTag.tagName === 'head') { + rewriter.emitRaw(' \n'); + rewriter.emitRaw(' \n'); + } else if (endTag.tagName === 'body' && needsNoScript) { + rewriter.emitRaw( + ' \n', + ); + } - return host; + rewriter.emitEndTag(endTag); + }); + + return new Observable(obs => { + const input = new Readable({ + encoding: 'utf8', + read(): void { + this.push(buffer); + this.push(null); + }, + }); + + const chunks: Array = []; + const output = new Writable({ + write(chunk: string | Buffer, encoding: string, callback: Function): void { + chunks.push(typeof chunk === 'string' ? Buffer.from(chunk, encoding) : chunk); + callback(); + }, + final(callback: (error?: Error) => void): void { + const full = Buffer.concat(chunks); + host.overwrite(path, full.toString()); + callback(); + obs.next(host); + obs.complete(); + }, + }); + + input.pipe(rewriter).pipe(output); + }); }; } -function addManifestToAssetsConfig(options: PwaOptions) { +export default function (options: PwaOptions): Rule { return (host: Tree, context: SchematicContext) => { + if (!options.title) { + options.title = options.project; + } + const {path: workspacePath, workspace } = getWorkspace(host); - const workspacePath = getWorkspacePath(host); - const workspace = getWorkspace(host); - const project = workspace.projects[options.project as string]; + if (!options.project) { + throw new SchematicsException('Option "project" is required.'); + } + const project = workspace.projects[options.project]; if (!project) { - throw new Error(`Project is not defined in this workspace.`); + throw new SchematicsException(`Project is not defined in this workspace.`); } - const assetEntry = join(normalize(project.root), 'src', 'manifest.json'); + if (project.projectType !== 'application') { + throw new SchematicsException(`PWA requires a project type of "application".`); + } + // Find all the relevant targets for the project const projectTargets = project.targets || project.architect; - if (!projectTargets) { - throw new Error(`Targets are not defined for this project.`); + if (!projectTargets || Object.keys(projectTargets).length === 0) { + throw new SchematicsException(`Targets are not defined for this project.`); } - ['build', 'test'].forEach((target) => { - - const applyTo = projectTargets[target].options; - const assets = applyTo.assets || (applyTo.assets = []); - - assets.push(assetEntry); + const buildTargets = []; + const testTargets = []; + for (const targetName in projectTargets) { + const target = projectTargets[targetName]; + if (!target) { + continue; + } - }); + if (target.builder === '@angular-devkit/build-angular:browser') { + buildTargets.push(target); + } else if (target.builder === '@angular-devkit/build-angular:karma') { + testTargets.push(target); + } + } + // Add manifest to asset configuration + const assetEntry = join(normalize(project.root), 'src', 'manifest.json'); + for (const target of [...buildTargets, ...testTargets]) { + if (target.options) { + if (target.options.assets) { + target.options.assets.push(assetEntry); + } else { + target.options.assets = [ assetEntry ]; + } + } else { + target.options = { assets: [ assetEntry ] }; + } + } host.overwrite(workspacePath, JSON.stringify(workspace, null, 2)); - return host; - }; -} + // Find all index.html files in build targets + const indexFiles = new Set(); + for (const target of buildTargets) { + if (target.options && target.options.index) { + indexFiles.add(target.options.index); + } -export default function (options: PwaOptions): Rule { - return (host: Tree, context: SchematicContext) => { - const workspace = getWorkspace(host); - if (!options.project) { - throw new SchematicsException('Option "project" is required.'); - } - const project = workspace.projects[options.project]; - if (project.projectType !== 'application') { - throw new SchematicsException(`PWA requires a project type of "application".`); + if (!target.configurations) { + continue; + } + for (const configName in target.configurations) { + const configuration = target.configurations[configName]; + if (configuration && configuration.index) { + indexFiles.add(configuration.index); + } + } } - const sourcePath = join(project.root as Path, 'src'); + // Setup sources for the assets files to add to the project + const sourcePath = join(normalize(project.root), 'src'); const assetsPath = join(sourcePath, 'assets'); - - options.title = options.title || options.project; - const rootTemplateSource = apply(url('./files/root'), [ template({ ...options }), - move(sourcePath), + move(getSystemPath(sourcePath)), ]); const assetsTemplateSource = apply(url('./files/assets'), [ template({ ...options }), - move(assetsPath), + move(getSystemPath(assetsPath)), ]); + // Setup service worker schematic options + const swOptions = { ...options }; + delete swOptions.title; + + // Chain the rules and return return chain([ - addServiceWorker(options), + externalSchematic('@schematics/angular', 'service-worker', swOptions), mergeWith(rootTemplateSource), mergeWith(assetsTemplateSource), - updateIndexFile(options), - addManifestToAssetsConfig(options), + ...[...indexFiles].map(path => updateIndexFile(path)), ])(host, context); }; } diff --git a/packages/angular/pwa/pwa/index_spec.ts b/packages/angular/pwa/pwa/index_spec.ts index c7a7dda07933..b607a797c8a5 100644 --- a/packages/angular/pwa/pwa/index_spec.ts +++ b/packages/angular/pwa/pwa/index_spec.ts @@ -47,63 +47,96 @@ describe('PWA Schematic', () => { appTree = schematicRunner.runExternalSchematic('@schematics/angular', 'application', appOptions, appTree); }); - it('should run the service worker schematic', () => { - const tree = schematicRunner.runSchematic('ng-add', defaultOptions, appTree); - const configText = tree.readContent('/angular.json'); - const config = JSON.parse(configText); - const swFlag = config.projects.bar.targets.build.configurations.production.serviceWorker; - expect(swFlag).toEqual(true); + it('should run the service worker schematic', (done) => { + schematicRunner.runSchematicAsync('ng-add', defaultOptions, appTree).toPromise().then(tree => { + const configText = tree.readContent('/angular.json'); + const config = JSON.parse(configText); + const swFlag = config.projects.bar.targets.build.configurations.production.serviceWorker; + expect(swFlag).toEqual(true); + done(); + }, done.fail); }); - it('should create icon files', () => { + it('should create icon files', (done) => { const dimensions = [72, 96, 128, 144, 152, 192, 384, 512]; const iconPath = '/projects/bar/src/assets/icons/icon-'; - const tree = schematicRunner.runSchematic('ng-add', defaultOptions, appTree); - dimensions.forEach(d => { - const path = `${iconPath}${d}x${d}.png`; - expect(tree.exists(path)).toEqual(true); - }); + schematicRunner.runSchematicAsync('ng-add', defaultOptions, appTree).toPromise().then(tree => { + dimensions.forEach(d => { + const path = `${iconPath}${d}x${d}.png`; + expect(tree.exists(path)).toEqual(true); + }); + done(); + }, done.fail); }); - it('should create a manifest file', () => { - const tree = schematicRunner.runSchematic('ng-add', defaultOptions, appTree); - expect(tree.exists('/projects/bar/src/manifest.json')).toEqual(true); + it('should create a manifest file', (done) => { + schematicRunner.runSchematicAsync('ng-add', defaultOptions, appTree).toPromise().then(tree => { + expect(tree.exists('/projects/bar/src/manifest.json')).toEqual(true); + done(); + }, done.fail); }); - it('should set the name & short_name in the manifest file', () => { - const tree = schematicRunner.runSchematic('ng-add', defaultOptions, appTree); - const manifestText = tree.readContent('/projects/bar/src/manifest.json'); - const manifest = JSON.parse(manifestText); - expect(manifest.name).toEqual(defaultOptions.title); - expect(manifest.short_name).toEqual(defaultOptions.title); + it('should set the name & short_name in the manifest file', (done) => { + schematicRunner.runSchematicAsync('ng-add', defaultOptions, appTree).toPromise().then(tree => { + const manifestText = tree.readContent('/projects/bar/src/manifest.json'); + const manifest = JSON.parse(manifestText); + + expect(manifest.name).toEqual(defaultOptions.title); + expect(manifest.short_name).toEqual(defaultOptions.title); + done(); + }, done.fail); }); - it('should set the name & short_name in the manifest file when no title provided', () => { + it('should set the name & short_name in the manifest file when no title provided', (done) => { const options = {...defaultOptions, title: undefined}; - const tree = schematicRunner.runSchematic('ng-add', options, appTree); - const manifestText = tree.readContent('/projects/bar/src/manifest.json'); - const manifest = JSON.parse(manifestText); - expect(manifest.name).toEqual(defaultOptions.project); - expect(manifest.short_name).toEqual(defaultOptions.project); + schematicRunner.runSchematicAsync('ng-add', options, appTree).toPromise().then(tree => { + const manifestText = tree.readContent('/projects/bar/src/manifest.json'); + const manifest = JSON.parse(manifestText); + + expect(manifest.name).toEqual(defaultOptions.project); + expect(manifest.short_name).toEqual(defaultOptions.project); + done(); + }, done.fail); + }); + + it('should update the index file', (done) => { + schematicRunner.runSchematicAsync('ng-add', defaultOptions, appTree).toPromise().then(tree => { + const content = tree.readContent('projects/bar/src/index.html'); + + expect(content).toMatch(//); + expect(content).toMatch(//); + expect(content) + .toMatch(/