From 43a2b287f202f03e1cdbdbcb7cface160268b76e Mon Sep 17 00:00:00 2001 From: Sean Ferguson Date: Fri, 13 Oct 2023 18:28:19 -0400 Subject: [PATCH] move OpenAPI codegen to @signal24/openapi-client-codegen --- .npmignore | 2 +- package.json | 9 +- src/helpers/openapi.ts | 134 ++------------------ src/vite-plugins/vite-openapi-plugin.cli.ts | 19 --- src/vite-plugins/vite-openapi-plugin.ts | 121 +----------------- yarn.lock | 30 +++-- 6 files changed, 38 insertions(+), 277 deletions(-) delete mode 100755 src/vite-plugins/vite-openapi-plugin.cli.ts diff --git a/.npmignore b/.npmignore index 7eef75e..e11c661 100644 --- a/.npmignore +++ b/.npmignore @@ -2,4 +2,4 @@ .eslintcache node_modules .vscode -yarn.lock +tsconfig.tsbuildinfo diff --git a/package.json b/package.json index 2a2c6f8..53d5c05 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,9 @@ { "name": "@signal24/vue-foundation", "type": "module", - "version": "4.7.3", + "version": "4.7.4", "description": "Common components, directives, and helpers for Vue 3 apps", "module": "./dist/vue-foundation.es.js", - "bin": { - "vf-generate-openapi-client": "./dist/src/vite-plugins/vite-openapi-plugin.cli.js" - }, "exports": { ".": { "import": "./dist/vue-foundation.es.js", @@ -24,7 +21,7 @@ "typings": "./dist/src/index.d.ts", "scripts": { "dev": "vite", - "build": "rm -rf dist && vite build && vue-tsc --declaration --emitDeclarationOnly -p tsconfig.app.json && tsc -p tsconfig.vite-plugins.json && find dist -name '*.tsbuildinfo' -delete && chmod +x dist/src/vite-plugins/vite-openapi-plugin.cli.js", + "build": "rm -rf dist && vite build && vue-tsc --declaration --emitDeclarationOnly -p tsconfig.app.json && tsc -p tsconfig.vite-plugins.json && find dist -name '*.tsbuildinfo' -delete", "build:watch": "fswatch -o src | xargs -n1 -I{} yarn build", "preview": "vite preview", "test:types": "vue-tsc --noEmit -p tsconfig.vitest.json --composite false", @@ -46,6 +43,7 @@ "devDependencies": { "@nabla/vite-plugin-eslint": "^1.5.0", "@rushstack/eslint-patch": "^1.3.2", + "@signal24/openapi-client-codegen": "^1.0.4", "@tsconfig/node18": "^18.2.0", "@types/jsdom": "^21.1.1", "@types/lodash": "^4.14.196", @@ -63,7 +61,6 @@ "eslint-plugin-unused-imports": "^3.0.0", "eslint-plugin-vue": "^9.16.1", "jsdom": "^22.1.0", - "openapi-typescript-codegen": "^0.25.0", "prettier": "^3.0.0", "sass": "^1.64.2", "start-server-and-test": "^2.0.0", diff --git a/src/helpers/openapi.ts b/src/helpers/openapi.ts index afe30a3..e33ef5a 100644 --- a/src/helpers/openapi.ts +++ b/src/helpers/openapi.ts @@ -1,130 +1,16 @@ -import { UserError } from './error'; +import { installOpenApiClientInterceptors, isOpenApiError } from '@signal24/openapi-client-codegen/helpers'; -interface IRequestOptions { - readonly method: 'GET' | 'PUT' | 'POST' | 'DELETE' | 'OPTIONS' | 'HEAD' | 'PATCH'; - readonly url: string; - readonly path?: Record; - readonly cookies?: Record; - readonly headers?: Record; - readonly query?: Record; - readonly formData?: Record; - readonly body?: any; - readonly mediaType?: string; - readonly responseHeader?: string; - readonly errors?: Record; -} - -interface IBaseHttpRequest { - request(options: IRequestOptions): ICancelablePromise; -} - -export interface IApiClient { - request: IBaseHttpRequest; -} - -export interface IApiError extends Error { - status: number; - statusText: string; - body: any; -} - -export declare class ICancelablePromise { - constructor(executor: (resolve: (value: any) => void, reject: (reason: any) => void, onCancel: (cancel: () => void) => void) => void); - then( - onfulfilled?: ((value: T) => TResult1 | PromiseLike) | undefined | null, - onrejected?: ((reason: any) => TResult2 | PromiseLike) | undefined | null - ): Promise; - catch(onrejected?: ((reason: any) => TResult | PromiseLike) | undefined | null): Promise; - finally(onfinally?: (() => void) | undefined | null): Promise; - cancel(): void; -} - -interface IWrappedApiClientOptions

{ - apiClient: IApiClient; - wrapper?: (options: IRequestOptions, fn: (options: IRequestOptions) => P) => P; - onRequest?: (options: IRequestOptions) => IRequestOptions; - onError?: (err: Error, options: IRequestOptions) => Error | null | void; - afterRequest?: (options: IRequestOptions) => void; - CancelablePromise: new (...arguments_: Arguments) => P; -} +import { UserError } from '.'; -export function isApiError(err: any): err is IApiError { - return err instanceof Error && 'status' in err && 'body' in err; -} - -export function installApiClientInterceptors({ apiClient, wrapper, onRequest, onError, afterRequest, CancelablePromise }: IWrappedApiClientOptions) { - const originalRequest = apiClient.request.request.bind(apiClient.request); - const resolvedWrapper = wrapper ?? ((options, fn) => fn(options)); - apiClient.request.request = (options: IRequestOptions) => { - return resolvedWrapper(options, options => { - options = rewriteOptionsForFileUpload(options); - - if (onRequest) { - options = onRequest(options); +export function installApiClientInterceptors(clientOptions: Parameters[0]) { + installOpenApiClientInterceptors({ + ...clientOptions, + onError(err, options) { + if (isOpenApiError(err) && err.status === 422 && typeof err.body === 'object' && 'error' in err.body) { + err = new UserError(err.body.error); } - return new CancelablePromise((resolve: (value: any) => void, reject: (err: any) => void, onCancel: (handler: () => void) => void) => { - const promise = originalRequest(options); - onCancel(promise.cancel); - promise - .then(resolve) - .catch(err => { - if (isApiError(err) && typeof err.body === 'object' && 'error' in err.body) { - if (err.status === 422) { - return reject(new UserError(err.body.error)); - } - - err.message = `${err.body.error} (${err.status})`; - } - if (onError) { - const handlerResult = onError(err, options); - if (handlerResult === null) { - return; - } - if (handlerResult instanceof Error) { - return reject(handlerResult); - } - } - reject(err); - }) - .finally(() => afterRequest?.(options)); - }); - }); - }; -} - -export class FileUploadRequest { - constructor(blob: Blob) { - this.blob = blob; - } - - validator = null; - lastModifiedDate = null; - size = 0; - path = ''; - name = ''; - type = ''; - blob!: Blob; -} - -function rewriteOptionsForFileUpload(options: IRequestOptions): IRequestOptions { - const hasFileUpload = typeof options.body === 'object' && Object.values(options.body).some(v => v instanceof FileUploadRequest); - if (!hasFileUpload) return options; - - const formData: Record = {}; - const jsonBody: Record = {}; - for (const [key, value] of Object.entries(options.body)) { - if (value instanceof FileUploadRequest) { - formData[key] = value.blob; - } else { - jsonBody[key] = value; + clientOptions.onError?.(err, options); } - } - formData._payload = new Blob([JSON.stringify(jsonBody)], { type: 'application/json' }); - - return { - ...options, - body: undefined, - formData - }; + }); } diff --git a/src/vite-plugins/vite-openapi-plugin.cli.ts b/src/vite-plugins/vite-openapi-plugin.cli.ts deleted file mode 100755 index b16aaa2..0000000 --- a/src/vite-plugins/vite-openapi-plugin.cli.ts +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env node - -import { existsSync } from 'fs'; - -import { generateConfiguredOpenapiClients, generateOpenapiClient } from './vite-openapi-plugin.js'; - -if (process.argv[2]) { - if (process.argv[2] === '--help') { - throw new Error('Usage: vf-generate-openapi-client [ []]'); - } - - if (!existsSync(process.argv[2])) { - throw new Error(`OpenAPI YAML file not found: ${process.argv[2]}`); - } - - await generateOpenapiClient(process.argv[2], process.argv[3]); -} else { - generateConfiguredOpenapiClients(); -} diff --git a/src/vite-plugins/vite-openapi-plugin.ts b/src/vite-plugins/vite-openapi-plugin.ts index b9a49d5..6f22bbf 100644 --- a/src/vite-plugins/vite-openapi-plugin.ts +++ b/src/vite-plugins/vite-openapi-plugin.ts @@ -1,48 +1,4 @@ -import { createHash } from 'node:crypto'; -import { copyFileSync, existsSync, readFileSync, watch } from 'node:fs'; -import { rm } from 'node:fs/promises'; - -import * as OpenAPI from 'openapi-typescript-codegen'; - -const DEFAULT_OUT_PATH = './src/openapi-client-generated'; - -let generatedHash: string | null = null; -let generatorMap: Record = {}; -let overridesMap: Record | null = null; -let overridesInverseMap: Record | null = null; - -export function loadOpenapiConfig() { - loadGeneratorMap(); - loadOverridesMap(); -} - -function loadGeneratorMap() { - if (!existsSync('./openapi-specs.json')) { - console.error('openapi-specs.json not found. Cannot generate OpenAPI client.'); - return; - } - - try { - const specsContent = readFileSync('./openapi-specs.json', 'utf8'); - generatorMap = JSON.parse(specsContent); - } catch (e) { - console.error('Failed to load openapi-specs.json:', e); - } -} - -function loadOverridesMap() { - if (!existsSync('./openapi-specs.dev.json')) { - return; - } - - try { - const overridesContent = readFileSync('./openapi-specs.dev.json', 'utf8'); - overridesMap = JSON.parse(overridesContent); - overridesInverseMap = Object.fromEntries(Object.entries(overridesMap!).map(([k, v]) => [v, k])); - } catch (e) { - console.error('Failed to load openapi-specs.dev.json:', e); - } -} +import { createWatchfulOpenapiClientGenerators } from '@signal24/openapi-client-codegen/generator'; export function openapiClientGeneratorPlugin(): { name: string; @@ -50,7 +6,7 @@ export function openapiClientGeneratorPlugin(): { buildStart(): void; closeBundle(): void; } { - let generators: ReturnType | null = null; + let generators: ReturnType | null = null; return { name: 'openapi-types-generator', @@ -59,7 +15,7 @@ export function openapiClientGeneratorPlugin(): { buildStart() { // apply a slight delay so any output doesn't get pushed off screen setTimeout(() => { - generators = createWatchfulGenerators(); + generators = createWatchfulOpenapiClientGenerators(); }, 250); }, @@ -72,74 +28,3 @@ export function openapiClientGeneratorPlugin(): { } }; } - -function createWatchfulGenerators() { - loadOpenapiConfig(); - return Object.entries(generatorMap).map(([openapiYamlPath, outPath]) => createWatchfulGenerator(openapiYamlPath, outPath)); -} - -function createWatchfulGenerator(openapiYamlPath: string, outPath: string) { - const resolvedPath = overridesMap?.[openapiYamlPath] ?? openapiYamlPath; - - if (!existsSync(resolvedPath)) { - console.log(`OpenAPI YAML file not found: ${resolvedPath}`); - return null; - } - - const watcher = watch(resolvedPath); - watcher.on('change', () => { - // give the writes a moment to settle - setTimeout(() => generateOpenapiClient(resolvedPath, outPath), 100); - }); - - generateOpenapiClient(resolvedPath, outPath); - - return { - close() { - watcher.close(); - } - }; -} - -export async function generateConfiguredOpenapiClients() { - loadOpenapiConfig(); - for (const [openapiYamlPath, outPath] of Object.entries(generatorMap)) { - const resolvedPath = overridesMap?.[openapiYamlPath] ?? openapiYamlPath; - await generateOpenapiClient(resolvedPath, outPath); - } -} - -export async function generateOpenapiClient(openapiYamlPath: string, outPath: string = DEFAULT_OUT_PATH) { - const yaml = readFileSync(openapiYamlPath, 'utf8'); - const hash = createHash('sha256').update(yaml).digest('hex'); - - if (hash === generatedHash) { - return; - } - - generatedHash = hash; - - try { - try { - await rm(outPath, { recursive: true }); - } catch (e) { - // ignore - } - - await OpenAPI.generate({ - input: openapiYamlPath, - output: outPath, - clientName: 'ApiClient', - useOptions: true, - useUnionTypes: true - }); - - if (overridesInverseMap?.[openapiYamlPath]) { - copyFileSync(openapiYamlPath, overridesInverseMap[openapiYamlPath]); - } - - console.log(`[${new Date().toISOString()}] Generated client from ${openapiYamlPath} to ${outPath}/`); - } catch (err) { - console.error(`[${new Date().toISOString()}] Error generating client from ${openapiYamlPath}:`, err); - } -} diff --git a/yarn.lock b/yarn.lock index 2f08463..c80fe7c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -444,6 +444,13 @@ resolved "https://registry.yarnpkg.com/@sideway/pinpoint/-/pinpoint-2.0.0.tgz#cff8ffadc372ad29fd3f78277aeb29e632cc70df" integrity sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ== +"@signal24/openapi-client-codegen@^1.0.4": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@signal24/openapi-client-codegen/-/openapi-client-codegen-1.0.4.tgz#d5cddd05e6df0c331fc12c110f39b1322bd562a6" + integrity sha512-7BbfdmXvlGwHobfrkEMUMr1egv8NzuwxdrR2Xt7Gud43hooG9NHg8knqby7BdgzJvJFyRCN1zswbKkz40YT8ig== + dependencies: + openapi-typescript-codegen "^0.25.0" + "@sinclair/typebox@^0.27.8": version "0.27.8" resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.27.8.tgz#6667fac16c436b5434a387a34dedb013198f6e6e" @@ -498,11 +505,16 @@ "@types/tough-cookie" "*" parse5 "^7.0.0" -"@types/json-schema@*", "@types/json-schema@^7.0.6", "@types/json-schema@^7.0.9": +"@types/json-schema@*", "@types/json-schema@^7.0.9": version "7.0.11" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3" integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ== +"@types/json-schema@^7.0.6": + version "7.0.13" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.13.tgz#02c24f4363176d2d18fc8b70b9f3c54aba178a85" + integrity sha512-RbSSoHliUbnXj3ny0CNFOoxrIDV6SUGyStHsvDqosw6CkdPV8TtWGlfecuK4ToyMEAql6pzNxgCFKanovUzlgQ== + "@types/lodash@^4.14.196": version "4.14.196" resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.196.tgz#a7c3d6fc52d8d71328b764e28e080b4169ec7a95" @@ -1256,9 +1268,9 @@ commander@^10.0.0: integrity sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug== commander@^11.0.0: - version "11.0.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-11.0.0.tgz#43e19c25dbedc8256203538e8d7e9346877a6f67" - integrity sha512-9HMlXtt/BNoYr8ooyjjNRdIilOTkVJXB+GhxMTtOKwk0R4j4lS4NpjuqmRxroBfnfTSHQIHQB7wryHhXarNjmQ== + version "11.1.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-11.1.0.tgz#62fdce76006a68e5c1ab3314dc92e800eb83d906" + integrity sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ== commander@^6.2.1: version "6.2.1" @@ -2162,12 +2174,12 @@ graphemer@^1.4.0: integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== handlebars@^4.7.7: - version "4.7.7" - resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.7.tgz#9ce33416aad02dbd6c8fafa8240d5d98004945a1" - integrity sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA== + version "4.7.8" + resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.8.tgz#41c42c18b1be2365439188c77c6afae71c0cd9e9" + integrity sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ== dependencies: minimist "^1.2.5" - neo-async "^2.6.0" + neo-async "^2.6.2" source-map "^0.6.1" wordwrap "^1.0.0" optionalDependencies: @@ -2755,7 +2767,7 @@ natural-compare@^1.4.0: resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== -neo-async@^2.6.0: +neo-async@^2.6.2: version "2.6.2" resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==