From 683dae27182d5ae7a8d1f4e9f75c64711e107d1d Mon Sep 17 00:00:00 2001 From: aramissennyeydd Date: Tue, 19 Mar 2024 10:21:09 +0100 Subject: [PATCH 1/7] starting watch command Signed-off-by: aramissennyeydd --- .../src/generated/.openapi-generator-ignore | 24 ++++++++++++ .../src/generated/apis/DefaultApi.client.ts | 9 +++-- packages/repo-tools/package.json | 1 + packages/repo-tools/src/commands/index.ts | 2 + .../package/schema/openapi/generate/client.ts | 14 +++++-- .../package/schema/openapi/generate/index.ts | 39 ++++++++++++++++--- .../package/schema/openapi/generate/server.ts | 17 +++++--- yarn.lock | 1 + 8 files changed, 91 insertions(+), 16 deletions(-) create mode 100644 packages/catalog-client/src/generated/.openapi-generator-ignore diff --git a/packages/catalog-client/src/generated/.openapi-generator-ignore b/packages/catalog-client/src/generated/.openapi-generator-ignore new file mode 100644 index 0000000000..bd8a6eec07 --- /dev/null +++ b/packages/catalog-client/src/generated/.openapi-generator-ignore @@ -0,0 +1,24 @@ +*.md +*.mustache +apis/baseapi.ts +apis/exception.ts +auth/* +http/* +middleware.ts +servers.ts +util.ts +configuration.ts +rxjsStub.ts +.gitignore +apis/*.ts +!apis/*.client.ts +models/*.ts +!models/*.model.ts +!index.ts +!**/index.ts +types/ObjectParamAPI.ts +types/ObservableAPI.ts +types/PromiseAPI.ts +git_push.sh +package.json +tsconfig.json \ No newline at end of file diff --git a/packages/catalog-client/src/generated/apis/DefaultApi.client.ts b/packages/catalog-client/src/generated/apis/DefaultApi.client.ts index 1f1c54d6f8..598d9c706f 100644 --- a/packages/catalog-client/src/generated/apis/DefaultApi.client.ts +++ b/packages/catalog-client/src/generated/apis/DefaultApi.client.ts @@ -276,13 +276,14 @@ export class DefaultApiClient { /** * Get a batch set of entities given an array of entityRefs. + * @param filter Filter for just the entities defined by this filter. * @param getEntitiesByRefsRequest */ public async getEntitiesByRefs( // @ts-ignore request: { body: GetEntitiesByRefsRequest; - query?: { + query: { filter?: Array; }; }, @@ -290,9 +291,11 @@ export class DefaultApiClient { ): Promise> { const baseUrl = await this.discoveryApi.getBaseUrl(pluginId); - const uriTemplate = `/entities/by-refs/{?filter*}`; + const uriTemplate = `/entities/by-refs{?filter*}`; - const uri = parser.parse(uriTemplate).expand({ ...request.query }); + const uri = parser.parse(uriTemplate).expand({ + ...request.query, + }); return await this.fetchApi.fetch(`${baseUrl}${uri}`, { headers: { diff --git a/packages/repo-tools/package.json b/packages/repo-tools/package.json index f4b5133b30..a8fef81b63 100644 --- a/packages/repo-tools/package.json +++ b/packages/repo-tools/package.json @@ -62,6 +62,7 @@ "@stoplight/types": "^14.0.0", "@useoptic/openapi-utilities": "^0.55.0", "chalk": "^4.0.0", + "chokidar": "^3.6.0", "codeowners-utils": "^1.0.2", "command-exists": "^1.2.9", "commander": "^12.0.0", diff --git a/packages/repo-tools/src/commands/index.ts b/packages/repo-tools/src/commands/index.ts index 5a69a74ff3..abe55356ef 100644 --- a/packages/repo-tools/src/commands/index.ts +++ b/packages/repo-tools/src/commands/index.ts @@ -58,6 +58,8 @@ function registerPackageCommand(program: Command) { .description( 'Additional properties that can be passed to @openapitools/openapi-generator-cli', ) + .option('--watch') + .description('Watch the OpenAPI spec for changes and regenerate on save.') .action( lazy(() => import('./package/schema/openapi/generate').then(m => m.command), diff --git a/packages/repo-tools/src/commands/package/schema/openapi/generate/client.ts b/packages/repo-tools/src/commands/package/schema/openapi/generate/client.ts index b843cd27ec..61be02979e 100644 --- a/packages/repo-tools/src/commands/package/schema/openapi/generate/client.ts +++ b/packages/repo-tools/src/commands/package/schema/openapi/generate/client.ts @@ -30,6 +30,7 @@ import { getPathToCurrentOpenApiSpec } from '../../../../../lib/openapi/helpers' async function generate( outputDirectory: string, clientAdditionalProperties?: string, + abortSignal?: AbortController, ) { const resolvedOpenapiPath = await getPathToCurrentOpenApiSpec(); const resolvedOutputDirectory = cliPaths.resolveTargetRoot( @@ -69,6 +70,7 @@ async function generate( additionalProperties, ], { + signal: abortSignal?.signal, maxBuffer: Number.MAX_VALUE, cwd: resolvePackagePath('@backstage/repo-tools'), env: { @@ -83,7 +85,9 @@ async function generate( const prettier = cliPaths.resolveTargetRoot('node_modules/.bin/prettier'); if (prettier) { - await exec(`${prettier} --write ${resolvedOutputDirectory}`); + await exec(`${prettier} --write ${resolvedOutputDirectory}`, [], { + signal: abortSignal?.signal, + }); } fs.removeSync(resolve(resolvedOutputDirectory, '.openapi-generator-ignore')); @@ -97,14 +101,18 @@ async function generate( export async function command( outputPackage: string, clientAdditionalProperties?: string, + abortSignal?: AbortController, ): Promise { try { - await generate(outputPackage, clientAdditionalProperties); + await generate(outputPackage, clientAdditionalProperties, abortSignal); console.log( chalk.green(`Generated client in ${outputPackage}/${OUTPUT_PATH}`), ); } catch (err) { - console.log(); + if (err.name === 'AbortError') { + console.debug('Server generation aborted.'); + return; + } console.log(chalk.red(`Client generation failed:`)); console.log(err); diff --git a/packages/repo-tools/src/commands/package/schema/openapi/generate/index.ts b/packages/repo-tools/src/commands/package/schema/openapi/generate/index.ts index 41884e2e90..9227b67ba8 100644 --- a/packages/repo-tools/src/commands/package/schema/openapi/generate/index.ts +++ b/packages/repo-tools/src/commands/package/schema/openapi/generate/index.ts @@ -17,6 +17,8 @@ import chalk from 'chalk'; import { OptionValues } from 'commander'; import { command as generateClient } from './client'; import { command as generateServer } from './server'; +import chokidar from 'chokidar'; +import { getPathToCurrentOpenApiSpec } from '../../../../../lib/openapi/helpers'; export async function command(opts: OptionValues) { if (!opts.clientPackage && !opts.server) { @@ -25,10 +27,37 @@ export async function command(opts: OptionValues) { ); process.exit(1); } - if (opts.clientPackage) { - await generateClient(opts.clientPackage, opts.clientAdditionalProperties); - } - if (opts.server) { - await generateServer(); + + const sharedCommand = async (abortSignal?: AbortController) => { + if (opts.clientPackage) { + await generateClient( + opts.clientPackage, + opts.clientAdditionalProperties, + abortSignal, + ); + } + if (opts.server) { + await generateServer(abortSignal); + } + }; + + if (opts.watch) { + try { + const resolvedOpenapiPath = await getPathToCurrentOpenApiSpec(); + let abortController = new AbortController(); + chokidar.watch(resolvedOpenapiPath).on('change', async () => { + console.log('detected changes'); + abortController.abort(); + await sharedCommand(abortController); + abortController = new AbortController(); + }); + await sharedCommand(); + await new Promise(() => {}); + } catch (err) { + console.error(chalk.red('Error: ', err)); + process.exit(1); + } + } else { + await sharedCommand(); } } diff --git a/packages/repo-tools/src/commands/package/schema/openapi/generate/server.ts b/packages/repo-tools/src/commands/package/schema/openapi/generate/server.ts index e6c56c3122..317c17f96d 100644 --- a/packages/repo-tools/src/commands/package/schema/openapi/generate/server.ts +++ b/packages/repo-tools/src/commands/package/schema/openapi/generate/server.ts @@ -25,7 +25,7 @@ import { getPathToCurrentOpenApiSpec } from '../../../../../lib/openapi/helpers' const exec = promisify(execCb); -async function generate() { +async function generate(abortSignal?: AbortController) { const openapiPath = await getPathToCurrentOpenApiSpec(); const yaml = YAML.load(await fs.readFile(openapiPath, 'utf8')); @@ -50,19 +50,26 @@ export const createOpenApiRouter = async ( `, ); - await exec(`yarn backstage-cli package lint --fix ${tsPath}`); + await exec(`yarn backstage-cli package lint --fix ${tsPath}`, { + signal: abortSignal?.signal, + }); if (await cliPaths.resolveTargetRoot('node_modules/.bin/prettier')) { await exec(`yarn prettier --write ${tsPath}`, { cwd: cliPaths.targetRoot, + signal: abortSignal?.signal, }); } } -export async function command(): Promise { +export async function command(abortSignal?: AbortController): Promise { try { - await generate(); - console.log(chalk.green('Generated all files.')); + await generate(abortSignal); + console.log(chalk.green('Generated server files.')); } catch (err) { + if (err.name === 'AbortError') { + console.debug('Server generation aborted.'); + return; + } console.log(chalk.red(`OpenAPI server stub generation failed.`)); console.log(err.message); process.exit(1); diff --git a/yarn.lock b/yarn.lock index d8070957c2..eba469581e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8309,6 +8309,7 @@ __metadata: "@types/prettier": ^2.0.0 "@useoptic/openapi-utilities": ^0.55.0 chalk: ^4.0.0 + chokidar: ^3.6.0 codeowners-utils: ^1.0.2 command-exists: ^1.2.9 commander: ^12.0.0 From 3fb2fd76559213742bdbebdde5faa7880d4df0f1 Mon Sep 17 00:00:00 2001 From: aramissennyeydd Date: Sun, 7 Jul 2024 17:36:45 -0400 Subject: [PATCH 2/7] implement watch command Signed-off-by: aramissennyeydd --- .../src/generated/.openapi-generator-ignore | 24 -------------- packages/repo-tools/package.json | 2 +- .../package/schema/openapi/generate/index.ts | 32 +++++++++++++------ yarn.lock | 2 +- 4 files changed, 25 insertions(+), 35 deletions(-) delete mode 100644 packages/catalog-client/src/generated/.openapi-generator-ignore diff --git a/packages/catalog-client/src/generated/.openapi-generator-ignore b/packages/catalog-client/src/generated/.openapi-generator-ignore deleted file mode 100644 index bd8a6eec07..0000000000 --- a/packages/catalog-client/src/generated/.openapi-generator-ignore +++ /dev/null @@ -1,24 +0,0 @@ -*.md -*.mustache -apis/baseapi.ts -apis/exception.ts -auth/* -http/* -middleware.ts -servers.ts -util.ts -configuration.ts -rxjsStub.ts -.gitignore -apis/*.ts -!apis/*.client.ts -models/*.ts -!models/*.model.ts -!index.ts -!**/index.ts -types/ObjectParamAPI.ts -types/ObservableAPI.ts -types/PromiseAPI.ts -git_push.sh -package.json -tsconfig.json \ No newline at end of file diff --git a/packages/repo-tools/package.json b/packages/repo-tools/package.json index a8fef81b63..620456443d 100644 --- a/packages/repo-tools/package.json +++ b/packages/repo-tools/package.json @@ -62,7 +62,7 @@ "@stoplight/types": "^14.0.0", "@useoptic/openapi-utilities": "^0.55.0", "chalk": "^4.0.0", - "chokidar": "^3.6.0", + "chokidar": "^3.5.3", "codeowners-utils": "^1.0.2", "command-exists": "^1.2.9", "commander": "^12.0.0", diff --git a/packages/repo-tools/src/commands/package/schema/openapi/generate/index.ts b/packages/repo-tools/src/commands/package/schema/openapi/generate/index.ts index 9227b67ba8..b10ae941d4 100644 --- a/packages/repo-tools/src/commands/package/schema/openapi/generate/index.ts +++ b/packages/repo-tools/src/commands/package/schema/openapi/generate/index.ts @@ -29,29 +29,43 @@ export async function command(opts: OptionValues) { } const sharedCommand = async (abortSignal?: AbortController) => { + const promises = []; if (opts.clientPackage) { - await generateClient( - opts.clientPackage, - opts.clientAdditionalProperties, - abortSignal, + promises.push( + generateClient( + opts.clientPackage, + opts.clientAdditionalProperties, + abortSignal, + ), ); } if (opts.server) { - await generateServer(abortSignal); + promises.push(generateServer(abortSignal)); } + await Promise.all(promises); }; if (opts.watch) { try { const resolvedOpenapiPath = await getPathToCurrentOpenApiSpec(); let abortController = new AbortController(); - chokidar.watch(resolvedOpenapiPath).on('change', async () => { - console.log('detected changes'); + const watcher = chokidar.watch(resolvedOpenapiPath); + watcher.on('change', () => { + console.log('Detected changes! Regenerating...'); abortController.abort(); - await sharedCommand(abortController); abortController = new AbortController(); + sharedCommand(abortController); + }); + watcher.on('error', error => { + console.error('Error happened', error); + }); + + watcher.on('ready', async () => { + console.log( + 'Watching for changes in OpenAPI spec. Press Ctrl+C to stop.', + ); + await sharedCommand(); }); - await sharedCommand(); await new Promise(() => {}); } catch (err) { console.error(chalk.red('Error: ', err)); diff --git a/yarn.lock b/yarn.lock index eba469581e..af7d13981a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8309,7 +8309,7 @@ __metadata: "@types/prettier": ^2.0.0 "@useoptic/openapi-utilities": ^0.55.0 chalk: ^4.0.0 - chokidar: ^3.6.0 + chokidar: ^3.5.3 codeowners-utils: ^1.0.2 command-exists: ^1.2.9 commander: ^12.0.0 From 568cfdd9a9879d5a39c7fb3f68074b7e145cff29 Mon Sep 17 00:00:00 2001 From: aramissennyeydd Date: Sun, 7 Jul 2024 17:55:09 -0400 Subject: [PATCH 3/7] improve error handling so that the main file process.exits Signed-off-by: aramissennyeydd --- .../package/schema/openapi/generate/client.ts | 4 ++-- .../package/schema/openapi/generate/index.ts | 20 +++++++++++++++---- .../package/schema/openapi/generate/server.ts | 1 - 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/packages/repo-tools/src/commands/package/schema/openapi/generate/client.ts b/packages/repo-tools/src/commands/package/schema/openapi/generate/client.ts index 61be02979e..66f750bfc3 100644 --- a/packages/repo-tools/src/commands/package/schema/openapi/generate/client.ts +++ b/packages/repo-tools/src/commands/package/schema/openapi/generate/client.ts @@ -81,6 +81,8 @@ async function generate( await exec( `yarn backstage-cli package lint --fix ${resolvedOutputDirectory}`, + [], + { signal: abortSignal?.signal }, ); const prettier = cliPaths.resolveTargetRoot('node_modules/.bin/prettier'); @@ -115,7 +117,5 @@ export async function command( } console.log(chalk.red(`Client generation failed:`)); console.log(err); - - process.exit(1); } } diff --git a/packages/repo-tools/src/commands/package/schema/openapi/generate/index.ts b/packages/repo-tools/src/commands/package/schema/openapi/generate/index.ts index b10ae941d4..a7e9d9a402 100644 --- a/packages/repo-tools/src/commands/package/schema/openapi/generate/index.ts +++ b/packages/repo-tools/src/commands/package/schema/openapi/generate/index.ts @@ -19,6 +19,7 @@ import { command as generateClient } from './client'; import { command as generateServer } from './server'; import chokidar from 'chokidar'; import { getPathToCurrentOpenApiSpec } from '../../../../../lib/openapi/helpers'; +import { debounce } from 'lodash'; export async function command(opts: OptionValues) { if (!opts.clientPackage && !opts.server) { @@ -50,11 +51,19 @@ export async function command(opts: OptionValues) { const resolvedOpenapiPath = await getPathToCurrentOpenApiSpec(); let abortController = new AbortController(); const watcher = chokidar.watch(resolvedOpenapiPath); - watcher.on('change', () => { + + // The generate command currently takes ~8 seconds to run, so let's debounce calling it so we don't have to cancel it so much. + const debouncedCommand = debounce(() => { console.log('Detected changes! Regenerating...'); abortController.abort(); abortController = new AbortController(); - sharedCommand(abortController); + sharedCommand(abortController).catch(err => { + console.error(chalk.red('Error: ', err)); + }); + }, 500); + + watcher.on('change', () => { + debouncedCommand(); }); watcher.on('error', error => { console.error('Error happened', error); @@ -64,7 +73,6 @@ export async function command(opts: OptionValues) { console.log( 'Watching for changes in OpenAPI spec. Press Ctrl+C to stop.', ); - await sharedCommand(); }); await new Promise(() => {}); } catch (err) { @@ -72,6 +80,10 @@ export async function command(opts: OptionValues) { process.exit(1); } } else { - await sharedCommand(); + try { + await sharedCommand(); + } catch (err) { + process.exit(1); + } } } diff --git a/packages/repo-tools/src/commands/package/schema/openapi/generate/server.ts b/packages/repo-tools/src/commands/package/schema/openapi/generate/server.ts index 317c17f96d..662efe4347 100644 --- a/packages/repo-tools/src/commands/package/schema/openapi/generate/server.ts +++ b/packages/repo-tools/src/commands/package/schema/openapi/generate/server.ts @@ -72,6 +72,5 @@ export async function command(abortSignal?: AbortController): Promise { } console.log(chalk.red(`OpenAPI server stub generation failed.`)); console.log(err.message); - process.exit(1); } } From 47fdbb4d96a22021f2f6463ab6fc4bb48bbce098 Mon Sep 17 00:00:00 2001 From: aramissennyeydd Date: Sun, 7 Jul 2024 17:58:34 -0400 Subject: [PATCH 4/7] add changeset and revert generated file change Signed-off-by: aramissennyeydd --- .changeset/slow-mayflies-sleep.md | 5 +++++ .../src/generated/apis/DefaultApi.client.ts | 9 +++------ 2 files changed, 8 insertions(+), 6 deletions(-) create mode 100644 .changeset/slow-mayflies-sleep.md diff --git a/.changeset/slow-mayflies-sleep.md b/.changeset/slow-mayflies-sleep.md new file mode 100644 index 0000000000..ebfb9d12d4 --- /dev/null +++ b/.changeset/slow-mayflies-sleep.md @@ -0,0 +1,5 @@ +--- +'@backstage/repo-tools': minor +--- + +Adds a `--watch` mode to the `schema openapi generate` command for a better local schema writing experience. diff --git a/packages/catalog-client/src/generated/apis/DefaultApi.client.ts b/packages/catalog-client/src/generated/apis/DefaultApi.client.ts index 598d9c706f..1f1c54d6f8 100644 --- a/packages/catalog-client/src/generated/apis/DefaultApi.client.ts +++ b/packages/catalog-client/src/generated/apis/DefaultApi.client.ts @@ -276,14 +276,13 @@ export class DefaultApiClient { /** * Get a batch set of entities given an array of entityRefs. - * @param filter Filter for just the entities defined by this filter. * @param getEntitiesByRefsRequest */ public async getEntitiesByRefs( // @ts-ignore request: { body: GetEntitiesByRefsRequest; - query: { + query?: { filter?: Array; }; }, @@ -291,11 +290,9 @@ export class DefaultApiClient { ): Promise> { const baseUrl = await this.discoveryApi.getBaseUrl(pluginId); - const uriTemplate = `/entities/by-refs{?filter*}`; + const uriTemplate = `/entities/by-refs/{?filter*}`; - const uri = parser.parse(uriTemplate).expand({ - ...request.query, - }); + const uri = parser.parse(uriTemplate).expand({ ...request.query }); return await this.fetchApi.fetch(`${baseUrl}${uri}`, { headers: { From abc3e3475cd8b0182587bb2a76f97e17572e90bb Mon Sep 17 00:00:00 2001 From: aramissennyeydd Date: Mon, 7 Oct 2024 23:17:58 -0400 Subject: [PATCH 5/7] add a unit test Signed-off-by: aramissennyeydd --- .../schema/openapi/generate/index.test.ts | 103 ++++++++++++++++++ .../package/schema/openapi/generate/index.ts | 3 +- packages/repo-tools/src/lib/runner.ts | 4 + 3 files changed, 109 insertions(+), 1 deletion(-) create mode 100644 packages/repo-tools/src/commands/package/schema/openapi/generate/index.test.ts diff --git a/packages/repo-tools/src/commands/package/schema/openapi/generate/index.test.ts b/packages/repo-tools/src/commands/package/schema/openapi/generate/index.test.ts new file mode 100644 index 0000000000..21a11c6165 --- /dev/null +++ b/packages/repo-tools/src/commands/package/schema/openapi/generate/index.test.ts @@ -0,0 +1,103 @@ +/* + * Copyright 2024 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 { createMockDirectory } from '@backstage/backend-test-utils'; +import path from 'path'; + +jest.mock( + 'lodash', + () => + ({ + ...jest.requireActual('lodash'), + debounce: (fn: any) => fn, + } as any as typeof import('lodash')), +); + +describe('generateOpenApiSchema', () => { + const inputDir = createMockDirectory(); + const outputDir = createMockDirectory(); + + beforeEach(() => { + inputDir.clear(); + outputDir.clear(); + + inputDir.addContent({ + 'openapi.yaml': ` + openapi: 3.0.0 + info: + title: Test API + version: 1.0.0 + paths: + /test: + get: + responses: + '200': + description: OK + `, + }); + jest.mock('../../../../../lib/openapi/helpers', () => ({ + getPathToCurrentOpenApiSpec: jest.fn(() => + Promise.resolve(path.join(inputDir.path, 'openapi.yaml')), + ), + })); + }); + it('should handle watch mode', async () => { + const generateClientMock = jest.fn(); + const generateServerMock = jest.fn(); + jest.doMock('./client', () => ({ command: generateClientMock })); + jest.doMock('./server', () => ({ command: generateServerMock })); + + const mockWatch = jest.fn(); + jest.mock('chokidar', () => ({ + watch: mockWatch, + })); + let resolve: (val?: any) => void; + const block = () => + new Promise(res => { + resolve = res; + }); + jest.mock('../../../../../lib/runner', () => ({ + block, + })); + + const mockOn: Record = {}; + mockWatch.mockReturnValue({ + on: jest.fn((event, cb) => { + console.log(event); + mockOn[event] = cb; + }), + } as any); + + const { command } = await import('./index'); + + const actions = async () => { + while (!mockOn.ready) { + // Wait for the watcher to be registered + await new Promise(r => setTimeout(r, 100)); + } + + expect(generateClientMock).toHaveBeenCalledTimes(0); + mockOn.change(); + expect(generateClientMock).toHaveBeenCalledTimes(1); + resolve(); + }; + + await Promise.all([ + actions(), + command({ watch: true, clientPackage: 'test123' }), + ]); + }); +}); diff --git a/packages/repo-tools/src/commands/package/schema/openapi/generate/index.ts b/packages/repo-tools/src/commands/package/schema/openapi/generate/index.ts index a7e9d9a402..e8a7eb7249 100644 --- a/packages/repo-tools/src/commands/package/schema/openapi/generate/index.ts +++ b/packages/repo-tools/src/commands/package/schema/openapi/generate/index.ts @@ -20,6 +20,7 @@ import { command as generateServer } from './server'; import chokidar from 'chokidar'; import { getPathToCurrentOpenApiSpec } from '../../../../../lib/openapi/helpers'; import { debounce } from 'lodash'; +import { block } from '../../../../../lib/runner'; export async function command(opts: OptionValues) { if (!opts.clientPackage && !opts.server) { @@ -74,7 +75,7 @@ export async function command(opts: OptionValues) { 'Watching for changes in OpenAPI spec. Press Ctrl+C to stop.', ); }); - await new Promise(() => {}); + await block(); } catch (err) { console.error(chalk.red('Error: ', err)); process.exit(1); diff --git a/packages/repo-tools/src/lib/runner.ts b/packages/repo-tools/src/lib/runner.ts index ea7b511938..8389caa63a 100644 --- a/packages/repo-tools/src/lib/runner.ts +++ b/packages/repo-tools/src/lib/runner.ts @@ -67,3 +67,7 @@ export async function runner( return resultsList; } + +export async function block() { + return new Promise(() => {}); +} From 879e4cb45abf34f42de3a272130a5a03945cf081 Mon Sep 17 00:00:00 2001 From: aramissennyeydd Date: Tue, 15 Oct 2024 21:16:39 -0400 Subject: [PATCH 6/7] fix error UX Signed-off-by: aramissennyeydd --- .../package/schema/openapi/generate/client.ts | 16 +++++++++++++--- .../package/schema/openapi/generate/index.ts | 17 ++++++++++++++--- .../package/schema/openapi/generate/server.ts | 19 ++++++++++++++++--- .../commands/repo/schema/openapi/verify.ts | 12 ++++++------ .../repo-tools/src/lib/openapi/helpers.ts | 10 ++++++++++ 5 files changed, 59 insertions(+), 15 deletions(-) diff --git a/packages/repo-tools/src/commands/package/schema/openapi/generate/client.ts b/packages/repo-tools/src/commands/package/schema/openapi/generate/client.ts index 66f750bfc3..742f303f51 100644 --- a/packages/repo-tools/src/commands/package/schema/openapi/generate/client.ts +++ b/packages/repo-tools/src/commands/package/schema/openapi/generate/client.ts @@ -103,7 +103,10 @@ async function generate( export async function command( outputPackage: string, clientAdditionalProperties?: string, - abortSignal?: AbortController, + { + abortSignal, + isWatch = false, + }: { abortSignal?: AbortController; isWatch?: boolean } = {}, ): Promise { try { await generate(outputPackage, clientAdditionalProperties, abortSignal); @@ -115,7 +118,14 @@ export async function command( console.debug('Server generation aborted.'); return; } - console.log(chalk.red(`Client generation failed:`)); - console.log(err); + if (isWatch) { + console.log(chalk.red(`Client generation failed:`)); + console.group(); + console.log(chalk.red(err.message)); + console.groupEnd(); + } else { + console.log(chalk.red(`Client generation failed.`)); + console.log(chalk.red(err.message)); + } } } diff --git a/packages/repo-tools/src/commands/package/schema/openapi/generate/index.ts b/packages/repo-tools/src/commands/package/schema/openapi/generate/index.ts index e8a7eb7249..1c9fd586fb 100644 --- a/packages/repo-tools/src/commands/package/schema/openapi/generate/index.ts +++ b/packages/repo-tools/src/commands/package/schema/openapi/generate/index.ts @@ -18,7 +18,10 @@ import { OptionValues } from 'commander'; import { command as generateClient } from './client'; import { command as generateServer } from './server'; import chokidar from 'chokidar'; -import { getPathToCurrentOpenApiSpec } from '../../../../../lib/openapi/helpers'; +import { + getPathToCurrentOpenApiSpec, + loadAndValidateOpenApiYaml, +} from '../../../../../lib/openapi/helpers'; import { debounce } from 'lodash'; import { block } from '../../../../../lib/runner'; @@ -31,18 +34,24 @@ export async function command(opts: OptionValues) { } const sharedCommand = async (abortSignal?: AbortController) => { + const resolvedOpenapiPath = await getPathToCurrentOpenApiSpec(); + await loadAndValidateOpenApiYaml(resolvedOpenapiPath); const promises = []; + const options = { + isWatch: opts.watch, + abortSignal, + }; if (opts.clientPackage) { promises.push( generateClient( opts.clientPackage, opts.clientAdditionalProperties, - abortSignal, + options, ), ); } if (opts.server) { - promises.push(generateServer(abortSignal)); + promises.push(generateServer(options)); } await Promise.all(promises); }; @@ -75,6 +84,8 @@ export async function command(opts: OptionValues) { 'Watching for changes in OpenAPI spec. Press Ctrl+C to stop.', ); }); + + debouncedCommand(); await block(); } catch (err) { console.error(chalk.red('Error: ', err)); diff --git a/packages/repo-tools/src/commands/package/schema/openapi/generate/server.ts b/packages/repo-tools/src/commands/package/schema/openapi/generate/server.ts index 662efe4347..1ef6fd10d2 100644 --- a/packages/repo-tools/src/commands/package/schema/openapi/generate/server.ts +++ b/packages/repo-tools/src/commands/package/schema/openapi/generate/server.ts @@ -61,7 +61,13 @@ export const createOpenApiRouter = async ( } } -export async function command(abortSignal?: AbortController): Promise { +export async function command({ + abortSignal, + isWatch = false, +}: { + abortSignal?: AbortController; + isWatch?: boolean; +}): Promise { try { await generate(abortSignal); console.log(chalk.green('Generated server files.')); @@ -70,7 +76,14 @@ export async function command(abortSignal?: AbortController): Promise { console.debug('Server generation aborted.'); return; } - console.log(chalk.red(`OpenAPI server stub generation failed.`)); - console.log(err.message); + if (isWatch) { + console.log(chalk.red(`Server generation failed:`)); + console.group(); + console.log(chalk.red(err.message)); + console.groupEnd(); + } else { + console.log(chalk.red(err.message)); + console.log(chalk.red(`OpenAPI server stub generation failed.`)); + } } } diff --git a/packages/repo-tools/src/commands/repo/schema/openapi/verify.ts b/packages/repo-tools/src/commands/repo/schema/openapi/verify.ts index 23e07d1230..35dd3e1702 100644 --- a/packages/repo-tools/src/commands/repo/schema/openapi/verify.ts +++ b/packages/repo-tools/src/commands/repo/schema/openapi/verify.ts @@ -15,12 +15,10 @@ */ import fs from 'fs-extra'; -import YAML from 'js-yaml'; -import { isEqual, cloneDeep } from 'lodash'; +import { isEqual } from 'lodash'; import { join } from 'path'; import chalk from 'chalk'; import { relative as relativePath, resolve as resolvePath } from 'path'; -import Parser from '@apidevtools/swagger-parser'; import { runner } from '../../../../lib/runner'; import { paths as cliPaths } from '../../../../lib/paths'; import { @@ -28,7 +26,10 @@ import { TS_SCHEMA_PATH, YAML_SCHEMA_PATH, } from '../../../../lib/openapi/constants'; -import { getPathToOpenApiSpec } from '../../../../lib/openapi/helpers'; +import { + getPathToOpenApiSpec, + loadAndValidateOpenApiYaml, +} from '../../../../lib/openapi/helpers'; async function verify(directoryPath: string) { let openapiPath = ''; @@ -38,8 +39,7 @@ async function verify(directoryPath: string) { // Unable to find spec at path. return; } - const yaml = YAML.load(await fs.readFile(openapiPath, 'utf8')); - await Parser.validate(cloneDeep(yaml) as any); + const yaml = await loadAndValidateOpenApiYaml(openapiPath); const schemaPath = join(directoryPath, TS_SCHEMA_PATH); if (!(await fs.pathExists(schemaPath))) { diff --git a/packages/repo-tools/src/lib/openapi/helpers.ts b/packages/repo-tools/src/lib/openapi/helpers.ts index f7e684a407..2359a1bbb0 100644 --- a/packages/repo-tools/src/lib/openapi/helpers.ts +++ b/packages/repo-tools/src/lib/openapi/helpers.ts @@ -18,6 +18,10 @@ import { pathExists } from 'fs-extra'; import { paths } from '../paths'; import { YAML_SCHEMA_PATH } from './constants'; import { resolve } from 'path'; +import YAML from 'js-yaml'; +import { cloneDeep } from 'lodash'; +import Parser from '@apidevtools/swagger-parser'; +import fs from 'fs-extra'; export const getPathToFile = async (directory: string, filename: string) => { return resolve(directory, filename); @@ -41,3 +45,9 @@ export const getPathToOpenApiSpec = async (directory: string) => { export const getPathToCurrentOpenApiSpec = async () => { return await assertExists(await getRelativePathToFile(YAML_SCHEMA_PATH)); }; + +export async function loadAndValidateOpenApiYaml(path: string) { + const yaml = YAML.load(await fs.readFile(path, 'utf8')); + await Parser.validate(cloneDeep(yaml) as any); + return yaml; +} From 87e7ba1a1e3d0e7f2197448852c1c58ebc948919 Mon Sep 17 00:00:00 2001 From: aramissennyeydd Date: Tue, 15 Oct 2024 21:28:58 -0400 Subject: [PATCH 7/7] fix test case Signed-off-by: aramissennyeydd --- .../commands/package/schema/openapi/generate/index.test.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/repo-tools/src/commands/package/schema/openapi/generate/index.test.ts b/packages/repo-tools/src/commands/package/schema/openapi/generate/index.test.ts index 21a11c6165..73d26924be 100644 --- a/packages/repo-tools/src/commands/package/schema/openapi/generate/index.test.ts +++ b/packages/repo-tools/src/commands/package/schema/openapi/generate/index.test.ts @@ -52,6 +52,7 @@ describe('generateOpenApiSchema', () => { getPathToCurrentOpenApiSpec: jest.fn(() => Promise.resolve(path.join(inputDir.path, 'openapi.yaml')), ), + loadAndValidateOpenApiYaml: jest.fn(), })); }); it('should handle watch mode', async () => { @@ -89,9 +90,11 @@ describe('generateOpenApiSchema', () => { await new Promise(r => setTimeout(r, 100)); } - expect(generateClientMock).toHaveBeenCalledTimes(0); - mockOn.change(); expect(generateClientMock).toHaveBeenCalledTimes(1); + mockOn.change(); + // Wait for the debounce to finish with the initial load. + await new Promise(r => setTimeout(r, 500)); + expect(generateClientMock).toHaveBeenCalledTimes(2); resolve(); };