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" ] ] 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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/lib/cli/index.ts b/packages/angular/cli/lib/cli/index.ts index cc57f6c68107..ef2f15d766de 100644 --- a/packages/angular/cli/lib/cli/index.ts +++ b/packages/angular/cli/lib/cli/index.ts @@ -8,13 +8,14 @@ import { createConsoleLogger } from '@angular-devkit/core/node'; import { format } from 'util'; -import { runCommand } from '../../models/command-runner'; -import { colors, removeColor } from '../../utilities/color'; -import { AngularWorkspace, getWorkspaceRaw } from '../../utilities/config'; -import { writeErrorToLogFile } from '../../utilities/log-file'; -import { findWorkspaceFile } from '../../utilities/project'; +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, Version } from '../../models/version'; +export { VERSION } from '../../src/utilities/version'; const debugEnv = process.env['NG_DEBUG']; const isDebug = debugEnv !== undefined && debugEnv !== '0' && debugEnv.toLowerCase() !== 'false'; @@ -75,16 +76,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) { + 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..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 { VERSION } from '../models/version'; -import { colors } from '../utilities/color'; -import { isWarningEnabled } from '../utilities/config'; +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/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..1e78c1348c40 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 { AngularWorkspace } from '../utilities/config'; -import { - Arguments, - CommandContext, - CommandDescription, - CommandDescriptionMap, - CommandScope, - Option, -} from './interface'; +import { analytics, logging } from '@angular-devkit/core'; +import { Option } from '../src/command-builder/utilities/json-schema'; +import { AngularWorkspace } from '../src/utilities/config'; +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..e4135dab4823 100644 --- a/packages/angular/cli/models/interface.ts +++ b/packages/angular/cli/models/interface.ts @@ -6,45 +6,8 @@ * found in the LICENSE file at https://angular.io/license */ -import { analytics, json, 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; -} +import { analytics, logging } from '@angular-devkit/core'; +import { AngularWorkspace } from '../src/utilities/config'; /** * 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..6c0ddc0a414b 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, @@ -21,15 +16,14 @@ import { } from '@angular-devkit/schematics/tools'; import * as inquirer from 'inquirer'; import * as systemPath from 'path'; -import { colors } from '../utilities/color'; -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 { 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 { 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/models/analytics-collector.ts b/packages/angular/cli/src/analytics/analytics-collector.ts similarity index 99% rename from packages/angular/cli/models/analytics-collector.ts rename to packages/angular/cli/src/analytics/analytics-collector.ts index 6754d5037059..5d0a8cf8c88e 100644 --- a/packages/angular/cli/models/analytics-collector.ts +++ b/packages/angular/cli/src/analytics/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/src/analytics/analytics.ts similarity index 90% rename from packages/angular/cli/models/analytics.ts rename to packages/angular/cli/src/analytics/analytics.ts index 1171e801dc49..34febb2a9dbc 100644 --- a/packages/angular/cli/models/analytics.ts +++ b/packages/angular/cli/src/analytics/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/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..0582d9a6e472 --- /dev/null +++ b/packages/angular/cli/src/command-builder/architect-base-command-module.ts @@ -0,0 +1,142 @@ +/** + * @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 { + 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.getWorkspaceOrThrow(); + + 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 architectHost = this.getArchitectHost(); + + return (this._architect = new Architect(architectHost, registry)); + } + + protected async getArchitectTargetOptions(target: Target): Promise { + const architectHost = this.getArchitectHost(); + 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 new file mode 100644 index 000000000000..8b46470f9d86 --- /dev/null +++ b/packages/angular/cli/src/command-builder/architect-command-module.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 { Argv } from 'yargs'; +import { ArchitectBaseCommandModule } from './architect-base-command-module'; +import { + CommandModuleError, + CommandModuleImplementation, + Options, + OtherOptions, +} from './command-module'; + +export interface ArchitectCommandArgs { + configuration?: string; + project?: string; +} + +export abstract class ArchitectCommandModule + extends ArchitectBaseCommandModule + implements CommandModuleImplementation +{ + abstract readonly multiTarget: boolean; + + 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 project = this.getArchitectProject(); + if (!project) { + return localYargs; + } + + const target = this.getArchitectTarget(); + const schemaOptions = await this.getArchitectTargetOptions({ + project, + target, + }); + + return this.addSchemaOptionsToCommand(localYargs, schemaOptions); + } + + async run(options: Options & OtherOptions): Promise { + 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; + 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({ configuration, target, project }, architectOptions); + } + + return result; + } else { + return await this.runSingleTarget({ configuration, target, project }, architectOptions); + } + } + + 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 { + return this.commandName; + } + + private getProjectNamesByTarget(target: string): string[] | undefined { + const workspace = this.getWorkspaceOrThrow(); + + 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; + } +} diff --git a/packages/angular/cli/src/command-builder/command-module.ts b/packages/angular/cli/src/command-builder/command-module.ts new file mode 100644 index 000000000000..85c003ac4a6a --- /dev/null +++ b/packages/angular/cli/src/command-builder/command-module.ts @@ -0,0 +1,250 @@ +/** + * @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, schema, 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 as yargsParser } from 'yargs/helpers'; +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] }; + +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; +} + +export interface FullDescribe { + describe?: string; + longDescription?: string; + longDescriptionRelativePath?: 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, + ...(this.longDescriptionPath + ? { + longDescriptionRelativePath: path.relative( + path.join(__dirname, '../../../../'), + this.longDescriptionPath, + ), + longDescription: readFileSync(this.longDescriptionPath, 'utf8'), + } + : {}), + }; + } + + 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: 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(camelCasedOptions); + } + + 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; + } + } + } + + async reportAnalytics( + options: (Options & OtherOptions) | 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; + } + + protected getWorkspaceOrThrow(): AngularWorkspace { + const { workspace } = this.context; + if (!workspace) { + throw new CommandModuleError('A workspace is required for this command.'); + } + + return workspace; + } +} + +/** + * 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/src/command-builder/command-runner.ts b/packages/angular/cli/src/command-builder/command-runner.ts new file mode 100644 index 000000000000..f2fc06776ceb --- /dev/null +++ b/packages/angular/cli/src/command-builder/command-runner.ts @@ -0,0 +1,161 @@ +/** + * @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 { AngularWorkspace } from '../utilities/config'; +import { CommandContext, CommandModuleError, CommandScope } from './command-module'; +import { jsonHelpUsage } from './utilities/json-help'; + +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; + +export async function runCommand( + args: string[], + logger: logging.Logger, + workspace: AngularWorkspace | undefined, +): Promise { + const { + $0, + _: positional, + help = false, + jsonHelp = false, + ...rest + } = yargsParser(args, { boolean: ['help', 'json-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) { + 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; + } + } + + const commandModule = new CommandModule(context); + const describe = jsonHelp ? commandModule.fullDescribe : commandModule.describe; + + localYargs = localYargs.command({ + command: commandModule.command, + 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 generating the help files. + describe !== undefined && typeof describe === 'object' + ? JSON.stringify(describe) + : describe, + 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), + }); + } + + 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 + .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/src/command-builder/schematics-command-module.ts b/packages/angular/cli/src/command-builder/schematics-command-module.ts new file mode 100644 index 000000000000..75f99317e328 --- /dev/null +++ b/packages/angular/cli/src/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 '../utilities/config'; +import { CommandModule, CommandModuleImplementation, CommandScope } from './command-module'; +import { Option, parseJsonSchemaToOptions } from './utilities/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/src/command-builder/utilities/json-help.ts b/packages/angular/cli/src/command-builder/utilities/json-help.ts new file mode 100644 index 000000000000..8e51e1153647 --- /dev/null +++ b/packages/angular/cli/src/command-builder/utilities/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; + shortDescription?: 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): Partial => { + try { + const { + longDescription, + describe: shortDescription, + longDescriptionRelativePath, + } = JSON.parse(rawDescription) as FullDescribe; + + return { + shortDescription, + longDescriptionRelativePath, + longDescription, + }; + } catch { + return { + shortDescription: 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/packages/angular/cli/src/command-builder/utilities/json-schema.ts b/packages/angular/cli/src/command-builder/utilities/json-schema.ts new file mode 100644 index 000000000000..8146cd71dbfd --- /dev/null +++ b/packages/angular/cli/src/command-builder/utilities/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/commands/add-impl.ts b/packages/angular/cli/src/commands/add/add-impl.ts similarity index 89% rename from packages/angular/cli/commands/add-impl.ts rename to packages/angular/cli/src/commands/add/add-impl.ts index a2cd46602e22..474300b63037 100644 --- a/packages/angular/cli/commands/add-impl.ts +++ b/packages/angular/cli/src/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 { SchematicCommand } from '../../../models/schematic-command'; +import { isPackageNameSafeForAnalytics } from '../../analytics/analytics'; +import { Options } from '../../command-builder/command-module'; +import { colors } from '../../utilities/color'; +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/src/commands/add/cli.ts b/packages/angular/cli/src/commands/add/cli.ts new file mode 100644 index 000000000000..4099d9f305ef --- /dev/null +++ b/packages/angular/cli/src/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 '../../command-builder/command-module'; +import { + SchematicsCommandArgs, + SchematicsCommandModule, +} from '../../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/src/commands/add/long-description.md similarity index 100% rename from packages/angular/cli/commands/add.md rename to packages/angular/cli/src/commands/add/long-description.md diff --git a/packages/angular/cli/src/commands/analytics/cli.ts b/packages/angular/cli/src/commands/analytics/cli.ts new file mode 100644 index 000000000000..47d85a3604cf --- /dev/null +++ b/packages/angular/cli/src/commands/analytics/cli.ts @@ -0,0 +1,84 @@ +/** + * @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 { + promptGlobalAnalytics, + promptProjectAnalytics, + setAnalyticsConfig, +} from '../../analytics/analytics'; +import { CommandModule, Options } from '../../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.'; + 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/src/commands/analytics/long-description.md b/packages/angular/cli/src/commands/analytics/long-description.md new file mode 100644 index 000000000000..6900aea53a45 --- /dev/null +++ b/packages/angular/cli/src/commands/analytics/long-description.md @@ -0,0 +1,10 @@ +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`. + +For further details, see [Gathering an Viewing CLI Usage Analytics](cli/usage-analytics-gathering). diff --git a/packages/angular/cli/src/commands/build/cli.ts b/packages/angular/cli/src/commands/build/cli.ts new file mode 100644 index 000000000000..434ff4f22f84 --- /dev/null +++ b/packages/angular/cli/src/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 '../../command-builder/architect-command-module'; +import { CommandModuleImplementation } from '../../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/src/commands/build/long-description.md similarity index 100% rename from packages/angular/cli/commands/build-long.md rename to packages/angular/cli/src/commands/build/long-description.md diff --git a/packages/angular/cli/src/commands/config/cli.ts b/packages/angular/cli/src/commands/config/cli.ts new file mode 100644 index 000000000000..8fbabe1ae443 --- /dev/null +++ b/packages/angular/cli/src/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 '../../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/src/commands/config/config-impl.ts similarity index 87% rename from packages/angular/cli/commands/config-impl.ts rename to packages/angular/cli/src/commands/config/config-impl.ts index 1e73b6985471..7688609e8372 100644 --- a/packages/angular/cli/commands/config-impl.ts +++ b/packages/angular/cli/src/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 '../../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/src/commands/config/long-description.md similarity index 80% rename from packages/angular/cli/commands/config-long.md rename to packages/angular/cli/src/commands/config/long-description.md index 7f44f63b3b32..78cc49e45662 100644 --- a/packages/angular/cli/commands/config-long.md +++ b/packages/angular/cli/src/commands/config/long-description.md @@ -6,8 +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](./usage-analytics-gathering). +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/deploy/cli.ts b/packages/angular/cli/src/commands/deploy/cli.ts new file mode 100644 index 000000000000..18f5aaadd803 --- /dev/null +++ b/packages/angular/cli/src/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 '../../command-builder/architect-command-module'; +import { CommandModuleImplementation } from '../../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/src/commands/deploy/long-description.md similarity index 100% rename from packages/angular/cli/commands/deploy-long.md rename to packages/angular/cli/src/commands/deploy/long-description.md diff --git a/packages/angular/cli/src/commands/doc/cli.ts b/packages/angular/cli/src/commands/doc/cli.ts new file mode 100644 index 000000000000..fd2423e3603d --- /dev/null +++ b/packages/angular/cli/src/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 '../../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/src/commands/e2e/cli.ts b/packages/angular/cli/src/commands/e2e/cli.ts new file mode 100644 index 000000000000..bb59ca78fc2a --- /dev/null +++ b/packages/angular/cli/src/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 '../../command-builder/architect-command-module'; +import { CommandModuleImplementation } from '../../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/src/commands/extract-i18n/cli.ts b/packages/angular/cli/src/commands/extract-i18n/cli.ts new file mode 100644 index 000000000000..5283204f4e9b --- /dev/null +++ b/packages/angular/cli/src/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 '../../command-builder/architect-command-module'; +import { CommandModuleImplementation } from '../../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/src/commands/generate/cli.ts b/packages/angular/cli/src/commands/generate/cli.ts new file mode 100644 index 000000000000..923c228d7e93 --- /dev/null +++ b/packages/angular/cli/src/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 '../../command-builder/command-module'; +import { + SchematicsCommandArgs, + SchematicsCommandModule, +} from '../../command-builder/schematics-command-module'; +import { Option } from '../../command-builder/utilities/json-schema'; +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/src/commands/generate/generate-impl.ts b/packages/angular/cli/src/commands/generate/generate-impl.ts new file mode 100644 index 000000000000..102d67c248c1 --- /dev/null +++ b/packages/angular/cli/src/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 '../../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/src/commands/lint/cli.ts b/packages/angular/cli/src/commands/lint/cli.ts new file mode 100644 index 000000000000..bd661e7d164b --- /dev/null +++ b/packages/angular/cli/src/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 '../../command-builder/architect-command-module'; +import { CommandModuleImplementation } from '../../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/src/commands/lint/long-description.md similarity index 100% rename from packages/angular/cli/commands/lint-long.md rename to packages/angular/cli/src/commands/lint/long-description.md diff --git a/packages/angular/cli/src/commands/make-this-awesome/cli.ts b/packages/angular/cli/src/commands/make-this-awesome/cli.ts new file mode 100644 index 000000000000..83bcd9df3740 --- /dev/null +++ b/packages/angular/cli/src/commands/make-this-awesome/cli.ts @@ -0,0 +1,39 @@ +/** + * @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, CommandModuleImplementation } from '../../command-builder/command-module'; +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 { + 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/src/commands/new/cli.ts b/packages/angular/cli/src/commands/new/cli.ts new file mode 100644 index 000000000000..68dc79e12ae7 --- /dev/null +++ b/packages/angular/cli/src/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 '../../command-builder/command-module'; +import { + SchematicsCommandArgs, + SchematicsCommandModule, +} from '../../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/src/commands/new/new-impl.ts similarity index 53% rename from packages/angular/cli/commands/new-impl.ts rename to packages/angular/cli/src/commands/new/new-impl.ts index b4869de0f043..b8629d39b4c9 100644 --- a/packages/angular/cli/commands/new-impl.ts +++ b/packages/angular/cli/src/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 '../../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/src/commands/run/cli.ts b/packages/angular/cli/src/commands/run/cli.ts new file mode 100644 index 000000000000..97b06f3f5a0c --- /dev/null +++ b/packages/angular/cli/src/commands/run/cli.ts @@ -0,0 +1,81 @@ +/** + * @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 { join } from 'path'; +import { Argv } from 'yargs'; +import { ArchitectBaseCommandModule } from '../../command-builder/architect-base-command-module'; +import { + CommandModule, + CommandModuleError, + CommandModuleImplementation, + CommandScope, + Options, + OtherOptions, +} from '../../command-builder/command-module'; + +export interface RunCommandArgs { + target: string; +} + +export class RunCommandModule + extends ArchitectBaseCommandModule + 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 this.getArchitectTargetOptions(target); + + return this.addSchemaOptionsToCommand(localYargs, schemaOptions); + } + + 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.'); + } + + return this.runSingleTarget(target, extraOptions); + } + + 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/src/commands/run/long-description.md similarity index 100% rename from packages/angular/cli/commands/run-long.md rename to packages/angular/cli/src/commands/run/long-description.md diff --git a/packages/angular/cli/src/commands/serve/cli.ts b/packages/angular/cli/src/commands/serve/cli.ts new file mode 100644 index 000000000000..537345cc568d --- /dev/null +++ b/packages/angular/cli/src/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 '../../command-builder/architect-command-module'; +import { CommandModuleImplementation } from '../../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/src/commands/test/cli.ts b/packages/angular/cli/src/commands/test/cli.ts new file mode 100644 index 000000000000..fd650fee01c9 --- /dev/null +++ b/packages/angular/cli/src/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 '../../command-builder/architect-command-module'; +import { CommandModuleImplementation } from '../../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/src/commands/test/long-description.md similarity index 100% rename from packages/angular/cli/commands/test-long.md rename to packages/angular/cli/src/commands/test/long-description.md diff --git a/packages/angular/cli/src/commands/update/cli.ts b/packages/angular/cli/src/commands/update/cli.ts new file mode 100644 index 000000000000..ee57fce365da --- /dev/null +++ b/packages/angular/cli/src/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 '../../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/src/commands/update/long-description.md similarity index 100% rename from packages/angular/cli/commands/update-long.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-impl.ts b/packages/angular/cli/src/commands/update/update-impl.ts similarity index 95% rename from packages/angular/cli/commands/update-impl.ts rename to packages/angular/cli/src/commands/update/update-impl.ts index 6f0359af2ef3..25cf5da8f848 100644 --- a/packages/angular/cli/commands/update-impl.ts +++ b/packages/angular/cli/src/commands/update/update-impl.ts @@ -14,33 +14,32 @@ 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 { Options } from '../../command-builder/command-module'; +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 { 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', -); +const UPDATE_SCHEMATIC_COLLECTION = path.join(__dirname, 'schematic/collection.json'); + +type UpdateCommandOptions = Options; /** * Disable CLI version mismatch checks and forces usage of the invoked CLI @@ -53,13 +52,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 +264,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 +296,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 +468,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 +826,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-impl.ts b/packages/angular/cli/src/commands/version/cli.ts similarity index 70% rename from packages/angular/cli/commands/version-impl.ts rename to packages/angular/cli/src/commands/version/cli.ts index 7df7544e3b4a..3523fd243f86 100644 --- a/packages/angular/cli/commands/version-impl.ts +++ b/packages/angular/cli/src/commands/version/cli.ts @@ -8,15 +8,11 @@ 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 { resolve } from 'path'; +import { Argv } from 'yargs'; +import { CommandModule, CommandModuleImplementation } from '../../command-builder/command-module'; +import { colors } from '../../utilities/color'; +import { getPackageManager } from '../../utilities/package-manager'; interface PartialPackageInfo { name: string; @@ -25,36 +21,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 = [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(resolve(__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 +72,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 +121,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 +157,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/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/models/version.ts b/packages/angular/cli/src/utilities/version.ts similarity index 71% rename from packages/angular/cli/models/version.ts rename to packages/angular/cli/src/utilities/version.ts index f24082ff1229..2c9db37d69a9 100644 --- a/packages/angular/cli/models/version.ts +++ b/packages/angular/cli/src/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; } } @@ -26,6 +27,8 @@ export 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/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_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/scripts/json-help.ts b/scripts/json-help.ts new file mode 100644 index 000000000000..a58c656851e9 --- /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/src/command-builder/utilities/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/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(); 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-json.ts b/tests/legacy-cli/e2e/tests/commands/help/help-json.ts index 898adfbe5bc6..3c8786e06ee1 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,58 @@ 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 ', + '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': '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}`); } } 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 "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/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"