diff --git a/.changeset/clean-paws-fry.md b/.changeset/clean-paws-fry.md new file mode 100644 index 0000000000..5e66a686c0 --- /dev/null +++ b/.changeset/clean-paws-fry.md @@ -0,0 +1,5 @@ +--- +'@backstage/repo-tools': minor +--- + +Introducing repo-tools package diff --git a/package.json b/package.json index 2f20eeeee3..d40e7dfbc0 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": "ts-node -T -P scripts/tsconfig.json scripts/api-extractor.ts", + "build:api-reports:only": "backstage-repo-tools api-reports", "build:api-docs": "LANG=en_EN yarn build:api-reports --docs", "tsc": "tsc", "tsc:full": "backstage-cli repo clean && tsc --skipLibCheck false --incremental false", @@ -49,16 +49,13 @@ "version": "1.8.0", "dependencies": { "@backstage/errors": "workspace:^", - "@manypkg/get-packages": "^1.1.3", - "@microsoft/api-documenter": "^7.17.11", - "@microsoft/api-extractor": "^7.23.0", - "@microsoft/api-extractor-model": "^7.17.2", - "@microsoft/tsdoc": "^0.14.1" + "@manypkg/get-packages": "^1.1.3" }, "devDependencies": { "@backstage/cli": "workspace:*", "@backstage/codemods": "workspace:*", "@backstage/create-app": "workspace:*", + "@backstage/repo-tools": "workspace:*", "@changesets/cli": "^2.14.0", "@octokit/rest": "^19.0.3", "@spotify/prettier-config": "^14.0.0", diff --git a/packages/repo-tools/.eslintrc.js b/packages/repo-tools/.eslintrc.js new file mode 100644 index 0000000000..599478bcc8 --- /dev/null +++ b/packages/repo-tools/.eslintrc.js @@ -0,0 +1,5 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname, { + rules: { + 'no-console': 0, + }, +}); diff --git a/packages/repo-tools/README.md b/packages/repo-tools/README.md new file mode 100644 index 0000000000..eb9f70dd72 --- /dev/null +++ b/packages/repo-tools/README.md @@ -0,0 +1,11 @@ +# @backstage/repo-tools + +This package provides a CLI for backstage repo tooling. + +## Installation + +Install the package via Yarn: + +```sh +yarn add @backstage/repo-tools +``` diff --git a/packages/repo-tools/bin/backstage-repo-tools b/packages/repo-tools/bin/backstage-repo-tools new file mode 100755 index 0000000000..377e4a32ca --- /dev/null +++ b/packages/repo-tools/bin/backstage-repo-tools @@ -0,0 +1,37 @@ +#!/usr/bin/env node +/* + * 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. + */ + +const path = require('path'); + +// Figure out whether we're running inside the backstage repo or as an installed dependency +/* eslint-disable-next-line no-restricted-syntax */ +const isLocal = require('fs').existsSync(path.resolve(__dirname, '../src')); + +if (!isLocal || process.env.BACKSTAGE_E2E_CLI_TEST) { + require('..'); +} else { + require('ts-node').register({ + transpileOnly: true, + /* eslint-disable-next-line no-restricted-syntax */ + project: path.resolve(__dirname, '../../../tsconfig.json'), + compilerOptions: { + module: 'CommonJS', + }, + }); + + require('../src'); +} diff --git a/packages/repo-tools/cli-report.md b/packages/repo-tools/cli-report.md new file mode 100644 index 0000000000..08568827ea --- /dev/null +++ b/packages/repo-tools/cli-report.md @@ -0,0 +1,29 @@ +## CLI Report file for "@backstage/repo-tools" + +> Do not edit this file. It is a report generated by `yarn build:api-reports` + +### `backstage-repo-tools` + +``` +Usage: backstage-repo-tools [options] [command] + +Options: + -V, --version + -h, --help + +Commands: + api-reports [options] [path...] + help [command] +``` + +### `backstage-repo-tools api-reports` + +``` +Usage: backstage-repo-tools api-reports [options] [path...] + +Options: + --ci + --tsc + --docs + -h, --help +``` diff --git a/packages/repo-tools/package.json b/packages/repo-tools/package.json new file mode 100644 index 0000000000..bffed77cab --- /dev/null +++ b/packages/repo-tools/package.json @@ -0,0 +1,52 @@ +{ + "name": "@backstage/repo-tools", + "description": "CLI for Backstage repo tooling ", + "version": "0.0.0", + "publishConfig": { + "access": "public" + }, + "backstage": { + "role": "cli" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "packages/repo-tools" + }, + "keywords": [ + "backstage" + ], + "license": "Apache-2.0", + "main": "dist/index.cjs.js", + "scripts": { + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "start": "nodemon --" + }, + "bin": { + "backstage-repo-tools": "bin/backstage-repo-tools" + }, + "dependencies": { + "@backstage/errors": "workspace:^", + "@microsoft/api-documenter": "^7.17.11", + "@microsoft/api-extractor": "^7.23.0", + "@microsoft/api-extractor-model": "^7.17.2", + "@microsoft/tsdoc": "0.14.1", + "chalk": "^4.0.0", + "commander": "^9.1.0", + "fs-extra": "10.1.0", + "ts-node": "^10.0.0" + }, + "files": [ + "bin", + "dist/**/*.js" + ], + "nodemonConfig": { + "watch": "./src", + "exec": "bin/backstage-repo-tools", + "ext": "ts" + } +} diff --git a/scripts/api-extractor.ts b/packages/repo-tools/src/commands/api-reports/api-extractor.ts similarity index 89% rename from scripts/api-extractor.ts rename to packages/repo-tools/src/commands/api-reports/api-extractor.ts index 244008584f..d434aae0a6 100644 --- a/scripts/api-extractor.ts +++ b/packages/repo-tools/src/commands/api-reports/api-extractor.ts @@ -24,7 +24,7 @@ import { dirname, join, } from 'path'; -import { spawnSync, execFile } from 'child_process'; +import { execFile } from 'child_process'; import prettier from 'prettier'; import fs from 'fs-extra'; import { @@ -49,11 +49,17 @@ import { import { DocTable } from '@microsoft/api-documenter/lib/nodes/DocTable'; import { DocTableRow } from '@microsoft/api-documenter/lib/nodes/DocTableRow'; import { DocHeading } from '@microsoft/api-documenter/lib/nodes/DocHeading'; -import { CustomMarkdownEmitter } from '@microsoft/api-documenter/lib/markdown/CustomMarkdownEmitter'; +import { + CustomMarkdownEmitter, + ICustomMarkdownEmitterOptions, +} 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'; -const tmpDir = resolvePath(__dirname, '../node_modules/.cache/api-extractor'); +const tmpDir = resolvePath( + process.cwd(), + './node_modules/.cache/api-extractor', +); /** * All of this monkey patching below is because MUI has these bare package.json file as a method @@ -99,7 +105,9 @@ function patchFileMessageFetcher( } = router; router.fetchAssociatedMessagesForReviewFile = - function patchedFetchAssociatedMessagesForReviewFile(ast) { + function patchedFetchAssociatedMessagesForReviewFile( + ast: AstDeclaration | undefined, + ) { const messages = fetchAssociatedMessagesForReviewFile.call(this, ast); return transform(messages, ast); }; @@ -113,7 +121,10 @@ function patchFileMessageFetcher( const originalGenerateReviewFileContent = ApiReportGenerator.generateReviewFileContent; ApiReportGenerator.generateReviewFileContent = - function decoratedGenerateReviewFileContent(collector, ...moreArgs) { + function decoratedGenerateReviewFileContent( + collector: { program: Program; messageRouter: any }, + ...moreArgs: any[] + ) { const program = collector.program as Program; // The purpose of this override is to allow the @ignore tag to be used to ignore warnings @@ -137,7 +148,9 @@ ApiReportGenerator.generateReviewFileContent = } const [, symbolName] = symbolMatch; - const sourceFile = program.getSourceFile(message.sourceFilePath); + const sourceFile = + message.sourceFilePath && + program.getSourceFile(message.sourceFilePath); if (!sourceFile) { throw new Error( `Failed to find source file in program at path "${message.sourceFilePath}"`, @@ -210,7 +223,7 @@ const ALLOW_WARNINGS = [ async function resolvePackagePath( packagePath: string, ): Promise { - const projectRoot = resolvePath(__dirname, '..'); + const projectRoot = resolvePath(process.cwd()); const fullPackageDir = resolvePath(projectRoot, packagePath); const stat = await fs.stat(fullPackageDir); @@ -228,7 +241,7 @@ async function resolvePackagePath( return relativePath(projectRoot, fullPackageDir); } -async function findSpecificPackageDirs(unresolvedPackageDirs: string[]) { +export async function findSpecificPackageDirs(unresolvedPackageDirs: string[]) { const packageDirs = new Array(); for (const unresolvedPackageDir of unresolvedPackageDirs) { @@ -246,9 +259,9 @@ async function findSpecificPackageDirs(unresolvedPackageDirs: string[]) { return packageDirs; } -async function findPackageDirs() { +export async function findPackageDirs() { const packageDirs = new Array(); - const projectRoot = resolvePath(__dirname, '..'); + const projectRoot = resolvePath(process.cwd()); for (const packageRoot of PACKAGE_ROOTS) { const dirs = await fs.readdir(resolvePath(projectRoot, packageRoot)); @@ -265,8 +278,8 @@ async function findPackageDirs() { return packageDirs; } -async function createTemporaryTsConfig(includedPackageDirs: string[]) { - const path = resolvePath(__dirname, '..', 'tsconfig.tmp.json'); +export async function createTemporaryTsConfig(includedPackageDirs: string[]) { + const path = resolvePath(process.cwd(), 'tsconfig.tmp.json'); process.once('exit', () => { fs.removeSync(path); @@ -284,7 +297,7 @@ async function createTemporaryTsConfig(includedPackageDirs: string[]) { return path; } -async function countApiReportWarnings(projectFolder: string) { +export async function countApiReportWarnings(projectFolder: string) { const path = resolvePath(projectFolder, 'api-report.md'); try { const content = await fs.readFile(path, 'utf8'); @@ -313,7 +326,7 @@ async function countApiReportWarnings(projectFolder: string) { } } -async function getTsDocConfig() { +export async function getTsDocConfig() { const tsdocConfigFile = await TSDocConfigFile.loadFile( require.resolve('@microsoft/api-extractor/extends/tsdoc-base.json'), ); @@ -349,7 +362,7 @@ interface ApiExtractionOptions { tsconfigFilePath: string; } -async function runApiExtraction({ +export async function runApiExtraction({ packageDirs, outputDir, isLocalBuild, @@ -358,7 +371,10 @@ async function runApiExtraction({ await fs.remove(outputDir); const entryPoints = packageDirs.map(packageDir => { - return resolvePath(__dirname, `../dist-types/${packageDir}/src/index.d.ts`); + return resolvePath( + process.cwd(), + `./dist-types/${packageDir}/src/index.d.ts`, + ); }); let compilerState: CompilerState | undefined = undefined; @@ -367,8 +383,12 @@ async function runApiExtraction({ for (const packageDir of packageDirs) { console.log(`## Processing ${packageDir}`); - const projectFolder = resolvePath(__dirname, '..', packageDir); - const packageFolder = resolvePath(__dirname, '../dist-types', packageDir); + const projectFolder = resolvePath(process.cwd(), packageDir); + const packageFolder = resolvePath( + process.cwd(), + './dist-types', + packageDir, + ); const warningCountBefore = await countApiReportWarnings(projectFolder); @@ -618,7 +638,7 @@ class ApiModelTransforms { if (serialized.members.length !== 1) { throw new Error( `Unexpected members in serialized ApiPackage, [${serialized.members - .map(m => m.kind) + .map((m: { kind: any }) => m.kind) .join(' ')}]`, ); } @@ -634,7 +654,7 @@ class ApiModelTransforms { members: [ { ...entryPoint, - members: entryPoint.members.map(member => + members: entryPoint.members.map((member: any) => transforms.reduce((m, t) => t(m), member), ), }, @@ -753,7 +773,7 @@ class ApiModelTransforms { }; } -async function buildDocs({ +export async function buildDocs({ inputDir, outputDir, }: { @@ -847,7 +867,11 @@ async function buildDocs({ } /** @override */ - emit(stringBuilder, docNode, options) { + emit( + stringBuilder: any, + docNode: DocNode, + options: ICustomMarkdownEmitterOptions, + ) { // Hack to get rid of the leading comment of each file, since // we want the front matter to come first stringBuilder._chunks.length = 0; @@ -886,7 +910,7 @@ async function buildDocs({ // so we hook in wherever we can. In this case we add the front matter // just before writing the breadcrumbs at the top. /** @override */ - _writeBreadcrumb(output, apiItem) { + _writeBreadcrumb(output: any, apiItem: ApiItem & { name: string }) { let title; let description; @@ -897,9 +921,12 @@ async function buildDocs({ } else if (apiItem.kind === 'Model') { title = 'Package Index'; description = 'Index of all Backstage Packages'; - } else { + } else if (apiItem.name) { title = apiItem.name; description = `API Reference for ${apiItem.name}`; + } else { + title = apiItem.displayName; + description = `API Reference for ${apiItem.displayName}`; } // Add our front matter @@ -925,7 +952,10 @@ async function buildDocs({ }; } - _writeModelTable(output, apiModel): void { + _writeModelTable( + output: { appendNode: (arg0: DocTable | DocHeading) => void }, + apiModel: { members: any }, + ): void { const configuration = this._tsdocConfiguration; const packagesTable = new DocTable({ @@ -998,7 +1028,10 @@ async function buildDocs({ documenter.generateFiles(); } -async function categorizePackageDirs(projectRoot, packageDirs) { +export async function categorizePackageDirs( + projectRoot: string, + packageDirs: any[], +) { const dirs = packageDirs.slice(); const tsPackageDirs = new Array(); const cliPackageDirs = new Array(); @@ -1086,11 +1119,11 @@ function parseHelpPage(helpPageContent: string) { // Trim away documentation const sectionItems = sectionLines .map(line => line.match(/^\s{1,8}(.*?)\s\s+/)?.[1]) - .filter(Boolean); + .filter(Boolean) as string[]; - if (sectionName.toLocaleLowerCase('en-US') === 'options:') { + if (sectionName?.toLocaleLowerCase('en-US') === 'options:') { options = sectionItems; - } else if (sectionName.toLocaleLowerCase('en-US') === 'commands:') { + } else if (sectionName?.toLocaleLowerCase('en-US') === 'commands:') { commands = sectionItems; } else { throw new Error(`Unknown CLI section: ${sectionName}`); @@ -1185,7 +1218,7 @@ interface CliExtractionOptions { isLocalBuild: boolean; } -async function runCliExtraction({ +export async function runCliExtraction({ projectRoot, packageDirs, isLocalBuild, @@ -1250,96 +1283,3 @@ async function runCliExtraction({ } } } - -async function main() { - const projectRoot = resolvePath(__dirname, '..'); - const isCiBuild = process.argv.includes('--ci'); - const isDocsBuild = process.argv.includes('--docs'); - const runTsc = process.argv.includes('--tsc'); - - const selectedPackageDirs = await findSpecificPackageDirs( - process.argv.slice(2).filter(arg => !arg.startsWith('--')), - ); - if (selectedPackageDirs && isCiBuild) { - throw new Error( - 'Package path arguments are not supported together with the --ci flag', - ); - } - if (!selectedPackageDirs && !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', - ); - console.log(''); - } - - let temporaryTsConfigPath: string | undefined; - if (selectedPackageDirs) { - temporaryTsConfigPath = await createTemporaryTsConfig(selectedPackageDirs); - } - const tsconfigFilePath = - temporaryTsConfigPath ?? resolvePath(projectRoot, '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); - } - } - - const packageDirs = selectedPackageDirs ?? (await findPackageDirs()); - - const { tsPackageDirs, cliPackageDirs } = await categorizePackageDirs( - projectRoot, - packageDirs, - ); - - if (tsPackageDirs.length > 0) { - console.log('# Generating package API reports'); - await runApiExtraction({ - packageDirs: tsPackageDirs, - outputDir: tmpDir, - isLocalBuild: !isCiBuild, - tsconfigFilePath, - }); - } - if (cliPackageDirs.length > 0) { - console.log('# Generating package CLI reports'); - await runCliExtraction({ - projectRoot, - packageDirs: cliPackageDirs, - isLocalBuild: !isCiBuild, - }); - } - - if (isDocsBuild) { - console.log('# Generating package documentation'); - await buildDocs({ - inputDir: tmpDir, - outputDir: resolvePath(projectRoot, 'docs/reference'), - }); - } -} - -main().catch(error => { - console.error(error.stack || String(error)); - process.exit(1); -}); diff --git a/packages/repo-tools/src/commands/api-reports/api-reports.ts b/packages/repo-tools/src/commands/api-reports/api-reports.ts new file mode 100644 index 0000000000..3ac740d596 --- /dev/null +++ b/packages/repo-tools/src/commands/api-reports/api-reports.ts @@ -0,0 +1,120 @@ +/* + * 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 { OptionValues } from 'commander'; +import { resolve as resolvePath } from 'path'; +import fs from 'fs-extra'; +import { spawnSync } from 'child_process'; +import { + findSpecificPackageDirs, + createTemporaryTsConfig, + findPackageDirs, + categorizePackageDirs, + runApiExtraction, + runCliExtraction, + buildDocs, +} from './api-extractor'; + +export default async (paths: string[], opts: OptionValues) => { + const tmpDir = resolvePath( + process.cwd(), + './node_modules/.cache/api-extractor', + ); + const projectRoot = resolvePath(process.cwd()); + const isCiBuild = opts.ci; + const isDocsBuild = opts.docs; + const runTsc = opts.tsc; + + const selectedPackageDirs = await findSpecificPackageDirs(paths); + + if (selectedPackageDirs && isCiBuild) { + throw new Error( + 'Package path arguments are not supported together with the --ci flag', + ); + } + if (!selectedPackageDirs && !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', + ); + console.log(''); + } + + let temporaryTsConfigPath: string | undefined; + if (selectedPackageDirs) { + temporaryTsConfigPath = await createTemporaryTsConfig(selectedPackageDirs); + } + const tsconfigFilePath = + temporaryTsConfigPath ?? resolvePath(projectRoot, '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); + } + } + + const packageDirs = selectedPackageDirs ?? (await findPackageDirs()); + + const { tsPackageDirs, cliPackageDirs } = await categorizePackageDirs( + projectRoot, + packageDirs, + ); + + if (tsPackageDirs.length > 0) { + console.log('# Generating package API reports'); + await runApiExtraction({ + packageDirs: tsPackageDirs, + outputDir: tmpDir, + isLocalBuild: !isCiBuild, + tsconfigFilePath, + }); + } + if (cliPackageDirs.length > 0) { + console.log('# Generating package CLI reports'); + await runCliExtraction({ + projectRoot, + packageDirs: cliPackageDirs, + isLocalBuild: !isCiBuild, + }); + } + + if (isDocsBuild) { + console.log('# Generating package documentation'); + await buildDocs({ + inputDir: tmpDir, + outputDir: resolvePath(projectRoot, 'docs/reference'), + }); + } +}; diff --git a/packages/repo-tools/src/commands/index.ts b/packages/repo-tools/src/commands/index.ts new file mode 100644 index 0000000000..c4281b0c4f --- /dev/null +++ b/packages/repo-tools/src/commands/index.ts @@ -0,0 +1,48 @@ +/* + * 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 { assertError } from '@backstage/errors'; +import { Command } from 'commander'; +import { exitWithError } from '../lib/errors'; + +export function registerCommands(program: Command) { + program + .command('api-reports [path...]') + .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') + .description('Generate an API report for selected packages') + .action( + lazy(() => import('./api-reports/api-reports').then(m => m.default)), + ); +} + +// Wraps an action function so that it always exits and handles errors +function lazy( + getActionFunc: () => Promise<(...args: any[]) => Promise>, +): (...args: any[]) => Promise { + return async (...args: any[]) => { + try { + const actionFunc = await getActionFunc(); + await actionFunc(...args); + + process.exit(0); + } catch (error) { + assertError(error); + exitWithError(error); + } + }; +} diff --git a/packages/repo-tools/src/index.ts b/packages/repo-tools/src/index.ts new file mode 100644 index 0000000000..ed0e2992d3 --- /dev/null +++ b/packages/repo-tools/src/index.ts @@ -0,0 +1,52 @@ +/* + * 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. + */ + +/** + * CLI for Backstage repo tooling + * + * @packageDocumentation + */ + +import { program } from 'commander'; +import chalk from 'chalk'; +import { exitWithError } from './lib/errors'; +import { registerCommands } from './commands'; + +const main = (argv: string[]) => { + program.name('backstage-repo-tools').version('1.0'); + + registerCommands(program); + + program.on('command:*', () => { + console.log(); + console.log(chalk.red(`Invalid command: ${program.args.join(' ')}`)); + console.log(); + program.outputHelp(); + process.exit(1); + }); + + program.parse(argv); +}; + +process.on('unhandledRejection', rejection => { + if (rejection instanceof Error) { + exitWithError(rejection); + } else { + exitWithError(new Error(`Unknown rejection: '${rejection}'`)); + } +}); + +main(process.argv); diff --git a/packages/repo-tools/src/lib/errors.ts b/packages/repo-tools/src/lib/errors.ts new file mode 100644 index 0000000000..dd3955a10a --- /dev/null +++ b/packages/repo-tools/src/lib/errors.ts @@ -0,0 +1,48 @@ +/* + * 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 chalk from 'chalk'; + +export class CustomError extends Error { + get name(): string { + return this.constructor.name; + } +} + +export class ExitCodeError extends CustomError { + readonly code: number; + + constructor(code: number, command?: string) { + super( + command + ? `Command '${command}' exited with code ${code}` + : `Child exited with code ${code}`, + ); + this.code = code; + } +} + +export function exitWithError(error: Error): never { + if (error instanceof ExitCodeError) { + process.stderr.write(`\n${chalk.red(error.message)}\n\n`); + process.exit(error.code); + } else { + process.stderr.write(`\n${chalk.red(`${error}`)}\n\n`); + process.exit(1); + } +} + +export class NotFoundError extends CustomError {} diff --git a/yarn.lock b/yarn.lock index 8ca56c7fcf..4134fc3035 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7653,6 +7653,24 @@ __metadata: languageName: unknown linkType: soft +"@backstage/repo-tools@workspace:*, @backstage/repo-tools@workspace:packages/repo-tools": + version: 0.0.0-use.local + resolution: "@backstage/repo-tools@workspace:packages/repo-tools" + dependencies: + "@backstage/errors": "workspace:^" + "@microsoft/api-documenter": ^7.17.11 + "@microsoft/api-extractor": ^7.23.0 + "@microsoft/api-extractor-model": ^7.17.2 + "@microsoft/tsdoc": 0.14.1 + chalk: ^4.0.0 + commander: ^9.1.0 + fs-extra: 10.1.0 + ts-node: ^10.0.0 + bin: + backstage-repo-tools: bin/backstage-repo-tools + languageName: unknown + linkType: soft + "@backstage/test-utils@workspace:^, @backstage/test-utils@workspace:packages/test-utils": version: 0.0.0-use.local resolution: "@backstage/test-utils@workspace:packages/test-utils" @@ -10487,13 +10505,6 @@ __metadata: languageName: node linkType: hard -"@microsoft/tsdoc@npm:^0.14.1": - version: 0.14.2 - resolution: "@microsoft/tsdoc@npm:0.14.2" - checksum: b167c89e916ba73ee20b9c9d5dba6aa3a0de25ed3d50050e8a344dca7cd43cb2e1059bd515c820369b6e708901dd3fda476a42bc643ca74a35671ce77f724a3a - languageName: node - linkType: hard - "@mswjs/cookies@npm:^0.2.0, @mswjs/cookies@npm:^0.2.2": version: 0.2.2 resolution: "@mswjs/cookies@npm:0.2.2" @@ -33053,12 +33064,9 @@ __metadata: "@backstage/codemods": "workspace:*" "@backstage/create-app": "workspace:*" "@backstage/errors": "workspace:^" + "@backstage/repo-tools": "workspace:*" "@changesets/cli": ^2.14.0 "@manypkg/get-packages": ^1.1.3 - "@microsoft/api-documenter": ^7.17.11 - "@microsoft/api-extractor": ^7.23.0 - "@microsoft/api-extractor-model": ^7.17.2 - "@microsoft/tsdoc": ^0.14.1 "@octokit/rest": ^19.0.3 "@spotify/prettier-config": ^14.0.0 "@techdocs/cli": "workspace:*"