From 2f98bf75642ac3b78383929930f2104e0d31c56b Mon Sep 17 00:00:00 2001 From: Daniel Nadeau <3473356+D4N14L@users.noreply.github.com> Date: Tue, 28 Jun 2022 13:56:47 -0700 Subject: [PATCH 1/5] Create @rushstack/heft-api-extractor-plugin and update for use with multi-phase Heft --- .../heft-api-extractor-plugin/.eslintrc.js | 10 +++++ .../heft-api-extractor-plugin/.npmignore | 31 +++++++++++++++ .../heft-api-extractor-plugin/CHANGELOG.json | 0 .../heft-api-extractor-plugin/CHANGELOG.md | 0 .../heft-api-extractor-plugin/LICENSE | 24 ++++++++++++ .../heft-api-extractor-plugin/README.md | 11 ++++++ .../config/api-extractor.json | 16 ++++++++ .../heft-api-extractor-plugin/config/rig.json | 7 ++++ .../heft-plugin.json | 10 +++++ .../heft-api-extractor-plugin/package.json | 39 +++++++++++++++++++ .../src}/ApiExtractorPlugin.ts | 0 .../src}/ApiExtractorRunner.ts | 0 .../heft-api-extractor-plugin/src/index.ts | 10 +++++ .../schemas/api-extractor-task.schema.json | 25 ++++++++++++ .../heft-api-extractor-plugin/tsconfig.json | 7 ++++ 15 files changed, 190 insertions(+) create mode 100644 heft-plugins/heft-api-extractor-plugin/.eslintrc.js create mode 100644 heft-plugins/heft-api-extractor-plugin/.npmignore create mode 100644 heft-plugins/heft-api-extractor-plugin/CHANGELOG.json create mode 100644 heft-plugins/heft-api-extractor-plugin/CHANGELOG.md create mode 100644 heft-plugins/heft-api-extractor-plugin/LICENSE create mode 100644 heft-plugins/heft-api-extractor-plugin/README.md create mode 100644 heft-plugins/heft-api-extractor-plugin/config/api-extractor.json create mode 100644 heft-plugins/heft-api-extractor-plugin/config/rig.json create mode 100644 heft-plugins/heft-api-extractor-plugin/heft-plugin.json create mode 100644 heft-plugins/heft-api-extractor-plugin/package.json rename {apps/heft/src/plugins/ApiExtractorPlugin => heft-plugins/heft-api-extractor-plugin/src}/ApiExtractorPlugin.ts (100%) rename {apps/heft/src/plugins/ApiExtractorPlugin => heft-plugins/heft-api-extractor-plugin/src}/ApiExtractorRunner.ts (100%) create mode 100644 heft-plugins/heft-api-extractor-plugin/src/index.ts create mode 100644 heft-plugins/heft-api-extractor-plugin/src/schemas/api-extractor-task.schema.json create mode 100644 heft-plugins/heft-api-extractor-plugin/tsconfig.json diff --git a/heft-plugins/heft-api-extractor-plugin/.eslintrc.js b/heft-plugins/heft-api-extractor-plugin/.eslintrc.js new file mode 100644 index 0000000000..4c934799d6 --- /dev/null +++ b/heft-plugins/heft-api-extractor-plugin/.eslintrc.js @@ -0,0 +1,10 @@ +// This is a workaround for https://github.com/eslint/eslint/issues/3458 +require('@rushstack/eslint-config/patch/modern-module-resolution'); + +module.exports = { + extends: [ + '@rushstack/eslint-config/profile/node-trusted-tool', + '@rushstack/eslint-config/mixins/friendly-locals' + ], + parserOptions: { tsconfigRootDir: __dirname } +}; diff --git a/heft-plugins/heft-api-extractor-plugin/.npmignore b/heft-plugins/heft-api-extractor-plugin/.npmignore new file mode 100644 index 0000000000..768cd34876 --- /dev/null +++ b/heft-plugins/heft-api-extractor-plugin/.npmignore @@ -0,0 +1,31 @@ +# THIS IS A STANDARD TEMPLATE FOR .npmignore FILES IN THIS REPO. + +# Ignore all files by default, to avoid accidentally publishing unintended files. +* + +# Use negative patterns to bring back the specific things we want to publish. +!/bin/** +!/lib/** +!/lib-*/** +!/dist/** +!ThirdPartyNotice.txt + +# Ignore certain patterns that should not get published. +/dist/*.stats.* +/lib/**/test/ +/lib-*/**/test/ +*.test.js + +# NOTE: These don't need to be specified, because NPM includes them automatically. +# +# package.json +# README (and its variants) +# CHANGELOG (and its variants) +# LICENSE / LICENCE + +#-------------------------------------------- +# DO NOT MODIFY THE TEMPLATE ABOVE THIS LINE +#-------------------------------------------- + +# (Add your project-specific overrides here) +!heft-plugin.json \ No newline at end of file diff --git a/heft-plugins/heft-api-extractor-plugin/CHANGELOG.json b/heft-plugins/heft-api-extractor-plugin/CHANGELOG.json new file mode 100644 index 0000000000..e69de29bb2 diff --git a/heft-plugins/heft-api-extractor-plugin/CHANGELOG.md b/heft-plugins/heft-api-extractor-plugin/CHANGELOG.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/heft-plugins/heft-api-extractor-plugin/LICENSE b/heft-plugins/heft-api-extractor-plugin/LICENSE new file mode 100644 index 0000000000..506de1faf1 --- /dev/null +++ b/heft-plugins/heft-api-extractor-plugin/LICENSE @@ -0,0 +1,24 @@ +@rushstack/heft-dev-cert-plugin + +Copyright (c) Microsoft Corporation. All rights reserved. + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/heft-plugins/heft-api-extractor-plugin/README.md b/heft-plugins/heft-api-extractor-plugin/README.md new file mode 100644 index 0000000000..f7bdf85cca --- /dev/null +++ b/heft-plugins/heft-api-extractor-plugin/README.md @@ -0,0 +1,11 @@ +# @rushstack/heft-api-extractor-plugin + +This is a Heft plugin to run API Extractor. + +## Links + +- [CHANGELOG.md]( + https://github.com/microsoft/rushstack/blob/main/heft-plugins/heft-api-extractor-plugin/CHANGELOG.md) - Find + out what's new in the latest version + +Heft is part of the [Rush Stack](https://rushstack.io/) family of projects. diff --git a/heft-plugins/heft-api-extractor-plugin/config/api-extractor.json b/heft-plugins/heft-api-extractor-plugin/config/api-extractor.json new file mode 100644 index 0000000000..74590d3c4f --- /dev/null +++ b/heft-plugins/heft-api-extractor-plugin/config/api-extractor.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + + "mainEntryPointFilePath": "/lib/index.d.ts", + "apiReport": { + "enabled": true, + "reportFolder": "../../../common/reviews/api" + }, + "docModel": { + "enabled": false + }, + "dtsRollup": { + "enabled": true, + "betaTrimmedFilePath": "/dist/.d.ts" + } +} diff --git a/heft-plugins/heft-api-extractor-plugin/config/rig.json b/heft-plugins/heft-api-extractor-plugin/config/rig.json new file mode 100644 index 0000000000..6ac88a9636 --- /dev/null +++ b/heft-plugins/heft-api-extractor-plugin/config/rig.json @@ -0,0 +1,7 @@ +{ + // The "rig.json" file directs tools to look for their config files in an external package. + // Documentation for this system: https://www.npmjs.com/package/@rushstack/rig-package + "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", + + "rigPackageName": "@rushstack/heft-node-rig" +} diff --git a/heft-plugins/heft-api-extractor-plugin/heft-plugin.json b/heft-plugins/heft-api-extractor-plugin/heft-plugin.json new file mode 100644 index 0000000000..b409d7c960 --- /dev/null +++ b/heft-plugins/heft-api-extractor-plugin/heft-plugin.json @@ -0,0 +1,10 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/heft/heft-plugin.schema.json", + + "taskPlugins": [ + { + "pluginName": "ApiExtractorPlugin", + "entryPoint": "./lib/ApiExtractorPlugin" + } + ] +} diff --git a/heft-plugins/heft-api-extractor-plugin/package.json b/heft-plugins/heft-api-extractor-plugin/package.json new file mode 100644 index 0000000000..d400f611ce --- /dev/null +++ b/heft-plugins/heft-api-extractor-plugin/package.json @@ -0,0 +1,39 @@ +{ + "name": "@rushstack/heft-api-extractor-plugin", + "version": "0.1.0", + "description": "A Heft plugin for using API Extractor. Intended for use with @rushstack/heft-typescript-plugin", + "repository": { + "type": "git", + "url": "https://github.com/microsoft/rushstack.git", + "directory": "heft-plugins/heft-api-extractor-plugin" + }, + "homepage": "https://rushstack.io/pages/heft/overview/", + "main": "lib/index.js", + "types": "dist/heft-api-extractor-plugin.d.ts", + "license": "MIT", + "scripts": { + "build": "node ./node_modules/@rushstack/heft-legacy/bin/heft --unmanaged build --clean", + "start": "node ./node_modules/@rushstack/heft-legacy/bin/heft --unmanaged test --clean --watch", + "_phase:build": "node ./node_modules/@rushstack/heft-legacy/bin/heft --unmanaged build --clean", + "_phase:test": "node ./node_modules/@rushstack/heft-legacy/bin/heft --unmanaged test --no-build" + }, + "peerDependencies": { + "@rushstack/heft": "^0.45.3" + }, + "dependencies": { + "@rushstack/heft-config-file": "workspace:*", + "@rushstack/node-core-library": "workspace:*", + "semver": "~7.3.0" + }, + "devDependencies": { + "@microsoft/api-extractor": "workspace:*", + "@rushstack/eslint-config": "workspace:*", + "@rushstack/heft": "workspace:*", + "@rushstack/heft-typescript-plugin": "workspace:*", + "@rushstack/heft-legacy": "npm:@rushstack/heft@0.45.14", + "@rushstack/heft-node-rig": "1.9.15", + "@types/heft-jest": "1.0.1", + "@types/node": "12.20.24", + "@types/semver": "7.3.5" + } +} diff --git a/apps/heft/src/plugins/ApiExtractorPlugin/ApiExtractorPlugin.ts b/heft-plugins/heft-api-extractor-plugin/src/ApiExtractorPlugin.ts similarity index 100% rename from apps/heft/src/plugins/ApiExtractorPlugin/ApiExtractorPlugin.ts rename to heft-plugins/heft-api-extractor-plugin/src/ApiExtractorPlugin.ts diff --git a/apps/heft/src/plugins/ApiExtractorPlugin/ApiExtractorRunner.ts b/heft-plugins/heft-api-extractor-plugin/src/ApiExtractorRunner.ts similarity index 100% rename from apps/heft/src/plugins/ApiExtractorPlugin/ApiExtractorRunner.ts rename to heft-plugins/heft-api-extractor-plugin/src/ApiExtractorRunner.ts diff --git a/heft-plugins/heft-api-extractor-plugin/src/index.ts b/heft-plugins/heft-api-extractor-plugin/src/index.ts new file mode 100644 index 0000000000..d97aea45e5 --- /dev/null +++ b/heft-plugins/heft-api-extractor-plugin/src/index.ts @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +/** + * A Heft plugin for using API Extractor. + * + * @packageDocumentation + */ + +export {}; diff --git a/heft-plugins/heft-api-extractor-plugin/src/schemas/api-extractor-task.schema.json b/heft-plugins/heft-api-extractor-plugin/src/schemas/api-extractor-task.schema.json new file mode 100644 index 0000000000..ba4768cbbc --- /dev/null +++ b/heft-plugins/heft-api-extractor-plugin/src/schemas/api-extractor-task.schema.json @@ -0,0 +1,25 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "API Extractor Task Configuration", + "description": "Defines additional Heft-specific configuration for the API Extractor task.", + "type": "object", + + "additionalProperties": false, + + "properties": { + "$schema": { + "description": "Part of the JSON Schema standard, this optional keyword declares the URL of the schema that the file conforms to. Editors may download the schema and use it to perform syntax highlighting.", + "type": "string" + }, + + "extends": { + "description": "Optionally specifies another JSON config file that this file extends from. This provides a way for standard settings to be shared across multiple projects.", + "type": "string" + }, + + "useProjectTypescriptVersion": { + "type": "boolean", + "description": "If set to true, use the project's TypeScript compiler version for API Extractor's analysis. API Extractor's included TypeScript compiler can generally correctly analyze typings generated by older compilers, and referencing the project's compiler can cause issues. If issues are encountered with API Extractor's included compiler, set this option to true. This corresponds to API Extractor's --typescript-compiler-folder CLI option and IExtractorInvokeOptions.typescriptCompilerFolder API option. This option defaults to false." + } + } +} diff --git a/heft-plugins/heft-api-extractor-plugin/tsconfig.json b/heft-plugins/heft-api-extractor-plugin/tsconfig.json new file mode 100644 index 0000000000..7512871fdb --- /dev/null +++ b/heft-plugins/heft-api-extractor-plugin/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "./node_modules/@rushstack/heft-node-rig/profiles/default/tsconfig-base.json", + + "compilerOptions": { + "types": ["node"] + } +} From 7fbf2dd2ce1bd0c866d4c861c8a9e9ab4d59a57b Mon Sep 17 00:00:00 2001 From: Daniel Nadeau <3473356+D4N14L@users.noreply.github.com> Date: Tue, 28 Jun 2022 13:57:49 -0700 Subject: [PATCH 2/5] Update for use with multi-phase Heft --- .../src/ApiExtractorPlugin.ts | 289 ++++++++++++------ .../src/ApiExtractorRunner.ts | 135 ++++---- 2 files changed, 260 insertions(+), 164 deletions(-) diff --git a/heft-plugins/heft-api-extractor-plugin/src/ApiExtractorPlugin.ts b/heft-plugins/heft-api-extractor-plugin/src/ApiExtractorPlugin.ts index ccfab7af3d..31e97bd55c 100644 --- a/heft-plugins/heft-api-extractor-plugin/src/ApiExtractorPlugin.ts +++ b/heft-plugins/heft-api-extractor-plugin/src/ApiExtractorPlugin.ts @@ -1,16 +1,21 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import { IHeftPlugin } from '../../pluginFramework/IHeftPlugin'; -import { HeftSession } from '../../pluginFramework/HeftSession'; -import { HeftConfiguration } from '../../configuration/HeftConfiguration'; +import * as path from 'path'; +import type * as TApiExtractor from '@microsoft/api-extractor'; +import type { + IHeftTaskPlugin, + IHeftTaskRunHookOptions, + IHeftTaskSession, + HeftConfiguration, + IHeftTaskCleanHookOptions +} from '@rushstack/heft'; +import { ConfigurationFile } from '@rushstack/heft-config-file'; + import { ApiExtractorRunner } from './ApiExtractorRunner'; -import { IBuildStageContext, IBundleSubstage } from '../../stages/BuildStage'; -import { CoreConfigFiles } from '../../utilities/CoreConfigFiles'; -import { ScopedLogger } from '../../pluginFramework/logging/ScopedLogger'; -import { IToolPackageResolution, ToolPackageResolver } from '../../utilities/ToolPackageResolver'; const PLUGIN_NAME: string = 'ApiExtractorPlugin'; +const PLUGIN_SCHEMA_PATH: string = path.resolve(__dirname, 'schemas', 'api-extractor-task.schema.json'); const CONFIG_FILE_LOCATION: string = './config/api-extractor.json'; export interface IApiExtractorPluginConfiguration { @@ -27,103 +32,215 @@ export interface IApiExtractorPluginConfiguration { useProjectTypescriptVersion?: boolean; } -interface IRunApiExtractorOptions { - heftConfiguration: HeftConfiguration; - buildFolder: string; - debugMode: boolean; - watchMode: boolean; - production: boolean; - apiExtractorJsonFilePath: string; -} - -export class ApiExtractorPlugin implements IHeftPlugin { - public readonly pluginName: string = PLUGIN_NAME; +export default class ApiExtractorPlugin implements IHeftTaskPlugin { + private _apiExtractorPromise: Promise | undefined; + private _apiExtractorConfigurationFilePathPromise: Promise | undefined; + private _apiExtractorTaskConfigurationPromise: + | Promise + | undefined; + + public apply(taskSession: IHeftTaskSession, heftConfiguration: HeftConfiguration): void { + taskSession.hooks.clean.tapPromise(PLUGIN_NAME, async (cleanOptions: IHeftTaskCleanHookOptions) => { + // Load up the configuration, but ignore if target files are missing, since we will be deleting + // them anyway. + const apiExtractorConfiguration: TApiExtractor.ExtractorConfig | undefined = + await this._getApiExtractorConfigurationAsync( + taskSession, + heftConfiguration, + /* ignoreMissingEntryPoint: */ true + ); + if (apiExtractorConfiguration) { + await this._updateCleanOptionsAsync(cleanOptions, apiExtractorConfiguration); + } + }); - private readonly _toolPackageResolver: ToolPackageResolver; + taskSession.hooks.run.tapPromise(PLUGIN_NAME, async (runOptions: IHeftTaskRunHookOptions) => { + const apiExtractor: typeof TApiExtractor | undefined = await this._getApiExtractorAsync( + taskSession, + heftConfiguration + ); + const apiExtractorConfiguration: TApiExtractor.ExtractorConfig | undefined = + await this._getApiExtractorConfigurationAsync(taskSession, heftConfiguration); + if (apiExtractor && apiExtractorConfiguration) { + await this._runApiExtractorAsync( + taskSession, + heftConfiguration, + runOptions, + apiExtractor, + apiExtractorConfiguration + ); + } + }); + } - public constructor(taskPackageResolver: ToolPackageResolver) { - this._toolPackageResolver = taskPackageResolver; + private async _getApiExtractorConfigurationFilePathAsync( + heftConfiguration: HeftConfiguration + ): Promise { + if (!this._apiExtractorConfigurationFilePathPromise) { + this._apiExtractorConfigurationFilePathPromise = + heftConfiguration.rigConfig.tryResolveConfigFilePathAsync(CONFIG_FILE_LOCATION); + } + return await this._apiExtractorConfigurationFilePathPromise; } - public apply(heftSession: HeftSession, heftConfiguration: HeftConfiguration): void { - const { buildFolder } = heftConfiguration; - - heftSession.hooks.build.tap(PLUGIN_NAME, async (build: IBuildStageContext) => { - build.hooks.bundle.tap(PLUGIN_NAME, (bundle: IBundleSubstage) => { - bundle.hooks.run.tapPromise(PLUGIN_NAME, async () => { - // API Extractor provides an ExtractorConfig.tryLoadForFolder() API that will probe for api-extractor.json - // including support for rig.json. However, Heft does not load the @microsoft/api-extractor package at all - // unless it sees a config/api-extractor.json file. Thus we need to do our own lookup here. - const apiExtractorJsonFilePath: string | undefined = - await heftConfiguration.rigConfig.tryResolveConfigFilePathAsync(CONFIG_FILE_LOCATION); - - if (apiExtractorJsonFilePath !== undefined) { - await this._runApiExtractorAsync(heftSession, { - heftConfiguration, - buildFolder, - debugMode: heftSession.debugMode, - watchMode: build.properties.watchMode, - production: build.properties.production, - apiExtractorJsonFilePath: apiExtractorJsonFilePath - }); - } - }); - }); + private async _getApiExtractorConfigurationAsync( + taskSession: IHeftTaskSession, + heftConfiguration: HeftConfiguration, + ignoreMissingEntryPoint?: boolean + ): Promise { + const configObjectFullPath: string | undefined = await this._getApiExtractorConfigurationFilePathAsync( + heftConfiguration + ); + if (!configObjectFullPath) { + return undefined; + } + + const apiExtractor: typeof TApiExtractor = (await this._getApiExtractorAsync( + taskSession, + heftConfiguration + ))!; + const configObject: TApiExtractor.IConfigFile = + apiExtractor.ExtractorConfig.loadFile(configObjectFullPath); + + return apiExtractor.ExtractorConfig.prepare({ + configObject, + configObjectFullPath, + ignoreMissingEntryPoint, + packageJsonFullPath: path.join(heftConfiguration.buildFolder, 'package.json'), + projectFolderLookupToken: heftConfiguration.buildFolder }); } - private async _runApiExtractorAsync( - heftSession: HeftSession, - options: IRunApiExtractorOptions - ): Promise { - const { heftConfiguration, buildFolder, debugMode, watchMode, production } = options; + private async _getApiExtractorAsync( + taskSession: IHeftTaskSession, + heftConfiguration: HeftConfiguration + ): Promise { + if (!this._apiExtractorPromise) { + this._apiExtractorPromise = this._getApiExtractorInnerAsync(taskSession, heftConfiguration); + } + return await this._apiExtractorPromise; + } - const logger: ScopedLogger = heftSession.requestScopedLogger('API Extractor Plugin'); + private async _getApiExtractorInnerAsync( + taskSession: IHeftTaskSession, + heftConfiguration: HeftConfiguration + ): Promise { + // API Extractor provides an ExtractorConfig.tryLoadForFolder() API that will probe for api-extractor.json + // including support for rig.json. However, Heft does not load the @microsoft/api-extractor package at all + // unless it sees a config/api-extractor.json file. Thus we need to do our own lookup here. + const apiExtractorConfigurationFilePath: string | undefined = + await this._getApiExtractorConfigurationFilePathAsync(heftConfiguration); + if (!apiExtractorConfigurationFilePath) { + return undefined; + } - const apiExtractorTaskConfiguration: IApiExtractorPluginConfiguration | undefined = - await CoreConfigFiles.apiExtractorTaskConfigurationLoader.tryLoadConfigurationFileForProjectAsync( - logger.terminal, - heftConfiguration.buildFolder, - heftConfiguration.rigConfig - ); + const apiExtractorPackagePath: string = await heftConfiguration.rigToolResolver.resolvePackageAsync( + '@microsoft/api-extractor', + taskSession.logger.terminal + ); + return await import(apiExtractorPackagePath); + } - if (watchMode) { - logger.terminal.writeWarningLine("API Extractor isn't currently supported in --watch mode."); - return; + private async _updateCleanOptionsAsync( + cleanOptions: IHeftTaskCleanHookOptions, + apiExtractorConfiguration: TApiExtractor.ExtractorConfig + ): Promise { + const extractorGeneratedFilePaths: string[] = []; + if (apiExtractorConfiguration.apiReportEnabled) { + // Keep apiExtractorConfiguration.reportFilePath as-is, since API-Extractor uses the existing + // content to write a warning if the output has changed. + extractorGeneratedFilePaths.push(apiExtractorConfiguration.reportTempFilePath); + } + if (apiExtractorConfiguration.docModelEnabled) { + extractorGeneratedFilePaths.push(apiExtractorConfiguration.apiJsonFilePath); + } + if (apiExtractorConfiguration.rollupEnabled) { + extractorGeneratedFilePaths.push( + apiExtractorConfiguration.alphaTrimmedFilePath, + apiExtractorConfiguration.betaTrimmedFilePath, + apiExtractorConfiguration.publicTrimmedFilePath, + apiExtractorConfiguration.untrimmedFilePath + ); + } + if (apiExtractorConfiguration.tsdocMetadataEnabled) { + extractorGeneratedFilePaths.push(apiExtractorConfiguration.tsdocMetadataFilePath); } - const resolution: IToolPackageResolution | undefined = - await this._toolPackageResolver.resolveToolPackagesAsync(options.heftConfiguration, logger.terminal); - - if (!resolution) { - logger.emitError(new Error('Unable to resolve a compiler package for tsconfig.json')); - return; + for (const generatedFilePath of extractorGeneratedFilePaths) { + if (generatedFilePath) { + cleanOptions.addDeleteOperations({ + sourceFolder: path.dirname(generatedFilePath), + includeGlobs: [path.basename(generatedFilePath)] + }); + } } + } - if (!resolution.apiExtractorPackagePath) { - logger.emitError( - new Error('Unable to resolve the "@microsoft/api-extractor" package for this project') + private async _getApiExtractorTaskConfigurationAsync( + taskSession: IHeftTaskSession, + heftConfiguration: HeftConfiguration + ): Promise { + if (!this._apiExtractorTaskConfigurationPromise) { + this._apiExtractorTaskConfigurationPromise = this._getApiExtractorTaskConfigurationInnerAsync( + taskSession, + heftConfiguration ); - return; } - const apiExtractorRunner: ApiExtractorRunner = new ApiExtractorRunner( - heftConfiguration.terminalProvider, - { - apiExtractorJsonFilePath: options.apiExtractorJsonFilePath, - apiExtractorPackagePath: resolution.apiExtractorPackagePath, - typescriptPackagePath: apiExtractorTaskConfiguration?.useProjectTypescriptVersion - ? resolution.typeScriptPackagePath - : undefined, - buildFolder: buildFolder, - production: production - }, - heftSession + return await this._apiExtractorTaskConfigurationPromise; + } + + private async _getApiExtractorTaskConfigurationInnerAsync( + taskSession: IHeftTaskSession, + heftConfiguration: HeftConfiguration + ): Promise { + const apiExtractorTaskConfigurationFileLoader: ConfigurationFile = + new ConfigurationFile({ + projectRelativeFilePath: 'config/api-extractor-task.json', + jsonSchemaPath: PLUGIN_SCHEMA_PATH + }); + + return await apiExtractorTaskConfigurationFileLoader.tryLoadConfigurationFileForProjectAsync( + taskSession.logger.terminal, + heftConfiguration.buildFolder, + heftConfiguration.rigConfig ); - if (debugMode) { - await apiExtractorRunner.invokeAsync(); - } else { - await apiExtractorRunner.invokeAsSubprocessAsync(); + } + + private async _runApiExtractorAsync( + taskSession: IHeftTaskSession, + heftConfiguration: HeftConfiguration, + runOptions: IHeftTaskRunHookOptions, + apiExtractor: typeof TApiExtractor, + apiExtractorConfiguration: TApiExtractor.ExtractorConfig + ): Promise { + // TODO: Handle watch mode + // if (watchMode) { + // taskSession.logger.terminal.writeWarningLine("API Extractor isn't currently supported in --watch mode."); + // return; + // } + + const apiExtractorTaskConfiguration: IApiExtractorPluginConfiguration | undefined = + await this._getApiExtractorTaskConfigurationAsync(taskSession, heftConfiguration); + + let typescriptPackagePath: string | undefined; + if (apiExtractorTaskConfiguration?.useProjectTypescriptVersion) { + typescriptPackagePath = await heftConfiguration.rigToolResolver.resolvePackageAsync( + 'typescript', + taskSession.logger.terminal + ); } + + const apiExtractorRunner: ApiExtractorRunner = new ApiExtractorRunner({ + apiExtractor, + apiExtractorConfiguration, + typescriptPackagePath, + buildFolder: heftConfiguration.buildFolder, + production: runOptions.production, + scopedLogger: taskSession.logger + }); + + // Run API Extractor + await apiExtractorRunner.invokeAsync(); } } diff --git a/heft-plugins/heft-api-extractor-plugin/src/ApiExtractorRunner.ts b/heft-plugins/heft-api-extractor-plugin/src/ApiExtractorRunner.ts index b006d93905..2945a0db47 100644 --- a/heft-plugins/heft-api-extractor-plugin/src/ApiExtractorRunner.ts +++ b/heft-plugins/heft-api-extractor-plugin/src/ApiExtractorRunner.ts @@ -2,30 +2,25 @@ // See LICENSE in the project root for license information. import * as semver from 'semver'; -import * as path from 'path'; -import { ITerminal, Path } from '@rushstack/node-core-library'; +import type { IScopedLogger } from '@rushstack/heft'; +import { type ITerminal, FileError, InternalError } from '@rushstack/node-core-library'; import type * as TApiExtractor from '@microsoft/api-extractor'; -import { - ISubprocessRunnerBaseConfiguration, - SubprocessRunnerBase -} from '../../utilities/subprocess/SubprocessRunnerBase'; -import { IScopedLogger } from '../../pluginFramework/logging/ScopedLogger'; +export interface IApiExtractorRunnerConfiguration { + /** + * The root folder of the build. + */ + buildFolder: string; -export interface IApiExtractorRunnerConfiguration extends ISubprocessRunnerBaseConfiguration { /** - * The path to the Extractor's config file ("api-extractor.json") - * - * For example, /home/username/code/repo/project/config/api-extractor.json + * The loaded and prepared Extractor config file ("api-extractor.json") */ - apiExtractorJsonFilePath: string; + apiExtractorConfiguration: TApiExtractor.ExtractorConfig; /** - * The path to the @microsoft/api-extractor package - * - * For example, /home/username/code/repo/project/node_modules/@microsoft/api-extractor + * The imported @microsoft/api-extractor package */ - apiExtractorPackagePath: string; + apiExtractor: typeof TApiExtractor; /** * The path to the typescript package @@ -38,25 +33,32 @@ export interface IApiExtractorRunnerConfiguration extends ISubprocessRunnerBaseC * If set to true, run API Extractor in production mode */ production: boolean; -} -export class ApiExtractorRunner extends SubprocessRunnerBase { - private _scopedLogger!: IScopedLogger; - private _terminal!: ITerminal; + /** + * The scoped logger to use for logging + */ + scopedLogger: IScopedLogger; +} - public get filename(): string { - return __filename; +export class ApiExtractorRunner { + private _configuration: IApiExtractorRunnerConfiguration; + private _scopedLogger: IScopedLogger; + private _terminal: ITerminal; + private _apiExtractor: typeof TApiExtractor; + + public constructor(configuration: IApiExtractorRunnerConfiguration) { + this._configuration = configuration; + this._apiExtractor = configuration.apiExtractor; + this._scopedLogger = configuration.scopedLogger; + this._terminal = configuration.scopedLogger.terminal; } public async invokeAsync(): Promise { - this._scopedLogger = await this.requestScopedLoggerAsync('api-extractor'); - this._terminal = this._scopedLogger.terminal; - - const apiExtractor: typeof TApiExtractor = require(this._configuration.apiExtractorPackagePath); - - this._scopedLogger.terminal.writeLine(`Using API Extractor version ${apiExtractor.Extractor.version}`); + this._scopedLogger.terminal.writeLine( + `Using API Extractor version ${this._apiExtractor.Extractor.version}` + ); - const apiExtractorVersion: semver.SemVer | null = semver.parse(apiExtractor.Extractor.version); + const apiExtractorVersion: semver.SemVer | null = semver.parse(this._apiExtractor.Extractor.version); if ( !apiExtractorVersion || apiExtractorVersion.major < 7 || @@ -65,73 +67,48 @@ export class ApiExtractorRunner extends SubprocessRunnerBase { switch (message.logLevel) { - case apiExtractor.ExtractorLogLevel.Error: { - let logMessage: string; + case this._apiExtractor.ExtractorLogLevel.Error: + case this._apiExtractor.ExtractorLogLevel.Warning: { + let errorToEmit: Error | undefined; if (message.sourceFilePath) { - const filePathForLog: string = Path.isUnderOrEqual( - message.sourceFilePath, - this._configuration.buildFolder - ) - ? path.relative(this._configuration.buildFolder, message.sourceFilePath) - : message.sourceFilePath; - logMessage = - `${filePathForLog}:${message.sourceFileLine}:${message.sourceFileColumn} - ` + - `(${message.category}) ${message.text}`; + errorToEmit = new FileError(`(${message.messageId}) ${message.text}`, { + absolutePath: message.sourceFilePath, + projectFolder: this._configuration.buildFolder, + line: message.sourceFileLine, + column: message.sourceFileColumn + }); } else { - logMessage = message.text; + errorToEmit = new Error(message.text); } - this._scopedLogger.emitError(new Error(logMessage)); - break; - } - - case apiExtractor.ExtractorLogLevel.Warning: { - let logMessage: string; - if (message.sourceFilePath) { - const filePathForLog: string = Path.isUnderOrEqual( - message.sourceFilePath, - this._configuration.buildFolder - ) - ? path.relative(this._configuration.buildFolder, message.sourceFilePath) - : message.sourceFilePath; - logMessage = - `${filePathForLog}:${message.sourceFileLine}:${message.sourceFileColumn} - ` + - `(${message.messageId}) ${message.text}`; + if (message.logLevel === this._apiExtractor.ExtractorLogLevel.Error) { + this._scopedLogger.emitError(errorToEmit); + } else if (message.logLevel === this._apiExtractor.ExtractorLogLevel.Warning) { + this._scopedLogger.emitWarning(errorToEmit); } else { - logMessage = message.text; + // Should never happen, but just in case + throw new InternalError(`Unexpected log level: ${message.logLevel}`); } - - this._scopedLogger.emitWarning(new Error(logMessage)); break; } - case apiExtractor.ExtractorLogLevel.Verbose: { + case this._apiExtractor.ExtractorLogLevel.Verbose: { this._terminal.writeVerboseLine(message.text); break; } - case apiExtractor.ExtractorLogLevel.Info: { + case this._apiExtractor.ExtractorLogLevel.Info: { this._terminal.writeLine(message.text); break; } - case apiExtractor.ExtractorLogLevel.None: { + case this._apiExtractor.ExtractorLogLevel.None: { // Ignore messages with ExtractorLogLevel.None break; } @@ -146,16 +123,18 @@ export class ApiExtractorRunner extends SubprocessRunnerBase 0) { - this._terminal.writeErrorLine( - `API Extractor completed with ${errorCount} error${errorCount > 1 ? 's' : ''}` - ); + let message: string = `API Extractor completed with ${errorCount} error${errorCount > 1 ? 's' : ''}`; + if (warningCount > 0) { + message += ` and ${warningCount} warning${warningCount > 1 ? 's' : ''}`; + } + this._terminal.writeErrorLine(message); } else if (warningCount > 0) { this._terminal.writeWarningLine( `API Extractor completed with ${warningCount} warning${warningCount > 1 ? 's' : ''}` From 642626fd3aa356de6b22ca1af1d6274b8fc363c8 Mon Sep 17 00:00:00 2001 From: Daniel Nadeau <3473356+D4N14L@users.noreply.github.com> Date: Tue, 28 Jun 2022 13:58:48 -0700 Subject: [PATCH 3/5] Add missing heft.json entry --- rush.json | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/rush.json b/rush.json index ec8da3b71e..115713056d 100644 --- a/rush.json +++ b/rush.json @@ -812,6 +812,13 @@ }, // "heft-plugins" folder (alphabetical order) + { + "packageName": "@rushstack/heft-api-extractor-plugin", + "projectFolder": "heft-plugins/heft-api-extractor-plugin", + "reviewCategory": "libraries", + "shouldPublish": true, + "cyclicDependencyProjects": ["@rushstack/heft-node-rig"] + }, { "packageName": "@rushstack/heft-dev-cert-plugin", "projectFolder": "heft-plugins/heft-dev-cert-plugin", From f45190ab05133cbd410032b30f25c7532db869e7 Mon Sep 17 00:00:00 2001 From: Daniel Nadeau <3473356+D4N14L@users.noreply.github.com> Date: Tue, 19 Jul 2022 13:57:20 -0700 Subject: [PATCH 4/5] PR feedback --- apps/api-extractor/src/api/ExtractorConfig.ts | 2 +- .../heft-api-extractor-plugin/CHANGELOG.json | 0 .../heft-api-extractor-plugin/CHANGELOG.md | 0 .../heft-api-extractor-plugin/LICENSE | 2 +- .../heft-api-extractor-plugin/README.md | 5 +- .../config/api-extractor.json | 16 -- .../heft-api-extractor-plugin/package.json | 5 +- .../src/ApiExtractorPlugin.ts | 171 ++++++++---------- .../src/ApiExtractorRunner.ts | 29 +-- .../heft-api-extractor-plugin/src/index.ts | 10 - 10 files changed, 88 insertions(+), 152 deletions(-) delete mode 100644 heft-plugins/heft-api-extractor-plugin/CHANGELOG.json delete mode 100644 heft-plugins/heft-api-extractor-plugin/CHANGELOG.md delete mode 100644 heft-plugins/heft-api-extractor-plugin/config/api-extractor.json delete mode 100644 heft-plugins/heft-api-extractor-plugin/src/index.ts diff --git a/apps/api-extractor/src/api/ExtractorConfig.ts b/apps/api-extractor/src/api/ExtractorConfig.ts index 33192aa2e2..c70a480336 100644 --- a/apps/api-extractor/src/api/ExtractorConfig.ts +++ b/apps/api-extractor/src/api/ExtractorConfig.ts @@ -191,7 +191,7 @@ export class ExtractorConfig { /** * The config file name "api-extractor.json". */ - public static readonly FILENAME: string = 'api-extractor.json'; + public static readonly FILENAME: 'api-extractor.json' = 'api-extractor.json'; /** * The full path to `extends/tsdoc-base.json` which contains the standard TSDoc configuration diff --git a/heft-plugins/heft-api-extractor-plugin/CHANGELOG.json b/heft-plugins/heft-api-extractor-plugin/CHANGELOG.json deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/heft-plugins/heft-api-extractor-plugin/CHANGELOG.md b/heft-plugins/heft-api-extractor-plugin/CHANGELOG.md deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/heft-plugins/heft-api-extractor-plugin/LICENSE b/heft-plugins/heft-api-extractor-plugin/LICENSE index 506de1faf1..3df59e864a 100644 --- a/heft-plugins/heft-api-extractor-plugin/LICENSE +++ b/heft-plugins/heft-api-extractor-plugin/LICENSE @@ -1,4 +1,4 @@ -@rushstack/heft-dev-cert-plugin +@rushstack/heft-api-extractor-plugin Copyright (c) Microsoft Corporation. All rights reserved. diff --git a/heft-plugins/heft-api-extractor-plugin/README.md b/heft-plugins/heft-api-extractor-plugin/README.md index f7bdf85cca..960f1e0afa 100644 --- a/heft-plugins/heft-api-extractor-plugin/README.md +++ b/heft-plugins/heft-api-extractor-plugin/README.md @@ -1,11 +1,12 @@ # @rushstack/heft-api-extractor-plugin -This is a Heft plugin to run API Extractor. +This is a Heft plugin for running API Extractor. ## Links - [CHANGELOG.md]( https://github.com/microsoft/rushstack/blob/main/heft-plugins/heft-api-extractor-plugin/CHANGELOG.md) - Find - out what's new in the latest version +out what's new in the latest version +- [@rushstack/heft](https://www.npmjs.com/package/@rushstack/heft) - Heft is a config-driven toolchain that invokes popular tools such as TypeScript, ESLint, Jest, Webpack, and API Extractor. Heft is part of the [Rush Stack](https://rushstack.io/) family of projects. diff --git a/heft-plugins/heft-api-extractor-plugin/config/api-extractor.json b/heft-plugins/heft-api-extractor-plugin/config/api-extractor.json deleted file mode 100644 index 74590d3c4f..0000000000 --- a/heft-plugins/heft-api-extractor-plugin/config/api-extractor.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", - - "mainEntryPointFilePath": "/lib/index.d.ts", - "apiReport": { - "enabled": true, - "reportFolder": "../../../common/reviews/api" - }, - "docModel": { - "enabled": false - }, - "dtsRollup": { - "enabled": true, - "betaTrimmedFilePath": "/dist/.d.ts" - } -} diff --git a/heft-plugins/heft-api-extractor-plugin/package.json b/heft-plugins/heft-api-extractor-plugin/package.json index d400f611ce..268a1a79c3 100644 --- a/heft-plugins/heft-api-extractor-plugin/package.json +++ b/heft-plugins/heft-api-extractor-plugin/package.json @@ -1,15 +1,13 @@ { "name": "@rushstack/heft-api-extractor-plugin", "version": "0.1.0", - "description": "A Heft plugin for using API Extractor. Intended for use with @rushstack/heft-typescript-plugin", + "description": "A Heft plugin for API Extractor", "repository": { "type": "git", "url": "https://github.com/microsoft/rushstack.git", "directory": "heft-plugins/heft-api-extractor-plugin" }, "homepage": "https://rushstack.io/pages/heft/overview/", - "main": "lib/index.js", - "types": "dist/heft-api-extractor-plugin.d.ts", "license": "MIT", "scripts": { "build": "node ./node_modules/@rushstack/heft-legacy/bin/heft --unmanaged build --clean", @@ -29,7 +27,6 @@ "@microsoft/api-extractor": "workspace:*", "@rushstack/eslint-config": "workspace:*", "@rushstack/heft": "workspace:*", - "@rushstack/heft-typescript-plugin": "workspace:*", "@rushstack/heft-legacy": "npm:@rushstack/heft@0.45.14", "@rushstack/heft-node-rig": "1.9.15", "@types/heft-jest": "1.0.1", diff --git a/heft-plugins/heft-api-extractor-plugin/src/ApiExtractorPlugin.ts b/heft-plugins/heft-api-extractor-plugin/src/ApiExtractorPlugin.ts index 31e97bd55c..41d7c68b50 100644 --- a/heft-plugins/heft-api-extractor-plugin/src/ApiExtractorPlugin.ts +++ b/heft-plugins/heft-api-extractor-plugin/src/ApiExtractorPlugin.ts @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import * as path from 'path'; import type * as TApiExtractor from '@microsoft/api-extractor'; import type { IHeftTaskPlugin, @@ -15,10 +14,17 @@ import { ConfigurationFile } from '@rushstack/heft-config-file'; import { ApiExtractorRunner } from './ApiExtractorRunner'; const PLUGIN_NAME: string = 'ApiExtractorPlugin'; -const PLUGIN_SCHEMA_PATH: string = path.resolve(__dirname, 'schemas', 'api-extractor-task.schema.json'); -const CONFIG_FILE_LOCATION: string = './config/api-extractor.json'; +const TASK_CONFIG_SCHEMA_PATH: string = `${__dirname}/schemas/api-extractor-task.schema.json`; +const TASK_CONFIG_RELATIVE_PATH: string = './config/api-extractor-task.json'; +const EXTRACTOR_CONFIG_FILENAME: typeof TApiExtractor.ExtractorConfig.FILENAME = 'api-extractor.json'; +const EXTRACTOR_CONFIG_RELATIVE_PATH: string = `./config/${EXTRACTOR_CONFIG_FILENAME}`; + +export interface IApiExtractorConfigurationResult { + apiExtractorPackage: typeof TApiExtractor; + apiExtractorConfiguration: TApiExtractor.ExtractorConfig; +} -export interface IApiExtractorPluginConfiguration { +export interface IApiExtractorTaskConfiguration { /** * If set to true, use the project's TypeScript compiler version for API Extractor's * analysis. API Extractor's included TypeScript compiler can generally correctly @@ -33,41 +39,37 @@ export interface IApiExtractorPluginConfiguration { } export default class ApiExtractorPlugin implements IHeftTaskPlugin { - private _apiExtractorPromise: Promise | undefined; - private _apiExtractorConfigurationFilePathPromise: Promise | undefined; - private _apiExtractorTaskConfigurationPromise: - | Promise + private _apiExtractor: typeof TApiExtractor | undefined; + private _apiExtractorConfigurationFilePath: string | undefined | null = null; + private _apiExtractorTaskConfigurationFileLoader: + | ConfigurationFile | undefined; public apply(taskSession: IHeftTaskSession, heftConfiguration: HeftConfiguration): void { taskSession.hooks.clean.tapPromise(PLUGIN_NAME, async (cleanOptions: IHeftTaskCleanHookOptions) => { // Load up the configuration, but ignore if target files are missing, since we will be deleting // them anyway. - const apiExtractorConfiguration: TApiExtractor.ExtractorConfig | undefined = + const result: IApiExtractorConfigurationResult | undefined = await this._getApiExtractorConfigurationAsync( taskSession, heftConfiguration, /* ignoreMissingEntryPoint: */ true ); - if (apiExtractorConfiguration) { - await this._updateCleanOptionsAsync(cleanOptions, apiExtractorConfiguration); + if (result) { + this._includeOutputPathsInClean(cleanOptions, result.apiExtractorConfiguration); } }); taskSession.hooks.run.tapPromise(PLUGIN_NAME, async (runOptions: IHeftTaskRunHookOptions) => { - const apiExtractor: typeof TApiExtractor | undefined = await this._getApiExtractorAsync( - taskSession, - heftConfiguration - ); - const apiExtractorConfiguration: TApiExtractor.ExtractorConfig | undefined = + const result: IApiExtractorConfigurationResult | undefined = await this._getApiExtractorConfigurationAsync(taskSession, heftConfiguration); - if (apiExtractor && apiExtractorConfiguration) { + if (result) { await this._runApiExtractorAsync( taskSession, heftConfiguration, runOptions, - apiExtractor, - apiExtractorConfiguration + result.apiExtractorPackage, + result.apiExtractorConfiguration ); } }); @@ -76,75 +78,86 @@ export default class ApiExtractorPlugin implements IHeftTaskPlugin { private async _getApiExtractorConfigurationFilePathAsync( heftConfiguration: HeftConfiguration ): Promise { - if (!this._apiExtractorConfigurationFilePathPromise) { - this._apiExtractorConfigurationFilePathPromise = - heftConfiguration.rigConfig.tryResolveConfigFilePathAsync(CONFIG_FILE_LOCATION); + // When null, we have yet to set the value. When undefined, the file could not be resolved. + if (this._apiExtractorConfigurationFilePath === null) { + this._apiExtractorConfigurationFilePath = + await heftConfiguration.rigConfig.tryResolveConfigFilePathAsync(EXTRACTOR_CONFIG_RELATIVE_PATH); } - return await this._apiExtractorConfigurationFilePathPromise; + return this._apiExtractorConfigurationFilePath; } private async _getApiExtractorConfigurationAsync( taskSession: IHeftTaskSession, heftConfiguration: HeftConfiguration, ignoreMissingEntryPoint?: boolean - ): Promise { - const configObjectFullPath: string | undefined = await this._getApiExtractorConfigurationFilePathAsync( - heftConfiguration - ); - if (!configObjectFullPath) { + ): Promise { + // API Extractor provides an ExtractorConfig.tryLoadForFolder() API that will probe for api-extractor.json + // including support for rig.json. However, Heft does not load the @microsoft/api-extractor package at all + // unless it sees a config/api-extractor.json file. Thus we need to do our own lookup here. + const apiExtractorConfigurationFilePath: string | undefined = + await this._getApiExtractorConfigurationFilePathAsync(heftConfiguration); + if (!apiExtractorConfigurationFilePath) { return undefined; } - const apiExtractor: typeof TApiExtractor = (await this._getApiExtractorAsync( + // Since the config file exists, we can assume that API Extractor is available. Attempt to resolve + // and import the package. If the resolution fails, a helpful error is thrown. + const apiExtractorPackage: typeof TApiExtractor = await this._getApiExtractorPackageAsync( taskSession, heftConfiguration - ))!; - const configObject: TApiExtractor.IConfigFile = - apiExtractor.ExtractorConfig.loadFile(configObjectFullPath); - - return apiExtractor.ExtractorConfig.prepare({ - configObject, - configObjectFullPath, - ignoreMissingEntryPoint, - packageJsonFullPath: path.join(heftConfiguration.buildFolder, 'package.json'), - projectFolderLookupToken: heftConfiguration.buildFolder - }); + ); + const apiExtractorConfigurationObject: TApiExtractor.IConfigFile = + apiExtractorPackage.ExtractorConfig.loadFile(apiExtractorConfigurationFilePath); + + // Load the configuration file. Always load from scratch. + const apiExtractorConfiguration: TApiExtractor.ExtractorConfig = + apiExtractorPackage.ExtractorConfig.prepare({ + ignoreMissingEntryPoint, + configObject: apiExtractorConfigurationObject, + configObjectFullPath: apiExtractorConfigurationFilePath, + packageJsonFullPath: `${heftConfiguration.buildFolder}/package.json`, + projectFolderLookupToken: heftConfiguration.buildFolder + }); + + return { apiExtractorPackage, apiExtractorConfiguration }; } - private async _getApiExtractorAsync( + private async _getApiExtractorPackageAsync( taskSession: IHeftTaskSession, heftConfiguration: HeftConfiguration - ): Promise { - if (!this._apiExtractorPromise) { - this._apiExtractorPromise = this._getApiExtractorInnerAsync(taskSession, heftConfiguration); + ): Promise { + if (!this._apiExtractor) { + const apiExtractorPackagePath: string = await heftConfiguration.rigToolResolver.resolvePackageAsync( + '@microsoft/api-extractor', + taskSession.logger.terminal + ); + this._apiExtractor = (await import(apiExtractorPackagePath)) as typeof TApiExtractor; } - return await this._apiExtractorPromise; + return this._apiExtractor; } - private async _getApiExtractorInnerAsync( + private async _getApiExtractorTaskConfigurationAsync( taskSession: IHeftTaskSession, heftConfiguration: HeftConfiguration - ): Promise { - // API Extractor provides an ExtractorConfig.tryLoadForFolder() API that will probe for api-extractor.json - // including support for rig.json. However, Heft does not load the @microsoft/api-extractor package at all - // unless it sees a config/api-extractor.json file. Thus we need to do our own lookup here. - const apiExtractorConfigurationFilePath: string | undefined = - await this._getApiExtractorConfigurationFilePathAsync(heftConfiguration); - if (!apiExtractorConfigurationFilePath) { - return undefined; + ): Promise { + if (!this._apiExtractorTaskConfigurationFileLoader) { + this._apiExtractorTaskConfigurationFileLoader = new ConfigurationFile({ + projectRelativeFilePath: TASK_CONFIG_RELATIVE_PATH, + jsonSchemaPath: TASK_CONFIG_SCHEMA_PATH + }); } - const apiExtractorPackagePath: string = await heftConfiguration.rigToolResolver.resolvePackageAsync( - '@microsoft/api-extractor', - taskSession.logger.terminal + return await this._apiExtractorTaskConfigurationFileLoader.tryLoadConfigurationFileForProjectAsync( + taskSession.logger.terminal, + heftConfiguration.buildFolder, + heftConfiguration.rigConfig ); - return await import(apiExtractorPackagePath); } - private async _updateCleanOptionsAsync( + private _includeOutputPathsInClean( cleanOptions: IHeftTaskCleanHookOptions, apiExtractorConfiguration: TApiExtractor.ExtractorConfig - ): Promise { + ): void { const extractorGeneratedFilePaths: string[] = []; if (apiExtractorConfiguration.apiReportEnabled) { // Keep apiExtractorConfiguration.reportFilePath as-is, since API-Extractor uses the existing @@ -168,45 +181,11 @@ export default class ApiExtractorPlugin implements IHeftTaskPlugin { for (const generatedFilePath of extractorGeneratedFilePaths) { if (generatedFilePath) { - cleanOptions.addDeleteOperations({ - sourceFolder: path.dirname(generatedFilePath), - includeGlobs: [path.basename(generatedFilePath)] - }); + cleanOptions.addDeleteOperations({ sourcePath: generatedFilePath }); } } } - private async _getApiExtractorTaskConfigurationAsync( - taskSession: IHeftTaskSession, - heftConfiguration: HeftConfiguration - ): Promise { - if (!this._apiExtractorTaskConfigurationPromise) { - this._apiExtractorTaskConfigurationPromise = this._getApiExtractorTaskConfigurationInnerAsync( - taskSession, - heftConfiguration - ); - } - - return await this._apiExtractorTaskConfigurationPromise; - } - - private async _getApiExtractorTaskConfigurationInnerAsync( - taskSession: IHeftTaskSession, - heftConfiguration: HeftConfiguration - ): Promise { - const apiExtractorTaskConfigurationFileLoader: ConfigurationFile = - new ConfigurationFile({ - projectRelativeFilePath: 'config/api-extractor-task.json', - jsonSchemaPath: PLUGIN_SCHEMA_PATH - }); - - return await apiExtractorTaskConfigurationFileLoader.tryLoadConfigurationFileForProjectAsync( - taskSession.logger.terminal, - heftConfiguration.buildFolder, - heftConfiguration.rigConfig - ); - } - private async _runApiExtractorAsync( taskSession: IHeftTaskSession, heftConfiguration: HeftConfiguration, @@ -220,7 +199,7 @@ export default class ApiExtractorPlugin implements IHeftTaskPlugin { // return; // } - const apiExtractorTaskConfiguration: IApiExtractorPluginConfiguration | undefined = + const apiExtractorTaskConfiguration: IApiExtractorTaskConfiguration | undefined = await this._getApiExtractorTaskConfigurationAsync(taskSession, heftConfiguration); let typescriptPackagePath: string | undefined; diff --git a/heft-plugins/heft-api-extractor-plugin/src/ApiExtractorRunner.ts b/heft-plugins/heft-api-extractor-plugin/src/ApiExtractorRunner.ts index 2945a0db47..285d17b5cd 100644 --- a/heft-plugins/heft-api-extractor-plugin/src/ApiExtractorRunner.ts +++ b/heft-plugins/heft-api-extractor-plugin/src/ApiExtractorRunner.ts @@ -41,10 +41,10 @@ export interface IApiExtractorRunnerConfiguration { } export class ApiExtractorRunner { - private _configuration: IApiExtractorRunnerConfiguration; - private _scopedLogger: IScopedLogger; - private _terminal: ITerminal; - private _apiExtractor: typeof TApiExtractor; + private readonly _configuration: IApiExtractorRunnerConfiguration; + private readonly _scopedLogger: IScopedLogger; + private readonly _terminal: ITerminal; + private readonly _apiExtractor: typeof TApiExtractor; public constructor(configuration: IApiExtractorRunnerConfiguration) { this._configuration = configuration; @@ -128,25 +128,10 @@ export class ApiExtractorRunner { extractorOptions ); - const { errorCount, warningCount } = apiExtractorResult; - if (errorCount > 0) { - let message: string = `API Extractor completed with ${errorCount} error${errorCount > 1 ? 's' : ''}`; - if (warningCount > 0) { - message += ` and ${warningCount} warning${warningCount > 1 ? 's' : ''}`; - } - this._terminal.writeErrorLine(message); - } else if (warningCount > 0) { - this._terminal.writeWarningLine( - `API Extractor completed with ${warningCount} warning${warningCount > 1 ? 's' : ''}` - ); - } - if (!apiExtractorResult.succeeded) { - throw new Error('API Extractor failed.'); - } - - if (apiExtractorResult.apiReportChanged && this._configuration.production) { - throw new Error('API Report changed.'); + this._scopedLogger.emitError(new Error('API Extractor failed.')); + } else if (apiExtractorResult.apiReportChanged && this._configuration.production) { + this._scopedLogger.emitError(new Error('API Report changed while in production mode.')); } } } diff --git a/heft-plugins/heft-api-extractor-plugin/src/index.ts b/heft-plugins/heft-api-extractor-plugin/src/index.ts deleted file mode 100644 index d97aea45e5..0000000000 --- a/heft-plugins/heft-api-extractor-plugin/src/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. -// See LICENSE in the project root for license information. - -/** - * A Heft plugin for using API Extractor. - * - * @packageDocumentation - */ - -export {}; From 5781874fe717693c868baf1797bf63916300a07d Mon Sep 17 00:00:00 2001 From: Daniel Nadeau <3473356+D4N14L@users.noreply.github.com> Date: Tue, 19 Jul 2022 16:33:13 -0700 Subject: [PATCH 5/5] PR feedback --- .../src/ApiExtractorPlugin.ts | 30 +++++++++++++++---- .../src/ApiExtractorRunner.ts | 10 +++++-- .../heft-lint-plugin/src/LintPlugin.ts | 4 +-- .../src/TypeScriptPlugin.ts | 2 +- 4 files changed, 35 insertions(+), 11 deletions(-) diff --git a/heft-plugins/heft-api-extractor-plugin/src/ApiExtractorPlugin.ts b/heft-plugins/heft-api-extractor-plugin/src/ApiExtractorPlugin.ts index 41d7c68b50..da91129e19 100644 --- a/heft-plugins/heft-api-extractor-plugin/src/ApiExtractorPlugin.ts +++ b/heft-plugins/heft-api-extractor-plugin/src/ApiExtractorPlugin.ts @@ -13,10 +13,14 @@ import { ConfigurationFile } from '@rushstack/heft-config-file'; import { ApiExtractorRunner } from './ApiExtractorRunner'; +// eslint-disable-next-line @rushstack/no-new-null +const UNINITIALIZED: null = null; + const PLUGIN_NAME: string = 'ApiExtractorPlugin'; const TASK_CONFIG_SCHEMA_PATH: string = `${__dirname}/schemas/api-extractor-task.schema.json`; const TASK_CONFIG_RELATIVE_PATH: string = './config/api-extractor-task.json'; const EXTRACTOR_CONFIG_FILENAME: typeof TApiExtractor.ExtractorConfig.FILENAME = 'api-extractor.json'; +const LEGACY_EXTRACTOR_CONFIG_RELATIVE_PATH: string = `./${EXTRACTOR_CONFIG_FILENAME}`; const EXTRACTOR_CONFIG_RELATIVE_PATH: string = `./config/${EXTRACTOR_CONFIG_FILENAME}`; export interface IApiExtractorConfigurationResult { @@ -40,7 +44,7 @@ export interface IApiExtractorTaskConfiguration { export default class ApiExtractorPlugin implements IHeftTaskPlugin { private _apiExtractor: typeof TApiExtractor | undefined; - private _apiExtractorConfigurationFilePath: string | undefined | null = null; + private _apiExtractorConfigurationFilePath: string | undefined | typeof UNINITIALIZED = UNINITIALIZED; private _apiExtractorTaskConfigurationFileLoader: | ConfigurationFile | undefined; @@ -76,12 +80,26 @@ export default class ApiExtractorPlugin implements IHeftTaskPlugin { } private async _getApiExtractorConfigurationFilePathAsync( + taskSession: IHeftTaskSession, heftConfiguration: HeftConfiguration ): Promise { - // When null, we have yet to set the value. When undefined, the file could not be resolved. - if (this._apiExtractorConfigurationFilePath === null) { + if (this._apiExtractorConfigurationFilePath === UNINITIALIZED) { this._apiExtractorConfigurationFilePath = await heftConfiguration.rigConfig.tryResolveConfigFilePathAsync(EXTRACTOR_CONFIG_RELATIVE_PATH); + if (this._apiExtractorConfigurationFilePath === undefined) { + this._apiExtractorConfigurationFilePath = + await heftConfiguration.rigConfig.tryResolveConfigFilePathAsync( + LEGACY_EXTRACTOR_CONFIG_RELATIVE_PATH + ); + if (this._apiExtractorConfigurationFilePath !== undefined) { + taskSession.logger.emitWarning( + new Error( + `The "${LEGACY_EXTRACTOR_CONFIG_RELATIVE_PATH}" configuration file path is not supported ` + + `in Heft. Please move it to "${EXTRACTOR_CONFIG_RELATIVE_PATH}".` + ) + ); + } + } } return this._apiExtractorConfigurationFilePath; } @@ -95,7 +113,7 @@ export default class ApiExtractorPlugin implements IHeftTaskPlugin { // including support for rig.json. However, Heft does not load the @microsoft/api-extractor package at all // unless it sees a config/api-extractor.json file. Thus we need to do our own lookup here. const apiExtractorConfigurationFilePath: string | undefined = - await this._getApiExtractorConfigurationFilePathAsync(heftConfiguration); + await this._getApiExtractorConfigurationFilePathAsync(taskSession, heftConfiguration); if (!apiExtractorConfigurationFilePath) { return undefined; } @@ -127,7 +145,7 @@ export default class ApiExtractorPlugin implements IHeftTaskPlugin { heftConfiguration: HeftConfiguration ): Promise { if (!this._apiExtractor) { - const apiExtractorPackagePath: string = await heftConfiguration.rigToolResolver.resolvePackageAsync( + const apiExtractorPackagePath: string = await heftConfiguration.rigPackageResolver.resolvePackageAsync( '@microsoft/api-extractor', taskSession.logger.terminal ); @@ -204,7 +222,7 @@ export default class ApiExtractorPlugin implements IHeftTaskPlugin { let typescriptPackagePath: string | undefined; if (apiExtractorTaskConfiguration?.useProjectTypescriptVersion) { - typescriptPackagePath = await heftConfiguration.rigToolResolver.resolvePackageAsync( + typescriptPackagePath = await heftConfiguration.rigPackageResolver.resolvePackageAsync( 'typescript', taskSession.logger.terminal ); diff --git a/heft-plugins/heft-api-extractor-plugin/src/ApiExtractorRunner.ts b/heft-plugins/heft-api-extractor-plugin/src/ApiExtractorRunner.ts index 285d17b5cd..eed414d611 100644 --- a/heft-plugins/heft-api-extractor-plugin/src/ApiExtractorRunner.ts +++ b/heft-plugins/heft-api-extractor-plugin/src/ApiExtractorRunner.ts @@ -40,6 +40,9 @@ export interface IApiExtractorRunnerConfiguration { scopedLogger: IScopedLogger; } +const MIN_SUPPORTED_MAJOR_VERSION: number = 7; +const MIN_SUPPORTED_MINOR_VERSION: number = 10; + export class ApiExtractorRunner { private readonly _configuration: IApiExtractorRunnerConfiguration; private readonly _scopedLogger: IScopedLogger; @@ -61,8 +64,11 @@ export class ApiExtractorRunner { const apiExtractorVersion: semver.SemVer | null = semver.parse(this._apiExtractor.Extractor.version); if ( !apiExtractorVersion || - apiExtractorVersion.major < 7 || - (apiExtractorVersion.major === 7 && apiExtractorVersion.minor < 10) + apiExtractorVersion.major < MIN_SUPPORTED_MAJOR_VERSION || + ( + apiExtractorVersion.major === MIN_SUPPORTED_MAJOR_VERSION && + apiExtractorVersion.minor < MIN_SUPPORTED_MINOR_VERSION + ) ) { this._scopedLogger.emitWarning(new Error(`Heft requires API Extractor version 7.10.0 or newer`)); } diff --git a/heft-plugins/heft-lint-plugin/src/LintPlugin.ts b/heft-plugins/heft-lint-plugin/src/LintPlugin.ts index aa94cffb01..6aa514aeab 100644 --- a/heft-plugins/heft-lint-plugin/src/LintPlugin.ts +++ b/heft-plugins/heft-lint-plugin/src/LintPlugin.ts @@ -75,7 +75,7 @@ export default class LintPlugin implements IHeftTaskPlugin { // Locate the tslint linter if enabled this._tslintConfigFilePath = await this._resolveTslintConfigFilePathAsync(heftConfiguration); if (this._tslintConfigFilePath) { - this._tslintToolPath = await heftConfiguration.rigToolResolver.resolvePackageAsync( + this._tslintToolPath = await heftConfiguration.rigPackageResolver.resolvePackageAsync( 'tslint', logger.terminal ); @@ -84,7 +84,7 @@ export default class LintPlugin implements IHeftTaskPlugin { // Locate the eslint linter if enabled this._eslintConfigFilePath = await this._resolveEslintConfigFilePathAsync(heftConfiguration); if (this._eslintConfigFilePath) { - this._eslintToolPath = await heftConfiguration.rigToolResolver.resolvePackageAsync( + this._eslintToolPath = await heftConfiguration.rigPackageResolver.resolvePackageAsync( 'eslint', logger.terminal ); diff --git a/heft-plugins/heft-typescript-plugin/src/TypeScriptPlugin.ts b/heft-plugins/heft-typescript-plugin/src/TypeScriptPlugin.ts index 6f465ba0a8..5b060f0653 100644 --- a/heft-plugins/heft-typescript-plugin/src/TypeScriptPlugin.ts +++ b/heft-plugins/heft-typescript-plugin/src/TypeScriptPlugin.ts @@ -325,7 +325,7 @@ export default class TypeScriptPlugin implements IHeftTaskPlugin { ): Promise { const terminal: ITerminal = taskSession.logger.terminal; - const typeScriptToolPath: string = await heftConfiguration.rigToolResolver.resolvePackageAsync( + const typeScriptToolPath: string = await heftConfiguration.rigPackageResolver.resolvePackageAsync( 'typescript', terminal );