From bb550436a476d74705742a8c36f38971b346b903 Mon Sep 17 00:00:00 2001 From: Alan Agius Date: Fri, 11 Mar 2022 18:51:34 +0100 Subject: [PATCH] feat(@angular/cli): add `ng analytics info` command With this change we add a subcommand to `ng analytics`. This command can be used tp display analytics gathering and reporting configuration. Example: ``` $ ng analytics info Global setting: disabled Local setting: enabled Effective status: disabled ``` --- .../angular/cli/src/analytics/analytics.ts | 23 ++++- .../cli/src/command-builder/command-module.ts | 1 + .../cli/src/command-builder/command-runner.ts | 22 +---- .../src/command-builder/utilities/command.ts | 34 +++++++ .../angular/cli/src/commands/analytics/cli.ts | 84 +++++++--------- .../cli/src/commands/analytics/info/cli.ts | 52 ++++++++++ .../commands/analytics/long-description.md | 9 -- .../src/commands/analytics/settings/cli.ts | 95 +++++++++++++++++++ .../e2e/tests/commands/help/help-json.ts | 27 ++++-- 9 files changed, 256 insertions(+), 91 deletions(-) create mode 100644 packages/angular/cli/src/command-builder/utilities/command.ts create mode 100644 packages/angular/cli/src/commands/analytics/info/cli.ts delete mode 100644 packages/angular/cli/src/commands/analytics/long-description.md create mode 100644 packages/angular/cli/src/commands/analytics/settings/cli.ts diff --git a/packages/angular/cli/src/analytics/analytics.ts b/packages/angular/cli/src/analytics/analytics.ts index 95486cfb77..3efa6a5e61 100644 --- a/packages/angular/cli/src/analytics/analytics.ts +++ b/packages/angular/cli/src/analytics/analytics.ts @@ -63,10 +63,11 @@ export function isPackageNameSafeForAnalytics(name: string): boolean { /** * Set analytics settings. This does not work if the user is not inside a project. - * @param level Which config to use. "global" for user-level, and "local" for project-level. + * @param global Which config to use. "global" for user-level, and "local" for project-level. * @param value Either a user ID, true to generate a new User ID, or false to disable analytics. */ -export function setAnalyticsConfig(level: 'global' | 'local', value: string | boolean) { +export function setAnalyticsConfig(global: boolean, value: string | boolean): void { + const level = global ? 'global' : 'local'; analyticsDebug('setting %s level analytics to: %s', level, value); const [config, configPath] = getWorkspaceRaw(level); if (!config || !configPath) { @@ -79,6 +80,8 @@ export function setAnalyticsConfig(level: 'global' | 'local', value: string | bo throw new Error(`Invalid config found at ${configPath}. CLI should be an object.`); } + console.log(`Configured ${level} analytics to "${analyticsConfigValueToHumanFormat(value)}".`); + if (value === true) { value = uuidV4(); } @@ -89,6 +92,18 @@ export function setAnalyticsConfig(level: 'global' | 'local', value: string | bo analyticsDebug('done'); } +export function analyticsConfigValueToHumanFormat(value: unknown): 'on' | 'off' | 'not set' | 'ci' { + if (value === false) { + return 'off'; + } else if (value === 'ci') { + return 'ci'; + } else if (typeof value === 'string' || value === true) { + return 'on'; + } else { + return 'not set'; + } +} + /** * Prompt the user for usage gathering permission. * @param force Whether to ask regardless of whether or not the user is using an interactive shell. @@ -110,7 +125,7 @@ export async function promptGlobalAnalytics(force = false) { }, ]); - setAnalyticsConfig('global', answers.analytics); + setAnalyticsConfig(true, answers.analytics); if (answers.analytics) { console.log(''); @@ -169,7 +184,7 @@ export async function promptProjectAnalytics(force = false): Promise { }, ]); - setAnalyticsConfig('local', answers.analytics); + setAnalyticsConfig(false, answers.analytics); if (answers.analytics) { console.log(''); diff --git a/packages/angular/cli/src/command-builder/command-module.ts b/packages/angular/cli/src/command-builder/command-module.ts index 2ff79b4448..87e2b0c313 100644 --- a/packages/angular/cli/src/command-builder/command-module.ts +++ b/packages/angular/cli/src/command-builder/command-module.ts @@ -43,6 +43,7 @@ export interface CommandContext { positional: string[]; options: { help: boolean; + jsonHelp: boolean; } & Record; }; } diff --git a/packages/angular/cli/src/command-builder/command-runner.ts b/packages/angular/cli/src/command-builder/command-runner.ts index 809323a590..7bdad2f8d3 100644 --- a/packages/angular/cli/src/command-builder/command-runner.ts +++ b/packages/angular/cli/src/command-builder/command-runner.ts @@ -29,6 +29,7 @@ import { VersionCommandModule } from '../commands/version/cli'; import { colors } from '../utilities/color'; import { AngularWorkspace } from '../utilities/config'; import { CommandContext, CommandModuleError, CommandScope } from './command-module'; +import { addCommandModuleToYargs, demandCommandFailureMessage } from './utilities/command'; import { jsonHelpUsage } from './utilities/json-help'; const COMMANDS = [ @@ -75,6 +76,7 @@ export async function runCommand( positional: positional.map((v) => v.toString()), options: { help, + jsonHelp, ...rest, }, }, @@ -90,23 +92,7 @@ export async function runCommand( } } - 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), - }); + localYargs = addCommandModuleToYargs(localYargs, CommandModule, context); } if (jsonHelp) { @@ -142,7 +128,7 @@ export async function runCommand( 'deprecated: %s': colors.yellow('deprecated:') + ' %s', 'Did you mean %s?': 'Unknown command. Did you mean %s?', }) - .demandCommand() + .demandCommand(1, demandCommandFailureMessage) .recommendCommands() .version(false) .showHelpOnFail(false) diff --git a/packages/angular/cli/src/command-builder/utilities/command.ts b/packages/angular/cli/src/command-builder/utilities/command.ts new file mode 100644 index 0000000000..cc55bee254 --- /dev/null +++ b/packages/angular/cli/src/command-builder/utilities/command.ts @@ -0,0 +1,34 @@ +/** + * @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 { CommandContext, CommandModule, CommandModuleImplementation } from '../command-module'; + +export const demandCommandFailureMessage = `You need to specify a command before moving on. Use '--help' to view the available commands.`; + +export function addCommandModuleToYargs< + T, + U extends Partial & { + new (context: CommandContext): Partial & CommandModule; + }, +>(localYargs: Argv, commandModule: U, context: CommandContext): Argv { + const cmd = new commandModule(context); + const describe = context.args.options.jsonHelp ? cmd.fullDescribe : cmd.describe; + + return localYargs.command({ + command: cmd.command, + aliases: cmd.aliases, + 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. + typeof describe === 'object' ? JSON.stringify(describe) : describe, + deprecated: cmd.deprecated, + builder: (argv) => cmd.builder(argv) as Argv, + handler: (args) => cmd.handler(args), + }); +} diff --git a/packages/angular/cli/src/commands/analytics/cli.ts b/packages/angular/cli/src/commands/analytics/cli.ts index e4916342cd..b2e37eb9b2 100644 --- a/packages/angular/cli/src/commands/analytics/cli.ts +++ b/packages/angular/cli/src/commands/analytics/cli.ts @@ -6,61 +6,45 @@ * 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'; + CommandModule, + CommandModuleImplementation, + Options, +} from '../../command-builder/command-module'; +import { + addCommandModuleToYargs, + demandCommandFailureMessage, +} from '../../command-builder/utilities/command'; +import { AnalyticsInfoCommandModule } from './info/cli'; +import { + AnalyticsCIModule, + AnalyticsOffModule, + AnalyticsOnModule, + AnalyticsPromptModule, +} from './settings/cli'; -interface AnalyticsCommandArgs { - setting: 'on' | 'off' | 'prompt' | 'ci' | string; - global: boolean; -} +export class AnalyticsCommandModule extends CommandModule implements CommandModuleImplementation { + command = 'analytics'; + describe = + 'Configures the gathering of Angular CLI usage metrics. See https://angular.io/cli/usage-analytics-gathering'; + longDescriptionPath?: string | undefined; -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 { + const subcommands = [ + AnalyticsCIModule, + AnalyticsInfoCommandModule, + AnalyticsOffModule, + AnalyticsOnModule, + AnalyticsPromptModule, + ].sort(); - builder(localYargs: Argv): Argv { - return localYargs - .positional('setting', { - description: 'Directly enables or disables all usage analytics for the user.', - choices: ['on', 'off', 'ci', 'prompt'], - type: 'string', - demandOption: true, - }) - .option('global', { - description: `Access the global configuration in the caller's home directory.`, - alias: ['g'], - type: 'boolean', - default: false, - }) - .strict(); - } - - async run({ setting, global }: Options): Promise { - const level = global ? 'global' : 'local'; - switch (setting) { - case 'off': - setAnalyticsConfig(level, false); - break; - case 'on': - setAnalyticsConfig(level, true); - break; - case 'ci': - setAnalyticsConfig(level, 'ci'); - break; - case 'prompt': - if (global) { - await promptGlobalAnalytics(true); - } else { - await promptProjectAnalytics(true); - } - break; + for (const module of subcommands) { + localYargs = addCommandModuleToYargs(localYargs, module, this.context); } + + return localYargs.demandCommand(1, demandCommandFailureMessage).strict(); } + + run(_options: Options<{}>): void {} } diff --git a/packages/angular/cli/src/commands/analytics/info/cli.ts b/packages/angular/cli/src/commands/analytics/info/cli.ts new file mode 100644 index 0000000000..a9481a043f --- /dev/null +++ b/packages/angular/cli/src/commands/analytics/info/cli.ts @@ -0,0 +1,52 @@ +/** + * @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 { Argv } from 'yargs'; +import { analyticsConfigValueToHumanFormat, createAnalytics } from '../../../analytics/analytics'; +import { + CommandModule, + CommandModuleImplementation, + Options, +} from '../../../command-builder/command-module'; +import { getWorkspaceRaw } from '../../../utilities/config'; + +export class AnalyticsInfoCommandModule + extends CommandModule + implements CommandModuleImplementation +{ + command = 'info'; + describe = 'Prints analytics gathering and reporting configuration in the console.'; + longDescriptionPath?: string | undefined; + + builder(localYargs: Argv): Argv { + return localYargs.strict(); + } + + async run(_options: Options<{}>): Promise { + const [globalWorkspace] = getWorkspaceRaw('global'); + const [localWorkspace] = getWorkspaceRaw('local'); + const globalSetting = globalWorkspace?.get(['cli', 'analytics']); + const localSetting = localWorkspace?.get(['cli', 'analytics']); + + const effectiveSetting = await createAnalytics( + !!this.context.workspace /** workspace */, + true /** skipPrompt */, + ); + + this.context.logger.info(tags.stripIndents` + Global setting: ${analyticsConfigValueToHumanFormat(globalSetting)} + Local setting: ${ + this.context.workspace + ? analyticsConfigValueToHumanFormat(localSetting) + : 'No local workspace configuration file.' + } + Effective status: ${effectiveSetting ? 'enabled' : 'disabled'} + `); + } +} diff --git a/packages/angular/cli/src/commands/analytics/long-description.md b/packages/angular/cli/src/commands/analytics/long-description.md deleted file mode 100644 index 313edd4680..0000000000 --- a/packages/angular/cli/src/commands/analytics/long-description.md +++ /dev/null @@ -1,9 +0,0 @@ -The value of `setting` 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. - -For further details, see [Gathering an Viewing CLI Usage Analytics](cli/usage-analytics-gathering). diff --git a/packages/angular/cli/src/commands/analytics/settings/cli.ts b/packages/angular/cli/src/commands/analytics/settings/cli.ts new file mode 100644 index 0000000000..2018715b89 --- /dev/null +++ b/packages/angular/cli/src/commands/analytics/settings/cli.ts @@ -0,0 +1,95 @@ +/** + * @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 { + promptGlobalAnalytics, + promptProjectAnalytics, + setAnalyticsConfig, +} from '../../../analytics/analytics'; +import { + CommandModule, + CommandModuleImplementation, + Options, +} from '../../../command-builder/command-module'; + +interface AnalyticsCommandArgs { + global: boolean; +} + +abstract class AnalyticsSettingModule + extends CommandModule + implements CommandModuleImplementation +{ + longDescriptionPath?: string | undefined; + + builder(localYargs: Argv): Argv { + return localYargs + .option('global', { + description: `Configure analytics gathering and reporting globally in the caller's home directory.`, + alias: ['g'], + type: 'boolean', + default: false, + }) + .strict(); + } + + abstract override run({ global }: Options): void; +} + +export class AnalyticsOffModule + extends AnalyticsSettingModule + implements CommandModuleImplementation +{ + command = 'off'; + describe = 'Disables analytics gathering and reporting for the user.'; + + run({ global }: Options): void { + setAnalyticsConfig(global, false); + } +} + +export class AnalyticsOnModule + extends AnalyticsSettingModule + implements CommandModuleImplementation +{ + command = 'on'; + describe = 'Enables analytics gathering and reporting for the user.'; + run({ global }: Options): void { + setAnalyticsConfig(global, true); + } +} + +export class AnalyticsCIModule + extends AnalyticsSettingModule + implements CommandModuleImplementation +{ + command = 'ci'; + describe = + 'Enables analytics and configures reporting for use with Continuous Integration, which uses a common CI user.'; + + run({ global }: Options): void { + setAnalyticsConfig(global, 'ci'); + } +} + +export class AnalyticsPromptModule + extends AnalyticsSettingModule + implements CommandModuleImplementation +{ + command = 'prompt'; + describe = 'Prompts the user to set the analytics gathering status interactively.'; + + async run({ global }: Options): Promise { + if (global) { + await promptGlobalAnalytics(true); + } else { + await promptProjectAnalytics(true); + } + } +} 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 0b357179f4..3903edd120 100644 --- a/tests/legacy-cli/e2e/tests/commands/help/help-json.ts +++ b/tests/legacy-cli/e2e/tests/commands/help/help-json.ts @@ -3,12 +3,13 @@ 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', + 'name': 'config', + 'command': 'ng config [value]', + 'shortDescription': + 'Retrieves or sets Angular configuration values in the angular.json file for the workspace.', + 'longDescriptionRelativePath': '@angular/cli/src/commands/config/long-description.md', 'longDescription': - 'The value of `setting` 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\nFor further details, see [Gathering an Viewing CLI Usage Analytics](cli/usage-analytics-gathering).\n', + 'A workspace has a single CLI configuration file, `angular.json`, at the top level.\nThe `projects` object contains a configuration object for each project in the workspace.\n\nYou can edit the configuration directly in a code editor,\nor indirectly on the command line using this command.\n\nThe configurable property names match command option names,\nexcept that in the configuration file, all names must use camelCase,\nwhile on the command line options can be given dash-case.\n\nFor further details, see [Workspace Configuration](guide/workspace-config).\n\nFor configuration of CLI usage analytics, see [Gathering an Viewing CLI Usage Analytics](cli/usage-analytics-gathering).\n', 'options': [ { 'name': 'global', @@ -23,21 +24,27 @@ export default async function () { 'description': 'Shows a help message for this command in the console.', }, { - 'name': 'setting', + 'name': 'json-path', 'type': 'string', - 'enum': ['on', 'off', 'ci', 'prompt'], - 'description': 'Directly enables or disables all usage analytics for the user.', + '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.', 'positional': 0, }, + { + 'name': 'value', + 'type': 'string', + 'description': 'If provided, a new value for the given configuration key.', + 'positional': 1, + }, ], }); - const { stdout } = await silentNg('analytics', '--help', '--json-help'); + const { stdout } = await silentNg('config', '--help', '--json-help'); const output = JSON.stringify(JSON.parse(stdout.trim())); if (output !== addHelpOutputSnapshot) { throw new Error( - `ng analytics JSON help output didn\'t match snapshot.\n\nExpected "${output}" to be "${addHelpOutputSnapshot}".`, + `ng config JSON help output didn\'t match snapshot.\n\nExpected "${output}" to be "${addHelpOutputSnapshot}".`, ); }