UNPKG

just-scripts

Version:
241 lines (210 loc) 8.78 kB
import { logger, TaskFunction } from 'just-task'; import * as fs from 'fs-extra'; import * as path from 'path'; import { tryRequire } from '../tryRequire'; import * as ApiExtractorTypes from './apiExtractorTypes'; /* eslint-disable @typescript-eslint/no-non-null-assertion */ /** * Options from `IExtractorConfigOptions` plus additional options specific to the task. */ export interface ApiExtractorOptions extends ApiExtractorTypes.IExtractorInvokeOptions { /** The project folder to be used for reporting output paths. */ projectFolder?: string; /** The config file path */ configJsonFilePath?: string; /** * @deprecated Update API Extractor and use option `newlineKind: 'os'`. */ fixNewlines?: boolean; /** * Callback after api-extractor is invoked. * @param result - Result of invoking api-extractor. Actual type is `ExtractorResult` from `@microsoft/api-extractor`. * @param extractorOptions - Options with which api-extractor was invoked. Actual type is `IExtractorInvokeOptions`. */ onResult?: (result: any, extractorOptions: any) => void; /** * Callback after the config file is loaded. * Provides the opportunity to modify the config before running API Extractor. */ onConfigLoaded?: (config: ApiExtractorTypes.IConfigFile) => void; } interface ApiExtractorContext { /** Original options */ options: ApiExtractorOptions; /** Just the options to pass to api-extractor */ extractorOptions: ApiExtractorTypes.IExtractorInvokeOptions; /** Loaded config file */ config: ApiExtractorTypes.ExtractorConfig; /** Actual api-extractor module */ apiExtractorModule: typeof ApiExtractorTypes; } type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>; export function apiExtractorVerifyTask(options: ApiExtractorOptions): TaskFunction; /** @deprecated Use object param version */ export function apiExtractorVerifyTask( configJsonFilePath: string, extractorOptions: Omit<ApiExtractorOptions, 'configJsonFilePath'>, ): TaskFunction; export function apiExtractorVerifyTask( configJsonFilePathOrOption: string | ApiExtractorOptions = {}, extractorOptions: Omit<ApiExtractorOptions, 'configJsonFilePath'> = {}, ): TaskFunction { const options = typeof configJsonFilePathOrOption === 'string' ? { ...extractorOptions, configJsonFilePath: configJsonFilePathOrOption } : { ...configJsonFilePathOrOption }; return function apiExtractorVerify() { const context = initApiExtractor(options); if (context) { const apiExtractorResult = apiExtractorWrapper(context); if (apiExtractorResult && !apiExtractorResult.succeeded) { throw new Error( 'The public API file is out of date. Please run the API snapshot and commit the updated API file.', ); } } }; } /** * Updates the API extractor snapshot * * Sample config which should be saved as api-extractor.json: * ``` * { * "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", * "mainEntryPointFilePath": "<projectFolder>/lib/index.d.ts", * "docModel": { * "enabled": true * }, * "dtsRollup": { * "enabled": true * }, * "apiReport": { * "enabled": true * } * } * ``` */ export function apiExtractorUpdateTask(options: ApiExtractorOptions): TaskFunction; /** @deprecated Use object param version */ export function apiExtractorUpdateTask( configJsonFilePath: string, extractorOptions: Omit<ApiExtractorOptions, 'configJsonFilePath'>, ): TaskFunction; export function apiExtractorUpdateTask( configJsonFilePathOrOption: string | ApiExtractorOptions = {}, extractorOptions: Omit<ApiExtractorOptions, 'configJsonFilePath'> = {}, ): TaskFunction { const options = typeof configJsonFilePathOrOption === 'string' ? { ...extractorOptions, configJsonFilePath: configJsonFilePathOrOption } : { ...configJsonFilePathOrOption }; return function apiExtractorUpdate() { const context = initApiExtractor(options); if (context) { let apiExtractorResult = apiExtractorWrapper(context); if (apiExtractorResult) { if (!apiExtractorResult.succeeded) { logger.warn(`- Update API: API file is out of date, updating...`); fs.mkdirpSync(path.dirname(context.config.reportFilePath)); // ensure destination exists fs.copyFileSync(context.config.reportTempFilePath, context.config.reportFilePath); logger.info(`- Update API: successfully updated API file, verifying the updates...`); apiExtractorResult = apiExtractorWrapper(context); if (!apiExtractorResult || !apiExtractorResult.succeeded) { throw new Error(`- Update API: failed to verify API updates.`); } else { logger.info(`- Update API: successully verified API file. Please commit API file as part of your changes.`); } } else { logger.info(`- Update API: API file is already up to date, no update needed.`); } } } }; } /** * Load the api-extractor module (if available) and the config file. * Returns undefined if api-extractor or the config file couldn't be found. */ function initApiExtractor(options: ApiExtractorOptions): ApiExtractorContext | undefined { const apiExtractorModule: typeof ApiExtractorTypes = tryRequire('@microsoft/api-extractor'); if (!apiExtractorModule) { logger.warn('@microsoft/api-extractor package not detected. This task will have no effect.'); return; } if (!apiExtractorModule.Extractor.invoke) { logger.warn('Please update your @microsoft/api-extractor package. This task will have no effect.'); return; } const { ExtractorConfig } = apiExtractorModule; const { configJsonFilePath = ExtractorConfig.FILENAME, fixNewlines, onResult, ...extractorOptions } = options; if (!fs.existsSync(configJsonFilePath)) { const defaultConfig = path.resolve(__dirname, '../../config/apiExtractor/api-extractor.json'); logger.warn( `Config file not found for api-extractor! Please copy ${defaultConfig} to project root folder to try again`, ); return; } const rawConfig = ExtractorConfig.loadFile(configJsonFilePath); // Allow modification of the config options.onConfigLoaded?.(rawConfig); // This follows the logic from ExtractorConfig.loadFileAndPrepare // https://github.com/microsoft/rushstack/blob/1eb3d8ccf2a87b90a1038bf464b0b73fb3c7fd78/apps/api-extractor/src/api/ExtractorConfig.ts#L455 const prepareConfig = { configObject: rawConfig, configObjectFullPath: path.resolve(configJsonFilePath), packageJsonFullPath: path.resolve('package.json'), }; // Respect projectFolder if provided. if (options.projectFolder) { prepareConfig.configObject.projectFolder = options.projectFolder; } const config = ExtractorConfig.prepare(prepareConfig); return { apiExtractorModule, config, extractorOptions, options }; } function apiExtractorWrapper({ apiExtractorModule, config, extractorOptions, options, }: ApiExtractorContext): ApiExtractorTypes.ExtractorResult | undefined { const { Extractor } = apiExtractorModule; logger.info(`Extracting Public API surface from '${config.mainEntryPointFilePath}'`); const result = Extractor.invoke(config, extractorOptions); if (options.onResult) { options.onResult(result, extractorOptions); } if (options.fixNewlines) { fixApiFileNewlines(options.localBuild ? config.reportFilePath : config.reportTempFilePath, { sampleFilePath: config.apiJsonFilePath, }); } return result; } /** * Update the newlines of the API report file to be consistent with other files in the repo, * and remove trailing spaces. * @param apiFilePath - Path to the API report file * @param newlineOptions - Provide either `newline` to specify the type of newlines to use, * or `sampleFilePath` to infer the newline type from a file. */ export function fixApiFileNewlines( apiFilePath: string, newlineOptions: { sampleFilePath?: string; newline?: string }, ): void { let newline: string; if (newlineOptions.newline) { newline = newlineOptions.newline; } else if (newlineOptions.sampleFilePath) { const sampleFile = fs.readFileSync(newlineOptions.sampleFilePath).toString(); newline = sampleFile.match(/\r?\n/)![0]; } else { throw new Error( 'fixApiFileNewlines: you must provide either newlineOptions.sampleFilePath or newlineOptions.newline', ); } const contents = fs.readFileSync(apiFilePath).toString(); // Replace newlines. Also remove trailing spaces (second regex gets a trailing space on the // last line of the file). fs.writeFileSync(apiFilePath, contents.replace(/ ?\r?\n/g, newline).replace(/ $/, '')); }