diff --git a/.changeset/eager-bobcats-doubt.md b/.changeset/eager-bobcats-doubt.md new file mode 100644 index 0000000000..0ee67f15a7 --- /dev/null +++ b/.changeset/eager-bobcats-doubt.md @@ -0,0 +1,5 @@ +--- +'@hey-api/openapi-ts': patch +--- + +feat(parser): add `events` hooks diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@hey-api/transformers/type-format-valibot/transformers.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@hey-api/transformers/type-format-valibot/transformers.gen.ts index ea991c983e..cc9dcf8412 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@hey-api/transformers/type-format-valibot/transformers.gen.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@hey-api/transformers/type-format-valibot/transformers.gen.ts @@ -2,12 +2,12 @@ import type { PostFooResponse } from './types.gen'; -export const postFooResponseTransformer = async (data: any): Promise => { - data = fooSchemaResponseTransformer(data); +const fooSchemaResponseTransformer = (data: any) => { + data.foo = BigInt(data.foo.toString()); return data; }; -const fooSchemaResponseTransformer = (data: any) => { - data.foo = BigInt(data.foo.toString()); +export const postFooResponseTransformer = async (data: any): Promise => { + data = fooSchemaResponseTransformer(data); return data; }; diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@hey-api/transformers/type-format-zod/transformers.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@hey-api/transformers/type-format-zod/transformers.gen.ts index ea991c983e..cc9dcf8412 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@hey-api/transformers/type-format-zod/transformers.gen.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@hey-api/transformers/type-format-zod/transformers.gen.ts @@ -2,12 +2,12 @@ import type { PostFooResponse } from './types.gen'; -export const postFooResponseTransformer = async (data: any): Promise => { - data = fooSchemaResponseTransformer(data); +const fooSchemaResponseTransformer = (data: any) => { + data.foo = BigInt(data.foo.toString()); return data; }; -const fooSchemaResponseTransformer = (data: any) => { - data.foo = BigInt(data.foo.toString()); +export const postFooResponseTransformer = async (data: any): Promise => { + data = fooSchemaResponseTransformer(data); return data; }; diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/plugins/@hey-api/transformers/type-format-valibot/transformers.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/plugins/@hey-api/transformers/type-format-valibot/transformers.gen.ts index ea991c983e..cc9dcf8412 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/plugins/@hey-api/transformers/type-format-valibot/transformers.gen.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/plugins/@hey-api/transformers/type-format-valibot/transformers.gen.ts @@ -2,12 +2,12 @@ import type { PostFooResponse } from './types.gen'; -export const postFooResponseTransformer = async (data: any): Promise => { - data = fooSchemaResponseTransformer(data); +const fooSchemaResponseTransformer = (data: any) => { + data.foo = BigInt(data.foo.toString()); return data; }; -const fooSchemaResponseTransformer = (data: any) => { - data.foo = BigInt(data.foo.toString()); +export const postFooResponseTransformer = async (data: any): Promise => { + data = fooSchemaResponseTransformer(data); return data; }; diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/plugins/@hey-api/transformers/type-format-zod/transformers.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/plugins/@hey-api/transformers/type-format-zod/transformers.gen.ts index ea991c983e..cc9dcf8412 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/plugins/@hey-api/transformers/type-format-zod/transformers.gen.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/plugins/@hey-api/transformers/type-format-zod/transformers.gen.ts @@ -2,12 +2,12 @@ import type { PostFooResponse } from './types.gen'; -export const postFooResponseTransformer = async (data: any): Promise => { - data = fooSchemaResponseTransformer(data); +const fooSchemaResponseTransformer = (data: any) => { + data.foo = BigInt(data.foo.toString()); return data; }; -const fooSchemaResponseTransformer = (data: any) => { - data.foo = BigInt(data.foo.toString()); +export const postFooResponseTransformer = async (data: any): Promise => { + data = fooSchemaResponseTransformer(data); return data; }; diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/transformers-all-of/transformers.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/transformers-all-of/transformers.gen.ts index 36c9e46797..9336baf5a1 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/transformers-all-of/transformers.gen.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/transformers-all-of/transformers.gen.ts @@ -2,15 +2,16 @@ import type { GetFooResponse } from './types.gen'; -export const getFooResponseTransformer = async (data: any): Promise => { - data = fooSchemaResponseTransformer(data); +const quxSchemaResponseTransformer = (data: any) => { + if (data.baz) { + data.baz = new Date(data.baz); + } return data; }; -const fooSchemaResponseTransformer = (data: any) => { - data.foo = data.foo.map((item: any) => { - return barSchemaResponseTransformer(item); - }); +const bazSchemaResponseTransformer = (data: any) => { + data = quxSchemaResponseTransformer(data); + data.bar = new Date(data.bar); return data; }; @@ -21,15 +22,14 @@ const barSchemaResponseTransformer = (data: any) => { return data; }; -const bazSchemaResponseTransformer = (data: any) => { - data = quxSchemaResponseTransformer(data); - data.bar = new Date(data.bar); +const fooSchemaResponseTransformer = (data: any) => { + data.foo = data.foo.map((item: any) => { + return barSchemaResponseTransformer(item); + }); return data; }; -const quxSchemaResponseTransformer = (data: any) => { - if (data.baz) { - data.baz = new Date(data.baz); - } +export const getFooResponseTransformer = async (data: any): Promise => { + data = fooSchemaResponseTransformer(data); return data; }; diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/transformers-any-of-null/transformers.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/transformers-any-of-null/transformers.gen.ts index dad31f0d02..62f0dd462c 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/transformers-any-of-null/transformers.gen.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/transformers-any-of-null/transformers.gen.ts @@ -2,13 +2,6 @@ import type { GetFooResponse } from './types.gen'; -export const getFooResponseTransformer = async (data: any): Promise => { - data = data.map((item: any) => { - return fooSchemaResponseTransformer(item); - }); - return data; -}; - const fooSchemaResponseTransformer = (data: any) => { if (data.foo) { data.foo = new Date(data.foo); @@ -21,3 +14,10 @@ const fooSchemaResponseTransformer = (data: any) => { } return data; }; + +export const getFooResponseTransformer = async (data: any): Promise => { + data = data.map((item: any) => { + return fooSchemaResponseTransformer(item); + }); + return data; +}; diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/plugins/@hey-api/transformers/type-format-valibot/transformers.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/plugins/@hey-api/transformers/type-format-valibot/transformers.gen.ts index ea991c983e..cc9dcf8412 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/plugins/@hey-api/transformers/type-format-valibot/transformers.gen.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/plugins/@hey-api/transformers/type-format-valibot/transformers.gen.ts @@ -2,12 +2,12 @@ import type { PostFooResponse } from './types.gen'; -export const postFooResponseTransformer = async (data: any): Promise => { - data = fooSchemaResponseTransformer(data); +const fooSchemaResponseTransformer = (data: any) => { + data.foo = BigInt(data.foo.toString()); return data; }; -const fooSchemaResponseTransformer = (data: any) => { - data.foo = BigInt(data.foo.toString()); +export const postFooResponseTransformer = async (data: any): Promise => { + data = fooSchemaResponseTransformer(data); return data; }; diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/plugins/@hey-api/transformers/type-format-zod/transformers.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/plugins/@hey-api/transformers/type-format-zod/transformers.gen.ts index ea991c983e..cc9dcf8412 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/plugins/@hey-api/transformers/type-format-zod/transformers.gen.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/plugins/@hey-api/transformers/type-format-zod/transformers.gen.ts @@ -2,12 +2,12 @@ import type { PostFooResponse } from './types.gen'; -export const postFooResponseTransformer = async (data: any): Promise => { - data = fooSchemaResponseTransformer(data); +const fooSchemaResponseTransformer = (data: any) => { + data.foo = BigInt(data.foo.toString()); return data; }; -const fooSchemaResponseTransformer = (data: any) => { - data.foo = BigInt(data.foo.toString()); +export const postFooResponseTransformer = async (data: any): Promise => { + data = fooSchemaResponseTransformer(data); return data; }; diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/transformers-all-of/transformers.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/transformers-all-of/transformers.gen.ts index 36c9e46797..9336baf5a1 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/transformers-all-of/transformers.gen.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/transformers-all-of/transformers.gen.ts @@ -2,15 +2,16 @@ import type { GetFooResponse } from './types.gen'; -export const getFooResponseTransformer = async (data: any): Promise => { - data = fooSchemaResponseTransformer(data); +const quxSchemaResponseTransformer = (data: any) => { + if (data.baz) { + data.baz = new Date(data.baz); + } return data; }; -const fooSchemaResponseTransformer = (data: any) => { - data.foo = data.foo.map((item: any) => { - return barSchemaResponseTransformer(item); - }); +const bazSchemaResponseTransformer = (data: any) => { + data = quxSchemaResponseTransformer(data); + data.bar = new Date(data.bar); return data; }; @@ -21,15 +22,14 @@ const barSchemaResponseTransformer = (data: any) => { return data; }; -const bazSchemaResponseTransformer = (data: any) => { - data = quxSchemaResponseTransformer(data); - data.bar = new Date(data.bar); +const fooSchemaResponseTransformer = (data: any) => { + data.foo = data.foo.map((item: any) => { + return barSchemaResponseTransformer(item); + }); return data; }; -const quxSchemaResponseTransformer = (data: any) => { - if (data.baz) { - data.baz = new Date(data.baz); - } +export const getFooResponseTransformer = async (data: any): Promise => { + data = fooSchemaResponseTransformer(data); return data; }; diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/transformers-any-of-null/transformers.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/transformers-any-of-null/transformers.gen.ts index 820d528e10..c3a28994ce 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/transformers-any-of-null/transformers.gen.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/transformers-any-of-null/transformers.gen.ts @@ -2,13 +2,6 @@ import type { GetFooResponse, NestedDateObjectResponse } from './types.gen'; -export const getFooResponseTransformer = async (data: any): Promise => { - data = data.map((item: any) => { - return fooSchemaResponseTransformer(item); - }); - return data; -}; - const fooSchemaResponseTransformer = (data: any) => { if (data.foo) { data.foo = new Date(data.foo); @@ -25,8 +18,10 @@ const fooSchemaResponseTransformer = (data: any) => { return data; }; -export const nestedDateObjectResponseTransformer = async (data: any): Promise => { - data = nestedDateObjectSchemaResponseTransformer(data); +export const getFooResponseTransformer = async (data: any): Promise => { + data = data.map((item: any) => { + return fooSchemaResponseTransformer(item); + }); return data; }; @@ -38,3 +33,8 @@ const nestedDateObjectSchemaResponseTransformer = (data: any) => { } return data; }; + +export const nestedDateObjectResponseTransformer = async (data: any): Promise => { + data = nestedDateObjectSchemaResponseTransformer(data); + return data; +}; diff --git a/packages/openapi-ts-tests/main/test/openapi-ts.config.ts b/packages/openapi-ts-tests/main/test/openapi-ts.config.ts index fb1858316c..ebde3fa6fa 100644 --- a/packages/openapi-ts-tests/main/test/openapi-ts.config.ts +++ b/packages/openapi-ts-tests/main/test/openapi-ts.config.ts @@ -40,11 +40,11 @@ export default defineConfig(() => { // 'dutchie.json', // 'invalid', // 'openai.yaml', - 'full.yaml', + // 'full.yaml', // 'opencode.yaml', // 'sdk-instance.yaml', // 'string-with-format.yaml', - // 'transformers.json', + 'transformers.json', // 'type-format.yaml', // 'validators.yaml', // 'validators-circular-ref.json', @@ -124,6 +124,26 @@ export default defineConfig(() => { // }, }, hooks: { + events: { + // 'plugin:handler:after': ({ plugin }) => { + // console.log(`(${plugin.name}): handler finished`); + // }, + // 'plugin:handler:before': ({ plugin }) => { + // console.log(`(${plugin.name}): handler starting`); + // }, + // 'symbol:register:after': ({ plugin, symbol }) => { + // console.log(`(global, ${plugin.name}) registered:`, symbol.id); + // }, + // 'symbol:register:before': ({ plugin, symbol }) => { + // console.log(`(global, ${plugin.name}):`, symbol.name); + // }, + // 'symbol:setValue:after': ({ plugin, symbol }) => { + // console.log(`(${plugin.name}) set value:`, symbol.id); + // }, + // 'symbol:setValue:before': ({ plugin, symbol }) => { + // console.log(`(${plugin.name}) setting value:`, symbol.id); + // }, + }, operations: { getKind() { // noop @@ -237,8 +257,8 @@ export default defineConfig(() => { // transformer: '@hey-api/transformers', // transformer: true, validator: { - request: 'arktype', - response: 'arktype', + request: 'zod', + response: 'zod', }, '~hooks': { symbols: { @@ -326,6 +346,14 @@ export default defineConfig(() => { // name: 'q{{name}}CoolWebhook', // }, '~hooks': { + events: { + // 'symbol:register:after': ({ plugin, symbol }) => { + // console.log(`(${plugin.name}) registered:`, symbol.id); + // }, + // 'symbol:register:before': ({ plugin, symbol }) => { + // console.log(`(${plugin.name}):`, symbol.name); + // }, + }, symbols: { // getFilePath: (symbol) => { // if (symbol.name) { diff --git a/packages/openapi-ts/src/ir/types.d.ts b/packages/openapi-ts/src/ir/types.d.ts index 5f9ec45222..af521f54d9 100644 --- a/packages/openapi-ts/src/ir/types.d.ts +++ b/packages/openapi-ts/src/ir/types.d.ts @@ -1,5 +1,3 @@ -import type { Symbol } from '@hey-api/codegen-core'; - import type { JsonSchemaDraft2020_12 } from '~/openApi/3.1.x/types/json-schema-draft-2020-12'; import type { SecuritySchemeObject, @@ -27,101 +25,7 @@ interface IRComponentsObject { schemas?: Record; } -interface IRHooks { - /** - * Hooks specifically for overriding operations behavior. - * - * Use these to classify operations, decide which outputs to generate, - * or apply custom behavior to individual operations. - */ - operations?: { - /** - * Classify the given operation into one or more kinds. - * - * Each kind determines how we treat the operation (e.g., generating queries or mutations). - * - * **Default behavior:** - * - GET → 'query' - * - DELETE, PATCH, POST, PUT → 'mutation' - * - * **Resolution order:** - * 1. If `isQuery` or `isMutation` returns `true` or `false`, that overrides `getKind`. - * 2. If `isQuery` or `isMutation` returns `undefined`, the result of `getKind` is used. - * - * @param operation - The operation object to classify. - * @returns An array containing one or more of 'query' or 'mutation', or undefined to fallback to default behavior. - * @example - * getKind: (operation) => { - * if (operation.method === 'get' && operation.path === '/search') { - * return ['query', 'mutation']; - * } - * return; // fallback to default behavior - * } - */ - getKind?: ( - operation: IROperationObject, - ) => ReadonlyArray<'mutation' | 'query'> | undefined; - /** - * Check if the given operation should be treated as a mutation. - * - * This affects which outputs are generated for the operation. - * - * **Default behavior:** DELETE, PATCH, POST, and PUT operations are treated as mutations. - * - * **Resolution order:** If this returns `true` or `false`, it overrides `getKind`. - * If it returns `undefined`, `getKind` is used instead. - * - * @param operation - The operation object to check. - * @returns true if the operation is a mutation, false otherwise, or undefined to fallback to `getKind`. - * @example - * isMutation: (operation) => { - * if (operation.method === 'post' && operation.path === '/search') { - * return true; - * } - * return; // fallback to default behavior - } - */ - isMutation?: (operation: IROperationObject) => boolean | undefined; - /** - * Check if the given operation should be treated as a query. - * - * This affects which outputs are generated for the operation. - * - * **Default behavior:** GET operations are treated as queries. - * - * **Resolution order:** If this returns `true` or `false`, it overrides `getKind`. - * If it returns `undefined`, `getKind` is used instead. - * - * @param operation - The operation object to check. - * @returns true if the operation is a query, false otherwise, or undefined to fallback to `getKind`. - * @example - * isQuery: (operation) => { - * if (operation.method === 'post' && operation.path === '/search') { - * return true; - * } - * return; // fallback to default behavior - } - */ - isQuery?: (operation: IROperationObject) => boolean | undefined; - }; - /** - * Hooks specifically for overriding symbols behavior. - * - * Use these to customize file placement. - */ - symbols?: { - /** - * Optional output strategy to override default plugin behavior. - * - * Use this to route generated symbols to specific files. - * - * @returns The file path to output the symbol to, or undefined to fallback to default behavior. - */ - getFilePath?: (symbol: Symbol) => string | undefined; - }; -} - -interface IROperationObject { +export interface IROperationObject { body?: IRBodyObject; deprecated?: boolean; description?: string; @@ -318,7 +222,6 @@ export namespace IR { export type BodyObject = IRBodyObject; export type ComponentsObject = IRComponentsObject; export type Context = any> = IRContext; - export type Hooks = IRHooks; export type Model = IRModel; export type OperationObject = IROperationObject; export type ParameterObject = IRParameterObject; diff --git a/packages/openapi-ts/src/parser/types/hooks.d.ts b/packages/openapi-ts/src/parser/types/hooks.d.ts new file mode 100644 index 0000000000..2095b35cfb --- /dev/null +++ b/packages/openapi-ts/src/parser/types/hooks.d.ts @@ -0,0 +1,189 @@ +import type { Symbol, SymbolIn } from '@hey-api/codegen-core'; + +import type { IROperationObject } from '~/ir/types'; +import type { PluginInstance } from '~/plugins/shared/utils/instance'; + +export type Hooks = { + /** + * Event hooks. + */ + events?: { + /** + * Triggered after executing a plugin handler. + * + * @param args Arguments object. + * @returns void + */ + 'plugin:handler:after'?: (args: { + /** Plugin that just executed. */ + plugin: PluginInstance; + }) => void; + /** + * Triggered before executing a plugin handler. + * + * @param args Arguments object. + * @returns void + */ + 'plugin:handler:before'?: (args: { + /** Plugin about to execute. */ + plugin: PluginInstance; + }) => void; + /** + * Triggered after registering a symbol. + * + * You can use this to perform actions after a symbol is registered. + * + * @param args Arguments object. + * @returns void + */ + 'symbol:register:after'?: (args: { + /** Plugin that registered the symbol. */ + plugin: PluginInstance; + /** The registered symbol. */ + symbol: Symbol; + }) => void; + /** + * Triggered before registering a symbol. + * + * You can use this to modify the symbol before it's registered. + * + * @param args Arguments object. + * @returns void + */ + 'symbol:register:before'?: (args: { + /** Plugin registering the symbol. */ + plugin: PluginInstance; + /** Symbol to register. */ + symbol: SymbolIn; + }) => void; + /** + * Triggered after setting a symbol value. + * + * You can use this to perform actions after a symbol's value is set. + * + * @param args Arguments object. + * @returns void + */ + 'symbol:setValue:after'?: (args: { + /** Plugin that set the symbol value. */ + plugin: PluginInstance; + /** The symbol. */ + symbol: Symbol; + /** The value that was set. */ + value: unknown; + }) => void; + /** + * Triggered before setting a symbol value. + * + * You can use this to modify the value before it's set. + * + * @param args Arguments object. + * @returns void + */ + 'symbol:setValue:before'?: (args: { + /** Plugin setting the symbol value. */ + plugin: PluginInstance; + /** The symbol. */ + symbol: Symbol; + /** The value to set. */ + value: unknown; + }) => void; + }; + /** + * Hooks specifically for overriding operations behavior. + * + * Use these to classify operations, decide which outputs to generate, + * or apply custom behavior to individual operations. + */ + operations?: { + /** + * Classify the given operation into one or more kinds. + * + * Each kind determines how we treat the operation (e.g., generating queries or mutations). + * + * **Default behavior:** + * - GET → 'query' + * - DELETE, PATCH, POST, PUT → 'mutation' + * + * **Resolution order:** + * 1. If `isQuery` or `isMutation` returns `true` or `false`, that overrides `getKind`. + * 2. If `isQuery` or `isMutation` returns `undefined`, the result of `getKind` is used. + * + * @param operation - The operation object to classify. + * @returns An array containing one or more of 'query' or 'mutation', or undefined to fallback to default behavior. + * @example + * ```ts + * getKind: (operation) => { + * if (operation.method === 'get' && operation.path === '/search') { + * return ['query', 'mutation']; + * } + * return; // fallback to default behavior + * } + * ``` + */ + getKind?: ( + operation: IROperationObject, + ) => ReadonlyArray<'mutation' | 'query'> | undefined; + /** + * Check if the given operation should be treated as a mutation. + * + * This affects which outputs are generated for the operation. + * + * **Default behavior:** DELETE, PATCH, POST, and PUT operations are treated as mutations. + * + * **Resolution order:** If this returns `true` or `false`, it overrides `getKind`. + * If it returns `undefined`, `getKind` is used instead. + * + * @param operation - The operation object to check. + * @returns true if the operation is a mutation, false otherwise, or undefined to fallback to `getKind`. + * @example + * ```ts + * isMutation: (operation) => { + * if (operation.method === 'post' && operation.path === '/search') { + * return true; + * } + * return; // fallback to default behavior + * } + * ``` + */ + isMutation?: (operation: IROperationObject) => boolean | undefined; + /** + * Check if the given operation should be treated as a query. + * + * This affects which outputs are generated for the operation. + * + * **Default behavior:** GET operations are treated as queries. + * + * **Resolution order:** If this returns `true` or `false`, it overrides `getKind`. + * If it returns `undefined`, `getKind` is used instead. + * + * @param operation - The operation object to check. + * @returns true if the operation is a query, false otherwise, or undefined to fallback to `getKind`. + * @example + * ```ts + * isQuery: (operation) => { + * if (operation.method === 'post' && operation.path === '/search') { + * return true; + * } + * return; // fallback to default behavior + * } + * ``` + */ + isQuery?: (operation: IROperationObject) => boolean | undefined; + }; + /** + * Hooks specifically for overriding symbols behavior. + * + * Use these to customize file placement. + */ + symbols?: { + /** + * Optional output strategy to override default plugin behavior. + * + * Use this to route generated symbols to specific files. + * + * @returns The file path to output the symbol to, or undefined to fallback to default behavior. + */ + getFilePath?: (symbol: Symbol) => string | undefined; + }; +}; diff --git a/packages/openapi-ts/src/plugins/@hey-api/schemas/__tests__/schemas.test.ts b/packages/openapi-ts/src/plugins/@hey-api/schemas/__tests__/schemas.test.ts index 479ea0bcb7..7a5b30e09f 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/schemas/__tests__/schemas.test.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/schemas/__tests__/schemas.test.ts @@ -156,7 +156,14 @@ describe('generateLegacySchemas', () => { config: { exportFromIndex: false, }, - context: {} as any, + context: { + config: { + // @ts-expect-error + parser: { + hooks: {}, + }, + }, + }, dependencies: [], gen: new Project({ renderers: {}, @@ -322,7 +329,14 @@ describe('generateLegacySchemas', () => { config: { exportFromIndex: false, }, - context: {} as any, + context: { + config: { + // @ts-expect-error + parser: { + hooks: {}, + }, + }, + }, dependencies: [], gen: new Project({ renderers: {}, diff --git a/packages/openapi-ts/src/plugins/@hey-api/sdk/__tests__/plugin.test.ts b/packages/openapi-ts/src/plugins/@hey-api/sdk/__tests__/plugin.test.ts index 86b288d486..e0e9cf7731 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/sdk/__tests__/plugin.test.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/sdk/__tests__/plugin.test.ts @@ -192,7 +192,14 @@ describe('handlerLegacy', () => { config: { exportFromIndex: false, }, - context: {} as any, + context: { + config: { + // @ts-expect-error + parser: { + hooks: {}, + }, + }, + }, dependencies: [], gen: new Project({ renderers: {}, @@ -390,7 +397,14 @@ describe('methodNameBuilder', () => { config: { exportFromIndex: false, }, - context: {} as any, + context: { + config: { + // @ts-expect-error + parser: { + hooks: {}, + }, + }, + }, dependencies: [], gen: new Project({ renderers: {}, @@ -549,7 +563,14 @@ describe('methodNameBuilder', () => { config: { exportFromIndex: false, }, - context: {} as any, + context: { + config: { + // @ts-expect-error + parser: { + hooks: {}, + }, + }, + }, dependencies: [], gen: new Project({ renderers: {}, @@ -710,7 +731,14 @@ describe('methodNameBuilder', () => { config: { exportFromIndex: false, }, - context: {} as any, + context: { + config: { + // @ts-expect-error + parser: { + hooks: {}, + }, + }, + }, dependencies: [], gen: new Project({ renderers: {}, diff --git a/packages/openapi-ts/src/plugins/@hey-api/sdk/operation.ts b/packages/openapi-ts/src/plugins/@hey-api/sdk/operation.ts index e63776dbef..f52b7b2600 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/sdk/operation.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/sdk/operation.ts @@ -466,16 +466,12 @@ export const operationStatements = ({ const pluginTransformers = plugin.getPluginOrThrow( plugin.config.transformer, ); - const symbolResponseTransformer = plugin.getSymbol( - pluginTransformers.api.selector('response', operation.id), - ); - if ( - symbolResponseTransformer && - plugin.getSymbolValue(symbolResponseTransformer) - ) { + const selector = pluginTransformers.api.selector('response', operation.id); + if (plugin.isSymbolRegistered(selector)) { + const ref = plugin.referenceSymbol(selector); requestOptions.push({ key: 'responseTransformer', - value: symbolResponseTransformer.placeholder, + value: ref.placeholder, }); } } diff --git a/packages/openapi-ts/src/plugins/@hey-api/transformers/plugin.ts b/packages/openapi-ts/src/plugins/@hey-api/transformers/plugin.ts index 6fdfca657e..b74608fe7a 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/transformers/plugin.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/transformers/plugin.ts @@ -61,17 +61,6 @@ const processSchemaType = ({ const selector = plugin.api.selector('response-ref', schema.$ref); if (!plugin.getSymbol(selector)) { - const symbol = plugin.registerSymbol({ - name: buildName({ - config: { - case: 'camelCase', - name: '{{name}}SchemaResponseTransformer', - }, - name: refToName(schema.$ref), - }), - selector, - }); - // create each schema response transformer only once const refSchema = plugin.context.resolveIrRef( schema.$ref, @@ -81,6 +70,16 @@ const processSchemaType = ({ schema: refSchema, }); if (nodes.length) { + const symbol = plugin.registerSymbol({ + name: buildName({ + config: { + case: 'camelCase', + name: '{{name}}SchemaResponseTransformer', + }, + name: refToName(schema.$ref), + }), + selector, + }); const node = tsc.constVariable({ expression: tsc.arrowFunction({ async: false, @@ -97,17 +96,13 @@ const processSchemaType = ({ name: symbol.placeholder, }); plugin.setSymbolValue(symbol, node); - } else { - // the created schema response transformer was empty, do not generate - // it and prevent any future attempts - plugin.setSymbolValue(symbol, null); } } - const symbolResponseTransformerRef = plugin.referenceSymbol(selector); - if (plugin.getSymbolValue(symbolResponseTransformerRef) !== null) { + if (plugin.isSymbolRegistered(selector)) { + const ref = plugin.referenceSymbol(selector); const callExpression = tsc.callExpression({ - functionName: symbolResponseTransformerRef.placeholder, + functionName: ref.placeholder, parameters: [dataExpression], }); @@ -341,7 +336,13 @@ export const handler: HeyApiTransformersPlugin['Handler'] = ({ plugin }) => { ); if (!symbolResponse) return; - const symbolResponseTransformer = plugin.registerSymbol({ + // TODO: parser - consider handling simple string response which is also a date + const nodes = schemaResponseTransformerNodes({ + plugin, + schema: response, + }); + if (!nodes.length) return; + const symbol = plugin.registerSymbol({ exported: true, name: buildName({ config: { @@ -352,42 +353,29 @@ export const handler: HeyApiTransformersPlugin['Handler'] = ({ plugin }) => { }), selector: plugin.api.selector('response', operation.id), }); - - // TODO: parser - consider handling simple string response which is also a date - const nodes = schemaResponseTransformerNodes({ - plugin, - schema: response, - }); - if (nodes.length) { - const responseTransformerNode = tsc.constVariable({ - exportConst: symbolResponseTransformer.exported, - expression: tsc.arrowFunction({ - async: true, - multiLine: true, - parameters: [ - { - name: dataVariableName, - // TODO: parser - add types, generate types without transforms - type: tsc.keywordTypeNode({ keyword: 'any' }), - }, + const value = tsc.constVariable({ + exportConst: symbol.exported, + expression: tsc.arrowFunction({ + async: true, + multiLine: true, + parameters: [ + { + name: dataVariableName, + // TODO: parser - add types, generate types without transforms + type: tsc.keywordTypeNode({ keyword: 'any' }), + }, + ], + returnType: tsc.typeReferenceNode({ + typeArguments: [ + tsc.typeReferenceNode({ typeName: symbolResponse.placeholder }), ], - returnType: tsc.typeReferenceNode({ - typeArguments: [ - tsc.typeReferenceNode({ typeName: symbolResponse.placeholder }), - ], - typeName: 'Promise', - }), - statements: ensureStatements(nodes), + typeName: 'Promise', }), - name: symbolResponseTransformer.placeholder, - }); - plugin.setSymbolValue( - symbolResponseTransformer, - responseTransformerNode, - ); - } else { - plugin.setSymbolValue(symbolResponseTransformer, null); - } + statements: ensureStatements(nodes), + }), + name: symbol.placeholder, + }); + plugin.setSymbolValue(symbol, value); }, { order: 'declarations', diff --git a/packages/openapi-ts/src/plugins/@hey-api/typescript/__tests__/plugin.test.ts b/packages/openapi-ts/src/plugins/@hey-api/typescript/__tests__/plugin.test.ts index b1935c6958..87819a8e5a 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/typescript/__tests__/plugin.test.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/typescript/__tests__/plugin.test.ts @@ -185,7 +185,14 @@ describe('generateLegacyTypes', () => { config: { exportFromIndex: false, }, - context: {} as any, + context: { + config: { + // @ts-expect-error + parser: { + hooks: {}, + }, + }, + }, dependencies: [], gen: new Project({ renderers: {}, diff --git a/packages/openapi-ts/src/plugins/@pinia/colada/mutationOptions.ts b/packages/openapi-ts/src/plugins/@pinia/colada/mutationOptions.ts index b8f308c24d..93cd67dc3a 100644 --- a/packages/openapi-ts/src/plugins/@pinia/colada/mutationOptions.ts +++ b/packages/openapi-ts/src/plugins/@pinia/colada/mutationOptions.ts @@ -141,5 +141,5 @@ export const createMutationOptions = ({ }), name: symbolMutationOptions.placeholder, }); - plugin.setSymbolValue(symbolMutationOptions.id, statement); + plugin.setSymbolValue(symbolMutationOptions, statement); }; diff --git a/packages/openapi-ts/src/plugins/shared/utils/instance.ts b/packages/openapi-ts/src/plugins/shared/utils/instance.ts index 16f25ae63f..cfd4778415 100644 --- a/packages/openapi-ts/src/plugins/shared/utils/instance.ts +++ b/packages/openapi-ts/src/plugins/shared/utils/instance.ts @@ -16,6 +16,7 @@ import { } from '~/ir/graph'; import type { IR } from '~/ir/types'; import type { OpenApi } from '~/openApi/types'; +import type { Hooks } from '~/parser/types/hooks'; import type { PluginConfigMap } from '~/plugins/config'; import type { Plugin } from '~/plugins/types'; import { jsonPointerToPath } from '~/utils/ref'; @@ -38,7 +39,7 @@ const defaultGetFilePath = (symbol: Symbol): string | undefined => { return symbol.meta.pluginName; }; -const defaultGetKind: Required['operations']>['getKind'] = ( +const defaultGetKind: Required['operations']>['getKind'] = ( operation, ) => { switch (operation.method) { @@ -54,11 +55,18 @@ const defaultGetKind: Required['operations']>['getKind'] = ( } }; +type EventHooks = { + [K in keyof Required>]: Array< + NonNullable[K]> + >; +}; + export class PluginInstance { api: T['api']; config: Omit; context: IR.Context; dependencies: Required>['dependencies'] = []; + private eventHooks: EventHooks; gen: IProject; private handler: Plugin.Config['handler']; name: T['resolvedConfig']['name']; @@ -87,6 +95,7 @@ export class PluginInstance { this.config = props.config; this.context = props.context; this.dependencies = props.dependencies; + this.eventHooks = this.buildEventHooks(); this.gen = props.gen; this.handler = props.handler; this.name = props.name; @@ -336,18 +345,6 @@ export class PluginInstance { } } - private forEachError(error: unknown, event: WalkEvent) { - const originalError = - error instanceof Error ? error : new Error(String(error)); - throw new HeyApiError({ - args: [event], - error: originalError, - event: event.type, - name: 'Error', - pluginName: this.name, - }); - } - /** * Retrieves a registered plugin instance by its name from the context. This * allows plugins to access other plugins that have been registered in the @@ -382,29 +379,6 @@ export class PluginInstance { return this.gen.symbols.get(symbolIdOrSelector); } - private getSymbolFilePath(symbol: Symbol): string | undefined { - const getFilePathFnPlugin = this.config['~hooks']?.symbols?.getFilePath; - const getFilePathFnPluginResult = getFilePathFnPlugin?.(symbol); - if (getFilePathFnPluginResult !== undefined) { - return getFilePathFnPluginResult; - } - const getFilePathFnParser = - this.context.config.parser.hooks.symbols?.getFilePath; - const getFilePathFnParserResult = getFilePathFnParser?.(symbol); - if (getFilePathFnParserResult !== undefined) { - return getFilePathFnParserResult; - } - return defaultGetFilePath(symbol); - } - - getSymbolValue(idOrSymbol: number | Symbol): unknown { - return this.gen.symbols.getValue(this.symbolToId(idOrSymbol)); - } - - hasSymbolValue(idOrSymbol: number | Symbol): boolean { - return this.gen.symbols.hasValue(this.symbolToId(idOrSymbol)); - } - hooks = { operation: { isMutation: (operation: IR.OperationObject): boolean => @@ -412,42 +386,8 @@ export class PluginInstance { isQuery: (operation: IR.OperationObject): boolean => this.isOperationKind(operation, 'query'), }, - symbol: { - getFilePath: (symbol: Symbol): string | undefined => - this.getSymbolFilePath(symbol), - }, }; - private isOperationKind( - operation: IR.OperationObject, - kind: 'mutation' | 'query', - ): boolean { - const methodName = kind === 'query' ? 'isQuery' : 'isMutation'; - const isFnPlugin = this.config['~hooks']?.operations?.[methodName]; - const isFnPluginResult = isFnPlugin?.(operation); - if (isFnPluginResult !== undefined) { - return isFnPluginResult; - } - const getKindFnPlugin = this.config['~hooks']?.operations?.getKind; - const getKindFnPluginResult = getKindFnPlugin?.(operation); - if (getKindFnPluginResult !== undefined) { - return getKindFnPluginResult.includes(kind); - } - const isFnParser = - this.context.config.parser.hooks.operations?.[methodName]; - const isFnParserResult = isFnParser?.(operation); - if (isFnParserResult !== undefined) { - return isFnParserResult; - } - const getKindFnParser = - this.context.config.parser.hooks.operations?.getKind; - const getKindFnParserResult = getKindFnParser?.(operation); - if (getKindFnParserResult !== undefined) { - return getKindFnParserResult.includes(kind); - } - return (defaultGetKind(operation) ?? []).includes(kind); - } - isSymbolRegistered(symbolIdOrSelector: number | Selector): boolean { return this.gen.symbols.isRegistered(symbolIdOrSelector); } @@ -457,7 +397,7 @@ export class PluginInstance { } registerSymbol(symbol: SymbolIn): Symbol { - return this.gen.symbols.register({ + const symbolIn: SymbolIn = { ...symbol, exportFrom: symbol.exportFrom ?? @@ -466,29 +406,113 @@ export class PluginInstance { this.config.exportFromIndex ? ['index'] : undefined), - getFilePath: symbol.getFilePath ?? this.hooks.symbol.getFilePath, + getFilePath: symbol.getFilePath ?? this.getSymbolFilePath.bind(this), meta: { pluginName: path.isAbsolute(this.name) ? 'custom' : this.name, ...symbol.meta, }, - }); + }; + for (const hook of this.eventHooks['symbol:register:before']) { + hook({ plugin: this, symbol: symbolIn }); + } + const symbolOut = this.gen.symbols.register(symbolIn); + for (const hook of this.eventHooks['symbol:register:after']) { + hook({ plugin: this, symbol: symbolOut }); + } + return symbolOut; } /** * Executes plugin's handler function. */ - async run() { + async run(): Promise { + for (const hook of this.eventHooks['plugin:handler:before']) { + hook({ plugin: this }); + } await this.handler({ plugin: this }); + for (const hook of this.eventHooks['plugin:handler:after']) { + hook({ plugin: this }); + } + } + + setSymbolValue(symbol: Symbol, value: unknown): void { + for (const hook of this.eventHooks['symbol:setValue:before']) { + hook({ plugin: this, symbol, value }); + } + this.gen.symbols.setValue(symbol.id, value); + for (const hook of this.eventHooks['symbol:setValue:after']) { + hook({ plugin: this, symbol, value }); + } + } + + private buildEventHooks(): EventHooks { + const result: EventHooks = { + 'plugin:handler:after': [], + 'plugin:handler:before': [], + 'symbol:register:after': [], + 'symbol:register:before': [], + 'symbol:setValue:after': [], + 'symbol:setValue:before': [], + }; + const scopes = [ + this.config['~hooks']?.events, + this.context.config.parser.hooks.events, + ]; + for (const scope of scopes) { + if (!scope) continue; + for (const [key, value] of Object.entries(scope)) { + if (value) { + result[key as keyof typeof result].push(value.bind(scope) as any); + } + } + } + return result; + } + + private forEachError(error: unknown, event: WalkEvent) { + const originalError = + error instanceof Error ? error : new Error(String(error)); + throw new HeyApiError({ + args: [event], + error: originalError, + event: event.type, + name: 'Error', + pluginName: this.name, + }); } - setSymbolValue( - idOrSymbol: number | Symbol, - value: unknown, - ): Map { - return this.gen.symbols.setValue(this.symbolToId(idOrSymbol), value); + private getSymbolFilePath(symbol: Symbol): string | undefined { + const hooks = [ + this.config['~hooks']?.symbols, + this.context.config.parser.hooks.symbols, + ]; + for (const hook of hooks) { + const result = hook?.getFilePath?.(symbol); + if (result !== undefined) return result; + } + return defaultGetFilePath(symbol); } - private symbolToId(idOrSymbol: number | Symbol): number { - return typeof idOrSymbol === 'number' ? idOrSymbol : idOrSymbol.id; + private isOperationKind( + operation: IR.OperationObject, + kind: 'mutation' | 'query', + ): boolean { + const method = kind === 'query' ? 'isQuery' : 'isMutation'; + const hooks = [ + this.config['~hooks']?.operations?.[method], + this.config['~hooks']?.operations?.getKind, + this.context.config.parser.hooks.operations?.[method], + this.context.config.parser.hooks.operations?.getKind, + defaultGetKind, + ]; + for (const hook of hooks) { + if (hook) { + const result = hook(operation); + if (result !== undefined) { + return typeof result === 'boolean' ? result : result.includes(kind); + } + } + } + return false; } } diff --git a/packages/openapi-ts/src/plugins/types.d.ts b/packages/openapi-ts/src/plugins/types.d.ts index a0876813be..af3a02bff2 100644 --- a/packages/openapi-ts/src/plugins/types.d.ts +++ b/packages/openapi-ts/src/plugins/types.d.ts @@ -1,7 +1,7 @@ import type { ValueToObject } from '~/config/utils/config'; import type { Package } from '~/config/utils/package'; -import type { IR } from '~/ir/types'; import type { OpenApi as LegacyOpenApi } from '~/openApi'; +import type { Hooks } from '~/parser/types/hooks'; import type { PluginInstance } from '~/plugins/shared/utils/instance'; import type { Client as LegacyClient } from '~/types/client'; import type { Files } from '~/types/utils'; @@ -68,7 +68,7 @@ type BaseConfig = { * Use these to classify resources, control which outputs are generated, * or provide custom behavior for specific resources. */ - '~hooks'?: IR.Hooks; + '~hooks'?: Hooks; }; /** diff --git a/packages/openapi-ts/src/types/parser.d.ts b/packages/openapi-ts/src/types/parser.d.ts index eff50d3250..db0a8152c6 100644 --- a/packages/openapi-ts/src/types/parser.d.ts +++ b/packages/openapi-ts/src/types/parser.d.ts @@ -1,4 +1,3 @@ -import type { IR } from '~/ir/types'; import type { OpenApiMetaObject, OpenApiOperationObject, @@ -7,6 +6,7 @@ import type { OpenApiResponseObject, OpenApiSchemaObject, } from '~/openApi/types'; +import type { Hooks } from '~/parser/types/hooks'; import type { StringCase, StringName } from './case'; @@ -24,7 +24,7 @@ export type UserParser = { * Use these to classify resources, control which outputs are generated, * or provide custom behavior for specific resources. */ - hooks?: IR.Hooks; + hooks?: Hooks; /** * Pagination configuration. */ @@ -206,7 +206,7 @@ export type Parser = { * Use these to classify resources, control which outputs are generated, * or provide custom behavior for specific resources. */ - hooks: IR.Hooks; + hooks: Hooks; /** * Pagination configuration. */