diff --git a/.gitignore b/.gitignore index 4dd985482a1..00198306371 100644 --- a/.gitignore +++ b/.gitignore @@ -80,3 +80,4 @@ dist # Heft .heft +.cache diff --git a/README.md b/README.md index dcf1894f854..3808626256c 100644 --- a/README.md +++ b/README.md @@ -51,11 +51,14 @@ These GitHub repositories provide supplementary resources for Rush Stack: | [/eslint/eslint-plugin](./eslint/eslint-plugin/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Feslint-plugin.svg)](https://badge.fury.io/js/%40rushstack%2Feslint-plugin) | [changelog](./eslint/eslint-plugin/CHANGELOG.md) | [@rushstack/eslint-plugin](https://www.npmjs.com/package/@rushstack/eslint-plugin) | | [/eslint/eslint-plugin-packlets](./eslint/eslint-plugin-packlets/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Feslint-plugin-packlets.svg)](https://badge.fury.io/js/%40rushstack%2Feslint-plugin-packlets) | [changelog](./eslint/eslint-plugin-packlets/CHANGELOG.md) | [@rushstack/eslint-plugin-packlets](https://www.npmjs.com/package/@rushstack/eslint-plugin-packlets) | | [/eslint/eslint-plugin-security](./eslint/eslint-plugin-security/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Feslint-plugin-security.svg)](https://badge.fury.io/js/%40rushstack%2Feslint-plugin-security) | [changelog](./eslint/eslint-plugin-security/CHANGELOG.md) | [@rushstack/eslint-plugin-security](https://www.npmjs.com/package/@rushstack/eslint-plugin-security) | +| [/heft-plugins/heft-api-extractor-plugin](./heft-plugins/heft-api-extractor-plugin/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Fheft-api-extractor-plugin.svg)](https://badge.fury.io/js/%40rushstack%2Fheft-api-extractor-plugin) | [changelog](./heft-plugins/heft-api-extractor-plugin/CHANGELOG.md) | [@rushstack/heft-api-extractor-plugin](https://www.npmjs.com/package/@rushstack/heft-api-extractor-plugin) | | [/heft-plugins/heft-dev-cert-plugin](./heft-plugins/heft-dev-cert-plugin/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Fheft-dev-cert-plugin.svg)](https://badge.fury.io/js/%40rushstack%2Fheft-dev-cert-plugin) | [changelog](./heft-plugins/heft-dev-cert-plugin/CHANGELOG.md) | [@rushstack/heft-dev-cert-plugin](https://www.npmjs.com/package/@rushstack/heft-dev-cert-plugin) | | [/heft-plugins/heft-jest-plugin](./heft-plugins/heft-jest-plugin/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Fheft-jest-plugin.svg)](https://badge.fury.io/js/%40rushstack%2Fheft-jest-plugin) | [changelog](./heft-plugins/heft-jest-plugin/CHANGELOG.md) | [@rushstack/heft-jest-plugin](https://www.npmjs.com/package/@rushstack/heft-jest-plugin) | +| [/heft-plugins/heft-lint-plugin](./heft-plugins/heft-lint-plugin/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Fheft-lint-plugin.svg)](https://badge.fury.io/js/%40rushstack%2Fheft-lint-plugin) | [changelog](./heft-plugins/heft-lint-plugin/CHANGELOG.md) | [@rushstack/heft-lint-plugin](https://www.npmjs.com/package/@rushstack/heft-lint-plugin) | | [/heft-plugins/heft-sass-plugin](./heft-plugins/heft-sass-plugin/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Fheft-sass-plugin.svg)](https://badge.fury.io/js/%40rushstack%2Fheft-sass-plugin) | [changelog](./heft-plugins/heft-sass-plugin/CHANGELOG.md) | [@rushstack/heft-sass-plugin](https://www.npmjs.com/package/@rushstack/heft-sass-plugin) | | [/heft-plugins/heft-serverless-stack-plugin](./heft-plugins/heft-serverless-stack-plugin/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Fheft-serverless-stack-plugin.svg)](https://badge.fury.io/js/%40rushstack%2Fheft-serverless-stack-plugin) | [changelog](./heft-plugins/heft-serverless-stack-plugin/CHANGELOG.md) | [@rushstack/heft-serverless-stack-plugin](https://www.npmjs.com/package/@rushstack/heft-serverless-stack-plugin) | | [/heft-plugins/heft-storybook-plugin](./heft-plugins/heft-storybook-plugin/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Fheft-storybook-plugin.svg)](https://badge.fury.io/js/%40rushstack%2Fheft-storybook-plugin) | [changelog](./heft-plugins/heft-storybook-plugin/CHANGELOG.md) | [@rushstack/heft-storybook-plugin](https://www.npmjs.com/package/@rushstack/heft-storybook-plugin) | +| [/heft-plugins/heft-typescript-plugin](./heft-plugins/heft-typescript-plugin/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Fheft-typescript-plugin.svg)](https://badge.fury.io/js/%40rushstack%2Fheft-typescript-plugin) | [changelog](./heft-plugins/heft-typescript-plugin/CHANGELOG.md) | [@rushstack/heft-typescript-plugin](https://www.npmjs.com/package/@rushstack/heft-typescript-plugin) | | [/heft-plugins/heft-webpack4-plugin](./heft-plugins/heft-webpack4-plugin/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Fheft-webpack4-plugin.svg)](https://badge.fury.io/js/%40rushstack%2Fheft-webpack4-plugin) | [changelog](./heft-plugins/heft-webpack4-plugin/CHANGELOG.md) | [@rushstack/heft-webpack4-plugin](https://www.npmjs.com/package/@rushstack/heft-webpack4-plugin) | | [/heft-plugins/heft-webpack5-plugin](./heft-plugins/heft-webpack5-plugin/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Fheft-webpack5-plugin.svg)](https://badge.fury.io/js/%40rushstack%2Fheft-webpack5-plugin) | [changelog](./heft-plugins/heft-webpack5-plugin/CHANGELOG.md) | [@rushstack/heft-webpack5-plugin](https://www.npmjs.com/package/@rushstack/heft-webpack5-plugin) | | [/libraries/api-extractor-model](./libraries/api-extractor-model/) | [![npm version](https://badge.fury.io/js/%40microsoft%2Fapi-extractor-model.svg)](https://badge.fury.io/js/%40microsoft%2Fapi-extractor-model) | [changelog](./libraries/api-extractor-model/CHANGELOG.md) | [@microsoft/api-extractor-model](https://www.npmjs.com/package/@microsoft/api-extractor-model) | @@ -120,8 +123,6 @@ These GitHub repositories provide supplementary resources for Rush Stack: | [/build-tests/eslint-7-test](./build-tests/eslint-7-test/) | This project contains a build test to validate ESLint 7 compatibility with the latest version of @rushstack/eslint-config (and by extension, the ESLint plugin) | | [/build-tests/hashed-folder-copy-plugin-webpack4-test](./build-tests/hashed-folder-copy-plugin-webpack4-test/) | Building this project exercises @rushstack/hashed-folder-copy-plugin with Webpack 4. | | [/build-tests/hashed-folder-copy-plugin-webpack5-test](./build-tests/hashed-folder-copy-plugin-webpack5-test/) | Building this project exercises @rushstack/hashed-folder-copy-plugin with Webpack 5. NOTE - THIS TEST IS CURRENTLY EXPECTED TO BE BROKEN | -| [/build-tests/heft-action-plugin](./build-tests/heft-action-plugin/) | This project contains a Heft plugin that adds a custom action | -| [/build-tests/heft-action-plugin-test](./build-tests/heft-action-plugin-test/) | This project exercises a custom Heft action | | [/build-tests/heft-copy-files-test](./build-tests/heft-copy-files-test/) | Building this project tests copying files with Heft | | [/build-tests/heft-example-plugin-01](./build-tests/heft-example-plugin-01/) | This is an example heft plugin that exposes hooks for other plugins | | [/build-tests/heft-example-plugin-02](./build-tests/heft-example-plugin-02/) | This is an example heft plugin that taps the hooks exposed from heft-example-plugin-01 | diff --git a/UPGRADING_AND_TESTING.md b/UPGRADING_AND_TESTING.md new file mode 100644 index 00000000000..cbada38b623 --- /dev/null +++ b/UPGRADING_AND_TESTING.md @@ -0,0 +1,155 @@ +# Upgrading to Multi-Phase Heft + +## Update heft.json +The new version of Heft uses an updated schema based on the spec defined in Github issue [#3181](https://github.com/microsoft/rushstack/issues/3181). The new schema can be found [here](https://github.com/D4N14L/rushstack/blob/user/danade/HeftNext2/apps/heft/src/schemas/heft.schema.json). + +There are a few differences between the old format and the new format. The following is an [example legacy "heft.json" file](https://github.com/microsoft/rushstack-samples/blob/main/heft/heft-node-jest-tutorial/config/heft.json): +```json +{ + "$schema": "https://developer.microsoft.com/json-schemas/heft/heft.schema.json", + "eventActions": [ + { + /** + * The kind of built-in operation that should be performed. + * The "deleteGlobs" action deletes files or folders that match the + * specified glob patterns. + */ + "actionKind": "deleteGlobs", + + /** + * The stage of the Heft run during which this action should occur. + * Note that actions specified in heft.json occur at the end of the + * stage of the Heft run. + */ + "heftEvent": "clean", + + /** + * A user-defined tag whose purpose is to allow configs to replace/delete + * handlers that were added by other configs. + */ + "actionId": "defaultClean", + + /** + * Glob patterns to be deleted. The paths are resolved relative to the project folder. + */ + "globsToDelete": ["dist", "lib", "temp"] + } + ], + + /** + * The list of Heft plugins to be loaded. + */ + "heftPlugins": [ + { + /** + * The path to the plugin package. + */ + "plugin": "@rushstack/heft-jest-plugin" + + /** + * An optional object that provides additional settings that may be defined by the plugin. + */ + // "options": { } + } + ] +} +``` +Given this "heft.json" configuration, we can see that only the Jest plugin has been added. However, legacy Heft included a few specific additional plugins by default, making the list of plugins used for this Heft configuration: + - Jest plugin + - TypeScript plugin + - Additional linting performed in the TypeScript plugin + - API Extractor plugin + +Additionally, any plugin added would simply need to be added to the `heftPlugins` array. + +The new version of Heft does not include any default plugins, so **these plugins must all be added individually to "package.json"**. These include the plugins: + - `@rushstack/heft-jest-plugin` + - `@rushstack/heft-typescript-plugin` + - `@rushstack/heft-lint-plugin` + - `@rushstack/heft-api-extractor-plugin` + +Additionally, these plugins must be added in the form of `phases` and `tasks`. The following is an **updated "heft.json" for the same sample project**, using the schema new schema specified above: +```json +{ + "$schema": "https://developer.microsoft.com/json-schemas/heft/heft.schema.json", + + "heftPlugins": [ + /** + * Heft plugins is still around as a field, though it now hosts lifecycle + * plugins, which are plugins that have access to different lifecycle + * hooks, like "onToolStart", "onToolStop", and "recordMetrics". + * These are specified using the same schema as "taskPlugin" below. + * + * The field was not renamed, as there are plans to implement a form of + * simplified plugin specification. This is not yet implemented. + */ + ] + + "phasesByName": { + "build": { + "tasksByName": { + "typescript": { + "taskPlugin": { + "pluginPackage": "@rushstack/heft-typescript-plugin" + /** + * Plugin names can also be specified, if the package contains more than + * one plugin. Ex: + * "pluginName": "TypeScriptPlugin" + */ + } + /** + * Additionally, you can specify events which provide simple functionality, + * but only one taskPlugin or one taskEvent can be specified per-task.\ + * + * "taskEvent": { + * "eventKind": "copyFiles|deleteFiles|runScript" + * "options": { + * ... (see schema) + * } + * } + */ + }, + "lint": { + "taskDependencies": ["typescript"], + "taskPlugin": { + "pluginPackage": "@rushstack/heft-lint-plugin" + } + }, + "api-extractor": { + "taskDependencies": ["typescript"], + "taskPlugin": { + "pluginPackage": "@rushstack/heft-api-extractor-plugin" + } + } + } + }, + "test": { + "phaseDependencies": ["build"], + "tasksByName": { + "jest": { + "taskPlugin": { + "pluginPackage": "@rushstack/heft-jest-plugin", + "pluginName": "JestPlugin" + } + } + } + } + } +} +``` +Immediately, there are a few key differences + - There are now two types of plugins, "lifecycle plugins" and "task plugins" + - Actions that can be performed by Heft are now defined in "heft.json". As such, we must create both the `build` and `test` actions that can be run by executing `heft build` or `heft test` on the CLI. + - Note: `heft build` will perform only the `build` phase, while `heft test` will perform both the `build` and `test` phases, due to the phase dependency. Additionally, the new `heft run` command can be used to run a scoped set of phases. For example, `heft run --only test` will only run the `test` phase + - All internal default plugins are now external, and must be included in this structure manually + - Usage of rigs is encouraged, since the rig brings "heft.json" with it + - Order of execution of plugins is determined by `phaseDependencies` between phases, and `taskDependencies` within phases + - Packages can now contain multiple plugins + - `heftEvents` have become `taskEvents`, and are implemented as a member of a task + +## Testing this Branch On Your Own Project +1. Following the above instructions, create a Heft rig in this branch that can be consumed by your project to perform it's build. All Rushstack plugins have been converted to be compatible with the updated Heft. You can optionally use `@rushstack/heft-node-rig` or `@rushstack/heft-web-rig` for your build, as these have also been converted. +2. Build this branch, including `@rushstack/heft` and the target rig package +3. Symlink the rig package into the appropriate `node_modules` folder for your project +4. Replace the `node_modules/@rushstack/heft` folder for your project with a symlink to the Heft folder in this branch +5. Run Heft against your project by executing `node ./node_modules/@rushstack/heft/lib/start.js build` to run the build phase, as described above \ No newline at end of file diff --git a/apps/api-documenter/package.json b/apps/api-documenter/package.json index 6e60d61a585..925235bec34 100644 --- a/apps/api-documenter/package.json +++ b/apps/api-documenter/package.json @@ -11,8 +11,8 @@ "license": "MIT", "scripts": { "build": "heft build --clean", - "_phase:build": "heft build --clean", - "_phase:test": "heft test --no-build" + "_phase:build": "heft run --only build -- --clean", + "_phase:test": "heft run --only test -- --clean" }, "bin": { "api-documenter": "./bin/api-documenter" diff --git a/apps/heft/package.json b/apps/heft/package.json index 21fd58c875e..5ad471b1bd5 100644 --- a/apps/heft/package.json +++ b/apps/heft/package.json @@ -42,10 +42,7 @@ "argparse": "~1.0.9", "chokidar": "~3.4.0", "fast-glob": "~3.2.4", - "glob": "~7.0.5", "glob-escape": "~0.0.2", - "prettier": "~2.3.0", - "semver": "~7.3.0", "tapable": "1.1.3", "true-case-path": "~2.2.1" }, @@ -55,13 +52,7 @@ "@rushstack/heft": "0.45.14", "@rushstack/heft-node-rig": "1.9.15", "@types/argparse": "1.0.38", - "@types/eslint": "8.2.0", - "@types/glob": "7.1.1", "@types/heft-jest": "1.0.1", - "@types/node": "12.20.24", - "@types/semver": "7.3.5", - "colors": "~1.2.1", - "tslint": "~5.20.1", - "typescript": "~4.6.3" + "@types/node": "12.20.24" } } diff --git a/apps/heft/src/cli/HeftCommandLine.ts b/apps/heft/src/cli/HeftCommandLine.ts deleted file mode 100644 index 3cecbee152c..00000000000 --- a/apps/heft/src/cli/HeftCommandLine.ts +++ /dev/null @@ -1,317 +0,0 @@ -import { - IBaseCommandLineDefinition, - ICommandLineFlagDefinition, - ICommandLineIntegerDefinition, - ICommandLineStringDefinition, - ICommandLineStringListDefinition, - ICommandLineChoiceDefinition, - ICommandLineChoiceListDefinition, - CommandLineAction, - CommandLineParser, - CommandLineFlagParameter, - CommandLineIntegerParameter, - CommandLineStringParameter, - CommandLineStringListParameter, - CommandLineChoiceParameter, - CommandLineChoiceListParameter, - CommandLineParameter -} from '@rushstack/ts-command-line'; -import { ITerminal } from '@rushstack/node-core-library'; - -/** - * @beta - * The base set of utility values provided in every object returned when registering a parameter. - */ -export interface IHeftBaseParameter { - /** - * The value specified on the command line for this parameter. - */ - readonly value?: TValue; - - /** - * If true, then the user has invoked Heft with a command line action that supports this parameter - * (as defined by the {@link IParameterAssociatedActionNames.associatedActionNames} option). - * - * @remarks - * For example, if `build` is one of the associated action names for `--my-integer-parameter`, - * then `actionAssociated` will be true if the user invokes `heft build`. - * - * To test whether the parameter was actually included (e.g. `heft build --my-integer-parameter 123`), - * verify the {@link IHeftBaseParameter.value} property is not `undefined`. - */ - readonly actionAssociated: boolean; - - /** - * The options {@link IHeftRegisterParameterOptions} used to create and register the parameter with - * a Heft command line action. - */ - readonly definition: IHeftRegisterParameterOptions; -} - -/** - * @beta - * The object returned when registering a choice type parameter. - */ -export type IHeftChoiceParameter = IHeftBaseParameter; - -/** - * @beta - * The object returned when registering a choiceList type parameter. - */ -export type IHeftChoiceListParameter = IHeftBaseParameter< - readonly string[], - ICommandLineChoiceListDefinition ->; - -/** - * @beta - * The object returned when registering a flag type parameter. - */ -export type IHeftFlagParameter = IHeftBaseParameter; - -/** - * @beta - * The object returned when registering an integer type parameter. - */ -export type IHeftIntegerParameter = IHeftBaseParameter; - -/** - * @beta - * The object returned when registering a string type parameter. - */ -export type IHeftStringParameter = IHeftBaseParameter; - -/** - * @beta - * The object returned when registering a stringList type parameter. - */ -export type IHeftStringListParameter = IHeftBaseParameter< - readonly string[], - ICommandLineStringListDefinition ->; - -/** - * @beta - * The configuration interface for associating a parameter definition with a Heft - * command line action in {@link IHeftRegisterParameterOptions}. - */ -export interface IParameterAssociatedActionNames { - /** - * A string list of one or more action names to associate the parameter with. - */ - associatedActionNames: string[]; -} - -/** - * @beta - * The options object provided to the command line parser when registering a parameter - * in addition to the action names used to associate the parameter with. - */ -export type IHeftRegisterParameterOptions = - TCommandLineDefinition & IParameterAssociatedActionNames; - -/** - * @beta - * Command line utilities provided for Heft plugin developers. - */ -export class HeftCommandLine { - private readonly _commandLineParser: CommandLineParser; - private readonly _terminal: ITerminal; - - /** - * @internal - */ - public constructor(commandLineParser: CommandLineParser, terminal: ITerminal) { - this._commandLineParser = commandLineParser; - this._terminal = terminal; - } - - /** - * Utility method used by Heft plugins to register a choice type parameter. - */ - public registerChoiceParameter( - options: IHeftRegisterParameterOptions - ): IHeftChoiceParameter { - return this._registerParameter( - options, - (action: CommandLineAction) => action.defineChoiceParameter(options), - (parameter: CommandLineChoiceParameter) => parameter.value - ); - } - - /** - * Utility method used by Heft plugins to register a choiceList type parameter. - */ - public registerChoiceListParameter( - options: IHeftRegisterParameterOptions - ): IHeftChoiceListParameter { - return this._registerParameter( - options, - (action: CommandLineAction) => action.defineChoiceListParameter(options), - (parameter: CommandLineChoiceListParameter) => parameter.values - ); - } - - /** - * Utility method used by Heft plugins to register a flag type parameter. - */ - public registerFlagParameter( - options: IHeftRegisterParameterOptions - ): IHeftFlagParameter { - return this._registerParameter( - options, - (action: CommandLineAction) => action.defineFlagParameter(options), - (parameter: CommandLineFlagParameter) => parameter.value - ); - } - - /** - * Utility method used by Heft plugins to register an integer type parameter. - */ - public registerIntegerParameter( - options: IHeftRegisterParameterOptions - ): IHeftIntegerParameter { - return this._registerParameter( - options, - (action: CommandLineAction) => action.defineIntegerParameter(options), - (parameter: CommandLineIntegerParameter) => parameter.value - ); - } - - /** - * Utility method used by Heft plugins to register a string type parameter. - */ - public registerStringParameter( - options: IHeftRegisterParameterOptions - ): IHeftStringParameter { - return this._registerParameter( - options, - (action: CommandLineAction) => action.defineStringParameter(options), - (parameter: CommandLineStringParameter) => parameter.value - ); - } - - /** - * Utility method used by Heft plugins to register a stringList type parameter. - */ - public registerStringListParameter( - options: IHeftRegisterParameterOptions - ): IHeftStringListParameter { - return this._registerParameter( - options, - (action: CommandLineAction) => action.defineStringListParameter(options), - (parameter: CommandLineStringListParameter) => parameter.values - ); - } - - private _registerParameter< - TCommandLineDefinition extends IBaseCommandLineDefinition, - TCommandLineParameter extends CommandLineParameter, - TValue - >( - options: IHeftRegisterParameterOptions, - defineParameterForAction: (action: CommandLineAction) => TCommandLineParameter, - getParameterValue: (parameter: TCommandLineParameter) => TValue | undefined - ): IHeftBaseParameter { - const actionParameterMap: Map = new Map(); - for (const action of this._getActions(options.associatedActionNames, options.parameterLongName)) { - this._verifyUniqueParameterName(action, options); - const parameter: TCommandLineParameter = defineParameterForAction(action); - actionParameterMap.set(action, parameter); - } - - const parameterObject: IHeftBaseParameter = Object.defineProperties( - {} as IHeftBaseParameter, - { - value: { - get: (): TValue | undefined => { - this._verifyParametersProcessed(options.parameterLongName); - if (this._commandLineParser.selectedAction) { - const parameter: TCommandLineParameter | undefined = actionParameterMap.get( - this._commandLineParser.selectedAction - ); - if (parameter) { - return getParameterValue(parameter); - } - } - - return undefined; - } - }, - - actionAssociated: { - get: (): boolean => { - if (!this._commandLineParser.selectedAction) { - throw new Error('Unable to determine the selected action prior to command line processing'); - } - if (actionParameterMap.get(this._commandLineParser.selectedAction)) { - return true; - } - return false; - } - }, - - definition: { - get: (): IHeftRegisterParameterOptions => { - return { ...options }; - } - } - } - ); - - return parameterObject; - } - - private _getActions(actionNames: string[], parameterLongName: string): CommandLineAction[] { - const actions: CommandLineAction[] = []; - for (const actionName of actionNames) { - const action: CommandLineAction | undefined = this._commandLineParser.tryGetAction(actionName); - if (action) { - if (action.parametersProcessed) { - throw new Error( - `Unable to register parameter "${parameterLongName}" for action "${action.actionName}". ` + - 'Parameters have already been processed.' - ); - } - actions.push(action); - } else { - this._terminal.writeVerboseLine( - `Unable to find action "${actionName}" while registering the "${parameterLongName}" parameter` - ); - } - } - return actions; - } - - private _verifyUniqueParameterName( - action: CommandLineAction, - options: IHeftRegisterParameterOptions - ): void { - const existingParameterLongNames: Set = new Set( - action.parameters.map((parameter) => parameter.longName) - ); - - if (existingParameterLongNames.has(options.parameterLongName)) { - throw new Error(`Attempting to register duplicate parameter long name: ${options.parameterLongName}`); - } - - if (options.parameterShortName) { - const existingParameterShortNames: Set = new Set( - action.parameters.map((parameter) => parameter.shortName) - ); - if (existingParameterShortNames.has(options.parameterShortName)) { - throw new Error( - `Attempting to register duplicate parameter short name: ${options.parameterShortName}` - ); - } - } - } - - private _verifyParametersProcessed(parameterName: string): void { - if (!this._commandLineParser.parametersProcessed) { - throw new Error( - `Unable to access parameter value for "${parameterName}" prior to command line processing` - ); - } - } -} diff --git a/apps/heft/src/cli/HeftCommandLineParser.ts b/apps/heft/src/cli/HeftCommandLineParser.ts index e9be91e0401..6df88f3a8a8 100644 --- a/apps/heft/src/cli/HeftCommandLineParser.ts +++ b/apps/heft/src/cli/HeftCommandLineParser.ts @@ -1,11 +1,8 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import { - CommandLineParser, - CommandLineStringListParameter, - CommandLineFlagParameter -} from '@rushstack/ts-command-line'; +import { ArgumentParser } from 'argparse'; +import { CommandLineParser, CommandLineFlagParameter } from '@rushstack/ts-command-line'; import { ITerminal, Terminal, @@ -15,26 +12,15 @@ import { Path, FileSystem } from '@rushstack/node-core-library'; -import { ArgumentParser } from 'argparse'; -import { SyncHook } from 'tapable'; import { MetricsCollector } from '../metrics/MetricsCollector'; -import { CleanAction } from './actions/CleanAction'; -import { BuildAction } from './actions/BuildAction'; -import { StartAction } from './actions/StartAction'; -import { TestAction } from './actions/TestAction'; -import { PluginManager } from '../pluginFramework/PluginManager'; import { HeftConfiguration } from '../configuration/HeftConfiguration'; -import { IHeftActionBaseOptions, IStages } from './actions/HeftActionBase'; import { InternalHeftSession } from '../pluginFramework/InternalHeftSession'; -import { CleanStage } from '../stages/CleanStage'; -import { BuildStage } from '../stages/BuildStage'; -import { TestStage } from '../stages/TestStage'; import { LoggingManager } from '../pluginFramework/logging/LoggingManager'; -import { ICustomActionOptions, CustomAction } from './actions/CustomAction'; import { Constants } from '../utilities/Constants'; -import { IHeftLifecycle, HeftLifecycleHooks } from '../pluginFramework/HeftLifecycle'; -import { HeftCommandLine } from './HeftCommandLine'; +import { PhaseAction } from './actions/PhaseAction'; +import { RunAction } from './actions/RunAction'; +import type { IHeftActionOptions } from './actions/IHeftAction'; /** * This interfaces specifies values for parameters that must be parsed before the CLI @@ -46,39 +32,34 @@ interface IPreInitializationArgumentValues { } export class HeftCommandLineParser extends CommandLineParser { + public readonly globalTerminal: ITerminal; + private _terminalProvider: ConsoleTerminalProvider; - private _terminal: ITerminal; private _loggingManager: LoggingManager; private _metricsCollector: MetricsCollector; - private _pluginManager: PluginManager; private _heftConfiguration: HeftConfiguration; - private _internalHeftSession: InternalHeftSession; - private _heftLifecycleHook: SyncHook; private _preInitializationArgumentValues: IPreInitializationArgumentValues; - private _unmanagedFlag!: CommandLineFlagParameter; private _debugFlag!: CommandLineFlagParameter; - private _pluginsParameter!: CommandLineStringListParameter; public get isDebug(): boolean { return !!this._preInitializationArgumentValues.debug; } - public get terminal(): ITerminal { - return this._terminal; - } - public constructor() { super({ toolFilename: 'heft', toolDescription: 'Heft is a pluggable build system designed for web projects.' }); + // Pre-initialize with known argument values to determine state of "--debug" this._preInitializationArgumentValues = this._getPreInitializationArgumentValues(); - this._terminalProvider = new ConsoleTerminalProvider(); - this._terminal = new Terminal(this._terminalProvider); + this._terminalProvider = new ConsoleTerminalProvider({ + debugEnabled: this.isDebug + }); + this.globalTerminal = new Terminal(this._terminalProvider); this._metricsCollector = new MetricsCollector(); this._loggingManager = new LoggingManager({ terminalProvider: this._terminalProvider @@ -93,72 +74,13 @@ export class HeftCommandLineParser extends CommandLineParser { cwd: process.cwd(), terminalProvider: this._terminalProvider }); - - const stages: IStages = { - buildStage: new BuildStage(this._heftConfiguration, this._loggingManager), - cleanStage: new CleanStage(this._heftConfiguration, this._loggingManager), - testStage: new TestStage(this._heftConfiguration, this._loggingManager) - }; - - const actionOptions: IHeftActionBaseOptions = { - terminal: this._terminal, - loggingManager: this._loggingManager, - metricsCollector: this._metricsCollector, - heftConfiguration: this._heftConfiguration, - stages - }; - - this._heftLifecycleHook = new SyncHook(['heftLifecycle']); - this._internalHeftSession = new InternalHeftSession({ - getIsDebugMode: () => this.isDebug, - ...stages, - heftLifecycleHook: this._heftLifecycleHook, - loggingManager: this._loggingManager, - metricsCollector: this._metricsCollector, - registerAction: (options: ICustomActionOptions) => { - const action: CustomAction = new CustomAction(options, actionOptions); - this.addAction(action); - }, - commandLine: new HeftCommandLine(this, this._terminal) - }); - - this._pluginManager = new PluginManager({ - terminal: this._terminal, - heftConfiguration: this._heftConfiguration, - internalHeftSession: this._internalHeftSession - }); - - const cleanAction: CleanAction = new CleanAction(actionOptions); - const buildAction: BuildAction = new BuildAction(actionOptions); - const startAction: StartAction = new StartAction(actionOptions); - const testAction: TestAction = new TestAction(actionOptions); - - this.addAction(cleanAction); - this.addAction(buildAction); - this.addAction(startAction); - this.addAction(testAction); } protected onDefineParameters(): void { - this._unmanagedFlag = this.defineFlagParameter({ - parameterLongName: '--unmanaged', - description: - 'Disables the Heft version selector: When Heft is invoked via the shell path, normally it' + - " will examine the project's package.json dependencies and try to use the locally installed version" + - ' of Heft. Specify "--unmanaged" to force the invoked version of Heft to be used. This is useful for' + - ' example if you want to test a different version of Heft.' - }); - this._debugFlag = this.defineFlagParameter({ parameterLongName: Constants.debugParameterLongName, description: 'Show the full call stack if an error occurs while executing the tool' }); - - this._pluginsParameter = this.defineStringListParameter({ - parameterLongName: Constants.pluginParameterLongName, - argumentName: 'PATH', - description: 'Used to specify Heft plugins.' - }); } public async execute(args?: string[]): Promise { @@ -181,17 +103,28 @@ export class HeftCommandLineParser extends CommandLineParser { pathToConvert: rigProfileFolder, baseFolder: this._heftConfiguration.buildFolder }); - this._terminal.writeLine(`Using rig configuration from ${relativeRigFolderPath}`); + this.globalTerminal.writeLine(`Using rig configuration from ${relativeRigFolderPath}`); } - await this._initializePluginsAsync(); - - const heftLifecycle: IHeftLifecycle = { - hooks: new HeftLifecycleHooks() + const internalHeftSession: InternalHeftSession = await InternalHeftSession.initializeAsync({ + heftConfiguration: this._heftConfiguration, + loggingManager: this._loggingManager, + metricsCollector: this._metricsCollector, + getIsDebugMode: () => this.isDebug + }); + + const actionOptions: IHeftActionOptions = { + internalHeftSession: internalHeftSession, + terminal: this.globalTerminal, + loggingManager: this._loggingManager, + metricsCollector: this._metricsCollector, + heftConfiguration: this._heftConfiguration }; - this._heftLifecycleHook.call(heftLifecycle); - await heftLifecycle.hooks.toolStart.promise(); + this.addAction(new RunAction(actionOptions)); + for (const phase of internalHeftSession.phases) { + this.addAction(new PhaseAction({ phase, ...actionOptions })); + } return await super.execute(args); } catch (e) { @@ -204,10 +137,10 @@ export class HeftCommandLineParser extends CommandLineParser { // The .heft/clean.json file is a fairly reliable heuristic for detecting projects created prior to // the big config file redesign with Heft 0.14.0 if (await FileSystem.existsAsync('.heft/clean.json')) { - this._terminal.writeErrorLine( + this.globalTerminal.writeErrorLine( '\nThis project has a ".heft/clean.json" file, which is now obsolete as of Heft 0.14.0.' ); - this._terminal.writeLine( + this.globalTerminal.writeLine( '\nFor instructions for migrating config files, please read UPGRADING.md in the @rushstack/heft package folder.\n' ); throw new AlreadyReportedError(); @@ -217,7 +150,6 @@ export class HeftCommandLineParser extends CommandLineParser { protected async onExecute(): Promise { try { await super.onExecute(); - await this._metricsCollector.flushAndTeardownAsync(); } catch (e) { await this._reportErrorAndSetExitCode(e as Error); } @@ -228,11 +160,11 @@ export class HeftCommandLineParser extends CommandLineParser { private _normalizeCwd(): void { const buildFolder: string = this._heftConfiguration.buildFolder; - this._terminal.writeLine(`Project build folder is "${buildFolder}"`); + this.globalTerminal.writeLine(`Project build folder is "${buildFolder}"`); const currentCwd: string = process.cwd(); if (currentCwd !== buildFolder) { // Update the CWD to the project's build root. Some tools, like Jest, use process.cwd() - this._terminal.writeVerboseLine(`CWD is "${currentCwd}". Normalizing to project build folder.`); + this.globalTerminal.writeVerboseLine(`CWD is "${currentCwd}". Normalizing to project build folder.`); // If `process.cwd()` and `buildFolder` differ only by casing on Windows, the chdir operation will not fix the casing, which is the entire purpose of the exercise. // As such, chdir to a different directory first. That directory needs to exist, so use the parent of the current directory. // This will not work if the current folder is the drive root, but that is a rather exotic case. @@ -244,40 +176,24 @@ export class HeftCommandLineParser extends CommandLineParser { private _getPreInitializationArgumentValues( args: string[] = process.argv ): IPreInitializationArgumentValues { - // This is a rough parsing of the --plugin parameters + // This is a rough parsing of the --debug parameter const parser: ArgumentParser = new ArgumentParser({ addHelp: false }); - parser.addArgument(this._pluginsParameter.longName, { dest: 'plugins', action: 'append' }); parser.addArgument(this._debugFlag.longName, { dest: 'debug', action: 'storeTrue' }); const [result]: IPreInitializationArgumentValues[] = parser.parseKnownArgs(args); return result; } - private async _initializePluginsAsync(): Promise { - this._pluginManager.initializeDefaultPlugins(); - - await this._pluginManager.initializePluginsFromConfigFileAsync(); - - const pluginSpecifiers: string[] = this._preInitializationArgumentValues.plugins || []; - for (const pluginSpecifier of pluginSpecifiers) { - this._pluginManager.initializePlugin(pluginSpecifier); - } - - this._pluginManager.afterInitializeAllPlugins(); - } - private async _reportErrorAndSetExitCode(error: Error): Promise { if (!(error instanceof AlreadyReportedError)) { - this._terminal.writeErrorLine(error.toString()); + this.globalTerminal.writeErrorLine(error.toString()); } if (this.isDebug) { - this._terminal.writeLine(); - this._terminal.writeErrorLine(error.stack!); + this.globalTerminal.writeLine(); + this.globalTerminal.writeErrorLine(error.stack!); } - await this._metricsCollector.flushAndTeardownAsync(); - if (!process.exitCode || process.exitCode > 0) { process.exit(process.exitCode); } else { diff --git a/apps/heft/src/cli/actions/BuildAction.ts b/apps/heft/src/cli/actions/BuildAction.ts deleted file mode 100644 index 927e71ebed5..00000000000 --- a/apps/heft/src/cli/actions/BuildAction.ts +++ /dev/null @@ -1,85 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. -// See LICENSE in the project root for license information. - -import { CommandLineFlagParameter, ICommandLineActionOptions } from '@rushstack/ts-command-line'; - -import { HeftActionBase, IHeftActionBaseOptions } from './HeftActionBase'; -import { CleanStage, ICleanStageOptions } from '../../stages/CleanStage'; -import { Logging } from '../../utilities/Logging'; -import { BuildStage, IBuildStageOptions, IBuildStageStandardParameters } from '../../stages/BuildStage'; - -export class BuildAction extends HeftActionBase { - protected _watchFlag!: CommandLineFlagParameter; - protected _productionFlag!: CommandLineFlagParameter; - protected _liteFlag!: CommandLineFlagParameter; - private _buildStandardParameters!: IBuildStageStandardParameters; - private _cleanFlag!: CommandLineFlagParameter; - - public constructor( - heftActionOptions: IHeftActionBaseOptions, - commandLineActionOptions: ICommandLineActionOptions = { - actionName: 'build', - summary: 'Build the project.', - documentation: '' - } - ) { - super(commandLineActionOptions, heftActionOptions); - } - - public onDefineParameters(): void { - super.onDefineParameters(); - - this._buildStandardParameters = BuildStage.defineStageStandardParameters(this); - this._productionFlag = this._buildStandardParameters.productionFlag; - this._liteFlag = this._buildStandardParameters.liteFlag; - - this._watchFlag = this.defineFlagParameter({ - parameterLongName: '--watch', - parameterShortName: '-w', - description: 'If provided, run tests in watch mode.' - }); - - this._cleanFlag = this.defineFlagParameter({ - parameterLongName: '--clean', - description: 'If specified, clean the package before building.' - }); - } - - protected async actionExecuteAsync(): Promise { - await this.runCleanIfRequestedAsync(); - await this.runBuildAsync(); - } - - protected async runCleanIfRequestedAsync(): Promise { - if (this._cleanFlag.value) { - const cleanStage: CleanStage = this.stages.cleanStage; - const cleanStageOptions: ICleanStageOptions = {}; - await cleanStage.initializeAsync(cleanStageOptions); - - await Logging.runFunctionWithLoggingBoundsAsync( - this.terminal, - 'Clean', - async () => await cleanStage.executeAsync() - ); - } - } - - protected async runBuildAsync(): Promise { - const buildStage: BuildStage = this.stages.buildStage; - const buildStageOptions: IBuildStageOptions = { - ...BuildStage.getOptionsFromStandardParameters(this._buildStandardParameters), - watchMode: this._watchFlag.value, - serveMode: false - }; - await buildStage.initializeAsync(buildStageOptions); - await buildStage.executeAsync(); - } - - protected async afterExecuteAsync(): Promise { - if (this._watchFlag.value) { - await new Promise(() => { - /* never continue if in --watch mode */ - }); - } - } -} diff --git a/apps/heft/src/cli/actions/CleanAction.ts b/apps/heft/src/cli/actions/CleanAction.ts deleted file mode 100644 index e72c1920299..00000000000 --- a/apps/heft/src/cli/actions/CleanAction.ts +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. -// See LICENSE in the project root for license information. - -import { CommandLineFlagParameter } from '@rushstack/ts-command-line'; - -import { HeftActionBase, IHeftActionBaseOptions } from './HeftActionBase'; -import { CleanStage, ICleanStageOptions } from '../../stages/CleanStage'; - -export class CleanAction extends HeftActionBase { - private _deleteCacheFlag!: CommandLineFlagParameter; - - public constructor(options: IHeftActionBaseOptions) { - super( - { - actionName: 'clean', - summary: 'Clean the project', - documentation: '' - }, - options - ); - } - - public onDefineParameters(): void { - super.onDefineParameters(); - - this._deleteCacheFlag = this.defineFlagParameter({ - parameterLongName: '--clear-cache', - description: - "If this flag is provided, the compiler cache will also be cleared. This isn't dangerous, " + - 'but may lead to longer compile times' - }); - } - - protected async actionExecuteAsync(): Promise { - const cleanStage: CleanStage = this.stages.cleanStage; - - const cleanStageOptions: ICleanStageOptions = { - deleteCache: this._deleteCacheFlag.value - }; - await cleanStage.initializeAsync(cleanStageOptions); - - await cleanStage.executeAsync(); - } -} diff --git a/apps/heft/src/cli/actions/CustomAction.ts b/apps/heft/src/cli/actions/CustomAction.ts deleted file mode 100644 index 81f762c875e..00000000000 --- a/apps/heft/src/cli/actions/CustomAction.ts +++ /dev/null @@ -1,164 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. -// See LICENSE in the project root for license information. - -import { - CommandLineFlagParameter, - CommandLineStringParameter, - CommandLineIntegerParameter, - CommandLineStringListParameter -} from '@rushstack/ts-command-line'; - -import { HeftActionBase, IHeftActionBaseOptions } from './HeftActionBase'; - -/** @beta */ -// eslint-disable-next-line @typescript-eslint/no-unused-vars -export interface ICustomActionParameterBase { - kind: 'flag' | 'integer' | 'string' | 'stringList'; // TODO: Add "choice" - - parameterLongName: string; - description: string; -} - -/** @beta */ -export interface ICustomActionParameterFlag extends ICustomActionParameterBase { - kind: 'flag'; -} - -/** @beta */ -export interface ICustomActionParameterInteger extends ICustomActionParameterBase { - kind: 'integer'; -} - -/** @beta */ -export interface ICustomActionParameterString extends ICustomActionParameterBase { - kind: 'string'; -} - -/** @beta */ -export interface ICustomActionParameterStringList extends ICustomActionParameterBase> { - kind: 'stringList'; -} - -/** @beta */ -export type CustomActionParameterType = string | boolean | number | ReadonlyArray | undefined; - -/** @beta */ -export type ICustomActionParameter = TParameter extends boolean - ? ICustomActionParameterFlag - : TParameter extends number - ? ICustomActionParameterInteger - : TParameter extends string - ? ICustomActionParameterString - : TParameter extends ReadonlyArray - ? ICustomActionParameterStringList - : never; - -/** @beta */ -export interface ICustomActionOptions { - actionName: string; - documentation: string; - summary?: string; - - parameters?: { [K in keyof TParameters]: ICustomActionParameter }; - - callback: (parameters: TParameters) => void | Promise; -} - -export class CustomAction extends HeftActionBase { - private _customActionOptions: ICustomActionOptions; - private _parameterValues!: Map CustomActionParameterType>; - - public constructor( - customActionOptions: ICustomActionOptions, - options: IHeftActionBaseOptions - ) { - super( - { - actionName: customActionOptions.actionName, - documentation: customActionOptions.documentation, - summary: customActionOptions.summary || '' - }, - options - ); - - this._customActionOptions = customActionOptions; - } - - public onDefineParameters(): void { - super.onDefineParameters(); - - this._parameterValues = new Map CustomActionParameterType>(); - for (const [callbackValueName, untypedParameterOption] of Object.entries( - this._customActionOptions.parameters || {} - )) { - if (this._parameterValues.has(callbackValueName)) { - throw new Error(`Duplicate callbackValueName: ${callbackValueName}`); - } - - let getParameterValue: () => CustomActionParameterType; - - const parameterOption: ICustomActionParameter = - untypedParameterOption as ICustomActionParameter; - switch (parameterOption.kind) { - case 'flag': { - const parameter: CommandLineFlagParameter = this.defineFlagParameter({ - parameterLongName: parameterOption.parameterLongName, - description: parameterOption.description - }); - getParameterValue = () => parameter.value; - break; - } - - case 'string': { - const parameter: CommandLineStringParameter = this.defineStringParameter({ - parameterLongName: parameterOption.parameterLongName, - description: parameterOption.description, - argumentName: 'VALUE' - }); - getParameterValue = () => parameter.value; - break; - } - - case 'integer': { - const parameter: CommandLineIntegerParameter = this.defineIntegerParameter({ - parameterLongName: parameterOption.parameterLongName, - description: parameterOption.description, - argumentName: 'VALUE' - }); - getParameterValue = () => parameter.value; - break; - } - - case 'stringList': { - const parameter: CommandLineStringListParameter = this.defineStringListParameter({ - parameterLongName: parameterOption.parameterLongName, - description: parameterOption.description, - argumentName: 'VALUE' - }); - getParameterValue = () => parameter.values; - break; - } - - default: { - throw new Error( - // @ts-expect-error All cases are handled above, therefore parameterOption is of type `never` - `Unrecognized parameter kind "${parameterOption.kind}" for parameter "${parameterOption.parameterLongName}` - ); - } - } - - this._parameterValues.set(callbackValueName, getParameterValue); - } - } - - protected async actionExecuteAsync(): Promise { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const parameterValues: Record = {}; - - for (const [callbackName, getParameterValue] of this._parameterValues.entries()) { - parameterValues[callbackName] = getParameterValue(); - } - - await this._customActionOptions.callback(parameterValues as TParameters); - } -} diff --git a/apps/heft/src/cli/actions/HeftActionBase.ts b/apps/heft/src/cli/actions/HeftActionBase.ts deleted file mode 100644 index 76fee89ffa6..00000000000 --- a/apps/heft/src/cli/actions/HeftActionBase.ts +++ /dev/null @@ -1,192 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. -// See LICENSE in the project root for license information. - -import { - CommandLineAction, - CommandLineFlagParameter, - ICommandLineActionOptions, - ICommandLineFlagDefinition, - IBaseCommandLineDefinition, - ICommandLineChoiceDefinition, - CommandLineChoiceParameter, - CommandLineIntegerParameter, - ICommandLineIntegerDefinition, - CommandLineStringParameter, - ICommandLineStringDefinition, - CommandLineStringListParameter, - ICommandLineStringListDefinition -} from '@rushstack/ts-command-line'; -import { - ITerminal, - IPackageJson, - Colors, - ConsoleTerminalProvider, - AlreadyReportedError -} from '@rushstack/node-core-library'; -import { performance } from 'perf_hooks'; - -import { IPerformanceData, MetricsCollector } from '../../metrics/MetricsCollector'; -import { HeftConfiguration } from '../../configuration/HeftConfiguration'; -import { BuildStage } from '../../stages/BuildStage'; -import { CleanStage } from '../../stages/CleanStage'; -import { TestStage } from '../../stages/TestStage'; -import { LoggingManager } from '../../pluginFramework/logging/LoggingManager'; -import { Constants } from '../../utilities/Constants'; - -export interface IStages { - buildStage: BuildStage; - cleanStage: CleanStage; - testStage: TestStage; -} - -export interface IHeftActionBaseOptions { - terminal: ITerminal; - loggingManager: LoggingManager; - metricsCollector: MetricsCollector; - heftConfiguration: HeftConfiguration; - stages: IStages; -} - -export abstract class HeftActionBase extends CommandLineAction { - protected readonly terminal: ITerminal; - protected readonly loggingManager: LoggingManager; - protected readonly metricsCollector: MetricsCollector; - protected readonly heftConfiguration: HeftConfiguration; - protected readonly stages: IStages; - protected verboseFlag!: CommandLineFlagParameter; - - public constructor( - commandLineOptions: ICommandLineActionOptions, - heftActionOptions: IHeftActionBaseOptions - ) { - super(commandLineOptions); - this.terminal = heftActionOptions.terminal; - this.loggingManager = heftActionOptions.loggingManager; - this.metricsCollector = heftActionOptions.metricsCollector; - this.heftConfiguration = heftActionOptions.heftConfiguration; - this.stages = heftActionOptions.stages; - this.setStartTime(); - } - - public onDefineParameters(): void { - this.verboseFlag = this.defineFlagParameter({ - parameterLongName: '--verbose', - parameterShortName: '-v', - description: 'If specified, log information useful for debugging.' - }); - } - - public defineChoiceParameter(options: ICommandLineChoiceDefinition): CommandLineChoiceParameter { - this._validateDefinedParameter(options); - return super.defineChoiceParameter(options); - } - - public defineFlagParameter(options: ICommandLineFlagDefinition): CommandLineFlagParameter { - this._validateDefinedParameter(options); - return super.defineFlagParameter(options); - } - - public defineIntegerParameter(options: ICommandLineIntegerDefinition): CommandLineIntegerParameter { - this._validateDefinedParameter(options); - return super.defineIntegerParameter(options); - } - - public defineStringParameter(options: ICommandLineStringDefinition): CommandLineStringParameter { - this._validateDefinedParameter(options); - return super.defineStringParameter(options); - } - - public defineStringListParameter( - options: ICommandLineStringListDefinition - ): CommandLineStringListParameter { - this._validateDefinedParameter(options); - return super.defineStringListParameter(options); - } - - public setStartTime(): void { - this.metricsCollector.setStartTime(); - } - - public recordMetrics(performanceData?: Partial): void { - this.metricsCollector.record(this.actionName, performanceData, this.getParameterStringMap()); - } - - public async onExecute(): Promise { - this.terminal.writeLine(`Starting ${this.actionName}`); - - if (this.verboseFlag.value) { - if (this.heftConfiguration.terminalProvider instanceof ConsoleTerminalProvider) { - this.heftConfiguration.terminalProvider.verboseEnabled = true; - } - } - - let encounteredError: boolean = false; - try { - await this.actionExecuteAsync(); - await this.afterExecuteAsync(); - } catch (e) { - encounteredError = true; - throw e; - } finally { - const warningStrings: string[] = this.loggingManager.getWarningStrings(); - const errorStrings: string[] = this.loggingManager.getErrorStrings(); - - const encounteredWarnings: boolean = warningStrings.length > 0; - encounteredError = encounteredError || errorStrings.length > 0; - - this.recordMetrics({ encounteredError }); - - this.terminal.writeLine( - Colors.bold( - (encounteredError ? Colors.red : encounteredWarnings ? Colors.yellow : Colors.green)( - `-------------------- Finished (${Math.round(performance.now()) / 1000}s) --------------------` - ) - ) - ); - - if (warningStrings.length > 0) { - this.terminal.writeWarningLine(`Encountered ${warningStrings.length} warnings:`); - for (const warningString of warningStrings) { - this.terminal.writeWarningLine(` ${warningString}`); - } - } - - if (errorStrings.length > 0) { - this.terminal.writeErrorLine(`Encountered ${errorStrings.length} errors:`); - for (const errorString of errorStrings) { - this.terminal.writeErrorLine(` ${errorString}`); - } - } - - const projectPackageJson: IPackageJson = this.heftConfiguration.projectPackageJson; - this.terminal.writeLine( - `Project: ${projectPackageJson.name}`, - Colors.dim(Colors.gray(`@${projectPackageJson.version}`)) - ); - this.terminal.writeLine(`Heft version: ${this.heftConfiguration.heftPackageJson.version}`); - this.terminal.writeLine(`Node version: ${process.version}`); - } - - if (encounteredError) { - throw new AlreadyReportedError(); - } - } - - protected abstract actionExecuteAsync(): Promise; - - /** - * @virtual - */ - protected async afterExecuteAsync(): Promise { - /* no-op by default */ - } - - private _validateDefinedParameter(options: IBaseCommandLineDefinition): void { - if ( - options.parameterLongName === Constants.pluginParameterLongName || - options.parameterLongName === Constants.debugParameterLongName - ) { - throw new Error(`Actions must not register a parameter with longName "${options.parameterLongName}".`); - } - } -} diff --git a/apps/heft/src/cli/actions/IHeftAction.ts b/apps/heft/src/cli/actions/IHeftAction.ts new file mode 100644 index 00000000000..6e401930346 --- /dev/null +++ b/apps/heft/src/cli/actions/IHeftAction.ts @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import type { CommandLineAction } from '@rushstack/ts-command-line'; +import type { ITerminal } from '@rushstack/node-core-library'; + +import type { HeftConfiguration } from '../../configuration/HeftConfiguration'; +import type { MetricsCollector } from '../../metrics/MetricsCollector'; +import type { LoggingManager } from '../../pluginFramework/logging/LoggingManager'; +import type { InternalHeftSession } from '../../pluginFramework/InternalHeftSession'; + +export interface IHeftActionOptions { + internalHeftSession: InternalHeftSession; + terminal: ITerminal; + loggingManager: LoggingManager; + metricsCollector: MetricsCollector; + heftConfiguration: HeftConfiguration; +} + +export interface IHeftAction extends CommandLineAction { + readonly terminal: ITerminal; + readonly loggingManager: LoggingManager; + readonly metricsCollector: MetricsCollector; + readonly heftConfiguration: HeftConfiguration; + readonly verbose: boolean; +} diff --git a/apps/heft/src/cli/actions/PhaseAction.ts b/apps/heft/src/cli/actions/PhaseAction.ts new file mode 100644 index 00000000000..1bb9f9e79e7 --- /dev/null +++ b/apps/heft/src/cli/actions/PhaseAction.ts @@ -0,0 +1,144 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { CommandLineAction, CommandLineFlagParameter } from '@rushstack/ts-command-line'; +import type { ITerminal } from '@rushstack/node-core-library'; + +import { + createOperations, + initializeAction, + executeInstrumentedAsync +} from '../../utilities/HeftActionUtilities'; +import { + IOperationExecutionManagerOptions, + OperationExecutionManager +} from '../../operations/OperationExecutionManager'; +import { Operation } from '../../operations/Operation'; +import { Selection } from '../../utilities/Selection'; +import type { InternalHeftSession } from '../../pluginFramework/InternalHeftSession'; +import type { HeftConfiguration } from '../../configuration/HeftConfiguration'; +import type { LoggingManager } from '../../pluginFramework/logging/LoggingManager'; +import type { MetricsCollector } from '../../metrics/MetricsCollector'; +import type { IHeftAction, IHeftActionOptions } from './IHeftAction'; +import type { HeftPhase } from '../../pluginFramework/HeftPhase'; +import { HeftParameterManager } from '../../configuration/HeftParameterManager'; + +export interface IPhaseActionOptions extends IHeftActionOptions { + phase: HeftPhase; +} + +export class PhaseAction extends CommandLineAction implements IHeftAction { + public readonly terminal: ITerminal; + public readonly loggingManager: LoggingManager; + public readonly metricsCollector: MetricsCollector; + public readonly heftConfiguration: HeftConfiguration; + + private _parameterManager: HeftParameterManager; + private _verboseFlag!: CommandLineFlagParameter; + private _productionFlag!: CommandLineFlagParameter; + private _cleanFlag!: CommandLineFlagParameter; + private _cleanCacheFlag!: CommandLineFlagParameter; + + private _internalSession: InternalHeftSession; + private _selectedPhases: Set; + + public get verbose(): boolean { + return this._verboseFlag.value; + } + + public constructor(options: IPhaseActionOptions) { + super({ + actionName: options.phase.phaseName, + documentation: + `Runs to the ${options.phase.phaseName} phase, including all transitive dependencies.` + + (options.phase.phaseDescription ? ` ${options.phase.phaseDescription}` : ''), + summary: `Runs to the ${options.phase.phaseName} phase, including all transitive dependencies.` + }); + + this.terminal = options.terminal; + this.loggingManager = options.loggingManager; + this.metricsCollector = options.metricsCollector; + this.heftConfiguration = options.heftConfiguration; + + this._parameterManager = new HeftParameterManager(); + this._internalSession = options.internalHeftSession; + + this._selectedPhases = Selection.expandAllDependencies( + [options.phase], + (phase: HeftPhase) => phase.dependencyPhases + ); + + initializeAction(this); + } + + public onDefineParameters(): void { + this._verboseFlag = this.defineFlagParameter({ + parameterLongName: '--verbose', + parameterShortName: '-v', + description: 'If specified, log information useful for debugging.' + }); + this._productionFlag = this.defineFlagParameter({ + parameterLongName: '--production', + description: 'If specified, run Heft in production mode.' + }); + this._cleanFlag = this.defineFlagParameter({ + parameterLongName: '--clean', + description: 'If specified, clean the outputs before running each phase.' + }); + this._cleanCacheFlag = this.defineFlagParameter({ + parameterLongName: '--clean-cache', + description: + 'If specified, clean the cache before running each phase. To use this flag, the ' + + '--clean flag must also be provided.' + }); + + // Add all the parameters for the action + for (const lifecyclePluginDefinition of this._internalSession.lifecycle.pluginDefinitions) { + this._parameterManager.addPluginParameters(lifecyclePluginDefinition); + } + for (const phase of this._selectedPhases) { + for (const task of phase.tasks) { + this._parameterManager.addPluginParameters(task.pluginDefinition); + } + } + + // Finalize and apply to the CommandLineParameterProvider + this._parameterManager.finalizeParameters(this); + } + + protected async onExecute(): Promise { + // Set the parameter manager on the internal session, which is used to provide the selected + // parameters to plugins. Set this in onExecute() instead of onDefineParameters() since + // we now know that this action is being executed, and the session should be populated with + // the executing parameters. + this._internalSession.parameterManager = this._parameterManager; + + // Execute the selected phases + await executeInstrumentedAsync({ + action: this, + executeAsync: async () => { + const operations: Set = createOperations({ + internalHeftSession: this._internalSession, + selectedPhases: this._selectedPhases, + terminal: this.terminal, + production: this._productionFlag.value, + verbose: this._verboseFlag.value, + clean: this._cleanFlag.value, + cleanCache: this._cleanCacheFlag.value + }); + const operationExecutionManagerOptions: IOperationExecutionManagerOptions = { + loggingManager: this.loggingManager, + terminal: this.terminal, + debugMode: this._internalSession.debugMode, + // TODO: Allow for running non-parallelized operations. + parallelism: undefined + }; + const executionManager: OperationExecutionManager = new OperationExecutionManager( + operations, + operationExecutionManagerOptions + ); + await executionManager.executeAsync(); + } + }); + } +} diff --git a/apps/heft/src/cli/actions/RunAction.ts b/apps/heft/src/cli/actions/RunAction.ts new file mode 100644 index 00000000000..c69d1c9047b --- /dev/null +++ b/apps/heft/src/cli/actions/RunAction.ts @@ -0,0 +1,194 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { + CommandLineFlagParameter, + CommandLineParameterProvider, + CommandLineStringListParameter, + ScopedCommandLineAction +} from '@rushstack/ts-command-line'; +import { AlreadyReportedError, InternalError, ITerminal } from '@rushstack/node-core-library'; + +import { + createOperations, + executeInstrumentedAsync, + initializeAction +} from '../../utilities/HeftActionUtilities'; +import { Selection } from '../../utilities/Selection'; +import { + OperationExecutionManager, + type IOperationExecutionManagerOptions +} from '../../operations/OperationExecutionManager'; +import { HeftParameterManager } from '../../configuration/HeftParameterManager'; +import type { HeftConfiguration } from '../../configuration/HeftConfiguration'; +import type { LoggingManager } from '../../pluginFramework/logging/LoggingManager'; +import type { MetricsCollector } from '../../metrics/MetricsCollector'; +import type { InternalHeftSession } from '../../pluginFramework/InternalHeftSession'; +import type { IHeftAction, IHeftActionOptions } from './IHeftAction'; +import type { HeftPhase } from '../../pluginFramework/HeftPhase'; +import type { Operation } from '../../operations/Operation'; + +export interface IRunActionOptions extends IHeftActionOptions {} + +export class RunAction extends ScopedCommandLineAction implements IHeftAction { + public readonly terminal: ITerminal; + public readonly loggingManager: LoggingManager; + public readonly metricsCollector: MetricsCollector; + public readonly heftConfiguration: HeftConfiguration; + + private _parameterManager: HeftParameterManager; + private _verboseFlag!: CommandLineFlagParameter; + private _productionFlag!: CommandLineFlagParameter; + private _cleanFlag!: CommandLineFlagParameter; + private _cleanCacheFlag!: CommandLineFlagParameter; + private _to!: CommandLineStringListParameter; + private _only!: CommandLineStringListParameter; + + private _internalSession: InternalHeftSession; + private _selectedPhases: Set | undefined; + + public get verbose(): boolean { + return this._verboseFlag.value; + } + + public constructor(options: IRunActionOptions) { + super({ + actionName: 'run', + documentation: 'Run a provided selection of Heft phases.', + summary: 'Run a provided selection of Heft phases.' + }); + + this.terminal = options.terminal; + this.loggingManager = options.loggingManager; + this.metricsCollector = options.metricsCollector; + this.heftConfiguration = options.heftConfiguration; + + this._parameterManager = new HeftParameterManager(); + this._internalSession = options.internalHeftSession; + + initializeAction(this); + } + + protected onDefineUnscopedParameters(): void { + this._to = this.defineStringListParameter({ + parameterLongName: '--to', + parameterShortName: '-t', + description: 'The phase to run to, including all transitive dependencies.', + argumentName: 'PHASE', + parameterGroup: ScopedCommandLineAction.ScopingParameterGroup + }); + this._only = this.defineStringListParameter({ + parameterLongName: '--only', + parameterShortName: '-o', + description: 'The phase to run.', + argumentName: 'PHASE', + parameterGroup: ScopedCommandLineAction.ScopingParameterGroup + }); + } + + protected onDefineScopedParameters(scopedParameterProvider: CommandLineParameterProvider): void { + // Define these flags here, since we want them to be available to all scoped actions. + // It also makes it easier to append these flags when using NPM scripts, for example: + // "npm run