From 9992cf3aec05f91207e44fa36d8abce1ecaffe37 Mon Sep 17 00:00:00 2001 From: Alan Agius Date: Mon, 28 Feb 2022 11:16:12 +0100 Subject: [PATCH 1/8] refactor(@angular/cli): replace command line arguments parser MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With this change we refactor the Angular CLI and replace the underlying args parser and command builder. We choose to use Yargs as our parser and command builder of choice. The main advantages of Yargs over other command builders are; - Highly configurable. - We already use it in other packages such as the compiler-cli/dev-infra etc.. - Commands and options can be added during runtime. This is a requirement that is needed to support architect and schematics commands. - Outstanding documentation. - The possibility to parse args without parser configuration (Free form). - Commands are built lazily based on the arguments passed. BREAKING CHANGE: Several changes in the Angular CLI commands and arguments handling. - `ng help` has been removed in favour of the `—-help` option. - `ng —-version` has been removed in favour of `ng version` and `ng v`. - Deprecated camel cased arguments are no longer supported. Ex. using `—-sourceMap` instead of `—-source-map` will result in an error. - `ng update`, `—-migrate-only` option no longer accepts a string of migration name, instead use `—-migrate-only -—name `. - `—-help json` help has been removed. Closes #20976, closes #16614 and closes #16241 --- package.json | 2 + packages/angular/cli/BUILD.bazel | 164 +------ packages/angular/cli/commands.json | 20 - packages/angular/cli/commands/add.json | 54 --- .../cli/commands/{ => add}/add-impl.ts | 75 +-- packages/angular/cli/commands/add/cli.ts | 67 +++ .../{add.md => add/long-description.md} | 0 .../angular/cli/commands/analytics-impl.ts | 100 ---- .../angular/cli/commands/analytics-long.md | 8 - packages/angular/cli/commands/analytics.json | 37 -- .../angular/cli/commands/analytics/cli.ts | 85 ++++ .../commands/analytics/long-description.md | 8 + packages/angular/cli/commands/build-impl.ts | 19 - packages/angular/cli/commands/build.json | 16 - packages/angular/cli/commands/build/cli.ts | 23 + .../long-description.md} | 0 packages/angular/cli/commands/config.json | 43 -- packages/angular/cli/commands/config/cli.ts | 60 +++ .../cli/commands/{ => config}/config-impl.ts | 24 +- .../long-description.md} | 0 .../angular/cli/commands/definitions.json | 66 --- packages/angular/cli/commands/deploy-impl.ts | 37 -- packages/angular/cli/commands/deploy.json | 34 -- packages/angular/cli/commands/deploy/cli.ts | 36 ++ .../long-description.md} | 0 packages/angular/cli/commands/doc-impl.ts | 52 --- packages/angular/cli/commands/doc.json | 46 -- packages/angular/cli/commands/doc/cli.ts | 90 ++++ packages/angular/cli/commands/e2e-impl.ts | 34 -- packages/angular/cli/commands/e2e-long.md | 4 - packages/angular/cli/commands/e2e.json | 17 - packages/angular/cli/commands/e2e/cli.ts | 35 ++ .../angular/cli/commands/easter-egg-impl.ts | 31 -- packages/angular/cli/commands/easter-egg.json | 12 - .../angular/cli/commands/extract-i18n-impl.ts | 19 - .../angular/cli/commands/extract-i18n.json | 15 - .../angular/cli/commands/extract-i18n/cli.ts | 20 + .../angular/cli/commands/generate-impl.ts | 120 ----- packages/angular/cli/commands/generate.json | 31 -- packages/angular/cli/commands/generate/cli.ts | 147 ++++++ .../cli/commands/generate/generate-impl.ts | 68 +++ packages/angular/cli/commands/help-impl.ts | 27 -- packages/angular/cli/commands/help-long.md | 7 - packages/angular/cli/commands/help.json | 13 - packages/angular/cli/commands/lint-impl.ts | 57 --- packages/angular/cli/commands/lint.json | 36 -- packages/angular/cli/commands/lint/cli.ts | 31 ++ .../long-description.md} | 0 .../cli/commands/make-this-awesome/cli.ts | 41 ++ packages/angular/cli/commands/new.json | 34 -- packages/angular/cli/commands/new.md | 16 - packages/angular/cli/commands/new/cli.ts | 53 +++ .../cli/commands/{ => new}/new-impl.ts | 26 +- packages/angular/cli/commands/run-impl.ts | 21 - packages/angular/cli/commands/run.json | 36 -- packages/angular/cli/commands/run/cli.ts | 114 +++++ .../{run-long.md => run/long-description.md} | 0 packages/angular/cli/commands/serve-impl.ts | 23 - packages/angular/cli/commands/serve.json | 17 - packages/angular/cli/commands/serve/cli.ts | 21 + packages/angular/cli/commands/test-impl.ts | 20 - packages/angular/cli/commands/test.json | 17 - packages/angular/cli/commands/test/cli.ts | 22 + .../long-description.md} | 0 packages/angular/cli/commands/update.json | 78 ---- packages/angular/cli/commands/update/cli.ts | 106 +++++ .../long-description.md} | 0 .../cli/commands/{ => update}/update-impl.ts | 52 ++- packages/angular/cli/commands/version.json | 13 - .../{version-impl.ts => version/cli.ts} | 101 ++-- .../angular/cli/lib/cli/command-runner.ts | 150 ++++++ packages/angular/cli/lib/cli/index.ts | 19 +- packages/angular/cli/lib/init.ts | 2 +- .../angular/cli/models/analytics-collector.ts | 2 +- packages/angular/cli/models/analytics.ts | 38 +- .../angular/cli/models/architect-command.ts | 434 ------------------ packages/angular/cli/models/command-runner.ts | 273 ----------- packages/angular/cli/models/command.ts | 166 +------ packages/angular/cli/models/interface.ts | 218 +-------- packages/angular/cli/models/parser.ts | 405 ---------------- packages/angular/cli/models/parser_spec.ts | 226 --------- .../angular/cli/models/schematic-command.ts | 183 +------- packages/angular/cli/package.json | 3 +- .../architect-command-module.ts | 296 ++++++++++++ .../command-builder/command-module.ts | 217 +++++++++ .../utilities/command-builder/json-schema.ts | 213 +++++++++ .../schematics-command-module.ts | 167 +++++++ packages/angular/cli/utilities/json-schema.ts | 301 ------------ .../angular/cli/utilities/json-schema_spec.ts | 75 --- .../cli/{models => utilities}/version.ts | 9 +- .../src/builders/browser/schema.json | 2 +- .../angular/app-shell/index_spec.ts | 24 +- packages/schematics/angular/collection.json | 3 - tests/legacy-cli/e2e/tests/basic/e2e.ts | 117 +++-- tests/legacy-cli/e2e/tests/basic/test.ts | 2 +- .../build/build-app-shell-with-schematic.ts | 2 +- .../e2e/tests/build/multiple-configs.ts | 2 +- .../e2e/tests/build/platform-server.ts | 2 +- .../tests/commands/additional-properties.ts | 15 +- .../commands/help/help-option-command.ts | 7 - .../e2e/tests/commands/help/help-option.ts | 9 - .../e2e/tests/commands/help/help.ts | 9 - .../e2e/tests/commands/unknown-option.ts | 20 +- .../e2e/tests/generate/help-output.ts | 111 ++--- .../library/library-consumption-ivy-full.ts | 2 +- .../library-consumption-ivy-partial.ts | 2 +- .../library/library-consumption-ve.ts | 2 +- .../e2e/tests/i18n/ivy-localize-app-shell.ts | 2 +- .../e2e/tests/i18n/ivy-localize-basehref.ts | 19 +- .../e2e/tests/i18n/ivy-localize-es2015.ts | 4 +- .../e2e/tests/i18n/ivy-localize-es5.ts | 4 +- .../i18n/ivy-localize-locale-data-augment.ts | 18 +- .../e2e/tests/i18n/ivy-localize-server.ts | 2 +- .../tests/i18n/ivy-localize-serviceworker.ts | 2 +- tests/legacy-cli/e2e/tests/misc/browsers.ts | 4 +- tests/legacy-cli/e2e/tests/misc/npm-7.ts | 2 +- tests/legacy-cli/e2e/tests/misc/version.ts | 8 - yarn.lock | 30 +- 118 files changed, 2531 insertions(+), 3993 deletions(-) delete mode 100644 packages/angular/cli/commands.json delete mode 100644 packages/angular/cli/commands/add.json rename packages/angular/cli/commands/{ => add}/add-impl.ts (89%) create mode 100644 packages/angular/cli/commands/add/cli.ts rename packages/angular/cli/commands/{add.md => add/long-description.md} (100%) delete mode 100644 packages/angular/cli/commands/analytics-impl.ts delete mode 100644 packages/angular/cli/commands/analytics-long.md delete mode 100644 packages/angular/cli/commands/analytics.json create mode 100644 packages/angular/cli/commands/analytics/cli.ts create mode 100644 packages/angular/cli/commands/analytics/long-description.md delete mode 100644 packages/angular/cli/commands/build-impl.ts delete mode 100644 packages/angular/cli/commands/build.json create mode 100644 packages/angular/cli/commands/build/cli.ts rename packages/angular/cli/commands/{build-long.md => build/long-description.md} (100%) delete mode 100644 packages/angular/cli/commands/config.json create mode 100644 packages/angular/cli/commands/config/cli.ts rename packages/angular/cli/commands/{ => config}/config-impl.ts (87%) rename packages/angular/cli/commands/{config-long.md => config/long-description.md} (100%) delete mode 100644 packages/angular/cli/commands/definitions.json delete mode 100644 packages/angular/cli/commands/deploy-impl.ts delete mode 100644 packages/angular/cli/commands/deploy.json create mode 100644 packages/angular/cli/commands/deploy/cli.ts rename packages/angular/cli/commands/{deploy-long.md => deploy/long-description.md} (100%) delete mode 100644 packages/angular/cli/commands/doc-impl.ts delete mode 100644 packages/angular/cli/commands/doc.json create mode 100644 packages/angular/cli/commands/doc/cli.ts delete mode 100644 packages/angular/cli/commands/e2e-impl.ts delete mode 100644 packages/angular/cli/commands/e2e-long.md delete mode 100644 packages/angular/cli/commands/e2e.json create mode 100644 packages/angular/cli/commands/e2e/cli.ts delete mode 100644 packages/angular/cli/commands/easter-egg-impl.ts delete mode 100644 packages/angular/cli/commands/easter-egg.json delete mode 100644 packages/angular/cli/commands/extract-i18n-impl.ts delete mode 100644 packages/angular/cli/commands/extract-i18n.json create mode 100644 packages/angular/cli/commands/extract-i18n/cli.ts delete mode 100644 packages/angular/cli/commands/generate-impl.ts delete mode 100644 packages/angular/cli/commands/generate.json create mode 100644 packages/angular/cli/commands/generate/cli.ts create mode 100644 packages/angular/cli/commands/generate/generate-impl.ts delete mode 100644 packages/angular/cli/commands/help-impl.ts delete mode 100644 packages/angular/cli/commands/help-long.md delete mode 100644 packages/angular/cli/commands/help.json delete mode 100644 packages/angular/cli/commands/lint-impl.ts delete mode 100644 packages/angular/cli/commands/lint.json create mode 100644 packages/angular/cli/commands/lint/cli.ts rename packages/angular/cli/commands/{lint-long.md => lint/long-description.md} (100%) create mode 100644 packages/angular/cli/commands/make-this-awesome/cli.ts delete mode 100644 packages/angular/cli/commands/new.json delete mode 100644 packages/angular/cli/commands/new.md create mode 100644 packages/angular/cli/commands/new/cli.ts rename packages/angular/cli/commands/{ => new}/new-impl.ts (53%) delete mode 100644 packages/angular/cli/commands/run-impl.ts delete mode 100644 packages/angular/cli/commands/run.json create mode 100644 packages/angular/cli/commands/run/cli.ts rename packages/angular/cli/commands/{run-long.md => run/long-description.md} (100%) delete mode 100644 packages/angular/cli/commands/serve-impl.ts delete mode 100644 packages/angular/cli/commands/serve.json create mode 100644 packages/angular/cli/commands/serve/cli.ts delete mode 100644 packages/angular/cli/commands/test-impl.ts delete mode 100644 packages/angular/cli/commands/test.json create mode 100644 packages/angular/cli/commands/test/cli.ts rename packages/angular/cli/commands/{test-long.md => test/long-description.md} (100%) delete mode 100644 packages/angular/cli/commands/update.json create mode 100644 packages/angular/cli/commands/update/cli.ts rename packages/angular/cli/commands/{update-long.md => update/long-description.md} (100%) rename packages/angular/cli/commands/{ => update}/update-impl.ts (95%) delete mode 100644 packages/angular/cli/commands/version.json rename packages/angular/cli/commands/{version-impl.ts => version/cli.ts} (71%) create mode 100644 packages/angular/cli/lib/cli/command-runner.ts delete mode 100644 packages/angular/cli/models/architect-command.ts delete mode 100644 packages/angular/cli/models/command-runner.ts delete mode 100644 packages/angular/cli/models/parser.ts delete mode 100644 packages/angular/cli/models/parser_spec.ts create mode 100644 packages/angular/cli/utilities/command-builder/architect-command-module.ts create mode 100644 packages/angular/cli/utilities/command-builder/command-module.ts create mode 100644 packages/angular/cli/utilities/command-builder/json-schema.ts create mode 100644 packages/angular/cli/utilities/command-builder/schematics-command-module.ts delete mode 100644 packages/angular/cli/utilities/json-schema.ts delete mode 100644 packages/angular/cli/utilities/json-schema_spec.ts rename packages/angular/cli/{models => utilities}/version.ts (83%) delete mode 100644 tests/legacy-cli/e2e/tests/commands/help/help-option-command.ts delete mode 100644 tests/legacy-cli/e2e/tests/commands/help/help-option.ts delete mode 100644 tests/legacy-cli/e2e/tests/commands/help/help.ts diff --git a/package.json b/package.json index b50743f1faa9..45605ca39ae2 100644 --- a/package.json +++ b/package.json @@ -115,6 +115,7 @@ "@types/semver": "^7.0.0", "@types/text-table": "^0.2.1", "@types/uuid": "^8.0.0", + "@types/yargs": "^17.0.8", "@types/yargs-parser": "^21.0.0", "@typescript-eslint/eslint-plugin": "5.14.0", "@typescript-eslint/parser": "5.14.0", @@ -216,6 +217,7 @@ "webpack-dev-server": "4.7.4", "webpack-merge": "5.8.0", "webpack-subresource-integrity": "5.1.0", + "yargs": "17.3.1", "yargs-parser": "21.0.1", "zone.js": "^0.11.3" } diff --git a/packages/angular/cli/BUILD.bazel b/packages/angular/cli/BUILD.bazel index e8d747de1cff..4be851a9fe70 100644 --- a/packages/angular/cli/BUILD.bazel +++ b/packages/angular/cli/BUILD.bazel @@ -26,24 +26,6 @@ ts_library( # @external_begin # These files are generated from the JSON schema "//packages/angular/cli:lib/config/workspace-schema.ts", - "//packages/angular/cli:commands/analytics.ts", - "//packages/angular/cli:commands/add.ts", - "//packages/angular/cli:commands/build.ts", - "//packages/angular/cli:commands/deploy.ts", - "//packages/angular/cli:commands/config.ts", - "//packages/angular/cli:commands/doc.ts", - "//packages/angular/cli:commands/e2e.ts", - "//packages/angular/cli:commands/easter-egg.ts", - "//packages/angular/cli:commands/generate.ts", - "//packages/angular/cli:commands/help.ts", - "//packages/angular/cli:commands/lint.ts", - "//packages/angular/cli:commands/new.ts", - "//packages/angular/cli:commands/serve.ts", - "//packages/angular/cli:commands/test.ts", - "//packages/angular/cli:commands/update.ts", - "//packages/angular/cli:commands/version.ts", - "//packages/angular/cli:commands/run.ts", - "//packages/angular/cli:commands/extract-i18n.ts", "//packages/angular/cli:src/commands/update/schematic/schema.ts", # @external_end ], @@ -79,6 +61,7 @@ ts_library( "@npm//@types/resolve", "@npm//@types/semver", "@npm//@types/uuid", + "@npm//@types/yargs", "@npm//@yarnpkg/lockfile", "@npm//ansi-colors", "@npm//ini", @@ -88,6 +71,7 @@ ts_library( "@npm//ora", "@npm//pacote", "@npm//semver", + "@npm//yargs", ], ) @@ -132,150 +116,6 @@ ts_json_schema( data = CLI_SCHEMA_DATA, ) -ts_json_schema( - name = "analytics_schema", - src = "commands/analytics.json", - data = [ - "commands/definitions.json", - ], -) - -ts_json_schema( - name = "add_schema", - src = "commands/add.json", - data = [ - "commands/definitions.json", - ], -) - -ts_json_schema( - name = "build_schema", - src = "commands/build.json", - data = [ - "commands/definitions.json", - ], -) - -ts_json_schema( - name = "deploy_schema", - src = "commands/deploy.json", - data = [ - "commands/definitions.json", - ], -) - -ts_json_schema( - name = "config_schema", - src = "commands/config.json", - data = [ - "commands/definitions.json", - ], -) - -ts_json_schema( - name = "doc_schema", - src = "commands/doc.json", - data = [ - "commands/definitions.json", - ], -) - -ts_json_schema( - name = "e2e_schema", - src = "commands/e2e.json", - data = [ - "commands/definitions.json", - ], -) - -ts_json_schema( - name = "easter_egg_schema", - src = "commands/easter-egg.json", - data = [ - "commands/definitions.json", - ], -) - -ts_json_schema( - name = "generate_schema", - src = "commands/generate.json", - data = [ - "commands/definitions.json", - ], -) - -ts_json_schema( - name = "help_schema", - src = "commands/help.json", - data = [ - "commands/definitions.json", - ], -) - -ts_json_schema( - name = "lint_schema", - src = "commands/lint.json", - data = [ - "commands/definitions.json", - ], -) - -ts_json_schema( - name = "new_schema", - src = "commands/new.json", - data = [ - "commands/definitions.json", - ], -) - -ts_json_schema( - name = "run_schema", - src = "commands/run.json", - data = [ - "commands/definitions.json", - ], -) - -ts_json_schema( - name = "serve_schema", - src = "commands/serve.json", - data = [ - "commands/definitions.json", - ], -) - -ts_json_schema( - name = "test_schema", - src = "commands/test.json", - data = [ - "commands/definitions.json", - ], -) - -ts_json_schema( - name = "update_schema", - src = "commands/update.json", - data = [ - "commands/definitions.json", - ], -) - -ts_json_schema( - name = "version_schema", - src = "commands/version.json", - data = [ - "commands/definitions.json", - ], -) - -ts_json_schema( - name = "extract-i18n_schema", - src = "commands/extract-i18n.json", - data = [ - "commands/definitions.json", - ], -) - ts_json_schema( name = "update_schematic_schema", src = "src/commands/update/schematic/schema.json", diff --git a/packages/angular/cli/commands.json b/packages/angular/cli/commands.json deleted file mode 100644 index 0b65947a0647..000000000000 --- a/packages/angular/cli/commands.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "add": "./commands/add.json", - "analytics": "./commands/analytics.json", - "build": "./commands/build.json", - "config": "./commands/config.json", - "deploy": "./commands/deploy.json", - "doc": "./commands/doc.json", - "e2e": "./commands/e2e.json", - "extract-i18n": "./commands/extract-i18n.json", - "make-this-awesome": "./commands/easter-egg.json", - "generate": "./commands/generate.json", - "help": "./commands/help.json", - "lint": "./commands/lint.json", - "new": "./commands/new.json", - "run": "./commands/run.json", - "serve": "./commands/serve.json", - "test": "./commands/test.json", - "update": "./commands/update.json", - "version": "./commands/version.json" -} diff --git a/packages/angular/cli/commands/add.json b/packages/angular/cli/commands/add.json deleted file mode 100644 index 99cd82d897fb..000000000000 --- a/packages/angular/cli/commands/add.json +++ /dev/null @@ -1,54 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema", - "$id": "ng-cli://commands/add.json", - "description": "Adds support for an external library to your project.", - "$longDescription": "./add.md", - - "$scope": "in", - "$impl": "./add-impl#AddCommand", - - "type": "object", - "allOf": [ - { - "properties": { - "collection": { - "type": "string", - "description": "The package to be added.", - "$default": { - "$source": "argv", - "index": 0 - } - }, - "registry": { - "description": "The NPM registry to use.", - "type": "string", - "oneOf": [ - { - "format": "uri" - }, - { - "format": "hostname" - } - ] - }, - "verbose": { - "description": "Display additional details about internal operations during execution.", - "type": "boolean", - "default": false - }, - "skipConfirmation": { - "description": "Skip asking a confirmation prompt before installing and executing the package. Ensure package name is correct prior to using this option.", - "type": "boolean", - "default": false - } - }, - "required": [] - }, - { - "$ref": "./definitions.json#/definitions/interactive" - }, - { - "$ref": "./definitions.json#/definitions/base" - } - ] -} diff --git a/packages/angular/cli/commands/add-impl.ts b/packages/angular/cli/commands/add/add-impl.ts similarity index 89% rename from packages/angular/cli/commands/add-impl.ts rename to packages/angular/cli/commands/add/add-impl.ts index a2cd46602e22..e0097759f863 100644 --- a/packages/angular/cli/commands/add-impl.ts +++ b/packages/angular/cli/commands/add/add-impl.ts @@ -11,23 +11,25 @@ import { NodePackageDoesNotSupportSchematics } from '@angular-devkit/schematics/ import npa from 'npm-package-arg'; import { dirname, join } from 'path'; import { intersects, prerelease, rcompare, satisfies, valid, validRange } from 'semver'; -import { PackageManager } from '../lib/config/workspace-schema'; -import { isPackageNameSafeForAnalytics } from '../models/analytics'; -import { Arguments } from '../models/interface'; -import { RunSchematicOptions, SchematicCommand } from '../models/schematic-command'; -import { colors } from '../utilities/color'; -import { installPackage, installTempPackage } from '../utilities/install-package'; -import { ensureCompatibleNpm, getPackageManager } from '../utilities/package-manager'; +import { PackageManager } from '../../lib/config/workspace-schema'; +import { isPackageNameSafeForAnalytics } from '../../models/analytics'; +import { SchematicCommand } from '../../models/schematic-command'; +import { colors } from '../../utilities/color'; +import { Options } from '../../utilities/command-builder/command-module'; +import { installPackage, installTempPackage } from '../../utilities/install-package'; +import { ensureCompatibleNpm, getPackageManager } from '../../utilities/package-manager'; import { NgAddSaveDepedency, PackageManifest, fetchPackageManifest, fetchPackageMetadata, -} from '../utilities/package-metadata'; -import { askConfirmation } from '../utilities/prompt'; -import { Spinner } from '../utilities/spinner'; -import { isTTY } from '../utilities/tty'; -import { Schema as AddCommandSchema } from './add'; +} from '../../utilities/package-metadata'; +import { askConfirmation } from '../../utilities/prompt'; +import { Spinner } from '../../utilities/spinner'; +import { isTTY } from '../../utilities/tty'; +import { AddCommandArgs } from './cli'; + +type AddCommandOptions = Options; /** * The set of packages that should have certain versions excluded from consideration @@ -39,19 +41,11 @@ const packageVersionExclusions: Record = { '@angular/localize': '9.x', }; -export class AddCommand extends SchematicCommand { +export class AddCommandModule extends SchematicCommand { override readonly allowPrivateSchematics = true; - override async initialize(options: AddCommandSchema & Arguments) { - if (options.registry) { - return super.initialize({ ...options, packageRegistry: options.registry }); - } else { - return super.initialize(options); - } - } - // eslint-disable-next-line max-lines-per-function - async run(options: AddCommandSchema & Arguments) { + async run(options: AddCommandOptions) { await ensureCompatibleNpm(this.context.root); if (!options.collection) { @@ -82,7 +76,7 @@ export class AddCommand extends SchematicCommand { // Already installed so just run schematic this.logger.info('Skipping installation: Package already installed'); - return this.executeSchematic(packageIdentifier.name, options['--']); + return this.executeSchematic(packageIdentifier.name, options); } } @@ -259,7 +253,7 @@ export class AddCommand extends SchematicCommand { } } - return this.executeSchematic(collectionName, options['--']); + return this.executeSchematic(collectionName, options); } private async isProjectVersionValid(packageIdentifier: npa.Result): Promise { @@ -286,7 +280,7 @@ export class AddCommand extends SchematicCommand { override async reportAnalytics( paths: string[], - options: AddCommandSchema & Arguments, + options: AddCommandOptions, dimensions: (boolean | number | string)[] = [], metrics: (boolean | number | string)[] = [], ): Promise { @@ -318,18 +312,29 @@ export class AddCommand extends SchematicCommand { private async executeSchematic( collectionName: string, - options: string[] = [], + options: AddCommandOptions & Record, ): Promise { - const runOptions: RunSchematicOptions = { - schematicOptions: options, - collectionName, - schematicName: 'ng-add', - dryRun: false, - force: false, - }; - try { - return await this.runSchematic(runOptions); + const { + collection, + verbose, + registry, + skipConfirmation, + skipInstall, + interactive, + force, + dryRun, + defaults: defaultVal, + ...schematicOptions + } = options; + + return await this.runSchematic({ + schematicOptions, + collectionName, + schematicName: 'ng-add', + dryRun: false, + force: false, + }); } catch (e) { if (e instanceof NodePackageDoesNotSupportSchematics) { this.logger.error(tags.oneLine` diff --git a/packages/angular/cli/commands/add/cli.ts b/packages/angular/cli/commands/add/cli.ts new file mode 100644 index 000000000000..7ea8824487ef --- /dev/null +++ b/packages/angular/cli/commands/add/cli.ts @@ -0,0 +1,67 @@ +/** + * @license + * Copyright Google LLC 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 { join } from 'path'; +import { Argv } from 'yargs'; +import { + CommandModuleImplementation, + Options, + OtherOptions, +} from '../../utilities/command-builder/command-module'; +import { + SchematicsCommandArgs, + SchematicsCommandModule, +} from '../../utilities/command-builder/schematics-command-module'; +import { AddCommandModule as OldCommandModule } from './add-impl'; + +export interface AddCommandArgs extends SchematicsCommandArgs { + collection: string; + verbose?: boolean; + registry?: string; + 'skip-confirmation'?: boolean; +} + +export class AddCommandModule + extends SchematicsCommandModule + implements CommandModuleImplementation +{ + command = 'add '; + describe = 'Adds support for an external library to your project.'; + longDescriptionPath = join(__dirname, 'long-description.md'); + + override async builder(argv: Argv): Promise> { + const localYargs = await super.builder(argv); + + return localYargs + .positional('collection', { + description: 'The package to be added.', + type: 'string', + demandOption: true, + }) + .option('registry', { description: 'The NPM registry to use.', type: 'string' }) + .option('verbose', { + description: 'Display additional details about internal operations during execution.', + type: 'boolean', + default: false, + }) + .option('skip-confirmation', { + description: + 'Skip asking a confirmation prompt before installing and executing the package. ' + + 'Ensure package name is correct prior to using this option.', + type: 'boolean', + default: false, + }) + .strict(false); + } + + run(options: Options & OtherOptions): Promise { + const command = new OldCommandModule(this.context, 'add'); + + return command.validateAndRun(options); + } +} diff --git a/packages/angular/cli/commands/add.md b/packages/angular/cli/commands/add/long-description.md similarity index 100% rename from packages/angular/cli/commands/add.md rename to packages/angular/cli/commands/add/long-description.md diff --git a/packages/angular/cli/commands/analytics-impl.ts b/packages/angular/cli/commands/analytics-impl.ts deleted file mode 100644 index b0cc575ad173..000000000000 --- a/packages/angular/cli/commands/analytics-impl.ts +++ /dev/null @@ -1,100 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 { - promptGlobalAnalytics, - promptProjectAnalytics, - setAnalyticsConfig, -} from '../models/analytics'; -import { Command } from '../models/command'; -import { Arguments } from '../models/interface'; -import { Schema as AnalyticsCommandSchema, ProjectSetting, SettingOrProject } from './analytics'; - -export class AnalyticsCommand extends Command { - public async run(options: AnalyticsCommandSchema & Arguments) { - // Our parser does not support positional enums (won't report invalid parameters). Do the - // validation manually. - // TODO(hansl): fix parser to better support positionals. This would be a breaking change. - if (options.settingOrProject === undefined) { - if (options['--']) { - // The user passed positional arguments but they didn't validate. - this.logger.error(`Argument ${JSON.stringify(options['--'][0])} is invalid.`); - this.logger.error(`Please provide one of the following value: on, off, ci or project.`); - - return 1; - } else { - // No argument were passed. - await this.printHelp(); - - return 2; - } - } else if ( - options.settingOrProject == SettingOrProject.Project && - options.projectSetting === undefined - ) { - this.logger.error( - `Argument ${JSON.stringify(options.settingOrProject)} requires a second ` + - `argument of one of the following value: on, off.`, - ); - - return 2; - } - - try { - switch (options.settingOrProject) { - case SettingOrProject.Off: - setAnalyticsConfig('global', false); - break; - - case SettingOrProject.On: - setAnalyticsConfig('global', true); - break; - - case SettingOrProject.Ci: - setAnalyticsConfig('global', 'ci'); - break; - - case SettingOrProject.Project: - switch (options.projectSetting) { - case ProjectSetting.Off: - setAnalyticsConfig('local', false); - break; - - case ProjectSetting.On: - setAnalyticsConfig('local', true); - break; - - case ProjectSetting.Prompt: - await promptProjectAnalytics(true); - break; - - default: - await this.printHelp(); - - return 3; - } - break; - - case SettingOrProject.Prompt: - await promptGlobalAnalytics(true); - break; - - default: - await this.printHelp(); - - return 4; - } - } catch (err) { - this.logger.fatal(err.message); - - return 1; - } - - return 0; - } -} diff --git a/packages/angular/cli/commands/analytics-long.md b/packages/angular/cli/commands/analytics-long.md deleted file mode 100644 index 87b9925d1473..000000000000 --- a/packages/angular/cli/commands/analytics-long.md +++ /dev/null @@ -1,8 +0,0 @@ -The value of _settingOrProject_ is one of the following. - -- "on" : Enables analytics gathering and reporting for the user. -- "off" : Disables analytics gathering and reporting for the user. -- "ci" : Enables analytics and configures reporting for use with Continuous Integration, - which uses a common CI user. -- "prompt" : Prompts the user to set the status interactively. -- "project" : Sets the default status for the project to the _projectSetting_ value, which can be any of the other values. The _projectSetting_ argument is ignored for all other values of _settingOrProject_. diff --git a/packages/angular/cli/commands/analytics.json b/packages/angular/cli/commands/analytics.json deleted file mode 100644 index ee2612b20399..000000000000 --- a/packages/angular/cli/commands/analytics.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema", - "$id": "ng-cli://commands/analytics.json", - "description": "Configures the gathering of Angular CLI usage metrics. See https://angular.io/cli/usage-analytics-gathering.", - "$longDescription": "./analytics-long.md", - - "$aliases": [], - "$scope": "all", - "$type": "native", - "$impl": "./analytics-impl#AnalyticsCommand", - - "type": "object", - "allOf": [ - { - "properties": { - "settingOrProject": { - "enum": ["on", "off", "ci", "project", "prompt"], - "description": "Directly enables or disables all usage analytics for the user, or prompts the user to set the status interactively, or sets the default status for the project.", - "$default": { - "$source": "argv", - "index": 0 - } - }, - "projectSetting": { - "enum": ["on", "off", "prompt"], - "description": "Sets the default analytics enablement status for the project.", - "$default": { - "$source": "argv", - "index": 1 - } - } - }, - "required": ["settingOrProject"] - }, - { "$ref": "./definitions.json#/definitions/base" } - ] -} diff --git a/packages/angular/cli/commands/analytics/cli.ts b/packages/angular/cli/commands/analytics/cli.ts new file mode 100644 index 000000000000..b1c004277ec7 --- /dev/null +++ b/packages/angular/cli/commands/analytics/cli.ts @@ -0,0 +1,85 @@ +/** + * @license + * Copyright Google LLC 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 { join } from 'path'; +import { Argv, string } from 'yargs'; +import { + promptGlobalAnalytics, + promptProjectAnalytics, + setAnalyticsConfig, +} from '../../models/analytics'; +import { CommandModule, Options } from '../../utilities/command-builder/command-module'; + +interface AnalyticsCommandArgs { + 'setting-or-project': 'on' | 'off' | 'ci' | 'project' | 'prompt' | string; + 'project-setting'?: 'on' | 'off' | 'prompt' | string; +} + +export class AnalyticsCommandModule extends CommandModule { + command = 'analytics '; + describe = + 'Configures the gathering of Angular CLI usage metrics. See https://angular.io/cli/usage-analytics-gathering.'; + longDescriptionPath = join(__dirname, 'long-description.md'); + + builder(localYargs: Argv): Argv { + return localYargs + .positional('setting-or-project', { + description: + 'Directly enables or disables all usage analytics for the user, or prompts the user to set the status interactively, ' + + 'or sets the default status for the project.', + choices: ['on', 'off', 'ci', 'prompt'], + type: 'string', + demandOption: true, + }) + .positional('project-setting', { + description: 'Sets the default analytics enablement status for the project.', + choices: ['on', 'off', 'prompt'], + type: 'string', + }) + .strict(); + } + + async run({ + settingOrProject, + projectSetting, + }: Options): Promise { + if (settingOrProject === 'project' && projectSetting === undefined) { + throw new Error( + 'Argument "project" requires a second argument of one of the following value: on, off.', + ); + } + + switch (settingOrProject) { + case 'off': + setAnalyticsConfig('global', false); + break; + case 'on': + setAnalyticsConfig('global', true); + break; + case 'ci': + setAnalyticsConfig('global', 'ci'); + break; + case 'project': + switch (projectSetting) { + case 'off': + setAnalyticsConfig('local', false); + break; + case 'on': + setAnalyticsConfig('local', true); + break; + case 'prompt': + await promptProjectAnalytics(true); + break; + } + break; + case 'prompt': + await promptGlobalAnalytics(true); + break; + } + } +} diff --git a/packages/angular/cli/commands/analytics/long-description.md b/packages/angular/cli/commands/analytics/long-description.md new file mode 100644 index 000000000000..ada011b82d31 --- /dev/null +++ b/packages/angular/cli/commands/analytics/long-description.md @@ -0,0 +1,8 @@ +The value of `setting-or-project` is one of the following. + +- `on`: Enables analytics gathering and reporting for the user. +- `off`: Disables analytics gathering and reporting for the user. +- `ci`: Enables analytics and configures reporting for use with Continuous Integration, + which uses a common CI user. +- `prompt`: Prompts the user to set the status interactively. +- `project`: Sets the default status for the project to the `project-setting` value, which can be any of the other values. The `project-setting` argument is ignored for all other values of `setting_or_project`. diff --git a/packages/angular/cli/commands/build-impl.ts b/packages/angular/cli/commands/build-impl.ts deleted file mode 100644 index 2d983a7514b1..000000000000 --- a/packages/angular/cli/commands/build-impl.ts +++ /dev/null @@ -1,19 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 { ArchitectCommand, ArchitectCommandOptions } from '../models/architect-command'; -import { Arguments } from '../models/interface'; -import { Schema as BuildCommandSchema } from './build'; - -export class BuildCommand extends ArchitectCommand { - public override readonly target = 'build'; - - public override async run(options: ArchitectCommandOptions & Arguments) { - return this.runArchitectTarget(options); - } -} diff --git a/packages/angular/cli/commands/build.json b/packages/angular/cli/commands/build.json deleted file mode 100644 index df9d93b85a19..000000000000 --- a/packages/angular/cli/commands/build.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema", - "$id": "ng-cli://commands/build.json", - "description": "Compiles an Angular app into an output directory named dist/ at the given output path. Must be executed from within a workspace directory.", - "$longDescription": "./build-long.md", - - "$aliases": ["b"], - "$scope": "in", - "$type": "architect", - "$impl": "./build-impl#BuildCommand", - - "allOf": [ - { "$ref": "./definitions.json#/definitions/architect" }, - { "$ref": "./definitions.json#/definitions/base" } - ] -} diff --git a/packages/angular/cli/commands/build/cli.ts b/packages/angular/cli/commands/build/cli.ts new file mode 100644 index 000000000000..03d71dc5c762 --- /dev/null +++ b/packages/angular/cli/commands/build/cli.ts @@ -0,0 +1,23 @@ +/** + * @license + * Copyright Google LLC 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 { join } from 'path'; +import { ArchitectCommandModule } from '../../utilities/command-builder/architect-command-module'; +import { CommandModuleImplementation } from '../../utilities/command-builder/command-module'; + +export class BuildCommandModule + extends ArchitectCommandModule + implements CommandModuleImplementation +{ + multiTarget = false; + command = 'build [project]'; + aliases = ['b']; + describe = + 'Compiles an Angular application or library into an output directory named dist/ at the given output path.'; + longDescriptionPath = join(__dirname, 'long-description.md'); +} diff --git a/packages/angular/cli/commands/build-long.md b/packages/angular/cli/commands/build/long-description.md similarity index 100% rename from packages/angular/cli/commands/build-long.md rename to packages/angular/cli/commands/build/long-description.md diff --git a/packages/angular/cli/commands/config.json b/packages/angular/cli/commands/config.json deleted file mode 100644 index bec13fca4c0f..000000000000 --- a/packages/angular/cli/commands/config.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema", - "$id": "ng-cli://commands/config.json", - "description": "Retrieves or sets Angular configuration values in the angular.json file for the workspace.", - "$longDescription": "", - - "$aliases": [], - "$scope": "all", - "$type": "native", - "$impl": "./config-impl#ConfigCommand", - - "type": "object", - "allOf": [ - { - "properties": { - "jsonPath": { - "type": "string", - "description": "The configuration key to set or query, in JSON path format. For example: \"a[3].foo.bar[2]\". If no new value is provided, returns the current value of this key.", - "$default": { - "$source": "argv", - "index": 0 - } - }, - "value": { - "type": ["string", "number", "boolean"], - "description": "If provided, a new value for the given configuration key.", - "$default": { - "$source": "argv", - "index": 1 - } - }, - "global": { - "type": "boolean", - "description": "Access the global configuration in the caller's home directory.", - "default": false, - "aliases": ["g"] - } - }, - "required": [] - }, - { "$ref": "./definitions.json#/definitions/base" } - ] -} diff --git a/packages/angular/cli/commands/config/cli.ts b/packages/angular/cli/commands/config/cli.ts new file mode 100644 index 000000000000..a9f9f0795dbf --- /dev/null +++ b/packages/angular/cli/commands/config/cli.ts @@ -0,0 +1,60 @@ +/** + * @license + * Copyright Google LLC 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 { join } from 'path'; +import { Argv } from 'yargs'; +import { + CommandModule, + CommandModuleImplementation, + Options, +} from '../../utilities/command-builder/command-module'; +import { ConfigCommand } from './config-impl'; + +export interface ConfigCommandArgs { + 'json-path': string; + value?: string; + global?: boolean; +} + +export class ConfigCommandModule + extends CommandModule + implements CommandModuleImplementation +{ + command = 'config [value]'; + describe = + 'Retrieves or sets Angular configuration values in the angular.json file for the workspace.'; + longDescriptionPath = join(__dirname, 'long-description.md'); + + builder(localYargs: Argv): Argv { + return localYargs + .positional('json-path', { + description: + `The configuration key to set or query, in JSON path format. ` + + `For example: "a[3].foo.bar[2]". If no new value is provided, returns the current value of this key.`, + type: 'string', + demandOption: true, + }) + .positional('value', { + description: 'If provided, a new value for the given configuration key.', + type: 'string', + }) + .option('global', { + description: `Access the global configuration in the caller's home directory.`, + alias: ['g'], + type: 'boolean', + default: false, + }) + .strict(); + } + + run(options: Options): Promise { + const command = new ConfigCommand(this.context, 'config'); + + return command.validateAndRun(options); + } +} diff --git a/packages/angular/cli/commands/config-impl.ts b/packages/angular/cli/commands/config/config-impl.ts similarity index 87% rename from packages/angular/cli/commands/config-impl.ts rename to packages/angular/cli/commands/config/config-impl.ts index 1e73b6985471..a032a8135f8a 100644 --- a/packages/angular/cli/commands/config-impl.ts +++ b/packages/angular/cli/commands/config/config-impl.ts @@ -8,11 +8,13 @@ import { JsonValue } from '@angular-devkit/core'; import { v4 as uuidV4 } from 'uuid'; -import { Command } from '../models/command'; -import { Arguments, CommandScope } from '../models/interface'; -import { getWorkspaceRaw, validateWorkspace } from '../utilities/config'; -import { JSONFile, parseJson } from '../utilities/json-file'; -import { Schema as ConfigCommandSchema } from './config'; +import { Command } from '../../models/command'; +import { Options } from '../../utilities/command-builder/command-module'; +import { getWorkspaceRaw, validateWorkspace } from '../../utilities/config'; +import { JSONFile, parseJson } from '../../utilities/json-file'; +import { ConfigCommandArgs } from './cli'; + +type ConfigCommandOptions = Options; const validCliPaths = new Map< string, @@ -95,14 +97,10 @@ function normalizeValue(value: string | undefined | boolean | number): JsonValue } } -export class ConfigCommand extends Command { - public async run(options: ConfigCommandSchema & Arguments) { +export class ConfigCommand extends Command { + public async run(options: ConfigCommandOptions) { const level = options.global ? 'global' : 'local'; - if (!options.global) { - await this.validateScope(CommandScope.InProject); - } - const [config] = getWorkspaceRaw(level); if (options.value == undefined) { @@ -118,7 +116,7 @@ export class ConfigCommand extends Command { } } - private get(jsonFile: JSONFile, options: ConfigCommandSchema) { + private get(jsonFile: JSONFile, options: ConfigCommandOptions) { let value; if (options.jsonPath) { value = jsonFile.get(parseJsonPath(options.jsonPath)); @@ -139,7 +137,7 @@ export class ConfigCommand extends Command { return 0; } - private async set(options: ConfigCommandSchema) { + private async set(options: ConfigCommandOptions) { if (!options.jsonPath?.trim()) { throw new Error('Invalid Path.'); } diff --git a/packages/angular/cli/commands/config-long.md b/packages/angular/cli/commands/config/long-description.md similarity index 100% rename from packages/angular/cli/commands/config-long.md rename to packages/angular/cli/commands/config/long-description.md diff --git a/packages/angular/cli/commands/definitions.json b/packages/angular/cli/commands/definitions.json deleted file mode 100644 index 9bac3acef232..000000000000 --- a/packages/angular/cli/commands/definitions.json +++ /dev/null @@ -1,66 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema", - "$id": "ng-cli://commands/definitions.json", - - "definitions": { - "architect": { - "properties": { - "project": { - "type": "string", - "description": "The name of the project to build. Can be an application or a library.", - "$default": { - "$source": "argv", - "index": 0 - } - }, - "configuration": { - "description": "One or more named builder configurations as a comma-separated list as specified in the \"configurations\" section of angular.json.\nThe builder uses the named configurations to run the given target.\nFor more information, see https://angular.io/guide/workspace-config#alternate-build-configurations.", - "type": "string", - "aliases": ["c"] - } - } - }, - "base": { - "type": "object", - "properties": { - "help": { - "enum": [true, false, "json", "JSON"], - "description": "Shows a help message for this command in the console.", - "default": false - } - } - }, - "schematic": { - "type": "object", - "properties": { - "dryRun": { - "type": "boolean", - "default": false, - "aliases": ["d"], - "description": "Run through and reports activity without writing out results." - }, - "force": { - "type": "boolean", - "default": false, - "aliases": ["f"], - "description": "Force overwriting of existing files." - } - } - }, - "interactive": { - "type": "object", - "properties": { - "interactive": { - "type": "boolean", - "default": "true", - "description": "Enable interactive input prompts." - }, - "defaults": { - "type": "boolean", - "default": "false", - "description": "Disable interactive input prompts for options with a default." - } - } - } - } -} diff --git a/packages/angular/cli/commands/deploy-impl.ts b/packages/angular/cli/commands/deploy-impl.ts deleted file mode 100644 index f8e400a2550b..000000000000 --- a/packages/angular/cli/commands/deploy-impl.ts +++ /dev/null @@ -1,37 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 { ArchitectCommand } from '../models/architect-command'; -import { Arguments } from '../models/interface'; -import { Schema as DeployCommandSchema } from './deploy'; - -const BuilderMissing = ` -Cannot find "deploy" target for the specified project. - -You should add a package that implements deployment capabilities for your -favorite platform. - -For example: - ng add @angular/fire - ng add @azure/ng-deploy - -Find more packages on npm https://www.npmjs.com/search?q=ng%20deploy -`; - -export class DeployCommand extends ArchitectCommand { - public override readonly target = 'deploy'; - public override readonly missingTargetError = BuilderMissing; - - public override async initialize( - options: DeployCommandSchema & Arguments, - ): Promise { - if (!options.help) { - return super.initialize(options); - } - } -} diff --git a/packages/angular/cli/commands/deploy.json b/packages/angular/cli/commands/deploy.json deleted file mode 100644 index cc7c860dde1c..000000000000 --- a/packages/angular/cli/commands/deploy.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema", - "$id": "ng-cli://commands/deploy.json", - "description": "Invokes the deploy builder for a specified project or for the default project in the workspace.", - "$longDescription": "./deploy-long.md", - - "$scope": "in", - "$type": "architect", - "$impl": "./deploy-impl#DeployCommand", - - "allOf": [ - { - "properties": { - "project": { - "type": "string", - "description": "The name of the project to deploy.", - "$default": { - "$source": "argv", - "index": 0 - } - }, - "configuration": { - "description": "One or more named builder configurations as a comma-separated list as specified in the \"configurations\" section of angular.json.\nThe builder uses the named configurations to run the given target.\nFor more information, see https://angular.io/guide/workspace-config#alternate-build-configurations.", - "type": "string", - "aliases": ["c"] - } - }, - "required": [] - }, - { - "$ref": "./definitions.json#/definitions/base" - } - ] -} diff --git a/packages/angular/cli/commands/deploy/cli.ts b/packages/angular/cli/commands/deploy/cli.ts new file mode 100644 index 000000000000..1ee748b340e7 --- /dev/null +++ b/packages/angular/cli/commands/deploy/cli.ts @@ -0,0 +1,36 @@ +/** + * @license + * Copyright Google LLC 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 { tags } from '@angular-devkit/core'; +import { join } from 'path'; +import { ArchitectCommandModule } from '../../utilities/command-builder/architect-command-module'; +import { CommandModuleImplementation } from '../../utilities/command-builder/command-module'; + +export class DeployCommandModule + extends ArchitectCommandModule + implements CommandModuleImplementation +{ + override missingErrorTarget = tags.stripIndents` + Cannot find "deploy" target for the specified project. + + You should add a package that implements deployment capabilities for your + favorite platform. + + For example: + ng add @angular/fire + ng add @azure/ng-deploy + + Find more packages on npm https://www.npmjs.com/search?q=ng%20deploy + `; + + multiTarget = false; + command = 'deploy [project]'; + longDescriptionPath = join(__dirname, 'long-description.md'); + describe = + 'Invokes the deploy builder for a specified project or for the default project in the workspace.'; +} diff --git a/packages/angular/cli/commands/deploy-long.md b/packages/angular/cli/commands/deploy/long-description.md similarity index 100% rename from packages/angular/cli/commands/deploy-long.md rename to packages/angular/cli/commands/deploy/long-description.md diff --git a/packages/angular/cli/commands/doc-impl.ts b/packages/angular/cli/commands/doc-impl.ts deleted file mode 100644 index 4cd4a7a14579..000000000000 --- a/packages/angular/cli/commands/doc-impl.ts +++ /dev/null @@ -1,52 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 open from 'open'; -import { Command } from '../models/command'; -import { Arguments } from '../models/interface'; -import { Schema as DocCommandSchema } from './doc'; - -export class DocCommand extends Command { - public async run(options: DocCommandSchema & Arguments) { - if (!options.keyword) { - this.logger.error('You should specify a keyword, for instance, `ng doc ActivatedRoute`.'); - - return 0; - } - - let domain = 'angular.io'; - - if (options.version) { - // version can either be a string containing "next" - if (options.version == 'next') { - domain = 'next.angular.io'; - // or a number where version must be a valid Angular version (i.e. not 0, 1 or 3) - } else if (!isNaN(+options.version) && ![0, 1, 3].includes(+options.version)) { - domain = `v${options.version}.angular.io`; - } else { - this.logger.error('Version should either be a number (2, 4, 5, 6...) or "next"'); - - return 0; - } - } else { - // we try to get the current Angular version of the project - // and use it if we can find it - try { - /* eslint-disable-next-line import/no-extraneous-dependencies */ - const currentNgVersion = (await import('@angular/core')).VERSION.major; - domain = `v${currentNgVersion}.angular.io`; - } catch {} - } - - await open( - options.search - ? `https://${domain}/api?query=${options.keyword}` - : `https://${domain}/docs?search=${options.keyword}`, - ); - } -} diff --git a/packages/angular/cli/commands/doc.json b/packages/angular/cli/commands/doc.json deleted file mode 100644 index bb01549c6099..000000000000 --- a/packages/angular/cli/commands/doc.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema", - "$id": "ng-cli://commands/doc.json", - "description": "Opens the official Angular documentation (angular.io) in a browser, and searches for a given keyword.", - "$longDescription": "", - - "$aliases": ["d"], - "$type": "native", - "$impl": "./doc-impl#DocCommand", - - "type": "object", - "allOf": [ - { - "properties": { - "keyword": { - "type": "string", - "description": "The keyword to search for, as provided in the search bar in angular.io.", - "$default": { - "$source": "argv", - "index": 0 - } - }, - "search": { - "aliases": ["s"], - "type": "boolean", - "default": false, - "description": "Search all of angular.io. Otherwise, searches only API reference documentation." - }, - "version": { - "oneOf": [ - { - "type": "number", - "minimum": 4 - }, - { - "enum": [2, "next"] - } - ], - "description": "Contains the version of Angular to use for the documentation. If not provided, the command uses your current Angular core version." - } - }, - "required": [] - }, - { "$ref": "./definitions.json#/definitions/base" } - ] -} diff --git a/packages/angular/cli/commands/doc/cli.ts b/packages/angular/cli/commands/doc/cli.ts new file mode 100644 index 000000000000..70bb47773419 --- /dev/null +++ b/packages/angular/cli/commands/doc/cli.ts @@ -0,0 +1,90 @@ +/** + * @license + * Copyright Google LLC 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 open from 'open'; +import { Argv } from 'yargs'; +import { + CommandModule, + CommandModuleImplementation, + Options, +} from '../../utilities/command-builder/command-module'; + +interface DocCommandArgs { + keyword: string; + search?: boolean; + version?: string; +} + +export class DocCommandModule + extends CommandModule + implements CommandModuleImplementation +{ + command = 'doc '; + aliases = ['d']; + describe = + 'Opens the official Angular documentation (angular.io) in a browser, and searches for a given keyword.'; + longDescriptionPath?: string | undefined; + + builder(localYargs: Argv): Argv { + return localYargs + .positional('keyword', { + description: 'The keyword to search for, as provided in the search bar in angular.io.', + type: 'string', + demandOption: true, + }) + .option('search', { + description: `Search all of angular.io. Otherwise, searches only API reference documentation.`, + alias: ['s'], + type: 'boolean', + default: false, + }) + .option('version', { + description: + 'Contains the version of Angular to use for the documentation. ' + + 'If not provided, the command uses your current Angular core version.', + type: 'string', + }) + .strict(); + } + + async run(options: Options): Promise { + let domain = 'angular.io'; + + if (options.version) { + // version can either be a string containing "next" + if (options.version === 'next') { + domain = 'next.angular.io'; + } else if (options.version === 'rc') { + domain = 'rc.angular.io'; + // or a number where version must be a valid Angular version (i.e. not 0, 1 or 3) + } else if (!isNaN(+options.version) && ![0, 1, 3].includes(+options.version)) { + domain = `v${options.version}.angular.io`; + } else { + this.context.logger.error( + 'Version should either be a number (2, 4, 5, 6...), "rc" or "next"', + ); + + return 1; + } + } else { + // we try to get the current Angular version of the project + // and use it if we can find it + try { + /* eslint-disable-next-line import/no-extraneous-dependencies */ + const currentNgVersion = (await import('@angular/core')).VERSION.major; + domain = `v${currentNgVersion}.angular.io`; + } catch {} + } + + await open( + options.search + ? `https://${domain}/api?query=${options.keyword}` + : `https://${domain}/docs?search=${options.keyword}`, + ); + } +} diff --git a/packages/angular/cli/commands/e2e-impl.ts b/packages/angular/cli/commands/e2e-impl.ts deleted file mode 100644 index 5a1df466d97d..000000000000 --- a/packages/angular/cli/commands/e2e-impl.ts +++ /dev/null @@ -1,34 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 { ArchitectCommand } from '../models/architect-command'; -import { Arguments } from '../models/interface'; -import { Schema as E2eCommandSchema } from './e2e'; - -export class E2eCommand extends ArchitectCommand { - public override readonly target = 'e2e'; - public override readonly multiTarget = true; - public override readonly missingTargetError = ` -Cannot find "e2e" target for the specified project. - -You should add a package that implements end-to-end testing capabilities. - -For example: - Cypress: ng add @cypress/schematic - Nightwatch: ng add @nightwatch/schematics - WebdriverIO: ng add @wdio/schematics - -More options will be added to the list as they become available. -`; - - override async initialize(options: E2eCommandSchema & Arguments) { - if (!options.help) { - return super.initialize(options); - } - } -} diff --git a/packages/angular/cli/commands/e2e-long.md b/packages/angular/cli/commands/e2e-long.md deleted file mode 100644 index 26363135a8ce..000000000000 --- a/packages/angular/cli/commands/e2e-long.md +++ /dev/null @@ -1,4 +0,0 @@ -The command takes an optional project name, as specified in the `projects` section of the `angular.json` workspace configuration file. -When a project name is not supplied, executes the `e2e` builder for the default project. - -To use the `ng e2e` command, use `ng add` to add a package that implements end-to-end testing capabilities. Adding the package automatically updates your workspace configuration, adding an `e2e` [CLI builder](guide/cli-builder). diff --git a/packages/angular/cli/commands/e2e.json b/packages/angular/cli/commands/e2e.json deleted file mode 100644 index a8c8cccc4b62..000000000000 --- a/packages/angular/cli/commands/e2e.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema", - "$id": "ng-cli://commands/e2e.json", - "description": "Builds and serves an Angular app, then runs end-to-end tests.", - "$longDescription": "./e2e-long.md", - - "$aliases": ["e"], - "$scope": "in", - "$type": "architect", - "$impl": "./e2e-impl#E2eCommand", - - "type": "object", - "allOf": [ - { "$ref": "./definitions.json#/definitions/architect" }, - { "$ref": "./definitions.json#/definitions/base" } - ] -} diff --git a/packages/angular/cli/commands/e2e/cli.ts b/packages/angular/cli/commands/e2e/cli.ts new file mode 100644 index 000000000000..4f833c8ef2c1 --- /dev/null +++ b/packages/angular/cli/commands/e2e/cli.ts @@ -0,0 +1,35 @@ +/** + * @license + * Copyright Google LLC 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 { tags } from '@angular-devkit/core'; +import { ArchitectCommandModule } from '../../utilities/command-builder/architect-command-module'; +import { CommandModuleImplementation } from '../../utilities/command-builder/command-module'; + +export class E2eCommandModule + extends ArchitectCommandModule + implements CommandModuleImplementation +{ + multiTarget = true; + override missingErrorTarget = tags.stripIndents` + Cannot find "e2e" target for the specified project. + + You should add a package that implements end-to-end testing capabilities. + + For example: + Cypress: ng add @cypress/schematic + Nightwatch: ng add @nightwatch/schematics + WebdriverIO: ng add @wdio/schematics + + More options will be added to the list as they become available. + `; + + command = 'e2e [project]'; + aliases = ['e']; + describe = 'Builds and serves an Angular application, then runs end-to-end tests.'; + longDescriptionPath?: string | undefined; +} diff --git a/packages/angular/cli/commands/easter-egg-impl.ts b/packages/angular/cli/commands/easter-egg-impl.ts deleted file mode 100644 index 3857c38444a5..000000000000 --- a/packages/angular/cli/commands/easter-egg-impl.ts +++ /dev/null @@ -1,31 +0,0 @@ -/** - * @license - * Copyright Google LLC 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'; -import { colors } from '../utilities/color'; -import { Schema as AwesomeCommandSchema } from './easter-egg'; - -function pickOne(of: string[]): string { - return of[Math.floor(Math.random() * of.length)]; -} - -export class AwesomeCommand extends Command { - 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!`, - `You're doing fine.`, - `You're already doing great.`, - `Nothing to do; already awesome. Exiting.`, - `Error 418: As Awesome As Can Get.`, - `I spy with my little eye a great developer!`, - `Noop... already awesome.`, - ]); - this.logger.info(colors.green(phrase)); - } -} diff --git a/packages/angular/cli/commands/easter-egg.json b/packages/angular/cli/commands/easter-egg.json deleted file mode 100644 index 79d9e1bb2edf..000000000000 --- a/packages/angular/cli/commands/easter-egg.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema", - "$id": "ng-cli://commands/easter-egg.json", - "description": "", - "$longDescription": "", - "$hidden": true, - - "$impl": "./easter-egg-impl#AwesomeCommand", - - "type": "object", - "allOf": [{ "$ref": "./definitions.json#/definitions/base" }] -} diff --git a/packages/angular/cli/commands/extract-i18n-impl.ts b/packages/angular/cli/commands/extract-i18n-impl.ts deleted file mode 100644 index 090a0e9d5fe1..000000000000 --- a/packages/angular/cli/commands/extract-i18n-impl.ts +++ /dev/null @@ -1,19 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 { ArchitectCommand } from '../models/architect-command'; -import { Arguments } from '../models/interface'; -import { Schema as ExtractI18nCommandSchema } from './extract-i18n'; - -export class ExtractI18nCommand extends ArchitectCommand { - public override readonly target = 'extract-i18n'; - - public override async run(options: ExtractI18nCommandSchema & Arguments) { - return this.runArchitectTarget(options); - } -} diff --git a/packages/angular/cli/commands/extract-i18n.json b/packages/angular/cli/commands/extract-i18n.json deleted file mode 100644 index 7451eae170a6..000000000000 --- a/packages/angular/cli/commands/extract-i18n.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema", - "$id": "ng-cli://commands/extract-i18n.json", - "description": "Extracts i18n messages from source code.", - "$longDescription": "", - "$scope": "in", - "$type": "architect", - "$impl": "./extract-i18n-impl#ExtractI18nCommand", - - "type": "object", - "allOf": [ - { "$ref": "./definitions.json#/definitions/architect" }, - { "$ref": "./definitions.json#/definitions/base" } - ] -} diff --git a/packages/angular/cli/commands/extract-i18n/cli.ts b/packages/angular/cli/commands/extract-i18n/cli.ts new file mode 100644 index 000000000000..4812a2bda54e --- /dev/null +++ b/packages/angular/cli/commands/extract-i18n/cli.ts @@ -0,0 +1,20 @@ +/** + * @license + * Copyright Google LLC 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 { ArchitectCommandModule } from '../../utilities/command-builder/architect-command-module'; +import { CommandModuleImplementation } from '../../utilities/command-builder/command-module'; + +export class ExtractI18nCommandModule + extends ArchitectCommandModule + implements CommandModuleImplementation +{ + multiTarget = false; + command = 'extract-i18n [project]'; + describe = 'Extracts i18n messages from source code.'; + longDescriptionPath?: string | undefined; +} diff --git a/packages/angular/cli/commands/generate-impl.ts b/packages/angular/cli/commands/generate-impl.ts deleted file mode 100644 index 49d71dd3555c..000000000000 --- a/packages/angular/cli/commands/generate-impl.ts +++ /dev/null @@ -1,120 +0,0 @@ -/** - * @license - * Copyright Google LLC 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, SubCommandDescription } from '../models/interface'; -import { SchematicCommand } from '../models/schematic-command'; -import { colors } from '../utilities/color'; -import { parseJsonSchemaToSubCommandDescription } from '../utilities/json-schema'; -import { Schema as GenerateCommandSchema } from './generate'; - -export class GenerateCommand extends SchematicCommand { - // Allows us to resolve aliases before reporting analytics - longSchematicName: string | undefined; - - override async initialize(options: GenerateCommandSchema & Arguments) { - // Fill up the schematics property of the command description. - const [collectionName, schematicName] = await this.parseSchematicInfo(options); - this.collectionName = collectionName; - this.schematicName = schematicName; - - await super.initialize(options); - - const collection = this.getCollection(collectionName); - const subcommands: { [name: string]: SubCommandDescription } = {}; - - const schematicNames = schematicName ? [schematicName] : collection.listSchematicNames(); - // Sort as a courtesy for the user. - schematicNames.sort(); - - for (const name of schematicNames) { - const schematic = this.getSchematic(collection, name, true); - this.longSchematicName = schematic.description.name; - let subcommand: SubCommandDescription; - if (schematic.description.schemaJson) { - subcommand = await parseJsonSchemaToSubCommandDescription( - name, - schematic.description.path, - this._workflow.registry, - schematic.description.schemaJson, - ); - } else { - continue; - } - - if ((await this.getDefaultSchematicCollection()) == collectionName) { - subcommands[name] = subcommand; - } else { - subcommands[`${collectionName}:${name}`] = subcommand; - } - } - - this.description.options.forEach((option) => { - if (option.name == 'schematic') { - option.subcommands = subcommands; - } - }); - } - - public async run(options: GenerateCommandSchema & Arguments) { - if (!this.schematicName || !this.collectionName) { - return this.printHelp(); - } - - return this.runSchematic({ - collectionName: this.collectionName, - schematicName: this.schematicName, - schematicOptions: options['--'] || [], - debug: !!options.debug || false, - dryRun: !!options.dryRun || false, - force: !!options.force || false, - }); - } - - override async reportAnalytics( - paths: string[], - options: GenerateCommandSchema & Arguments, - ): Promise { - if (!this.collectionName || !this.schematicName) { - return; - } - const escapedSchematicName = (this.longSchematicName || this.schematicName).replace(/\//g, '_'); - - return super.reportAnalytics( - ['generate', this.collectionName.replace(/\//g, '_'), escapedSchematicName], - options, - ); - } - - private async parseSchematicInfo( - options: GenerateCommandSchema, - ): Promise<[string, string | undefined]> { - let collectionName = await this.getDefaultSchematicCollection(); - - let schematicName = options.schematic; - - if (schematicName && schematicName.includes(':')) { - [collectionName, schematicName] = schematicName.split(':', 2); - } - - return [collectionName, schematicName]; - } - - public override async printHelp() { - await super.printHelp(); - - this.logger.info(''); - // Find the generate subcommand. - const subcommand = this.description.options.filter((x) => x.subcommands)[0]; - if (Object.keys((subcommand && subcommand.subcommands) || {}).length == 1) { - this.logger.info(`\nTo see help for a schematic run:`); - this.logger.info(colors.cyan(` ng generate --help`)); - } - - return 0; - } -} diff --git a/packages/angular/cli/commands/generate.json b/packages/angular/cli/commands/generate.json deleted file mode 100644 index 53228340abd4..000000000000 --- a/packages/angular/cli/commands/generate.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema", - "$id": "ng-cli://commands/generate.json", - "description": "Generates and/or modifies files based on a schematic.", - "$longDescription": "", - - "$aliases": ["g"], - "$scope": "in", - "$type": "schematics", - "$impl": "./generate-impl#GenerateCommand", - - "allOf": [ - { - "type": "object", - "properties": { - "schematic": { - "type": "string", - "description": "The schematic or collection:schematic to generate.", - "$default": { - "$source": "argv", - "index": 0 - } - } - }, - "required": [] - }, - { "$ref": "./definitions.json#/definitions/base" }, - { "$ref": "./definitions.json#/definitions/schematic" }, - { "$ref": "./definitions.json#/definitions/interactive" } - ] -} diff --git a/packages/angular/cli/commands/generate/cli.ts b/packages/angular/cli/commands/generate/cli.ts new file mode 100644 index 000000000000..8311d4d2ad1b --- /dev/null +++ b/packages/angular/cli/commands/generate/cli.ts @@ -0,0 +1,147 @@ +/** + * @license + * Copyright Google LLC 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 { Argv } from 'yargs'; +import { + CommandModuleImplementation, + Options, + OtherOptions, +} from '../../utilities/command-builder/command-module'; +import { Option } from '../../utilities/command-builder/json-schema'; +import { + SchematicsCommandArgs, + SchematicsCommandModule, +} from '../../utilities/command-builder/schematics-command-module'; +import { GenerateCommand } from './generate-impl'; + +export interface GenerateCommandArgs extends SchematicsCommandArgs { + schematic?: string; +} + +export class GenerateCommandModule + extends SchematicsCommandModule + implements CommandModuleImplementation +{ + command = 'generate [schematic]'; + aliases = 'g'; + describe = 'Generates and/or modifies files based on a schematic.'; + longDescriptionPath?: string | undefined; + + override async builder(argv: Argv): Promise> { + const [, schematicNameFromArgs] = this.parseSchematicInfo( + // positional = [generate, component] or [generate] + this.context.args.positional[1], + ); + + const baseYargs = await super.builder(argv); + if (this.schematicName) { + return baseYargs; + } + + // When we do know the schematic name we need to add the 'schematic' + // positional option as the schematic will be accessable as a subcommand. + let localYargs = schematicNameFromArgs + ? baseYargs + : baseYargs.positional('schematic', { + describe: 'The schematic or collection:schematic to generate.', + type: 'string', + demandOption: true, + }); + + const collectionName = await this.getCollectionName(); + const workflow = this.getOrCreateWorkflow(collectionName); + const collection = workflow.engine.createCollection(collectionName); + const schematicsInCollection = collection.description.schematics; + + // We cannot use `collection.listSchematicNames()` as this doesn't return hidden schematics. + const schematicNames = new Set(Object.keys(schematicsInCollection).sort()); + + if (schematicNameFromArgs && schematicNames.has(schematicNameFromArgs)) { + // No need to process all schematics since we know which one the user invoked. + schematicNames.clear(); + schematicNames.add(schematicNameFromArgs); + } + + for (const schematicName of schematicNames) { + const { + description: { schemaJson, aliases: schematicAliases, hidden: schematicHidden }, + } = collection.createSchematic(schematicName, true); + + if (!schemaJson) { + continue; + } + + const { + description, + 'x-deprecated': xDeprecated, + aliases = schematicAliases, + hidden = schematicHidden, + } = schemaJson; + const options = await this.getSchematicOptions(collection, schematicName, workflow); + + localYargs = localYargs.command({ + command: await this.generateCommandString(collectionName, schematicName, options), + // When 'describe' is set to false, it results in a hidden command. + describe: hidden === true ? false : typeof description === 'string' ? description : '', + deprecated: xDeprecated === true || typeof xDeprecated === 'string' ? xDeprecated : false, + aliases: Array.isArray(aliases) ? (aliases as string[]) : undefined, + builder: (localYargs) => this.addSchemaOptionsToCommand(localYargs, options).strict(), + handler: (options) => + this.handler({ ...options, schematic: `${collectionName}:${schematicName}` }), + }); + } + + return localYargs; + } + + run( + options: Options & OtherOptions, + ): number | void | Promise { + const command = new GenerateCommand(this.context, 'generate'); + + return command.validateAndRun(options); + } + + /** + * Generate a command string to be passed to the command builder. + * + * @example `component [name]` or `@schematics/angular:component [name]`. + */ + private async generateCommandString( + collectionName: string, + schematicName: string, + options: Option[], + ): Promise { + const [collectionNameFromArgs] = this.parseSchematicInfo( + // positional = [generate, component] or [generate] + this.context.args.positional[1], + ); + + const dasherizedSchematicName = strings.dasherize(schematicName); + + // Only add the collection name as part of the command when it's not the default collection or when it has been provided via the CLI. + // Ex:`ng generate @schematics/angular:component` + const commandName = + !!collectionNameFromArgs || + (await this.getDefaultSchematicCollection()) !== (await this.getCollectionName()) + ? collectionName + ':' + dasherizedSchematicName + : dasherizedSchematicName; + + const positionalArgs = options + .filter((o) => o.positional !== undefined) + .map((o) => { + const label = `${strings.dasherize(o.name)}${o.type === 'array' ? ' ..' : ''}`; + + return o.required ? `<${label}>` : `[${label}]`; + }) + .join(' '); + + return `${commandName}${positionalArgs ? ' ' + positionalArgs : ''}`; + } +} diff --git a/packages/angular/cli/commands/generate/generate-impl.ts b/packages/angular/cli/commands/generate/generate-impl.ts new file mode 100644 index 000000000000..a0aec5ca48b5 --- /dev/null +++ b/packages/angular/cli/commands/generate/generate-impl.ts @@ -0,0 +1,68 @@ +/** + * @license + * Copyright Google LLC 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 { SchematicCommand } from '../../models/schematic-command'; +import { Options, OtherOptions } from '../../utilities/command-builder/command-module'; +import { GenerateCommandArgs } from './cli'; + +type GenerateCommandOptions = Options; + +export class GenerateCommand extends SchematicCommand { + // Allows us to resolve aliases before reporting analytics + longSchematicName: string | undefined; + + override async initialize(options: GenerateCommandOptions) { + // Fill up the schematics property of the command description. + const [collectionName, schematicName] = await this.parseSchematicInfo(options.schematic); + this.collectionName = collectionName ?? (await this.getDefaultSchematicCollection()); + this.schematicName = schematicName; + + await super.initialize(options); + } + + public async run(options: GenerateCommandOptions & OtherOptions) { + if (!this.schematicName || !this.collectionName) { + return 1; + } + + const { dryRun, force, interactive, defaults, schematic, ...schematicOptions } = options; + + return this.runSchematic({ + collectionName: this.collectionName, + schematicName: this.schematicName, + schematicOptions: schematicOptions, + debug: false, + dryRun, + force, + }); + } + + override async reportAnalytics(paths: string[], options: GenerateCommandOptions): Promise { + if (!this.collectionName || !this.schematicName) { + return; + } + const escapedSchematicName = (this.longSchematicName || this.schematicName).replace(/\//g, '_'); + + return super.reportAnalytics( + ['generate', this.collectionName.replace(/\//g, '_'), escapedSchematicName], + options, + ); + } + + private parseSchematicInfo( + schematic: string | undefined, + ): [collectionName: string | undefined, schematicName: string | undefined] { + if (schematic?.includes(':')) { + const [collectionName, schematicName] = schematic.split(':', 2); + + return [collectionName, schematicName]; + } + + return [undefined, schematic]; + } +} diff --git a/packages/angular/cli/commands/help-impl.ts b/packages/angular/cli/commands/help-impl.ts deleted file mode 100644 index c7ccc282493d..000000000000 --- a/packages/angular/cli/commands/help-impl.ts +++ /dev/null @@ -1,27 +0,0 @@ -/** - * @license - * Copyright Google LLC 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'; -import { colors } from '../utilities/color'; -import { Schema as HelpCommandSchema } from './help'; - -export class HelpCommand extends Command { - async run() { - this.logger.info(`Available Commands:`); - - for (const cmd of Object.values(await Command.commandMap())) { - if (cmd.hidden) { - continue; - } - - const aliasInfo = cmd.aliases.length > 0 ? ` (${cmd.aliases.join(', ')})` : ''; - this.logger.info(` ${colors.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-long.md b/packages/angular/cli/commands/help-long.md deleted file mode 100644 index cc4b790f906e..000000000000 --- a/packages/angular/cli/commands/help-long.md +++ /dev/null @@ -1,7 +0,0 @@ -For help with individual commands, use the `--help` or `-h` option with the command. - -For example, - -```sh -ng help serve -``` diff --git a/packages/angular/cli/commands/help.json b/packages/angular/cli/commands/help.json deleted file mode 100644 index a6513118d0e4..000000000000 --- a/packages/angular/cli/commands/help.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema", - "$id": "ng-cli://commands/help.json", - "description": "Lists available commands and their short descriptions.", - "$longDescription": "./help-long.md", - - "$scope": "all", - "$aliases": [], - "$impl": "./help-impl#HelpCommand", - - "type": "object", - "allOf": [{ "$ref": "./definitions.json#/definitions/base" }] -} diff --git a/packages/angular/cli/commands/lint-impl.ts b/packages/angular/cli/commands/lint-impl.ts deleted file mode 100644 index e9fb4dc801b3..000000000000 --- a/packages/angular/cli/commands/lint-impl.ts +++ /dev/null @@ -1,57 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 { spawnSync } from 'child_process'; -import * as path from 'path'; -import { ArchitectCommand } from '../models/architect-command'; -import { Arguments } from '../models/interface'; -import { askConfirmation } from '../utilities/prompt'; -import { Schema as LintCommandSchema } from './lint'; - -const MissingBuilder = ` -Cannot find "lint" target for the specified project. - -You should add a package that implements linting capabilities. - -For example: - ng add @angular-eslint/schematics -`; - -export class LintCommand extends ArchitectCommand { - override readonly target = 'lint'; - override readonly multiTarget = true; - - override async initialize(options: LintCommandSchema & Arguments): Promise { - if (!options.help) { - return super.initialize(options); - } - } - - override async onMissingTarget(): Promise { - this.logger.warn(MissingBuilder); - - const shouldAdd = await askConfirmation('Would you like to add ESLint now?', true, false); - if (shouldAdd) { - // Run `ng add @angular-eslint/schematics` - const binPath = path.resolve(__dirname, '../bin/ng.js'); - const { status, error } = spawnSync( - process.execPath, - [binPath, 'add', '@angular-eslint/schematics'], - { - stdio: 'inherit', - }, - ); - - if (error) { - throw error; - } - - return status ?? 0; - } - } -} diff --git a/packages/angular/cli/commands/lint.json b/packages/angular/cli/commands/lint.json deleted file mode 100644 index 824632e79f76..000000000000 --- a/packages/angular/cli/commands/lint.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema", - "$id": "ng-cli://commands/lint.json", - "description": "Runs linting tools on Angular app code in a given project folder.", - "$longDescription": "./lint-long.md", - - "$aliases": ["l"], - "$scope": "in", - "$type": "architect", - "$impl": "./lint-impl#LintCommand", - - "type": "object", - "allOf": [ - { - "properties": { - "project": { - "type": "string", - "description": "The name of the project to lint.", - "$default": { - "$source": "argv", - "index": 0 - } - }, - "configuration": { - "description": "One or more named builder configurations as a comma-separated list as specified in the \"configurations\" section of angular.json.\nThe builder uses the named configurations to run the given target.\nFor more information, see https://angular.io/guide/workspace-config#alternate-build-configurations.", - "type": "string", - "aliases": ["c"] - } - }, - "required": [] - }, - { - "$ref": "./definitions.json#/definitions/base" - } - ] -} diff --git a/packages/angular/cli/commands/lint/cli.ts b/packages/angular/cli/commands/lint/cli.ts new file mode 100644 index 000000000000..1715b5a7a0bf --- /dev/null +++ b/packages/angular/cli/commands/lint/cli.ts @@ -0,0 +1,31 @@ +/** + * @license + * Copyright Google LLC 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 { tags } from '@angular-devkit/core'; +import { join } from 'path'; +import { ArchitectCommandModule } from '../../utilities/command-builder/architect-command-module'; +import { CommandModuleImplementation } from '../../utilities/command-builder/command-module'; + +export class LintCommandModule + extends ArchitectCommandModule + implements CommandModuleImplementation +{ + override missingErrorTarget = tags.stripIndents` + Cannot find "lint" target for the specified project. + + You should add a package that implements linting capabilities. + + For example: + ng add @angular-eslint/schematics + `; + + multiTarget = true; + command = 'lint [project]'; + longDescriptionPath = join(__dirname, 'long-description.md'); + describe = 'Runs linting tools on Angular application code in a given project folder.'; +} diff --git a/packages/angular/cli/commands/lint-long.md b/packages/angular/cli/commands/lint/long-description.md similarity index 100% rename from packages/angular/cli/commands/lint-long.md rename to packages/angular/cli/commands/lint/long-description.md diff --git a/packages/angular/cli/commands/make-this-awesome/cli.ts b/packages/angular/cli/commands/make-this-awesome/cli.ts new file mode 100644 index 000000000000..9705533289d5 --- /dev/null +++ b/packages/angular/cli/commands/make-this-awesome/cli.ts @@ -0,0 +1,41 @@ +/** + * @license + * Copyright Google LLC 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 { Argv } from 'yargs'; +import { colors } from '../../utilities/color'; +import { + CommandModule, + CommandModuleImplementation, +} from '../../utilities/command-builder/command-module'; + +export class AwesomeCommandModule extends CommandModule implements CommandModuleImplementation { + command = 'make-this-awesome'; + describe: false = false; + longDescriptionPath?: string | undefined; + + builder(localYargs: Argv): Argv { + return localYargs; + } + + run(): void { + const pickOne = (of: string[]) => of[Math.floor(Math.random() * of.length)]; + + const phrase = pickOne([ + `You're on it, there's nothing for me to do!`, + `Let's take a look... nope, it's all good!`, + `You're doing fine.`, + `You're already doing great.`, + `Nothing to do; already awesome. Exiting.`, + `Error 418: As Awesome As Can Get.`, + `I spy with my little eye a great developer!`, + `Noop... already awesome.`, + ]); + + this.context.logger.info(colors.green(phrase)); + } +} diff --git a/packages/angular/cli/commands/new.json b/packages/angular/cli/commands/new.json deleted file mode 100644 index 90efa76056be..000000000000 --- a/packages/angular/cli/commands/new.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema", - "$id": "ng-cli://commands/new.json", - "description": "Creates a new workspace and an initial Angular application.", - "$longDescription": "./new.md", - - "$aliases": ["n"], - "$scope": "out", - "$type": "schematic", - "$impl": "./new-impl#NewCommand", - - "type": "object", - "allOf": [ - { - "properties": { - "collection": { - "type": "string", - "aliases": ["c"], - "description": "A collection of schematics to use in generating the initial application." - }, - "verbose": { - "type": "boolean", - "default": false, - "aliases": ["v"], - "description": "Add more details to output logging." - } - }, - "required": [] - }, - { "$ref": "./definitions.json#/definitions/base" }, - { "$ref": "./definitions.json#/definitions/schematic" }, - { "$ref": "./definitions.json#/definitions/interactive" } - ] -} diff --git a/packages/angular/cli/commands/new.md b/packages/angular/cli/commands/new.md deleted file mode 100644 index 135e1b2c108a..000000000000 --- a/packages/angular/cli/commands/new.md +++ /dev/null @@ -1,16 +0,0 @@ -Creates and initializes a new Angular application that is the default project for a new workspace. - -Provides interactive prompts for optional configuration, such as adding routing support. -All prompts can safely be allowed to default. - -- The new workspace folder is given the specified project name, and contains configuration files at the top level. - -- By default, the files for a new initial application (with the same name as the workspace) are placed in the `src/` subfolder. - -- The new application's configuration appears in the `projects` section of the `angular.json` workspace configuration file, under its project name. - -- Subsequent applications that you generate in the workspace reside in the `projects/` subfolder. - -If you plan to have multiple applications in the workspace, you can create an empty workspace by setting the `--create-application` option to false. -You can then use `ng generate application` to create an initial application. -This allows a workspace name different from the initial app name, and ensures that all applications reside in the `/projects` subfolder, matching the structure of the configuration file. diff --git a/packages/angular/cli/commands/new/cli.ts b/packages/angular/cli/commands/new/cli.ts new file mode 100644 index 000000000000..38174ff81e72 --- /dev/null +++ b/packages/angular/cli/commands/new/cli.ts @@ -0,0 +1,53 @@ +/** + * @license + * Copyright Google LLC 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 { Argv } from 'yargs'; +import { + CommandModuleImplementation, + CommandScope, + Options, + OtherOptions, +} from '../../utilities/command-builder/command-module'; +import { + SchematicsCommandArgs, + SchematicsCommandModule, +} from '../../utilities/command-builder/schematics-command-module'; +import { NewCommand } from './new-impl'; + +export interface NewCommandArgs extends SchematicsCommandArgs { + collection?: string; +} + +export class NewCommandModule + extends SchematicsCommandModule + implements CommandModuleImplementation +{ + protected override schematicName = 'ng-new'; + static override scope = CommandScope.Out; + + command = 'new [name]'; + aliases = 'n'; + describe = 'Creates a new Angular workspace.'; + longDescriptionPath?: string | undefined; + + override async builder(argv: Argv): Promise> { + const baseYargs = await super.builder(argv); + + return baseYargs.option('collection', { + alias: 'c', + describe: 'A collection of schematics to use in generating the initial application.', + type: 'string', + }); + } + + run(options: Options & OtherOptions): number | void | Promise { + const command = new NewCommand(this.context, 'new'); + + return command.validateAndRun(options); + } +} diff --git a/packages/angular/cli/commands/new-impl.ts b/packages/angular/cli/commands/new/new-impl.ts similarity index 53% rename from packages/angular/cli/commands/new-impl.ts rename to packages/angular/cli/commands/new/new-impl.ts index b4869de0f043..964ba7244dd2 100644 --- a/packages/angular/cli/commands/new-impl.ts +++ b/packages/angular/cli/commands/new/new-impl.ts @@ -6,33 +6,37 @@ * found in the LICENSE file at https://angular.io/license */ -import { Arguments } from '../models/interface'; -import { SchematicCommand } from '../models/schematic-command'; -import { VERSION } from '../models/version'; -import { Schema as NewCommandSchema } from './new'; +import { SchematicCommand } from '../../models/schematic-command'; +import { Options, OtherOptions } from '../../utilities/command-builder/command-module'; +import { VERSION } from '../../utilities/version'; +import { NewCommandArgs } from './cli'; -export class NewCommand extends SchematicCommand { +type NewCommandOptions = Options; + +export class NewCommand extends SchematicCommand { public override readonly allowMissingWorkspace = true; override schematicName = 'ng-new'; - override async initialize(options: NewCommandSchema & Arguments) { + override async initialize(options: NewCommandOptions) { this.collectionName = options.collection || (await this.getDefaultSchematicCollection()); return super.initialize(options); } - public async run(options: NewCommandSchema & Arguments) { + public async run(options: NewCommandOptions & OtherOptions) { // Register the version of the CLI in the registry. const version = VERSION.full; this._workflow.registry.addSmartDefaultProvider('ng-cli-version', () => version); + const { dryRun, force, interactive, defaults, collection, ...schematicOptions } = options; + return this.runSchematic({ collectionName: this.collectionName, schematicName: this.schematicName, - schematicOptions: options['--'] || [], - debug: !!options.debug, - dryRun: !!options.dryRun, - force: !!options.force, + schematicOptions, + debug: false, + dryRun, + force, }); } } diff --git a/packages/angular/cli/commands/run-impl.ts b/packages/angular/cli/commands/run-impl.ts deleted file mode 100644 index d9cee91850aa..000000000000 --- a/packages/angular/cli/commands/run-impl.ts +++ /dev/null @@ -1,21 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 { ArchitectCommand, ArchitectCommandOptions } from '../models/architect-command'; -import { Arguments } from '../models/interface'; -import { Schema as RunCommandSchema } from './run'; - -export class RunCommand extends ArchitectCommand { - public override async run(options: ArchitectCommandOptions & Arguments) { - if (options.target) { - return this.runArchitectTarget(options); - } else { - throw new Error('Invalid architect target.'); - } - } -} diff --git a/packages/angular/cli/commands/run.json b/packages/angular/cli/commands/run.json deleted file mode 100644 index f4e2287dbf35..000000000000 --- a/packages/angular/cli/commands/run.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema", - "$id": "ng-cli://commands/run.json", - "description": "Runs an Architect target with an optional custom builder configuration defined in your project.", - "$longDescription": "./run-long.md", - - "$aliases": [], - "$scope": "in", - "$type": "architect", - "$impl": "./run-impl#RunCommand", - - "type": "object", - "allOf": [ - { - "properties": { - "target": { - "type": "string", - "description": "The Architect target to run.", - "$default": { - "$source": "argv", - "index": 0 - } - }, - "configuration": { - "description": "One or more named builder configurations as a comma-separated list as specified in the \"configurations\" section of angular.json.\nThe builder uses the named configurations to run the given target.\nFor more information, see https://angular.io/guide/workspace-config#alternate-build-configurations.", - "type": "string", - "aliases": ["c"] - } - }, - "required": [] - }, - { - "$ref": "./definitions.json#/definitions/base" - } - ] -} diff --git a/packages/angular/cli/commands/run/cli.ts b/packages/angular/cli/commands/run/cli.ts new file mode 100644 index 000000000000..7674f1f656cb --- /dev/null +++ b/packages/angular/cli/commands/run/cli.ts @@ -0,0 +1,114 @@ +/** + * @license + * Copyright Google LLC 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 { Architect, Target } from '@angular-devkit/architect'; +import { WorkspaceNodeModulesArchitectHost } from '@angular-devkit/architect/node'; +import { json } from '@angular-devkit/core'; +import { join } from 'path'; +import { Argv } from 'yargs'; +import { isPackageNameSafeForAnalytics } from '../../models/analytics'; +import { getArchitectTargetOptions } from '../../utilities/command-builder/architect-command-module'; +import { + CommandModule, + CommandModuleError, + CommandModuleImplementation, + CommandScope, + Options, + OtherOptions, +} from '../../utilities/command-builder/command-module'; + +export interface RunCommandArgs { + target: string; +} + +export class RunCommandModule + extends CommandModule + implements CommandModuleImplementation +{ + static override scope = CommandScope.In; + + command = 'run '; + describe = + 'Runs an Architect target with an optional custom builder configuration defined in your project.'; + longDescriptionPath = join(__dirname, 'long-description.md'); + + async builder(argv: Argv): Promise> { + const localYargs: Argv = argv + .positional('target', { + describe: 'The Architect target to run.', + type: 'string', + demandOption: true, + }) + .strict(); + + const target = this.makeTargetSpecifier(); + if (!target) { + return localYargs; + } + + const schemaOptions = await getArchitectTargetOptions(this.context, target); + + return this.addSchemaOptionsToCommand(localYargs, schemaOptions); + } + + async run(options: Options & OtherOptions): Promise { + const { logger, workspace } = this.context; + if (!workspace) { + throw new CommandModuleError('A workspace is required for this command.'); + } + + const registry = new json.schema.CoreSchemaRegistry(); + registry.addPostTransform(json.schema.transforms.addUndefinedDefaults); + registry.useXDeprecatedProvider((msg) => logger.warn(msg)); + + const architectHost = new WorkspaceNodeModulesArchitectHost(workspace, workspace.basePath); + const architect = new Architect(architectHost, registry); + + const target = this.makeTargetSpecifier(options); + + if (!target) { + throw new CommandModuleError('Cannot determine project or target.'); + } + + const builderName = await architectHost.getBuilderNameForTarget(target); + await this.reportAnalytics({ + ...(await architectHost.getOptionsForTarget(target)), + ...options, + }); + + const { target: _target, ...extraOptions } = options; + const run = await architect.scheduleTarget(target, extraOptions as json.JsonObject, { + logger, + analytics: isPackageNameSafeForAnalytics(builderName) ? await this.getAnalytics() : undefined, + }); + + const { error, success } = await run.output.toPromise(); + await run.stop(); + + if (error) { + logger.error(error); + } + + return success ? 0 : 1; + } + + protected makeTargetSpecifier(options?: Options): Target | undefined { + const architectTarget = options?.target ?? this.context.args.positional[1]; + if (!architectTarget) { + return undefined; + } + + const [project = '', target = '', configuration] = architectTarget.split(':'); + + return { + project, + target, + configuration, + }; + } +} diff --git a/packages/angular/cli/commands/run-long.md b/packages/angular/cli/commands/run/long-description.md similarity index 100% rename from packages/angular/cli/commands/run-long.md rename to packages/angular/cli/commands/run/long-description.md diff --git a/packages/angular/cli/commands/serve-impl.ts b/packages/angular/cli/commands/serve-impl.ts deleted file mode 100644 index 9d8dc3bec6eb..000000000000 --- a/packages/angular/cli/commands/serve-impl.ts +++ /dev/null @@ -1,23 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 { ArchitectCommand, ArchitectCommandOptions } from '../models/architect-command'; -import { Arguments } from '../models/interface'; -import { Schema as ServeCommandSchema } from './serve'; - -export class ServeCommand extends ArchitectCommand { - public override readonly target = 'serve'; - - public validate() { - return true; - } - - public override async run(options: ArchitectCommandOptions & Arguments) { - return this.runArchitectTarget(options); - } -} diff --git a/packages/angular/cli/commands/serve.json b/packages/angular/cli/commands/serve.json deleted file mode 100644 index efc7ba4089ae..000000000000 --- a/packages/angular/cli/commands/serve.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema", - "$id": "ng-cli://commands/serve.json", - "description": "Builds and serves your app, rebuilding on file changes.", - "$longDescription": "", - - "$aliases": ["s"], - "$scope": "in", - "$type": "architect", - "$impl": "./serve-impl#ServeCommand", - - "type": "object", - "allOf": [ - { "$ref": "./definitions.json#/definitions/architect" }, - { "$ref": "./definitions.json#/definitions/base" } - ] -} diff --git a/packages/angular/cli/commands/serve/cli.ts b/packages/angular/cli/commands/serve/cli.ts new file mode 100644 index 000000000000..6231a9060980 --- /dev/null +++ b/packages/angular/cli/commands/serve/cli.ts @@ -0,0 +1,21 @@ +/** + * @license + * Copyright Google LLC 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 { ArchitectCommandModule } from '../../utilities/command-builder/architect-command-module'; +import { CommandModuleImplementation } from '../../utilities/command-builder/command-module'; + +export class ServeCommandModule + extends ArchitectCommandModule + implements CommandModuleImplementation +{ + multiTarget = false; + command = 'serve [project]'; + aliases = ['s']; + describe = 'Builds and serves your application, rebuilding on file changes.'; + longDescriptionPath?: string | undefined; +} diff --git a/packages/angular/cli/commands/test-impl.ts b/packages/angular/cli/commands/test-impl.ts deleted file mode 100644 index 511520b0f02b..000000000000 --- a/packages/angular/cli/commands/test-impl.ts +++ /dev/null @@ -1,20 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 { ArchitectCommand, ArchitectCommandOptions } from '../models/architect-command'; -import { Arguments } from '../models/interface'; -import { Schema as TestCommandSchema } from './test'; - -export class TestCommand extends ArchitectCommand { - public override readonly target = 'test'; - public override readonly multiTarget = true; - - public override async run(options: ArchitectCommandOptions & Arguments) { - return this.runArchitectTarget(options); - } -} diff --git a/packages/angular/cli/commands/test.json b/packages/angular/cli/commands/test.json deleted file mode 100644 index 5fb4ce014c48..000000000000 --- a/packages/angular/cli/commands/test.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema", - "$id": "ng-cli://commands/test.json", - "description": "Runs unit tests in a project.", - "$longDescription": "./test-long.md", - - "$aliases": ["t"], - "$scope": "in", - "$type": "architect", - "$impl": "./test-impl#TestCommand", - - "type": "object", - "allOf": [ - { "$ref": "./definitions.json#/definitions/architect" }, - { "$ref": "./definitions.json#/definitions/base" } - ] -} diff --git a/packages/angular/cli/commands/test/cli.ts b/packages/angular/cli/commands/test/cli.ts new file mode 100644 index 000000000000..4c7dd6cbe23b --- /dev/null +++ b/packages/angular/cli/commands/test/cli.ts @@ -0,0 +1,22 @@ +/** + * @license + * Copyright Google LLC 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 { join } from 'path'; +import { ArchitectCommandModule } from '../../utilities/command-builder/architect-command-module'; +import { CommandModuleImplementation } from '../../utilities/command-builder/command-module'; + +export class TestCommandModule + extends ArchitectCommandModule + implements CommandModuleImplementation +{ + multiTarget = true; + command = 'test [project]'; + aliases = ['t']; + describe = 'Runs unit tests in a project.'; + longDescriptionPath = join(__dirname, 'long-description.md'); +} diff --git a/packages/angular/cli/commands/test-long.md b/packages/angular/cli/commands/test/long-description.md similarity index 100% rename from packages/angular/cli/commands/test-long.md rename to packages/angular/cli/commands/test/long-description.md diff --git a/packages/angular/cli/commands/update.json b/packages/angular/cli/commands/update.json deleted file mode 100644 index 043cedcd3cb2..000000000000 --- a/packages/angular/cli/commands/update.json +++ /dev/null @@ -1,78 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema", - "$id": "ng-cli://commands/update.json", - "description": "Updates your application and its dependencies. See https://update.angular.io/", - "$longDescription": "./update-long.md", - - "$scope": "all", - "$aliases": [], - "$type": "schematics", - "$impl": "./update-impl#UpdateCommand", - - "type": "object", - "allOf": [ - { - "$ref": "./definitions.json#/definitions/base" - }, - { - "type": "object", - "properties": { - "packages": { - "description": "The names of package(s) to update.", - "type": "array", - "items": { - "type": "string" - }, - "$default": { - "$source": "argv" - } - }, - "force": { - "description": "Ignore peer dependency version mismatches. Passes the `--force` flag to the package manager when installing packages.", - "default": false, - "type": "boolean" - }, - "next": { - "description": "Use the prerelease version, including beta and RCs.", - "default": false, - "type": "boolean" - }, - "migrateOnly": { - "description": "Only perform a migration, do not update the installed version.", - "oneOf": [ - { - "type": "boolean" - }, - { - "type": "string", - "description": "The name of the migration to run." - } - ] - }, - "from": { - "description": "Version from which to migrate from. Only available with a single package being updated, and only on migration only.", - "type": "string" - }, - "to": { - "description": "Version up to which to apply migrations. Only available with a single package being updated, and only on migrations only. Requires from to be specified. Default to the installed version detected.", - "type": "string" - }, - "allowDirty": { - "description": "Whether to allow updating when the repository contains modified or untracked files.", - "type": "boolean" - }, - "verbose": { - "description": "Display additional details about internal operations during execution.", - "type": "boolean", - "default": false - }, - "createCommits": { - "description": "Create source control commits for updates and migrations.", - "type": "boolean", - "default": false, - "aliases": ["C"] - } - } - } - ] -} diff --git a/packages/angular/cli/commands/update/cli.ts b/packages/angular/cli/commands/update/cli.ts new file mode 100644 index 000000000000..1d611ba910f1 --- /dev/null +++ b/packages/angular/cli/commands/update/cli.ts @@ -0,0 +1,106 @@ +/** + * @license + * Copyright Google LLC 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 { Argv } from 'yargs'; +import { + CommandModule, + CommandScope, + Options, + OtherOptions, +} from '../../utilities/command-builder/command-module'; +import { UpdateCommand } from './update-impl'; + +export interface UpdateCommandArgs { + packages?: string | string[]; + force: boolean; + next: boolean; + 'migrate-only'?: boolean; + name?: string; + from?: string; + to?: string; + 'allow-dirty': boolean; + verbose: boolean; + 'create-commits': boolean; +} + +export class UpdateCommandModule extends CommandModule { + static override scope = CommandScope.In; + + command = 'update [packages..]'; + describe = 'Updates your workspace and its dependencies. See https://update.angular.io/.'; + longDescriptionPath?: string | undefined; + + builder(localYargs: Argv): Argv { + return localYargs + .positional('packages', { + description: 'The names of package(s) to update.', + type: 'string', + }) + .option('force', { + description: + 'Ignore peer dependency version mismatches. ' + + 'Passes the `--force` flag to the package manager when installing packages.', + type: 'boolean', + default: false, + }) + .option('next', { + description: 'Use the prerelease version, including beta and RCs.', + type: 'boolean', + default: false, + }) + .option('migrate-only', { + description: 'Only perform a migration, do not update the installed version.', + type: 'boolean', + }) + .option('name', { + description: 'The name of the migration to run.', + type: 'string', + implies: ['migrate-only'], + conflicts: ['to', 'from'], + }) + .option('from', { + description: + 'Version from which to migrate from. Only available with a single package being updated, and only on migration only.', + type: 'string', + implies: ['to', 'migrate-only'], + conflicts: ['name'], + }) + .option('to', { + describe: + 'Version up to which to apply migrations. Only available with a single package being updated, ' + + 'and only on migrations only. Requires from to be specified. Default to the installed version detected.', + type: 'string', + implies: ['from', 'migrate-only'], + conflicts: ['name'], + }) + .option('allow-dirty', { + describe: + 'Whether to allow updating when the repository contains modified or untracked files.', + type: 'boolean', + default: false, + }) + .option('verbose', { + describe: 'Display additional details about internal operations during execution.', + type: 'boolean', + default: false, + }) + .option('create-commits', { + describe: 'Create source control commits for updates and migrations.', + type: 'boolean', + alias: ['C'], + default: false, + }) + .strict(); + } + + run(options: Options & OtherOptions): Promise { + const command = new UpdateCommand(this.context, 'update'); + + return command.validateAndRun(options); + } +} diff --git a/packages/angular/cli/commands/update-long.md b/packages/angular/cli/commands/update/long-description.md similarity index 100% rename from packages/angular/cli/commands/update-long.md rename to packages/angular/cli/commands/update/long-description.md diff --git a/packages/angular/cli/commands/update-impl.ts b/packages/angular/cli/commands/update/update-impl.ts similarity index 95% rename from packages/angular/cli/commands/update-impl.ts rename to packages/angular/cli/commands/update/update-impl.ts index 6f0359af2ef3..b8b5a521018b 100644 --- a/packages/angular/cli/commands/update-impl.ts +++ b/packages/angular/cli/commands/update/update-impl.ts @@ -14,34 +14,36 @@ import npa from 'npm-package-arg'; import pickManifest from 'npm-pick-manifest'; import * as path from 'path'; import * as semver from 'semver'; -import { PackageManager } from '../lib/config/workspace-schema'; -import { Command } from '../models/command'; -import { Arguments } from '../models/interface'; -import { SchematicEngineHost } from '../models/schematic-engine-host'; -import { VERSION } from '../models/version'; -import { colors } from '../utilities/color'; -import { installAllPackages, runTempPackageBin } from '../utilities/install-package'; -import { writeErrorToLogFile } from '../utilities/log-file'; -import { ensureCompatibleNpm, getPackageManager } from '../utilities/package-manager'; +import { PackageManager } from '../../lib/config/workspace-schema'; +import { Command } from '../../models/command'; +import { SchematicEngineHost } from '../../models/schematic-engine-host'; +import { colors } from '../../utilities/color'; +import { Options } from '../../utilities/command-builder/command-module'; +import { installAllPackages, runTempPackageBin } from '../../utilities/install-package'; +import { writeErrorToLogFile } from '../../utilities/log-file'; +import { ensureCompatibleNpm, getPackageManager } from '../../utilities/package-manager'; import { PackageIdentifier, PackageManifest, fetchPackageManifest, fetchPackageMetadata, -} from '../utilities/package-metadata'; +} from '../../utilities/package-metadata'; import { PackageTreeNode, findPackageJson, getProjectDependencies, readPackageJson, -} from '../utilities/package-tree'; -import { Schema as UpdateCommandSchema } from './update'; +} from '../../utilities/package-tree'; +import { VERSION } from '../../utilities/version'; +import { UpdateCommandArgs } from './cli'; const UPDATE_SCHEMATIC_COLLECTION = path.join( __dirname, - '../src/commands/update/schematic/collection.json', + '../../src/commands/update/schematic/collection.json', ); +type UpdateCommandOptions = Options; + /** * Disable CLI version mismatch checks and forces usage of the invoked CLI * instead of invoking the local installed version. @@ -53,13 +55,12 @@ const disableVersionCheck = disableVersionCheckEnv.toLowerCase() !== 'false'; const ANGULAR_PACKAGES_REGEXP = /^@(?:angular|nguniversal)\//; - -export class UpdateCommand extends Command { +export class UpdateCommand extends Command { public override readonly allowMissingWorkspace = true; private workflow!: NodeWorkflow; private packageManager = PackageManager.Npm; - override async initialize(options: UpdateCommandSchema & Arguments) { + override async initialize(options: UpdateCommandOptions) { this.packageManager = await getPackageManager(this.context.root); this.workflow = new NodeWorkflow(this.context.root, { packageManager: this.packageManager, @@ -266,13 +267,13 @@ export class UpdateCommand extends Command { } // eslint-disable-next-line max-lines-per-function - async run(options: UpdateCommandSchema & Arguments) { + async run(options: UpdateCommandOptions) { await ensureCompatibleNpm(this.context.root); // Check if the current installed CLI version is older than the latest compatible version. if (!disableVersionCheck) { const cliVersionToInstall = await this.checkCLIVersion( - options['--'], + options.packages, options.verbose, options.next, ); @@ -298,7 +299,9 @@ export class UpdateCommand extends Command { }; const packages: PackageIdentifier[] = []; - for (const request of options['--'] || []) { + const packagesFromOptions = + typeof options.packages === 'string' ? [options.packages] : options.packages; + for (const request of packagesFromOptions ?? []) { try { const packageIdentifier = npa(request); @@ -468,11 +471,11 @@ export class UpdateCommand extends Command { } let result: boolean; - if (typeof options.migrateOnly == 'string') { + if (options.name) { result = await this.executeMigration( packageName, migrations, - options.migrateOnly, + options.name, options.createCommits, ); } else { @@ -826,12 +829,15 @@ export class UpdateCommand extends Command { * @returns the version to install or null when there is no update to install. */ private async checkCLIVersion( - packagesToUpdate: string[] | undefined, + packagesToUpdate: string[] | string | undefined, verbose = false, next = false, ): Promise { const { version } = await fetchPackageManifest( - `@angular/cli@${this.getCLIUpdateRunnerVersion(packagesToUpdate, next)}`, + `@angular/cli@${this.getCLIUpdateRunnerVersion( + typeof packagesToUpdate === 'string' ? [packagesToUpdate] : packagesToUpdate, + next, + )}`, this.logger, { verbose, diff --git a/packages/angular/cli/commands/version.json b/packages/angular/cli/commands/version.json deleted file mode 100644 index 8f4c3ff1fdd1..000000000000 --- a/packages/angular/cli/commands/version.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema", - "$id": "ng-cli://commands/version.json", - "description": "Outputs Angular CLI version.", - "$longDescription": "", - - "$aliases": ["v"], - "$scope": "all", - "$impl": "./version-impl#VersionCommand", - - "type": "object", - "allOf": [{ "$ref": "./definitions.json#/definitions/base" }] -} diff --git a/packages/angular/cli/commands/version-impl.ts b/packages/angular/cli/commands/version/cli.ts similarity index 71% rename from packages/angular/cli/commands/version-impl.ts rename to packages/angular/cli/commands/version/cli.ts index 7df7544e3b4a..e86be716689f 100644 --- a/packages/angular/cli/commands/version-impl.ts +++ b/packages/angular/cli/commands/version/cli.ts @@ -8,15 +8,13 @@ import { execSync } from 'child_process'; import nodeModule from 'module'; -import { Command } from '../models/command'; -import { colors } from '../utilities/color'; -import { getPackageManager } from '../utilities/package-manager'; -import { Schema as VersionCommandSchema } from './version'; - -/** - * Major versions of Node.js that are officially supported by Angular. - */ -const SUPPORTED_NODE_MAJORS = [12, 14, 16]; +import { Argv } from 'yargs'; +import { colors } from '../../utilities/color'; +import { + CommandModule, + CommandModuleImplementation, +} from '../../utilities/command-builder/command-module'; +import { getPackageManager } from '../../utilities/package-manager'; interface PartialPackageInfo { name: string; @@ -25,36 +23,49 @@ interface PartialPackageInfo { devDependencies?: Record; } -export class VersionCommand extends Command { - public static aliases = ['v']; +/** + * Major versions of Node.js that are officially supported by Angular. + */ +const SUPPORTED_NODE_MAJORS = [12, 14, 16]; + +const PACKAGE_PATTERNS = [ + /^@angular\/.*/, + /^@angular-devkit\/.*/, + /^@bazel\/.*/, + /^@ngtools\/.*/, + /^@nguniversal\/.*/, + /^@schematics\/.*/, + /^rxjs$/, + /^typescript$/, + /^ng-packagr$/, + /^webpack$/, +]; + +export class VersionCommandModule extends CommandModule implements CommandModuleImplementation { + command = 'version'; + aliases = ['v']; + describe = 'Outputs Angular CLI version.'; + longDescriptionPath?: string | undefined; + + builder(localYargs: Argv): Argv { + return localYargs; + } - private readonly localRequire = nodeModule.createRequire(__filename); - // Trailing slash is used to allow the path to be treated as a directory - private readonly workspaceRequire = nodeModule.createRequire(this.context.root + '/'); + async run(): Promise { + const logger = this.context.logger; + const localRequire = nodeModule.createRequire(__filename); + // Trailing slash is used to allow the path to be treated as a directory + const workspaceRequire = nodeModule.createRequire(this.context.root + '/'); - async run() { - const cliPackage: PartialPackageInfo = this.localRequire('../package.json'); + const cliPackage: PartialPackageInfo = localRequire('../../package.json'); let workspacePackage: PartialPackageInfo | undefined; try { - workspacePackage = this.workspaceRequire('./package.json'); + workspacePackage = workspaceRequire('./package.json'); } catch {} const [nodeMajor] = process.versions.node.split('.').map((part) => Number(part)); const unsupportedNodeVersion = !SUPPORTED_NODE_MAJORS.includes(nodeMajor); - const patterns = [ - /^@angular\/.*/, - /^@angular-devkit\/.*/, - /^@bazel\/.*/, - /^@ngtools\/.*/, - /^@nguniversal\/.*/, - /^@schematics\/.*/, - /^rxjs$/, - /^typescript$/, - /^ng-packagr$/, - /^webpack$/, - ]; - const packageNames = [ ...Object.keys(cliPackage.dependencies || {}), ...Object.keys(cliPackage.devDependencies || {}), @@ -63,13 +74,13 @@ export class VersionCommand extends Command { ]; const versions = packageNames - .filter((x) => patterns.some((p) => p.test(x))) + .filter((x) => PACKAGE_PATTERNS.some((p) => p.test(x))) .reduce((acc, name) => { if (name in acc) { return acc; } - acc[name] = this.getVersion(name); + acc[name] = this.getVersion(name, workspaceRequire, localRequire); return acc; }, {} as { [module: string]: string }); @@ -112,12 +123,12 @@ export class VersionCommand extends Command { .map((x) => colors.red(x)) .join('\n'); - this.logger.info(asciiArt); - this.logger.info( + logger.info(asciiArt); + logger.info( ` Angular CLI: ${ngCliVersion} Node: ${process.versions.node}${unsupportedNodeVersion ? ' (Unsupported)' : ''} - Package Manager: ${await this.getPackageManager()} + Package Manager: ${await this.getPackageManagerVersion()} OS: ${process.platform} ${process.arch} Angular: ${angularCoreVersion} @@ -148,42 +159,44 @@ export class VersionCommand extends Command { ); if (unsupportedNodeVersion) { - this.logger.warn( + logger.warn( `Warning: The current version of Node (${process.versions.node}) is not supported by Angular.`, ); } } - private getVersion(moduleName: string): string { + private getVersion( + moduleName: string, + workspaceRequire: NodeRequire, + localRequire: NodeRequire, + ): string { let packageInfo: PartialPackageInfo | undefined; let cliOnly = false; // Try to find the package in the workspace try { - packageInfo = this.workspaceRequire(`${moduleName}/package.json`); + packageInfo = workspaceRequire(`${moduleName}/package.json`); } catch {} // If not found, try to find within the CLI if (!packageInfo) { try { - packageInfo = this.localRequire(`${moduleName}/package.json`); + packageInfo = localRequire(`${moduleName}/package.json`); cliOnly = true; } catch {} } - let version: string | undefined; - // If found, attempt to get the version if (packageInfo) { try { - version = packageInfo.version + (cliOnly ? ' (cli-only)' : ''); + return packageInfo.version + (cliOnly ? ' (cli-only)' : ''); } catch {} } - return version || ''; + return ''; } - private async getPackageManager(): Promise { + private async getPackageManagerVersion(): Promise { try { const manager = await getPackageManager(this.context.root); const version = execSync(`${manager} --version`, { diff --git a/packages/angular/cli/lib/cli/command-runner.ts b/packages/angular/cli/lib/cli/command-runner.ts new file mode 100644 index 000000000000..11744fa7bc85 --- /dev/null +++ b/packages/angular/cli/lib/cli/command-runner.ts @@ -0,0 +1,150 @@ +/** + * @license + * Copyright Google LLC 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 { logging } from '@angular-devkit/core'; +import yargs from 'yargs'; +import { Parser } from 'yargs/helpers'; +import { AddCommandModule } from '../../commands/add/cli'; +import { AnalyticsCommandModule } from '../../commands/analytics/cli'; +import { BuildCommandModule } from '../../commands/build/cli'; +import { ConfigCommandModule } from '../../commands/config/cli'; +import { DeployCommandModule } from '../../commands/deploy/cli'; +import { DocCommandModule } from '../../commands/doc/cli'; +import { E2eCommandModule } from '../../commands/e2e/cli'; +import { ExtractI18nCommandModule } from '../../commands/extract-i18n/cli'; +import { GenerateCommandModule } from '../../commands/generate/cli'; +import { LintCommandModule } from '../../commands/lint/cli'; +import { AwesomeCommandModule } from '../../commands/make-this-awesome/cli'; +import { NewCommandModule } from '../../commands/new/cli'; +import { RunCommandModule } from '../../commands/run/cli'; +import { ServeCommandModule } from '../../commands/serve/cli'; +import { TestCommandModule } from '../../commands/test/cli'; +import { UpdateCommandModule } from '../../commands/update/cli'; +import { VersionCommandModule } from '../../commands/version/cli'; +import { colors } from '../../utilities/color'; +import { CommandContext, CommandModuleError } from '../../utilities/command-builder/command-module'; +import { AngularWorkspace } from '../../utilities/config'; + +const COMMANDS = [ + VersionCommandModule, + DocCommandModule, + AwesomeCommandModule, + ConfigCommandModule, + AnalyticsCommandModule, + AddCommandModule, + GenerateCommandModule, + BuildCommandModule, + E2eCommandModule, + TestCommandModule, + ServeCommandModule, + ExtractI18nCommandModule, + DeployCommandModule, + LintCommandModule, + NewCommandModule, + UpdateCommandModule, + RunCommandModule, +]; + +const yargsParser = Parser as unknown as typeof Parser.default & { + camelCase(str: string): string; +}; + +export async function runCommand( + args: string[], + logger: logging.Logger, + workspace: AngularWorkspace | undefined, +): Promise { + const { + $0, + _: positional, + help = false, + ...rest + } = yargsParser(args, { boolean: ['help'], alias: { 'collection': 'c' } }); + + const context: CommandContext = { + workspace, + logger, + currentDirectory: process.cwd(), + root: workspace?.basePath ?? process.cwd(), + args: { + positional: positional.map((v) => v.toString()), + options: { + help, + ...rest, + }, + }, + }; + + let localYargs = yargs(args); + for (const CommandModule of COMMANDS) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const commandModule = new CommandModule(context) as any; + + localYargs = localYargs.command({ + command: commandModule.command, + aliases: commandModule.aliases, + describe: commandModule.describe, + deprecated: commandModule.deprecated, + builder: (x) => commandModule.builder(x), + handler: ({ _, $0, ...options }) => { + // Camelize options as yargs will return the object in kebab-case when camel casing is disabled. + const camelCasedOptions: Record = {}; + for (const [key, value] of Object.entries(options)) { + camelCasedOptions[yargsParser.camelCase(key)] = value; + } + + return commandModule.handler(camelCasedOptions); + }, + }); + } + + await localYargs + .scriptName('ng') + // https://github.com/yargs/yargs/blob/main/docs/advanced.md#customizing-yargs-parser + .parserConfiguration({ + 'populate--': true, + 'unknown-options-as-args': false, + 'dot-notation': false, + 'boolean-negation': true, + 'strip-aliased': true, + 'strip-dashed': true, + 'camel-case-expansion': false, + }) + .option('json-help', { + describe: 'Show help in JSON format.', + implies: ['help'], + hidden: true, + type: 'boolean', + }) + .help('help', 'Shows a help message for this command in the console.') + // A complete list of strings can be found: https://github.com/yargs/yargs/blob/main/locales/en.json + .updateStrings({ + 'Commands:': colors.cyan('Commands:'), + 'Options:': colors.cyan('Options:'), + 'Positionals:': colors.cyan('Arguments:'), + 'deprecated': colors.yellow('deprecated'), + 'deprecated: %s': colors.yellow('deprecated:') + ' %s', + 'Did you mean %s?': 'Unknown command. Did you mean %s?', + }) + .demandCommand() + .recommendCommands() + .version(false) + .showHelpOnFail(false) + .strict() + .fail((msg, err) => { + throw msg + ? // Validation failed example: `Unknown argument:` + new CommandModuleError(msg) + : // Unknown exception, re-throw. + err; + }) + .wrap(yargs.terminalWidth()) + .parseAsync(); + + return process.exitCode ?? 0; +} diff --git a/packages/angular/cli/lib/cli/index.ts b/packages/angular/cli/lib/cli/index.ts index cc57f6c68107..3070ea229f89 100644 --- a/packages/angular/cli/lib/cli/index.ts +++ b/packages/angular/cli/lib/cli/index.ts @@ -6,15 +6,17 @@ * found in the LICENSE file at https://angular.io/license */ +import { schema } from '@angular-devkit/core'; import { createConsoleLogger } from '@angular-devkit/core/node'; import { format } from 'util'; -import { runCommand } from '../../models/command-runner'; import { colors, removeColor } from '../../utilities/color'; +import { CommandModuleError } from '../../utilities/command-builder/command-module'; import { AngularWorkspace, getWorkspaceRaw } from '../../utilities/config'; import { writeErrorToLogFile } from '../../utilities/log-file'; import { findWorkspaceFile } from '../../utilities/project'; +import { runCommand } from './command-runner'; -export { VERSION, Version } from '../../models/version'; +export { VERSION } from '../../utilities/version'; const debugEnv = process.env['NG_DEBUG']; const isDebug = debugEnv !== undefined && debugEnv !== '0' && debugEnv.toLowerCase() !== 'false'; @@ -75,16 +77,11 @@ export default async function (options: { testing?: boolean; cliArgs: string[] } } try { - const maybeExitCode = await runCommand(options.cliArgs, logger, workspace); - if (typeof maybeExitCode === 'number') { - console.assert(Number.isInteger(maybeExitCode)); - - return maybeExitCode; - } - - return 0; + return await runCommand(options.cliArgs, logger, workspace); } catch (err) { - if (err instanceof Error) { + if (err instanceof CommandModuleError || err instanceof schema.SchemaValidationException) { + logger.fatal(`Error: ${err.message}`); + } else if (err instanceof Error) { try { const logPath = writeErrorToLogFile(err); logger.fatal( diff --git a/packages/angular/cli/lib/init.ts b/packages/angular/cli/lib/init.ts index cf18b8bcd77b..5e6045a81229 100644 --- a/packages/angular/cli/lib/init.ts +++ b/packages/angular/cli/lib/init.ts @@ -11,9 +11,9 @@ import 'symbol-observable'; import { promises as fs } from 'fs'; import * as path from 'path'; import { SemVer } from 'semver'; -import { VERSION } from '../models/version'; import { colors } from '../utilities/color'; import { isWarningEnabled } from '../utilities/config'; +import { VERSION } from '../utilities/version'; (async () => { /** diff --git a/packages/angular/cli/models/analytics-collector.ts b/packages/angular/cli/models/analytics-collector.ts index 6754d5037059..5d0a8cf8c88e 100644 --- a/packages/angular/cli/models/analytics-collector.ts +++ b/packages/angular/cli/models/analytics-collector.ts @@ -12,7 +12,7 @@ import debug from 'debug'; import * as https from 'https'; import * as os from 'os'; import * as querystring from 'querystring'; -import { VERSION } from './version'; +import { VERSION } from '../utilities/version'; interface BaseParameters extends analytics.CustomDimensionsAndMetricsOptions { [key: string]: string | number | boolean | undefined | (string | number | boolean | undefined)[]; diff --git a/packages/angular/cli/models/analytics.ts b/packages/angular/cli/models/analytics.ts index 1171e801dc49..34febb2a9dbc 100644 --- a/packages/angular/cli/models/analytics.ts +++ b/packages/angular/cli/models/analytics.ts @@ -6,14 +6,14 @@ * found in the LICENSE file at https://angular.io/license */ -import { json, tags } from '@angular-devkit/core'; +import { analytics, json, tags } from '@angular-devkit/core'; import debug from 'debug'; import * as inquirer from 'inquirer'; import { v4 as uuidV4 } from 'uuid'; -import { VERSION } from '../models/version'; import { colors } from '../utilities/color'; import { getWorkspace, getWorkspaceRaw } from '../utilities/config'; import { isTTY } from '../utilities/tty'; +import { VERSION } from '../utilities/version'; import { AnalyticsCollector } from './analytics-collector'; /* eslint-disable no-console */ @@ -367,3 +367,37 @@ export async function getSharedAnalytics(): Promise { + let config = await getGlobalAnalytics(); + // If in workspace and global analytics is enabled, defer to workspace level + if (workspace && config) { + const skipAnalytics = + skipPrompt || + (process.env['NG_CLI_ANALYTICS'] && + (process.env['NG_CLI_ANALYTICS'].toLowerCase() === 'false' || + process.env['NG_CLI_ANALYTICS'] === '0')); + // TODO: This should honor the `no-interactive` option. + // It is currently not an `ng` option but rather only an option for specific commands. + // The concept of `ng`-wide options are needed to cleanly handle this. + if (!skipAnalytics && !(await hasWorkspaceAnalyticsConfiguration())) { + await promptProjectAnalytics(); + } + config = await getWorkspaceAnalytics(); + } + + const maybeSharedAnalytics = await getSharedAnalytics(); + + if (config && maybeSharedAnalytics) { + return new analytics.MultiAnalytics([config, maybeSharedAnalytics]); + } else if (config) { + return config; + } else if (maybeSharedAnalytics) { + return maybeSharedAnalytics; + } else { + return new analytics.NoopAnalytics(); + } +} diff --git a/packages/angular/cli/models/architect-command.ts b/packages/angular/cli/models/architect-command.ts deleted file mode 100644 index 5eaebe95e137..000000000000 --- a/packages/angular/cli/models/architect-command.ts +++ /dev/null @@ -1,434 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 { Architect, Target } from '@angular-devkit/architect'; -import { WorkspaceNodeModulesArchitectHost } from '@angular-devkit/architect/node'; -import { json, schema, tags } from '@angular-devkit/core'; -import { existsSync } from 'fs'; -import * as path from 'path'; -import { parseJsonSchemaToOptions } from '../utilities/json-schema'; -import { getPackageManager } from '../utilities/package-manager'; -import { isPackageNameSafeForAnalytics } from './analytics'; -import { BaseCommandOptions, Command } from './command'; -import { Arguments, Option } from './interface'; -import { parseArguments } from './parser'; - -export interface ArchitectCommandOptions extends BaseCommandOptions { - project?: string; - configuration?: string; - prod?: boolean; - target?: string; -} - -export abstract class ArchitectCommand< - T extends ArchitectCommandOptions = ArchitectCommandOptions, -> extends Command { - protected _architect!: Architect; - protected _architectHost!: WorkspaceNodeModulesArchitectHost; - protected _registry!: json.schema.SchemaRegistry; - protected override readonly useReportAnalytics = false; - - // If this command supports running multiple targets. - protected multiTarget = false; - - target: string | undefined; - missingTargetError: string | undefined; - - protected async onMissingTarget(projectName?: string): Promise { - if (this.missingTargetError) { - this.logger.fatal(this.missingTargetError); - - return 1; - } - - if (projectName) { - this.logger.fatal(`Project '${projectName}' does not support the '${this.target}' target.`); - } else { - this.logger.fatal(`No projects support the '${this.target}' target.`); - } - - return 1; - } - - // eslint-disable-next-line max-lines-per-function - public override async initialize(options: T & Arguments): Promise { - this._registry = new json.schema.CoreSchemaRegistry(); - this._registry.addPostTransform(json.schema.transforms.addUndefinedDefaults); - this._registry.useXDeprecatedProvider((msg) => this.logger.warn(msg)); - - if (!this.workspace) { - this.logger.fatal('A workspace is required for this command.'); - - return 1; - } - - this._architectHost = new WorkspaceNodeModulesArchitectHost( - this.workspace, - this.workspace.basePath, - ); - this._architect = new Architect(this._architectHost, this._registry); - - if (!this.target) { - if (options.help) { - // This is a special case where we just return. - return; - } - - const specifier = this._makeTargetSpecifier(options); - if (!specifier.project || !specifier.target) { - this.logger.fatal('Cannot determine project or target for command.'); - - return 1; - } - - return; - } - - let projectName = options.project; - if (projectName && !this.workspace.projects.has(projectName)) { - this.logger.fatal(`Project '${projectName}' does not exist.`); - - return 1; - } - - const commandLeftovers = options['--']; - const targetProjectNames: string[] = []; - for (const [name, project] of this.workspace.projects) { - if (project.targets.has(this.target)) { - targetProjectNames.push(name); - } - } - - if (projectName && !targetProjectNames.includes(projectName)) { - return await this.onMissingTarget(projectName); - } - - if (targetProjectNames.length === 0) { - return await this.onMissingTarget(); - } - - if (!projectName && commandLeftovers && commandLeftovers.length > 0) { - const builderNames = new Set(); - const leftoverMap = new Map(); - let potentialProjectNames = new Set(targetProjectNames); - for (const name of targetProjectNames) { - const builderName = await this._architectHost.getBuilderNameForTarget({ - project: name, - target: this.target, - }); - - if (this.multiTarget) { - builderNames.add(builderName); - } - - let builderDesc; - try { - builderDesc = await this._architectHost.resolveBuilder(builderName); - } catch (e) { - if (e.code === 'MODULE_NOT_FOUND') { - await this.warnOnMissingNodeModules(this.workspace.basePath); - this.logger.fatal(`Could not find the '${builderName}' builder's node package.`); - - return 1; - } - throw e; - } - - const optionDefs = await parseJsonSchemaToOptions( - this._registry, - builderDesc.optionSchema as json.JsonObject, - ); - const parsedOptions = parseArguments([...commandLeftovers], optionDefs); - const builderLeftovers = parsedOptions['--'] || []; - leftoverMap.set(name, { optionDefs, parsedOptions }); - - potentialProjectNames = new Set( - builderLeftovers.filter((x) => potentialProjectNames.has(x)), - ); - } - - if (potentialProjectNames.size === 1) { - projectName = [...potentialProjectNames][0]; - - // remove the project name from the leftovers - const optionInfo = leftoverMap.get(projectName); - if (optionInfo) { - const locations = []; - let i = 0; - while (i < commandLeftovers.length) { - i = commandLeftovers.indexOf(projectName, i + 1); - if (i === -1) { - break; - } - locations.push(i); - } - delete optionInfo.parsedOptions['--']; - for (const location of locations) { - const tempLeftovers = [...commandLeftovers]; - tempLeftovers.splice(location, 1); - const tempArgs = parseArguments([...tempLeftovers], optionInfo.optionDefs); - delete tempArgs['--']; - if (JSON.stringify(optionInfo.parsedOptions) === JSON.stringify(tempArgs)) { - options['--'] = tempLeftovers; - break; - } - } - } - } - - if (!projectName && this.multiTarget && builderNames.size > 1) { - this.logger.fatal(tags.oneLine` - Architect commands with command line overrides cannot target different builders. The - '${this.target}' target would run on projects ${targetProjectNames.join()} which have the - following builders: ${'\n ' + [...builderNames].join('\n ')} - `); - - return 1; - } - } - - if (!projectName && !this.multiTarget) { - const defaultProjectName = this.workspace.extensions['defaultProject'] as string; - if (targetProjectNames.length === 1) { - projectName = targetProjectNames[0]; - } else if (defaultProjectName && targetProjectNames.includes(defaultProjectName)) { - projectName = defaultProjectName; - } else if (options.help) { - // This is a special case where we just return. - return; - } else { - this.logger.fatal( - this.missingTargetError || 'Cannot determine project or target for command.', - ); - - return 1; - } - } - - options.project = projectName; - - const builderConf = await this._architectHost.getBuilderNameForTarget({ - project: projectName || (targetProjectNames.length > 0 ? targetProjectNames[0] : ''), - target: this.target, - }); - - let builderDesc; - try { - builderDesc = await this._architectHost.resolveBuilder(builderConf); - } catch (e) { - if (e.code === 'MODULE_NOT_FOUND') { - await this.warnOnMissingNodeModules(this.workspace.basePath); - this.logger.fatal(`Could not find the '${builderConf}' builder's node package.`); - - return 1; - } - throw e; - } - - this.description.options.push( - ...(await parseJsonSchemaToOptions( - this._registry, - builderDesc.optionSchema as json.JsonObject, - )), - ); - - // Update options to remove analytics from options if the builder isn't safelisted. - for (const o of this.description.options) { - if (o.userAnalytics && !isPackageNameSafeForAnalytics(builderConf)) { - o.userAnalytics = undefined; - } - } - } - - private async warnOnMissingNodeModules(basePath: string): Promise { - // Check for a `node_modules` directory (npm, yarn non-PnP, etc.) - if (existsSync(path.resolve(basePath, 'node_modules'))) { - return; - } - - // Check for yarn PnP files - if ( - existsSync(path.resolve(basePath, '.pnp.js')) || - existsSync(path.resolve(basePath, '.pnp.cjs')) || - existsSync(path.resolve(basePath, '.pnp.mjs')) - ) { - return; - } - - const packageManager = await getPackageManager(basePath); - this.logger.warn( - `Node packages may not be installed. Try installing with '${packageManager} install'.`, - ); - } - - async run(options: ArchitectCommandOptions & Arguments) { - return await this.runArchitectTarget(options); - } - - protected async runSingleTarget(target: Target, targetOptions: string[]) { - // 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 = await this._architectHost.getBuilderNameForTarget(target); - let builderDesc; - try { - builderDesc = await this._architectHost.resolveBuilder(builderConf); - } catch (e) { - if (e.code === 'MODULE_NOT_FOUND') { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - await this.warnOnMissingNodeModules(this.workspace!.basePath); - this.logger.fatal(`Could not find the '${builderConf}' builder's node package.`); - - return 1; - } - throw e; - } - const targetOptionArray = await parseJsonSchemaToOptions( - this._registry, - builderDesc.optionSchema as json.JsonObject, - ); - const overrides = parseArguments(targetOptions, targetOptionArray, this.logger); - - const allowAdditionalProperties = - typeof builderDesc.optionSchema === 'object' && builderDesc.optionSchema.additionalProperties; - - if (overrides['--'] && !allowAdditionalProperties) { - (overrides['--'] || []).forEach((additional) => { - this.logger.fatal(`Unknown option: '${additional.split(/=/)[0]}'`); - }); - - return 1; - } - - await this.reportAnalytics([this.description.name], { - ...((await this._architectHost.getOptionsForTarget(target)) as unknown as T), - ...overrides, - }); - - const run = await this._architect.scheduleTarget(target, overrides as json.JsonObject, { - logger: this.logger, - analytics: isPackageNameSafeForAnalytics(builderConf) ? this.analytics : undefined, - }); - - const { error, success } = await run.output.toPromise(); - await run.stop(); - - if (error) { - this.logger.error(error); - } - - return success ? 0 : 1; - } - - protected async runArchitectTarget( - options: ArchitectCommandOptions & Arguments, - ): Promise { - const extra = options['--'] || []; - - 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. - let result = 0; - for (const project of this.getProjectNamesByTarget(this.target)) { - result |= await this.runSingleTarget({ ...targetSpec, project } as Target, extra); - } - - return result; - } else { - return await this.runSingleTarget(targetSpec, extra); - } - } catch (e) { - if (e instanceof schema.SchemaValidationException) { - const newErrors: schema.SchemaValidatorError[] = []; - for (const schemaError of e.errors) { - if (schemaError.keyword === 'additionalProperties') { - const unknownProperty = schemaError.params?.additionalProperty; - if (unknownProperty in options) { - const dashes = unknownProperty.length === 1 ? '-' : '--'; - this.logger.fatal(`Unknown option: '${dashes}${unknownProperty}'`); - continue; - } - } - newErrors.push(schemaError); - } - - if (newErrors.length > 0) { - this.logger.error(new schema.SchemaValidationException(newErrors).message); - } - - return 1; - } else { - throw e; - } - } - } - - private getProjectNamesByTarget(targetName: string): string[] { - const allProjectsForTargetName: string[] = []; - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - for (const [name, project] of this.workspace!.projects) { - if (project.targets.has(targetName)) { - allProjectsForTargetName.push(name); - } - } - - if (this.multiTarget) { - // For multi target commands, we always list all projects that have the target. - return allProjectsForTargetName; - } else { - // For single target commands, we try the default project first, - // then the full list if it has a single project, then error out. - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const maybeDefaultProject = this.workspace!.extensions['defaultProject'] as string; - if (maybeDefaultProject && allProjectsForTargetName.includes(maybeDefaultProject)) { - return [maybeDefaultProject]; - } - - if (allProjectsForTargetName.length === 1) { - return allProjectsForTargetName; - } - - throw new Error(`Could not determine a single project for the '${targetName}' target.`); - } - } - - private _makeTargetSpecifier(commandOptions: ArchitectCommandOptions): Target { - let project, target, configuration; - - if (commandOptions.target) { - [project, target, configuration] = commandOptions.target.split(':'); - - if (commandOptions.configuration) { - configuration = commandOptions.configuration; - } - } else { - project = commandOptions.project; - target = this.target; - if (commandOptions.configuration) { - configuration = `${configuration ? `${configuration},` : ''}${ - commandOptions.configuration - }`; - } - } - - if (!project) { - project = ''; - } - if (!target) { - target = ''; - } - - return { - project, - configuration: configuration || '', - target, - }; - } -} diff --git a/packages/angular/cli/models/command-runner.ts b/packages/angular/cli/models/command-runner.ts deleted file mode 100644 index 0b8b01fe4baa..000000000000 --- a/packages/angular/cli/models/command-runner.ts +++ /dev/null @@ -1,273 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 { - analytics, - isJsonObject, - json, - logging, - schema, - strings, - tags, -} from '@angular-devkit/core'; -import { readFileSync } from 'fs'; -import { join, resolve } from 'path'; -import { AngularWorkspace } from '../utilities/config'; -import { readAndParseJson } from '../utilities/json-file'; -import { parseJsonSchemaToCommandDescription } from '../utilities/json-schema'; -import { - getGlobalAnalytics, - getSharedAnalytics, - getWorkspaceAnalytics, - hasWorkspaceAnalyticsConfiguration, - promptProjectAnalytics, -} from './analytics'; -import { Command } from './command'; -import { CommandDescription } from './interface'; -import * as parser from './parser'; - -// NOTE: Update commands.json if changing this. It's still deep imported in one CI validation -const standardCommands = { - 'add': '../commands/add.json', - 'analytics': '../commands/analytics.json', - 'build': '../commands/build.json', - 'deploy': '../commands/deploy.json', - 'config': '../commands/config.json', - 'doc': '../commands/doc.json', - 'e2e': '../commands/e2e.json', - 'extract-i18n': '../commands/extract-i18n.json', - 'make-this-awesome': '../commands/easter-egg.json', - 'generate': '../commands/generate.json', - 'help': '../commands/help.json', - 'lint': '../commands/lint.json', - 'new': '../commands/new.json', - 'run': '../commands/run.json', - 'serve': '../commands/serve.json', - 'test': '../commands/test.json', - 'update': '../commands/update.json', - 'version': '../commands/version.json', -}; - -export interface CommandMapOptions { - [key: string]: string; -} - -/** - * Create the analytics instance. - * @private - */ -async function _createAnalytics( - workspace: boolean, - skipPrompt = false, -): Promise { - let config = await getGlobalAnalytics(); - // If in workspace and global analytics is enabled, defer to workspace level - if (workspace && config) { - const skipAnalytics = - skipPrompt || - (process.env['NG_CLI_ANALYTICS'] && - (process.env['NG_CLI_ANALYTICS'].toLowerCase() === 'false' || - process.env['NG_CLI_ANALYTICS'] === '0')); - // TODO: This should honor the `no-interactive` option. - // It is currently not an `ng` option but rather only an option for specific commands. - // The concept of `ng`-wide options are needed to cleanly handle this. - if (!skipAnalytics && !(await hasWorkspaceAnalyticsConfiguration())) { - await promptProjectAnalytics(); - } - config = await getWorkspaceAnalytics(); - } - - const maybeSharedAnalytics = await getSharedAnalytics(); - - if (config && maybeSharedAnalytics) { - return new analytics.MultiAnalytics([config, maybeSharedAnalytics]); - } else if (config) { - return config; - } else if (maybeSharedAnalytics) { - return maybeSharedAnalytics; - } else { - return new analytics.NoopAnalytics(); - } -} - -async function loadCommandDescription( - name: string, - path: string, - registry: json.schema.CoreSchemaRegistry, -): Promise { - const schemaPath = resolve(__dirname, path); - const schema = readAndParseJson(schemaPath); - if (!isJsonObject(schema)) { - throw new Error('Invalid command JSON loaded from ' + JSON.stringify(schemaPath)); - } - - return parseJsonSchemaToCommandDescription(name, schemaPath, registry, schema); -} - -/** - * Run a command. - * @param args Raw unparsed arguments. - * @param logger The logger to use. - * @param workspace Workspace information. - * @param commands The map of supported commands. - * @param options Additional options. - */ -export async function runCommand( - args: string[], - logger: logging.Logger, - workspace: AngularWorkspace | undefined, - commands: CommandMapOptions = standardCommands, - options: { analytics?: analytics.Analytics; currentDirectory: string } = { - currentDirectory: process.cwd(), - }, -): Promise { - // 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 Promise.resolve(JSON.parse(content)); - } else { - return null; - } - }); - - let commandName: string | undefined = undefined; - for (let i = 0; i < args.length; i++) { - const arg = args[i]; - - if (!arg.startsWith('-')) { - commandName = arg; - args.splice(i, 1); - break; - } - } - - let description: CommandDescription | null = null; - - // if no commands were found, use `help`. - if (!commandName) { - if (args.length === 1 && args[0] === '--version') { - commandName = 'version'; - } else { - commandName = 'help'; - } - - if (!(commandName in commands)) { - logger.error(tags.stripIndent` - The "${commandName}" 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; - } - } - - if (commandName in commands) { - description = await loadCommandDescription(commandName, commands[commandName], registry); - } else { - const commandNames = Object.keys(commands); - - // Optimize loading for common aliases - if (commandName.length === 1) { - commandNames.sort((a, b) => { - const aMatch = a[0] === commandName; - const bMatch = b[0] === commandName; - if (aMatch && !bMatch) { - return -1; - } else if (!aMatch && bMatch) { - return 1; - } else { - return 0; - } - }); - } - - for (const name of commandNames) { - const aliasDesc = await loadCommandDescription(name, commands[name], registry); - const aliases = aliasDesc.aliases; - - if (aliases && aliases.some((alias) => alias === commandName)) { - commandName = name; - description = aliasDesc; - break; - } - } - } - - if (!description) { - const commandsDistance = {} as { [name: string]: number }; - const name = commandName; - const allCommands = Object.keys(commands).sort((a, b) => { - if (!(a in commandsDistance)) { - commandsDistance[a] = strings.levenshtein(a, name); - } - if (!(b in commandsDistance)) { - commandsDistance[b] = strings.levenshtein(b, name); - } - - 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]}"? - `); - - return 1; - } - - try { - const parsedOptions = parser.parseArguments(args, description.options, logger); - Command.setCommandMap(async () => { - const map: Record = {}; - for (const [name, path] of Object.entries(commands)) { - map[name] = await loadCommandDescription(name, path, registry); - } - - return map; - }); - - const analytics = - options.analytics || (await _createAnalytics(!!workspace, description.name === 'update')); - const context = { - workspace, - analytics, - currentDirectory: options.currentDirectory, - root: workspace?.basePath ?? options.currentDirectory, - }; - const command = new description.impl(context, description, logger); - - // Flush on an interval (if the event loop is waiting). - let analyticsFlushPromise = Promise.resolve(); - const analyticsFlushInterval = setInterval(() => { - analyticsFlushPromise = analyticsFlushPromise.then(() => analytics.flush()); - }, 1000); - - const result = await command.validateAndRun(parsedOptions); - - // Flush one last time. - clearInterval(analyticsFlushInterval); - await analyticsFlushPromise.then(() => analytics.flush()); - - return result; - } catch (e) { - if (e instanceof parser.ParseArgumentException) { - logger.fatal('Cannot parse arguments. See below for the reasons.'); - logger.fatal(' ' + e.comments.join('\n ')); - - return 1; - } else { - throw e; - } - } -} diff --git a/packages/angular/cli/models/command.ts b/packages/angular/cli/models/command.ts index d40b21620d98..d1d58013239c 100644 --- a/packages/angular/cli/models/command.ts +++ b/packages/angular/cli/models/command.ts @@ -6,148 +6,41 @@ * found in the LICENSE file at https://angular.io/license */ -import { analytics, logging, strings, tags } from '@angular-devkit/core'; -import { colors } from '../utilities/color'; +import { analytics, logging } from '@angular-devkit/core'; +import { Option } from '../utilities/command-builder/json-schema'; import { AngularWorkspace } from '../utilities/config'; -import { - Arguments, - CommandContext, - CommandDescription, - CommandDescriptionMap, - CommandScope, - Option, -} from './interface'; +import { CommandContext } from './interface'; export interface BaseCommandOptions { - help?: boolean | string; + jsonHelp?: boolean; } -export abstract class Command { +export abstract class Command { protected allowMissingWorkspace = false; protected useReportAnalytics = true; readonly workspace?: AngularWorkspace; - readonly analytics: analytics.Analytics; + protected readonly analytics: analytics.Analytics; + protected readonly commandOptions: Option[] = []; + protected readonly logger: logging.Logger; - protected static commandMap: () => Promise; - static setCommandMap(map: () => Promise) { - this.commandMap = map; - } - - constructor( - protected readonly context: CommandContext, - public readonly description: CommandDescription, - protected readonly logger: logging.Logger, - ) { + constructor(protected readonly context: CommandContext, protected readonly commandName: string) { this.workspace = context.workspace; + this.logger = context.logger; this.analytics = context.analytics || new analytics.NoopAnalytics(); } - async initialize(options: T & Arguments): Promise {} - - async printHelp(): Promise { - await this.printHelpUsage(); - await this.printHelpOptions(); - - return 0; - } - - async printJsonHelp(): Promise { - const replacer = (key: string, value: string) => - key === 'name' ? strings.dasherize(value) : value; - this.logger.info(JSON.stringify(this.description, replacer, 2)); - - return 0; - } - - 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 async printHelpOptions(options: Option[] = this.description.options) { - const args = options.filter((opt) => opt.positional !== undefined); - const opts = options.filter((opt) => opt.positional === undefined); - - const formatDescription = (description: string) => - ` ${description.replace(/\n/g, '\n ')}`; - - if (args.length > 0) { - this.logger.info(`arguments:`); - args.forEach((o) => { - this.logger.info(` ${colors.cyan(o.name)}`); - if (o.description) { - this.logger.info(formatDescription(o.description)); - } - }); - } - 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.localeCompare(b.name)) - .forEach((o) => { - const aliases = - o.aliases && o.aliases.length > 0 - ? '(' + o.aliases.map((a) => `-${a}`).join(' ') + ')' - : ''; - this.logger.info(` ${colors.cyan('--' + strings.dasherize(o.name))} ${aliases}`); - if (o.description) { - this.logger.info(formatDescription(o.description)); - } - }); - } - } - - async validateScope(scope?: CommandScope): Promise { - switch (scope === undefined ? this.description.scope : scope) { - case CommandScope.OutProject: - if (this.workspace) { - 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.workspace.filePath}". - `); - // eslint-disable-next-line no-throw-literal - throw 1; - } - break; - case CommandScope.InProject: - if (!this.workspace) { - 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. - `); - // eslint-disable-next-line no-throw-literal - throw 1; - } - break; - case CommandScope.Everywhere: - // Can't miss this. - break; - } - } + async initialize(options: T): Promise {} async reportAnalytics( paths: string[], - options: Arguments, + options: T, dimensions: (boolean | number | string)[] = [], metrics: (boolean | number | string)[] = [], ): Promise { - for (const option of this.description.options) { + for (const option of this.commandOptions) { const ua = option.userAnalytics; - const v = options[option.name]; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const v = (options as any)[option.name]; if (v !== undefined && !Array.isArray(v) && ua) { dimensions[ua] = v; @@ -157,32 +50,23 @@ export abstract class Command this.analytics.pageview('/command/' + paths.join('/'), { dimensions, metrics }); } - abstract run(options: T & Arguments): Promise; + abstract run(options: T): Promise; - async validateAndRun(options: T & Arguments): Promise { - if (!(options.help === true || options.help === 'json' || options.help === 'JSON')) { - await this.validateScope(); - } + async validateAndRun(options: T): Promise { let result = await this.initialize(options); if (typeof result === 'number' && result !== 0) { return result; } - if (options.help === true) { - return this.printHelp(); - } else if (options.help === 'json' || options.help === 'JSON') { - return this.printJsonHelp(); - } else { - const startTime = +new Date(); - if (this.useReportAnalytics) { - await this.reportAnalytics([this.description.name], options); - } - result = await this.run(options); - const endTime = +new Date(); + const startTime = +new Date(); + if (this.useReportAnalytics) { + await this.reportAnalytics([this.commandName], options); + } + result = await this.run(options); + const endTime = +new Date(); - this.analytics.timing(this.description.name, 'duration', endTime - startTime); + this.analytics.timing(this.commandName, 'duration', endTime - startTime); - return result; - } + return result; } } diff --git a/packages/angular/cli/models/interface.ts b/packages/angular/cli/models/interface.ts index 9c908d913247..652f1279df60 100644 --- a/packages/angular/cli/models/interface.ts +++ b/packages/angular/cli/models/interface.ts @@ -6,46 +6,9 @@ * found in the LICENSE file at https://angular.io/license */ -import { analytics, json, logging } from '@angular-devkit/core'; +import { analytics, logging } from '@angular-devkit/core'; import { AngularWorkspace } from '../utilities/config'; -/** - * Value type of arguments. - */ -export type Value = number | string | boolean | (number | string | boolean)[]; - -/** - * 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[]; -} - -/** - * The base interface for Command, understood by the command runner. - */ -export interface CommandInterface { - printHelp(options: T): Promise; - printJsonHelp(options: T): Promise; - validateAndRun(options: T): Promise; -} - -/** - * Command constructor. - */ -export interface CommandConstructor { - new ( - context: CommandContext, - description: CommandDescription, - logger: logging.Logger, - ): CommandInterface; -} - /** * A command runner context. */ @@ -57,183 +20,6 @@ export interface CommandContext { // This property is optional for backward compatibility. analytics?: analytics.Analytics; -} - -/** - * Value types of an Option. - */ -export enum OptionType { - Any = 'any', - Array = 'array', - Boolean = 'boolean', - Number = 'number', - String = 'string', -} - -/** - * 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; - - /** - * The type of option value. If multiple types exist, this type will be the first one, and the - * types array will contain all types accepted. - */ - type: OptionType; - - /** - * {@see type} - */ - types?: OptionType[]; - - /** - * If this field is set, only values contained in this field are valid. This array can be mixed - * types (strings, numbers, boolean). For example, if this field is "enum: ['hello', true]", - * then "type" will be either string or boolean, types will be at least both, and the values - * accepted will only be either 'hello' or true (not false or any other string). - * This mean that prefixing with `no-` will not work on this field. - */ - enum?: Value[]; - - /** - * If this option maps to a subcommand in the parent command, will contain all the subcommands - * supported. There is a maximum of 1 subcommand Option per command, and the type of this - * option will always be "string" (no other types). The value of this option will map into - * this map and return the extra information. - */ - subcommands?: { - [name: string]: SubCommandDescription; - }; - - /** - * Aliases supported by this option. - */ - aliases: string[]; - - /** - * Whether this option is required or not. - */ - required?: boolean; - - /** - * Format field of this option. - */ - format?: string; - - /** - * Whether this option should be hidden from the help output. It will still show up in JSON help. - */ - hidden?: boolean; - - /** - * Default value of this option. - */ - default?: string | number | boolean; - - /** - * If this option can be used as an argument, the position of the argument. Otherwise omitted. - */ - positional?: number; - - /** - * Smart default object. - */ - $default?: OptionSmartDefault; - - /** - * Whether or not to report this option to the Angular Team, and which custom field to use. - * If this is falsey, do not report this option. - */ - userAnalytics?: number; - - /** - * Deprecation. If this flag is not false a warning will be shown on the console. Either `true` - * or a string to show the user as a notice. - */ - deprecated?: boolean | string; -} - -/** - * Scope of the command. - */ -export enum CommandScope { - InProject = 'in', - OutProject = 'out', - Everywhere = 'all', - - Default = InProject, -} - -/** - * A description of a command and its options. - */ -export interface SubCommandDescription { - /** - * The name of the subcommand. - */ - name: string; - - /** - * Short description (1-2 lines) of this sub command. - */ - description: string; - - /** - * A long description of the sub command, in Markdown format. - */ - longDescription?: string; - - /** - * Additional notes about usage of this sub command, in Markdown format. - */ - usageNotes?: string; - - /** - * List of all supported options. - */ - options: Option[]; - - /** - * Aliases supported for this sub command. - */ - aliases: string[]; -} - -/** - * A description of a command, its metadata. - */ -export interface CommandDescription extends SubCommandDescription { - /** - * Scope of the command, whether it can be executed in a project, outside of a project or - * anywhere. - */ - scope: CommandScope; - - /** - * Whether this command should be hidden from a list of all commands. - */ - hidden: boolean; - - /** - * The constructor of the command, which should be extending the abstract Command<> class. - */ - impl: CommandConstructor; -} - -export interface OptionSmartDefault { - $source: string; - [key: string]: json.JsonValue; -} -export interface CommandDescriptionMap { - [key: string]: CommandDescription; + logger: logging.Logger; } diff --git a/packages/angular/cli/models/parser.ts b/packages/angular/cli/models/parser.ts deleted file mode 100644 index b1e98d0b3f2a..000000000000 --- a/packages/angular/cli/models/parser.ts +++ /dev/null @@ -1,405 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 { BaseException, logging, strings } from '@angular-devkit/core'; -import { Arguments, Option, OptionType, Value } from './interface'; - -export class ParseArgumentException extends BaseException { - constructor( - public readonly comments: string[], - public readonly parsed: Arguments, - public readonly ignored: string[], - ) { - super(`One or more errors occurred while parsing arguments:\n ${comments.join('\n ')}`); - } -} - -function _coerceType(str: string | undefined, type: OptionType, v?: Value): Value | undefined { - switch (type) { - case OptionType.Any: - if (Array.isArray(v)) { - return v.concat(str || ''); - } - - 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 OptionType.String: - return str || ''; - - case OptionType.Boolean: - switch (str) { - case 'false': - return false; - - case undefined: - case '': - case 'true': - return true; - - default: - return undefined; - } - - case OptionType.Number: - if (str === undefined) { - return 0; - } else if (str === '') { - return undefined; - } else if (Number.isFinite(+str)) { - return +str; - } else { - return undefined; - } - - case OptionType.Array: - return Array.isArray(v) - ? v.concat(str || '') - : v === undefined - ? [str || ''] - : [v + '', str || '']; - - default: - return undefined; - } -} - -function _coerce(str: string | undefined, o: Option | null, v?: Value): Value | undefined { - if (!o) { - return _coerceType(str, OptionType.Any, v); - } else { - const types = o.types || [o.type]; - - // Try all the types one by one and pick the first one that returns a value contained in the - // enum. If there's no enum, just return the first one that matches. - for (const type of types) { - const maybeResult = _coerceType(str, type, v); - if (maybeResult !== undefined && (!o.enum || o.enum.includes(maybeResult))) { - return maybeResult; - } - } - - return undefined; - } -} - -function _getOptionFromName(name: string, options: Option[]): Option | undefined { - const camelName = /(-|_)/.test(name) ? strings.camelize(name) : name; - - for (const option of options) { - if (option.name === name || option.name === camelName) { - return option; - } - - if (option.aliases.some((x) => x === name || x === camelName)) { - return option; - } - } - - return undefined; -} - -function _removeLeadingDashes(key: string): string { - const from = key.startsWith('--') ? 2 : key.startsWith('-') ? 1 : 0; - - return key.substr(from); -} - -function _assignOption( - arg: string, - nextArg: string | undefined, - { - options, - parsedOptions, - leftovers, - ignored, - errors, - warnings, - }: { - options: Option[]; - parsedOptions: Arguments; - positionals: string[]; - leftovers: string[]; - ignored: string[]; - errors: string[]; - warnings: string[]; - }, -) { - const from = arg.startsWith('--') ? 2 : 1; - let consumedNextArg = false; - let key = arg.substr(from); - let option: Option | null = null; - let value: string | undefined = ''; - 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 from = key.startsWith('no-') ? 3 : 2; - const maybeOption = _getOptionFromName(strings.camelize(key.substr(from)), 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) { - value = nextArg; - let shouldShift = true; - - if (value && value.startsWith('-') && _coerce(undefined, maybeOption) !== undefined) { - // Verify if not having a value results in a correct parse, if so don't shift. - shouldShift = false; - } - - // Only absorb it if it leads to a better value. - if (shouldShift && _coerce(value, maybeOption) !== undefined) { - consumedNextArg = true; - } else { - value = ''; - } - option = maybeOption; - } - } - } else { - key = arg.substring(0, i); - option = _getOptionFromName(_removeLeadingDashes(key), options) || null; - if (option) { - value = arg.substring(i + 1); - } - } - - if (option === null) { - if (nextArg && !nextArg.startsWith('-')) { - leftovers.push(arg, nextArg); - consumedNextArg = true; - } else { - leftovers.push(arg); - } - } else { - const v = _coerce(value, option, parsedOptions[option.name]); - if (v !== undefined) { - if (parsedOptions[option.name] !== v) { - if (parsedOptions[option.name] !== undefined && option.type !== OptionType.Array) { - warnings.push( - `Option ${JSON.stringify(option.name)} was already specified with value ` + - `${JSON.stringify(parsedOptions[option.name])}. The new value ${JSON.stringify(v)} ` + - `will override it.`, - ); - } - - parsedOptions[option.name] = v; - } - } else { - let error = `Argument ${key} could not be parsed using value ${JSON.stringify(value)}.`; - if (option.enum) { - error += ` Valid values are: ${option.enum.map((x) => JSON.stringify(x)).join(', ')}.`; - } else { - error += `Valid type(s) is: ${(option.types || [option.type]).join(', ')}`; - } - - errors.push(error); - ignored.push(arg); - } - - if (/^[a-z]+[A-Z]/.test(key)) { - warnings.push( - 'Support for camel case arguments has been deprecated and will be removed in a future major version.\n' + - `Use '--${strings.dasherize(key)}' instead of '--${key}'.`, - ); - } - } - - return consumedNextArg; -} - -/** - * 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); - } - } - - if (leftovers.length) { - 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}. - * @param logger Logger to use to warn users. - * @returns An object that contains a property per option. - */ -export function parseArguments( - args: string[], - options: Option[] | null, - logger?: logging.Logger, -): Arguments { - if (options === null) { - options = []; - } - - const leftovers: string[] = []; - const positionals: string[] = []; - const parsedOptions: Arguments = {}; - - const ignored: string[] = []; - const errors: string[] = []; - const warnings: string[] = []; - - const state = { options, parsedOptions, positionals, leftovers, ignored, errors, warnings }; - - for (let argIndex = 0; argIndex < args.length; argIndex++) { - const arg = args[argIndex]; - let consumedNextArg = false; - - if (arg == '--') { - // If we find a --, we're done. - leftovers.push(...args.slice(argIndex + 1)); - break; - } - - if (arg.startsWith('--')) { - consumedNextArg = _assignOption(arg, args[argIndex + 1], state); - } 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]; - // If the next character is an '=', treat it as a long flag. - if (arg[i + 1] == '=') { - const f = '-' + flag + arg.slice(i + 1); - consumedNextArg = _assignOption(f, args[argIndex + 1], state); - break; - } - // 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) { - const arg = '-' + flag; - consumedNextArg = _assignOption(arg, args[argIndex + 1], state); - } 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); - } - - if (consumedNextArg) { - argIndex++; - } - } - - // Deal with positionals. - // TODO(hansl): this is by far the most complex piece of code in this file. Try to refactor it - // simpler. - 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, AND fit the type, we need to remove it. - if (option.positional === pos) { - const coercedValue = _coerce(positionals[i], option, parsedOptions[option.name]); - if (parsedOptions[option.name] === undefined && coercedValue !== undefined) { - parsedOptions[option.name] = coercedValue; - 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]; - } - - if (warnings.length > 0 && logger) { - warnings.forEach((message) => logger.warn(message)); - } - - if (errors.length > 0) { - throw new ParseArgumentException(errors, parsedOptions, ignored); - } - - return parsedOptions; -} diff --git a/packages/angular/cli/models/parser_spec.ts b/packages/angular/cli/models/parser_spec.ts deleted file mode 100644 index 1f543d8d560e..000000000000 --- a/packages/angular/cli/models/parser_spec.ts +++ /dev/null @@ -1,226 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 { logging } from '@angular-devkit/core'; -import { Arguments, Option, OptionType } from './interface'; -import { ParseArgumentException, 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: 'strUpper', 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: '' }, - { name: 'p3', positional: 2, aliases: [], type: OptionType.Number, description: '' }, - { - name: 't1', - aliases: [], - type: OptionType.Boolean, - types: [OptionType.Boolean, OptionType.String], - description: '', - }, - { - name: 't2', - aliases: [], - type: OptionType.Boolean, - types: [OptionType.Boolean, OptionType.Number], - description: '', - }, - { - name: 't3', - aliases: [], - type: OptionType.Number, - types: [OptionType.Number, OptionType.Any], - description: '', - }, - { name: 'e1', aliases: [], type: OptionType.String, enum: ['hello', 'world'], description: '' }, - { name: 'e2', aliases: [], type: OptionType.String, enum: ['hello', ''], description: '' }, - { - name: 'e3', - aliases: [], - type: OptionType.Boolean, - types: [OptionType.Boolean, OptionType.String], - enum: ['json', true, false], - description: '', - }, - ]; - - const tests: { [test: string]: Partial | ['!!!', Partial, string[]] } = { - '-S=b': { strUpper: 'b' }, - '--bool': { bool: true }, - '--bool=1': ['!!!', {}, ['--bool=1']], - '--bool ': { bool: true, p1: '' }, - '-- --bool=1': { '--': ['--bool=1'] }, - '--bool=yellow': ['!!!', {}, ['--bool=yellow']], - '--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 }, ['--num=true']], - '-- --bool --num=true': { '--': ['--bool', '--num=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'] }, - '--bool --bool=false': { bool: false }, - '--bool --bool=false --bool': { bool: true }, - '--num=1 --num=2 --num=3': { num: 3 }, - '--str=1 --str=2 --str=3': { str: '3' }, - '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, p1: 'val1', p2: 'val2', '--': ['--etc', '--v'] }, - ['--num'], - ], - '--bool val1 --etc --num=1 val2 --v': { - bool: true, - num: 1, - p1: 'val1', - p2: 'val2', - '--': ['--etc', '--v'], - }, - '--arr=a d': { arr: ['a'], p1: 'd' }, - '--arr=a --arr=b --arr c d': { arr: ['a', 'b', 'c'], p1: 'd' }, - '--arr=1 --arr --arr c d': { arr: ['1', '', 'c'], p1: 'd' }, - '--arr=1 --arr --arr c d e': { arr: ['1', '', 'c'], p1: 'd', p2: 'e' }, - '--str=1': { str: '1' }, - '--str=': { str: '' }, - '--str ': { str: '' }, - '--str ': { str: '', p1: '' }, - '--str ': { str: '', p1: '', p2: '', '--': [''] }, - '--hello-world=1': { helloWorld: '1' }, - '--hello-bool': { helloBool: true }, - '--helloBool': { helloBool: true }, - '--no-helloBool': { helloBool: false }, - '--noHelloBool': { helloBool: false }, - '--noBool': { bool: false }, - '-b': { bool: true }, - '-b=true': { bool: true }, - '-sb': { bool: true, str: '' }, - '-s=b': { str: 'b' }, - '-bs': { bool: true, str: '' }, - '--t1=true': { t1: true }, - '--t1': { t1: true }, - '--t1 --num': { t1: true, num: 0 }, - '--no-t1': { t1: false }, - '--t1=yellow': { t1: 'yellow' }, - '--no-t1=true': { '--': ['--no-t1=true'] }, - '--t1=123': { t1: '123' }, - '--t2=true': { t2: true }, - '--t2': { t2: true }, - '--no-t2': { t2: false }, - '--t2=yellow': ['!!!', {}, ['--t2=yellow']], - '--no-t2=true': { '--': ['--no-t2=true'] }, - '--t2=123': { t2: 123 }, - '--t3=a': { t3: 'a' }, - '--t3': { t3: 0 }, - '--t3 true': { t3: true }, - '--e1 hello': { e1: 'hello' }, - '--e1=hello': { e1: 'hello' }, - '--e1 yellow': ['!!!', { p1: 'yellow' }, ['--e1']], - '--e1=yellow': ['!!!', {}, ['--e1=yellow']], - '--e1': ['!!!', {}, ['--e1']], - '--e1 true': ['!!!', { p1: 'true' }, ['--e1']], - '--e1=true': ['!!!', {}, ['--e1=true']], - '--e2 hello': { e2: 'hello' }, - '--e2=hello': { e2: 'hello' }, - '--e2 yellow': { p1: 'yellow', e2: '' }, - '--e2=yellow': ['!!!', {}, ['--e2=yellow']], - '--e2': { e2: '' }, - '--e2 true': { p1: 'true', e2: '' }, - '--e2=true': ['!!!', {}, ['--e2=true']], - '--e3 json': { e3: 'json' }, - '--e3=json': { e3: 'json' }, - '--e3 yellow': { p1: 'yellow', e3: true }, - '--e3=yellow': ['!!!', {}, ['--e3=yellow']], - '--e3': { e3: true }, - '--e3 true': { e3: true }, - '--e3=true': { e3: true }, - 'a b c 1': { p1: 'a', p2: 'b', '--': ['c', '1'] }, - - '-p=1 -c=prod': { '--': ['-p=1', '-c=prod'] }, - '--p --c': { '--': ['--p', '--c'] }, - '--p=123': { '--': ['--p=123'] }, - '--p -c': { '--': ['--p', '-c'] }, - '-p --c': { '--': ['-p', '--c'] }, - '-p --c 123': { '--': ['-p', '--c', '123'] }, - '--c 123 -p': { '--': ['--c', '123', '-p'] }, - }; - - Object.entries(tests).forEach(([str, expected]) => { - it(`works for ${str}`, () => { - try { - const originalArgs = str.split(' '); - const args = originalArgs.slice(); - - const actual = parseArguments(args, options); - - expect(Array.isArray(expected)).toBe(false); - expect(actual).toEqual(expected as Arguments); - expect(args).toEqual(originalArgs); - } catch (e) { - if (!(e instanceof ParseArgumentException)) { - throw e; - } - - // The expected values are an array. - expect(Array.isArray(expected)).toBe(true); - expect(e.parsed).toEqual(expected[1] as Arguments); - expect(e.ignored).toEqual(expected[2] as string[]); - } - }); - }); - - it('handles a flag being added multiple times', () => { - const options = [{ name: 'bool', aliases: [], type: OptionType.Boolean, description: '' }]; - - const logger = new logging.Logger(''); - const messages: string[] = []; - - logger.subscribe((entry) => messages.push(entry.message)); - - let result = parseArguments(['--bool'], options, logger); - expect(result).toEqual({ bool: true }); - expect(messages).toEqual([]); - - result = parseArguments(['--bool', '--bool'], options, logger); - expect(result).toEqual({ bool: true }); - expect(messages).toEqual([]); - - result = parseArguments(['--bool', '--bool=false'], options, logger); - expect(result).toEqual({ bool: false }); - expect(messages.length).toEqual(1); - expect(messages[0]).toMatch(/\bbool\b.*\btrue\b.*\bfalse\b/); - messages.shift(); - - result = parseArguments(['--bool', '--bool=false', '--bool=false'], options, logger); - expect(result).toEqual({ bool: false }); - expect(messages.length).toEqual(1); - expect(messages[0]).toMatch(/\bbool\b.*\btrue\b.*\bfalse\b/); - messages.shift(); - }); -}); diff --git a/packages/angular/cli/models/schematic-command.ts b/packages/angular/cli/models/schematic-command.ts index 884ba71f7d9d..8e8da4a49ce6 100644 --- a/packages/angular/cli/models/schematic-command.ts +++ b/packages/angular/cli/models/schematic-command.ts @@ -6,13 +6,8 @@ * found in the LICENSE file at https://angular.io/license */ -import { logging, normalize, schema, strings, tags, workspaces } from '@angular-devkit/core'; -import { - DryRunEvent, - UnsuccessfulWorkflowExecution, - formats, - workflow, -} from '@angular-devkit/schematics'; +import { schema, tags, workspaces } from '@angular-devkit/core'; +import { DryRunEvent, UnsuccessfulWorkflowExecution, formats } from '@angular-devkit/schematics'; import { FileSystemCollection, FileSystemEngine, @@ -22,14 +17,13 @@ import { import * as inquirer from 'inquirer'; import * as systemPath from 'path'; import { colors } from '../utilities/color'; +import { parseJsonSchemaToOptions } from '../utilities/command-builder/json-schema'; import { getProjectByCwd, getSchematicDefaults, getWorkspace } from '../utilities/config'; -import { parseJsonSchemaToOptions } from '../utilities/json-schema'; import { ensureCompatibleNpm, getPackageManager } from '../utilities/package-manager'; import { isTTY } from '../utilities/tty'; import { isPackageNameSafeForAnalytics } from './analytics'; import { BaseCommandOptions, Command } from './command'; -import { Arguments, CommandContext, CommandDescription, Option } from './interface'; -import { parseArguments, parseFreeFormArguments } from './parser'; +import { CommandContext } from './interface'; import { SchematicEngineHost } from './schematic-engine-host'; export interface BaseSchematicSchema { @@ -38,14 +32,13 @@ export interface BaseSchematicSchema { force?: boolean; interactive?: boolean; defaults?: boolean; - packageRegistry?: string; + registry?: string; } export interface RunSchematicOptions extends BaseSchematicSchema { collectionName: string; schematicName: string; - additionalOptions?: { [key: string]: {} }; - schematicOptions?: string[]; + schematicOptions?: Record; showNothingDone?: boolean; } @@ -66,12 +59,12 @@ export abstract class SchematicCommand< protected collectionName = this.defaultCollectionName; protected schematicName?: string; - constructor(context: CommandContext, description: CommandDescription, logger: logging.Logger) { - super(context, description, logger); + constructor(context: CommandContext, commandName: string) { + super(context, commandName); } - public override async initialize(options: T & Arguments) { - await this.createWorkflow(options); + public override async initialize(options: T): Promise { + this._workflow = await this.createWorkflow(options); if (this.schematicName) { // Set the options. @@ -82,11 +75,10 @@ export abstract class SchematicCommand< schematic.description.schemaJson || {}, ); - this.description.description = schematic.description.description; - this.description.options.push(...options.filter((x) => !x.hidden)); + this.commandOptions.push(...options); // Remove any user analytics from schematics that are NOT part of our safelist. - for (const o of this.description.options) { + for (const o of this.commandOptions) { if (o.userAnalytics && !isPackageNameSafeForAnalytics(this.collectionName)) { o.userAnalytics = undefined; } @@ -94,88 +86,6 @@ export abstract class SchematicCommand< } } - public override async printHelp() { - await super.printHelp(); - this.logger.info(''); - - const subCommandOption = this.description.options.filter((x) => x.subcommands)[0]; - - if (!subCommandOption || !subCommandOption.subcommands) { - return 0; - } - - const schematicNames = Object.keys(subCommandOption.subcommands); - - if (schematicNames.length > 1) { - this.logger.info('Available Schematics:'); - - const namesPerCollection: { [c: string]: string[] } = {}; - schematicNames.forEach((name) => { - let [collectionName, schematicName] = name.split(/:/, 2); - if (!schematicName) { - schematicName = collectionName; - collectionName = this.collectionName; - } - - if (!namesPerCollection[collectionName]) { - namesPerCollection[collectionName] = []; - } - - namesPerCollection[collectionName].push(schematicName); - }); - - const defaultCollection = await 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}`); - }); - }); - } - - return 0; - } - - override async printHelpUsage() { - const subCommandOption = this.description.options.filter((x) => x.subcommands)[0]; - - if (!subCommandOption || !subCommandOption.subcommands) { - return; - } - - const schematicNames = Object.keys(subCommandOption.subcommands); - if (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 == (await this.getDefaultSchematicCollection()) - ? schematicName - : schematicNames[0]; - - const schematicOptions = subCommandOption.subcommands[schematicNames[0]].options; - 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 getEngine(): FileSystemEngine { return this._workflow.engine; } @@ -199,25 +109,10 @@ export abstract class SchematicCommand< return collection.createSchematic(schematicName, allowPrivate); } - protected setPathOptions(options: Option[], workingDir: string) { - if (workingDir === '') { - return {}; - } - - return options - .filter((o) => o.format === 'path') - .map((o) => o.name) - .reduce((acc, curr) => { - acc[curr] = workingDir; - - return acc; - }, {} as { [name: string]: string }); - } - /* * Runtime hook to allow specifying customized workflow */ - protected async createWorkflow(options: BaseSchematicSchema): Promise { + protected async createWorkflow(options: BaseSchematicSchema): Promise { if (this._workflow) { return this._workflow; } @@ -228,7 +123,7 @@ export abstract class SchematicCommand< force, dryRun, packageManager: await getPackageManager(root), - packageRegistry: options.packageRegistry, + packageRegistry: options.registry, // A schema registry is required to allow customizing addUndefinedDefaults registry: new schema.CoreSchemaRegistry(formats.standardFormats), resolvePaths: this.workspace @@ -294,7 +189,8 @@ export abstract class SchematicCommand< workflow.engineHost.registerOptionsTransform(async (_, options) => { if (shouldReportAnalytics) { shouldReportAnalytics = false; - await this.reportAnalytics([this.description.name], options as Arguments); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + await this.reportAnalytics([this.commandName], options as any); } return options; @@ -403,7 +299,7 @@ export abstract class SchematicCommand< } protected async runSchematic(options: RunSchematicOptions) { - const { schematicOptions, debug, dryRun } = options; + const { schematicOptions: input = {}, debug, dryRun } = options; let { collectionName, schematicName } = options; let nothingDone = true; @@ -412,8 +308,6 @@ export abstract class SchematicCommand< const workflow = this._workflow; - const workingDir = normalize(systemPath.relative(this.context.root, process.cwd())); - // Get the option object from the schematic schema. const schematic = this.getSchematic( this.getCollection(collectionName), @@ -425,36 +319,6 @@ export abstract class SchematicCommand< 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 allowAdditionalProperties = - typeof schematic.description.schemaJson === 'object' && - schematic.description.schemaJson.additionalProperties; - - if (args['--'] && !allowAdditionalProperties) { - args['--'].forEach((additional) => { - this.logger.fatal(`Unknown option: '${additional.split(/=/)[0]}'`); - }); - - return 1; - } - - const pathOptions = o ? this.setPathOptions(o, workingDir) : {}; - const input = { - ...pathOptions, - ...args, - ...options.additionalOptions, - }; - workflow.reporter.subscribe((event: DryRunEvent) => { nothingDone = false; @@ -481,7 +345,7 @@ export abstract class SchematicCommand< loggingQueue.push(`${colors.yellow('DELETE')} ${eventPath}`); break; case 'rename': - const eventToPath = event.to.startsWith('/') ? event.to.substr(1) : event.to; + const eventToPath = event.to.startsWith('/') ? event.to.substring(1) : event.to; loggingQueue.push(`${colors.blue('RENAME')} ${eventPath} => ${eventToPath}`); break; } @@ -546,17 +410,6 @@ export abstract class SchematicCommand< }); }); } - - protected async parseFreeFormArguments(schematicOptions: string[]) { - return parseFreeFormArguments(schematicOptions); - } - - protected async parseArguments( - schematicOptions: string[], - options: Option[] | null, - ): Promise { - return parseArguments(schematicOptions, options, this.logger); - } } function getProjectsByPath( diff --git a/packages/angular/cli/package.json b/packages/angular/cli/package.json index 9763f2e18a62..b25577341b17 100644 --- a/packages/angular/cli/package.json +++ b/packages/angular/cli/package.json @@ -43,7 +43,8 @@ "resolve": "1.22.0", "semver": "7.3.5", "symbol-observable": "4.0.0", - "uuid": "8.3.2" + "uuid": "8.3.2", + "yargs": "17.3.1" }, "devDependencies": { "rxjs": "6.6.7" diff --git a/packages/angular/cli/utilities/command-builder/architect-command-module.ts b/packages/angular/cli/utilities/command-builder/architect-command-module.ts new file mode 100644 index 000000000000..744618faaf83 --- /dev/null +++ b/packages/angular/cli/utilities/command-builder/architect-command-module.ts @@ -0,0 +1,296 @@ +/** + * @license + * Copyright Google LLC 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 { Architect, Target } from '@angular-devkit/architect'; +import { WorkspaceNodeModulesArchitectHost } from '@angular-devkit/architect/node'; +import { json } from '@angular-devkit/core'; +import { existsSync } from 'fs'; +import { resolve } from 'path'; +import { Argv } from 'yargs'; +import { isPackageNameSafeForAnalytics } from '../../models/analytics'; +import { getPackageManager } from '../package-manager'; +import { + CommandContext, + CommandModule, + CommandModuleError, + CommandModuleImplementation, + CommandScope, + Options, + OtherOptions, +} from './command-module'; +import { Option, parseJsonSchemaToOptions } from './json-schema'; + +export interface ArchitectCommandArgs { + configuration?: string; + project?: string; +} + +export abstract class ArchitectCommandModule + extends CommandModule + implements CommandModuleImplementation +{ + static override scope = CommandScope.In; + abstract readonly multiTarget: boolean; + readonly missingErrorTarget: string | undefined; + protected override shouldReportAnalytics = false; + + async builder(argv: Argv): Promise> { + const localYargs: Argv = argv + .positional('project', { + describe: 'The name of the project to build. Can be an application or a library.', + type: 'string', + }) + .option('configuration', { + describe: + `One or more named builder configurations as a comma-separated ` + + `list as specified in the "configurations" section in angular.json.\n` + + `The builder uses the named configurations to run the given target.\n` + + `For more information, see https://angular.io/guide/workspace-config#alternate-build-configurations.`, + alias: 'c', + type: 'string', + }) + .strict(); + + const targetSpecifier = this.makeTargetSpecifier(); + if (!targetSpecifier.project) { + return localYargs; + } + + const schemaOptions = await getArchitectTargetOptions(this.context, targetSpecifier); + + return this.addSchemaOptionsToCommand(localYargs, schemaOptions); + } + + async run(options: Options): Promise { + const { logger, workspace } = this.context; + if (!workspace) { + logger.fatal('A workspace is required for this command.'); + + return 1; + } + + const registry = new json.schema.CoreSchemaRegistry(); + registry.addPostTransform(json.schema.transforms.addUndefinedDefaults); + registry.useXDeprecatedProvider((msg) => this.context.logger.warn(msg)); + + const architectHost = new WorkspaceNodeModulesArchitectHost(workspace, workspace.basePath); + const architect = new Architect(architectHost, registry); + + const targetSpec = this.makeTargetSpecifier(options); + if (!targetSpec.project) { + const target = this.getArchitectTarget(); + + // This runs each target sequentially. + // Running them in parallel would jumble the log messages. + let result = 0; + const projectNames = this.getProjectNamesByTarget(target); + if (!projectNames) { + throw new CommandModuleError( + this.missingErrorTarget ?? 'Cannot determine project or target for command.', + ); + } + + for (const project of projectNames) { + result |= await this.runSingleTarget({ ...targetSpec, project }, options, architect); + } + + return result; + } else { + return await this.runSingleTarget(targetSpec, options, architect); + } + } + + private getArchitectProject(): string | undefined { + const workspace = this.context.workspace; + if (!workspace) { + return undefined; + } + + const [, projectName] = this.context.args.positional; + + if (projectName) { + if (!workspace.projects.has(projectName)) { + throw new CommandModuleError(`Project '${projectName}' does not exist.`); + } + + return projectName; + } + + const target = this.getArchitectTarget(); + const projectFromTarget = this.getProjectNamesByTarget(target); + + return projectFromTarget?.length ? projectFromTarget[0] : undefined; + } + + private getArchitectTarget(): string { + // 'build [project]' -> 'build' + return this.command?.split(' ', 1)[0]; + } + + private makeTargetSpecifier(options?: Options): Target { + return { + project: options?.project ?? this.getArchitectProject() ?? '', + target: this.getArchitectTarget(), + configuration: options?.configuration ?? '', + }; + } + + private getProjectNamesByTarget(target: string): string[] | undefined { + const workspace = this.context.workspace; + if (!workspace) { + throw new CommandModuleError('A workspace is required for this command.'); + } + + const allProjectsForTargetName: string[] = []; + for (const [name, project] of workspace.projects) { + if (project.targets.has(target)) { + allProjectsForTargetName.push(name); + } + } + + if (allProjectsForTargetName.length === 0) { + return undefined; + } + + if (this.multiTarget) { + // For multi target commands, we always list all projects that have the target. + return allProjectsForTargetName; + } else { + // For single target commands, we try the default project first, + // then the full list if it has a single project, then error out. + const maybeDefaultProject = workspace.extensions['defaultProject']; + if ( + typeof maybeDefaultProject === 'string' && + allProjectsForTargetName.includes(maybeDefaultProject) + ) { + return [maybeDefaultProject]; + } + + if (allProjectsForTargetName.length === 1) { + return allProjectsForTargetName; + } + } + + return undefined; + } + + private async runSingleTarget( + target: Target, + options: Options & OtherOptions, + architect: Architect, + ): Promise { + // Remove options + const { configuration, project, ...extraOptions } = options; + const architectHost = await this.getArchitectHost(); + + let builderName: string; + try { + builderName = await architectHost.getBuilderNameForTarget(target); + } catch (e) { + throw new CommandModuleError(this.missingErrorTarget ?? e.message); + } + + await this.reportAnalytics({ + ...(await architectHost.getOptionsForTarget(target)), + ...extraOptions, + }); + + const { logger } = this.context; + + const run = await architect.scheduleTarget(target, extraOptions as json.JsonObject, { + logger, + analytics: isPackageNameSafeForAnalytics(builderName) ? await this.getAnalytics() : undefined, + }); + + const { error, success } = await run.output.toPromise(); + await run.stop(); + + if (error) { + logger.error(error); + } + + return success ? 0 : 1; + } + + private _architectHost: WorkspaceNodeModulesArchitectHost | undefined; + private getArchitectHost(): WorkspaceNodeModulesArchitectHost { + if (this._architectHost) { + return this._architectHost; + } + + const { workspace } = this.context; + if (!workspace) { + throw new CommandModuleError('A workspace is required for this command.'); + } + + return (this._architectHost = new WorkspaceNodeModulesArchitectHost( + workspace, + workspace.basePath, + )); + } +} + +/** + * Get architect target schema options. + */ +export async function getArchitectTargetOptions( + context: CommandContext, + target: Target, +): Promise { + const { workspace } = context; + if (!workspace) { + return []; + } + + const architectHost = new WorkspaceNodeModulesArchitectHost(workspace, workspace.basePath); + const builderConf = await architectHost.getBuilderNameForTarget(target); + + let builderDesc; + try { + builderDesc = await architectHost.resolveBuilder(builderConf); + } catch (e) { + if (e.code === 'MODULE_NOT_FOUND') { + await warnOnMissingNodeModules(context); + throw new CommandModuleError(`Could not find the '${builderConf}' builder's node package.`); + } + + throw e; + } + + return parseJsonSchemaToOptions( + new json.schema.CoreSchemaRegistry(), + builderDesc.optionSchema as json.JsonObject, + true, + ); +} + +export async function warnOnMissingNodeModules(context: CommandContext): Promise { + const basePath = context.workspace?.basePath; + if (!basePath) { + return; + } + + // Check for a `node_modules` directory (npm, yarn non-PnP, etc.) + if (existsSync(resolve(basePath, 'node_modules'))) { + return; + } + + // Check for yarn PnP files + if ( + existsSync(resolve(basePath, '.pnp.js')) || + existsSync(resolve(basePath, '.pnp.cjs')) || + existsSync(resolve(basePath, '.pnp.mjs')) + ) { + return; + } + + const packageManager = await getPackageManager(basePath); + context.logger.warn( + `Node packages may not be installed. Try installing with '${packageManager} install'.`, + ); +} diff --git a/packages/angular/cli/utilities/command-builder/command-module.ts b/packages/angular/cli/utilities/command-builder/command-module.ts new file mode 100644 index 000000000000..5e7332542e29 --- /dev/null +++ b/packages/angular/cli/utilities/command-builder/command-module.ts @@ -0,0 +1,217 @@ +/** + * @license + * Copyright Google LLC 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 { analytics, logging, normalize, strings } from '@angular-devkit/core'; +import { readFileSync } from 'fs'; +import * as path from 'path'; +import { + Argv, + CamelCaseKey, + PositionalOptions, + CommandModule as YargsCommandModule, + Options as YargsOptions, +} from 'yargs'; +import { createAnalytics } from '../../models/analytics'; +import { AngularWorkspace } from '../config'; +import { Option } from './json-schema'; + +export type Options = { [key in keyof T as CamelCaseKey]: T[key] }; + +export enum CommandScope { + /** Command can only run inside an Angular workspace. */ + In, + /** Command can only run outside an Angular workspace. */ + Out, + /** Command can run inside and outside an Angular workspace. */ + Both, +} + +export interface CommandContext { + currentDirectory: string; + root: string; + workspace?: AngularWorkspace; + logger: logging.Logger; + /** Arguments parsed in free from without parser configuration. */ + args: { + positional: string[]; + options: { + help: boolean; + } & Record; + }; +} + +export type OtherOptions = Record; + +export interface CommandModuleImplementation + extends Omit, 'builder' | 'handler'> { + /** Path used to load the long description for the command in JSON help text. */ + longDescriptionPath?: string; + /** Object declaring the options the command accepts, or a function accepting and returning a yargs instance. */ + builder(argv: Argv): Promise> | Argv; + /** A function which will be passed the parsed argv. */ + run(options: Options & OtherOptions): Promise | number | void; + /** a function which will be passed the parsed argv. */ + handler(args: Options & OtherOptions): Promise | void; +} + +export interface FullDescribe { + describe?: string; + longDescription?: string; +} + +export abstract class CommandModule implements CommandModuleImplementation { + abstract readonly command: string; + abstract readonly describe: string | false; + abstract readonly longDescriptionPath?: string; + protected shouldReportAnalytics = true; + static scope = CommandScope.Both; + + private readonly optionsWithAnalytics = new Map(); + + constructor(protected readonly context: CommandContext) {} + + /** + * Description object which contains the long command descroption. + * This is used to generate JSON help wich is used in AIO. + * + * `false` will result in a hidden command. + */ + public get fullDescribe(): FullDescribe | false { + return this.describe === false + ? false + : { + describe: this.describe, + longDescription: this.longDescriptionPath + ? readFileSync(this.longDescriptionPath, 'utf8') + : undefined, + }; + } + + protected get commandName(): string { + return this.command.split(' ', 1)[0]; + } + + abstract builder(argv: Argv): Promise> | Argv; + abstract run(options: Options & OtherOptions): Promise | number | void; + + async handler(args: Options & OtherOptions): Promise { + // Gather and report analytics. + const analytics = await this.getAnalytics(); + if (this.shouldReportAnalytics) { + await this.reportAnalytics(args); + } + + // Run and time command. + const startTime = Date.now(); + const result = await this.run(args); + const endTime = Date.now(); + + analytics.timing(this.commandName, 'duration', endTime - startTime); + await analytics.flush(); + + if (typeof result === 'number' && result > 0) { + process.exitCode = result; + } + } + + async reportAnalytics( + options: Options & OtherOptions, + paths: string[] = [], + dimensions: (boolean | number | string)[] = [], + ): Promise { + for (const [name, ua] of this.optionsWithAnalytics) { + const value = options[name]; + + if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { + dimensions[ua] = value; + } + } + + const analytics = await this.getAnalytics(); + analytics.pageview('/command/' + [this.commandName, ...paths].join('/'), { + dimensions, + metrics: [], + }); + } + + private _analytics: analytics.Analytics | undefined; + protected async getAnalytics(): Promise { + if (this._analytics) { + return this._analytics; + } + + return (this._analytics = await createAnalytics( + !!this.context.workspace, + this.commandName === 'update', + )); + } + + /** + * Adds schema options to a command also this keeps track of options that are required for analytics. + * **Note:** This method should be called from the command bundler method. + */ + protected addSchemaOptionsToCommand(localYargs: Argv, options: Option[]): Argv { + const workingDir = normalize(path.relative(this.context.root, process.cwd())); + + for (const option of options) { + const { + default: defaultVal, + positional, + deprecated, + description, + alias, + userAnalytics, + type, + hidden, + name, + choices, + format, + } = option; + + const sharedOptions: YargsOptions & PositionalOptions = { + alias, + hidden, + description, + deprecated, + choices, + // This should only be done when `--help` is used otherwise default will override options set in angular.json. + ...(this.context.args.options.help ? { default: defaultVal } : {}), + }; + + // Special case for schematics + if (workingDir && format === 'path' && name === 'path' && hidden) { + sharedOptions.default = workingDir; + } + + if (positional === undefined) { + localYargs = localYargs.option(strings.dasherize(name), { + type, + ...sharedOptions, + }); + } else { + localYargs = localYargs.positional(strings.dasherize(name), { + type: type === 'array' || type === 'count' ? 'string' : type, + ...sharedOptions, + }); + } + + // Record option of analytics. + if (userAnalytics !== undefined) { + this.optionsWithAnalytics.set(name, userAnalytics); + } + } + + return localYargs; + } +} + +/** + * Creates an known command module error. + * This is used so during executation we can filter between known validation error and real non handled errors. + */ +export class CommandModuleError extends Error {} diff --git a/packages/angular/cli/utilities/command-builder/json-schema.ts b/packages/angular/cli/utilities/command-builder/json-schema.ts new file mode 100644 index 000000000000..8146cd71dbfd --- /dev/null +++ b/packages/angular/cli/utilities/command-builder/json-schema.ts @@ -0,0 +1,213 @@ +/** + * @license + * Copyright Google LLC 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 yargs from 'yargs'; + +/** + * An option description. + */ +export interface Option extends yargs.Options { + /** + * The name of the option. + */ + name: string; + + /** + * Whether this option is required or not. + */ + required?: boolean; + + /** + * Format field of this option. + */ + format?: string; + + /** + * Whether this option should be hidden from the help output. It will still show up in JSON help. + */ + hidden?: boolean; + + /** + * If this option can be used as an argument, the position of the argument. Otherwise omitted. + */ + positional?: number; + + /** + * Whether or not to report this option to the Angular Team, and which custom field to use. + * If this is falsey, do not report this option. + */ + userAnalytics?: number; +} + +export async function parseJsonSchemaToOptions( + registry: json.schema.SchemaRegistry, + schema: json.JsonObject, + interactive = true, +): 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|definitions)\//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; + } + }) as ('string' | 'number' | 'boolean' | 'array')[]; + + if (types.length == 0) { + // This means it's not usable on the command line. e.g. an Object. + return; + } + + // Only keep enum values we support (booleans, numbers and strings). + const enumValues = ((json.isJsonArray(current.enum) && current.enum) || []).filter((x) => { + switch (typeof x) { + case 'boolean': + case 'number': + case 'string': + return true; + + default: + return false; + } + }) as (string | true | number)[]; + + let defaultValue: string | number | boolean | undefined = undefined; + if (current.default !== undefined) { + switch (types[0]) { + case 'string': + if (typeof current.default == 'string') { + defaultValue = current.default; + } + break; + case 'number': + if (typeof current.default == 'number') { + defaultValue = current.default; + } + break; + case 'boolean': + if (typeof current.default == 'boolean') { + defaultValue = current.default; + } + break; + } + } + + const type = types[0]; + const $default = current.$default; + const $defaultIndex = + json.isJsonObject($default) && $default['$source'] == 'argv' ? $default['index'] : undefined; + const positional: number | undefined = + typeof $defaultIndex == 'number' ? $defaultIndex : undefined; + + let required = json.isJsonArray(schema.required) ? schema.required.includes(name) : false; + if (required && interactive && current['x-prompt']) { + required = false; + } + + const alias = json.isJsonArray(current.aliases) + ? [...current.aliases].map((x) => '' + x) + : current.alias + ? ['' + current.alias] + : []; + const format = typeof current.format == 'string' ? current.format : undefined; + const visible = current.visible === undefined || current.visible === true; + const hidden = !!current.hidden || !visible; + + const xUserAnalytics = current['x-user-analytics']; + const userAnalytics = typeof xUserAnalytics == 'number' ? xUserAnalytics : undefined; + + // Deprecated is set only if it's true or a string. + const xDeprecated = current['x-deprecated']; + const deprecated = + xDeprecated === true || typeof xDeprecated === 'string' ? xDeprecated : undefined; + + const option: Option = { + name, + description: '' + (current.description === undefined ? '' : current.description), + type, + default: defaultValue, + choices: enumValues.length ? enumValues : undefined, + required, + alias, + format, + hidden, + userAnalytics, + deprecated, + positional, + }; + + options.push(option); + } + + const flattenedSchema = await registry.flatten(schema).toPromise(); + json.schema.visitJsonSchema(flattenedSchema, visitor); + + // Sort by positional and name. + return options.sort((a, b) => { + if (a.positional) { + return b.positional ? a.positional - b.positional : a.name.localeCompare(b.name); + } else if (b.positional) { + return -1; + } + + return a.name.localeCompare(b.name); + }); +} diff --git a/packages/angular/cli/utilities/command-builder/schematics-command-module.ts b/packages/angular/cli/utilities/command-builder/schematics-command-module.ts new file mode 100644 index 000000000000..51c033d22689 --- /dev/null +++ b/packages/angular/cli/utilities/command-builder/schematics-command-module.ts @@ -0,0 +1,167 @@ +/** + * @license + * Copyright Google LLC 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 { Collection } from '@angular-devkit/schematics'; +import { + FileSystemCollectionDescription, + FileSystemSchematicDescription, + NodeWorkflow, +} from '@angular-devkit/schematics/tools'; +import { Argv } from 'yargs'; +import { SchematicEngineHost } from '../../models/schematic-engine-host'; +import { getProjectByCwd, getWorkspace } from '../config'; +import { CommandModule, CommandModuleImplementation, CommandScope } from './command-module'; +import { Option, parseJsonSchemaToOptions } from './json-schema'; + +const DEFAULT_SCHEMATICS_COLLECTION = '@schematics/angular'; + +export interface SchematicsCommandArgs { + interactive: boolean; + force: boolean; + 'dry-run': boolean; + defaults: boolean; +} + +export abstract class SchematicsCommandModule + extends CommandModule + implements CommandModuleImplementation +{ + static override scope = CommandScope.In; + protected readonly schematicName: string | undefined; + + async builder(argv: Argv): Promise> { + const localYargs: Argv = argv + .option('interactive', { + describe: 'Enable interactive input prompts.', + type: 'boolean', + default: true, + }) + .option('dry-run', { + describe: 'Run through and reports activity without writing out results.', + type: 'boolean', + default: false, + }) + .option('defaults', { + describe: 'Disable interactive input prompts for options with a default.', + type: 'boolean', + default: false, + }) + .option('force', { + describe: 'Force overwriting of existing files.', + type: 'boolean', + default: false, + }) + .strict(); + + if (this.schematicName) { + const collectionName = await this.getCollectionName(); + const workflow = this.getOrCreateWorkflow(collectionName); + const collection = workflow.engine.createCollection(collectionName); + const options = await this.getSchematicOptions(collection, this.schematicName, workflow); + + return this.addSchemaOptionsToCommand(localYargs, options); + } + + return localYargs; + } + + /** Get schematic schema options.*/ + protected async getSchematicOptions( + collection: Collection, + schematicName: string, + workflow: NodeWorkflow, + ): Promise { + const schematic = collection.createSchematic(schematicName, true); + const { schemaJson } = schematic.description; + + if (!schemaJson) { + return []; + } + + return parseJsonSchemaToOptions(workflow.registry, schemaJson); + } + + protected async getCollectionName(): Promise { + const { + options: { collection }, + positional, + } = this.context.args; + + return ( + (typeof collection === 'string' ? collection : undefined) ?? + // positional = [generate, lint] or [new, collection-package] + this.parseSchematicInfo(positional[1])[0] ?? + (await this.getDefaultSchematicCollection()) + ); + } + + private _workflow: NodeWorkflow | undefined; + protected getOrCreateWorkflow(collectionName: string): NodeWorkflow { + if (this._workflow) { + return this._workflow; + } + + const { root, workspace } = this.context; + + return new NodeWorkflow(root, { + resolvePaths: workspace + ? // Workspace + collectionName === DEFAULT_SCHEMATICS_COLLECTION + ? // Favor __dirname for @schematics/angular to use the build-in version + [__dirname, process.cwd(), root] + : [process.cwd(), root, __dirname] + : // Global + [__dirname, process.cwd()], + engineHostCreator: (options) => new SchematicEngineHost(options.resolvePaths), + }); + } + + private _defaultSchematicCollection: string | undefined; + protected async getDefaultSchematicCollection(): Promise { + if (this._defaultSchematicCollection) { + return this._defaultSchematicCollection; + } + + let workspace = await getWorkspace('local'); + + if (workspace) { + const project = getProjectByCwd(workspace); + if (project) { + const value = workspace.getProjectCli(project)['defaultCollection']; + if (typeof value == 'string') { + return (this._defaultSchematicCollection = value); + } + } + + const value = workspace.getCli()['defaultCollection']; + if (typeof value === 'string') { + return (this._defaultSchematicCollection = value); + } + } + + workspace = await getWorkspace('global'); + const value = workspace?.getCli()['defaultCollection']; + if (typeof value === 'string') { + return (this._defaultSchematicCollection = value); + } + + return (this._defaultSchematicCollection = DEFAULT_SCHEMATICS_COLLECTION); + } + + protected parseSchematicInfo( + schematic: string | undefined, + ): [collectionName: string | undefined, schematicName: string | undefined] { + if (schematic?.includes(':')) { + const [collectionName, schematicName] = schematic.split(':', 2); + + return [collectionName, schematicName]; + } + + return [undefined, schematic]; + } +} diff --git a/packages/angular/cli/utilities/json-schema.ts b/packages/angular/cli/utilities/json-schema.ts deleted file mode 100644 index f396d4a063d9..000000000000 --- a/packages/angular/cli/utilities/json-schema.ts +++ /dev/null @@ -1,301 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 { BaseException, json } from '@angular-devkit/core'; -import { ExportStringRef } from '@angular-devkit/schematics/tools'; -import { readFileSync } from 'fs'; -import { dirname, resolve } from 'path'; -import { - CommandConstructor, - CommandDescription, - CommandScope, - Option, - OptionType, - SubCommandDescription, - Value, -} from '../models/interface'; - -export class CommandJsonPathException extends BaseException { - constructor(public readonly path: string, public override readonly name: string) { - super(`File ${path} was not found while constructing the subcommand ${name}.`); - } -} - -function _getEnumFromValue( - value: json.JsonValue, - enumeration: E, - defaultValue: T, -): T { - if (typeof value !== 'string') { - return defaultValue; - } - - if (Object.values(enumeration).includes(value)) { - return value as unknown as T; - } - - return defaultValue; -} - -export async function parseJsonSchemaToSubCommandDescription( - name: string, - jsonPath: string, - registry: json.schema.SchemaRegistry, - schema: json.JsonObject, -): Promise { - 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); - } - }); - } - if (json.isJsonArray(schema.aliases)) { - schema.aliases.forEach((value) => { - if (typeof value == 'string') { - aliases.push(value); - } - }); - } - if (typeof schema.alias == 'string') { - aliases.push(schema.alias); - } - - let longDescription = ''; - if (typeof schema.$longDescription == 'string' && schema.$longDescription) { - const ldPath = resolve(dirname(jsonPath), schema.$longDescription); - try { - longDescription = readFileSync(ldPath, 'utf-8'); - } catch (e) { - throw new CommandJsonPathException(ldPath, name); - } - } - let usageNotes = ''; - if (typeof schema.$usageNotes == 'string' && schema.$usageNotes) { - const unPath = resolve(dirname(jsonPath), schema.$usageNotes); - try { - usageNotes = readFileSync(unPath, 'utf-8'); - } catch (e) { - throw new CommandJsonPathException(unPath, name); - } - } - - const description = '' + (schema.description === undefined ? '' : schema.description); - - return { - name, - description, - ...(longDescription ? { longDescription } : {}), - ...(usageNotes ? { usageNotes } : {}), - options, - aliases, - }; -} - -export async function parseJsonSchemaToCommandDescription( - name: string, - jsonPath: string, - registry: json.schema.SchemaRegistry, - schema: json.JsonObject, -): Promise { - const subcommand = await parseJsonSchemaToSubCommandDescription(name, jsonPath, registry, schema); - - // 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 scope = _getEnumFromValue(schema.$scope, CommandScope, CommandScope.Default); - const hidden = !!schema.$hidden; - - return { - ...subcommand, - scope, - hidden, - 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|definitions)\//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; - } - - // Only keep enum values we support (booleans, numbers and strings). - const enumValues = ((json.isJsonArray(current.enum) && current.enum) || []).filter((x) => { - switch (typeof x) { - case 'boolean': - case 'number': - case 'string': - return true; - - default: - return false; - } - }) as Value[]; - - let defaultValue: string | number | boolean | undefined = undefined; - if (current.default !== undefined) { - switch (types[0]) { - case 'string': - if (typeof current.default == 'string') { - defaultValue = current.default; - } - break; - case 'number': - if (typeof current.default == 'number') { - defaultValue = current.default; - } - break; - case 'boolean': - if (typeof current.default == 'boolean') { - defaultValue = current.default; - } - break; - } - } - - const type = types[0]; - 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) - : current.alias - ? ['' + current.alias] - : []; - const format = typeof current.format == 'string' ? current.format : undefined; - const visible = current.visible === undefined || current.visible === true; - const hidden = !!current.hidden || !visible; - - const xUserAnalytics = current['x-user-analytics']; - const userAnalytics = typeof xUserAnalytics == 'number' ? xUserAnalytics : undefined; - - // Deprecated is set only if it's true or a string. - const xDeprecated = current['x-deprecated']; - const deprecated = - xDeprecated === true || typeof xDeprecated === 'string' ? xDeprecated : undefined; - - const option: Option = { - name, - description: '' + (current.description === undefined ? '' : current.description), - ...(types.length == 1 ? { type } : { type, types }), - ...(defaultValue !== undefined ? { default: defaultValue } : {}), - ...(enumValues && enumValues.length > 0 ? { enum: enumValues } : {}), - required, - aliases, - ...(format !== undefined ? { format } : {}), - hidden, - ...(userAnalytics ? { userAnalytics } : {}), - ...(deprecated !== undefined ? { deprecated } : {}), - ...(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/json-schema_spec.ts b/packages/angular/cli/utilities/json-schema_spec.ts deleted file mode 100644 index f300cc4bc077..000000000000 --- a/packages/angular/cli/utilities/json-schema_spec.ts +++ /dev/null @@ -1,75 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 { schema } from '@angular-devkit/core'; -import { readFileSync } from 'fs'; -import { join } from 'path'; -import { CommandJsonPathException, parseJsonSchemaToCommandDescription } from './json-schema'; - -describe('parseJsonSchemaToCommandDescription', () => { - let registry: schema.CoreSchemaRegistry; - const baseSchemaJson = { - '$schema': 'http://json-schema.org/schema', - '$id': 'ng-cli://commands/version.json', - 'description': 'Outputs Angular CLI version.', - '$longDescription': 'not a file ref', - - '$aliases': ['v'], - '$scope': 'all', - '$impl': './version-impl#VersionCommand', - - 'type': 'object', - 'allOf': [{ '$ref': './definitions.json#/definitions/base' }], - }; - - beforeEach(() => { - 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 Promise.resolve(JSON.parse(content)); - } else { - return null; - } - }); - }); - - it(`should throw on invalid $longDescription path`, async () => { - const name = 'version'; - const schemaPath = join(__dirname, './bad-sample.json'); - const schemaJson = { ...baseSchemaJson, $longDescription: 'not a file ref' }; - try { - await parseJsonSchemaToCommandDescription(name, schemaPath, registry, schemaJson); - } catch (error) { - const refPath = join(__dirname, schemaJson.$longDescription); - expect(error).toEqual(new CommandJsonPathException(refPath, name)); - - return; - } - expect(true).toBe(false, 'function should have thrown'); - }); - - it(`should throw on invalid $usageNotes path`, async () => { - const name = 'version'; - const schemaPath = join(__dirname, './bad-sample.json'); - const schemaJson = { ...baseSchemaJson, $usageNotes: 'not a file ref' }; - try { - await parseJsonSchemaToCommandDescription(name, schemaPath, registry, schemaJson); - } catch (error) { - const refPath = join(__dirname, schemaJson.$usageNotes); - expect(error).toEqual(new CommandJsonPathException(refPath, name)); - - return; - } - expect(true).toBe(false, 'function should have thrown'); - }); -}); diff --git a/packages/angular/cli/models/version.ts b/packages/angular/cli/utilities/version.ts similarity index 83% rename from packages/angular/cli/models/version.ts rename to packages/angular/cli/utilities/version.ts index f24082ff1229..802e7fbc2b2a 100644 --- a/packages/angular/cli/models/version.ts +++ b/packages/angular/cli/utilities/version.ts @@ -10,15 +10,16 @@ import { readFileSync } from 'fs'; import { resolve } from 'path'; // Same structure as used in framework packages -export class Version { +class Version { public readonly major: string; public readonly minor: string; public readonly patch: string; constructor(public readonly full: string) { - this.major = full.split('.')[0]; - this.minor = full.split('.')[1]; - this.patch = full.split('.').slice(2).join('.'); + const [major, minor, patch] = full.split('-', 1)[0].split('.', 3); + this.major = major; + this.minor = minor; + this.patch = patch; } } diff --git a/packages/angular_devkit/build_angular/src/builders/browser/schema.json b/packages/angular_devkit/build_angular/src/builders/browser/schema.json index a577723c4116..7a7dc42b8a73 100644 --- a/packages/angular_devkit/build_angular/src/builders/browser/schema.json +++ b/packages/angular_devkit/build_angular/src/builders/browser/schema.json @@ -193,7 +193,7 @@ }, "outputPath": { "type": "string", - "description": "The full path for the new output directory, relative to the current workspace.\n\nBy default, writes output to a folder named dist/ in the current project." + "description": "The full path for the new output directory, relative to the current workspace.\nBy default, writes output to a folder named dist/ in the current project." }, "resourcesOutputPath": { "type": "string", diff --git a/packages/schematics/angular/app-shell/index_spec.ts b/packages/schematics/angular/app-shell/index_spec.ts index 68c9909e6e91..aa2e884eee82 100644 --- a/packages/schematics/angular/app-shell/index_spec.ts +++ b/packages/schematics/angular/app-shell/index_spec.ts @@ -49,13 +49,13 @@ describe('App Shell Schematic', () => { .runSchematicAsync('application', { ...appOptions, routing: false }, appTree) .toPromise(); await expectAsync( - schematicRunner.runSchematicAsync('appShell', defaultOptions, appTree).toPromise(), + schematicRunner.runSchematicAsync('app-shell', defaultOptions, appTree).toPromise(), ).toBeRejected(); }); it('should add a universal app', async () => { const tree = await schematicRunner - .runSchematicAsync('appShell', defaultOptions, appTree) + .runSchematicAsync('app-shell', defaultOptions, appTree) .toPromise(); const filePath = '/projects/bar/src/app/app.server.module.ts'; expect(tree.exists(filePath)).toEqual(true); @@ -63,7 +63,7 @@ describe('App Shell Schematic', () => { it('should add app shell configuration', async () => { const tree = await schematicRunner - .runSchematicAsync('appShell', defaultOptions, appTree) + .runSchematicAsync('app-shell', defaultOptions, appTree) .toPromise(); const filePath = '/angular.json'; const content = tree.readContent(filePath); @@ -78,7 +78,7 @@ describe('App Shell Schematic', () => { it('should add router module to client app module', async () => { const tree = await schematicRunner - .runSchematicAsync('appShell', defaultOptions, appTree) + .runSchematicAsync('app-shell', defaultOptions, appTree) .toPromise(); const filePath = '/projects/bar/src/app/app.module.ts'; const content = tree.readContent(filePath); @@ -91,7 +91,7 @@ describe('App Shell Schematic', () => { appTree.commitUpdate(updateRecorder); const tree = await schematicRunner - .runSchematicAsync('appShell', defaultOptions, appTree) + .runSchematicAsync('app-shell', defaultOptions, appTree) .toPromise(); const filePath = '/projects/bar/src/app/app.module.ts'; const content = tree.readContent(filePath); @@ -134,7 +134,7 @@ describe('App Shell Schematic', () => { const htmlPath = '/projects/bar/src/app/app.component.html'; appTree.overwrite(htmlPath, ''); const tree = await schematicRunner - .runSchematicAsync('appShell', defaultOptions, appTree) + .runSchematicAsync('app-shell', defaultOptions, appTree) .toPromise(); const content = tree.readContent(htmlPath); @@ -146,7 +146,7 @@ describe('App Shell Schematic', () => { it('should not re-add the router outlet (inline template)', async () => { makeInlineTemplate(appTree, ''); const tree = await schematicRunner - .runSchematicAsync('appShell', defaultOptions, appTree) + .runSchematicAsync('app-shell', defaultOptions, appTree) .toPromise(); const content = tree.readContent('/projects/bar/src/app/app.component.ts'); const matches = content.match(/<\/router-outlet>/g); @@ -157,7 +157,7 @@ describe('App Shell Schematic', () => { it('should add router imports to server module', async () => { const tree = await schematicRunner - .runSchematicAsync('appShell', defaultOptions, appTree) + .runSchematicAsync('app-shell', defaultOptions, appTree) .toPromise(); const filePath = '/projects/bar/src/app/app.server.module.ts'; const content = tree.readContent(filePath); @@ -174,7 +174,7 @@ describe('App Shell Schematic', () => { workspace.projects.bar.architect.server.options.main = 'server.ts'; appTree.overwrite('angular.json', JSON.stringify(workspace, undefined, 2)); - tree = await schematicRunner.runSchematicAsync('appShell', defaultOptions, tree).toPromise(); + tree = await schematicRunner.runSchematicAsync('app-shell', defaultOptions, tree).toPromise(); const filePath = '/projects/bar/src/app/app.server.module.ts'; const content = tree.readContent(filePath); expect(content).toMatch(/import { Routes, RouterModule } from '@angular\/router';/); @@ -182,7 +182,7 @@ describe('App Shell Schematic', () => { it('should define a server route', async () => { const tree = await schematicRunner - .runSchematicAsync('appShell', defaultOptions, appTree) + .runSchematicAsync('app-shell', defaultOptions, appTree) .toPromise(); const filePath = '/projects/bar/src/app/app.server.module.ts'; const content = tree.readContent(filePath); @@ -191,7 +191,7 @@ describe('App Shell Schematic', () => { it('should import RouterModule with forRoot', async () => { const tree = await schematicRunner - .runSchematicAsync('appShell', defaultOptions, appTree) + .runSchematicAsync('app-shell', defaultOptions, appTree) .toPromise(); const filePath = '/projects/bar/src/app/app.server.module.ts'; const content = tree.readContent(filePath); @@ -203,7 +203,7 @@ describe('App Shell Schematic', () => { it('should create the shell component', async () => { const tree = await schematicRunner - .runSchematicAsync('appShell', defaultOptions, appTree) + .runSchematicAsync('app-shell', defaultOptions, appTree) .toPromise(); expect(tree.exists('/projects/bar/src/app/app-shell/app-shell.component.ts')).toBe(true); const content = tree.readContent('/projects/bar/src/app/app.server.module.ts'); diff --git a/packages/schematics/angular/collection.json b/packages/schematics/angular/collection.json index aa8acf318481..6a6e8e78bf63 100755 --- a/packages/schematics/angular/collection.json +++ b/packages/schematics/angular/collection.json @@ -13,7 +13,6 @@ "hidden": true }, "service-worker": { - "aliases": ["serviceWorker"], "factory": "./service-worker", "description": "Initializes a service worker setup.", "schema": "./service-worker/schema.json" @@ -102,7 +101,6 @@ "hidden": true }, "app-shell": { - "aliases": ["appShell"], "factory": "./app-shell", "description": "Create an app shell.", "schema": "./app-shell/schema.json" @@ -114,7 +112,6 @@ "description": "Generate a library project for Angular." }, "web-worker": { - "aliases": ["webWorker"], "factory": "./web-worker", "schema": "./web-worker/schema.json", "description": "Create a Web Worker." diff --git a/tests/legacy-cli/e2e/tests/basic/e2e.ts b/tests/legacy-cli/e2e/tests/basic/e2e.ts index d547320908f6..2668bdce30c1 100644 --- a/tests/legacy-cli/e2e/tests/basic/e2e.ts +++ b/tests/legacy-cli/e2e/tests/basic/e2e.ts @@ -1,58 +1,77 @@ -import { - ng, - execAndWaitForOutputToMatch, - killAllProcesses -} from '../../utils/process'; -import {expectToFail} from '../../utils/utils'; -import {moveFile, copyFile, replaceInFile} from '../../utils/fs'; +import { ng, execAndWaitForOutputToMatch, killAllProcesses } from '../../utils/process'; +import { expectToFail } from '../../utils/utils'; +import { moveFile, copyFile, replaceInFile } from '../../utils/fs'; export default function () { - return Promise.resolve() - // Should fail without serving - .then(() => expectToFail(() => ng('e2e', 'test-project', '--devServerTarget='))) - // These should work. - .then(() => ng('e2e', 'test-project')) - .then(() => ng('e2e', 'test-project', '--devServerTarget=test-project:serve')) - // Should accept different config file - .then(() => moveFile('./e2e/protractor.conf.js', - './e2e/renamed-protractor.conf.js')) - .then(() => ng('e2e', 'test-project', - '--protractorConfig=e2e/renamed-protractor.conf.js')) - .then(() => moveFile('./e2e/renamed-protractor.conf.js', './e2e/protractor.conf.js')) - // Should accept different multiple spec files - .then(() => moveFile('./e2e/src/app.e2e-spec.ts', - './e2e/src/renamed-app.e2e-spec.ts')) - .then(() => copyFile('./e2e/src/renamed-app.e2e-spec.ts', - './e2e/src/another-app.e2e-spec.ts')) - .then(() => ng('e2e', 'test-project', '--specs', './e2e/renamed-app.e2e-spec.ts', - '--specs', './e2e/another-app.e2e-spec.ts')) - // Rename the spec back to how it was. - .then(() => moveFile('./e2e/src/renamed-app.e2e-spec.ts', - './e2e/src/app.e2e-spec.ts')) - // Suites block need to be added in the protractor.conf.js file to test suites - .then(() => replaceInFile('e2e/protractor.conf.js', `allScriptsTimeout: 11000,`, - `allScriptsTimeout: 11000, + return ( + Promise.resolve() + // Should fail without serving + .then(() => expectToFail(() => ng('e2e', 'test-project', '--dev-server-target='))) + // These should work. + .then(() => ng('e2e', 'test-project')) + .then(() => ng('e2e', 'test-project', '--dev-server-target=test-project:serve')) + // Should accept different config file + .then(() => moveFile('./e2e/protractor.conf.js', './e2e/renamed-protractor.conf.js')) + .then(() => ng('e2e', 'test-project', '--protractor-config=e2e/renamed-protractor.conf.js')) + .then(() => moveFile('./e2e/renamed-protractor.conf.js', './e2e/protractor.conf.js')) + // Should accept different multiple spec files + .then(() => moveFile('./e2e/src/app.e2e-spec.ts', './e2e/src/renamed-app.e2e-spec.ts')) + .then(() => + copyFile('./e2e/src/renamed-app.e2e-spec.ts', './e2e/src/another-app.e2e-spec.ts'), + ) + .then(() => + ng( + 'e2e', + 'test-project', + '--specs', + './e2e/renamed-app.e2e-spec.ts', + '--specs', + './e2e/another-app.e2e-spec.ts', + ), + ) + // Rename the spec back to how it was. + .then(() => moveFile('./e2e/src/renamed-app.e2e-spec.ts', './e2e/src/app.e2e-spec.ts')) + // Suites block need to be added in the protractor.conf.js file to test suites + .then(() => + replaceInFile( + 'e2e/protractor.conf.js', + `allScriptsTimeout: 11000,`, + `allScriptsTimeout: 11000, suites: { app: './e2e/src/app.e2e-spec.ts' }, - `)) - .then(() => ng('e2e', 'test-project', '--suite=app')) - // Remove suites block from protractor.conf.js file after testing suites - .then(() => replaceInFile('e2e/protractor.conf.js', `allScriptsTimeout: 11000, + `, + ), + ) + .then(() => ng('e2e', 'test-project', '--suite=app')) + // Remove suites block from protractor.conf.js file after testing suites + .then(() => + replaceInFile( + 'e2e/protractor.conf.js', + `allScriptsTimeout: 11000, suites: { app: './e2e/src/app.e2e-spec.ts' }, - `, `allScriptsTimeout: 11000,` - )) - // Should run side-by-side with `ng serve` - .then(() => execAndWaitForOutputToMatch('ng', ['serve'], - / Compiled successfully./)) - .then(() => ng('e2e', 'test-project', '--devServerTarget=')) - // Should fail without updated webdriver - .then(() => replaceInFile('e2e/protractor.conf.js', /chromeDriver: String.raw`[^`]*`,/, '')) - .then(() => expectToFail(() => ng('e2e', 'test-project', '--no-webdriver-update', '--devServerTarget='))) - .then(() => killAllProcesses(), (err) => { - killAllProcesses(); - throw err; - }); + `, + `allScriptsTimeout: 11000,`, + ), + ) + // Should run side-by-side with `ng serve` + .then(() => execAndWaitForOutputToMatch('ng', ['serve'], / Compiled successfully./)) + .then(() => ng('e2e', 'test-project', '--dev-server-target=')) + // Should fail without updated webdriver + .then(() => replaceInFile('e2e/protractor.conf.js', /chromeDriver: String.raw`[^`]*`,/, '')) + .then(() => + expectToFail(() => + ng('e2e', 'test-project', '--no-webdriver-update', '--dev-server-target='), + ), + ) + .then( + () => killAllProcesses(), + (err) => { + killAllProcesses(); + throw err; + }, + ) + ); } diff --git a/tests/legacy-cli/e2e/tests/basic/test.ts b/tests/legacy-cli/e2e/tests/basic/test.ts index 9ae72b9026d1..6010b335035e 100644 --- a/tests/legacy-cli/e2e/tests/basic/test.ts +++ b/tests/legacy-cli/e2e/tests/basic/test.ts @@ -5,5 +5,5 @@ export default function () { // make sure both --watch=false work return ng('test', '--watch=false') .then(() => moveFile('./karma.conf.js', './karma.conf.bis.js')) - .then(() => ng('test', '--watch=false', '--karmaConfig=karma.conf.bis.js')); + .then(() => ng('test', '--watch=false', '--karma-config=karma.conf.bis.js')); } diff --git a/tests/legacy-cli/e2e/tests/build/build-app-shell-with-schematic.ts b/tests/legacy-cli/e2e/tests/build/build-app-shell-with-schematic.ts index 95ece32ea6f5..00f5cedf71c2 100644 --- a/tests/legacy-cli/e2e/tests/build/build-app-shell-with-schematic.ts +++ b/tests/legacy-cli/e2e/tests/build/build-app-shell-with-schematic.ts @@ -8,7 +8,7 @@ const snapshots = require('../../ng-snapshot/package.json'); export default async function () { await appendToFile('src/app/app.component.html', ''); - await ng('generate', 'appShell', '--project', 'test-project'); + await ng('generate', 'app-shell', '--project', 'test-project'); const isSnapshotBuild = getGlobalVariable('argv')['ng-snapshots']; if (isSnapshotBuild) { diff --git a/tests/legacy-cli/e2e/tests/build/multiple-configs.ts b/tests/legacy-cli/e2e/tests/build/multiple-configs.ts index 31623fc9a34b..f25d05f7eff0 100644 --- a/tests/legacy-cli/e2e/tests/build/multiple-configs.ts +++ b/tests/legacy-cli/e2e/tests/build/multiple-configs.ts @@ -51,7 +51,7 @@ export default async function () { await expectToFail(() => expectFileToExist('dist/test-project/favicon.ico')); await expectToFail(() => expectFileToExist('dist/test-project/main.js.map')); // Use two configurations and two overrides, one of which overrides a config. - await ng('build', '--configuration=one,two', '--vendor-chunk=false', '--sourceMap=true'); + await ng('build', '--configuration=one,two', '--vendor-chunk=false', '--source-map=true'); await expectToFail(() => expectFileToExist('dist/test-project/favicon.ico')); await expectFileToExist('dist/test-project/main.js.map'); await expectToFail(() => expectFileToExist('dist/test-project/vendor.js')); diff --git a/tests/legacy-cli/e2e/tests/build/platform-server.ts b/tests/legacy-cli/e2e/tests/build/platform-server.ts index 0afb776366fb..d5938c2efcc1 100644 --- a/tests/legacy-cli/e2e/tests/build/platform-server.ts +++ b/tests/legacy-cli/e2e/tests/build/platform-server.ts @@ -59,7 +59,7 @@ export default async function () { ); // works with optimization and bundleDependencies enabled - await ng('run', 'test-project:server', '--optimization', '--bundleDependencies'); + await ng('run', 'test-project:server', '--optimization', '--bundle-dependencies'); await exec(normalize('node'), 'dist/test-project/server/main.js'); await expectFileToMatch( 'dist/test-project/server/index.html', diff --git a/tests/legacy-cli/e2e/tests/commands/additional-properties.ts b/tests/legacy-cli/e2e/tests/commands/additional-properties.ts index b9a477c7cff6..a53006853fca 100644 --- a/tests/legacy-cli/e2e/tests/commands/additional-properties.ts +++ b/tests/legacy-cli/e2e/tests/commands/additional-properties.ts @@ -2,16 +2,19 @@ import { createDir, rimraf, writeMultipleFiles } from '../../utils/fs'; import { execAndWaitForOutputToMatch } from '../../utils/process'; import { updateJsonFile } from '../../utils/project'; -export default async function() { +export default async function () { await createDir('example-builder'); await writeMultipleFiles({ 'example-builder/package.json': '{ "builders": "./builders.json" }', - 'example-builder/schema.json': '{ "$schema": "http://json-schema.org/draft-07/schema", "type": "object", "additionalProperties": true }', - 'example-builder/builders.json': '{ "$schema": "@angular-devkit/architect/src/builders-schema.json", "builders": { "example": { "implementation": "./example", "schema": "./schema.json" } } }', - 'example-builder/example.js': 'module.exports.default = require("@angular-devkit/architect").createBuilder((options) => { console.log(options); return { success: true }; });', + 'example-builder/schema.json': + '{ "$schema": "http://json-schema.org/draft-07/schema", "type": "object", "additionalProperties": true }', + 'example-builder/builders.json': + '{ "$schema": "@angular-devkit/architect/src/builders-schema.json", "builders": { "example": { "implementation": "./example", "schema": "./schema.json" } } }', + 'example-builder/example.js': + 'module.exports.default = require("@angular-devkit/architect").createBuilder((options) => { console.log(options); return { success: true }; });', }); - await updateJsonFile('angular.json', json => { + await updateJsonFile('angular.json', (json) => { const appArchitect = json.projects['test-project'].architect; appArchitect.example = { builder: './example-builder:example', @@ -21,7 +24,7 @@ export default async function() { await execAndWaitForOutputToMatch( 'ng', ['run', 'test-project:example', '--additional', 'property'], - /'{ '--': \[ '--additional', 'property' \] }'/, + /Unknown argument: additional/, ); await rimraf('example-builder'); diff --git a/tests/legacy-cli/e2e/tests/commands/help/help-option-command.ts b/tests/legacy-cli/e2e/tests/commands/help/help-option-command.ts deleted file mode 100644 index c6782765b839..000000000000 --- a/tests/legacy-cli/e2e/tests/commands/help/help-option-command.ts +++ /dev/null @@ -1,7 +0,0 @@ -import {silentNg} from '../../../utils/process'; - - -export default function() { - return Promise.resolve() - .then(() => silentNg('--help', 'build')); -} diff --git a/tests/legacy-cli/e2e/tests/commands/help/help-option.ts b/tests/legacy-cli/e2e/tests/commands/help/help-option.ts deleted file mode 100644 index 03b96b5758d9..000000000000 --- a/tests/legacy-cli/e2e/tests/commands/help/help-option.ts +++ /dev/null @@ -1,9 +0,0 @@ -import {silentNg} from '../../../utils/process'; - - -export default function() { - return Promise.resolve() - .then(() => silentNg('--help')) - .then(() => process.chdir('/')) - .then(() => silentNg('--help')); -} diff --git a/tests/legacy-cli/e2e/tests/commands/help/help.ts b/tests/legacy-cli/e2e/tests/commands/help/help.ts deleted file mode 100644 index f326f6a81ff8..000000000000 --- a/tests/legacy-cli/e2e/tests/commands/help/help.ts +++ /dev/null @@ -1,9 +0,0 @@ -import {silentNg} from '../../../utils/process'; - - -export default function() { - return Promise.resolve() - .then(() => silentNg('help')) - .then(() => process.chdir('/')) - .then(() => silentNg('help')); -} diff --git a/tests/legacy-cli/e2e/tests/commands/unknown-option.ts b/tests/legacy-cli/e2e/tests/commands/unknown-option.ts index 220d74bc1646..f0f4cde0693f 100644 --- a/tests/legacy-cli/e2e/tests/commands/unknown-option.ts +++ b/tests/legacy-cli/e2e/tests/commands/unknown-option.ts @@ -1,27 +1,17 @@ import { execAndWaitForOutputToMatch, ng } from '../../utils/process'; import { expectToFail } from '../../utils/utils'; -export default async function() { +export default async function () { await expectToFail(() => ng('build', '--notanoption')); await execAndWaitForOutputToMatch( 'ng', - [ 'build', '--notanoption' ], - /Unknown option: '--notanoption'/, + ['build', '--notanoption'], + /Unknown argument: notanoption/, ); - await expectToFail(() => execAndWaitForOutputToMatch( - 'ng', - [ 'build', '--notanoption' ], - /should NOT have additional properties\(notanoption\)./, - )); - - const ngGenerateArgs = [ 'generate', 'component', 'component-name', '--notanoption' ]; + const ngGenerateArgs = ['generate', 'component', 'component-name', '--notanoption']; await expectToFail(() => ng(...ngGenerateArgs)); - await execAndWaitForOutputToMatch( - 'ng', - ngGenerateArgs, - /Unknown option: '--notanoption'/, - ); + await execAndWaitForOutputToMatch('ng', ngGenerateArgs, /Unknown argument: notanoption/); } diff --git a/tests/legacy-cli/e2e/tests/generate/help-output.ts b/tests/legacy-cli/e2e/tests/generate/help-output.ts index 22b0e8a397e0..9b82e36a2ba2 100644 --- a/tests/legacy-cli/e2e/tests/generate/help-output.ts +++ b/tests/legacy-cli/e2e/tests/generate/help-output.ts @@ -1,21 +1,22 @@ -import {join} from 'path'; -import {ng, ProcessOutput} from '../../utils/process'; -import {writeMultipleFiles, createDir} from '../../utils/fs'; +import { join } from 'path'; +import { ng, ProcessOutput } from '../../utils/process'; +import { writeMultipleFiles, createDir } from '../../utils/fs'; import { updateJsonFile } from '../../utils/project'; - -export default function() { +export default function () { // setup temp collection const genRoot = join('node_modules/fake-schematics/'); - return Promise.resolve() - .then(() => createDir(genRoot)) - .then(() => writeMultipleFiles({ - [join(genRoot, 'package.json')]: ` + return ( + Promise.resolve() + .then(() => createDir(genRoot)) + .then(() => + writeMultipleFiles({ + [join(genRoot, 'package.json')]: ` { "schematics": "./collection.json" }`, - [join(genRoot, 'collection.json')]: ` + [join(genRoot, 'collection.json')]: ` { "schematics": { "fake": { @@ -25,11 +26,12 @@ export default function() { }, } }`, - [join(genRoot, 'fake-schema.json')]: ` + [join(genRoot, 'fake-schema.json')]: ` { "$id": "FakeSchema", "title": "Fake Schema", "type": "object", + "required": ["a"], "properties": { "b": { "type": "string", @@ -59,47 +61,50 @@ export default function() { "type": "string", "description": "optB" } - }, - "required": [] + } }`, - [join(genRoot, 'fake.js')]: ` + [join(genRoot, 'fake.js')]: ` function def(options) { return (host, context) => { return host; }; } exports.default = def; - `}, - )) - .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 (1).'); - } - if (!/opt-a[\s\S]*opt-b[\s\S]*opt-c/.test(stdout)) { - throw new Error('Help signature options are incorrect.'); - } - }) - // set up default collection. - .then(() => updateJsonFile('angular.json', json => { - json.cli = json.cli || {} as any; - json.cli.defaultCollection = 'fake-schematics'; - })) - .then(() => ng('generate', 'fake', '--help')) - // verify same output - .then(({stdout}) => { - if (!/ng generate 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.'); - } - }) + `, + }), + ) + .then(() => ng('generate', 'fake-schematics:fake', '--help')) + .then(({ stdout }) => { + if (!/ng generate fake-schematics:fake \[b\]/.test(stdout)) { + 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.'); + } + }) + // set up default collection. + .then(() => + updateJsonFile('angular.json', (json) => { + json.cli = json.cli || ({} as any); + json.cli.defaultCollection = 'fake-schematics'; + }), + ) + .then(() => ng('generate', 'fake', '--help')) + // verify same output + .then(({ stdout }) => { + if (!/ng generate fake \[b\]/.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.'); + } + }) - // should print all the available schematics in a collection - // when a collection has more than 1 schematic - .then(() => writeMultipleFiles({ - [join(genRoot, 'collection.json')]: ` + // should print all the available schematics in a collection + // when a collection has more than 1 schematic + .then(() => + writeMultipleFiles({ + [join(genRoot, 'collection.json')]: ` { "schematics": { "fake": { @@ -114,13 +119,13 @@ export default function() { }, } }`, - })) - .then(() => ng('generate', '--help')) - .then(({stdout}) => { - if (!/Collection \"fake-schematics\" \(default\):[\s\S]*fake[\s\S]*fake-two/.test(stdout)) { - throw new Error( - `Help result is wrong, it didn't contain all the schematics.`); - } - }); - + }), + ) + .then(() => ng('generate', '--help')) + .then(({ stdout }) => { + if (!/fake[\s\S]*fake-two/.test(stdout)) { + throw new Error(`Help result is wrong, it didn't contain all the schematics.`); + } + }) + ); } diff --git a/tests/legacy-cli/e2e/tests/generate/library/library-consumption-ivy-full.ts b/tests/legacy-cli/e2e/tests/generate/library/library-consumption-ivy-full.ts index 27443966a96c..d25092d1b3da 100644 --- a/tests/legacy-cli/e2e/tests/generate/library/library-consumption-ivy-full.ts +++ b/tests/legacy-cli/e2e/tests/generate/library/library-consumption-ivy-full.ts @@ -97,7 +97,7 @@ export default async function () { async function runTests(): Promise { // Check that the tests succeeds both with named project, unnamed (should test app), and prod. await ng('e2e'); - await ng('e2e', 'test-project', '--devServerTarget=test-project:serve:production'); + await ng('e2e', 'test-project', '--dev-server-target=test-project:serve:production'); // Validate that sourcemaps for the library exists. await ng('build', '--configuration=development'); diff --git a/tests/legacy-cli/e2e/tests/generate/library/library-consumption-ivy-partial.ts b/tests/legacy-cli/e2e/tests/generate/library/library-consumption-ivy-partial.ts index 4ca8f3fb3ad7..c3ecbab506cb 100644 --- a/tests/legacy-cli/e2e/tests/generate/library/library-consumption-ivy-partial.ts +++ b/tests/legacy-cli/e2e/tests/generate/library/library-consumption-ivy-partial.ts @@ -97,7 +97,7 @@ export default async function () { async function runTests(): Promise { // Check that the tests succeeds both with named project, unnamed (should test app), and prod. await ng('e2e'); - await ng('e2e', 'test-project', '--devServerTarget=test-project:serve:production'); + await ng('e2e', 'test-project', '--dev-server-target=test-project:serve:production'); // Validate that sourcemaps for the library exists. await ng('build', '--configuration=development'); diff --git a/tests/legacy-cli/e2e/tests/generate/library/library-consumption-ve.ts b/tests/legacy-cli/e2e/tests/generate/library/library-consumption-ve.ts index 7341457faafa..05aa8cf54ae3 100644 --- a/tests/legacy-cli/e2e/tests/generate/library/library-consumption-ve.ts +++ b/tests/legacy-cli/e2e/tests/generate/library/library-consumption-ve.ts @@ -105,7 +105,7 @@ export default async function () { async function runTests(): Promise { // Check that the tests succeeds both with named project, unnamed (should test app), and prod. await ng('e2e'); - await ng('e2e', 'test-project', '--devServerTarget=test-project:serve:production'); + await ng('e2e', 'test-project', '--dev-server-target=test-project:serve:production'); // Validate that sourcemaps for the library exists. await ng('build', '--configuration=development'); diff --git a/tests/legacy-cli/e2e/tests/i18n/ivy-localize-app-shell.ts b/tests/legacy-cli/e2e/tests/i18n/ivy-localize-app-shell.ts index 5f72c6dd7470..d029e6b138b6 100644 --- a/tests/legacy-cli/e2e/tests/i18n/ivy-localize-app-shell.ts +++ b/tests/legacy-cli/e2e/tests/i18n/ivy-localize-app-shell.ts @@ -29,7 +29,7 @@ export default async function () { }); await appendToFile('src/app/app.component.html', ''); - await ng('generate', 'appShell', '--project', 'test-project'); + await ng('generate', 'app-shell', '--project', 'test-project'); if (isSnapshotBuild) { await updateJsonFile('package.json', (packageJson) => { diff --git a/tests/legacy-cli/e2e/tests/i18n/ivy-localize-basehref.ts b/tests/legacy-cli/e2e/tests/i18n/ivy-localize-basehref.ts index 4aa4d5a3cdbb..fb74452a5ae4 100644 --- a/tests/legacy-cli/e2e/tests/i18n/ivy-localize-basehref.ts +++ b/tests/legacy-cli/e2e/tests/i18n/ivy-localize-basehref.ts @@ -17,12 +17,12 @@ const baseHrefs = { de: '', }; -export default async function() { +export default async function () { // Setup i18n tests and config. await setupI18nConfig(); // Update angular.json - await updateJsonFile('angular.json', workspaceJson => { + await updateJsonFile('angular.json', (workspaceJson) => { const appProject = workspaceJson.projects['test-project']; // tslint:disable-next-line: no-any const i18n: Record = appProject.i18n; @@ -64,8 +64,8 @@ export default async function() { await ng( 'e2e', `--configuration=${lang}`, - '--devServerTarget=', - `--baseUrl=http://localhost:4200${baseHrefs[lang] || '/'}`, + '--dev-server-target=', + `--base-url=http://localhost:4200${baseHrefs[lang] || '/'}`, ); } finally { server.close(); @@ -73,7 +73,7 @@ export default async function() { } // Update angular.json - await updateJsonFile('angular.json', workspaceJson => { + await updateJsonFile('angular.json', (workspaceJson) => { const appArchitect = workspaceJson.projects['test-project'].architect; appArchitect['build'].options.baseHref = '/test/'; @@ -94,8 +94,8 @@ export default async function() { await ng( 'e2e', `--configuration=${lang}`, - '--devServerTarget=', - `--baseUrl=http://localhost:4200/test${baseHrefs[lang] || '/'}`, + '--dev-server-target=', + `--base-url=http://localhost:4200/test${baseHrefs[lang] || '/'}`, ); } finally { server.close(); @@ -106,6 +106,9 @@ export default async function() { await ng('build', '--base-href', 'http://www.domain.com/', '--configuration=development'); for (const { lang, outputPath } of langTranslations) { // Verify the HTML base HREF attribute is present - await expectFileToMatch(`${outputPath}/index.html`, `href="http://www.domain.com${baseHrefs[lang] || '/'}"`); + await expectFileToMatch( + `${outputPath}/index.html`, + `href="http://www.domain.com${baseHrefs[lang] || '/'}"`, + ); } } diff --git a/tests/legacy-cli/e2e/tests/i18n/ivy-localize-es2015.ts b/tests/legacy-cli/e2e/tests/i18n/ivy-localize-es2015.ts index 287f826ff457..00f276fbbe2b 100644 --- a/tests/legacy-cli/e2e/tests/i18n/ivy-localize-es2015.ts +++ b/tests/legacy-cli/e2e/tests/i18n/ivy-localize-es2015.ts @@ -41,8 +41,8 @@ export default async function () { await ng( 'e2e', `--configuration=${lang}`, - '--devServerTarget=', - `--baseUrl=http://localhost:4200/${lang}/`, + '--dev-server-target=', + `--base-url=http://localhost:4200/${lang}/`, ); } finally { server.close(); diff --git a/tests/legacy-cli/e2e/tests/i18n/ivy-localize-es5.ts b/tests/legacy-cli/e2e/tests/i18n/ivy-localize-es5.ts index 07a8b639a4aa..8e996d396e85 100644 --- a/tests/legacy-cli/e2e/tests/i18n/ivy-localize-es5.ts +++ b/tests/legacy-cli/e2e/tests/i18n/ivy-localize-es5.ts @@ -49,8 +49,8 @@ export default async function () { await ng( 'e2e', `--configuration=${lang}`, - '--devServerTarget=', - `--baseUrl=http://localhost:4200/${lang}/`, + '--dev-server-target=', + `--base-url=http://localhost:4200/${lang}/`, ); } finally { server.close(); diff --git a/tests/legacy-cli/e2e/tests/i18n/ivy-localize-locale-data-augment.ts b/tests/legacy-cli/e2e/tests/i18n/ivy-localize-locale-data-augment.ts index 3c0d8b59a39f..b1a9eaf307db 100644 --- a/tests/legacy-cli/e2e/tests/i18n/ivy-localize-locale-data-augment.ts +++ b/tests/legacy-cli/e2e/tests/i18n/ivy-localize-locale-data-augment.ts @@ -1,14 +1,20 @@ -import { expectFileToMatch, prependToFile, readFile, replaceInFile, writeFile } from '../../utils/fs'; +import { + expectFileToMatch, + prependToFile, + readFile, + replaceInFile, + writeFile, +} from '../../utils/fs'; import { ng } from '../../utils/process'; import { updateJsonFile } from '../../utils/project'; import { externalServer, langTranslations, setupI18nConfig } from './setup'; -export default async function() { +export default async function () { // Setup i18n tests and config. await setupI18nConfig(); // Update angular.json to only localize one locale - await updateJsonFile('angular.json', workspaceJson => { + await updateJsonFile('angular.json', (workspaceJson) => { const appProject = workspaceJson.projects['test-project']; appProject.architect['build'].options.localize = ['fr']; }); @@ -19,7 +25,7 @@ export default async function() { // Augment the locale data and import into the main application file const localeData = await readFile('node_modules/@angular/common/locales/global/fr.js'); await writeFile('src/fr-changed.js', localeData.replace('janvier', 'changed-janvier')); - await prependToFile('src/main.ts', 'import \'./fr-changed.js\';\n'); + await prependToFile('src/main.ts', "import './fr-changed.js';\n"); // Run a build and test await ng('build'); @@ -42,8 +48,8 @@ export default async function() { await ng( 'e2e', `--configuration=${lang}`, - '--devServerTarget=', - `--baseUrl=http://localhost:4200/${lang}/`, + '--dev-server-target=', + `--base-url=http://localhost:4200/${lang}/`, ); } finally { server.close(); diff --git a/tests/legacy-cli/e2e/tests/i18n/ivy-localize-server.ts b/tests/legacy-cli/e2e/tests/i18n/ivy-localize-server.ts index 5a930a9dd75e..82447344ad8e 100644 --- a/tests/legacy-cli/e2e/tests/i18n/ivy-localize-server.ts +++ b/tests/legacy-cli/e2e/tests/i18n/ivy-localize-server.ts @@ -116,7 +116,7 @@ export default async function () { const server = i18nApp(lang).listen(4200, 'localhost'); try { // Execute without a devserver. - await ng('e2e', `--configuration=${lang}`, '--devServerTarget='); + await ng('e2e', `--configuration=${lang}`, '--dev-server-target='); } finally { server.close(); } diff --git a/tests/legacy-cli/e2e/tests/i18n/ivy-localize-serviceworker.ts b/tests/legacy-cli/e2e/tests/i18n/ivy-localize-serviceworker.ts index dfb32d768289..297e4225d179 100644 --- a/tests/legacy-cli/e2e/tests/i18n/ivy-localize-serviceworker.ts +++ b/tests/legacy-cli/e2e/tests/i18n/ivy-localize-serviceworker.ts @@ -180,7 +180,7 @@ export default async function () { ); // Execute without a devserver. - await ng('e2e', '--devServerTarget='); + await ng('e2e', '--dev-server-target='); } finally { server.close(); } diff --git a/tests/legacy-cli/e2e/tests/misc/browsers.ts b/tests/legacy-cli/e2e/tests/misc/browsers.ts index a33c9146e394..370490644396 100644 --- a/tests/legacy-cli/e2e/tests/misc/browsers.ts +++ b/tests/legacy-cli/e2e/tests/misc/browsers.ts @@ -47,8 +47,8 @@ export default async function () { await ng( 'e2e', 'test-project', - '--protractorConfig=e2e/protractor-saucelabs.conf.js', - '--devServerTarget=', + '--protractor-config=e2e/protractor-saucelabs.conf.js', + '--dev-server-target=', ); } finally { server.close(); diff --git a/tests/legacy-cli/e2e/tests/misc/npm-7.ts b/tests/legacy-cli/e2e/tests/misc/npm-7.ts index 1789210a534a..1692096638a4 100644 --- a/tests/legacy-cli/e2e/tests/misc/npm-7.ts +++ b/tests/legacy-cli/e2e/tests/misc/npm-7.ts @@ -38,7 +38,7 @@ export default async function () { await npm('install', '--global', 'npm@7.4.0'); // Ensure `ng add` shows npm warning - const { message: stderrAdd } = await expectToFail(() => ng('add')); + const { stderr: stderrAdd } = await ng('add', '@angular/localize'); if (!stderrAdd.includes(warningText)) { throw new Error('ng add expected to show npm version warning.'); } diff --git a/tests/legacy-cli/e2e/tests/misc/version.ts b/tests/legacy-cli/e2e/tests/misc/version.ts index c39c3167bf0b..4ad57adc9726 100644 --- a/tests/legacy-cli/e2e/tests/misc/version.ts +++ b/tests/legacy-cli/e2e/tests/misc/version.ts @@ -3,14 +3,6 @@ import { ng } from '../../utils/process'; export default async function () { const { stdout: commandOutput } = await ng('version'); - const { stdout: optionOutput } = await ng('--version'); - if (!optionOutput.includes('Angular CLI:')) { - throw new Error('version not displayed'); - } - - if (commandOutput !== optionOutput) { - throw new Error('version variants have differing output'); - } if (commandOutput.includes(process.versions.node + ' (Unsupported)')) { throw new Error('Node version should not show unsupported entry'); diff --git a/yarn.lock b/yarn.lock index a9d983a06299..597b61bbf691 100644 --- a/yarn.lock +++ b/yarn.lock @@ -178,7 +178,6 @@ "@angular/dev-infra-private@https://github.com/angular/dev-infra-private-builds.git#5e484f9c4ab6b47f84263d115d6cf9e13ce4f32a": version "0.0.0-104c49ad795097101ab3aa268a8e9af2cdf04a8d" - uid "5e484f9c4ab6b47f84263d115d6cf9e13ce4f32a" resolved "https://github.com/angular/dev-infra-private-builds.git#5e484f9c4ab6b47f84263d115d6cf9e13ce4f32a" dependencies: "@angular-devkit/build-angular" "14.0.0-next.3" @@ -2218,7 +2217,7 @@ resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.0.tgz#0c60e537fa790f5f9472ed2776c2b71ec117351b" integrity sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA== -"@types/yargs@^17.0.0": +"@types/yargs@^17.0.0", "@types/yargs@^17.0.8": version "17.0.9" resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.9.tgz#f1f931a4e5ae2c0134dea10f501088636a50b46a" integrity sha512-Ci8+4/DOtkHRylcisKmVMtmVO5g7weUVCKcsu1sJvF1bn0wExTmbHmhFKj7AnEm0de800iovGhdSKzYnzbaHpg== @@ -9297,7 +9296,6 @@ sass@1.49.9, sass@^1.49.0: "sauce-connect-proxy@https://saucelabs.com/downloads/sc-4.6.4-linux.tar.gz": version "0.0.0" - uid "992e2cb0d91e54b27a4f5bbd2049f3b774718115" resolved "https://saucelabs.com/downloads/sc-4.6.4-linux.tar.gz#992e2cb0d91e54b27a4f5bbd2049f3b774718115" saucelabs@^1.5.0: @@ -11158,6 +11156,19 @@ yargs@17.1.1: y18n "^5.0.5" yargs-parser "^20.2.2" +yargs@17.3.1, yargs@^17.0.0, yargs@^17.2.1: + version "17.3.1" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.3.1.tgz#da56b28f32e2fd45aefb402ed9c26f42be4c07b9" + integrity sha512-WUANQeVgjLbNsEmGk20f+nlHgOqzRFpiGWVaBrYGYIGANIIu3lWjoyi0fNlFmJkvfhCZ6BXINe7/W2O2bV4iaA== + dependencies: + cliui "^7.0.2" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.3" + y18n "^5.0.5" + yargs-parser "^21.0.0" + yargs@^15.3.1, yargs@^15.4.1: version "15.4.1" resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8" @@ -11188,19 +11199,6 @@ yargs@^16.0.0, yargs@^16.1.1: y18n "^5.0.5" yargs-parser "^20.2.2" -yargs@^17.0.0, yargs@^17.2.1: - version "17.3.1" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.3.1.tgz#da56b28f32e2fd45aefb402ed9c26f42be4c07b9" - integrity sha512-WUANQeVgjLbNsEmGk20f+nlHgOqzRFpiGWVaBrYGYIGANIIu3lWjoyi0fNlFmJkvfhCZ6BXINe7/W2O2bV4iaA== - dependencies: - cliui "^7.0.2" - escalade "^3.1.1" - get-caller-file "^2.0.5" - require-directory "^2.1.1" - string-width "^4.2.3" - y18n "^5.0.5" - yargs-parser "^21.0.0" - yauzl@^2.10.0: version "2.10.0" resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9" From 851de8ede9518cb2a7ad5bd7d1f9e50644823692 Mon Sep 17 00:00:00 2001 From: Alan Agius Date: Mon, 28 Feb 2022 11:17:47 +0100 Subject: [PATCH 2/8] refactor(@angular/cli): introspect yargs to generate JSON Help With this change we update yargs help method to output help in JSON format which is needed to generate the documents that are used to generate AIO man pages. --- .../angular/cli/lib/cli/command-runner.ts | 31 +++- .../command-builder/command-module.ts | 13 +- .../utilities/command-builder/json-help.ts | 145 ++++++++++++++++++ scripts/json-help.ts | 93 +++++++++++ scripts/snapshots.ts | 23 +-- .../e2e/tests/commands/help/help-json.ts | 66 ++++++-- 6 files changed, 331 insertions(+), 40 deletions(-) create mode 100644 packages/angular/cli/utilities/command-builder/json-help.ts create mode 100644 scripts/json-help.ts diff --git a/packages/angular/cli/lib/cli/command-runner.ts b/packages/angular/cli/lib/cli/command-runner.ts index 11744fa7bc85..f92a0f179a62 100644 --- a/packages/angular/cli/lib/cli/command-runner.ts +++ b/packages/angular/cli/lib/cli/command-runner.ts @@ -27,7 +27,12 @@ import { TestCommandModule } from '../../commands/test/cli'; import { UpdateCommandModule } from '../../commands/update/cli'; import { VersionCommandModule } from '../../commands/version/cli'; import { colors } from '../../utilities/color'; -import { CommandContext, CommandModuleError } from '../../utilities/command-builder/command-module'; +import { + CommandContext, + CommandModuleError, + CommandScope, +} from '../../utilities/command-builder/command-module'; +import { jsonHelpUsage } from '../../utilities/command-builder/json-help'; import { AngularWorkspace } from '../../utilities/config'; const COMMANDS = [ @@ -63,8 +68,9 @@ export async function runCommand( $0, _: positional, help = false, + jsonHelp = false, ...rest - } = yargsParser(args, { boolean: ['help'], alias: { 'collection': 'c' } }); + } = yargsParser(args, { boolean: ['help', 'json-help'], alias: { 'collection': 'c' } }); const context: CommandContext = { workspace, @@ -82,13 +88,27 @@ export async function runCommand( let localYargs = yargs(args); for (const CommandModule of COMMANDS) { + if (!jsonHelp) { + // Skip scope validation when running with '--json-help' since it's easier to generate the output for all commands this way. + const scope = CommandModule.scope; + if ((scope === CommandScope.In && !workspace) || (scope === CommandScope.Out && workspace)) { + continue; + } + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any const commandModule = new CommandModule(context) as any; + const describe = jsonHelp ? commandModule.fullDescribe : commandModule.describe; localYargs = localYargs.command({ command: commandModule.command, aliases: commandModule.aliases, - describe: commandModule.describe, + describe: + // We cannot add custom fields in help, such as long command description which is used in AIO. + // Therefore, we get around this by adding a complex object as a string which we later parse when geneerating the help files. + describe !== undefined && typeof describe === 'object' + ? JSON.stringify(describe) + : describe, deprecated: commandModule.deprecated, builder: (x) => commandModule.builder(x), handler: ({ _, $0, ...options }) => { @@ -103,6 +123,11 @@ export async function runCommand( }); } + if (jsonHelp) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (localYargs as any).getInternalMethods().getUsageInstance().help = () => jsonHelpUsage(); + } + await localYargs .scriptName('ng') // https://github.com/yargs/yargs/blob/main/docs/advanced.md#customizing-yargs-parser diff --git a/packages/angular/cli/utilities/command-builder/command-module.ts b/packages/angular/cli/utilities/command-builder/command-module.ts index 5e7332542e29..fef0e34d1882 100644 --- a/packages/angular/cli/utilities/command-builder/command-module.ts +++ b/packages/angular/cli/utilities/command-builder/command-module.ts @@ -62,6 +62,7 @@ export interface CommandModuleImplementation export interface FullDescribe { describe?: string; longDescription?: string; + longDescriptionRelativePath?: string; } export abstract class CommandModule implements CommandModuleImplementation { @@ -86,9 +87,15 @@ export abstract class CommandModule implements CommandModuleI ? false : { describe: this.describe, - longDescription: this.longDescriptionPath - ? readFileSync(this.longDescriptionPath, 'utf8') - : undefined, + ...(this.longDescriptionPath + ? { + longDescriptionRelativePath: path.relative( + path.join(__dirname, '../../../../'), + this.longDescriptionPath, + ), + longDescription: readFileSync(this.longDescriptionPath, 'utf8'), + } + : {}), }; } diff --git a/packages/angular/cli/utilities/command-builder/json-help.ts b/packages/angular/cli/utilities/command-builder/json-help.ts new file mode 100644 index 000000000000..c943194da495 --- /dev/null +++ b/packages/angular/cli/utilities/command-builder/json-help.ts @@ -0,0 +1,145 @@ +/** + * @license + * Copyright Google LLC 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 yargs from 'yargs'; +import { FullDescribe } from './command-module'; + +export interface JsonHelp { + name: string; + description?: string; + command: string; + longDescription?: string; + longDescriptionRelativePath?: string; + options: JsonHelpOption[]; + subcommands?: { + name: string; + description: string; + aliases: string[]; + deprecated: string | boolean; + }[]; +} + +interface JsonHelpOption { + name: string; + type?: string; + deprecated: boolean | string; + aliases?: string[]; + default?: string; + required?: boolean; + positional?: number; + enum?: string[]; + description?: string; +} + +export function jsonHelpUsage(): string { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const localYargs = yargs as any; + const { + deprecatedOptions, + alias: aliases, + array, + string, + boolean, + number, + choices, + demandedOptions, + default: defaultVal, + hiddenOptions = [], + } = localYargs.getOptions(); + + const internalMethods = localYargs.getInternalMethods(); + const usageInstance = internalMethods.getUsageInstance(); + const context = internalMethods.getContext(); + const descriptions = usageInstance.getDescriptions(); + const groups = localYargs.getGroups(); + const positional = groups[usageInstance.getPositionalGroupName()] as string[] | undefined; + + const hidden = new Set(hiddenOptions); + const normalizeOptions: JsonHelpOption[] = []; + const allAliases = new Set([...Object.values(aliases).flat()]); + + for (const [names, type] of [ + [array, 'array'], + [string, 'string'], + [boolean, 'boolean'], + [number, 'number'], + ]) { + for (const name of names) { + if (allAliases.has(name) || hidden.has(name)) { + // Ignore hidden, aliases and already visited option. + continue; + } + + const positionalIndex = positional?.indexOf(name) ?? -1; + const alias = aliases[name]; + + normalizeOptions.push({ + name, + type, + deprecated: deprecatedOptions[name], + aliases: alias?.length > 0 ? alias : undefined, + default: defaultVal[name], + required: demandedOptions[name], + enum: choices[name], + description: descriptions[name]?.replace('__yargsString__:', ''), + positional: positionalIndex >= 0 ? positionalIndex : undefined, + }); + } + } + + // https://github.com/yargs/yargs/blob/00e4ebbe3acd438e73fdb101e75b4f879eb6d345/lib/usage.ts#L124 + const subcommands = ( + usageInstance.getCommands() as [ + name: string, + description: string, + isDefault: boolean, + aliases: string[], + deprecated: string | boolean, + ][] + ) + .map(([name, description, _, aliases, deprecated]) => ({ + name: name.split(' ', 1)[0], + command: name, + description, + aliases, + deprecated, + })) + .sort((a, b) => a.name.localeCompare(b.name)); + + const parseDescription = (rawDescription: string) => { + try { + const { + longDescription, + describe: description, + longDescriptionRelativePath, + } = JSON.parse(rawDescription) as FullDescribe; + + return { + description, + longDescriptionRelativePath, + longDescription, + }; + } catch { + return { + description: rawDescription, + }; + } + }; + + const [command, rawDescription] = usageInstance.getUsage()[0] ?? []; + + const output: JsonHelp = { + name: [...context.commands].pop(), + command: command?.replace('$0', localYargs['$0']), + ...parseDescription(rawDescription), + options: normalizeOptions.sort((a, b) => a.name.localeCompare(b.name)), + subcommands: subcommands.length ? subcommands : undefined, + }; + + return JSON.stringify(output, undefined, 2); +} diff --git a/scripts/json-help.ts b/scripts/json-help.ts new file mode 100644 index 000000000000..1cab27bf727d --- /dev/null +++ b/scripts/json-help.ts @@ -0,0 +1,93 @@ +/** + * @license + * Copyright Google LLC 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 { logging } from '@angular-devkit/core'; +import { spawn } from 'child_process'; +import { promises as fs } from 'fs'; +import * as os from 'os'; +import { JsonHelp } from 'packages/angular/cli/utilities/command-builder/json-help'; +import * as path from 'path'; +import { packages } from '../lib/packages'; +import create from './create'; + +export default async function (opts = {}, logger: logging.Logger) { + logger.info('Creating temporary project...'); + const newProjectTempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'angular-cli-create-')); + const newProjectName = 'help-project'; + const newProjectRoot = path.join(newProjectTempRoot, newProjectName); + await create({ _: [newProjectName] }, logger.createChild('create'), newProjectTempRoot); + + logger.info('Gathering JSON Help...'); + const ngPath = path.join(newProjectRoot, 'node_modules/.bin/ng'); + const helpOutputRoot = path.join(packages['@angular/cli'].dist, 'help'); + await fs.mkdir(helpOutputRoot); + + const runNgCommandJsonHelp = async (args: string[]) => { + const process = spawn(ngPath, [...args, '--json-help', '--help'], { + cwd: newProjectRoot, + stdio: ['ignore', 'pipe', 'inherit'], + }); + + let result = ''; + process.stdout.on('data', (data) => { + result += data.toString(); + }); + + return new Promise((resolve, reject) => { + process + .on('close', (code) => { + if (code === 0) { + resolve(JSON.parse(result.trim())); + } else { + reject( + new Error( + `Command failed: ${ngPath} ${args.map((x) => JSON.stringify(x)).join(', ')}`, + ), + ); + } + }) + .on('error', (err) => reject(err)); + }); + }; + + const { subcommands: commands = [] } = await runNgCommandJsonHelp([]); + const commandsHelp = commands.map((command) => + runNgCommandJsonHelp([command.name]).then((c) => ({ + ...command, + ...c, + })), + ); + + for await (const command of commandsHelp) { + const commandName = command.name; + const commandOptionNames = new Set([...command.options.map(({ name }) => name)]); + + const subCommandsHelp = command.subcommands?.map((subcommand) => + runNgCommandJsonHelp([command.name, subcommand.name]).then((s) => ({ + ...s, + ...subcommand, + // Filter options which are inherited from the parent command. + // Ex: `interactive` in `ng generate lib`. + options: s.options.filter((o) => !commandOptionNames.has(o.name)), + })), + ); + + const jsonOutput = JSON.stringify( + { + ...command, + subcommands: subCommandsHelp ? await Promise.all(subCommandsHelp) : undefined, + }, + undefined, + 2, + ); + + const filePath = path.join(helpOutputRoot, commandName + '.json'); + await fs.writeFile(filePath, jsonOutput); + logger.info(filePath); + } +} diff --git a/scripts/snapshots.ts b/scripts/snapshots.ts index c1de7a60956c..9289837ee8ac 100644 --- a/scripts/snapshots.ts +++ b/scripts/snapshots.ts @@ -13,7 +13,7 @@ import * as os from 'os'; import * as path from 'path'; import { PackageInfo, packages } from '../lib/packages'; import build from './build-bazel'; -import create from './create'; +import jsonHelp from './json-help'; // Added to the README.md of the snapshot. This is markdown. const readmeHeaderFn = (pkg: PackageInfo) => ` @@ -164,31 +164,12 @@ export default async function (opts: SnapshotsOptions, logger: logging.Logger) { _exec('git', ['config', '--global', 'push.default', 'simple'], {}, logger); } - // Creating a new project and reading the help. - logger.info('Creating temporary project...'); - const newProjectTempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'angular-cli-create-')); - const newProjectName = 'help-project'; - const newProjectRoot = path.join(newProjectTempRoot, newProjectName); - await create({ _: [newProjectName] }, logger.createChild('create'), newProjectTempRoot); + await jsonHelp(undefined, logger); // Run build. logger.info('Building...'); await build({ snapshot: true }, logger.createChild('build')); - logger.info('Gathering JSON Help...'); - const ngPath = path.join(newProjectRoot, 'node_modules/.bin/ng'); - const helpOutputRoot = path.join(packages['@angular/cli'].dist, 'help'); - fs.mkdirSync(helpOutputRoot); - const commands = require('../packages/angular/cli/commands.json'); - for (const commandName of Object.keys(commands)) { - const options = { cwd: newProjectRoot }; - const childLogger = logger.createChild(commandName); - const stdout = _exec(ngPath, [commandName, '--help=json'], options, childLogger); - // Make sure the output is JSON before printing it, and format it as well. - const jsonOutput = JSON.stringify(JSON.parse(stdout.trim()), undefined, 2); - fs.writeFileSync(path.join(helpOutputRoot, commandName + '.json'), jsonOutput); - } - if (!githubToken) { logger.info('No token given, skipping actual publishing...'); diff --git a/tests/legacy-cli/e2e/tests/commands/help/help-json.ts b/tests/legacy-cli/e2e/tests/commands/help/help-json.ts index 898adfbe5bc6..9c54564fa850 100644 --- a/tests/legacy-cli/e2e/tests/commands/help/help-json.ts +++ b/tests/legacy-cli/e2e/tests/commands/help/help-json.ts @@ -1,19 +1,59 @@ import { silentNg } from '../../../utils/process'; +export default async function () { + // This test is use as a sanity check. + const addHelpOutputSnapshot = JSON.stringify({ + name: 'analytics', + command: 'ng analytics ', + description: + 'Configures the gathering of Angular CLI usage metrics. See https://angular.io/cli/usage-analytics-gathering.', + longDescriptionRelativePath: '@angular/cli/src/commands/analytics/long-description.md', + longDescription: + 'The value of `setting-or-project` is one of the following.\n\n- `on`: Enables analytics gathering and reporting for the user.\n- `off`: Disables analytics gathering and reporting for the user.\n- `ci`: Enables analytics and configures reporting for use with Continuous Integration,\n which uses a common CI user.\n- `prompt`: Prompts the user to set the status interactively.\n- `project`: Sets the default status for the project to the `project-setting` value, which can be any of the other values. The `project-setting` argument is ignored for all other values of `setting_or_project`.\n', + options: [ + { + name: 'help', + type: 'boolean', + description: 'Shows a help message for this command in the console.', + }, + { + name: 'project-setting', + type: 'string', + enum: ['on', 'off', 'prompt'], + description: 'Sets the default analytics enablement status for the project.', + positional: 1, + }, + { + name: 'setting-or-project', + type: 'string', + enum: ['on', 'off', 'ci', 'prompt'], + description: + 'Directly enables or disables all usage analytics for the user, or prompts the user to set the status interactively, or sets the default status for the project.', + positional: 0, + }, + ], + }); -export default async function() { - const commands = require('@angular/cli/commands.json'); - for (const commandName of Object.keys(commands)) { - const { stdout } = await silentNg(commandName, '--help=json'); + const { stdout } = await silentNg('analytics', '--help', '--json-help'); + const output = JSON.stringify(JSON.parse(stdout.trim())); - if (stdout.trim()) { - JSON.parse(stdout, (key, value) => { - if (key === 'name' && /[A-Z]/.test(value)) { - throw new Error(`Option named '${value}' is not kebab case.`); - } - }); - } else { - console.warn(`No JSON output for command [${commandName}].`); - } + if (output !== addHelpOutputSnapshot) { + throw new Error( + `ng analytics JSON help output didn\'t match snapshot.\n\nExpected "${output}" to be "${addHelpOutputSnapshot}".`, + ); + } + + const { stdout: stdout2 } = await silentNg('--help', '--json-help'); + try { + JSON.parse(stdout2.trim()); + } catch (error) { + throw new Error(`'ng --help ---json-help' failed to return JSON.\n${error.message}`); + } + + const { stdout: stdout3 } = await silentNg('generate', '--help', '--json-help'); + try { + JSON.parse(stdout3.trim()); + } catch (error) { + throw new Error(`'ng generate --help ---json-help' failed to return JSON.\n${error.message}`); } } From c65a0670ff1d956d250c0631646c6d7eed1be604 Mon Sep 17 00:00:00 2001 From: Alan Agius Date: Mon, 28 Feb 2022 11:18:50 +0100 Subject: [PATCH 3/8] build: update UA validation script With this change we update the UA usage script to read schemas directly insteads of relying on JSON help. --- scripts/validate-user-analytics.ts | 85 ++++++++++-------------------- 1 file changed, 28 insertions(+), 57 deletions(-) diff --git a/scripts/validate-user-analytics.ts b/scripts/validate-user-analytics.ts index 754a1a3b13c8..ced9ab28460e 100644 --- a/scripts/validate-user-analytics.ts +++ b/scripts/validate-user-analytics.ts @@ -6,41 +6,22 @@ * found in the LICENSE file at https://angular.io/license */ -import { analytics, logging, tags } from '@angular-devkit/core'; -import { spawnSync } from 'child_process'; +import { analytics, logging, schema, strings, tags } from '@angular-devkit/core'; import * as fs from 'fs'; -import * as os from 'os'; +import { glob as globCb } from 'glob'; import * as path from 'path'; -import { CommandDescriptionMap, Option } from '../packages/angular/cli/models/interface'; -import create from './create'; +import { promisify } from 'util'; +import { packages } from '../lib/packages'; const userAnalyticsTable = require('./templates/user-analytics-table').default; const dimensionsTableRe = /([\s\S]*)/m; const metricsTableRe = /([\s\S]*)/m; -/** - * Execute a command. - * @private - */ -function _exec(command: string, args: string[], opts: { cwd?: string }, logger: logging.Logger) { - const { status, error, stdout } = spawnSync(command, args, { - stdio: ['ignore', 'pipe', 'inherit'], - ...opts, - }); - - if (status != 0) { - logger.error(`Command failed: ${command} ${args.map((x) => JSON.stringify(x)).join(', ')}`); - throw error; - } - - return stdout.toString('utf-8'); -} - async function _checkDimensions(dimensionsTable: string, logger: logging.Logger) { const data: { userAnalytics: number; type: string; name: string }[] = new Array(200); - function _updateData(userAnalytics: number, name: string, type: string) { + function updateData(userAnalytics: number, name: string, type: string) { if (data[userAnalytics]) { if (data[userAnalytics].name !== name) { logger.error(tags.stripIndents` @@ -77,47 +58,37 @@ async function _checkDimensions(dimensionsTable: string, logger: logging.Logger) `Invalid value found in enum AnalyticsDimensions: ${JSON.stringify(userAnalytics)}`, ); } - _updateData(userAnalytics, flagName, type); + updateData(userAnalytics, flagName, type); } - // Creating a new project and reading the help. - logger.info('Creating temporary project for gathering help...'); - - const newProjectTempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'angular-cli-create-')); - const newProjectName = 'help-project'; - const newProjectRoot = path.join(newProjectTempRoot, newProjectName); - await create({ _: [newProjectName] }, logger.createChild('create'), newProjectTempRoot); + logger.info('Gathering options for user-analytics...'); - const commandDescription: CommandDescriptionMap = {}; + const userAnalyticsGatherer = (obj: Object) => { + for (const [key, value] of Object.entries(obj)) { + if (value && typeof value === 'object') { + if ('x-user-analytics' in value) { + const type = + [...schema.getTypesOfSchema(value)].find((type) => type !== 'object') ?? 'string'; - logger.info('Gathering options...'); - - const commands = require('../packages/angular/cli/commands.json'); - const ngPath = path.join(newProjectRoot, 'node_modules/.bin/ng'); - for (const commandName of Object.keys(commands)) { - const options = { cwd: newProjectRoot }; - const childLogger = logger.createChild(commandName); - const stdout = _exec(ngPath, [commandName, '--help=json'], options, childLogger); - commandDescription[commandName] = JSON.parse(stdout.trim()); - } - - function _checkOptionsForAnalytics(options: Option[]) { - for (const option of options) { - if (option.subcommands) { - for (const subcommand of Object.values(option.subcommands)) { - _checkOptionsForAnalytics(subcommand.options); + updateData(value['x-user-analytics'], 'Flag: --' + strings.dasherize(key), type); + } else { + userAnalyticsGatherer(value); } } - - if (option.userAnalytics === undefined) { - continue; - } - _updateData(option.userAnalytics, 'Flag: --' + option.name, option.type); } - } + }; + + const glob = promisify(globCb); - for (const commandName of Object.keys(commandDescription)) { - _checkOptionsForAnalytics(commandDescription[commandName].options); + // Find all the schemas + const packagesPaths = Object.values(packages).map(({ root }) => root); + for (const packagePath of packagesPaths) { + const schemasPaths = await glob('**/schema.json', { cwd: packagePath }); + + for (const schemaPath of schemasPaths) { + const schema = await fs.promises.readFile(path.join(packagePath, schemaPath), 'utf8'); + userAnalyticsGatherer(JSON.parse(schema)); + } } const generatedTable = userAnalyticsTable({ flags: data }).trim(); From a409adcede5b82b0ded5c272566463c201ead7e2 Mon Sep 17 00:00:00 2001 From: Alan Agius Date: Tue, 1 Mar 2022 15:28:31 +0100 Subject: [PATCH 4/8] refactor(@angular/cli): re-organize the Angular CLI package folder structure --- .../cli/bin/postinstall/analytics-prompt.js | 2 +- packages/angular/cli/lib/cli/index.ts | 14 ++-- packages/angular/cli/lib/init.ts | 6 +- packages/angular/cli/models/command.ts | 4 +- packages/angular/cli/models/interface.ts | 2 +- .../angular/cli/models/schematic-command.ts | 12 +-- .../analytics}/analytics-collector.ts | 0 .../{models => src/analytics}/analytics.ts | 0 .../architect-command-module.ts | 68 +---------------- .../command-builder/command-module.ts | 6 +- .../command-builder}/command-runner.ts | 46 ++++++------ .../schematics-command-module.ts | 4 +- .../command-builder/utilities/architect.ts | 73 +++++++++++++++++++ .../command-builder/utilities}/json-help.ts | 2 +- .../command-builder/utilities}/json-schema.ts | 0 .../cli/{ => src}/commands/add/add-impl.ts | 8 +- .../angular/cli/{ => src}/commands/add/cli.ts | 4 +- .../commands/add/long-description.md | 0 .../cli/{ => src}/commands/analytics/cli.ts | 6 +- .../commands/analytics/long-description.md | 0 .../cli/{ => src}/commands/build/cli.ts | 4 +- .../commands/build/long-description.md | 0 .../cli/{ => src}/commands/config/cli.ts | 2 +- .../{ => src}/commands/config/config-impl.ts | 4 +- .../commands/config/long-description.md | 2 - .../cli/{ => src}/commands/deploy/cli.ts | 4 +- .../commands/deploy/long-description.md | 0 .../angular/cli/{ => src}/commands/doc/cli.ts | 2 +- .../angular/cli/{ => src}/commands/e2e/cli.ts | 4 +- .../{ => src}/commands/extract-i18n/cli.ts | 4 +- .../cli/{ => src}/commands/generate/cli.ts | 6 +- .../commands/generate/generate-impl.ts | 4 +- .../cli/{ => src}/commands/lint/cli.ts | 4 +- .../commands/lint/long-description.md | 0 .../commands/make-this-awesome/cli.ts | 5 +- .../angular/cli/{ => src}/commands/new/cli.ts | 4 +- .../cli/{ => src}/commands/new/new-impl.ts | 4 +- .../angular/cli/{ => src}/commands/run/cli.ts | 6 +- .../commands/run/long-description.md | 0 .../cli/{ => src}/commands/serve/cli.ts | 4 +- .../cli/{ => src}/commands/test/cli.ts | 4 +- .../commands/test/long-description.md | 0 .../cli/{ => src}/commands/update/cli.ts | 2 +- .../commands/update/long-description.md | 0 .../src/commands/update/schematic/index.ts | 7 +- .../{ => src}/commands/update/update-impl.ts | 13 ++-- .../cli/{ => src}/commands/version/cli.ts | 10 +-- packages/angular/cli/src/typings.ts | 6 +- .../angular/cli/{ => src}/utilities/color.ts | 0 .../angular/cli/{ => src}/utilities/config.ts | 12 +-- .../cli/{ => src}/utilities/find-up.ts | 0 .../{ => src}/utilities/install-package.ts | 4 +- .../cli/{ => src}/utilities/json-file.ts | 0 .../cli/{ => src}/utilities/log-file.ts | 0 .../cli/{ => src}/utilities/package-json.ts | 0 .../{ => src}/utilities/package-manager.ts | 2 +- .../{ => src}/utilities/package-metadata.ts | 0 .../cli/{ => src}/utilities/package-tree.ts | 0 .../cli/{ => src}/utilities/project.ts | 0 .../angular/cli/{ => src}/utilities/prompt.ts | 0 .../cli/{ => src}/utilities/spinner.ts | 0 .../angular/cli/{ => src}/utilities/tty.ts | 0 .../cli/{ => src}/utilities/version.ts | 4 +- scripts/json-help.ts | 2 +- 64 files changed, 187 insertions(+), 199 deletions(-) rename packages/angular/cli/{models => src/analytics}/analytics-collector.ts (100%) rename packages/angular/cli/{models => src/analytics}/analytics.ts (100%) rename packages/angular/cli/{utilities => src}/command-builder/architect-command-module.ts (79%) rename packages/angular/cli/{utilities => src}/command-builder/command-module.ts (97%) rename packages/angular/cli/{lib/cli => src/command-builder}/command-runner.ts (77%) rename packages/angular/cli/{utilities => src}/command-builder/schematics-command-module.ts (97%) create mode 100644 packages/angular/cli/src/command-builder/utilities/architect.ts rename packages/angular/cli/{utilities/command-builder => src/command-builder/utilities}/json-help.ts (98%) rename packages/angular/cli/{utilities/command-builder => src/command-builder/utilities}/json-schema.ts (100%) rename packages/angular/cli/{ => src}/commands/add/add-impl.ts (97%) rename packages/angular/cli/{ => src}/commands/add/cli.ts (94%) rename packages/angular/cli/{ => src}/commands/add/long-description.md (100%) rename packages/angular/cli/{ => src}/commands/analytics/cli.ts (93%) rename packages/angular/cli/{ => src}/commands/analytics/long-description.md (100%) rename packages/angular/cli/{ => src}/commands/build/cli.ts (75%) rename packages/angular/cli/{ => src}/commands/build/long-description.md (100%) rename packages/angular/cli/{ => src}/commands/config/cli.ts (96%) rename packages/angular/cli/{ => src}/commands/config/config-impl.ts (97%) rename packages/angular/cli/{ => src}/commands/config/long-description.md (82%) rename packages/angular/cli/{ => src}/commands/deploy/cli.ts (83%) rename packages/angular/cli/{ => src}/commands/deploy/long-description.md (100%) rename packages/angular/cli/{ => src}/commands/doc/cli.ts (97%) rename packages/angular/cli/{ => src}/commands/e2e/cli.ts (83%) rename packages/angular/cli/{ => src}/commands/extract-i18n/cli.ts (71%) rename packages/angular/cli/{ => src}/commands/generate/cli.ts (96%) rename packages/angular/cli/{ => src}/commands/generate/generate-impl.ts (93%) rename packages/angular/cli/{ => src}/commands/lint/cli.ts (81%) rename packages/angular/cli/{ => src}/commands/lint/long-description.md (100%) rename packages/angular/cli/{ => src}/commands/make-this-awesome/cli.ts (90%) rename packages/angular/cli/{ => src}/commands/new/cli.ts (91%) rename packages/angular/cli/{ => src}/commands/new/new-impl.ts (88%) rename packages/angular/cli/{ => src}/commands/run/cli.ts (93%) rename packages/angular/cli/{ => src}/commands/run/long-description.md (100%) rename packages/angular/cli/{ => src}/commands/serve/cli.ts (72%) rename packages/angular/cli/{ => src}/commands/test/cli.ts (73%) rename packages/angular/cli/{ => src}/commands/test/long-description.md (100%) rename packages/angular/cli/{ => src}/commands/update/cli.ts (98%) rename packages/angular/cli/{ => src}/commands/update/long-description.md (100%) rename packages/angular/cli/{ => src}/commands/update/update-impl.ts (98%) rename packages/angular/cli/{ => src}/commands/version/cli.ts (95%) rename packages/angular/cli/{ => src}/utilities/color.ts (100%) rename packages/angular/cli/{ => src}/utilities/config.ts (97%) rename packages/angular/cli/{ => src}/utilities/find-up.ts (100%) rename packages/angular/cli/{ => src}/utilities/install-package.ts (98%) rename packages/angular/cli/{ => src}/utilities/json-file.ts (100%) rename packages/angular/cli/{ => src}/utilities/log-file.ts (100%) rename packages/angular/cli/{ => src}/utilities/package-json.ts (100%) rename packages/angular/cli/{ => src}/utilities/package-manager.ts (97%) rename packages/angular/cli/{ => src}/utilities/package-metadata.ts (100%) rename packages/angular/cli/{ => src}/utilities/package-tree.ts (100%) rename packages/angular/cli/{ => src}/utilities/project.ts (100%) rename packages/angular/cli/{ => src}/utilities/prompt.ts (100%) rename packages/angular/cli/{ => src}/utilities/spinner.ts (100%) rename packages/angular/cli/{ => src}/utilities/tty.ts (100%) rename packages/angular/cli/{ => src}/utilities/version.ts (88%) diff --git a/packages/angular/cli/bin/postinstall/analytics-prompt.js b/packages/angular/cli/bin/postinstall/analytics-prompt.js index d9e0b4873878..2635b8483cb9 100644 --- a/packages/angular/cli/bin/postinstall/analytics-prompt.js +++ b/packages/angular/cli/bin/postinstall/analytics-prompt.js @@ -14,7 +14,7 @@ if ('NG_CLI_ANALYTICS' in process.env) { } try { - var analytics = require('../../models/analytics'); + var analytics = require('../../src/analytics/analytics'); analytics .hasGlobalAnalyticsConfiguration() diff --git a/packages/angular/cli/lib/cli/index.ts b/packages/angular/cli/lib/cli/index.ts index 3070ea229f89..e1d43055d8e6 100644 --- a/packages/angular/cli/lib/cli/index.ts +++ b/packages/angular/cli/lib/cli/index.ts @@ -9,14 +9,14 @@ import { schema } from '@angular-devkit/core'; import { createConsoleLogger } from '@angular-devkit/core/node'; import { format } from 'util'; -import { colors, removeColor } from '../../utilities/color'; -import { CommandModuleError } from '../../utilities/command-builder/command-module'; -import { AngularWorkspace, getWorkspaceRaw } from '../../utilities/config'; -import { writeErrorToLogFile } from '../../utilities/log-file'; -import { findWorkspaceFile } from '../../utilities/project'; -import { runCommand } from './command-runner'; +import { CommandModuleError } from '../../src/command-builder/command-module'; +import { runCommand } from '../../src/command-builder/command-runner'; +import { colors, removeColor } from '../../src/utilities/color'; +import { AngularWorkspace, getWorkspaceRaw } from '../../src/utilities/config'; +import { writeErrorToLogFile } from '../../src/utilities/log-file'; +import { findWorkspaceFile } from '../../src/utilities/project'; -export { VERSION } from '../../utilities/version'; +export { VERSION } from '../../src/utilities/version'; const debugEnv = process.env['NG_DEBUG']; const isDebug = debugEnv !== undefined && debugEnv !== '0' && debugEnv.toLowerCase() !== 'false'; diff --git a/packages/angular/cli/lib/init.ts b/packages/angular/cli/lib/init.ts index 5e6045a81229..5e0d5aaeb8b3 100644 --- a/packages/angular/cli/lib/init.ts +++ b/packages/angular/cli/lib/init.ts @@ -11,9 +11,9 @@ import 'symbol-observable'; import { promises as fs } from 'fs'; import * as path from 'path'; import { SemVer } from 'semver'; -import { colors } from '../utilities/color'; -import { isWarningEnabled } from '../utilities/config'; -import { VERSION } from '../utilities/version'; +import { colors } from '../src/utilities/color'; +import { isWarningEnabled } from '../src/utilities/config'; +import { VERSION } from '../src/utilities/version'; (async () => { /** diff --git a/packages/angular/cli/models/command.ts b/packages/angular/cli/models/command.ts index d1d58013239c..1e78c1348c40 100644 --- a/packages/angular/cli/models/command.ts +++ b/packages/angular/cli/models/command.ts @@ -7,8 +7,8 @@ */ import { analytics, logging } from '@angular-devkit/core'; -import { Option } from '../utilities/command-builder/json-schema'; -import { AngularWorkspace } from '../utilities/config'; +import { Option } from '../src/command-builder/utilities/json-schema'; +import { AngularWorkspace } from '../src/utilities/config'; import { CommandContext } from './interface'; export interface BaseCommandOptions { diff --git a/packages/angular/cli/models/interface.ts b/packages/angular/cli/models/interface.ts index 652f1279df60..e4135dab4823 100644 --- a/packages/angular/cli/models/interface.ts +++ b/packages/angular/cli/models/interface.ts @@ -7,7 +7,7 @@ */ import { analytics, logging } from '@angular-devkit/core'; -import { AngularWorkspace } from '../utilities/config'; +import { AngularWorkspace } from '../src/utilities/config'; /** * A command runner context. diff --git a/packages/angular/cli/models/schematic-command.ts b/packages/angular/cli/models/schematic-command.ts index 8e8da4a49ce6..6c0ddc0a414b 100644 --- a/packages/angular/cli/models/schematic-command.ts +++ b/packages/angular/cli/models/schematic-command.ts @@ -16,12 +16,12 @@ import { } from '@angular-devkit/schematics/tools'; import * as inquirer from 'inquirer'; import * as systemPath from 'path'; -import { colors } from '../utilities/color'; -import { parseJsonSchemaToOptions } from '../utilities/command-builder/json-schema'; -import { getProjectByCwd, getSchematicDefaults, getWorkspace } from '../utilities/config'; -import { ensureCompatibleNpm, getPackageManager } from '../utilities/package-manager'; -import { isTTY } from '../utilities/tty'; -import { isPackageNameSafeForAnalytics } from './analytics'; +import { isPackageNameSafeForAnalytics } from '../src/analytics/analytics'; +import { parseJsonSchemaToOptions } from '../src/command-builder/utilities/json-schema'; +import { colors } from '../src/utilities/color'; +import { getProjectByCwd, getSchematicDefaults, getWorkspace } from '../src/utilities/config'; +import { ensureCompatibleNpm, getPackageManager } from '../src/utilities/package-manager'; +import { isTTY } from '../src/utilities/tty'; import { BaseCommandOptions, Command } from './command'; import { CommandContext } from './interface'; import { SchematicEngineHost } from './schematic-engine-host'; diff --git a/packages/angular/cli/models/analytics-collector.ts b/packages/angular/cli/src/analytics/analytics-collector.ts similarity index 100% rename from packages/angular/cli/models/analytics-collector.ts rename to packages/angular/cli/src/analytics/analytics-collector.ts diff --git a/packages/angular/cli/models/analytics.ts b/packages/angular/cli/src/analytics/analytics.ts similarity index 100% rename from packages/angular/cli/models/analytics.ts rename to packages/angular/cli/src/analytics/analytics.ts diff --git a/packages/angular/cli/utilities/command-builder/architect-command-module.ts b/packages/angular/cli/src/command-builder/architect-command-module.ts similarity index 79% rename from packages/angular/cli/utilities/command-builder/architect-command-module.ts rename to packages/angular/cli/src/command-builder/architect-command-module.ts index 744618faaf83..4ad170a95ac3 100644 --- a/packages/angular/cli/utilities/command-builder/architect-command-module.ts +++ b/packages/angular/cli/src/command-builder/architect-command-module.ts @@ -9,13 +9,9 @@ import { Architect, Target } from '@angular-devkit/architect'; import { WorkspaceNodeModulesArchitectHost } from '@angular-devkit/architect/node'; import { json } from '@angular-devkit/core'; -import { existsSync } from 'fs'; -import { resolve } from 'path'; import { Argv } from 'yargs'; -import { isPackageNameSafeForAnalytics } from '../../models/analytics'; -import { getPackageManager } from '../package-manager'; +import { isPackageNameSafeForAnalytics } from '../analytics/analytics'; import { - CommandContext, CommandModule, CommandModuleError, CommandModuleImplementation, @@ -23,7 +19,7 @@ import { Options, OtherOptions, } from './command-module'; -import { Option, parseJsonSchemaToOptions } from './json-schema'; +import { getArchitectTargetOptions } from './utilities/architect'; export interface ArchitectCommandArgs { configuration?: string; @@ -234,63 +230,3 @@ export abstract class ArchitectCommandModule )); } } - -/** - * Get architect target schema options. - */ -export async function getArchitectTargetOptions( - context: CommandContext, - target: Target, -): Promise { - const { workspace } = context; - if (!workspace) { - return []; - } - - const architectHost = new WorkspaceNodeModulesArchitectHost(workspace, workspace.basePath); - const builderConf = await architectHost.getBuilderNameForTarget(target); - - let builderDesc; - try { - builderDesc = await architectHost.resolveBuilder(builderConf); - } catch (e) { - if (e.code === 'MODULE_NOT_FOUND') { - await warnOnMissingNodeModules(context); - throw new CommandModuleError(`Could not find the '${builderConf}' builder's node package.`); - } - - throw e; - } - - return parseJsonSchemaToOptions( - new json.schema.CoreSchemaRegistry(), - builderDesc.optionSchema as json.JsonObject, - true, - ); -} - -export async function warnOnMissingNodeModules(context: CommandContext): Promise { - const basePath = context.workspace?.basePath; - if (!basePath) { - return; - } - - // Check for a `node_modules` directory (npm, yarn non-PnP, etc.) - if (existsSync(resolve(basePath, 'node_modules'))) { - return; - } - - // Check for yarn PnP files - if ( - existsSync(resolve(basePath, '.pnp.js')) || - existsSync(resolve(basePath, '.pnp.cjs')) || - existsSync(resolve(basePath, '.pnp.mjs')) - ) { - return; - } - - const packageManager = await getPackageManager(basePath); - context.logger.warn( - `Node packages may not be installed. Try installing with '${packageManager} install'.`, - ); -} diff --git a/packages/angular/cli/utilities/command-builder/command-module.ts b/packages/angular/cli/src/command-builder/command-module.ts similarity index 97% rename from packages/angular/cli/utilities/command-builder/command-module.ts rename to packages/angular/cli/src/command-builder/command-module.ts index fef0e34d1882..b5412a42ce46 100644 --- a/packages/angular/cli/utilities/command-builder/command-module.ts +++ b/packages/angular/cli/src/command-builder/command-module.ts @@ -16,9 +16,9 @@ import { CommandModule as YargsCommandModule, Options as YargsOptions, } from 'yargs'; -import { createAnalytics } from '../../models/analytics'; -import { AngularWorkspace } from '../config'; -import { Option } from './json-schema'; +import { createAnalytics } from '../analytics/analytics'; +import { AngularWorkspace } from '../utilities/config'; +import { Option } from './utilities/json-schema'; export type Options = { [key in keyof T as CamelCaseKey]: T[key] }; diff --git a/packages/angular/cli/lib/cli/command-runner.ts b/packages/angular/cli/src/command-builder/command-runner.ts similarity index 77% rename from packages/angular/cli/lib/cli/command-runner.ts rename to packages/angular/cli/src/command-builder/command-runner.ts index f92a0f179a62..5cfcbacbba1d 100644 --- a/packages/angular/cli/lib/cli/command-runner.ts +++ b/packages/angular/cli/src/command-builder/command-runner.ts @@ -9,31 +9,27 @@ import { logging } from '@angular-devkit/core'; import yargs from 'yargs'; import { Parser } from 'yargs/helpers'; -import { AddCommandModule } from '../../commands/add/cli'; -import { AnalyticsCommandModule } from '../../commands/analytics/cli'; -import { BuildCommandModule } from '../../commands/build/cli'; -import { ConfigCommandModule } from '../../commands/config/cli'; -import { DeployCommandModule } from '../../commands/deploy/cli'; -import { DocCommandModule } from '../../commands/doc/cli'; -import { E2eCommandModule } from '../../commands/e2e/cli'; -import { ExtractI18nCommandModule } from '../../commands/extract-i18n/cli'; -import { GenerateCommandModule } from '../../commands/generate/cli'; -import { LintCommandModule } from '../../commands/lint/cli'; -import { AwesomeCommandModule } from '../../commands/make-this-awesome/cli'; -import { NewCommandModule } from '../../commands/new/cli'; -import { RunCommandModule } from '../../commands/run/cli'; -import { ServeCommandModule } from '../../commands/serve/cli'; -import { TestCommandModule } from '../../commands/test/cli'; -import { UpdateCommandModule } from '../../commands/update/cli'; -import { VersionCommandModule } from '../../commands/version/cli'; -import { colors } from '../../utilities/color'; -import { - CommandContext, - CommandModuleError, - CommandScope, -} from '../../utilities/command-builder/command-module'; -import { jsonHelpUsage } from '../../utilities/command-builder/json-help'; -import { AngularWorkspace } from '../../utilities/config'; +import { AddCommandModule } from '../commands/add/cli'; +import { AnalyticsCommandModule } from '../commands/analytics/cli'; +import { BuildCommandModule } from '../commands/build/cli'; +import { ConfigCommandModule } from '../commands/config/cli'; +import { DeployCommandModule } from '../commands/deploy/cli'; +import { DocCommandModule } from '../commands/doc/cli'; +import { E2eCommandModule } from '../commands/e2e/cli'; +import { ExtractI18nCommandModule } from '../commands/extract-i18n/cli'; +import { GenerateCommandModule } from '../commands/generate/cli'; +import { LintCommandModule } from '../commands/lint/cli'; +import { AwesomeCommandModule } from '../commands/make-this-awesome/cli'; +import { NewCommandModule } from '../commands/new/cli'; +import { RunCommandModule } from '../commands/run/cli'; +import { ServeCommandModule } from '../commands/serve/cli'; +import { TestCommandModule } from '../commands/test/cli'; +import { UpdateCommandModule } from '../commands/update/cli'; +import { VersionCommandModule } from '../commands/version/cli'; +import { colors } from '../utilities/color'; +import { AngularWorkspace } from '../utilities/config'; +import { CommandContext, CommandModuleError, CommandScope } from './command-module'; +import { jsonHelpUsage } from './utilities/json-help'; const COMMANDS = [ VersionCommandModule, diff --git a/packages/angular/cli/utilities/command-builder/schematics-command-module.ts b/packages/angular/cli/src/command-builder/schematics-command-module.ts similarity index 97% rename from packages/angular/cli/utilities/command-builder/schematics-command-module.ts rename to packages/angular/cli/src/command-builder/schematics-command-module.ts index 51c033d22689..75f99317e328 100644 --- a/packages/angular/cli/utilities/command-builder/schematics-command-module.ts +++ b/packages/angular/cli/src/command-builder/schematics-command-module.ts @@ -14,9 +14,9 @@ import { } from '@angular-devkit/schematics/tools'; import { Argv } from 'yargs'; import { SchematicEngineHost } from '../../models/schematic-engine-host'; -import { getProjectByCwd, getWorkspace } from '../config'; +import { getProjectByCwd, getWorkspace } from '../utilities/config'; import { CommandModule, CommandModuleImplementation, CommandScope } from './command-module'; -import { Option, parseJsonSchemaToOptions } from './json-schema'; +import { Option, parseJsonSchemaToOptions } from './utilities/json-schema'; const DEFAULT_SCHEMATICS_COLLECTION = '@schematics/angular'; diff --git a/packages/angular/cli/src/command-builder/utilities/architect.ts b/packages/angular/cli/src/command-builder/utilities/architect.ts new file mode 100644 index 000000000000..63e68193bfd5 --- /dev/null +++ b/packages/angular/cli/src/command-builder/utilities/architect.ts @@ -0,0 +1,73 @@ +/** + * @license + * Copyright Google LLC 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 { Target } from '@angular-devkit/architect'; +import { WorkspaceNodeModulesArchitectHost } from '@angular-devkit/architect/node'; +import { json } from '@angular-devkit/core'; +import { existsSync } from 'fs'; +import { resolve } from 'path'; +import { getPackageManager } from '../../utilities/package-manager'; +import { CommandContext, CommandModuleError } from '../command-module'; +import { Option, parseJsonSchemaToOptions } from './json-schema'; + +export async function getArchitectTargetOptions( + context: CommandContext, + target: Target, +): Promise { + const { workspace } = context; + if (!workspace) { + return []; + } + + const architectHost = new WorkspaceNodeModulesArchitectHost(workspace, workspace.basePath); + const builderConf = await architectHost.getBuilderNameForTarget(target); + + let builderDesc; + try { + builderDesc = await architectHost.resolveBuilder(builderConf); + } catch (e) { + if (e.code === 'MODULE_NOT_FOUND') { + await warnOnMissingNodeModules(context); + throw new CommandModuleError(`Could not find the '${builderConf}' builder's node package.`); + } + + throw e; + } + + return parseJsonSchemaToOptions( + new json.schema.CoreSchemaRegistry(), + builderDesc.optionSchema as json.JsonObject, + true, + ); +} + +export async function warnOnMissingNodeModules(context: CommandContext): Promise { + const basePath = context.workspace?.basePath; + if (!basePath) { + return; + } + + // Check for a `node_modules` directory (npm, yarn non-PnP, etc.) + if (existsSync(resolve(basePath, 'node_modules'))) { + return; + } + + // Check for yarn PnP files + if ( + existsSync(resolve(basePath, '.pnp.js')) || + existsSync(resolve(basePath, '.pnp.cjs')) || + existsSync(resolve(basePath, '.pnp.mjs')) + ) { + return; + } + + const packageManager = await getPackageManager(basePath); + context.logger.warn( + `Node packages may not be installed. Try installing with '${packageManager} install'.`, + ); +} diff --git a/packages/angular/cli/utilities/command-builder/json-help.ts b/packages/angular/cli/src/command-builder/utilities/json-help.ts similarity index 98% rename from packages/angular/cli/utilities/command-builder/json-help.ts rename to packages/angular/cli/src/command-builder/utilities/json-help.ts index c943194da495..047fcdc3fa6c 100644 --- a/packages/angular/cli/utilities/command-builder/json-help.ts +++ b/packages/angular/cli/src/command-builder/utilities/json-help.ts @@ -7,7 +7,7 @@ */ import yargs from 'yargs'; -import { FullDescribe } from './command-module'; +import { FullDescribe } from '../command-module'; export interface JsonHelp { name: string; diff --git a/packages/angular/cli/utilities/command-builder/json-schema.ts b/packages/angular/cli/src/command-builder/utilities/json-schema.ts similarity index 100% rename from packages/angular/cli/utilities/command-builder/json-schema.ts rename to packages/angular/cli/src/command-builder/utilities/json-schema.ts diff --git a/packages/angular/cli/commands/add/add-impl.ts b/packages/angular/cli/src/commands/add/add-impl.ts similarity index 97% rename from packages/angular/cli/commands/add/add-impl.ts rename to packages/angular/cli/src/commands/add/add-impl.ts index e0097759f863..474300b63037 100644 --- a/packages/angular/cli/commands/add/add-impl.ts +++ b/packages/angular/cli/src/commands/add/add-impl.ts @@ -11,11 +11,11 @@ import { NodePackageDoesNotSupportSchematics } from '@angular-devkit/schematics/ import npa from 'npm-package-arg'; import { dirname, join } from 'path'; import { intersects, prerelease, rcompare, satisfies, valid, validRange } from 'semver'; -import { PackageManager } from '../../lib/config/workspace-schema'; -import { isPackageNameSafeForAnalytics } from '../../models/analytics'; -import { SchematicCommand } from '../../models/schematic-command'; +import { PackageManager } from '../../../lib/config/workspace-schema'; +import { SchematicCommand } from '../../../models/schematic-command'; +import { isPackageNameSafeForAnalytics } from '../../analytics/analytics'; +import { Options } from '../../command-builder/command-module'; import { colors } from '../../utilities/color'; -import { Options } from '../../utilities/command-builder/command-module'; import { installPackage, installTempPackage } from '../../utilities/install-package'; import { ensureCompatibleNpm, getPackageManager } from '../../utilities/package-manager'; import { diff --git a/packages/angular/cli/commands/add/cli.ts b/packages/angular/cli/src/commands/add/cli.ts similarity index 94% rename from packages/angular/cli/commands/add/cli.ts rename to packages/angular/cli/src/commands/add/cli.ts index 7ea8824487ef..4099d9f305ef 100644 --- a/packages/angular/cli/commands/add/cli.ts +++ b/packages/angular/cli/src/commands/add/cli.ts @@ -12,11 +12,11 @@ import { CommandModuleImplementation, Options, OtherOptions, -} from '../../utilities/command-builder/command-module'; +} from '../../command-builder/command-module'; import { SchematicsCommandArgs, SchematicsCommandModule, -} from '../../utilities/command-builder/schematics-command-module'; +} from '../../command-builder/schematics-command-module'; import { AddCommandModule as OldCommandModule } from './add-impl'; export interface AddCommandArgs extends SchematicsCommandArgs { diff --git a/packages/angular/cli/commands/add/long-description.md b/packages/angular/cli/src/commands/add/long-description.md similarity index 100% rename from packages/angular/cli/commands/add/long-description.md rename to packages/angular/cli/src/commands/add/long-description.md diff --git a/packages/angular/cli/commands/analytics/cli.ts b/packages/angular/cli/src/commands/analytics/cli.ts similarity index 93% rename from packages/angular/cli/commands/analytics/cli.ts rename to packages/angular/cli/src/commands/analytics/cli.ts index b1c004277ec7..ea70271fd10c 100644 --- a/packages/angular/cli/commands/analytics/cli.ts +++ b/packages/angular/cli/src/commands/analytics/cli.ts @@ -7,13 +7,13 @@ */ import { join } from 'path'; -import { Argv, string } from 'yargs'; +import { Argv } from 'yargs'; import { promptGlobalAnalytics, promptProjectAnalytics, setAnalyticsConfig, -} from '../../models/analytics'; -import { CommandModule, Options } from '../../utilities/command-builder/command-module'; +} from '../../analytics/analytics'; +import { CommandModule, Options } from '../../command-builder/command-module'; interface AnalyticsCommandArgs { 'setting-or-project': 'on' | 'off' | 'ci' | 'project' | 'prompt' | string; diff --git a/packages/angular/cli/commands/analytics/long-description.md b/packages/angular/cli/src/commands/analytics/long-description.md similarity index 100% rename from packages/angular/cli/commands/analytics/long-description.md rename to packages/angular/cli/src/commands/analytics/long-description.md diff --git a/packages/angular/cli/commands/build/cli.ts b/packages/angular/cli/src/commands/build/cli.ts similarity index 75% rename from packages/angular/cli/commands/build/cli.ts rename to packages/angular/cli/src/commands/build/cli.ts index 03d71dc5c762..434ff4f22f84 100644 --- a/packages/angular/cli/commands/build/cli.ts +++ b/packages/angular/cli/src/commands/build/cli.ts @@ -7,8 +7,8 @@ */ import { join } from 'path'; -import { ArchitectCommandModule } from '../../utilities/command-builder/architect-command-module'; -import { CommandModuleImplementation } from '../../utilities/command-builder/command-module'; +import { ArchitectCommandModule } from '../../command-builder/architect-command-module'; +import { CommandModuleImplementation } from '../../command-builder/command-module'; export class BuildCommandModule extends ArchitectCommandModule diff --git a/packages/angular/cli/commands/build/long-description.md b/packages/angular/cli/src/commands/build/long-description.md similarity index 100% rename from packages/angular/cli/commands/build/long-description.md rename to packages/angular/cli/src/commands/build/long-description.md diff --git a/packages/angular/cli/commands/config/cli.ts b/packages/angular/cli/src/commands/config/cli.ts similarity index 96% rename from packages/angular/cli/commands/config/cli.ts rename to packages/angular/cli/src/commands/config/cli.ts index a9f9f0795dbf..8fbabe1ae443 100644 --- a/packages/angular/cli/commands/config/cli.ts +++ b/packages/angular/cli/src/commands/config/cli.ts @@ -12,7 +12,7 @@ import { CommandModule, CommandModuleImplementation, Options, -} from '../../utilities/command-builder/command-module'; +} from '../../command-builder/command-module'; import { ConfigCommand } from './config-impl'; export interface ConfigCommandArgs { diff --git a/packages/angular/cli/commands/config/config-impl.ts b/packages/angular/cli/src/commands/config/config-impl.ts similarity index 97% rename from packages/angular/cli/commands/config/config-impl.ts rename to packages/angular/cli/src/commands/config/config-impl.ts index a032a8135f8a..7688609e8372 100644 --- a/packages/angular/cli/commands/config/config-impl.ts +++ b/packages/angular/cli/src/commands/config/config-impl.ts @@ -8,8 +8,8 @@ import { JsonValue } from '@angular-devkit/core'; import { v4 as uuidV4 } from 'uuid'; -import { Command } from '../../models/command'; -import { Options } from '../../utilities/command-builder/command-module'; +import { Command } from '../../../models/command'; +import { Options } from '../../command-builder/command-module'; import { getWorkspaceRaw, validateWorkspace } from '../../utilities/config'; import { JSONFile, parseJson } from '../../utilities/json-file'; import { ConfigCommandArgs } from './cli'; diff --git a/packages/angular/cli/commands/config/long-description.md b/packages/angular/cli/src/commands/config/long-description.md similarity index 82% rename from packages/angular/cli/commands/config/long-description.md rename to packages/angular/cli/src/commands/config/long-description.md index 7f44f63b3b32..2ed7e8c7a6c6 100644 --- a/packages/angular/cli/commands/config/long-description.md +++ b/packages/angular/cli/src/commands/config/long-description.md @@ -9,5 +9,3 @@ except that in the configuration file, all names must use camelCase, while on the command line options can be given in either camelCase or dash-case. For further details, see [Workspace Configuration](guide/workspace-config). - -For configuration of CLI usage analytics, see [Gathering an Viewing CLI Usage Analytics](./usage-analytics-gathering). diff --git a/packages/angular/cli/commands/deploy/cli.ts b/packages/angular/cli/src/commands/deploy/cli.ts similarity index 83% rename from packages/angular/cli/commands/deploy/cli.ts rename to packages/angular/cli/src/commands/deploy/cli.ts index 1ee748b340e7..18f5aaadd803 100644 --- a/packages/angular/cli/commands/deploy/cli.ts +++ b/packages/angular/cli/src/commands/deploy/cli.ts @@ -8,8 +8,8 @@ import { tags } from '@angular-devkit/core'; import { join } from 'path'; -import { ArchitectCommandModule } from '../../utilities/command-builder/architect-command-module'; -import { CommandModuleImplementation } from '../../utilities/command-builder/command-module'; +import { ArchitectCommandModule } from '../../command-builder/architect-command-module'; +import { CommandModuleImplementation } from '../../command-builder/command-module'; export class DeployCommandModule extends ArchitectCommandModule diff --git a/packages/angular/cli/commands/deploy/long-description.md b/packages/angular/cli/src/commands/deploy/long-description.md similarity index 100% rename from packages/angular/cli/commands/deploy/long-description.md rename to packages/angular/cli/src/commands/deploy/long-description.md diff --git a/packages/angular/cli/commands/doc/cli.ts b/packages/angular/cli/src/commands/doc/cli.ts similarity index 97% rename from packages/angular/cli/commands/doc/cli.ts rename to packages/angular/cli/src/commands/doc/cli.ts index 70bb47773419..fd2423e3603d 100644 --- a/packages/angular/cli/commands/doc/cli.ts +++ b/packages/angular/cli/src/commands/doc/cli.ts @@ -12,7 +12,7 @@ import { CommandModule, CommandModuleImplementation, Options, -} from '../../utilities/command-builder/command-module'; +} from '../../command-builder/command-module'; interface DocCommandArgs { keyword: string; diff --git a/packages/angular/cli/commands/e2e/cli.ts b/packages/angular/cli/src/commands/e2e/cli.ts similarity index 83% rename from packages/angular/cli/commands/e2e/cli.ts rename to packages/angular/cli/src/commands/e2e/cli.ts index 4f833c8ef2c1..bb59ca78fc2a 100644 --- a/packages/angular/cli/commands/e2e/cli.ts +++ b/packages/angular/cli/src/commands/e2e/cli.ts @@ -7,8 +7,8 @@ */ import { tags } from '@angular-devkit/core'; -import { ArchitectCommandModule } from '../../utilities/command-builder/architect-command-module'; -import { CommandModuleImplementation } from '../../utilities/command-builder/command-module'; +import { ArchitectCommandModule } from '../../command-builder/architect-command-module'; +import { CommandModuleImplementation } from '../../command-builder/command-module'; export class E2eCommandModule extends ArchitectCommandModule diff --git a/packages/angular/cli/commands/extract-i18n/cli.ts b/packages/angular/cli/src/commands/extract-i18n/cli.ts similarity index 71% rename from packages/angular/cli/commands/extract-i18n/cli.ts rename to packages/angular/cli/src/commands/extract-i18n/cli.ts index 4812a2bda54e..5283204f4e9b 100644 --- a/packages/angular/cli/commands/extract-i18n/cli.ts +++ b/packages/angular/cli/src/commands/extract-i18n/cli.ts @@ -6,8 +6,8 @@ * found in the LICENSE file at https://angular.io/license */ -import { ArchitectCommandModule } from '../../utilities/command-builder/architect-command-module'; -import { CommandModuleImplementation } from '../../utilities/command-builder/command-module'; +import { ArchitectCommandModule } from '../../command-builder/architect-command-module'; +import { CommandModuleImplementation } from '../../command-builder/command-module'; export class ExtractI18nCommandModule extends ArchitectCommandModule diff --git a/packages/angular/cli/commands/generate/cli.ts b/packages/angular/cli/src/commands/generate/cli.ts similarity index 96% rename from packages/angular/cli/commands/generate/cli.ts rename to packages/angular/cli/src/commands/generate/cli.ts index 8311d4d2ad1b..923c228d7e93 100644 --- a/packages/angular/cli/commands/generate/cli.ts +++ b/packages/angular/cli/src/commands/generate/cli.ts @@ -12,12 +12,12 @@ import { CommandModuleImplementation, Options, OtherOptions, -} from '../../utilities/command-builder/command-module'; -import { Option } from '../../utilities/command-builder/json-schema'; +} from '../../command-builder/command-module'; import { SchematicsCommandArgs, SchematicsCommandModule, -} from '../../utilities/command-builder/schematics-command-module'; +} from '../../command-builder/schematics-command-module'; +import { Option } from '../../command-builder/utilities/json-schema'; import { GenerateCommand } from './generate-impl'; export interface GenerateCommandArgs extends SchematicsCommandArgs { diff --git a/packages/angular/cli/commands/generate/generate-impl.ts b/packages/angular/cli/src/commands/generate/generate-impl.ts similarity index 93% rename from packages/angular/cli/commands/generate/generate-impl.ts rename to packages/angular/cli/src/commands/generate/generate-impl.ts index a0aec5ca48b5..102d67c248c1 100644 --- a/packages/angular/cli/commands/generate/generate-impl.ts +++ b/packages/angular/cli/src/commands/generate/generate-impl.ts @@ -6,8 +6,8 @@ * found in the LICENSE file at https://angular.io/license */ -import { SchematicCommand } from '../../models/schematic-command'; -import { Options, OtherOptions } from '../../utilities/command-builder/command-module'; +import { SchematicCommand } from '../../../models/schematic-command'; +import { Options, OtherOptions } from '../../command-builder/command-module'; import { GenerateCommandArgs } from './cli'; type GenerateCommandOptions = Options; diff --git a/packages/angular/cli/commands/lint/cli.ts b/packages/angular/cli/src/commands/lint/cli.ts similarity index 81% rename from packages/angular/cli/commands/lint/cli.ts rename to packages/angular/cli/src/commands/lint/cli.ts index 1715b5a7a0bf..bd661e7d164b 100644 --- a/packages/angular/cli/commands/lint/cli.ts +++ b/packages/angular/cli/src/commands/lint/cli.ts @@ -8,8 +8,8 @@ import { tags } from '@angular-devkit/core'; import { join } from 'path'; -import { ArchitectCommandModule } from '../../utilities/command-builder/architect-command-module'; -import { CommandModuleImplementation } from '../../utilities/command-builder/command-module'; +import { ArchitectCommandModule } from '../../command-builder/architect-command-module'; +import { CommandModuleImplementation } from '../../command-builder/command-module'; export class LintCommandModule extends ArchitectCommandModule diff --git a/packages/angular/cli/commands/lint/long-description.md b/packages/angular/cli/src/commands/lint/long-description.md similarity index 100% rename from packages/angular/cli/commands/lint/long-description.md rename to packages/angular/cli/src/commands/lint/long-description.md diff --git a/packages/angular/cli/commands/make-this-awesome/cli.ts b/packages/angular/cli/src/commands/make-this-awesome/cli.ts similarity index 90% rename from packages/angular/cli/commands/make-this-awesome/cli.ts rename to packages/angular/cli/src/commands/make-this-awesome/cli.ts index 9705533289d5..45b77f36da93 100644 --- a/packages/angular/cli/commands/make-this-awesome/cli.ts +++ b/packages/angular/cli/src/commands/make-this-awesome/cli.ts @@ -7,11 +7,8 @@ */ import { Argv } from 'yargs'; +import { CommandModule, CommandModuleImplementation } from '../../command-builder/command-module'; import { colors } from '../../utilities/color'; -import { - CommandModule, - CommandModuleImplementation, -} from '../../utilities/command-builder/command-module'; export class AwesomeCommandModule extends CommandModule implements CommandModuleImplementation { command = 'make-this-awesome'; diff --git a/packages/angular/cli/commands/new/cli.ts b/packages/angular/cli/src/commands/new/cli.ts similarity index 91% rename from packages/angular/cli/commands/new/cli.ts rename to packages/angular/cli/src/commands/new/cli.ts index 38174ff81e72..68dc79e12ae7 100644 --- a/packages/angular/cli/commands/new/cli.ts +++ b/packages/angular/cli/src/commands/new/cli.ts @@ -12,11 +12,11 @@ import { CommandScope, Options, OtherOptions, -} from '../../utilities/command-builder/command-module'; +} from '../../command-builder/command-module'; import { SchematicsCommandArgs, SchematicsCommandModule, -} from '../../utilities/command-builder/schematics-command-module'; +} from '../../command-builder/schematics-command-module'; import { NewCommand } from './new-impl'; export interface NewCommandArgs extends SchematicsCommandArgs { diff --git a/packages/angular/cli/commands/new/new-impl.ts b/packages/angular/cli/src/commands/new/new-impl.ts similarity index 88% rename from packages/angular/cli/commands/new/new-impl.ts rename to packages/angular/cli/src/commands/new/new-impl.ts index 964ba7244dd2..b8629d39b4c9 100644 --- a/packages/angular/cli/commands/new/new-impl.ts +++ b/packages/angular/cli/src/commands/new/new-impl.ts @@ -6,8 +6,8 @@ * found in the LICENSE file at https://angular.io/license */ -import { SchematicCommand } from '../../models/schematic-command'; -import { Options, OtherOptions } from '../../utilities/command-builder/command-module'; +import { SchematicCommand } from '../../../models/schematic-command'; +import { Options, OtherOptions } from '../../command-builder/command-module'; import { VERSION } from '../../utilities/version'; import { NewCommandArgs } from './cli'; diff --git a/packages/angular/cli/commands/run/cli.ts b/packages/angular/cli/src/commands/run/cli.ts similarity index 93% rename from packages/angular/cli/commands/run/cli.ts rename to packages/angular/cli/src/commands/run/cli.ts index 7674f1f656cb..9ce685705e94 100644 --- a/packages/angular/cli/commands/run/cli.ts +++ b/packages/angular/cli/src/commands/run/cli.ts @@ -11,8 +11,7 @@ import { WorkspaceNodeModulesArchitectHost } from '@angular-devkit/architect/nod import { json } from '@angular-devkit/core'; import { join } from 'path'; import { Argv } from 'yargs'; -import { isPackageNameSafeForAnalytics } from '../../models/analytics'; -import { getArchitectTargetOptions } from '../../utilities/command-builder/architect-command-module'; +import { isPackageNameSafeForAnalytics } from '../../analytics/analytics'; import { CommandModule, CommandModuleError, @@ -20,7 +19,8 @@ import { CommandScope, Options, OtherOptions, -} from '../../utilities/command-builder/command-module'; +} from '../../command-builder/command-module'; +import { getArchitectTargetOptions } from '../../command-builder/utilities/architect'; export interface RunCommandArgs { target: string; diff --git a/packages/angular/cli/commands/run/long-description.md b/packages/angular/cli/src/commands/run/long-description.md similarity index 100% rename from packages/angular/cli/commands/run/long-description.md rename to packages/angular/cli/src/commands/run/long-description.md diff --git a/packages/angular/cli/commands/serve/cli.ts b/packages/angular/cli/src/commands/serve/cli.ts similarity index 72% rename from packages/angular/cli/commands/serve/cli.ts rename to packages/angular/cli/src/commands/serve/cli.ts index 6231a9060980..537345cc568d 100644 --- a/packages/angular/cli/commands/serve/cli.ts +++ b/packages/angular/cli/src/commands/serve/cli.ts @@ -6,8 +6,8 @@ * found in the LICENSE file at https://angular.io/license */ -import { ArchitectCommandModule } from '../../utilities/command-builder/architect-command-module'; -import { CommandModuleImplementation } from '../../utilities/command-builder/command-module'; +import { ArchitectCommandModule } from '../../command-builder/architect-command-module'; +import { CommandModuleImplementation } from '../../command-builder/command-module'; export class ServeCommandModule extends ArchitectCommandModule diff --git a/packages/angular/cli/commands/test/cli.ts b/packages/angular/cli/src/commands/test/cli.ts similarity index 73% rename from packages/angular/cli/commands/test/cli.ts rename to packages/angular/cli/src/commands/test/cli.ts index 4c7dd6cbe23b..fd650fee01c9 100644 --- a/packages/angular/cli/commands/test/cli.ts +++ b/packages/angular/cli/src/commands/test/cli.ts @@ -7,8 +7,8 @@ */ import { join } from 'path'; -import { ArchitectCommandModule } from '../../utilities/command-builder/architect-command-module'; -import { CommandModuleImplementation } from '../../utilities/command-builder/command-module'; +import { ArchitectCommandModule } from '../../command-builder/architect-command-module'; +import { CommandModuleImplementation } from '../../command-builder/command-module'; export class TestCommandModule extends ArchitectCommandModule diff --git a/packages/angular/cli/commands/test/long-description.md b/packages/angular/cli/src/commands/test/long-description.md similarity index 100% rename from packages/angular/cli/commands/test/long-description.md rename to packages/angular/cli/src/commands/test/long-description.md diff --git a/packages/angular/cli/commands/update/cli.ts b/packages/angular/cli/src/commands/update/cli.ts similarity index 98% rename from packages/angular/cli/commands/update/cli.ts rename to packages/angular/cli/src/commands/update/cli.ts index 1d611ba910f1..ee57fce365da 100644 --- a/packages/angular/cli/commands/update/cli.ts +++ b/packages/angular/cli/src/commands/update/cli.ts @@ -12,7 +12,7 @@ import { CommandScope, Options, OtherOptions, -} from '../../utilities/command-builder/command-module'; +} from '../../command-builder/command-module'; import { UpdateCommand } from './update-impl'; export interface UpdateCommandArgs { diff --git a/packages/angular/cli/commands/update/long-description.md b/packages/angular/cli/src/commands/update/long-description.md similarity index 100% rename from packages/angular/cli/commands/update/long-description.md rename to packages/angular/cli/src/commands/update/long-description.md diff --git a/packages/angular/cli/src/commands/update/schematic/index.ts b/packages/angular/cli/src/commands/update/schematic/index.ts index 76f3e0a09f43..d61dd591e8d2 100644 --- a/packages/angular/cli/src/commands/update/schematic/index.ts +++ b/packages/angular/cli/src/commands/update/schematic/index.ts @@ -10,11 +10,8 @@ import { logging, tags } from '@angular-devkit/core'; import { Rule, SchematicContext, SchematicsException, Tree } from '@angular-devkit/schematics'; import * as npa from 'npm-package-arg'; import * as semver from 'semver'; -import { Dependency, JsonSchemaForNpmPackageJsonFiles } from '../../../../utilities/package-json'; -import { - NpmRepositoryPackageJson, - getNpmPackageJson, -} from '../../../../utilities/package-metadata'; +import { Dependency, JsonSchemaForNpmPackageJsonFiles } from '../../../utilities/package-json'; +import { NpmRepositoryPackageJson, getNpmPackageJson } from '../../../utilities/package-metadata'; import { Schema as UpdateSchema } from './schema'; type VersionRange = string & { __VERSION_RANGE: void }; diff --git a/packages/angular/cli/commands/update/update-impl.ts b/packages/angular/cli/src/commands/update/update-impl.ts similarity index 98% rename from packages/angular/cli/commands/update/update-impl.ts rename to packages/angular/cli/src/commands/update/update-impl.ts index b8b5a521018b..25cf5da8f848 100644 --- a/packages/angular/cli/commands/update/update-impl.ts +++ b/packages/angular/cli/src/commands/update/update-impl.ts @@ -14,11 +14,11 @@ import npa from 'npm-package-arg'; import pickManifest from 'npm-pick-manifest'; import * as path from 'path'; import * as semver from 'semver'; -import { PackageManager } from '../../lib/config/workspace-schema'; -import { Command } from '../../models/command'; -import { SchematicEngineHost } from '../../models/schematic-engine-host'; +import { PackageManager } from '../../../lib/config/workspace-schema'; +import { Command } from '../../../models/command'; +import { SchematicEngineHost } from '../../../models/schematic-engine-host'; +import { Options } from '../../command-builder/command-module'; import { colors } from '../../utilities/color'; -import { Options } from '../../utilities/command-builder/command-module'; import { installAllPackages, runTempPackageBin } from '../../utilities/install-package'; import { writeErrorToLogFile } from '../../utilities/log-file'; import { ensureCompatibleNpm, getPackageManager } from '../../utilities/package-manager'; @@ -37,10 +37,7 @@ import { import { VERSION } from '../../utilities/version'; import { UpdateCommandArgs } from './cli'; -const UPDATE_SCHEMATIC_COLLECTION = path.join( - __dirname, - '../../src/commands/update/schematic/collection.json', -); +const UPDATE_SCHEMATIC_COLLECTION = path.join(__dirname, 'schematic/collection.json'); type UpdateCommandOptions = Options; diff --git a/packages/angular/cli/commands/version/cli.ts b/packages/angular/cli/src/commands/version/cli.ts similarity index 95% rename from packages/angular/cli/commands/version/cli.ts rename to packages/angular/cli/src/commands/version/cli.ts index e86be716689f..9ed6f288740e 100644 --- a/packages/angular/cli/commands/version/cli.ts +++ b/packages/angular/cli/src/commands/version/cli.ts @@ -8,12 +8,10 @@ import { execSync } from 'child_process'; import nodeModule from 'module'; +import { resolve } from 'path'; import { Argv } from 'yargs'; +import { CommandModule, CommandModuleImplementation } from '../../command-builder/command-module'; import { colors } from '../../utilities/color'; -import { - CommandModule, - CommandModuleImplementation, -} from '../../utilities/command-builder/command-module'; import { getPackageManager } from '../../utilities/package-manager'; interface PartialPackageInfo { @@ -53,11 +51,11 @@ export class VersionCommandModule extends CommandModule implements CommandModule async run(): Promise { const logger = this.context.logger; - const localRequire = nodeModule.createRequire(__filename); + const localRequire = nodeModule.createRequire(resolve(__filename, '../../../')); // Trailing slash is used to allow the path to be treated as a directory const workspaceRequire = nodeModule.createRequire(this.context.root + '/'); - const cliPackage: PartialPackageInfo = localRequire('../../package.json'); + const cliPackage: PartialPackageInfo = localRequire('./package.json'); let workspacePackage: PartialPackageInfo | undefined; try { workspacePackage = workspaceRequire('./package.json'); diff --git a/packages/angular/cli/src/typings.ts b/packages/angular/cli/src/typings.ts index 169bbb457e68..63fc2bf0ceaf 100644 --- a/packages/angular/cli/src/typings.ts +++ b/packages/angular/cli/src/typings.ts @@ -18,9 +18,9 @@ declare module 'ini' { declare module 'npm-pick-manifest' { function pickManifest( - metadata: import('../utilities/package-metadata').PackageMetadata, + metadata: import('./utilities/package-metadata').PackageMetadata, selector: string, - ): import('../utilities/package-metadata').PackageManifest; + ): import('./utilities/package-metadata').PackageManifest; export = pickManifest; } @@ -33,5 +33,5 @@ declare module 'pacote' { export function packument( specifier: string, options: Record, - ): Promise; + ): Promise; } diff --git a/packages/angular/cli/utilities/color.ts b/packages/angular/cli/src/utilities/color.ts similarity index 100% rename from packages/angular/cli/utilities/color.ts rename to packages/angular/cli/src/utilities/color.ts diff --git a/packages/angular/cli/utilities/config.ts b/packages/angular/cli/src/utilities/config.ts similarity index 97% rename from packages/angular/cli/utilities/config.ts rename to packages/angular/cli/src/utilities/config.ts index 8fd804eda393..8549e8e66ff5 100644 --- a/packages/angular/cli/utilities/config.ts +++ b/packages/angular/cli/src/utilities/config.ts @@ -10,7 +10,7 @@ import { json, workspaces } from '@angular-devkit/core'; import { existsSync, readFileSync, statSync, writeFileSync } from 'fs'; import * as os from 'os'; import * as path from 'path'; -import { PackageManager } from '../lib/config/workspace-schema'; +import { PackageManager } from '../../lib/config/workspace-schema'; import { findUp } from './find-up'; import { JSONFile, readAndParseJson } from './json-file'; @@ -43,11 +43,7 @@ function createWorkspaceHost(): workspaces.WorkspaceHost { }; } -function getSchemaLocation(): string { - return path.join(__dirname, '../lib/config/schema.json'); -} - -export const workspaceSchemaPath = getSchemaLocation(); +export const workspaceSchemaPath = path.join(__dirname, '../../lib/config/schema.json'); const configNames = ['angular.json', '.angular.json']; const globalFileName = '.angular-config.json'; @@ -214,9 +210,7 @@ export function getWorkspaceRaw( } export async function validateWorkspace(data: json.JsonObject): Promise { - const schema = readAndParseJson( - path.join(__dirname, '../lib/config/schema.json'), - ) as json.schema.JsonSchema; + const schema = readAndParseJson(workspaceSchemaPath) as json.schema.JsonSchema; const { formats } = await import('@angular-devkit/schematics'); const registry = new json.schema.CoreSchemaRegistry(formats.standardFormats); const validator = await registry.compile(schema).toPromise(); diff --git a/packages/angular/cli/utilities/find-up.ts b/packages/angular/cli/src/utilities/find-up.ts similarity index 100% rename from packages/angular/cli/utilities/find-up.ts rename to packages/angular/cli/src/utilities/find-up.ts diff --git a/packages/angular/cli/utilities/install-package.ts b/packages/angular/cli/src/utilities/install-package.ts similarity index 98% rename from packages/angular/cli/utilities/install-package.ts rename to packages/angular/cli/src/utilities/install-package.ts index 8142135915a5..9205af7a39e9 100644 --- a/packages/angular/cli/utilities/install-package.ts +++ b/packages/angular/cli/src/utilities/install-package.ts @@ -10,8 +10,8 @@ import { spawn, spawnSync } from 'child_process'; import { existsSync, mkdtempSync, readFileSync, realpathSync, rmdirSync, writeFileSync } from 'fs'; import { tmpdir } from 'os'; import { join, resolve } from 'path'; -import { PackageManager } from '../lib/config/workspace-schema'; -import { NgAddSaveDepedency } from '../utilities/package-metadata'; +import { PackageManager } from '../../lib/config/workspace-schema'; +import { NgAddSaveDepedency } from './package-metadata'; import { Spinner } from './spinner'; interface PackageManagerOptions { diff --git a/packages/angular/cli/utilities/json-file.ts b/packages/angular/cli/src/utilities/json-file.ts similarity index 100% rename from packages/angular/cli/utilities/json-file.ts rename to packages/angular/cli/src/utilities/json-file.ts diff --git a/packages/angular/cli/utilities/log-file.ts b/packages/angular/cli/src/utilities/log-file.ts similarity index 100% rename from packages/angular/cli/utilities/log-file.ts rename to packages/angular/cli/src/utilities/log-file.ts diff --git a/packages/angular/cli/utilities/package-json.ts b/packages/angular/cli/src/utilities/package-json.ts similarity index 100% rename from packages/angular/cli/utilities/package-json.ts rename to packages/angular/cli/src/utilities/package-json.ts diff --git a/packages/angular/cli/utilities/package-manager.ts b/packages/angular/cli/src/utilities/package-manager.ts similarity index 97% rename from packages/angular/cli/utilities/package-manager.ts rename to packages/angular/cli/src/utilities/package-manager.ts index 82acba8ab923..14721e7e694a 100644 --- a/packages/angular/cli/utilities/package-manager.ts +++ b/packages/angular/cli/src/utilities/package-manager.ts @@ -11,7 +11,7 @@ import { constants, promises as fs } from 'fs'; import { join } from 'path'; import { satisfies, valid } from 'semver'; import { promisify } from 'util'; -import { PackageManager } from '../lib/config/workspace-schema'; +import { PackageManager } from '../../lib/config/workspace-schema'; import { getConfiguredPackageManager } from './config'; const exec = promisify(execCb); diff --git a/packages/angular/cli/utilities/package-metadata.ts b/packages/angular/cli/src/utilities/package-metadata.ts similarity index 100% rename from packages/angular/cli/utilities/package-metadata.ts rename to packages/angular/cli/src/utilities/package-metadata.ts diff --git a/packages/angular/cli/utilities/package-tree.ts b/packages/angular/cli/src/utilities/package-tree.ts similarity index 100% rename from packages/angular/cli/utilities/package-tree.ts rename to packages/angular/cli/src/utilities/package-tree.ts diff --git a/packages/angular/cli/utilities/project.ts b/packages/angular/cli/src/utilities/project.ts similarity index 100% rename from packages/angular/cli/utilities/project.ts rename to packages/angular/cli/src/utilities/project.ts diff --git a/packages/angular/cli/utilities/prompt.ts b/packages/angular/cli/src/utilities/prompt.ts similarity index 100% rename from packages/angular/cli/utilities/prompt.ts rename to packages/angular/cli/src/utilities/prompt.ts diff --git a/packages/angular/cli/utilities/spinner.ts b/packages/angular/cli/src/utilities/spinner.ts similarity index 100% rename from packages/angular/cli/utilities/spinner.ts rename to packages/angular/cli/src/utilities/spinner.ts diff --git a/packages/angular/cli/utilities/tty.ts b/packages/angular/cli/src/utilities/tty.ts similarity index 100% rename from packages/angular/cli/utilities/tty.ts rename to packages/angular/cli/src/utilities/tty.ts diff --git a/packages/angular/cli/utilities/version.ts b/packages/angular/cli/src/utilities/version.ts similarity index 88% rename from packages/angular/cli/utilities/version.ts rename to packages/angular/cli/src/utilities/version.ts index 802e7fbc2b2a..2c9db37d69a9 100644 --- a/packages/angular/cli/utilities/version.ts +++ b/packages/angular/cli/src/utilities/version.ts @@ -27,6 +27,8 @@ class Version { // export const VERSION = new Version('0.0.0-PLACEHOLDER'); export const VERSION = new Version( ( - JSON.parse(readFileSync(resolve(__dirname, '../package.json'), 'utf-8')) as { version: string } + JSON.parse(readFileSync(resolve(__dirname, '../../package.json'), 'utf-8')) as { + version: string; + } ).version, ); diff --git a/scripts/json-help.ts b/scripts/json-help.ts index 1cab27bf727d..a58c656851e9 100644 --- a/scripts/json-help.ts +++ b/scripts/json-help.ts @@ -10,7 +10,7 @@ import { logging } from '@angular-devkit/core'; import { spawn } from 'child_process'; import { promises as fs } from 'fs'; import * as os from 'os'; -import { JsonHelp } from 'packages/angular/cli/utilities/command-builder/json-help'; +import { JsonHelp } from 'packages/angular/cli/src/command-builder/utilities/json-help'; import * as path from 'path'; import { packages } from '../lib/packages'; import create from './create'; From a4b1f1bb953c661e3f3561a02452d127ecd690df Mon Sep 17 00:00:00 2001 From: Alan Agius Date: Wed, 2 Mar 2022 08:59:40 +0100 Subject: [PATCH 5/8] test: add temporary circular dependencies This temporary until the old command modules are removed. --- goldens/circular-deps/packages.json | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/goldens/circular-deps/packages.json b/goldens/circular-deps/packages.json index f47d35c030b4..9da0cf1ec292 100644 --- a/goldens/circular-deps/packages.json +++ b/goldens/circular-deps/packages.json @@ -2,5 +2,25 @@ [ "packages/angular_devkit/build_angular/src/utils/bundle-calculator.ts", "packages/angular_devkit/build_angular/src/webpack/utils/stats.ts" + ], + [ + "packages/angular/cli/src/commands/add/add-impl.ts", + "packages/angular/cli/src/commands/add/cli.ts" + ], + [ + "packages/angular/cli/src/commands/config/cli.ts", + "packages/angular/cli/src/commands/config/config-impl.ts" + ], + [ + "packages/angular/cli/src/commands/generate/cli.ts", + "packages/angular/cli/src/commands/generate/generate-impl.ts" + ], + [ + "packages/angular/cli/src/commands/new/cli.ts", + "packages/angular/cli/src/commands/new/new-impl.ts" + ], + [ + "packages/angular/cli/src/commands/update/cli.ts", + "packages/angular/cli/src/commands/update/update-impl.ts" ] ] From 1e22136fda9bde55a64a2894cccb8bff8f729298 Mon Sep 17 00:00:00 2001 From: Alan Agius Date: Wed, 2 Mar 2022 10:53:04 +0100 Subject: [PATCH 6/8] refactor(@angular/cli): move move architect common logic into a base class --- .../architect-base-command-module.ts | 156 ++++++++++++++++++ .../architect-command-module.ts | 105 ++---------- .../cli/src/command-builder/command-module.ts | 24 ++- .../cli/src/command-builder/command-runner.ts | 14 +- .../command-builder/utilities/architect.ts | 57 ------- packages/angular/cli/src/commands/run/cli.ts | 47 +----- 6 files changed, 198 insertions(+), 205 deletions(-) create mode 100644 packages/angular/cli/src/command-builder/architect-base-command-module.ts diff --git a/packages/angular/cli/src/command-builder/architect-base-command-module.ts b/packages/angular/cli/src/command-builder/architect-base-command-module.ts new file mode 100644 index 000000000000..aa328185d882 --- /dev/null +++ b/packages/angular/cli/src/command-builder/architect-base-command-module.ts @@ -0,0 +1,156 @@ +/** + * @license + * Copyright Google LLC 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 { Architect, Target } from '@angular-devkit/architect'; +import { WorkspaceNodeModulesArchitectHost } from '@angular-devkit/architect/node'; +import { json } from '@angular-devkit/core'; +import { existsSync } from 'fs'; +import { resolve } from 'path'; +import { isPackageNameSafeForAnalytics } from '../analytics/analytics'; +import { getPackageManager } from '../utilities/package-manager'; +import { + CommandModule, + CommandModuleError, + CommandModuleImplementation, + CommandScope, + OtherOptions, +} from './command-module'; +import { Option, parseJsonSchemaToOptions } from './utilities/json-schema'; + +export abstract class ArchitectBaseCommandModule + extends CommandModule + implements CommandModuleImplementation +{ + static override scope = CommandScope.In; + protected override shouldReportAnalytics = false; + protected readonly missingErrorTarget: string | undefined; + + protected async runSingleTarget(target: Target, options: OtherOptions): Promise { + // Remove options + const architectHost = await this.getArchitectHost(); + + let builderName: string; + try { + builderName = await architectHost.getBuilderNameForTarget(target); + } catch (e) { + throw new CommandModuleError(this.missingErrorTarget ?? e.message); + } + + await this.reportAnalytics({ + ...(await architectHost.getOptionsForTarget(target)), + ...options, + }); + + const { logger } = this.context; + + const run = await this.getArchitect().scheduleTarget(target, options as json.JsonObject, { + logger, + analytics: isPackageNameSafeForAnalytics(builderName) ? await this.getAnalytics() : undefined, + }); + + const { error, success } = await run.output.toPromise(); + await run.stop(); + + if (error) { + logger.error(error); + } + + return success ? 0 : 1; + } + + private _architectHost: WorkspaceNodeModulesArchitectHost | undefined; + protected getArchitectHost(): WorkspaceNodeModulesArchitectHost { + if (this._architectHost) { + return this._architectHost; + } + + const { workspace } = this.context; + if (!workspace) { + throw new CommandModuleError('A workspace is required for this command.'); + } + + return (this._architectHost = new WorkspaceNodeModulesArchitectHost( + workspace, + workspace.basePath, + )); + } + + private _architect: Architect | undefined; + protected getArchitect(): Architect { + if (this._architect) { + return this._architect; + } + + const registry = new json.schema.CoreSchemaRegistry(); + registry.addPostTransform(json.schema.transforms.addUndefinedDefaults); + registry.useXDeprecatedProvider((msg) => this.context.logger.warn(msg)); + + const { workspace } = this.context; + if (!workspace) { + throw new CommandModuleError('Cannot invoke this command outside of a workspace'); + } + + const architectHost = this.getArchitectHost(); + + return (this._architect = new Architect(architectHost, registry)); + } + + protected async getArchitectTargetOptions(target: Target): Promise { + const { workspace } = this.context; + if (!workspace) { + throw new CommandModuleError('A workspace is required for this command.'); + } + + const architectHost = new WorkspaceNodeModulesArchitectHost(workspace, workspace.basePath); + const builderConf = await architectHost.getBuilderNameForTarget(target); + + let builderDesc; + try { + builderDesc = await architectHost.resolveBuilder(builderConf); + } catch (e) { + if (e.code === 'MODULE_NOT_FOUND') { + await this.warnOnMissingNodeModules(); + throw new CommandModuleError(`Could not find the '${builderConf}' builder's node package.`); + } + + throw e; + } + + return parseJsonSchemaToOptions( + new json.schema.CoreSchemaRegistry(), + builderDesc.optionSchema as json.JsonObject, + true, + ); + } + + private async warnOnMissingNodeModules(): Promise { + const basePath = this.context.workspace?.basePath; + if (!basePath) { + return; + } + + // Check for a `node_modules` directory (npm, yarn non-PnP, etc.) + if (existsSync(resolve(basePath, 'node_modules'))) { + return; + } + + // Check for yarn PnP files + if ( + existsSync(resolve(basePath, '.pnp.js')) || + existsSync(resolve(basePath, '.pnp.cjs')) || + existsSync(resolve(basePath, '.pnp.mjs')) + ) { + return; + } + + const packageManager = await getPackageManager(basePath); + this.context.logger.warn( + `Node packages may not be installed. Try installing with '${packageManager} install'.`, + ); + } +} diff --git a/packages/angular/cli/src/command-builder/architect-command-module.ts b/packages/angular/cli/src/command-builder/architect-command-module.ts index 4ad170a95ac3..7aac582015e2 100644 --- a/packages/angular/cli/src/command-builder/architect-command-module.ts +++ b/packages/angular/cli/src/command-builder/architect-command-module.ts @@ -6,20 +6,14 @@ * found in the LICENSE file at https://angular.io/license */ -import { Architect, Target } from '@angular-devkit/architect'; -import { WorkspaceNodeModulesArchitectHost } from '@angular-devkit/architect/node'; -import { json } from '@angular-devkit/core'; import { Argv } from 'yargs'; -import { isPackageNameSafeForAnalytics } from '../analytics/analytics'; +import { ArchitectBaseCommandModule } from './architect-base-command-module'; import { - CommandModule, CommandModuleError, CommandModuleImplementation, - CommandScope, Options, OtherOptions, } from './command-module'; -import { getArchitectTargetOptions } from './utilities/architect'; export interface ArchitectCommandArgs { configuration?: string; @@ -27,13 +21,10 @@ export interface ArchitectCommandArgs { } export abstract class ArchitectCommandModule - extends CommandModule + extends ArchitectBaseCommandModule implements CommandModuleImplementation { - static override scope = CommandScope.In; abstract readonly multiTarget: boolean; - readonly missingErrorTarget: string | undefined; - protected override shouldReportAnalytics = false; async builder(argv: Argv): Promise> { const localYargs: Argv = argv @@ -52,17 +43,21 @@ export abstract class ArchitectCommandModule }) .strict(); - const targetSpecifier = this.makeTargetSpecifier(); - if (!targetSpecifier.project) { + const project = this.getArchitectProject(); + if (!project) { return localYargs; } - const schemaOptions = await getArchitectTargetOptions(this.context, targetSpecifier); + const target = this.getArchitectTarget(); + const schemaOptions = await this.getArchitectTargetOptions({ + project, + target, + }); return this.addSchemaOptionsToCommand(localYargs, schemaOptions); } - async run(options: Options): Promise { + async run(options: Options & OtherOptions): Promise { const { logger, workspace } = this.context; if (!workspace) { logger.fatal('A workspace is required for this command.'); @@ -70,17 +65,10 @@ export abstract class ArchitectCommandModule return 1; } - const registry = new json.schema.CoreSchemaRegistry(); - registry.addPostTransform(json.schema.transforms.addUndefinedDefaults); - registry.useXDeprecatedProvider((msg) => this.context.logger.warn(msg)); - - const architectHost = new WorkspaceNodeModulesArchitectHost(workspace, workspace.basePath); - const architect = new Architect(architectHost, registry); - - const targetSpec = this.makeTargetSpecifier(options); - if (!targetSpec.project) { - const target = this.getArchitectTarget(); + const target = this.getArchitectTarget(); + const { configuration = '', project, ...architectOptions } = options; + if (!project) { // This runs each target sequentially. // Running them in parallel would jumble the log messages. let result = 0; @@ -92,12 +80,12 @@ export abstract class ArchitectCommandModule } for (const project of projectNames) { - result |= await this.runSingleTarget({ ...targetSpec, project }, options, architect); + result |= await this.runSingleTarget({ configuration, target, project }, architectOptions); } return result; } else { - return await this.runSingleTarget(targetSpec, options, architect); + return await this.runSingleTarget({ configuration, target, project }, architectOptions); } } @@ -128,14 +116,6 @@ export abstract class ArchitectCommandModule return this.command?.split(' ', 1)[0]; } - private makeTargetSpecifier(options?: Options): Target { - return { - project: options?.project ?? this.getArchitectProject() ?? '', - target: this.getArchitectTarget(), - configuration: options?.configuration ?? '', - }; - } - private getProjectNamesByTarget(target: string): string[] | undefined { const workspace = this.context.workspace; if (!workspace) { @@ -174,59 +154,4 @@ export abstract class ArchitectCommandModule return undefined; } - - private async runSingleTarget( - target: Target, - options: Options & OtherOptions, - architect: Architect, - ): Promise { - // Remove options - const { configuration, project, ...extraOptions } = options; - const architectHost = await this.getArchitectHost(); - - let builderName: string; - try { - builderName = await architectHost.getBuilderNameForTarget(target); - } catch (e) { - throw new CommandModuleError(this.missingErrorTarget ?? e.message); - } - - await this.reportAnalytics({ - ...(await architectHost.getOptionsForTarget(target)), - ...extraOptions, - }); - - const { logger } = this.context; - - const run = await architect.scheduleTarget(target, extraOptions as json.JsonObject, { - logger, - analytics: isPackageNameSafeForAnalytics(builderName) ? await this.getAnalytics() : undefined, - }); - - const { error, success } = await run.output.toPromise(); - await run.stop(); - - if (error) { - logger.error(error); - } - - return success ? 0 : 1; - } - - private _architectHost: WorkspaceNodeModulesArchitectHost | undefined; - private getArchitectHost(): WorkspaceNodeModulesArchitectHost { - if (this._architectHost) { - return this._architectHost; - } - - const { workspace } = this.context; - if (!workspace) { - throw new CommandModuleError('A workspace is required for this command.'); - } - - return (this._architectHost = new WorkspaceNodeModulesArchitectHost( - workspace, - workspace.basePath, - )); - } } diff --git a/packages/angular/cli/src/command-builder/command-module.ts b/packages/angular/cli/src/command-builder/command-module.ts index b5412a42ce46..6f8f1e30c491 100644 --- a/packages/angular/cli/src/command-builder/command-module.ts +++ b/packages/angular/cli/src/command-builder/command-module.ts @@ -10,16 +10,22 @@ import { analytics, logging, normalize, strings } from '@angular-devkit/core'; import { readFileSync } from 'fs'; import * as path from 'path'; import { + ArgumentsCamelCase, Argv, CamelCaseKey, PositionalOptions, CommandModule as YargsCommandModule, Options as YargsOptions, } from 'yargs'; +import { Parser } from 'yargs/helpers'; import { createAnalytics } from '../analytics/analytics'; import { AngularWorkspace } from '../utilities/config'; import { Option } from './utilities/json-schema'; +const yargsParser = Parser as unknown as typeof Parser.default & { + camelCase(str: string): string; +}; + export type Options = { [key in keyof T as CamelCaseKey]: T[key] }; export enum CommandScope { @@ -55,8 +61,6 @@ export interface CommandModuleImplementation builder(argv: Argv): Promise> | Argv; /** A function which will be passed the parsed argv. */ run(options: Options & OtherOptions): Promise | number | void; - /** a function which will be passed the parsed argv. */ - handler(args: Options & OtherOptions): Promise | void; } export interface FullDescribe { @@ -106,16 +110,24 @@ export abstract class CommandModule implements CommandModuleI abstract builder(argv: Argv): Promise> | Argv; abstract run(options: Options & OtherOptions): Promise | number | void; - async handler(args: Options & OtherOptions): Promise { + async handler(args: ArgumentsCamelCase & OtherOptions): Promise { + const { _, $0, ...options } = args; + + // Camelize options as yargs will return the object in kebab-case when camel casing is disabled. + const camelCasedOptions: Record = {}; + for (const [key, value] of Object.entries(options)) { + camelCasedOptions[yargsParser.camelCase(key)] = value; + } + // Gather and report analytics. const analytics = await this.getAnalytics(); if (this.shouldReportAnalytics) { - await this.reportAnalytics(args); + await this.reportAnalytics(camelCasedOptions); } // Run and time command. const startTime = Date.now(); - const result = await this.run(args); + const result = await this.run(camelCasedOptions as Options & OtherOptions); const endTime = Date.now(); analytics.timing(this.commandName, 'duration', endTime - startTime); @@ -127,7 +139,7 @@ export abstract class CommandModule implements CommandModuleI } async reportAnalytics( - options: Options & OtherOptions, + options: (Options & OtherOptions) | OtherOptions, paths: string[] = [], dimensions: (boolean | number | string)[] = [], ): Promise { diff --git a/packages/angular/cli/src/command-builder/command-runner.ts b/packages/angular/cli/src/command-builder/command-runner.ts index 5cfcbacbba1d..e60811d58bc1 100644 --- a/packages/angular/cli/src/command-builder/command-runner.ts +++ b/packages/angular/cli/src/command-builder/command-runner.ts @@ -51,9 +51,7 @@ const COMMANDS = [ RunCommandModule, ]; -const yargsParser = Parser as unknown as typeof Parser.default & { - camelCase(str: string): string; -}; +const yargsParser = Parser as unknown as typeof Parser.default; export async function runCommand( args: string[], @@ -107,15 +105,7 @@ export async function runCommand( : describe, deprecated: commandModule.deprecated, builder: (x) => commandModule.builder(x), - handler: ({ _, $0, ...options }) => { - // Camelize options as yargs will return the object in kebab-case when camel casing is disabled. - const camelCasedOptions: Record = {}; - for (const [key, value] of Object.entries(options)) { - camelCasedOptions[yargsParser.camelCase(key)] = value; - } - - return commandModule.handler(camelCasedOptions); - }, + handler: (x) => commandModule.handler(x), }); } diff --git a/packages/angular/cli/src/command-builder/utilities/architect.ts b/packages/angular/cli/src/command-builder/utilities/architect.ts index 63e68193bfd5..3b756db992c8 100644 --- a/packages/angular/cli/src/command-builder/utilities/architect.ts +++ b/packages/angular/cli/src/command-builder/utilities/architect.ts @@ -14,60 +14,3 @@ import { resolve } from 'path'; import { getPackageManager } from '../../utilities/package-manager'; import { CommandContext, CommandModuleError } from '../command-module'; import { Option, parseJsonSchemaToOptions } from './json-schema'; - -export async function getArchitectTargetOptions( - context: CommandContext, - target: Target, -): Promise { - const { workspace } = context; - if (!workspace) { - return []; - } - - const architectHost = new WorkspaceNodeModulesArchitectHost(workspace, workspace.basePath); - const builderConf = await architectHost.getBuilderNameForTarget(target); - - let builderDesc; - try { - builderDesc = await architectHost.resolveBuilder(builderConf); - } catch (e) { - if (e.code === 'MODULE_NOT_FOUND') { - await warnOnMissingNodeModules(context); - throw new CommandModuleError(`Could not find the '${builderConf}' builder's node package.`); - } - - throw e; - } - - return parseJsonSchemaToOptions( - new json.schema.CoreSchemaRegistry(), - builderDesc.optionSchema as json.JsonObject, - true, - ); -} - -export async function warnOnMissingNodeModules(context: CommandContext): Promise { - const basePath = context.workspace?.basePath; - if (!basePath) { - return; - } - - // Check for a `node_modules` directory (npm, yarn non-PnP, etc.) - if (existsSync(resolve(basePath, 'node_modules'))) { - return; - } - - // Check for yarn PnP files - if ( - existsSync(resolve(basePath, '.pnp.js')) || - existsSync(resolve(basePath, '.pnp.cjs')) || - existsSync(resolve(basePath, '.pnp.mjs')) - ) { - return; - } - - const packageManager = await getPackageManager(basePath); - context.logger.warn( - `Node packages may not be installed. Try installing with '${packageManager} install'.`, - ); -} diff --git a/packages/angular/cli/src/commands/run/cli.ts b/packages/angular/cli/src/commands/run/cli.ts index 9ce685705e94..97b06f3f5a0c 100644 --- a/packages/angular/cli/src/commands/run/cli.ts +++ b/packages/angular/cli/src/commands/run/cli.ts @@ -6,12 +6,10 @@ * found in the LICENSE file at https://angular.io/license */ -import { Architect, Target } from '@angular-devkit/architect'; -import { WorkspaceNodeModulesArchitectHost } from '@angular-devkit/architect/node'; -import { json } from '@angular-devkit/core'; +import { Target } from '@angular-devkit/architect'; import { join } from 'path'; import { Argv } from 'yargs'; -import { isPackageNameSafeForAnalytics } from '../../analytics/analytics'; +import { ArchitectBaseCommandModule } from '../../command-builder/architect-base-command-module'; import { CommandModule, CommandModuleError, @@ -20,14 +18,13 @@ import { Options, OtherOptions, } from '../../command-builder/command-module'; -import { getArchitectTargetOptions } from '../../command-builder/utilities/architect'; export interface RunCommandArgs { target: string; } export class RunCommandModule - extends CommandModule + extends ArchitectBaseCommandModule implements CommandModuleImplementation { static override scope = CommandScope.In; @@ -51,50 +48,20 @@ export class RunCommandModule return localYargs; } - const schemaOptions = await getArchitectTargetOptions(this.context, target); + const schemaOptions = await this.getArchitectTargetOptions(target); return this.addSchemaOptionsToCommand(localYargs, schemaOptions); } - async run(options: Options & OtherOptions): Promise { - const { logger, workspace } = this.context; - if (!workspace) { - throw new CommandModuleError('A workspace is required for this command.'); - } - - const registry = new json.schema.CoreSchemaRegistry(); - registry.addPostTransform(json.schema.transforms.addUndefinedDefaults); - registry.useXDeprecatedProvider((msg) => logger.warn(msg)); - - const architectHost = new WorkspaceNodeModulesArchitectHost(workspace, workspace.basePath); - const architect = new Architect(architectHost, registry); - + async run(options: Options & OtherOptions): Promise { const target = this.makeTargetSpecifier(options); + const { target: _target, ...extraOptions } = options; if (!target) { throw new CommandModuleError('Cannot determine project or target.'); } - const builderName = await architectHost.getBuilderNameForTarget(target); - await this.reportAnalytics({ - ...(await architectHost.getOptionsForTarget(target)), - ...options, - }); - - const { target: _target, ...extraOptions } = options; - const run = await architect.scheduleTarget(target, extraOptions as json.JsonObject, { - logger, - analytics: isPackageNameSafeForAnalytics(builderName) ? await this.getAnalytics() : undefined, - }); - - const { error, success } = await run.output.toPromise(); - await run.stop(); - - if (error) { - logger.error(error); - } - - return success ? 0 : 1; + return this.runSingleTarget(target, extraOptions); } protected makeTargetSpecifier(options?: Options): Target | undefined { From 84e97c473fd7fa1d1a4246ef65f3b39c3af69e89 Mon Sep 17 00:00:00 2001 From: Alan Agius Date: Thu, 3 Mar 2022 09:38:25 +0100 Subject: [PATCH 7/8] refactor(@angular/cli): remove unnecessary castings and types --- packages/angular/cli/src/command-builder/command-module.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/angular/cli/src/command-builder/command-module.ts b/packages/angular/cli/src/command-builder/command-module.ts index 6f8f1e30c491..9d1a5fa72574 100644 --- a/packages/angular/cli/src/command-builder/command-module.ts +++ b/packages/angular/cli/src/command-builder/command-module.ts @@ -17,15 +17,11 @@ import { CommandModule as YargsCommandModule, Options as YargsOptions, } from 'yargs'; -import { Parser } from 'yargs/helpers'; +import { Parser as yargsParser } from 'yargs/helpers'; import { createAnalytics } from '../analytics/analytics'; import { AngularWorkspace } from '../utilities/config'; import { Option } from './utilities/json-schema'; -const yargsParser = Parser as unknown as typeof Parser.default & { - camelCase(str: string): string; -}; - export type Options = { [key in keyof T as CamelCaseKey]: T[key] }; export enum CommandScope { From 9dafd5a9fe836a74b9d131a4f51b04eb18823dfe Mon Sep 17 00:00:00 2001 From: Alan Agius Date: Wed, 9 Mar 2022 16:34:31 +0100 Subject: [PATCH 8/8] refactor(@angular/cli): several small refactoring and code quality improvements This PR brings a number of small refactors to improve code quality in the new args parser implementation. --- packages/angular/cli/lib/cli/index.ts | 3 +- .../architect-base-command-module.ts | 18 +------- .../architect-command-module.ts | 16 ++----- .../cli/src/command-builder/command-module.ts | 42 +++++++++++++------ .../cli/src/command-builder/command-runner.ts | 14 +++---- .../command-builder/utilities/architect.ts | 16 ------- .../command-builder/utilities/json-help.ts | 10 ++--- .../angular/cli/src/commands/analytics/cli.ts | 3 +- .../commands/analytics/long-description.md | 2 + .../src/commands/config/long-description.md | 4 +- .../cli/src/commands/make-this-awesome/cli.ts | 1 + .../angular/cli/src/commands/version/cli.ts | 2 +- .../e2e/tests/commands/help/help-json.ts | 41 +++++++++--------- 13 files changed, 76 insertions(+), 96 deletions(-) delete mode 100644 packages/angular/cli/src/command-builder/utilities/architect.ts diff --git a/packages/angular/cli/lib/cli/index.ts b/packages/angular/cli/lib/cli/index.ts index e1d43055d8e6..ef2f15d766de 100644 --- a/packages/angular/cli/lib/cli/index.ts +++ b/packages/angular/cli/lib/cli/index.ts @@ -6,7 +6,6 @@ * found in the LICENSE file at https://angular.io/license */ -import { schema } from '@angular-devkit/core'; import { createConsoleLogger } from '@angular-devkit/core/node'; import { format } from 'util'; import { CommandModuleError } from '../../src/command-builder/command-module'; @@ -79,7 +78,7 @@ export default async function (options: { testing?: boolean; cliArgs: string[] } try { return await runCommand(options.cliArgs, logger, workspace); } catch (err) { - if (err instanceof CommandModuleError || err instanceof schema.SchemaValidationException) { + if (err instanceof CommandModuleError) { logger.fatal(`Error: ${err.message}`); } else if (err instanceof Error) { try { diff --git a/packages/angular/cli/src/command-builder/architect-base-command-module.ts b/packages/angular/cli/src/command-builder/architect-base-command-module.ts index aa328185d882..0582d9a6e472 100644 --- a/packages/angular/cli/src/command-builder/architect-base-command-module.ts +++ b/packages/angular/cli/src/command-builder/architect-base-command-module.ts @@ -31,7 +31,6 @@ export abstract class ArchitectBaseCommandModule protected readonly missingErrorTarget: string | undefined; protected async runSingleTarget(target: Target, options: OtherOptions): Promise { - // Remove options const architectHost = await this.getArchitectHost(); let builderName: string; @@ -69,10 +68,7 @@ export abstract class ArchitectBaseCommandModule return this._architectHost; } - const { workspace } = this.context; - if (!workspace) { - throw new CommandModuleError('A workspace is required for this command.'); - } + const workspace = this.getWorkspaceOrThrow(); return (this._architectHost = new WorkspaceNodeModulesArchitectHost( workspace, @@ -90,23 +86,13 @@ export abstract class ArchitectBaseCommandModule registry.addPostTransform(json.schema.transforms.addUndefinedDefaults); registry.useXDeprecatedProvider((msg) => this.context.logger.warn(msg)); - const { workspace } = this.context; - if (!workspace) { - throw new CommandModuleError('Cannot invoke this command outside of a workspace'); - } - const architectHost = this.getArchitectHost(); return (this._architect = new Architect(architectHost, registry)); } protected async getArchitectTargetOptions(target: Target): Promise { - const { workspace } = this.context; - if (!workspace) { - throw new CommandModuleError('A workspace is required for this command.'); - } - - const architectHost = new WorkspaceNodeModulesArchitectHost(workspace, workspace.basePath); + const architectHost = this.getArchitectHost(); const builderConf = await architectHost.getBuilderNameForTarget(target); let builderDesc; diff --git a/packages/angular/cli/src/command-builder/architect-command-module.ts b/packages/angular/cli/src/command-builder/architect-command-module.ts index 7aac582015e2..8b46470f9d86 100644 --- a/packages/angular/cli/src/command-builder/architect-command-module.ts +++ b/packages/angular/cli/src/command-builder/architect-command-module.ts @@ -58,14 +58,8 @@ export abstract class ArchitectCommandModule } async run(options: Options & OtherOptions): Promise { - const { logger, workspace } = this.context; - if (!workspace) { - logger.fatal('A workspace is required for this command.'); - - return 1; - } - const target = this.getArchitectTarget(); + const { configuration = '', project, ...architectOptions } = options; if (!project) { @@ -112,15 +106,11 @@ export abstract class ArchitectCommandModule } private getArchitectTarget(): string { - // 'build [project]' -> 'build' - return this.command?.split(' ', 1)[0]; + return this.commandName; } private getProjectNamesByTarget(target: string): string[] | undefined { - const workspace = this.context.workspace; - if (!workspace) { - throw new CommandModuleError('A workspace is required for this command.'); - } + const workspace = this.getWorkspaceOrThrow(); const allProjectsForTargetName: string[] = []; for (const [name, project] of workspace.projects) { diff --git a/packages/angular/cli/src/command-builder/command-module.ts b/packages/angular/cli/src/command-builder/command-module.ts index 9d1a5fa72574..85c003ac4a6a 100644 --- a/packages/angular/cli/src/command-builder/command-module.ts +++ b/packages/angular/cli/src/command-builder/command-module.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import { analytics, logging, normalize, strings } from '@angular-devkit/core'; +import { analytics, logging, normalize, schema, strings } from '@angular-devkit/core'; import { readFileSync } from 'fs'; import * as path from 'path'; import { @@ -38,7 +38,7 @@ export interface CommandContext { root: string; workspace?: AngularWorkspace; logger: logging.Logger; - /** Arguments parsed in free from without parser configuration. */ + /** Arguments parsed in free-from without parser configuration. */ args: { positional: string[]; options: { @@ -121,16 +121,25 @@ export abstract class CommandModule implements CommandModuleI await this.reportAnalytics(camelCasedOptions); } - // Run and time command. - const startTime = Date.now(); - const result = await this.run(camelCasedOptions as Options & OtherOptions); - const endTime = Date.now(); - - analytics.timing(this.commandName, 'duration', endTime - startTime); - await analytics.flush(); - - if (typeof result === 'number' && result > 0) { - process.exitCode = result; + let exitCode: number | void | undefined; + try { + // Run and time command. + const startTime = Date.now(); + exitCode = await this.run(camelCasedOptions as Options & OtherOptions); + const endTime = Date.now(); + analytics.timing(this.commandName, 'duration', endTime - startTime); + await analytics.flush(); + } catch (e) { + if (e instanceof schema.SchemaValidationException) { + this.context.logger.fatal(`Error: ${e.message}`); + exitCode = 1; + } else { + throw e; + } + } finally { + if (typeof exitCode === 'number' && exitCode > 0) { + process.exitCode = exitCode; + } } } @@ -223,6 +232,15 @@ export abstract class CommandModule implements CommandModuleI return localYargs; } + + protected getWorkspaceOrThrow(): AngularWorkspace { + const { workspace } = this.context; + if (!workspace) { + throw new CommandModuleError('A workspace is required for this command.'); + } + + return workspace; + } } /** diff --git a/packages/angular/cli/src/command-builder/command-runner.ts b/packages/angular/cli/src/command-builder/command-runner.ts index e60811d58bc1..f2fc06776ceb 100644 --- a/packages/angular/cli/src/command-builder/command-runner.ts +++ b/packages/angular/cli/src/command-builder/command-runner.ts @@ -90,22 +90,22 @@ export async function runCommand( } } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const commandModule = new CommandModule(context) as any; + const commandModule = new CommandModule(context); const describe = jsonHelp ? commandModule.fullDescribe : commandModule.describe; localYargs = localYargs.command({ command: commandModule.command, - aliases: commandModule.aliases, + aliases: 'aliases' in commandModule ? commandModule.aliases : undefined, describe: // We cannot add custom fields in help, such as long command description which is used in AIO. - // Therefore, we get around this by adding a complex object as a string which we later parse when geneerating the help files. + // Therefore, we get around this by adding a complex object as a string which we later parse when generating the help files. describe !== undefined && typeof describe === 'object' ? JSON.stringify(describe) : describe, - deprecated: commandModule.deprecated, - builder: (x) => commandModule.builder(x), - handler: (x) => commandModule.handler(x), + deprecated: 'deprecated' in commandModule ? commandModule.deprecated : undefined, + builder: (argv) => commandModule.builder(argv) as yargs.Argv, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + handler: (args: any) => commandModule.handler(args), }); } diff --git a/packages/angular/cli/src/command-builder/utilities/architect.ts b/packages/angular/cli/src/command-builder/utilities/architect.ts deleted file mode 100644 index 3b756db992c8..000000000000 --- a/packages/angular/cli/src/command-builder/utilities/architect.ts +++ /dev/null @@ -1,16 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 { Target } from '@angular-devkit/architect'; -import { WorkspaceNodeModulesArchitectHost } from '@angular-devkit/architect/node'; -import { json } from '@angular-devkit/core'; -import { existsSync } from 'fs'; -import { resolve } from 'path'; -import { getPackageManager } from '../../utilities/package-manager'; -import { CommandContext, CommandModuleError } from '../command-module'; -import { Option, parseJsonSchemaToOptions } from './json-schema'; diff --git a/packages/angular/cli/src/command-builder/utilities/json-help.ts b/packages/angular/cli/src/command-builder/utilities/json-help.ts index 047fcdc3fa6c..8e51e1153647 100644 --- a/packages/angular/cli/src/command-builder/utilities/json-help.ts +++ b/packages/angular/cli/src/command-builder/utilities/json-help.ts @@ -11,7 +11,7 @@ import { FullDescribe } from '../command-module'; export interface JsonHelp { name: string; - description?: string; + shortDescription?: string; command: string; longDescription?: string; longDescriptionRelativePath?: string; @@ -111,22 +111,22 @@ export function jsonHelpUsage(): string { })) .sort((a, b) => a.name.localeCompare(b.name)); - const parseDescription = (rawDescription: string) => { + const parseDescription = (rawDescription: string): Partial => { try { const { longDescription, - describe: description, + describe: shortDescription, longDescriptionRelativePath, } = JSON.parse(rawDescription) as FullDescribe; return { - description, + shortDescription, longDescriptionRelativePath, longDescription, }; } catch { return { - description: rawDescription, + shortDescription: rawDescription, }; } }; diff --git a/packages/angular/cli/src/commands/analytics/cli.ts b/packages/angular/cli/src/commands/analytics/cli.ts index ea70271fd10c..47d85a3604cf 100644 --- a/packages/angular/cli/src/commands/analytics/cli.ts +++ b/packages/angular/cli/src/commands/analytics/cli.ts @@ -22,8 +22,7 @@ interface AnalyticsCommandArgs { export class AnalyticsCommandModule extends CommandModule { command = 'analytics '; - describe = - 'Configures the gathering of Angular CLI usage metrics. See https://angular.io/cli/usage-analytics-gathering.'; + describe = 'Configures the gathering of Angular CLI usage metrics.'; longDescriptionPath = join(__dirname, 'long-description.md'); builder(localYargs: Argv): Argv { diff --git a/packages/angular/cli/src/commands/analytics/long-description.md b/packages/angular/cli/src/commands/analytics/long-description.md index ada011b82d31..6900aea53a45 100644 --- a/packages/angular/cli/src/commands/analytics/long-description.md +++ b/packages/angular/cli/src/commands/analytics/long-description.md @@ -6,3 +6,5 @@ The value of `setting-or-project` is one of the following. which uses a common CI user. - `prompt`: Prompts the user to set the status interactively. - `project`: Sets the default status for the project to the `project-setting` value, which can be any of the other values. The `project-setting` argument is ignored for all other values of `setting_or_project`. + +For further details, see [Gathering an Viewing CLI Usage Analytics](cli/usage-analytics-gathering). diff --git a/packages/angular/cli/src/commands/config/long-description.md b/packages/angular/cli/src/commands/config/long-description.md index 2ed7e8c7a6c6..78cc49e45662 100644 --- a/packages/angular/cli/src/commands/config/long-description.md +++ b/packages/angular/cli/src/commands/config/long-description.md @@ -6,6 +6,8 @@ or indirectly on the command line using this command. The configurable property names match command option names, except that in the configuration file, all names must use camelCase, -while on the command line options can be given in either camelCase or dash-case. +while on the command line options can be given dash-case. For further details, see [Workspace Configuration](guide/workspace-config). + +For configuration of CLI usage analytics, see [Gathering an Viewing CLI Usage Analytics](cli/usage-analytics-gathering). diff --git a/packages/angular/cli/src/commands/make-this-awesome/cli.ts b/packages/angular/cli/src/commands/make-this-awesome/cli.ts index 45b77f36da93..83bcd9df3740 100644 --- a/packages/angular/cli/src/commands/make-this-awesome/cli.ts +++ b/packages/angular/cli/src/commands/make-this-awesome/cli.ts @@ -13,6 +13,7 @@ import { colors } from '../../utilities/color'; export class AwesomeCommandModule extends CommandModule implements CommandModuleImplementation { command = 'make-this-awesome'; describe: false = false; + deprecated = false; longDescriptionPath?: string | undefined; builder(localYargs: Argv): Argv { diff --git a/packages/angular/cli/src/commands/version/cli.ts b/packages/angular/cli/src/commands/version/cli.ts index 9ed6f288740e..3523fd243f86 100644 --- a/packages/angular/cli/src/commands/version/cli.ts +++ b/packages/angular/cli/src/commands/version/cli.ts @@ -24,7 +24,7 @@ interface PartialPackageInfo { /** * Major versions of Node.js that are officially supported by Angular. */ -const SUPPORTED_NODE_MAJORS = [12, 14, 16]; +const SUPPORTED_NODE_MAJORS = [14, 16]; const PACKAGE_PATTERNS = [ /^@angular\/.*/, diff --git a/tests/legacy-cli/e2e/tests/commands/help/help-json.ts b/tests/legacy-cli/e2e/tests/commands/help/help-json.ts index 9c54564fa850..3c8786e06ee1 100644 --- a/tests/legacy-cli/e2e/tests/commands/help/help-json.ts +++ b/tests/legacy-cli/e2e/tests/commands/help/help-json.ts @@ -3,33 +3,32 @@ import { silentNg } from '../../../utils/process'; export default async function () { // This test is use as a sanity check. const addHelpOutputSnapshot = JSON.stringify({ - name: 'analytics', - command: 'ng analytics ', - description: - 'Configures the gathering of Angular CLI usage metrics. See https://angular.io/cli/usage-analytics-gathering.', - longDescriptionRelativePath: '@angular/cli/src/commands/analytics/long-description.md', - longDescription: - 'The value of `setting-or-project` is one of the following.\n\n- `on`: Enables analytics gathering and reporting for the user.\n- `off`: Disables analytics gathering and reporting for the user.\n- `ci`: Enables analytics and configures reporting for use with Continuous Integration,\n which uses a common CI user.\n- `prompt`: Prompts the user to set the status interactively.\n- `project`: Sets the default status for the project to the `project-setting` value, which can be any of the other values. The `project-setting` argument is ignored for all other values of `setting_or_project`.\n', - options: [ + 'name': 'analytics', + 'command': 'ng analytics ', + 'shortDescription': 'Configures the gathering of Angular CLI usage metrics.', + 'longDescriptionRelativePath': '@angular/cli/src/commands/analytics/long-description.md', + 'longDescription': + 'The value of `setting-or-project` is one of the following.\n\n- `on`: Enables analytics gathering and reporting for the user.\n- `off`: Disables analytics gathering and reporting for the user.\n- `ci`: Enables analytics and configures reporting for use with Continuous Integration,\n which uses a common CI user.\n- `prompt`: Prompts the user to set the status interactively.\n- `project`: Sets the default status for the project to the `project-setting` value, which can be any of the other values. The `project-setting` argument is ignored for all other values of `setting_or_project`.\n\nFor further details, see [Gathering an Viewing CLI Usage Analytics](cli/usage-analytics-gathering).\n', + 'options': [ { - name: 'help', - type: 'boolean', - description: 'Shows a help message for this command in the console.', + 'name': 'help', + 'type': 'boolean', + 'description': 'Shows a help message for this command in the console.', }, { - name: 'project-setting', - type: 'string', - enum: ['on', 'off', 'prompt'], - description: 'Sets the default analytics enablement status for the project.', - positional: 1, + 'name': 'project-setting', + 'type': 'string', + 'enum': ['on', 'off', 'prompt'], + 'description': 'Sets the default analytics enablement status for the project.', + 'positional': 1, }, { - name: 'setting-or-project', - type: 'string', - enum: ['on', 'off', 'ci', 'prompt'], - description: + 'name': 'setting-or-project', + 'type': 'string', + 'enum': ['on', 'off', 'ci', 'prompt'], + 'description': 'Directly enables or disables all usage analytics for the user, or prompts the user to set the status interactively, or sets the default status for the project.', - positional: 0, + 'positional': 0, }, ], });