From b56bfd12debed9800634df6e6d6e0092e1c4578b Mon Sep 17 00:00:00 2001 From: Juan Pablo Garcia Ripa Date: Mon, 28 Nov 2022 18:10:10 +0100 Subject: [PATCH 1/7] add cofig options to allow warnings and omit messages Signed-off-by: Juan Pablo Garcia Ripa --- packages/repo-tools/cli-report.md | 3 ++ packages/repo-tools/package.json | 1 + .../src/commands/api-reports/api-extractor.ts | 51 +++++++++++-------- .../src/commands/api-reports/api-reports.ts | 22 ++++++-- packages/repo-tools/src/commands/index.ts | 13 +++++ packages/repo-tools/src/lib/paths.ts | 20 ++++++++ yarn.lock | 1 + 7 files changed, 86 insertions(+), 25 deletions(-) create mode 100644 packages/repo-tools/src/lib/paths.ts diff --git a/packages/repo-tools/cli-report.md b/packages/repo-tools/cli-report.md index 309ddad2a3..b2a171cf42 100644 --- a/packages/repo-tools/cli-report.md +++ b/packages/repo-tools/cli-report.md @@ -26,6 +26,9 @@ Options: --ci --tsc --docs + --allow-warnings [allowWarningsPaths...] + --folders + --omitMessages -h, --help ``` diff --git a/packages/repo-tools/package.json b/packages/repo-tools/package.json index a0caa3ee9c..b779b0798f 100644 --- a/packages/repo-tools/package.json +++ b/packages/repo-tools/package.json @@ -30,6 +30,7 @@ "backstage-repo-tools": "bin/backstage-repo-tools" }, "dependencies": { + "@backstage/cli-common": "workspace:^", "@backstage/errors": "workspace:^", "@manypkg/get-packages": "^1.1.3", "@microsoft/api-documenter": "^7.17.11", diff --git a/packages/repo-tools/src/commands/api-reports/api-extractor.ts b/packages/repo-tools/src/commands/api-reports/api-extractor.ts index bae62ccc7e..9f4f9b238d 100644 --- a/packages/repo-tools/src/commands/api-reports/api-extractor.ts +++ b/packages/repo-tools/src/commands/api-reports/api-extractor.ts @@ -65,9 +65,10 @@ import { } from '@microsoft/api-documenter/lib/markdown/CustomMarkdownEmitter'; import { IMarkdownEmitterContext } from '@microsoft/api-documenter/lib/markdown/MarkdownEmitter'; import { AstDeclaration } from '@microsoft/api-extractor/lib/analyzer/AstDeclaration'; +import { paths as cliPaths } from '../../lib/paths'; const tmpDir = resolvePath( - process.cwd(), + cliPaths.targetRoot, './node_modules/.cache/api-extractor', ); @@ -219,21 +220,10 @@ ApiReportGenerator.generateReviewFileContent = }); }; -const PACKAGE_ROOTS = ['packages', 'plugins']; - -const ALLOW_WARNINGS = [ - 'packages/core-components', - 'plugins/catalog', - 'plugins/catalog-import', - 'plugins/git-release-manager', - 'plugins/jenkins', - 'plugins/kubernetes', -]; - async function resolvePackagePath( packagePath: string, ): Promise { - const projectRoot = resolvePath(process.cwd()); + const projectRoot = resolvePath(cliPaths.targetRoot); const fullPackageDir = resolvePath(projectRoot, packagePath); const stat = await fs.stat(fullPackageDir); @@ -269,11 +259,11 @@ export async function findSpecificPackageDirs(unresolvedPackageDirs: string[]) { return packageDirs; } -export async function findPackageDirs() { +export async function findPackageDirs(packageRoots: string[]) { const packageDirs = new Array(); - const projectRoot = resolvePath(process.cwd()); + const projectRoot = resolvePath(cliPaths.targetRoot); - for (const packageRoot of PACKAGE_ROOTS) { + for (const packageRoot of packageRoots) { const dirs = await fs.readdir(resolvePath(projectRoot, packageRoot)); for (const dir of dirs) { const packageDir = await resolvePackagePath(join(packageRoot, dir)); @@ -289,7 +279,7 @@ export async function findPackageDirs() { } export async function createTemporaryTsConfig(includedPackageDirs: string[]) { - const path = resolvePath(process.cwd(), 'tsconfig.tmp.json'); + const path = resolvePath(cliPaths.targetRoot, 'tsconfig.tmp.json'); process.once('exit', () => { fs.removeSync(path); @@ -375,6 +365,8 @@ interface ApiExtractionOptions { outputDir: string; isLocalBuild: boolean; tsconfigFilePath: string; + allowWarnings: boolean | string[]; + omitMessages?: string[]; } export async function runApiExtraction({ @@ -382,25 +374,39 @@ export async function runApiExtraction({ outputDir, isLocalBuild, tsconfigFilePath, + allowWarnings, + omitMessages = [], }: ApiExtractionOptions) { await fs.remove(outputDir); const entryPoints = packageDirs.map(packageDir => { return resolvePath( - process.cwd(), + cliPaths.targetRoot, `./dist-types/${packageDir}/src/index.d.ts`, ); }); let compilerState: CompilerState | undefined = undefined; + const allowWarningPkg = Array.isArray(allowWarnings) ? allowWarnings : []; + + const messagesConf: { [key: string]: { logLevel: string } } = {}; + for (const messageCode of omitMessages) { + messagesConf[messageCode] = { + logLevel: 'none', + }; + } const warnings = new Array(); for (const packageDir of packageDirs) { console.log(`## Processing ${packageDir}`); - const projectFolder = resolvePath(process.cwd(), packageDir); + const noBail = Array.isArray(allowWarnings) + ? allowWarnings.includes(packageDir) + : allowWarnings; + + const projectFolder = resolvePath(cliPaths.targetRoot, packageDir); const packageFolder = resolvePath( - process.cwd(), + cliPaths.targetRoot, './dist-types', packageDir, ); @@ -453,6 +459,7 @@ export async function runApiExtraction({ logLevel: 'warning' as ExtractorLogLevel.Warning, addToApiReportFile: true, }, + ...messagesConf, }, tsdocMessageReporting: { default: { @@ -543,12 +550,12 @@ export async function runApiExtraction({ } const warningCountAfter = await countApiReportWarnings(projectFolder); - if (warningCountAfter > 0 && !ALLOW_WARNINGS.includes(packageDir)) { + if (warningCountAfter > 0 && !noBail) { throw new Error( `The API Report for ${packageDir} is not allowed to have warnings`, ); } - if (warningCountAfter === 0 && ALLOW_WARNINGS.includes(packageDir)) { + if (warningCountAfter === 0 && allowWarningPkg.includes(packageDir)) { console.log( `No need to allow warnings for ${packageDir}, it does not have any`, ); diff --git a/packages/repo-tools/src/commands/api-reports/api-reports.ts b/packages/repo-tools/src/commands/api-reports/api-reports.ts index 3ac740d596..c66d084d64 100644 --- a/packages/repo-tools/src/commands/api-reports/api-reports.ts +++ b/packages/repo-tools/src/commands/api-reports/api-reports.ts @@ -27,16 +27,29 @@ import { runCliExtraction, buildDocs, } from './api-extractor'; +import { paths as cliPaths } from '../../lib/paths'; export default async (paths: string[], opts: OptionValues) => { + console.log(opts); + console.log({ + ownDir: cliPaths.ownDir, + ownRoot: cliPaths.ownRoot, + targetDir: cliPaths.targetDir, + targetRoot: cliPaths.targetRoot, + 'process.cwd()': process.cwd(), + }); const tmpDir = resolvePath( - process.cwd(), + cliPaths.targetRoot, './node_modules/.cache/api-extractor', ); - const projectRoot = resolvePath(process.cwd()); + + const projectRoot = resolvePath(cliPaths.targetRoot); const isCiBuild = opts.ci; const isDocsBuild = opts.docs; const runTsc = opts.tsc; + const packageRoots = opts.folders; + const allowWarnings: boolean | string[] = opts.allowWarnings; + const omitMessages = opts.omitMessages; const selectedPackageDirs = await findSpecificPackageDirs(paths); @@ -85,7 +98,8 @@ export default async (paths: string[], opts: OptionValues) => { } } - const packageDirs = selectedPackageDirs ?? (await findPackageDirs()); + const packageDirs = + selectedPackageDirs ?? (await findPackageDirs(packageRoots)); const { tsPackageDirs, cliPackageDirs } = await categorizePackageDirs( projectRoot, @@ -99,6 +113,8 @@ export default async (paths: string[], opts: OptionValues) => { outputDir: tmpDir, isLocalBuild: !isCiBuild, tsconfigFilePath, + allowWarnings, + omitMessages, }); } if (cliPackageDirs.length > 0) { diff --git a/packages/repo-tools/src/commands/index.ts b/packages/repo-tools/src/commands/index.ts index ebbcb5be8e..5821dfdd3d 100644 --- a/packages/repo-tools/src/commands/index.ts +++ b/packages/repo-tools/src/commands/index.ts @@ -24,6 +24,19 @@ export function registerCommands(program: Command) { .option('--ci', 'CI run checks that there is no changes on API reports') .option('--tsc', 'executes the tsc compilation before extracting the APIs') .option('--docs', 'generates the api documentation') + .option( + '--allow-warnings [allowWarningsPaths...]', + 'continue processing packages after getting errors on selected packages', + false, + ) + .option('--folders ', 'packages folder containers', [ + 'packages', + 'plugins', + ]) + .option( + '--omitMessages ', + 'select some message code to be omited on the API Extractor (i.e ae-cyclic-inherit-doc)', + ) .description('Generate an API report for selected packages') .action( lazy(() => import('./api-reports/api-reports').then(m => m.default)), diff --git a/packages/repo-tools/src/lib/paths.ts b/packages/repo-tools/src/lib/paths.ts new file mode 100644 index 0000000000..2c658c27b3 --- /dev/null +++ b/packages/repo-tools/src/lib/paths.ts @@ -0,0 +1,20 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { findPaths } from '@backstage/cli-common'; + +/* eslint-disable-next-line no-restricted-syntax */ +export const paths = findPaths(__dirname); diff --git a/yarn.lock b/yarn.lock index 441430dd19..e39514bb98 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8467,6 +8467,7 @@ __metadata: version: 0.0.0-use.local resolution: "@backstage/repo-tools@workspace:packages/repo-tools" dependencies: + "@backstage/cli-common": "workspace:^" "@backstage/errors": "workspace:^" "@manypkg/get-packages": ^1.1.3 "@microsoft/api-documenter": ^7.17.11 From 1176a73050b45422d42cef1a0dc66eb88b31c104 Mon Sep 17 00:00:00 2001 From: Juan Pablo Garcia Ripa Date: Wed, 30 Nov 2022 23:51:05 +0100 Subject: [PATCH 2/7] use package.json workspace package as default roots Signed-off-by: Juan Pablo Garcia Ripa --- package.json | 2 +- packages/repo-tools/cli-report.md | 5 +- packages/repo-tools/package.json | 4 + .../src/commands/api-reports/api-extractor.ts | 73 ++++++++----------- .../src/commands/api-reports/api-reports.ts | 38 +++++----- packages/repo-tools/src/commands/index.ts | 10 +-- yarn.lock | 9 +++ 7 files changed, 69 insertions(+), 72 deletions(-) diff --git a/package.json b/package.json index 524cdf4bbe..497a652cdf 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "build:backend": "yarn workspace backend build", "build:all": "backstage-cli repo build --all", "build:api-reports": "yarn build:api-reports:only --tsc", - "build:api-reports:only": "backstage-repo-tools api-reports", + "build:api-reports:only": "backstage-repo-tools api-reports --allow-warnings packages/core-components plugins/catalog plugins/catalog-import plugins/git-release-manager plugins/jenkins plugins/kubernetes", "build:api-docs": "LANG=en_EN yarn build:api-reports --docs", "tsc": "tsc", "tsc:full": "backstage-cli repo clean && tsc --skipLibCheck false --incremental false", diff --git a/packages/repo-tools/cli-report.md b/packages/repo-tools/cli-report.md index b2a171cf42..ba15ecc674 100644 --- a/packages/repo-tools/cli-report.md +++ b/packages/repo-tools/cli-report.md @@ -12,7 +12,7 @@ Options: -h, --help Commands: - api-reports [options] [path...] + api-reports [options] [paths...] type-deps help [command] ``` @@ -20,14 +20,13 @@ Commands: ### `backstage-repo-tools api-reports` ``` -Usage: backstage-repo-tools api-reports [options] [path...] +Usage: backstage-repo-tools api-reports [options] [paths...] Options: --ci --tsc --docs --allow-warnings [allowWarningsPaths...] - --folders --omitMessages -h, --help ``` diff --git a/packages/repo-tools/package.json b/packages/repo-tools/package.json index b779b0798f..657bbb4404 100644 --- a/packages/repo-tools/package.json +++ b/packages/repo-tools/package.json @@ -40,8 +40,12 @@ "chalk": "^4.0.0", "commander": "^9.1.0", "fs-extra": "10.1.0", + "is-glob": "^4.0.3", "ts-node": "^10.0.0" }, + "devDependencies": { + "@types/is-glob": "^4.0.2" + }, "files": [ "bin", "dist/**/*.js" diff --git a/packages/repo-tools/src/commands/api-reports/api-extractor.ts b/packages/repo-tools/src/commands/api-reports/api-extractor.ts index 9f4f9b238d..d1e8d86240 100644 --- a/packages/repo-tools/src/commands/api-reports/api-extractor.ts +++ b/packages/repo-tools/src/commands/api-reports/api-extractor.ts @@ -67,8 +67,14 @@ import { IMarkdownEmitterContext } from '@microsoft/api-documenter/lib/markdown/ import { AstDeclaration } from '@microsoft/api-extractor/lib/analyzer/AstDeclaration'; import { paths as cliPaths } from '../../lib/paths'; -const tmpDir = resolvePath( - cliPaths.targetRoot, +import g from 'glob'; +import isGlob from 'is-glob'; + +import { promisify } from 'util'; + +const glob = promisify(g); + +const tmpDir = cliPaths.resolveTargetRoot( './node_modules/.cache/api-extractor', ); @@ -220,11 +226,10 @@ ApiReportGenerator.generateReviewFileContent = }); }; -async function resolvePackagePath( +export async function resolvePackagePath( packagePath: string, ): Promise { - const projectRoot = resolvePath(cliPaths.targetRoot); - const fullPackageDir = resolvePath(projectRoot, packagePath); + const fullPackageDir = cliPaths.resolveTargetRoot(packagePath); const stat = await fs.stat(fullPackageDir); if (!stat.isDirectory()) { @@ -237,49 +242,29 @@ async function resolvePackagePath( } catch (_) { return undefined; } - - return relativePath(projectRoot, fullPackageDir); + return relativePath(cliPaths.targetRoot, fullPackageDir); } -export async function findSpecificPackageDirs(unresolvedPackageDirs: string[]) { +export async function findPackageDirs(selectedPaths: string[]) { const packageDirs = new Array(); + for (const packageRoot of selectedPaths) { + const fullPath = cliPaths.resolveTargetRoot(packageRoot); - for (const unresolvedPackageDir of unresolvedPackageDirs) { - const packageDir = await resolvePackagePath(unresolvedPackageDir); - if (!packageDir) { - throw new Error(`'${unresolvedPackageDir}' is not a valid package path`); - } - packageDirs.push(packageDir); - } - - if (packageDirs.length === 0) { - return undefined; - } - - return packageDirs; -} - -export async function findPackageDirs(packageRoots: string[]) { - const packageDirs = new Array(); - const projectRoot = resolvePath(cliPaths.targetRoot); - - for (const packageRoot of packageRoots) { - const dirs = await fs.readdir(resolvePath(projectRoot, packageRoot)); + // if the path contain any glob notation we resolve all the paths to process one by one + const dirs = isGlob(fullPath) ? await glob(fullPath) : [fullPath]; for (const dir of dirs) { - const packageDir = await resolvePackagePath(join(packageRoot, dir)); + const packageDir = await resolvePackagePath(dir); if (!packageDir) { continue; } - packageDirs.push(packageDir); } } - return packageDirs; } export async function createTemporaryTsConfig(includedPackageDirs: string[]) { - const path = resolvePath(cliPaths.targetRoot, 'tsconfig.tmp.json'); + const path = cliPaths.resolveTargetRoot('tsconfig.tmp.json'); process.once('exit', () => { fs.removeSync(path); @@ -380,8 +365,7 @@ export async function runApiExtraction({ await fs.remove(outputDir); const entryPoints = packageDirs.map(packageDir => { - return resolvePath( - cliPaths.targetRoot, + return cliPaths.resolveTargetRoot( `./dist-types/${packageDir}/src/index.d.ts`, ); }); @@ -404,9 +388,8 @@ export async function runApiExtraction({ ? allowWarnings.includes(packageDir) : allowWarnings; - const projectFolder = resolvePath(cliPaths.targetRoot, packageDir); - const packageFolder = resolvePath( - cliPaths.targetRoot, + const projectFolder = cliPaths.resolveTargetRoot(packageDir); + const packageFolder = cliPaths.resolveTargetRoot( './dist-types', packageDir, ); @@ -1138,10 +1121,7 @@ export async function buildDocs({ documenter.generateFiles(); } -export async function categorizePackageDirs( - projectRoot: string, - packageDirs: any[], -) { +export async function categorizePackageDirs(packageDirs: any[]) { const dirs = packageDirs.slice(); const tsPackageDirs = new Array(); const cliPackageDirs = new Array(); @@ -1157,7 +1137,7 @@ export async function categorizePackageDirs( } const pkgJson = await fs - .readJson(resolvePath(projectRoot, dir, 'package.json')) + .readJson(cliPaths.resolveTargetRoot(dir, 'package.json')) .catch(error => { if (error.code === 'ENOENT') { return undefined; @@ -1211,6 +1191,7 @@ function parseHelpPage(helpPageContent: string) { let options = new Array(); let commands = new Array(); + let commandArguments = new Array(); while (lines.length > 0) { while (lines.length > 0 && !lines[0].endsWith(':')) { @@ -1235,6 +1216,8 @@ function parseHelpPage(helpPageContent: string) { options = sectionItems; } else if (sectionName?.toLocaleLowerCase('en-US') === 'commands:') { commands = sectionItems; + } else if (sectionName?.toLocaleLowerCase('en-US') === 'arguments:') { + commandArguments = sectionItems; } else { throw new Error(`Unknown CLI section: ${sectionName}`); } @@ -1245,6 +1228,7 @@ function parseHelpPage(helpPageContent: string) { usage, options, commands, + commandArguments, }; } @@ -1256,6 +1240,7 @@ interface CliHelpPage { usage: string | undefined; options: string[]; commands: string[]; + commandArguments: string[]; } async function exploreCliHelpPages( @@ -1335,7 +1320,7 @@ export async function runCliExtraction({ }: CliExtractionOptions) { for (const packageDir of packageDirs) { console.log(`## Processing ${packageDir}`); - const fullDir = resolvePath(projectRoot, packageDir); + const fullDir = cliPaths.resolveTargetRoot(packageDir); const pkgJson = await fs.readJson(resolvePath(fullDir, 'package.json')); if (!pkgJson.bin) { diff --git a/packages/repo-tools/src/commands/api-reports/api-reports.ts b/packages/repo-tools/src/commands/api-reports/api-reports.ts index c66d084d64..9b0d78d6a8 100644 --- a/packages/repo-tools/src/commands/api-reports/api-reports.ts +++ b/packages/repo-tools/src/commands/api-reports/api-reports.ts @@ -19,7 +19,6 @@ import { resolve as resolvePath } from 'path'; import fs from 'fs-extra'; import { spawnSync } from 'child_process'; import { - findSpecificPackageDirs, createTemporaryTsConfig, findPackageDirs, categorizePackageDirs, @@ -30,14 +29,6 @@ import { import { paths as cliPaths } from '../../lib/paths'; export default async (paths: string[], opts: OptionValues) => { - console.log(opts); - console.log({ - ownDir: cliPaths.ownDir, - ownRoot: cliPaths.ownRoot, - targetDir: cliPaths.targetDir, - targetRoot: cliPaths.targetRoot, - 'process.cwd()': process.cwd(), - }); const tmpDir = resolvePath( cliPaths.targetRoot, './node_modules/.cache/api-extractor', @@ -47,25 +38,26 @@ export default async (paths: string[], opts: OptionValues) => { const isCiBuild = opts.ci; const isDocsBuild = opts.docs; const runTsc = opts.tsc; - const packageRoots = opts.folders; + const selectedPaths = paths.length ? paths : await getWorkspacePkgs(); const allowWarnings: boolean | string[] = opts.allowWarnings; const omitMessages = opts.omitMessages; - const selectedPackageDirs = await findSpecificPackageDirs(paths); + const selectedPackageDirs = await findPackageDirs(selectedPaths); - if (selectedPackageDirs && isCiBuild) { + if (paths.length && isCiBuild) { + // TODO @sarabadu we can remove this validation to allow `/plugins/*` on CI?? throw new Error( 'Package path arguments are not supported together with the --ci flag', ); } - if (!selectedPackageDirs && !isCiBuild && !isDocsBuild) { + if (!paths.length && !isCiBuild && !isDocsBuild) { console.log(''); console.log( 'TIP: You can generate api-reports for select packages by passing package paths:', ); console.log(''); console.log( - ' yarn build:api-reports packages/config packages/core-plugin-api', + ' yarn build:api-reports packages/config packages/core-plugin-api plugins/*', ); console.log(''); } @@ -98,12 +90,8 @@ export default async (paths: string[], opts: OptionValues) => { } } - const packageDirs = - selectedPackageDirs ?? (await findPackageDirs(packageRoots)); - const { tsPackageDirs, cliPackageDirs } = await categorizePackageDirs( - projectRoot, - packageDirs, + selectedPackageDirs, ); if (tsPackageDirs.length > 0) { @@ -134,3 +122,15 @@ export default async (paths: string[], opts: OptionValues) => { }); } }; +async function getWorkspacePkgs() { + const pkgJson = await fs + .readJson(cliPaths.resolveTargetRoot('package.json')) + .catch(error => { + if (error.code === 'ENOENT') { + return undefined; + } + throw error; + }); + const workspaces = pkgJson?.workspaces?.packages; + return workspaces; +} diff --git a/packages/repo-tools/src/commands/index.ts b/packages/repo-tools/src/commands/index.ts index 5821dfdd3d..bbc6773f83 100644 --- a/packages/repo-tools/src/commands/index.ts +++ b/packages/repo-tools/src/commands/index.ts @@ -20,7 +20,11 @@ import { exitWithError } from '../lib/errors'; export function registerCommands(program: Command) { program - .command('api-reports [path...]') + .command('api-reports') + .argument( + '[paths...]', + 'path of package folder to extract API reports, `workspaces.packages` from root packages.json by default', + ) .option('--ci', 'CI run checks that there is no changes on API reports') .option('--tsc', 'executes the tsc compilation before extracting the APIs') .option('--docs', 'generates the api documentation') @@ -29,10 +33,6 @@ export function registerCommands(program: Command) { 'continue processing packages after getting errors on selected packages', false, ) - .option('--folders ', 'packages folder containers', [ - 'packages', - 'plugins', - ]) .option( '--omitMessages ', 'select some message code to be omited on the API Extractor (i.e ae-cyclic-inherit-doc)', diff --git a/yarn.lock b/yarn.lock index e39514bb98..c9a9d1daea 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8474,9 +8474,11 @@ __metadata: "@microsoft/api-extractor": ^7.23.0 "@microsoft/api-extractor-model": ^7.17.2 "@microsoft/tsdoc": 0.14.1 + "@types/is-glob": ^4.0.2 chalk: ^4.0.0 commander: ^9.1.0 fs-extra: 10.1.0 + is-glob: ^4.0.3 ts-node: ^10.0.0 bin: backstage-repo-tools: bin/backstage-repo-tools @@ -14201,6 +14203,13 @@ __metadata: languageName: node linkType: hard +"@types/is-glob@npm:^4.0.2": + version: 4.0.2 + resolution: "@types/is-glob@npm:4.0.2" + checksum: 50b0a52b6d179781b36bfce35155e1e0dc66b62e2943153d7d7c7079c40ba6236528a254de8be6c52ff9a7a351996887802efd7fd763da3e2121315e4ffe2edf + languageName: node + linkType: hard + "@types/istanbul-lib-coverage@npm:*, @types/istanbul-lib-coverage@npm:^2.0.0, @types/istanbul-lib-coverage@npm:^2.0.1": version: 2.0.1 resolution: "@types/istanbul-lib-coverage@npm:2.0.1" From a8611bcac44753c1e0144b7333226c9b3c2e7faf Mon Sep 17 00:00:00 2001 From: Juan Pablo Garcia Ripa Date: Thu, 1 Dec 2022 14:16:17 +0100 Subject: [PATCH 3/7] add changeset Signed-off-by: Juan Pablo Garcia Ripa --- .changeset/lemon-coats-camp.md | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 .changeset/lemon-coats-camp.md diff --git a/.changeset/lemon-coats-camp.md b/.changeset/lemon-coats-camp.md new file mode 100644 index 0000000000..a2d87476f6 --- /dev/null +++ b/.changeset/lemon-coats-camp.md @@ -0,0 +1,11 @@ +--- +'@backstage/repo-tools': minor +--- + +Add new command options to the `api-report` + +- added `--allowWarnings` to continue processing packages if some packages have warnings +- added `--omitMessages` to pass some warnings messages code to be omitted from the api-report.md files +- The `paths` argument for this command now takes as default the value on `workspaces.packages` inside the root package.json +- The `paths` argument now allow glob patterns +- change the path resolution to use the `@backstage/cli-common` packages instead From 3dc6a522e96e724274e845b25759c475c54ae9a0 Mon Sep 17 00:00:00 2001 From: Juan Pablo Garcia Ripa Date: Fri, 2 Dec 2022 15:21:24 +0100 Subject: [PATCH 4/7] move `resolvePackagePath` to lib/path + tests Signed-off-by: Juan Pablo Garcia Ripa --- packages/repo-tools/package.json | 5 +- .../src/commands/api-reports/api-extractor.ts | 21 +----- packages/repo-tools/src/lib/paths.test.ts | 66 +++++++++++++++++++ packages/repo-tools/src/lib/paths.ts | 23 +++++++ yarn.lock | 3 + 5 files changed, 97 insertions(+), 21 deletions(-) create mode 100644 packages/repo-tools/src/lib/paths.test.ts diff --git a/packages/repo-tools/package.json b/packages/repo-tools/package.json index 657bbb4404..9b51d35922 100644 --- a/packages/repo-tools/package.json +++ b/packages/repo-tools/package.json @@ -44,7 +44,10 @@ "ts-node": "^10.0.0" }, "devDependencies": { - "@types/is-glob": "^4.0.2" + "@backstage/cli": "workspace:^", + "@types/is-glob": "^4.0.2", + "@types/mock-fs": "^4.13.0", + "mock-fs": "^5.1.0" }, "files": [ "bin", diff --git a/packages/repo-tools/src/commands/api-reports/api-extractor.ts b/packages/repo-tools/src/commands/api-reports/api-extractor.ts index d1e8d86240..b2fab40ca5 100644 --- a/packages/repo-tools/src/commands/api-reports/api-extractor.ts +++ b/packages/repo-tools/src/commands/api-reports/api-extractor.ts @@ -65,7 +65,7 @@ import { } from '@microsoft/api-documenter/lib/markdown/CustomMarkdownEmitter'; import { IMarkdownEmitterContext } from '@microsoft/api-documenter/lib/markdown/MarkdownEmitter'; import { AstDeclaration } from '@microsoft/api-extractor/lib/analyzer/AstDeclaration'; -import { paths as cliPaths } from '../../lib/paths'; +import { paths as cliPaths, resolvePackagePath } from '../../lib/paths'; import g from 'glob'; import isGlob from 'is-glob'; @@ -226,25 +226,6 @@ ApiReportGenerator.generateReviewFileContent = }); }; -export async function resolvePackagePath( - packagePath: string, -): Promise { - const fullPackageDir = cliPaths.resolveTargetRoot(packagePath); - - const stat = await fs.stat(fullPackageDir); - if (!stat.isDirectory()) { - return undefined; - } - - try { - const packageJsonPath = join(fullPackageDir, 'package.json'); - await fs.access(packageJsonPath); - } catch (_) { - return undefined; - } - return relativePath(cliPaths.targetRoot, fullPackageDir); -} - export async function findPackageDirs(selectedPaths: string[]) { const packageDirs = new Array(); for (const packageRoot of selectedPaths) { diff --git a/packages/repo-tools/src/lib/paths.test.ts b/packages/repo-tools/src/lib/paths.test.ts new file mode 100644 index 0000000000..90d84aec3c --- /dev/null +++ b/packages/repo-tools/src/lib/paths.test.ts @@ -0,0 +1,66 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import mockFs from 'mock-fs'; +import { resolve as resolvePath } from 'path'; +import { resolvePackagePath, paths } from './paths'; + +describe('paths', () => { + jest.spyOn(paths, 'targetRoot', 'get').mockReturnValue('/root'); + jest.spyOn(paths, 'resolveTargetRoot').mockImplementation((...path) => { + return resolvePath('/root', ...path); + }); + + beforeEach(() => { + mockFs({ + [paths.targetRoot]: { + 'package.json': JSON.stringify({ name: 'test' }), + packages: { + 'package-a': { + 'package.json': '{}', + }, + 'package-b': { + 'package.json': '{}', + }, + 'package-c': {}, + 'README.md': 'Hello World', + }, + }, + }); + }); + + afterEach(() => { + mockFs.restore(); + }); + + describe('resolvePackagePath', () => { + it('should return undefined if the package does not exist or does not contain a package.json', async () => { + expect(await resolvePackagePath('packages/package-d')).toBeUndefined(); + expect(await resolvePackagePath('packages/package-c')).toBeUndefined(); + }); + it('should return the path to the package if it exists and has a package.json', async () => { + expect(await resolvePackagePath('packages/package-a')).toBe( + 'packages/package-a', + ); + expect(await resolvePackagePath('packages/package-b')).toBe( + 'packages/package-b', + ); + }); + it('should return undefined if the pat is not a directory', async () => { + expect(await resolvePackagePath('packages/README.md')).toBeUndefined(); + }); + }); +}); diff --git a/packages/repo-tools/src/lib/paths.ts b/packages/repo-tools/src/lib/paths.ts index 2c658c27b3..f387e9be51 100644 --- a/packages/repo-tools/src/lib/paths.ts +++ b/packages/repo-tools/src/lib/paths.ts @@ -15,6 +15,29 @@ */ import { findPaths } from '@backstage/cli-common'; +import { relative as relativePath, join } from 'path'; +import fs from 'fs-extra'; /* eslint-disable-next-line no-restricted-syntax */ export const paths = findPaths(__dirname); + +export async function resolvePackagePath( + packagePath: string, +): Promise { + const fullPackageDir = paths.resolveTargetRoot(packagePath); + + try { + const stat = await fs.stat(fullPackageDir); + if (!stat.isDirectory()) { + return undefined; + } + + const packageJsonPath = join(fullPackageDir, 'package.json'); + + await fs.access(packageJsonPath); + } catch (e) { + console.log(`folder omitted: ${fullPackageDir}, cause: ${e}`); + return undefined; + } + return relativePath(paths.targetRoot, fullPackageDir); +} diff --git a/yarn.lock b/yarn.lock index c9a9d1daea..e92df381ca 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8467,6 +8467,7 @@ __metadata: version: 0.0.0-use.local resolution: "@backstage/repo-tools@workspace:packages/repo-tools" dependencies: + "@backstage/cli": "workspace:^" "@backstage/cli-common": "workspace:^" "@backstage/errors": "workspace:^" "@manypkg/get-packages": ^1.1.3 @@ -8475,10 +8476,12 @@ __metadata: "@microsoft/api-extractor-model": ^7.17.2 "@microsoft/tsdoc": 0.14.1 "@types/is-glob": ^4.0.2 + "@types/mock-fs": ^4.13.0 chalk: ^4.0.0 commander: ^9.1.0 fs-extra: 10.1.0 is-glob: ^4.0.3 + mock-fs: ^5.1.0 ts-node: ^10.0.0 bin: backstage-repo-tools: bin/backstage-repo-tools From b35c1770cf61beb8177314db8510092282da7852 Mon Sep 17 00:00:00 2001 From: Juan Pablo Garcia Ripa Date: Mon, 5 Dec 2022 19:25:40 +0100 Subject: [PATCH 5/7] change path argument to options and allow csv and glob for paths Signed-off-by: Juan Pablo Garcia Ripa --- .changeset/lemon-coats-camp.md | 7 +- package.json | 2 +- packages/repo-tools/cli-report.md | 9 +- packages/repo-tools/package.json | 2 + .../src/commands/api-reports/api-extractor.ts | 40 ++---- .../src/commands/api-reports/api-reports.ts | 122 ++++++++++++------ packages/repo-tools/src/commands/index.ts | 14 +- packages/repo-tools/src/lib/paths.test.ts | 51 +++++++- packages/repo-tools/src/lib/paths.ts | 25 ++++ yarn.lock | 2 + 10 files changed, 187 insertions(+), 87 deletions(-) diff --git a/.changeset/lemon-coats-camp.md b/.changeset/lemon-coats-camp.md index a2d87476f6..ae31098e2f 100644 --- a/.changeset/lemon-coats-camp.md +++ b/.changeset/lemon-coats-camp.md @@ -4,8 +4,9 @@ Add new command options to the `api-report` -- added `--allowWarnings` to continue processing packages if some packages have warnings -- added `--omitMessages` to pass some warnings messages code to be omitted from the api-report.md files +- added `--allow-warnings`, `-a` to continue processing packages if some packages have warnings +- added `--omit-messages`, `-o` to pass some warnings messages code to be omitted from the api-report.md files +- added `--paths`, `-p` to select packages path to process - The `paths` argument for this command now takes as default the value on `workspaces.packages` inside the root package.json -- The `paths` argument now allow glob patterns +- Removed the `paths` argument replaced by the option `--paths` - change the path resolution to use the `@backstage/cli-common` packages instead diff --git a/package.json b/package.json index 497a652cdf..5182436762 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "build:backend": "yarn workspace backend build", "build:all": "backstage-cli repo build --all", "build:api-reports": "yarn build:api-reports:only --tsc", - "build:api-reports:only": "backstage-repo-tools api-reports --allow-warnings packages/core-components plugins/catalog plugins/catalog-import plugins/git-release-manager plugins/jenkins plugins/kubernetes", + "build:api-reports:only": "backstage-repo-tools api-reports --allow-warnings 'packages/core-components,plugins/+(catalog|catalog-import|git-release-manager|jenkins|kubernetes)'", "build:api-docs": "LANG=en_EN yarn build:api-reports --docs", "tsc": "tsc", "tsc:full": "backstage-cli repo clean && tsc --skipLibCheck false --incremental false", diff --git a/packages/repo-tools/cli-report.md b/packages/repo-tools/cli-report.md index ba15ecc674..d3446e0262 100644 --- a/packages/repo-tools/cli-report.md +++ b/packages/repo-tools/cli-report.md @@ -12,7 +12,7 @@ Options: -h, --help Commands: - api-reports [options] [paths...] + api-reports [options] type-deps help [command] ``` @@ -20,14 +20,15 @@ Commands: ### `backstage-repo-tools api-reports` ``` -Usage: backstage-repo-tools api-reports [options] [paths...] +Usage: backstage-repo-tools api-reports [options] Options: + -p --paths [paths...] --ci --tsc --docs - --allow-warnings [allowWarningsPaths...] - --omitMessages + -a, --allow-warnings [allowWarningsPaths...] + -o, --omit-messages -h, --help ``` diff --git a/packages/repo-tools/package.json b/packages/repo-tools/package.json index 9b51d35922..6025af9bce 100644 --- a/packages/repo-tools/package.json +++ b/packages/repo-tools/package.json @@ -40,7 +40,9 @@ "chalk": "^4.0.0", "commander": "^9.1.0", "fs-extra": "10.1.0", + "glob": "^8.0.3", "is-glob": "^4.0.3", + "minimatch": "^5.1.1", "ts-node": "^10.0.0" }, "devDependencies": { diff --git a/packages/repo-tools/src/commands/api-reports/api-extractor.ts b/packages/repo-tools/src/commands/api-reports/api-extractor.ts index b2fab40ca5..46f4ea984b 100644 --- a/packages/repo-tools/src/commands/api-reports/api-extractor.ts +++ b/packages/repo-tools/src/commands/api-reports/api-extractor.ts @@ -65,14 +65,9 @@ import { } from '@microsoft/api-documenter/lib/markdown/CustomMarkdownEmitter'; import { IMarkdownEmitterContext } from '@microsoft/api-documenter/lib/markdown/MarkdownEmitter'; import { AstDeclaration } from '@microsoft/api-extractor/lib/analyzer/AstDeclaration'; -import { paths as cliPaths, resolvePackagePath } from '../../lib/paths'; +import { paths as cliPaths } from '../../lib/paths'; -import g from 'glob'; -import isGlob from 'is-glob'; - -import { promisify } from 'util'; - -const glob = promisify(g); +import minimatch from 'minimatch'; const tmpDir = cliPaths.resolveTargetRoot( './node_modules/.cache/api-extractor', @@ -226,24 +221,6 @@ ApiReportGenerator.generateReviewFileContent = }); }; -export async function findPackageDirs(selectedPaths: string[]) { - const packageDirs = new Array(); - for (const packageRoot of selectedPaths) { - const fullPath = cliPaths.resolveTargetRoot(packageRoot); - - // if the path contain any glob notation we resolve all the paths to process one by one - const dirs = isGlob(fullPath) ? await glob(fullPath) : [fullPath]; - for (const dir of dirs) { - const packageDir = await resolvePackagePath(dir); - if (!packageDir) { - continue; - } - packageDirs.push(packageDir); - } - } - return packageDirs; -} - export async function createTemporaryTsConfig(includedPackageDirs: string[]) { const path = cliPaths.resolveTargetRoot('tsconfig.tmp.json'); @@ -331,7 +308,7 @@ interface ApiExtractionOptions { outputDir: string; isLocalBuild: boolean; tsconfigFilePath: string; - allowWarnings: boolean | string[]; + allowWarnings?: boolean | string[]; omitMessages?: string[]; } @@ -340,7 +317,7 @@ export async function runApiExtraction({ outputDir, isLocalBuild, tsconfigFilePath, - allowWarnings, + allowWarnings = false, omitMessages = [], }: ApiExtractionOptions) { await fs.remove(outputDir); @@ -366,7 +343,7 @@ export async function runApiExtraction({ for (const packageDir of packageDirs) { console.log(`## Processing ${packageDir}`); const noBail = Array.isArray(allowWarnings) - ? allowWarnings.includes(packageDir) + ? allowWarnings.some(aw => aw === packageDir || minimatch(packageDir, aw)) : allowWarnings; const projectFolder = cliPaths.resolveTargetRoot(packageDir); @@ -514,6 +491,9 @@ export async function runApiExtraction({ } const warningCountAfter = await countApiReportWarnings(projectFolder); + if (noBail) { + console.log(`Skipping warnings check for ${packageDir}`); + } if (warningCountAfter > 0 && !noBail) { throw new Error( `The API Report for ${packageDir} is not allowed to have warnings`, @@ -1289,13 +1269,11 @@ function generateCliReport(name: string, models: CliModel[]): string { } interface CliExtractionOptions { - projectRoot: string; packageDirs: string[]; isLocalBuild: boolean; } export async function runCliExtraction({ - projectRoot, packageDirs, isLocalBuild, }: CliExtractionOptions) { @@ -1344,7 +1322,7 @@ export async function runCliExtraction({ console.log(''); console.log( `The conflicting file is ${relativePath( - projectRoot, + cliPaths.targetRoot, reportPath, )}, expecting the following content:`, ); diff --git a/packages/repo-tools/src/commands/api-reports/api-reports.ts b/packages/repo-tools/src/commands/api-reports/api-reports.ts index 9b0d78d6a8..11b65823c7 100644 --- a/packages/repo-tools/src/commands/api-reports/api-reports.ts +++ b/packages/repo-tools/src/commands/api-reports/api-reports.ts @@ -15,49 +15,42 @@ */ import { OptionValues } from 'commander'; -import { resolve as resolvePath } from 'path'; import fs from 'fs-extra'; import { spawnSync } from 'child_process'; import { createTemporaryTsConfig, - findPackageDirs, categorizePackageDirs, runApiExtraction, runCliExtraction, buildDocs, } from './api-extractor'; -import { paths as cliPaths } from '../../lib/paths'; +import { findPackageDirs, paths as cliPaths } from '../../lib/paths'; -export default async (paths: string[], opts: OptionValues) => { - const tmpDir = resolvePath( - cliPaths.targetRoot, +export default async (opts: OptionValues) => { + const tmpDir = cliPaths.resolveTargetRoot( './node_modules/.cache/api-extractor', ); - const projectRoot = resolvePath(cliPaths.targetRoot); const isCiBuild = opts.ci; const isDocsBuild = opts.docs; const runTsc = opts.tsc; - const selectedPaths = paths.length ? paths : await getWorkspacePkgs(); - const allowWarnings: boolean | string[] = opts.allowWarnings; - const omitMessages = opts.omitMessages; + const parsedPaths = parseArrayOption(opts.paths); + const isAllPackages = !Array.isArray(parsedPaths) || !parsedPaths?.length; + const selectedPaths = isAllPackages ? await getWorkspacePkgs() : parsedPaths; const selectedPackageDirs = await findPackageDirs(selectedPaths); - if (paths.length && isCiBuild) { - // TODO @sarabadu we can remove this validation to allow `/plugins/*` on CI?? - throw new Error( - 'Package path arguments are not supported together with the --ci flag', - ); - } - if (!paths.length && !isCiBuild && !isDocsBuild) { + const allowWarnings = parseArrayOption(opts.allowWarnings); + const omitMessages = parseArrayOption(opts.omitMessages); + + if (isAllPackages && !isCiBuild && !isDocsBuild) { console.log(''); console.log( 'TIP: You can generate api-reports for select packages by passing package paths:', ); console.log(''); console.log( - ' yarn build:api-reports packages/config packages/core-plugin-api plugins/*', + ' yarn build:api-reports -p packages/config -p packages/core-plugin-api,plugins/*', ); console.log(''); } @@ -67,27 +60,11 @@ export default async (paths: string[], opts: OptionValues) => { temporaryTsConfigPath = await createTemporaryTsConfig(selectedPackageDirs); } const tsconfigFilePath = - temporaryTsConfigPath ?? resolvePath(projectRoot, 'tsconfig.json'); + temporaryTsConfigPath ?? cliPaths.resolveTargetRoot('tsconfig.json'); if (runTsc) { - await fs.remove(resolvePath(projectRoot, 'dist-types')); - const { status } = spawnSync( - 'yarn', - [ - 'tsc', - ['--project', tsconfigFilePath], - ['--skipLibCheck', 'false'], - ['--incremental', 'false'], - ].flat(), - { - stdio: 'inherit', - shell: true, - cwd: projectRoot, - }, - ); - if (status !== 0) { - process.exit(status || undefined); - } + console.log('# Compiling TypeScript'); + await generateTSC(tsconfigFilePath); } const { tsPackageDirs, cliPackageDirs } = await categorizePackageDirs( @@ -102,13 +79,12 @@ export default async (paths: string[], opts: OptionValues) => { isLocalBuild: !isCiBuild, tsconfigFilePath, allowWarnings, - omitMessages, + omitMessages: Array.isArray(omitMessages) ? omitMessages : [], }); } if (cliPackageDirs.length > 0) { console.log('# Generating package CLI reports'); await runCliExtraction({ - projectRoot, packageDirs: cliPackageDirs, isLocalBuild: !isCiBuild, }); @@ -118,10 +94,49 @@ export default async (paths: string[], opts: OptionValues) => { console.log('# Generating package documentation'); await buildDocs({ inputDir: tmpDir, - outputDir: resolvePath(projectRoot, 'docs/reference'), + outputDir: cliPaths.resolveTargetRoot('docs/reference'), }); } }; + +/** + * Generates the TypeScript declaration files for the specified project, using the provided `tsconfig.json` file. + * + * Any existing declaration files in the `dist-types` directory will be deleted before generating the new ones. + * + * If the `tsc` command exits with a non-zero exit code, the process will be terminated with the same exit code. + * + * @param tsconfigFilePath {string} The path to the `tsconfig.json` file to use for generating the declaration files. + * @returns {Promise} A promise that resolves when the declaration files have been generated. + */ +export async function generateTSC(tsconfigFilePath: string) { + await fs.remove(cliPaths.resolveTargetRoot('dist-types')); + const { status } = spawnSync( + 'yarn', + [ + 'tsc', + ['--project', tsconfigFilePath], + ['--skipLibCheck', 'false'], + ['--incremental', 'false'], + ].flat(), + { + stdio: 'inherit', + shell: true, + cwd: cliPaths.targetRoot, + }, + ); + if (status !== 0) { + process.exit(status || undefined); + } +} + +/** + * Retrieves the list of package names in the "workspaces" field of the `package.json` file in the current workspace root. + * + * If the file does not exist, or the "workspaces" field is not present, returns `undefined`. + * + * @returns {Promise} The list of package names, or `undefined` if not found. + */ async function getWorkspacePkgs() { const pkgJson = await fs .readJson(cliPaths.resolveTargetRoot('package.json')) @@ -134,3 +149,30 @@ async function getWorkspacePkgs() { const workspaces = pkgJson?.workspaces?.packages; return workspaces; } + +/** + * Splits each string in the input array on comma, and returns an array of the resulting substrings. + * If the input array is `undefined`, returns `undefined`. If the input value is `true` or `false`, + * returns the value as-is. + * + * @param value An array of strings to be split on comma, or a boolean value (inherithed from commanderjs array args). + * @returns An array of the resulting substrings, the original boolean value, or `undefined` if the input value is `undefined`. + * + * @example + * parseOption(['foo,bar,baz']) + * // returns ['foo', 'bar', 'baz'] + * + * parseOption(true) + * // returns true + * + * parseOption() + * // returns undefined + */ +function parseArrayOption(value: string[] | boolean | undefined) { + if (typeof value === 'boolean') { + return value; + } + return value?.flatMap((str: string) => + str.includes(',') ? str.split(',') : str, + ); +} diff --git a/packages/repo-tools/src/commands/index.ts b/packages/repo-tools/src/commands/index.ts index bbc6773f83..5742ce7241 100644 --- a/packages/repo-tools/src/commands/index.ts +++ b/packages/repo-tools/src/commands/index.ts @@ -21,21 +21,21 @@ import { exitWithError } from '../lib/errors'; export function registerCommands(program: Command) { program .command('api-reports') - .argument( - '[paths...]', - 'path of package folder to extract API reports, `workspaces.packages` from root packages.json by default', + .option( + '-p --paths [paths...]', + 'paths of package folder to extract API reports, `workspaces.packages` from root packages.json by default. Allows glob patterns and comma separated values', ) .option('--ci', 'CI run checks that there is no changes on API reports') .option('--tsc', 'executes the tsc compilation before extracting the APIs') .option('--docs', 'generates the api documentation') .option( - '--allow-warnings [allowWarningsPaths...]', - 'continue processing packages after getting errors on selected packages', + '-a, --allow-warnings [allowWarningsPaths...]', + 'continue processing packages after getting errors on selected packages Allows glob patterns and comma separated values (i.e. packages/core,plugins/core-*)', false, ) .option( - '--omitMessages ', - 'select some message code to be omited on the API Extractor (i.e ae-cyclic-inherit-doc)', + '-o, --omit-messages ', + 'select some message code to be omited on the API Extractor (comma separated values i.e ae-cyclic-inherit-doc,ae-missing-getter )', ) .description('Generate an API report for selected packages') .action( diff --git a/packages/repo-tools/src/lib/paths.test.ts b/packages/repo-tools/src/lib/paths.test.ts index 90d84aec3c..c642900612 100644 --- a/packages/repo-tools/src/lib/paths.test.ts +++ b/packages/repo-tools/src/lib/paths.test.ts @@ -16,7 +16,7 @@ import mockFs from 'mock-fs'; import { resolve as resolvePath } from 'path'; -import { resolvePackagePath, paths } from './paths'; +import { resolvePackagePath, paths, findPackageDirs } from './paths'; describe('paths', () => { jest.spyOn(paths, 'targetRoot', 'get').mockReturnValue('/root'); @@ -38,6 +38,14 @@ describe('paths', () => { 'package-c': {}, 'README.md': 'Hello World', }, + plugins: { + 'plugin-a': { + 'package.json': '{}', + }, + 'plugin-b': { + 'package.json': '{}', + }, + }, }, }); }); @@ -59,8 +67,49 @@ describe('paths', () => { 'packages/package-b', ); }); + it('should work with absolute paths', async () => { + expect(await resolvePackagePath('/root/packages/package-a')).toBe( + 'packages/package-a', + ); + }); it('should return undefined if the pat is not a directory', async () => { expect(await resolvePackagePath('packages/README.md')).toBeUndefined(); }); }); + describe('findPackageDirs', () => { + it('should return only the given packages', async () => { + expect(await findPackageDirs(['packages/package-a'])).toEqual([ + 'packages/package-a', + ]); + }); + it('should return only the given packages when using glob patterns', async () => { + expect(await findPackageDirs(['packages/*'])).toEqual([ + 'packages/package-a', + 'packages/package-b', + ]); + expect(await findPackageDirs(['packages/*', 'plugins/*'])).toEqual([ + 'packages/package-a', + 'packages/package-b', + 'plugins/plugin-a', + 'plugins/plugin-b', + ]); + }); + it('should return only the given packages when using absolute paths', async () => { + expect( + await findPackageDirs([ + '/root/packages/package-a', + '/root/plugins/plugin-b', + ]), + ).toEqual(['packages/package-a', 'plugins/plugin-b']); + }); + it('should return only the given packages when using absolute paths with glob patterns', async () => { + expect( + await findPackageDirs(['/root/packages/*', '/root/plugins/*-a']), + ).toEqual([ + 'packages/package-a', + 'packages/package-b', + 'plugins/plugin-a', + ]); + }); + }); }); diff --git a/packages/repo-tools/src/lib/paths.ts b/packages/repo-tools/src/lib/paths.ts index f387e9be51..874bb48ea1 100644 --- a/packages/repo-tools/src/lib/paths.ts +++ b/packages/repo-tools/src/lib/paths.ts @@ -18,6 +18,13 @@ import { findPaths } from '@backstage/cli-common'; import { relative as relativePath, join } from 'path'; import fs from 'fs-extra'; +import g from 'glob'; +import isGlob from 'is-glob'; + +import { promisify } from 'util'; + +const glob = promisify(g); + /* eslint-disable-next-line no-restricted-syntax */ export const paths = findPaths(__dirname); @@ -41,3 +48,21 @@ export async function resolvePackagePath( } return relativePath(paths.targetRoot, fullPackageDir); } + +export async function findPackageDirs(selectedPaths: string[] = []) { + const packageDirs = new Array(); + for (const packageRoot of selectedPaths) { + const fullPath = paths.resolveTargetRoot(packageRoot); + + // if the path contain any glob notation we resolve all the paths to process one by one + const dirs = isGlob(fullPath) ? await glob(fullPath) : [fullPath]; + for (const dir of dirs) { + const packageDir = await resolvePackagePath(dir); + if (!packageDir) { + continue; + } + packageDirs.push(packageDir); + } + } + return packageDirs; +} diff --git a/yarn.lock b/yarn.lock index e92df381ca..cc9dcb0642 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8480,7 +8480,9 @@ __metadata: chalk: ^4.0.0 commander: ^9.1.0 fs-extra: 10.1.0 + glob: ^8.0.3 is-glob: ^4.0.3 + minimatch: ^5.1.1 mock-fs: ^5.1.0 ts-node: ^10.0.0 bin: From e5dc85a82139b42811f59bfe31058ead200c0285 Mon Sep 17 00:00:00 2001 From: Juan Pablo Garcia Ripa Date: Tue, 6 Dec 2022 11:12:07 +0100 Subject: [PATCH 6/7] adding tests to each option Signed-off-by: Juan Pablo Garcia Ripa --- .../commands/api-reports/api-reports.test.ts | 505 ++++++++++++++++++ .../src/commands/api-reports/api-reports.ts | 47 +- .../src/commands/api-reports/generateTSC.ts | 50 ++ packages/repo-tools/src/commands/index.ts | 4 +- 4 files changed, 571 insertions(+), 35 deletions(-) create mode 100644 packages/repo-tools/src/commands/api-reports/api-reports.test.ts create mode 100644 packages/repo-tools/src/commands/api-reports/generateTSC.ts diff --git a/packages/repo-tools/src/commands/api-reports/api-reports.test.ts b/packages/repo-tools/src/commands/api-reports/api-reports.test.ts new file mode 100644 index 0000000000..fa5bfd6973 --- /dev/null +++ b/packages/repo-tools/src/commands/api-reports/api-reports.test.ts @@ -0,0 +1,505 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import mockFs from 'mock-fs'; +import { resolve as resolvePath } from 'path'; +import * as pathsLib from '../../lib/paths'; + +import { + buildDocs, + runCliExtraction, + runApiExtraction, + categorizePackageDirs, +} from './api-extractor'; + +import { buildApiReports } from './api-reports'; +import { generateTSC } from './generateTSC'; + +jest.mock('./generateTSC'); +// create mocks for the dependencies of the `buildApiReports` function +jest.mock('./api-extractor', () => ({ + createTemporaryTsConfig: jest.fn(), + categorizePackageDirs: jest.fn().mockImplementation(async (p: string[]) => { + console.log('categorizePackageDirs', p); + return { + tsPackageDirs: p, + cliPackageDirs: p, + }; + }), + runApiExtraction: jest.fn(), + runCliExtraction: jest.fn(), + buildDocs: jest.fn(), +})); + +const paths = pathsLib.paths; + +jest.spyOn(paths, 'targetRoot', 'get').mockReturnValue('/root'); +jest.spyOn(paths, 'resolveTargetRoot').mockImplementation((...path) => { + return resolvePath('/root', ...path); +}); + +describe('buildApiReports', () => { + beforeEach(() => { + mockFs({ + [paths.targetRoot]: { + 'package.json': JSON.stringify({ + workspaces: { packages: ['packages/*', 'plugins/*'] }, + }), + packages: { + 'package-a': { + 'package.json': '{}', + }, + 'package-b': { + 'package.json': '{}', + }, + 'package-c': {}, + 'README.md': 'Hello World', + }, + plugins: { + 'plugin-a': { + 'package.json': '{}', + }, + 'plugin-b': { + 'package.json': '{}', + }, + 'plugin-c': { + 'package.json': '{}', + }, + }, + }, + }); + }); + + afterEach(() => { + mockFs.restore(); + jest.clearAllMocks(); + }); + it('should run whitout any options', async () => { + const opts = {}; + + await buildApiReports(opts); + + expect(categorizePackageDirs).toHaveBeenCalledWith([ + 'packages/package-a', + 'packages/package-b', + 'plugins/plugin-a', + 'plugins/plugin-b', + 'plugins/plugin-c', + ]); + + expect(generateTSC).not.toHaveBeenCalled(); + expect(runApiExtraction).toHaveBeenCalledWith({ + packageDirs: [ + 'packages/package-a', + 'packages/package-b', + 'plugins/plugin-a', + 'plugins/plugin-b', + 'plugins/plugin-c', + ], + tsconfigFilePath: '/root/tsconfig.json', + allowWarnings: undefined, + omitMessages: [], + isLocalBuild: true, + outputDir: '/root/node_modules/.cache/api-extractor', + }); + expect(runCliExtraction).toHaveBeenCalledWith({ + packageDirs: [ + 'packages/package-a', + 'packages/package-b', + 'plugins/plugin-a', + 'plugins/plugin-b', + + 'plugins/plugin-c', + ], + isLocalBuild: true, + }); + + expect(buildDocs).not.toHaveBeenCalled(); + }); + + describe('paths', () => { + it('should generate API reports for one specific package', async () => { + const opts = { + paths: ['packages/package-a'], + }; + + await buildApiReports(opts); + + expect(categorizePackageDirs).toHaveBeenCalledWith([ + 'packages/package-a', + ]); + + expect(runApiExtraction).toHaveBeenCalledWith({ + packageDirs: ['packages/package-a'], + tsconfigFilePath: '/root/tsconfig.json', + allowWarnings: undefined, + omitMessages: [], + isLocalBuild: true, + outputDir: '/root/node_modules/.cache/api-extractor', + }); + expect(runCliExtraction).toHaveBeenCalledWith({ + packageDirs: ['packages/package-a'], + isLocalBuild: true, + }); + + expect(buildDocs).not.toHaveBeenCalled(); + }); + it('should generate API reports for multiple specific packages', async () => { + const opts = { + paths: ['packages/package-a', 'packages/package-b'], + }; + + await buildApiReports(opts); + + expect(categorizePackageDirs).toHaveBeenCalledWith([ + 'packages/package-a', + 'packages/package-b', + ]); + + expect(runApiExtraction).toHaveBeenCalledWith({ + packageDirs: ['packages/package-a', 'packages/package-b'], + tsconfigFilePath: '/root/tsconfig.json', + allowWarnings: undefined, + omitMessages: [], + isLocalBuild: true, + outputDir: '/root/node_modules/.cache/api-extractor', + }); + expect(runCliExtraction).toHaveBeenCalledWith({ + packageDirs: ['packages/package-a', 'packages/package-b'], + isLocalBuild: true, + }); + + expect(buildDocs).not.toHaveBeenCalled(); + }); + it('should generate API reports for all packages matching the glob pattern', async () => { + const opts = { + paths: ['packages/*'], + }; + + await buildApiReports(opts); + + expect(categorizePackageDirs).toHaveBeenCalledWith([ + 'packages/package-a', + 'packages/package-b', + ]); + + expect(runApiExtraction).toHaveBeenCalledWith({ + packageDirs: ['packages/package-a', 'packages/package-b'], + tsconfigFilePath: '/root/tsconfig.json', + allowWarnings: undefined, + omitMessages: [], + isLocalBuild: true, + outputDir: '/root/node_modules/.cache/api-extractor', + }); + expect(runCliExtraction).toHaveBeenCalledWith({ + packageDirs: ['packages/package-a', 'packages/package-b'], + isLocalBuild: true, + }); + + expect(buildDocs).not.toHaveBeenCalled(); + }); + + it('should generate API reports for all packages matching multiple glob patterns', async () => { + const opts = { + paths: ['packages/*', 'plugins/*a'], + }; + + await buildApiReports(opts); + + expect(categorizePackageDirs).toHaveBeenCalledWith([ + 'packages/package-a', + 'packages/package-b', + 'plugins/plugin-a', + ]); + + expect(runApiExtraction).toHaveBeenCalledWith({ + packageDirs: [ + 'packages/package-a', + 'packages/package-b', + 'plugins/plugin-a', + ], + tsconfigFilePath: '/root/tsconfig.json', + allowWarnings: undefined, + omitMessages: [], + isLocalBuild: true, + outputDir: '/root/node_modules/.cache/api-extractor', + }); + expect(runCliExtraction).toHaveBeenCalledWith({ + packageDirs: [ + 'packages/package-a', + 'packages/package-b', + 'plugins/plugin-a', + ], + isLocalBuild: true, + }); + + expect(buildDocs).not.toHaveBeenCalled(); + }); + + it('should generate API reports for specific packages and glob pattern', async () => { + const opts = { + paths: ['packages/package-a', 'plugins/*'], + }; + + await buildApiReports(opts); + + expect(categorizePackageDirs).toHaveBeenCalledWith([ + 'packages/package-a', + 'plugins/plugin-a', + 'plugins/plugin-b', + 'plugins/plugin-c', + ]); + + expect(runApiExtraction).toHaveBeenCalledWith({ + packageDirs: [ + 'packages/package-a', + 'plugins/plugin-a', + 'plugins/plugin-b', + 'plugins/plugin-c', + ], + tsconfigFilePath: '/root/tsconfig.json', + allowWarnings: undefined, + omitMessages: [], + isLocalBuild: true, + outputDir: '/root/node_modules/.cache/api-extractor', + }); + expect(runCliExtraction).toHaveBeenCalledWith({ + packageDirs: [ + 'packages/package-a', + 'plugins/plugin-a', + 'plugins/plugin-b', + 'plugins/plugin-c', + ], + isLocalBuild: true, + }); + + expect(buildDocs).not.toHaveBeenCalled(); + }); + }); + describe('allowWarnings', () => { + it('should accept boolean values', async () => { + const opts = { + paths: ['packages/*'], + allowWarnings: true, + }; + + await buildApiReports(opts); + + expect(runApiExtraction).toHaveBeenCalledWith({ + packageDirs: ['packages/package-a', 'packages/package-b'], + tsconfigFilePath: '/root/tsconfig.json', + allowWarnings: true, + omitMessages: [], + isLocalBuild: true, + outputDir: '/root/node_modules/.cache/api-extractor', + }); + }); + + it('should accept single path value', async () => { + const opts = { + paths: ['packages/*'], + allowWarnings: ['packages/package-a'], + }; + + await buildApiReports(opts); + + expect(runApiExtraction).toHaveBeenCalledWith({ + packageDirs: ['packages/package-a', 'packages/package-b'], + tsconfigFilePath: '/root/tsconfig.json', + allowWarnings: ['packages/package-a'], + omitMessages: [], + isLocalBuild: true, + outputDir: '/root/node_modules/.cache/api-extractor', + }); + }); + + it('should accept multiple path values as array', async () => { + const opts = { + paths: ['packages/*'], + allowWarnings: ['packages/package-a', 'packages/package-b'], + }; + + await buildApiReports(opts); + + expect(runApiExtraction).toHaveBeenCalledWith({ + packageDirs: ['packages/package-a', 'packages/package-b'], + tsconfigFilePath: '/root/tsconfig.json', + allowWarnings: ['packages/package-a', 'packages/package-b'], + omitMessages: [], + isLocalBuild: true, + outputDir: '/root/node_modules/.cache/api-extractor', + }); + }); + + it('should accept multiple path values as comma separated string', async () => { + const opts = { + paths: ['packages/*'], + allowWarnings: ['packages/package-a,packages/package-b'], + }; + + await buildApiReports(opts); + + expect(runApiExtraction).toHaveBeenCalledWith({ + packageDirs: ['packages/package-a', 'packages/package-b'], + tsconfigFilePath: '/root/tsconfig.json', + allowWarnings: ['packages/package-a', 'packages/package-b'], + omitMessages: [], + isLocalBuild: true, + outputDir: '/root/node_modules/.cache/api-extractor', + }); + }); + + it('should accept multiple path values as comma separated string with spaces', async () => { + const opts = { + paths: ['packages/*'], + allowWarnings: ['packages/package-a, packages/package-b'], + }; + + await buildApiReports(opts); + + expect(runApiExtraction).toHaveBeenCalledWith({ + packageDirs: ['packages/package-a', 'packages/package-b'], + tsconfigFilePath: '/root/tsconfig.json', + allowWarnings: ['packages/package-a', 'packages/package-b'], + omitMessages: [], + isLocalBuild: true, + outputDir: '/root/node_modules/.cache/api-extractor', + }); + }); + }); + describe('omitMessages', () => { + it('should accept single message value', async () => { + const opts = { + paths: ['packages/*'], + omitMessages: ['ae-missing-release-tag'], + }; + + await buildApiReports(opts); + + expect(runApiExtraction).toHaveBeenCalledWith({ + packageDirs: ['packages/package-a', 'packages/package-b'], + tsconfigFilePath: '/root/tsconfig.json', + allowWarnings: undefined, + omitMessages: ['ae-missing-release-tag'], + isLocalBuild: true, + outputDir: '/root/node_modules/.cache/api-extractor', + }); + }); + + it('should accept multiple message values as array', async () => { + const opts = { + paths: ['packages/*'], + omitMessages: ['ae-missing-release-tag', 'ae-missing-annotations'], + }; + + await buildApiReports(opts); + + expect(runApiExtraction).toHaveBeenCalledWith({ + packageDirs: ['packages/package-a', 'packages/package-b'], + tsconfigFilePath: '/root/tsconfig.json', + allowWarnings: undefined, + omitMessages: ['ae-missing-release-tag', 'ae-missing-annotations'], + isLocalBuild: true, + outputDir: '/root/node_modules/.cache/api-extractor', + }); + }); + it('should accept multiple message values as comma separated string', async () => { + const opts = { + paths: ['packages/*'], + omitMessages: ['ae-missing-release-tag,ae-missing-annotations'], + }; + + await buildApiReports(opts); + + expect(runApiExtraction).toHaveBeenCalledWith({ + packageDirs: ['packages/package-a', 'packages/package-b'], + tsconfigFilePath: '/root/tsconfig.json', + allowWarnings: undefined, + omitMessages: ['ae-missing-release-tag', 'ae-missing-annotations'], + isLocalBuild: true, + outputDir: '/root/node_modules/.cache/api-extractor', + }); + }); + + it('should accept multiple message values as comma separated string with spaces', async () => { + const opts = { + paths: ['packages/*'], + omitMessages: ['ae-missing-release-tag, ae-missing-annotations'], + }; + + await buildApiReports(opts); + + expect(runApiExtraction).toHaveBeenCalledWith({ + packageDirs: ['packages/package-a', 'packages/package-b'], + tsconfigFilePath: '/root/tsconfig.json', + allowWarnings: undefined, + omitMessages: ['ae-missing-release-tag', 'ae-missing-annotations'], + isLocalBuild: true, + outputDir: '/root/node_modules/.cache/api-extractor', + }); + }); + }); + describe('isCI', () => { + it('should set localBuild to false if CI option is passed', async () => { + const opts = { + paths: ['packages/*'], + ci: true, + }; + + await buildApiReports(opts); + + expect(runApiExtraction).toHaveBeenCalledWith({ + packageDirs: ['packages/package-a', 'packages/package-b'], + tsconfigFilePath: '/root/tsconfig.json', + allowWarnings: undefined, + omitMessages: [], + isLocalBuild: false, + outputDir: '/root/node_modules/.cache/api-extractor', + }); + expect(runCliExtraction).toHaveBeenCalledWith({ + packageDirs: ['packages/package-a', 'packages/package-b'], + isLocalBuild: false, + }); + }); + }); + describe('docs', () => { + it('should run typedoc if docs option is passed', async () => { + const opts = { + paths: ['packages/*'], + docs: true, + }; + + await buildApiReports(opts); + + expect(buildDocs).toHaveBeenCalledWith({ + inputDir: '/root/node_modules/.cache/api-extractor', + outputDir: '/root/docs/reference', + }); + }); + }); + describe('tsc', () => { + it('should run tsc if tsc option is passed', async () => { + const opts = { + paths: ['packages/*'], + tsc: true, + }; + + await buildApiReports(opts); + + expect(generateTSC).toHaveBeenCalled(); + }); + }); +}); diff --git a/packages/repo-tools/src/commands/api-reports/api-reports.ts b/packages/repo-tools/src/commands/api-reports/api-reports.ts index 11b65823c7..b59f498d7b 100644 --- a/packages/repo-tools/src/commands/api-reports/api-reports.ts +++ b/packages/repo-tools/src/commands/api-reports/api-reports.ts @@ -16,7 +16,6 @@ import { OptionValues } from 'commander'; import fs from 'fs-extra'; -import { spawnSync } from 'child_process'; import { createTemporaryTsConfig, categorizePackageDirs, @@ -25,8 +24,18 @@ import { buildDocs, } from './api-extractor'; import { findPackageDirs, paths as cliPaths } from '../../lib/paths'; +import { generateTSC } from './generateTSC'; -export default async (opts: OptionValues) => { +type Options = { + ci?: boolean; + docs?: boolean; + tsc?: boolean; + paths?: string[]; + allowWarnings?: string[] | boolean; + omitMessages?: string[]; +} & OptionValues; + +export const buildApiReports = async (opts: Options) => { const tmpDir = cliPaths.resolveTargetRoot( './node_modules/.cache/api-extractor', ); @@ -90,6 +99,7 @@ export default async (opts: OptionValues) => { }); } + console.log(isDocsBuild); if (isDocsBuild) { console.log('# Generating package documentation'); await buildDocs({ @@ -99,37 +109,6 @@ export default async (opts: OptionValues) => { } }; -/** - * Generates the TypeScript declaration files for the specified project, using the provided `tsconfig.json` file. - * - * Any existing declaration files in the `dist-types` directory will be deleted before generating the new ones. - * - * If the `tsc` command exits with a non-zero exit code, the process will be terminated with the same exit code. - * - * @param tsconfigFilePath {string} The path to the `tsconfig.json` file to use for generating the declaration files. - * @returns {Promise} A promise that resolves when the declaration files have been generated. - */ -export async function generateTSC(tsconfigFilePath: string) { - await fs.remove(cliPaths.resolveTargetRoot('dist-types')); - const { status } = spawnSync( - 'yarn', - [ - 'tsc', - ['--project', tsconfigFilePath], - ['--skipLibCheck', 'false'], - ['--incremental', 'false'], - ].flat(), - { - stdio: 'inherit', - shell: true, - cwd: cliPaths.targetRoot, - }, - ); - if (status !== 0) { - process.exit(status || undefined); - } -} - /** * Retrieves the list of package names in the "workspaces" field of the `package.json` file in the current workspace root. * @@ -173,6 +152,6 @@ function parseArrayOption(value: string[] | boolean | undefined) { return value; } return value?.flatMap((str: string) => - str.includes(',') ? str.split(',') : str, + str.includes(',') ? str.split(',').map(s => s.trim()) : str, ); } diff --git a/packages/repo-tools/src/commands/api-reports/generateTSC.ts b/packages/repo-tools/src/commands/api-reports/generateTSC.ts new file mode 100644 index 0000000000..5c10c5085f --- /dev/null +++ b/packages/repo-tools/src/commands/api-reports/generateTSC.ts @@ -0,0 +1,50 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import fs from 'fs-extra'; +import { spawnSync } from 'child_process'; +import { paths as cliPaths } from '../../lib/paths'; + +/** + * Generates the TypeScript declaration files for the specified project, using the provided `tsconfig.json` file. + * + * Any existing declaration files in the `dist-types` directory will be deleted before generating the new ones. + * + * If the `tsc` command exits with a non-zero exit code, the process will be terminated with the same exit code. + * + * @param tsconfigFilePath {string} The path to the `tsconfig.json` file to use for generating the declaration files. + * @returns {Promise} A promise that resolves when the declaration files have been generated. + */ + +export async function generateTSC(tsconfigFilePath: string) { + await fs.remove(cliPaths.resolveTargetRoot('dist-types')); + const { status } = spawnSync( + 'yarn', + [ + 'tsc', + ['--project', tsconfigFilePath], + ['--skipLibCheck', 'false'], + ['--incremental', 'false'], + ].flat(), + { + stdio: 'inherit', + shell: true, + cwd: cliPaths.targetRoot, + }, + ); + if (status !== 0) { + process.exit(status || undefined); + } +} diff --git a/packages/repo-tools/src/commands/index.ts b/packages/repo-tools/src/commands/index.ts index 5742ce7241..3f3b2cdbe7 100644 --- a/packages/repo-tools/src/commands/index.ts +++ b/packages/repo-tools/src/commands/index.ts @@ -39,7 +39,9 @@ export function registerCommands(program: Command) { ) .description('Generate an API report for selected packages') .action( - lazy(() => import('./api-reports/api-reports').then(m => m.default)), + lazy(() => + import('./api-reports/api-reports').then(m => m.buildApiReports), + ), ); program From 808b53d492679d5e4e83932e6397438c56cb5f55 Mon Sep 17 00:00:00 2001 From: Juan Pablo Garcia Ripa Date: Wed, 7 Dec 2022 11:43:21 +0100 Subject: [PATCH 7/7] change the options to only allow csv Signed-off-by: Juan Pablo Garcia Ripa --- .changeset/lemon-coats-camp.md | 7 +- packages/repo-tools/cli-report.md | 10 +- .../commands/api-reports/api-reports.test.ts | 194 +++++++----------- .../src/commands/api-reports/api-reports.ts | 58 +++--- ...rateTSC.ts => generateTypeDeclarations.ts} | 2 +- packages/repo-tools/src/commands/index.ts | 14 +- 6 files changed, 120 insertions(+), 165 deletions(-) rename packages/repo-tools/src/commands/api-reports/{generateTSC.ts => generateTypeDeclarations.ts} (95%) diff --git a/.changeset/lemon-coats-camp.md b/.changeset/lemon-coats-camp.md index ae31098e2f..f30a20f432 100644 --- a/.changeset/lemon-coats-camp.md +++ b/.changeset/lemon-coats-camp.md @@ -1,12 +1,11 @@ --- -'@backstage/repo-tools': minor +'@backstage/repo-tools': patch --- Add new command options to the `api-report` -- added `--allow-warnings`, `-a` to continue processing packages if some packages have warnings +- added `--allow-warnings`, `-a` to continue processing packages if selected packages have warnings +- added `--allow-all-warnings` to continue processing packages any packages have warnings - added `--omit-messages`, `-o` to pass some warnings messages code to be omitted from the api-report.md files -- added `--paths`, `-p` to select packages path to process - The `paths` argument for this command now takes as default the value on `workspaces.packages` inside the root package.json -- Removed the `paths` argument replaced by the option `--paths` - change the path resolution to use the `@backstage/cli-common` packages instead diff --git a/packages/repo-tools/cli-report.md b/packages/repo-tools/cli-report.md index d3446e0262..3378d20ea7 100644 --- a/packages/repo-tools/cli-report.md +++ b/packages/repo-tools/cli-report.md @@ -12,7 +12,7 @@ Options: -h, --help Commands: - api-reports [options] + api-reports [options] [paths...] type-deps help [command] ``` @@ -20,15 +20,15 @@ Commands: ### `backstage-repo-tools api-reports` ``` -Usage: backstage-repo-tools api-reports [options] +Usage: backstage-repo-tools api-reports [options] [paths...] Options: - -p --paths [paths...] --ci --tsc --docs - -a, --allow-warnings [allowWarningsPaths...] - -o, --omit-messages + -a, --allow-warnings + --allow-all-warnings + -o, --omit-messages -h, --help ``` diff --git a/packages/repo-tools/src/commands/api-reports/api-reports.test.ts b/packages/repo-tools/src/commands/api-reports/api-reports.test.ts index fa5bfd6973..7121d8a06f 100644 --- a/packages/repo-tools/src/commands/api-reports/api-reports.test.ts +++ b/packages/repo-tools/src/commands/api-reports/api-reports.test.ts @@ -26,9 +26,9 @@ import { } from './api-extractor'; import { buildApiReports } from './api-reports'; -import { generateTSC } from './generateTSC'; +import { generateTypeDeclarations } from './generateTypeDeclarations'; -jest.mock('./generateTSC'); +jest.mock('./generateTypeDeclarations'); // create mocks for the dependencies of the `buildApiReports` function jest.mock('./api-extractor', () => ({ createTemporaryTsConfig: jest.fn(), @@ -44,17 +44,17 @@ jest.mock('./api-extractor', () => ({ buildDocs: jest.fn(), })); -const paths = pathsLib.paths; +const projectPaths = pathsLib.paths; -jest.spyOn(paths, 'targetRoot', 'get').mockReturnValue('/root'); -jest.spyOn(paths, 'resolveTargetRoot').mockImplementation((...path) => { +jest.spyOn(projectPaths, 'targetRoot', 'get').mockReturnValue('/root'); +jest.spyOn(projectPaths, 'resolveTargetRoot').mockImplementation((...path) => { return resolvePath('/root', ...path); }); describe('buildApiReports', () => { beforeEach(() => { mockFs({ - [paths.targetRoot]: { + [projectPaths.targetRoot]: { 'package.json': JSON.stringify({ workspaces: { packages: ['packages/*', 'plugins/*'] }, }), @@ -89,8 +89,9 @@ describe('buildApiReports', () => { }); it('should run whitout any options', async () => { const opts = {}; + const paths: string[] = []; - await buildApiReports(opts); + await buildApiReports(paths, opts); expect(categorizePackageDirs).toHaveBeenCalledWith([ 'packages/package-a', @@ -100,7 +101,7 @@ describe('buildApiReports', () => { 'plugins/plugin-c', ]); - expect(generateTSC).not.toHaveBeenCalled(); + expect(generateTypeDeclarations).not.toHaveBeenCalled(); expect(runApiExtraction).toHaveBeenCalledWith({ packageDirs: [ 'packages/package-a', @@ -110,7 +111,7 @@ describe('buildApiReports', () => { 'plugins/plugin-c', ], tsconfigFilePath: '/root/tsconfig.json', - allowWarnings: undefined, + allowWarnings: [], omitMessages: [], isLocalBuild: true, outputDir: '/root/node_modules/.cache/api-extractor', @@ -132,11 +133,10 @@ describe('buildApiReports', () => { describe('paths', () => { it('should generate API reports for one specific package', async () => { - const opts = { - paths: ['packages/package-a'], - }; + const paths = ['packages/package-a']; + const opts = {}; - await buildApiReports(opts); + await buildApiReports(paths, opts); expect(categorizePackageDirs).toHaveBeenCalledWith([ 'packages/package-a', @@ -145,7 +145,7 @@ describe('buildApiReports', () => { expect(runApiExtraction).toHaveBeenCalledWith({ packageDirs: ['packages/package-a'], tsconfigFilePath: '/root/tsconfig.json', - allowWarnings: undefined, + allowWarnings: [], omitMessages: [], isLocalBuild: true, outputDir: '/root/node_modules/.cache/api-extractor', @@ -158,11 +158,10 @@ describe('buildApiReports', () => { expect(buildDocs).not.toHaveBeenCalled(); }); it('should generate API reports for multiple specific packages', async () => { - const opts = { - paths: ['packages/package-a', 'packages/package-b'], - }; + const paths = ['packages/package-a', 'packages/package-b']; + const opts = {}; - await buildApiReports(opts); + await buildApiReports(paths, opts); expect(categorizePackageDirs).toHaveBeenCalledWith([ 'packages/package-a', @@ -172,7 +171,7 @@ describe('buildApiReports', () => { expect(runApiExtraction).toHaveBeenCalledWith({ packageDirs: ['packages/package-a', 'packages/package-b'], tsconfigFilePath: '/root/tsconfig.json', - allowWarnings: undefined, + allowWarnings: [], omitMessages: [], isLocalBuild: true, outputDir: '/root/node_modules/.cache/api-extractor', @@ -185,11 +184,10 @@ describe('buildApiReports', () => { expect(buildDocs).not.toHaveBeenCalled(); }); it('should generate API reports for all packages matching the glob pattern', async () => { - const opts = { - paths: ['packages/*'], - }; + const paths = ['packages/*']; + const opts = {}; - await buildApiReports(opts); + await buildApiReports(paths, opts); expect(categorizePackageDirs).toHaveBeenCalledWith([ 'packages/package-a', @@ -199,7 +197,7 @@ describe('buildApiReports', () => { expect(runApiExtraction).toHaveBeenCalledWith({ packageDirs: ['packages/package-a', 'packages/package-b'], tsconfigFilePath: '/root/tsconfig.json', - allowWarnings: undefined, + allowWarnings: [], omitMessages: [], isLocalBuild: true, outputDir: '/root/node_modules/.cache/api-extractor', @@ -213,11 +211,10 @@ describe('buildApiReports', () => { }); it('should generate API reports for all packages matching multiple glob patterns', async () => { - const opts = { - paths: ['packages/*', 'plugins/*a'], - }; + const paths = ['packages/*', 'plugins/*a']; + const opts = {}; - await buildApiReports(opts); + await buildApiReports(paths, opts); expect(categorizePackageDirs).toHaveBeenCalledWith([ 'packages/package-a', @@ -232,7 +229,7 @@ describe('buildApiReports', () => { 'plugins/plugin-a', ], tsconfigFilePath: '/root/tsconfig.json', - allowWarnings: undefined, + allowWarnings: [], omitMessages: [], isLocalBuild: true, outputDir: '/root/node_modules/.cache/api-extractor', @@ -250,11 +247,10 @@ describe('buildApiReports', () => { }); it('should generate API reports for specific packages and glob pattern', async () => { - const opts = { - paths: ['packages/package-a', 'plugins/*'], - }; + const opts = {}; + const paths = ['packages/package-a', 'plugins/*']; - await buildApiReports(opts); + await buildApiReports(paths, opts); expect(categorizePackageDirs).toHaveBeenCalledWith([ 'packages/package-a', @@ -271,7 +267,7 @@ describe('buildApiReports', () => { 'plugins/plugin-c', ], tsconfigFilePath: '/root/tsconfig.json', - allowWarnings: undefined, + allowWarnings: [], omitMessages: [], isLocalBuild: true, outputDir: '/root/node_modules/.cache/api-extractor', @@ -290,31 +286,13 @@ describe('buildApiReports', () => { }); }); describe('allowWarnings', () => { - it('should accept boolean values', async () => { - const opts = { - paths: ['packages/*'], - allowWarnings: true, - }; - - await buildApiReports(opts); - - expect(runApiExtraction).toHaveBeenCalledWith({ - packageDirs: ['packages/package-a', 'packages/package-b'], - tsconfigFilePath: '/root/tsconfig.json', - allowWarnings: true, - omitMessages: [], - isLocalBuild: true, - outputDir: '/root/node_modules/.cache/api-extractor', - }); - }); - it('should accept single path value', async () => { const opts = { - paths: ['packages/*'], - allowWarnings: ['packages/package-a'], + allowWarnings: 'packages/package-a', }; + const paths = ['packages/*']; - await buildApiReports(opts); + await buildApiReports(paths, opts); expect(runApiExtraction).toHaveBeenCalledWith({ packageDirs: ['packages/package-a', 'packages/package-b'], @@ -326,31 +304,13 @@ describe('buildApiReports', () => { }); }); - it('should accept multiple path values as array', async () => { - const opts = { - paths: ['packages/*'], - allowWarnings: ['packages/package-a', 'packages/package-b'], - }; - - await buildApiReports(opts); - - expect(runApiExtraction).toHaveBeenCalledWith({ - packageDirs: ['packages/package-a', 'packages/package-b'], - tsconfigFilePath: '/root/tsconfig.json', - allowWarnings: ['packages/package-a', 'packages/package-b'], - omitMessages: [], - isLocalBuild: true, - outputDir: '/root/node_modules/.cache/api-extractor', - }); - }); - it('should accept multiple path values as comma separated string', async () => { const opts = { - paths: ['packages/*'], - allowWarnings: ['packages/package-a,packages/package-b'], + allowWarnings: 'packages/package-a,packages/package-b', }; + const paths = ['packages/*']; - await buildApiReports(opts); + await buildApiReports(paths, opts); expect(runApiExtraction).toHaveBeenCalledWith({ packageDirs: ['packages/package-a', 'packages/package-b'], @@ -364,11 +324,11 @@ describe('buildApiReports', () => { it('should accept multiple path values as comma separated string with spaces', async () => { const opts = { - paths: ['packages/*'], - allowWarnings: ['packages/package-a, packages/package-b'], + allowWarnings: 'packages/package-a, packages/package-b', }; + const paths = ['packages/*']; - await buildApiReports(opts); + await buildApiReports(paths, opts); expect(runApiExtraction).toHaveBeenCalledWith({ packageDirs: ['packages/package-a', 'packages/package-b'], @@ -380,54 +340,56 @@ describe('buildApiReports', () => { }); }); }); + describe('allowAllWarnings', () => { + it('should accept boolean values', async () => { + const opts = { + allowAllWarnings: true, + }; + const paths = ['packages/*']; + + await buildApiReports(paths, opts); + + expect(runApiExtraction).toHaveBeenCalledWith({ + packageDirs: ['packages/package-a', 'packages/package-b'], + tsconfigFilePath: '/root/tsconfig.json', + allowWarnings: true, + omitMessages: [], + isLocalBuild: true, + outputDir: '/root/node_modules/.cache/api-extractor', + }); + }); + }); describe('omitMessages', () => { it('should accept single message value', async () => { const opts = { - paths: ['packages/*'], - omitMessages: ['ae-missing-release-tag'], + omitMessages: 'ae-missing-release-tag', }; + const paths = ['packages/*']; - await buildApiReports(opts); + await buildApiReports(paths, opts); expect(runApiExtraction).toHaveBeenCalledWith({ packageDirs: ['packages/package-a', 'packages/package-b'], tsconfigFilePath: '/root/tsconfig.json', - allowWarnings: undefined, + allowWarnings: [], omitMessages: ['ae-missing-release-tag'], isLocalBuild: true, outputDir: '/root/node_modules/.cache/api-extractor', }); }); - it('should accept multiple message values as array', async () => { - const opts = { - paths: ['packages/*'], - omitMessages: ['ae-missing-release-tag', 'ae-missing-annotations'], - }; - - await buildApiReports(opts); - - expect(runApiExtraction).toHaveBeenCalledWith({ - packageDirs: ['packages/package-a', 'packages/package-b'], - tsconfigFilePath: '/root/tsconfig.json', - allowWarnings: undefined, - omitMessages: ['ae-missing-release-tag', 'ae-missing-annotations'], - isLocalBuild: true, - outputDir: '/root/node_modules/.cache/api-extractor', - }); - }); it('should accept multiple message values as comma separated string', async () => { const opts = { - paths: ['packages/*'], - omitMessages: ['ae-missing-release-tag,ae-missing-annotations'], + omitMessages: 'ae-missing-release-tag,ae-missing-annotations', }; + const paths = ['packages/*']; - await buildApiReports(opts); + await buildApiReports(paths, opts); expect(runApiExtraction).toHaveBeenCalledWith({ packageDirs: ['packages/package-a', 'packages/package-b'], tsconfigFilePath: '/root/tsconfig.json', - allowWarnings: undefined, + allowWarnings: [], omitMessages: ['ae-missing-release-tag', 'ae-missing-annotations'], isLocalBuild: true, outputDir: '/root/node_modules/.cache/api-extractor', @@ -436,16 +398,16 @@ describe('buildApiReports', () => { it('should accept multiple message values as comma separated string with spaces', async () => { const opts = { - paths: ['packages/*'], - omitMessages: ['ae-missing-release-tag, ae-missing-annotations'], + omitMessages: 'ae-missing-release-tag, ae-missing-annotations', }; + const paths = ['packages/*']; - await buildApiReports(opts); + await buildApiReports(paths, opts); expect(runApiExtraction).toHaveBeenCalledWith({ packageDirs: ['packages/package-a', 'packages/package-b'], tsconfigFilePath: '/root/tsconfig.json', - allowWarnings: undefined, + allowWarnings: [], omitMessages: ['ae-missing-release-tag', 'ae-missing-annotations'], isLocalBuild: true, outputDir: '/root/node_modules/.cache/api-extractor', @@ -455,16 +417,16 @@ describe('buildApiReports', () => { describe('isCI', () => { it('should set localBuild to false if CI option is passed', async () => { const opts = { - paths: ['packages/*'], ci: true, }; + const paths = ['packages/*']; - await buildApiReports(opts); + await buildApiReports(paths, opts); expect(runApiExtraction).toHaveBeenCalledWith({ packageDirs: ['packages/package-a', 'packages/package-b'], tsconfigFilePath: '/root/tsconfig.json', - allowWarnings: undefined, + allowWarnings: [], omitMessages: [], isLocalBuild: false, outputDir: '/root/node_modules/.cache/api-extractor', @@ -478,11 +440,11 @@ describe('buildApiReports', () => { describe('docs', () => { it('should run typedoc if docs option is passed', async () => { const opts = { - paths: ['packages/*'], docs: true, }; + const paths = ['packages/*']; - await buildApiReports(opts); + await buildApiReports(paths, opts); expect(buildDocs).toHaveBeenCalledWith({ inputDir: '/root/node_modules/.cache/api-extractor', @@ -493,13 +455,13 @@ describe('buildApiReports', () => { describe('tsc', () => { it('should run tsc if tsc option is passed', async () => { const opts = { - paths: ['packages/*'], tsc: true, }; + const paths = ['packages/*']; - await buildApiReports(opts); + await buildApiReports(paths, opts); - expect(generateTSC).toHaveBeenCalled(); + expect(generateTypeDeclarations).toHaveBeenCalled(); }); }); }); diff --git a/packages/repo-tools/src/commands/api-reports/api-reports.ts b/packages/repo-tools/src/commands/api-reports/api-reports.ts index b59f498d7b..6270072912 100644 --- a/packages/repo-tools/src/commands/api-reports/api-reports.ts +++ b/packages/repo-tools/src/commands/api-reports/api-reports.ts @@ -24,18 +24,18 @@ import { buildDocs, } from './api-extractor'; import { findPackageDirs, paths as cliPaths } from '../../lib/paths'; -import { generateTSC } from './generateTSC'; +import { generateTypeDeclarations } from './generateTypeDeclarations'; type Options = { ci?: boolean; docs?: boolean; tsc?: boolean; - paths?: string[]; - allowWarnings?: string[] | boolean; - omitMessages?: string[]; + allowWarnings?: string; + allowAllWarnings?: boolean; + omitMessages?: string; } & OptionValues; -export const buildApiReports = async (opts: Options) => { +export const buildApiReports = async (paths: string[] = [], opts: Options) => { const tmpDir = cliPaths.resolveTargetRoot( './node_modules/.cache/api-extractor', ); @@ -43,15 +43,16 @@ export const buildApiReports = async (opts: Options) => { const isCiBuild = opts.ci; const isDocsBuild = opts.docs; const runTsc = opts.tsc; - - const parsedPaths = parseArrayOption(opts.paths); - const isAllPackages = !Array.isArray(parsedPaths) || !parsedPaths?.length; - const selectedPaths = isAllPackages ? await getWorkspacePkgs() : parsedPaths; - const selectedPackageDirs = await findPackageDirs(selectedPaths); - const allowWarnings = parseArrayOption(opts.allowWarnings); + const allowAllWarnings = opts.allowAllWarnings; const omitMessages = parseArrayOption(opts.omitMessages); + const isAllPackages = !paths?.length; + const selectedPaths = isAllPackages + ? await getWorkspacePackagePathPatterns() + : paths; + const selectedPackageDirs = await findPackageDirs(selectedPaths); + if (isAllPackages && !isCiBuild && !isDocsBuild) { console.log(''); console.log( @@ -59,7 +60,7 @@ export const buildApiReports = async (opts: Options) => { ); console.log(''); console.log( - ' yarn build:api-reports -p packages/config -p packages/core-plugin-api,plugins/*', + ' yarn build:api-reports packages/config packages/core-plugin-api plugins/*', ); console.log(''); } @@ -73,7 +74,7 @@ export const buildApiReports = async (opts: Options) => { if (runTsc) { console.log('# Compiling TypeScript'); - await generateTSC(tsconfigFilePath); + await generateTypeDeclarations(tsconfigFilePath); } const { tsPackageDirs, cliPackageDirs } = await categorizePackageDirs( @@ -87,7 +88,7 @@ export const buildApiReports = async (opts: Options) => { outputDir: tmpDir, isLocalBuild: !isCiBuild, tsconfigFilePath, - allowWarnings, + allowWarnings: allowAllWarnings || allowWarnings, omitMessages: Array.isArray(omitMessages) ? omitMessages : [], }); } @@ -99,7 +100,6 @@ export const buildApiReports = async (opts: Options) => { }); } - console.log(isDocsBuild); if (isDocsBuild) { console.log('# Generating package documentation'); await buildDocs({ @@ -116,7 +116,7 @@ export const buildApiReports = async (opts: Options) => { * * @returns {Promise} The list of package names, or `undefined` if not found. */ -async function getWorkspacePkgs() { +async function getWorkspacePackagePathPatterns() { const pkgJson = await fs .readJson(cliPaths.resolveTargetRoot('package.json')) .catch(error => { @@ -130,28 +130,22 @@ async function getWorkspacePkgs() { } /** - * Splits each string in the input array on comma, and returns an array of the resulting substrings. - * If the input array is `undefined`, returns `undefined`. If the input value is `true` or `false`, - * returns the value as-is. + * Splits the input string on comma, and returns an array of the resulting substrings. + * for `undefined` or an empty string, returns an empty array. * - * @param value An array of strings to be split on comma, or a boolean value (inherithed from commanderjs array args). - * @returns An array of the resulting substrings, the original boolean value, or `undefined` if the input value is `undefined`. + * @param value A string to be split on comma. + * @returns An array of the resulting substrings, or an empty array if the input value is `undefined` or an empty string. * * @example - * parseOption(['foo,bar,baz']) + * parseOption('foo,bar,baz') * // returns ['foo', 'bar', 'baz'] * - * parseOption(true) - * // returns true + * parseOption('') + * // returns [] * * parseOption() - * // returns undefined + * // returns [] */ -function parseArrayOption(value: string[] | boolean | undefined) { - if (typeof value === 'boolean') { - return value; - } - return value?.flatMap((str: string) => - str.includes(',') ? str.split(',').map(s => s.trim()) : str, - ); +function parseArrayOption(value: string | undefined) { + return value ? value.split(',').map(s => s.trim()) : []; } diff --git a/packages/repo-tools/src/commands/api-reports/generateTSC.ts b/packages/repo-tools/src/commands/api-reports/generateTypeDeclarations.ts similarity index 95% rename from packages/repo-tools/src/commands/api-reports/generateTSC.ts rename to packages/repo-tools/src/commands/api-reports/generateTypeDeclarations.ts index 5c10c5085f..58f4117bc2 100644 --- a/packages/repo-tools/src/commands/api-reports/generateTSC.ts +++ b/packages/repo-tools/src/commands/api-reports/generateTypeDeclarations.ts @@ -28,7 +28,7 @@ import { paths as cliPaths } from '../../lib/paths'; * @returns {Promise} A promise that resolves when the declaration files have been generated. */ -export async function generateTSC(tsconfigFilePath: string) { +export async function generateTypeDeclarations(tsconfigFilePath: string) { await fs.remove(cliPaths.resolveTargetRoot('dist-types')); const { status } = spawnSync( 'yarn', diff --git a/packages/repo-tools/src/commands/index.ts b/packages/repo-tools/src/commands/index.ts index 3f3b2cdbe7..f7a16b6d4e 100644 --- a/packages/repo-tools/src/commands/index.ts +++ b/packages/repo-tools/src/commands/index.ts @@ -20,21 +20,21 @@ import { exitWithError } from '../lib/errors'; export function registerCommands(program: Command) { program - .command('api-reports') - .option( - '-p --paths [paths...]', - 'paths of package folder to extract API reports, `workspaces.packages` from root packages.json by default. Allows glob patterns and comma separated values', - ) + .command('api-reports [paths...]') .option('--ci', 'CI run checks that there is no changes on API reports') .option('--tsc', 'executes the tsc compilation before extracting the APIs') .option('--docs', 'generates the api documentation') .option( - '-a, --allow-warnings [allowWarningsPaths...]', + '-a, --allow-warnings ', 'continue processing packages after getting errors on selected packages Allows glob patterns and comma separated values (i.e. packages/core,plugins/core-*)', + ) + .option( + '--allow-all-warnings', + 'continue processing packages after getting errors on all packages', false, ) .option( - '-o, --omit-messages ', + '-o, --omit-messages ', 'select some message code to be omited on the API Extractor (comma separated values i.e ae-cyclic-inherit-doc,ae-missing-getter )', ) .description('Generate an API report for selected packages')