From 6a985600a718f98f54acd145cb705ae64c957f54 Mon Sep 17 00:00:00 2001 From: Aramis Sennyey Date: Thu, 16 Mar 2023 12:37:29 -0400 Subject: [PATCH] Add CLI command for full repo check. Signed-off-by: Aramis Sennyey --- packages/cli/package.json | 1 + packages/cli/src/commands/index.ts | 7 ++ packages/cli/src/commands/openapi/generate.ts | 86 +++++++++++++++---- packages/cli/src/commands/openapi/verify.ts | 51 ++++++----- plugins/catalog-backend/schema/openapi.ts | 2 +- yarn.lock | 1 + 6 files changed, 110 insertions(+), 38 deletions(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index 8f166db53d..cc1d0a4d1c 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -106,6 +106,7 @@ "node-libs-browser": "^2.2.1", "npm-packlist": "^5.0.0", "ora": "^5.3.0", + "p-limit": "^3.0.2", "postcss": "^8.1.0", "process": "^0.11.10", "react-dev-utils": "^12.0.0-next.60", diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts index 6c29741060..713863b685 100644 --- a/packages/cli/src/commands/index.ts +++ b/packages/cli/src/commands/index.ts @@ -87,6 +87,13 @@ export function registerRepoCommand(program: Command) { ) .action(lazy(() => import('./openapi/verify').then(m => m.bulkCommand))); + command + .command('schema:openapi:generate') + .description( + 'Generates a Typescript file from an OpenAPI yaml spec. For use with the `@backstage/backend-openapi-utils` ApiRouter type.', + ) + .action(lazy(() => import('./openapi/generate').then(m => m.bulkCommand))); + command .command('test') .allowUnknownOption(true) // Allows the command to run, but we still need to parse raw args diff --git a/packages/cli/src/commands/openapi/generate.ts b/packages/cli/src/commands/openapi/generate.ts index f90f152b09..14a1217b7b 100644 --- a/packages/cli/src/commands/openapi/generate.ts +++ b/packages/cli/src/commands/openapi/generate.ts @@ -17,25 +17,81 @@ import fs from 'fs-extra'; import { paths } from '../../lib/paths'; import YAML from 'js-yaml'; +import chalk from 'chalk'; +import { resolve } from 'path'; +import { PackageGraph } from '../../lib/monorepo'; +import pLimit from 'p-limit'; +import { relative as relativePath } from 'path'; + +async function generate( + directoryPath: string, + config?: { skipMissingYamlFile: boolean }, +) { + const { skipMissingYamlFile } = config ?? {}; + const openapiPath = resolve(directoryPath, 'openapi.yaml'); + if (!(await fs.pathExists(openapiPath))) { + if (skipMissingYamlFile) { + return; + } + throw new Error('Could not find openapi.yaml in root of directory.'); + } + const yaml = YAML.load(await fs.readFile(openapiPath, 'utf8')); + + // For now, we're not adding a header or linting after pasting. + await fs.writeFile( + resolve(directoryPath, 'schema/openapi.ts'), + `export default ${JSON.stringify(yaml, null, 2)} as const`, + ); +} export async function command() { - const openapiPath = paths.resolveTarget('openapi.yaml'); - if (!(await fs.pathExists(openapiPath))) { - console.warn('Could not find openapi.yaml in root of directory.'); - process.exit(1); - } try { - const yaml = YAML.load( - await fs.readFile(paths.resolveTarget('openapi.yaml'), 'utf8'), - ); - - // For now, we're not adding a header or linting after pasting. - await fs.writeFile( - paths.resolveTarget('schema/openapi.ts'), - `export default ${JSON.stringify(yaml, null, 2)} as const`, - ); + await generate(paths.resolveTarget('.')); + console.log(chalk.green('OpenAPI files successfully generated.')); } catch (err) { - console.error(err); + console.error(chalk.red(err.message)); + process.exit(1); + } +} + +export async function bulkCommand(): Promise { + const packages = await PackageGraph.listTargetPackages(); + const limit = pLimit(5); + + const resultsList = await Promise.all( + packages.map(pkg => + limit(async () => { + let resultText = ''; + try { + await generate(pkg.dir, { skipMissingYamlFile: true }); + } catch (err) { + resultText = err.message; + } + + return { + relativeDir: relativePath(paths.targetRoot, pkg.dir), + resultText, + }; + }), + ), + ); + + let failed = false; + for (const { relativeDir, resultText } of resultsList) { + if (resultText) { + console.log(); + console.log( + chalk.red( + `OpenAPI yaml to Typescript generation failed in ${relativeDir}:`, + ), + ); + console.log(resultText.trimStart()); + + failed = true; + } + } + + if (failed) { process.exit(1); } } diff --git a/packages/cli/src/commands/openapi/verify.ts b/packages/cli/src/commands/openapi/verify.ts index f93672aca9..ad0031622f 100644 --- a/packages/cli/src/commands/openapi/verify.ts +++ b/packages/cli/src/commands/openapi/verify.ts @@ -25,6 +25,7 @@ import { PackageGraph } from '../../lib/monorepo'; import { cloneDeep } from 'lodash'; import Parser from '@apidevtools/swagger-parser'; import { detectRoleFromPackage } from '../../lib/role'; +import pLimit from 'p-limit'; const SUPPORTED_ROLES = [ 'backend', @@ -35,14 +36,15 @@ const SUPPORTED_ROLES = [ async function verify( directoryPath: string, - { checkRole }: { checkRole: boolean }, + config: { checkRole: boolean } = { checkRole: false }, ) { + const { checkRole } = config ?? {}; if (checkRole) { const role = detectRoleFromPackage( await fs.readJson(resolve(directoryPath, 'package.json')), ); - if (!SUPPORTED_ROLES.some(r => r === role)) { + if (!role || !SUPPORTED_ROLES.includes(role)) { console.log(chalk.red(`Unsupported role ${role}`)); process.exit(1); } @@ -61,15 +63,17 @@ async function verify( throw new Error('No `schema/openapi.ts` file found.'); } const schema = await import(join(directoryPath, 'schema/openapi')); - if (schema.default) { - if (!isEqual(schema.default, yaml)) { - throw new Error( - '`openapi.yaml` and `schema/openapi.ts` do not match. Please run `yarn build:openapi` to generate the `schema/openapi.ts` file from the `openapi.yaml` file.', - ); - } - } else { + if (!schema.default) { throw new Error('`schemas/openapi.ts` needs to have a default export.'); } + if (!isEqual(schema.default, yaml)) { + throw new Error( + `\`openapi.yaml\` and \`schema/openapi.ts\` do not match. Please run \`yarn --cwd ${relativePath( + paths.targetRoot, + directoryPath, + )} schema:openapi:generate\` to regenerate \`schema/openapi.ts\`.`, + ); + } } export async function command() { @@ -84,28 +88,31 @@ export async function command() { export async function bulkCommand(): Promise { const packages = await PackageGraph.listTargetPackages(); + const limit = pLimit(5); const resultsList = await Promise.all( - packages.map(async pkg => { - let resultText = ''; - try { - await verify(pkg.dir, { checkRole: false }); - } catch (err) { - resultText = err.message; - } + packages.map(pkg => + limit(async () => { + let resultText = ''; + try { + await verify(pkg.dir); + } catch (err) { + resultText = err.message; + } - return { - relativeDir: relativePath(paths.targetRoot, pkg.dir), - resultText, - }; - }), + return { + relativeDir: relativePath(paths.targetRoot, pkg.dir), + resultText, + }; + }), + ), ); let failed = false; for (const { relativeDir, resultText } of resultsList) { if (resultText) { console.log(); - console.log(chalk.red(`Lint failed in ${relativeDir}:`)); + console.log(chalk.red(`OpenAPI validation failed in ${relativeDir}:`)); console.log(resultText.trimStart()); failed = true; diff --git a/plugins/catalog-backend/schema/openapi.ts b/plugins/catalog-backend/schema/openapi.ts index be58a2ea74..3d775c54cf 100644 --- a/plugins/catalog-backend/schema/openapi.ts +++ b/plugins/catalog-backend/schema/openapi.ts @@ -112,7 +112,7 @@ export default { required: false, schema: { type: 'integer', - minimum: 1, + minimum: 0, }, }, limit: { diff --git a/yarn.lock b/yarn.lock index 10ce468f7c..3697631867 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4062,6 +4062,7 @@ __metadata: nodemon: ^2.0.2 npm-packlist: ^5.0.0 ora: ^5.3.0 + p-limit: ^3.0.2 postcss: ^8.1.0 process: ^0.11.10 react-dev-utils: ^12.0.0-next.60