Skip to content

Commit

Permalink
move OpenAPI codegen to @signal24/openapi-client-codegen
Browse files Browse the repository at this point in the history
  • Loading branch information
fergusean committed Oct 14, 2023
1 parent a35b7a0 commit 43a2b28
Show file tree
Hide file tree
Showing 6 changed files with 38 additions and 277 deletions.
2 changes: 1 addition & 1 deletion .npmignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
.eslintcache
node_modules
.vscode
yarn.lock
tsconfig.tsbuildinfo
9 changes: 3 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand Down
134 changes: 10 additions & 124 deletions src/helpers/openapi.ts
Original file line number Diff line number Diff line change
@@ -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<string, any>;
readonly cookies?: Record<string, any>;
readonly headers?: Record<string, any>;
readonly query?: Record<string, any>;
readonly formData?: Record<string, any>;
readonly body?: any;
readonly mediaType?: string;
readonly responseHeader?: string;
readonly errors?: Record<number, string>;
}

interface IBaseHttpRequest {
request<T>(options: IRequestOptions): ICancelablePromise<T>;
}

export interface IApiClient {
request: IBaseHttpRequest;
}

export interface IApiError extends Error {
status: number;
statusText: string;
body: any;
}

export declare class ICancelablePromise<T = any> {
constructor(executor: (resolve: (value: any) => void, reject: (reason: any) => void, onCancel: (cancel: () => void) => void) => void);
then<TResult1 = any, TResult2 = never>(
onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | undefined | null,
onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | undefined | null
): Promise<TResult1 | TResult2>;
catch<TResult = never>(onrejected?: ((reason: any) => TResult | PromiseLike<TResult>) | undefined | null): Promise<T | TResult>;
finally(onfinally?: (() => void) | undefined | null): Promise<T>;
cancel(): void;
}

interface IWrappedApiClientOptions<P extends ICancelablePromise = ICancelablePromise, Arguments extends unknown[] = any[]> {
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<typeof installOpenApiClientInterceptors>[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<string, any> = {};
const jsonBody: Record<string, any> = {};
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
};
});
}
19 changes: 0 additions & 19 deletions src/vite-plugins/vite-openapi-plugin.cli.ts

This file was deleted.

121 changes: 3 additions & 118 deletions src/vite-plugins/vite-openapi-plugin.ts
Original file line number Diff line number Diff line change
@@ -1,56 +1,12 @@
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<string, string> = {};
let overridesMap: Record<string, string> | null = null;
let overridesInverseMap: Record<string, string> | 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;
apply: 'serve';
buildStart(): void;
closeBundle(): void;
} {
let generators: ReturnType<typeof createWatchfulGenerators> | null = null;
let generators: ReturnType<typeof createWatchfulOpenapiClientGenerators> | null = null;

return {
name: 'openapi-types-generator',
Expand All @@ -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);
},

Expand All @@ -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);
}
}
Loading

0 comments on commit 43a2b28

Please sign in to comment.