diff --git a/.changeset/fluffy-tomatoes-warn.md b/.changeset/fluffy-tomatoes-warn.md new file mode 100644 index 00000000..f44cd385 --- /dev/null +++ b/.changeset/fluffy-tomatoes-warn.md @@ -0,0 +1,5 @@ +--- +'@hey-api/openapi-ts': patch +--- + +fix: add support for custom plugins diff --git a/packages/openapi-ts/src/index.ts b/packages/openapi-ts/src/index.ts index 7c507fa8..65068aa7 100644 --- a/packages/openapi-ts/src/index.ts +++ b/packages/openapi-ts/src/index.ts @@ -15,8 +15,9 @@ import { operationParameterFilterFn, operationParameterNameFn, } from './openApi/config'; +import type { ClientPlugins } from './plugins'; import { defaultPluginConfigs } from './plugins'; -import type { PluginNames } from './plugins/types'; +import type { DefaultPluginConfigsMap, PluginNames } from './plugins/types'; import type { Client } from './types/client'; import type { ClientConfig, Config, UserConfig } from './types/config'; import { CLIENTS } from './types/config'; @@ -164,8 +165,10 @@ const getOutput = (userConfig: ClientConfig): Config['output'] => { }; const getPluginOrder = ({ + pluginConfigs, userPlugins, }: { + pluginConfigs: DefaultPluginConfigsMap; userPlugins: ReadonlyArray; }): Config['pluginOrder'] => { const circularReferenceTracker = new Set(); @@ -179,12 +182,19 @@ const getPluginOrder = ({ if (!visitedNodes.has(name)) { circularReferenceTracker.add(name); - for (const dependency of defaultPluginConfigs[name]._dependencies || []) { + const pluginConfig = pluginConfigs[name]; + + if (!pluginConfig) { + throw new Error( + `🚫 unknown plugin dependency "${name}" - do you need to register a custom plugin with this name?`, + ); + } + + for (const dependency of pluginConfig._dependencies || []) { dfs(dependency); } - for (const dependency of defaultPluginConfigs[name] - ._optionalDependencies || []) { + for (const dependency of pluginConfig._optionalDependencies || []) { if (userPlugins.includes(dependency)) { dfs(dependency); } @@ -218,18 +228,37 @@ const getPlugins = ( return plugin; } - // @ts-ignore + // @ts-expect-error userPluginsConfig[plugin.name] = plugin; return plugin.name; }); - const pluginOrder = getPluginOrder({ userPlugins }); + + const pluginOrder = getPluginOrder({ + pluginConfigs: { + ...userPluginsConfig, + ...defaultPluginConfigs, + }, + userPlugins, + }); const plugins = pluginOrder.reduce( (result, name) => { - // @ts-ignore + const defaultOptions = defaultPluginConfigs[name]; + const userOptions = userPluginsConfig[name]; + if (userOptions && defaultOptions) { + const nativePluginOption = Object.keys(userOptions).find((key) => + key.startsWith('_'), + ); + if (nativePluginOption) { + throw new Error( + `🚫 cannot register plugin "${userOptions.name}" - attempting to override a native plugin option "${nativePluginOption}"`, + ); + } + } + // @ts-expect-error result[name] = { - ...defaultPluginConfigs[name], - ...userPluginsConfig[name], + ...defaultOptions, + ...userOptions, }; return result; }, diff --git a/packages/openapi-ts/src/plugins/types.d.ts b/packages/openapi-ts/src/plugins/types.d.ts index 8bdd152d..e69901f6 100644 --- a/packages/openapi-ts/src/plugins/types.d.ts +++ b/packages/openapi-ts/src/plugins/types.d.ts @@ -40,7 +40,8 @@ export interface PluginName { } interface CommonConfig { - name: PluginNames; + // eslint-disable-next-line @typescript-eslint/ban-types + name: PluginNames | (string & {}); output?: string; } diff --git a/packages/openapi-ts/test/plugins.spec.ts b/packages/openapi-ts/test/plugins.spec.ts index 3ec579ba..5ad51faf 100644 --- a/packages/openapi-ts/test/plugins.spec.ts +++ b/packages/openapi-ts/test/plugins.spec.ts @@ -2,9 +2,10 @@ import { readFileSync } from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; -import { describe, expect, it } from 'vitest'; +import { describe, expect, it, vi } from 'vitest'; import { createClient } from '../'; +import type { PluginConfig } from '../src/plugins/types'; import type { UserConfig } from '../src/types/config'; import { getFilePaths } from './utils'; @@ -223,4 +224,87 @@ for (const version of versions) { }); }); }); + + describe('custom plugin', () => { + it('handles a custom plugin', async () => { + const myPlugin: PluginConfig<{ + customOption: boolean; + name: string; + output: string; + }> = { + _dependencies: ['@hey-api/types'], + _handler: vi.fn(), + _handlerLegacy: vi.fn(), + customOption: true, + name: 'my-plugin', + output: 'my-plugin', + }; + + await createClient({ + client: '@hey-api/client-fetch', + experimentalParser: true, + input: path.join(__dirname, 'spec', '3.1.x', 'full.json'), + output: path.join(outputDir, myPlugin.name, 'default'), + // @ts-expect-error + plugins: [myPlugin], + }); + + expect(myPlugin._handler).toHaveBeenCalled(); + expect(myPlugin._handlerLegacy).not.toHaveBeenCalled(); + }); + + it('throws on invalid dependency', async () => { + const myPlugin: PluginConfig<{ + name: string; + output: string; + }> = { + // @ts-expect-error + _dependencies: ['@hey-api/oops'], + _handler: vi.fn(), + _handlerLegacy: vi.fn(), + name: 'my-plugin', + output: 'my-plugin', + }; + + await expect(() => + createClient({ + client: '@hey-api/client-fetch', + experimentalParser: true, + input: path.join(__dirname, 'spec', '3.1.x', 'full.json'), + output: path.join(outputDir, myPlugin.name, 'default'), + // @ts-expect-error + plugins: [myPlugin], + }), + ).rejects.toThrowError(/unknown plugin/g); + + expect(myPlugin._handler).not.toHaveBeenCalled(); + expect(myPlugin._handlerLegacy).not.toHaveBeenCalled(); + }); + + it('throws on native plugin override', async () => { + const myPlugin: PluginConfig<{ + name: string; + output: string; + }> = { + _handler: vi.fn(), + _handlerLegacy: vi.fn(), + name: '@hey-api/types', + output: 'my-plugin', + }; + + await expect(() => + createClient({ + client: '@hey-api/client-fetch', + experimentalParser: true, + input: path.join(__dirname, 'spec', '3.1.x', 'full.json'), + output: path.join(outputDir, myPlugin.name, 'default'), + // @ts-expect-error + plugins: [myPlugin], + }), + ).rejects.toThrowError(/cannot register plugin/g); + + expect(myPlugin._handler).not.toHaveBeenCalled(); + expect(myPlugin._handlerLegacy).not.toHaveBeenCalled(); + }); + }); }