From f065b5afde0e9688996b0867fffdaa5b4431d0c2 Mon Sep 17 00:00:00 2001 From: Martin Hochel Date: Fri, 21 Oct 2022 18:25:02 +0200 Subject: [PATCH] fix(scripts): make internal api stripping work and add rolluped dts after check --- scripts/tasks/api-extractor.ts | 124 ++++++++++++++++++++++++++++----- scripts/tasks/utils.ts | 23 +++--- 2 files changed, 117 insertions(+), 30 deletions(-) diff --git a/scripts/tasks/api-extractor.ts b/scripts/tasks/api-extractor.ts index 109d138cbdb6e8..37a43362b86365 100644 --- a/scripts/tasks/api-extractor.ts +++ b/scripts/tasks/api-extractor.ts @@ -1,6 +1,6 @@ import * as glob from 'glob'; import * as path from 'path'; -import { apiExtractorVerifyTask, task, series, logger, TaskFunction } from 'just-scripts'; +import { apiExtractorVerifyTask, task, series, logger, TaskFunction, tscTask } from 'just-scripts'; import { ExtractorMessageCategory } from '@microsoft/api-extractor'; import chalk from 'chalk'; import { getTsPathAliasesConfig, getTsPathAliasesApiExtractorConfig } from './utils'; @@ -67,6 +67,10 @@ export function apiExtractor() { const args: ReturnType & Partial = getJustArgv(); const { isUsingTsSolutionConfigs, packageJson, tsConfig, tsConfigPath } = getTsPathAliasesConfig(); + const messages = { + TS7016: [] as string[], + TS2305: [] as string[], + }; return apiExtractorConfigsForExecution.length ? (series( @@ -82,6 +86,36 @@ export function apiExtractor() { typescriptCompilerFolder: args['typescript-compiler-folder'], configJsonFilePath: args.config ?? configPath, localBuild: args.local ?? !process.env.TF_BUILD, + onResult: result => { + if (result.succeeded === true) { + return; + } + + if (messages.TS2305.length) { + const errTitle = [ + chalk.bgRed.white.bold(`api-extractor | API VIOLATION:`), + chalk.red(` Your package public API uses \`@internal\` marked API's from following packages:`), + '\n', + ].join(''); + const logErr = formatApiViolationMessage(messages.TS2305); + + logger.error(errTitle, logErr, '\n'); + } + + if (messages.TS7016.length) { + const errTitle = [ + chalk.bgRed.white.bold(`api-extractor | MISSING DEPENDENCY TYPE DECLARATIONS:`), + chalk.red(` Package dependencies are missing index.d.ts type definitions:`), + '\n', + ].join(''); + const logErr = formatMissingApiViolationMessage(messages.TS7016); + const logFix = chalk.blueBright( + `${chalk.bold('🛠 FIX')}: run '${chalk.italic(`yarn lage generate-api --to ${packageJson.name}`)}'`, + ); + + logger.error(errTitle, logErr, '\n', logFix, '\n'); + } + }, messageCallback: message => { if (!isUsingTsSolutionConfigs) { @@ -92,23 +126,11 @@ export function apiExtractor() { } if (message.messageId === compilerMessages.TS2305) { - logger.error( - chalk.bgRed.white.bold(`api-extractor | API VIOLATION:`), - chalk.red(`Looks like your package public API surface uses \`@internal\` marked API's!`), - '\n', - ); + messages.TS2305.push(message.text); } if (message.messageId === compilerMessages.TS7016) { - logger.error( - chalk.bgRed.white.bold(`api-extractor | MISSING DEPENDENCY TYPE DECLARATIONS:`), - chalk.red(`Looks like your package dependencies don't have generated index.d.ts type definitions.`), - '\n', - chalk.blueBright( - `🛠 Fix this by running: ${chalk.italic(`yarn lage generate-api --to ${packageJson.name}`)}`, - ), - '\n', - ); + messages.TS7016.push(message.text); } }, onConfigLoaded: config => { @@ -117,7 +139,12 @@ export function apiExtractor() { } logger.info(`api-extractor: package is using TS path aliases. Overriding TS compiler settings.`); - const compilerConfig = getTsPathAliasesApiExtractorConfig({ tsConfig, tsConfigPath, packageJson }); + + const compilerConfig = getTsPathAliasesApiExtractorConfig({ + tsConfig, + tsConfigPath, + packageJson, + }); config.compiler = compilerConfig; }, @@ -138,3 +165,68 @@ export function apiExtractor() { logger.info(`skipping api-extractor execution - no configs present`); }; } + +/** + * + * @example + * + * ``` + (TS2305) Module '"@fluentui/react-shared-contexts"' has no exported member 'ThemeContextValue_unstable'. + (TS2305) Module '"@fluentui/react-shared-contexts"' has no exported member 'TooltipVisibilityContextValue_unstable'. + + ↓ ↓ ↓ + + @fluentui/react-shared-contexts: + - TooltipVisibilityContextValue_unstable + - ThemeContextValue_unstable + ``` + */ +function formatApiViolationMessage(messages: string[]) { + const regexPkg = /'"(@fluentui\/[a-z-]+)"'/i; + const exportedTokenRegex = /'([a-z-_]+)'/i; + + const byPackage = messages.reduce((acc, curr) => { + const [, packageName] = regexPkg.exec(curr) ?? []; + const [, exportedToken] = exportedTokenRegex.exec(curr) ?? []; + if (acc[packageName]) { + acc[packageName].push(exportedToken); + return acc; + } + acc[packageName] = [exportedToken]; + return acc; + }, {} as Record); + + return Object.entries(byPackage) + .map(([packageName, tokens]) => { + return [ + chalk.red.underline(packageName) + ':', + tokens.map(token => chalk.italic.red(' - ' + token)).join('\n'), + ].join('\n'); + }) + .join('\n'); +} + +/** + * + * @example + ``` + (TS7016) Could not find a declaration file for module '@fluentui/react-theme' + (TS7016) Could not find a declaration file for module '@fluentui/react-shared-contexts' + + ↓ ↓ ↓ + + - @fluentui/react-theme + - @fluentui/react-shared-contexts + ``` + */ +function formatMissingApiViolationMessage(messages: string[]) { + const regexPkg = /'(@fluentui\/[a-z-]+)'/i; + + return Object.values( + messages.reduce((acc, curr) => { + const [, packageName] = regexPkg.exec(curr) ?? []; + acc[curr] = chalk.italic.red('\t- ' + packageName); + return acc; + }, {} as Record), + ).join('\n'); +} diff --git a/scripts/tasks/utils.ts b/scripts/tasks/utils.ts index 97d3174cdfa6ff..f2d7d22e6276c2 100644 --- a/scripts/tasks/utils.ts +++ b/scripts/tasks/utils.ts @@ -2,8 +2,6 @@ import * as fs from 'fs'; import * as path from 'path'; import * as jju from 'jju'; import type { TscTaskOptions } from 'just-scripts'; -import { offsetFromRoot } from '@nrwl/devkit'; -import { appRootPath } from '@nrwl/tao/src/utils/app-root'; export function getTsPathAliasesConfig() { const cwd = process.cwd(); @@ -46,26 +44,23 @@ export function getTsPathAliasesApiExtractorConfig(options: { tsConfigPath: string; packageJson: PackageJson; }) { - const rootOffset = offsetFromRoot(path.dirname(options.tsConfigPath.replace(appRootPath, ''))); /** - * This special TSConfig config is all that's needed for api-extractor so it has all type information used for package: + * Customized TSConfig that uses `tsconfig.lib.json` as base with some required overrides: * * NOTES: - * - `compilerOptions.paths` doesn't work, nor is possible to turn them off when `extends` is used + * - `extends` is properly resolved via api-extractor which uses TS api + * - `skipLibCheck` needs to be explicitly set to `false` so errors propagate to api-extractor + * - `paths` is set to `undefined` so api-extractor won't use source files rather rollup-ed declaration files only * */ const apiExtractorTsConfig: TsConfig = { - include: options.tsConfig.include, - /** - * `files` might be used to specify additional `d.ts` or global type definitions. IF they exist in package tsconfig we need to include them - */ - ...(options.tsConfig.files ? { files: options.tsConfig.files } : null), + ...options.tsConfig, compilerOptions: { + ...options.tsConfig.compilerOptions, ...enableAllowSyntheticDefaultImports({ pkgJson: options.packageJson }), - strict: true, - lib: options.tsConfig.compilerOptions.lib, - typeRoots: ['node_modules/@types', `${rootOffset}typings`], - types: options.tsConfig.compilerOptions.types, + skipLibCheck: false, + // NOTE: just-scripts provides invalid types for tsconfig, thus `paths` cannot be set to dictionary,nor null or `{}` + paths: undefined, }, };