diff --git a/.changeset/moody-knives-accept.md b/.changeset/moody-knives-accept.md new file mode 100644 index 000000000..e301479b1 --- /dev/null +++ b/.changeset/moody-knives-accept.md @@ -0,0 +1,5 @@ +--- +'@hey-api/openapi-ts': patch +--- + +fix: update types for custom plugins so defineConfig does not throw diff --git a/packages/openapi-ts/src/index.ts b/packages/openapi-ts/src/index.ts index cf8ae8215..b55a454e0 100644 --- a/packages/openapi-ts/src/index.ts +++ b/packages/openapi-ts/src/index.ts @@ -12,6 +12,7 @@ import { parseExperimental, parseLegacy } from './openApi'; import type { ClientPlugins, UserPlugins } from './plugins'; import { defaultPluginConfigs } from './plugins'; import type { + AnyPluginName, DefaultPluginConfigs, PluginContext, PluginNames, @@ -190,14 +191,14 @@ const getPluginsConfig = ({ userPluginsConfig, }: { pluginConfigs: DefaultPluginConfigs; - userPlugins: ReadonlyArray; + userPlugins: ReadonlyArray; userPluginsConfig: Config['plugins']; }): Pick => { - const circularReferenceTracker = new Set(); - const pluginOrder = new Set(); + const circularReferenceTracker = new Set(); + const pluginOrder = new Set(); const plugins: Config['plugins'] = {}; - const dfs = (name: PluginNames) => { + const dfs = (name: AnyPluginName) => { if (circularReferenceTracker.has(name)) { throw new Error(`Circular reference detected at '${name}'`); } @@ -205,15 +206,15 @@ const getPluginsConfig = ({ if (!pluginOrder.has(name)) { circularReferenceTracker.add(name); - const pluginConfig = pluginConfigs[name]; + const pluginConfig = pluginConfigs[name as PluginNames]; if (!pluginConfig) { throw new Error( `🚫 unknown plugin dependency "${name}" - do you need to register a custom plugin with this name?`, ); } - const defaultOptions = defaultPluginConfigs[name]; - const userOptions = userPluginsConfig[name]; + const defaultOptions = defaultPluginConfigs[name as PluginNames]; + const userOptions = userPluginsConfig[name as PluginNames]; if (userOptions && defaultOptions) { const nativePluginOption = Object.keys(userOptions).find((key) => key.startsWith('_'), @@ -243,7 +244,8 @@ const getPluginsConfig = ({ }, pluginByTag: (tag) => { for (const userPlugin of userPlugins) { - const defaultConfig = defaultPluginConfigs[userPlugin]; + const defaultConfig = + defaultPluginConfigs[userPlugin as PluginNames]; if ( defaultConfig && defaultConfig._tags?.includes(tag) && @@ -274,7 +276,7 @@ const getPluginsConfig = ({ } return { - pluginOrder: Array.from(pluginOrder), + pluginOrder: Array.from(pluginOrder) as ReadonlyArray, plugins, }; }; diff --git a/packages/openapi-ts/src/plugins/types.d.ts b/packages/openapi-ts/src/plugins/types.d.ts index 20a26accf..6b4b316de 100644 --- a/packages/openapi-ts/src/plugins/types.d.ts +++ b/packages/openapi-ts/src/plugins/types.d.ts @@ -20,11 +20,14 @@ export type PluginNames = | 'fastify' | 'zod'; +// eslint-disable-next-line @typescript-eslint/ban-types +export type AnyPluginName = PluginNames | (string & {}); + type PluginTag = 'transformer' | 'validator'; export interface PluginContext { ensureDependency: (name: PluginNames | true) => void; - pluginByTag: (tag: PluginTag) => PluginNames | undefined; + pluginByTag: (tag: PluginTag) => AnyPluginName | undefined; } interface BaseConfig { @@ -35,8 +38,7 @@ interface BaseConfig { * barrel file? */ exportFromIndex?: boolean; - // eslint-disable-next-line @typescript-eslint/ban-types - name: PluginNames | (string & {}); + name: AnyPluginName; output?: string; } @@ -45,7 +47,7 @@ interface Meta { * Dependency plugins will be always processed, regardless of whether user * explicitly defines them in their `plugins` config. */ - _dependencies?: ReadonlyArray; + _dependencies?: ReadonlyArray; /** * Allows overriding config before it's sent to the parser. An example is * defining `validator` as `true` and the plugin figures out which plugin @@ -83,7 +85,15 @@ export namespace Plugin { export type DefineConfig = ( config?: Plugin.UserConfig, - ) => Plugin.Config; + ) => Omit, 'name'> & { + /** + * Cast name to `any` so it doesn't throw type error in `plugins` array. + * We could allow any `string` as plugin `name` in the object syntax, but + * that TypeScript trick would cause all string methods to appear as + * suggested auto completions, which is undesirable. + */ + name: any; + }; /** * Plugin implementation for experimental parser. diff --git a/packages/openapi-ts/test/plugins.test.ts b/packages/openapi-ts/test/plugins.test.ts index b04ecc073..62230e31e 100644 --- a/packages/openapi-ts/test/plugins.test.ts +++ b/packages/openapi-ts/test/plugins.test.ts @@ -244,7 +244,7 @@ for (const version of versions) { it('handles a custom plugin', async () => { const myPlugin: Plugin.Config<{ customOption: boolean; - name: string; + name: any; output: string; }> = { _dependencies: ['@hey-api/typescript'], @@ -260,7 +260,6 @@ for (const version of versions) { experimentalParser: true, input: path.join(__dirname, 'spec', '3.1.x', 'full.json'), output: path.join(outputDir, myPlugin.name, 'default'), - // @ts-expect-error plugins: [myPlugin], }); @@ -270,10 +269,9 @@ for (const version of versions) { it('throws on invalid dependency', async () => { const myPlugin: Plugin.Config<{ - name: string; + name: any; output: string; }> = { - // @ts-expect-error _dependencies: ['@hey-api/oops'], _handler: vi.fn(), _handlerLegacy: vi.fn(), @@ -290,7 +288,6 @@ for (const version of versions) { level: 'silent', }, output: path.join(outputDir, myPlugin.name, 'default'), - // @ts-expect-error plugins: [myPlugin], }), ).rejects.toThrowError(/unknown plugin/g); @@ -301,7 +298,7 @@ for (const version of versions) { it('throws on native plugin override', async () => { const myPlugin: Plugin.Config<{ - name: string; + name: any; output: string; }> = { _handler: vi.fn(), @@ -319,7 +316,6 @@ for (const version of versions) { level: 'silent', }, output: path.join(outputDir, myPlugin.name, 'default'), - // @ts-expect-error plugins: [myPlugin], }), ).rejects.toThrowError(/cannot register plugin/g);