diff --git a/.changeset/tangy-cities-carry.md b/.changeset/tangy-cities-carry.md new file mode 100644 index 000000000..5d409454f --- /dev/null +++ b/.changeset/tangy-cities-carry.md @@ -0,0 +1,6 @@ +--- +"@hey-api/openapi-ts": patch +"@hey-api/shared": patch +--- + +**output**: add `module` option diff --git a/docs/openapi-ts/clients/ofetch.md b/docs/openapi-ts/clients/ofetch.md index 79e4daeba..32c06bf77 100644 --- a/docs/openapi-ts/clients/ofetch.md +++ b/docs/openapi-ts/clients/ofetch.md @@ -144,7 +144,7 @@ Interceptors (middleware) can be used to modify requests before they're sent or The `ofetch` client supports two complementary options: - built-in Hey API interceptors exposed via `client.interceptors` -- native `ofetch` hooks passed through config (e.g. `onRequest`) +- native `ofetch` hooks passed through config (e.g., `onRequest`) ### Example: Request interceptor diff --git a/docs/openapi-ts/configuration/output.md b/docs/openapi-ts/configuration/output.md index 3e1ec5670..cd55e9cb6 100644 --- a/docs/openapi-ts/configuration/output.md +++ b/docs/openapi-ts/configuration/output.md @@ -36,7 +36,11 @@ export default { You can learn more about complex use cases in the [Advanced](/openapi-ts/configuration#advanced) section. -## File Name +## File + +Control how files are named and annotated in the generated output. + +### File Name You can customize the naming and casing pattern for files using the `fileName` option. @@ -108,66 +112,95 @@ export default { ::: -## Module Extension +### File Header -You can customize the extension used for TypeScript modules. +The generated output includes a notice in every file warning that any modifications will be lost when the files are regenerated. You can customize or disable this notice using the `header` option. ::: code-group -```js [default] -export default { - input: 'hey-api/backend', // sign up at app.heyapi.dev - output: { - importFileExtension: undefined, // [!code ++] - path: 'src/client', - }, -}; +```js [example] +/* eslint-disable */ +// This file is auto-generated by @hey-api/openapi-ts + +/** ... */ ``` -```js [disabled] + +```js [config] export default { input: 'hey-api/backend', // sign up at app.heyapi.dev output: { - importFileExtension: null, // [!code ++] + header: (ctx) => [ // [!code ++] + '/* eslint-disable */', // [!code ++] + ...ctx.defaultValue, // [!code ++] + ], // [!code ++] path: 'src/client', }, }; ``` + + +::: + +## Module + +Control how module specifiers are generated in the output. + +### Module Extension + +Set `module.extension` to define the file extension used in import specifiers. This is useful when targeting environments that require fully specified imports (e.g., Node ESM or certain bundlers). + +::: code-group + +```js [example] +import foo from './foo.js'; +import bar from './bar.js'; +``` -```js [js] +```js [config] export default { input: 'hey-api/backend', // sign up at app.heyapi.dev output: { - importFileExtension: '.js', // [!code ++] + module: { + extension: '.js', // [!code ++] + }, path: 'src/client', }, }; ``` -```js [ts] +::: + +### Module Path + +Use `module.resolve` for full control over how module specifiers are generated. This lets you override specific modules or redirect them to custom locations (e.g., CDNs or internal aliases). + +::: code-group + +```js [example] +import * as z from 'https://esm.sh/zod'; +``` + + +```js [config] export default { input: 'hey-api/backend', // sign up at app.heyapi.dev output: { - importFileExtension: '.ts', // [!code ++] + module: { + resolve(path) { // [!code ++] + if (path === 'zod') { // [!code ++] + return 'https://esm.sh/zod'; // [!code ++] + } // [!code ++] + }, // [!code ++] + }, path: 'src/client', }, }; ``` + ::: -By default, we don't add a file extension and let the runtime resolve it. - -```js -import foo from './foo'; -``` - -If we detect a [TSConfig file](#tsconfig-path) with `moduleResolution` option set to `nodenext`, we default the extension to `.js`. - -```js -import foo from './foo.js'; -``` - ## Source Source is a copy of the input specification used to generate your output. It can be used to power documentation tools or to persist a stable snapshot alongside your generated files. @@ -337,36 +370,6 @@ export type ChatCompletion_N2 = number; ::: -## File Header - -The generated output includes a notice in every file warning that any modifications will be lost when the files are regenerated. You can customize or disable this notice using the `header` option. - -::: code-group - - -```js [config] -export default { - input: 'hey-api/backend', // sign up at app.heyapi.dev - output: { - header: [ - '/* eslint-disable */', // [!code ++] - '// This file is auto-generated by @hey-api/openapi-ts', // [!code ++] - ], - path: 'src/client', - }, -}; -``` - - -```ts [example] -/* eslint-disable */ -// This file is auto-generated by @hey-api/openapi-ts - -/** ... */ -``` - -::: - ## TSConfig Path We use the [TSConfig file](https://www.typescriptlang.org/tsconfig/) to generate output matching your project's settings. By default, we attempt to find a TSConfig file starting from the location of the `@hey-api/openapi-ts` configuration file and traversing up. diff --git a/docs/openapi-ts/migrating.md b/docs/openapi-ts/migrating.md index e4483af66..1ada00c98 100644 --- a/docs/openapi-ts/migrating.md +++ b/docs/openapi-ts/migrating.md @@ -422,7 +422,7 @@ If you need to access individual fields, you can do so using the [`.shape`](http ### Bundle `@hey-api/client-*` plugins -In previous releases, you had to install a separate client package to generate a fully working output, e.g. `npm install @hey-api/client-fetch`. This created a few challenges: getting started was slower, upgrading was sometimes painful, and bundling too. Beginning with v0.73.0, all Hey API clients are bundled by default and don't require installing any additional dependencies. You can remove any installed client packages and re-run `@hey-api/openapi-ts`. +In previous releases, you had to install a separate client package to generate a fully working output, e.g., `npm install @hey-api/client-fetch`. This created a few challenges: getting started was slower, upgrading was sometimes painful, and bundling too. Beginning with v0.73.0, all Hey API clients are bundled by default and don't require installing any additional dependencies. You can remove any installed client packages and re-run `@hey-api/openapi-ts`. ```sh npm uninstall @hey-api/client-fetch diff --git a/docs/openapi-ts/plugins/concepts/resolvers.md b/docs/openapi-ts/plugins/concepts/resolvers.md index dd2b4421c..2549e1b5c 100644 --- a/docs/openapi-ts/plugins/concepts/resolvers.md +++ b/docs/openapi-ts/plugins/concepts/resolvers.md @@ -130,7 +130,7 @@ export const vAmount = v.number(); ### Replace default base -You might want to replace the default base schema, e.g. `v.object()`. +You might want to replace the default base schema, e.g., `v.object()`. ```js export const vUser = v.object({ diff --git a/docs/openapi-ts/plugins/pinia-colada.md b/docs/openapi-ts/plugins/pinia-colada.md index 980e2bb3d..c4853f4e7 100644 --- a/docs/openapi-ts/plugins/pinia-colada.md +++ b/docs/openapi-ts/plugins/pinia-colada.md @@ -60,7 +60,7 @@ The Pinia Colada plugin will generate the following artifacts, depending on the ## Queries -Queries are generated from [query operations](/openapi-ts/configuration/parser#hooks-query-operations). The generated query functions follow the naming convention of SDK functions and by default append `Query`, e.g. `getPetByIdQuery()`. +Queries are generated from [query operations](/openapi-ts/configuration/parser#hooks-query-operations). The generated query functions follow the naming convention of SDK functions and by default append `Query`, e.g., `getPetByIdQuery()`. ::: code-group @@ -191,7 +191,7 @@ export default { ::: -Alternatively, you can access the same query key by calling query key functions. The generated query key functions follow the naming convention of SDK functions and by default append `QueryKey`, e.g. `getPetByIdQueryKey()`. +Alternatively, you can access the same query key by calling query key functions. The generated query key functions follow the naming convention of SDK functions and by default append `QueryKey`, e.g., `getPetByIdQueryKey()`. ::: code-group @@ -223,7 +223,7 @@ You can customize the naming and casing pattern for `queryKeys` functions using ## Mutations -Mutations are generated from [mutation operations](/openapi-ts/configuration/parser#hooks-mutation-operations). The generated mutation functions follow the naming convention of SDK functions and by default append `Mutation`, e.g. `addPetMutation()`. +Mutations are generated from [mutation operations](/openapi-ts/configuration/parser#hooks-mutation-operations). The generated mutation functions follow the naming convention of SDK functions and by default append `Mutation`, e.g., `addPetMutation()`. ::: code-group diff --git a/docs/openapi-ts/plugins/tanstack-query.md b/docs/openapi-ts/plugins/tanstack-query.md index 760d5fda2..ace8cd5ad 100644 --- a/docs/openapi-ts/plugins/tanstack-query.md +++ b/docs/openapi-ts/plugins/tanstack-query.md @@ -115,7 +115,7 @@ The TanStack Query plugin will generate the following artifacts, depending on th ## Queries -Queries are generated from [query operations](/openapi-ts/configuration/parser#hooks-query-operations). The generated query functions follow the naming convention of SDK functions and by default append `Options`, e.g. `getPetByIdOptions()`. +Queries are generated from [query operations](/openapi-ts/configuration/parser#hooks-query-operations). The generated query functions follow the naming convention of SDK functions and by default append `Options`, e.g., `getPetByIdOptions()`. ::: code-group @@ -281,7 +281,7 @@ export default { ::: -Alternatively, you can access the same query key by calling query key functions. The generated query key functions follow the naming convention of SDK functions and by default append `QueryKey`, e.g. `getPetByIdQueryKey()`. +Alternatively, you can access the same query key by calling query key functions. The generated query key functions follow the naming convention of SDK functions and by default append `QueryKey`, e.g., `getPetByIdQueryKey()`. ::: code-group @@ -313,7 +313,7 @@ You can customize the naming and casing pattern for `queryKeys` functions using ## Infinite Queries -Infinite queries are generated from [query operations](/openapi-ts/configuration/parser#hooks-query-operations) if we detect a [pagination](/openapi-ts/configuration/parser#pagination) parameter. The generated infinite query functions follow the naming convention of SDK functions and by default append `InfiniteOptions`, e.g. `getFooInfiniteOptions()`. +Infinite queries are generated from [query operations](/openapi-ts/configuration/parser#hooks-query-operations) if we detect a [pagination](/openapi-ts/configuration/parser#pagination) parameter. The generated infinite query functions follow the naming convention of SDK functions and by default append `InfiniteOptions`, e.g., `getFooInfiniteOptions()`. ::: code-group @@ -483,7 +483,7 @@ export default { ::: -Alternatively, you can access the same query key by calling query key functions. The generated query key functions follow the naming convention of SDK functions and by default append `InfiniteQueryKey`, e.g. `getPetByIdInfiniteQueryKey()`. +Alternatively, you can access the same query key by calling query key functions. The generated query key functions follow the naming convention of SDK functions and by default append `InfiniteQueryKey`, e.g., `getPetByIdInfiniteQueryKey()`. ::: code-group @@ -515,7 +515,7 @@ You can customize the naming and casing pattern for `infiniteQueryKeys` function ## Mutations -Mutations are generated from [mutation operations](/openapi-ts/configuration/parser#hooks-mutation-operations). The generated mutation functions follow the naming convention of SDK functions and by default append `Mutation`, e.g. `addPetMutation()`. +Mutations are generated from [mutation operations](/openapi-ts/configuration/parser#hooks-mutation-operations). The generated mutation functions follow the naming convention of SDK functions and by default append `Mutation`, e.g., `addPetMutation()`. ::: code-group diff --git a/docs/openapi-ts/plugins/transformers.md b/docs/openapi-ts/plugins/transformers.md index 4f8814ced..370540cef 100644 --- a/docs/openapi-ts/plugins/transformers.md +++ b/docs/openapi-ts/plugins/transformers.md @@ -21,7 +21,7 @@ Before deciding whether transformers are right for you, let's explain how they w Transformers handle only the most common scenarios. Some of the known limitations are: -- union types are not transformed (e.g. if you have multiple possible response shapes) +- union types are not transformed (e.g., if you have multiple possible response shapes) - only types defined through `$ref` are transformed - error responses are not transformed diff --git a/packages/openapi-python/src/config/output/config.ts b/packages/openapi-python/src/config/output/config.ts index a71c7ccf6..71f23ac89 100644 --- a/packages/openapi-python/src/config/output/config.ts +++ b/packages/openapi-python/src/config/output/config.ts @@ -24,6 +24,7 @@ export function getOutput(userConfig: { output: MaybeArray name: '{{name}}', suffix: '_gen', }, + module: {}, path: '', postProcess: [], preferExportAll: false, @@ -48,8 +49,8 @@ export function getOutput(userConfig: { output: MaybeArray }, value: userOutput, }) as Output; - if (output.importFileExtension && !output.importFileExtension.startsWith('.')) { - output.importFileExtension = `.${output.importFileExtension}`; + if (output.module.extension && !output.module.extension.startsWith('.')) { + output.module.extension = `.${output.module.extension}`; } output.postProcess = normalizePostProcess(userOutput.postProcess); output.source = resolveSource(output); diff --git a/packages/openapi-python/src/config/output/types.ts b/packages/openapi-python/src/config/output/types.ts index 30a471eb1..4923cf25e 100644 --- a/packages/openapi-python/src/config/output/types.ts +++ b/packages/openapi-python/src/config/output/types.ts @@ -1,20 +1,8 @@ import type { BaseOutput, BaseUserOutput, UserPostProcessor } from '@hey-api/shared'; -import type { AnyString } from '@hey-api/types'; import type { PostProcessorPreset } from './postprocess'; -type ImportFileExtensions = '.py'; - -export type UserOutput = BaseUserOutput & { - /** - * If specified, this will be the file extension used when importing - * other modules. By default, we don't add a file extension and let the - * runtime resolve it. If you're using moduleResolution `nodenext` or - * `node16`, we default to `.js`. - * - * @default undefined - */ - importFileExtension?: ImportFileExtensions | AnyString | null; +export type UserOutput = BaseUserOutput<'.py'> & { /** * Post-processing commands to run on the output folder, executed in order. * @@ -35,14 +23,7 @@ export type UserOutput = BaseUserOutput & { preferExportAll?: boolean; }; -export type Output = BaseOutput & { - /** - * If specified, this will be the file extension used when importing - * other modules. By default, we don't add a file extension and let the - * runtime resolve it. If you're using moduleResolution `nodenext` or - * `node16`, we default to `.js`. - */ - importFileExtension: ImportFileExtensions | AnyString | null | undefined; +export type Output = BaseOutput<'.py'> & { /** * Whether `export * from 'module'` should be used when possible * instead of named exports. diff --git a/packages/openapi-python/src/createClient.ts b/packages/openapi-python/src/createClient.ts index 0ac745a4f..c7a892848 100644 --- a/packages/openapi-python/src/createClient.ts +++ b/packages/openapi-python/src/createClient.ts @@ -131,9 +131,8 @@ export async function createClient({ const result = typeof header === 'function' ? header({ ...ctx, defaultValue }) : header; return result === undefined ? defaultValue : result; }, + module: config.output.module, preferExportAll: config.output.preferExportAll, - preferFileExtension: config.output.importFileExtension || undefined, - resolveModuleName: config.output.resolveModuleName, }), ], root: config.output.path, diff --git a/packages/openapi-python/src/py-dsl/utils/render.ts b/packages/openapi-python/src/py-dsl/utils/render.ts index 275bd0910..1aaac5655 100644 --- a/packages/openapi-python/src/py-dsl/utils/render.ts +++ b/packages/openapi-python/src/py-dsl/utils/render.ts @@ -1,5 +1,5 @@ import type { RenderContext, Renderer } from '@hey-api/codegen-core'; -import type { ResolveModuleName } from '@hey-api/shared'; +import type { BaseOutput } from '@hey-api/shared'; import type { MaybeArray, MaybeFunc } from '@hey-api/types'; import { py } from '../../py-compiler'; @@ -36,36 +36,27 @@ export class PythonRenderer implements Renderer { */ private _header?: HeaderArg; /** - * Whether `export * from 'module'` should be used when possible instead of named exports. - * - * @private - */ - private _preferExportAll: boolean; - /** - * Controls whether imports/exports include a file extension (e.g., '.ts' or '.js'). + * Options for module specifier resolution. * * @private */ - private _preferFileExtension: string; + private _module?: Partial['module']; /** - * Optional function to transform module specifiers. + * Whether `export * from 'module'` should be used when possible instead of named exports. * * @private */ - private _resolveModuleName?: ResolveModuleName; + private _preferExportAll: boolean; constructor( - args: { + args: Pick, 'module'> & { header?: HeaderArg; preferExportAll?: boolean; - preferFileExtension?: string; - resolveModuleName?: ResolveModuleName; } = {}, ) { this._header = args.header; + this._module = args.module; this._preferExportAll = args.preferExportAll ?? false; - this._preferFileExtension = args.preferFileExtension ?? ''; - this._resolveModuleName = args.resolveModuleName; } render(ctx: RenderContext): string { @@ -200,10 +191,10 @@ export class PythonRenderer implements Renderer { const sortKey = moduleSortKey({ file: ctx.file, fromFile: exp.from, - preferFileExtension: this._preferFileExtension, + preferFileExtension: this._module?.extension || '', root: ctx.project.root, }); - const modulePath = this._resolveModuleName?.(sortKey[2], ctx) ?? sortKey[2]; + const modulePath = this._module?.resolve?.(sortKey[2], ctx) ?? sortKey[2]; const [groupIndex] = sortKey; if (!groups.has(groupIndex)) groups.set(groupIndex, new Map()); @@ -266,10 +257,10 @@ export class PythonRenderer implements Renderer { const sortKey = moduleSortKey({ file: ctx.file, fromFile: imp.from, - preferFileExtension: this._preferFileExtension, + preferFileExtension: this._module?.extension || '', root: ctx.project.root, }); - const modulePath = this._resolveModuleName?.(sortKey[2], ctx) ?? sortKey[2]; + const modulePath = this._module?.resolve?.(sortKey[2], ctx) ?? sortKey[2]; const [groupIndex] = sortKey; if (!groups.has(groupIndex)) groups.set(groupIndex, new Map()); diff --git a/packages/openapi-ts/src/config/output/__tests__/config.test.ts b/packages/openapi-ts/src/config/output/__tests__/config.test.ts index ddb84c07f..4c48bd996 100644 --- a/packages/openapi-ts/src/config/output/__tests__/config.test.ts +++ b/packages/openapi-ts/src/config/output/__tests__/config.test.ts @@ -18,7 +18,7 @@ describe('getOutput', () => { }); describe('module resolution detection', () => { - it('should set importFileExtension when moduleResolution is NodeNext', () => { + it('should set module.extension when moduleResolution is NodeNext', () => { const tsconfigPath = path.join(tmpDir, 'tsconfig.json'); fs.writeFileSync( tsconfigPath, @@ -36,10 +36,10 @@ describe('getOutput', () => { }, }); - expect(output.importFileExtension).toBe('.js'); + expect(output.module.extension).toBe('.js'); }); - it('should set importFileExtension when moduleResolution is Node16', () => { + it('should set module.extension when moduleResolution is Node16', () => { const tsconfigPath = path.join(tmpDir, 'tsconfig.json'); fs.writeFileSync( tsconfigPath, @@ -57,10 +57,10 @@ describe('getOutput', () => { }, }); - expect(output.importFileExtension).toBe('.js'); + expect(output.module.extension).toBe('.js'); }); - it('should set importFileExtension when module is NodeNext (implicit moduleResolution)', () => { + it('should set module.extension when module is NodeNext (implicit moduleResolution)', () => { const tsconfigPath = path.join(tmpDir, 'tsconfig.json'); fs.writeFileSync( tsconfigPath, @@ -78,10 +78,10 @@ describe('getOutput', () => { }, }); - expect(output.importFileExtension).toBe('.js'); + expect(output.module.extension).toBe('.js'); }); - it('should set importFileExtension when module is Node16 (implicit moduleResolution)', () => { + it('should set module.extension when module is Node16 (implicit moduleResolution)', () => { const tsconfigPath = path.join(tmpDir, 'tsconfig.json'); fs.writeFileSync( tsconfigPath, @@ -99,10 +99,10 @@ describe('getOutput', () => { }, }); - expect(output.importFileExtension).toBe('.js'); + expect(output.module.extension).toBe('.js'); }); - it('should not set importFileExtension for other module types', () => { + it('should not set module.extension for other module types', () => { const tsconfigPath = path.join(tmpDir, 'tsconfig.json'); fs.writeFileSync( tsconfigPath, @@ -120,10 +120,10 @@ describe('getOutput', () => { }, }); - expect(output.importFileExtension).toBeUndefined(); + expect(output.module.extension).toBeUndefined(); }); - it('should not override explicit importFileExtension setting', () => { + it('should not override explicit module.extension setting', () => { const tsconfigPath = path.join(tmpDir, 'tsconfig.json'); fs.writeFileSync( tsconfigPath, @@ -136,13 +136,15 @@ describe('getOutput', () => { const output = getOutput({ output: { - importFileExtension: '.ts', + module: { + extension: '.ts', + }, path: tmpDir, tsConfigPath: tsconfigPath, }, }); - expect(output.importFileExtension).toBe('.ts'); + expect(output.module.extension).toBe('.ts'); }); it('should work when both module and moduleResolution are set', () => { @@ -164,7 +166,7 @@ describe('getOutput', () => { }, }); - expect(output.importFileExtension).toBe('.js'); + expect(output.module.extension).toBe('.js'); }); it('should handle missing tsconfig gracefully', () => { @@ -174,7 +176,7 @@ describe('getOutput', () => { }, }); - expect(output.importFileExtension).toBeUndefined(); + expect(output.module.extension).toBeUndefined(); }); }); }); diff --git a/packages/openapi-ts/src/config/output/config.ts b/packages/openapi-ts/src/config/output/config.ts index 678bb8af5..8d3077955 100644 --- a/packages/openapi-ts/src/config/output/config.ts +++ b/packages/openapi-ts/src/config/output/config.ts @@ -37,6 +37,7 @@ export function getOutput(userConfig: { output: MaybeArray }, format: null, lint: null, + module: {}, path: '', postProcess: [], preferExportAll: false, @@ -57,13 +58,27 @@ export function getOutput(userConfig: { output: MaybeArray }, value: fields.fileName, }), + module: valueToObject({ + defaultValue: { + extension: fields.importFileExtension, + resolve: fields.resolveModuleName, + }, + mappers: { + object: (moduleFields) => ({ + ...moduleFields, + extension: fields.importFileExtension ?? moduleFields.extension, + resolve: fields.resolveModuleName ?? moduleFields.resolve, + }), + }, + value: fields.module, + }), }), }, value: userOutput, }) as Output; output.tsConfig = loadTsConfig(findTsConfigPath(__dirname, output.tsConfigPath)); if ( - output.importFileExtension === undefined && + output.module.extension === undefined && (output.tsConfig?.compilerOptions?.moduleResolution === 'nodenext' || output.tsConfig?.compilerOptions?.moduleResolution === 'NodeNext' || output.tsConfig?.compilerOptions?.moduleResolution === 'node16' || @@ -73,10 +88,10 @@ export function getOutput(userConfig: { output: MaybeArray output.tsConfig?.compilerOptions?.module === 'node16' || output.tsConfig?.compilerOptions?.module === 'Node16') ) { - output.importFileExtension = '.js'; + output.module.extension = '.js'; } - if (output.importFileExtension && !output.importFileExtension.startsWith('.')) { - output.importFileExtension = `.${output.importFileExtension}`; + if (output.module.extension && !output.module.extension.startsWith('.')) { + output.module.extension = `.${output.module.extension}`; } output.postProcess = normalizePostProcess(userOutput.postProcess ?? legacyPostProcess); output.source = resolveSource(output); diff --git a/packages/openapi-ts/src/config/output/types.ts b/packages/openapi-ts/src/config/output/types.ts index 196490cd9..40c52cb85 100644 --- a/packages/openapi-ts/src/config/output/types.ts +++ b/packages/openapi-ts/src/config/output/types.ts @@ -4,9 +4,7 @@ import type { TsConfigJsonResolved } from 'get-tsconfig'; import type { Formatters, Linters, PostProcessorPreset } from './postprocess'; -type ImportFileExtensions = '.js' | '.ts'; - -export type UserOutput = BaseUserOutput & { +export type UserOutput = BaseUserOutput<'.js' | '.ts'> & { /** * Which formatter to use to process output folder? * @@ -21,8 +19,9 @@ export type UserOutput = BaseUserOutput & { * `node16`, we default to `.js`. * * @default undefined + * @deprecated Use `module.extension` instead. */ - importFileExtension?: ImportFileExtensions | AnyString | null; + importFileExtension?: '.js' | '.ts' | AnyString | null; /** * Which linter to use to process output folder? * @@ -60,18 +59,11 @@ export type UserOutput = BaseUserOutput & { tsConfigPath?: AnyString | null; }; -export type Output = BaseOutput & { +export type Output = BaseOutput<'.js' | '.ts'> & { /** * Which formatter to use to process output folder? */ format: Formatters | null; - /** - * If specified, this will be the file extension used when importing - * other modules. By default, we don't add a file extension and let the - * runtime resolve it. If you're using moduleResolution `nodenext` or - * `node16`, we default to `.js`. - */ - importFileExtension: ImportFileExtensions | AnyString | null | undefined; /** * Which linter to use to process output folder? */ diff --git a/packages/openapi-ts/src/createClient.ts b/packages/openapi-ts/src/createClient.ts index ead0a82d9..0b5194bda 100644 --- a/packages/openapi-ts/src/createClient.ts +++ b/packages/openapi-ts/src/createClient.ts @@ -131,9 +131,8 @@ export async function createClient({ const result = typeof header === 'function' ? header({ ...ctx, defaultValue }) : header; return result === undefined ? defaultValue : result; }, + module: config.output.module, preferExportAll: config.output.preferExportAll, - preferFileExtension: config.output.importFileExtension || undefined, - resolveModuleName: config.output.resolveModuleName, }), ], root: config.output.path, diff --git a/packages/openapi-ts/src/generate/client.ts b/packages/openapi-ts/src/generate/client.ts index e6a1db931..9b8f923d3 100644 --- a/packages/openapi-ts/src/generate/client.ts +++ b/packages/openapi-ts/src/generate/client.ts @@ -2,8 +2,8 @@ import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; -import type { IProject, ProjectRenderMeta } from '@hey-api/codegen-core'; -import type { DefinePlugin, OutputHeader } from '@hey-api/shared'; +import type { IProject } from '@hey-api/codegen-core'; +import type { BaseOutput, DefinePlugin, OutputHeader } from '@hey-api/shared'; import { ensureDirSync, isEnvironment, outputHeaderToPrefix } from '@hey-api/shared'; import type { Config } from '../config/types'; @@ -100,12 +100,11 @@ function renameFile({ function replaceImports({ filePath, header, - meta, + module, renamed, -}: { +}: Pick & { filePath: string; header?: string; - meta: ProjectRenderMeta; renamed: Map; }): void { let content = fs.readFileSync(filePath, 'utf8'); @@ -124,8 +123,7 @@ function replaceImports({ const fileName = path.basename(importPath, extension); const importDir = path.dirname(importPath); const replacedName = - (renamed.get(fileName) ?? fileName) + - (meta.importFileExtension ? meta.importFileExtension : extension); + (renamed.get(fileName) ?? fileName) + (module.extension ? module.extension : extension); const replacedMatch = match.slice(0, importIndex) + [importDir, replacedName].filter(Boolean).join('/') + @@ -145,13 +143,12 @@ function replaceImports({ */ export function generateClientBundle({ header, - meta, + module, outputPath, plugin, project, -}: { +}: Pick & { header?: OutputHeader; - meta: ProjectRenderMeta; outputPath: string; plugin: DefinePlugin['Config']; project: IProject; @@ -203,7 +200,7 @@ export function generateClientBundle({ replaceImports({ filePath: path.resolve(coreOutputPath, file), header: headerPrefix, - meta, + module, renamed, }); } @@ -213,7 +210,7 @@ export function generateClientBundle({ replaceImports({ filePath: path.resolve(clientOutputPath, file), header: headerPrefix, - meta, + module, renamed, }); } diff --git a/packages/openapi-ts/src/generate/output.ts b/packages/openapi-ts/src/generate/output.ts index 562b1a823..64436aaf8 100644 --- a/packages/openapi-ts/src/generate/output.ts +++ b/packages/openapi-ts/src/generate/output.ts @@ -25,9 +25,7 @@ export async function generateOutput(context: Context): Promise { // @ts-expect-error config._FRAGILE_CLIENT_BUNDLE_RENAMED = generateClientBundle({ header: config.output.header, - meta: { - importFileExtension: config.output.importFileExtension, - }, + module: config.output.module, outputPath, // @ts-expect-error plugin: client, diff --git a/packages/openapi-ts/src/ts-dsl/utils/render.ts b/packages/openapi-ts/src/ts-dsl/utils/render.ts index cbc59b9ed..4f083f065 100644 --- a/packages/openapi-ts/src/ts-dsl/utils/render.ts +++ b/packages/openapi-ts/src/ts-dsl/utils/render.ts @@ -1,5 +1,5 @@ import type { RenderContext, Renderer } from '@hey-api/codegen-core'; -import type { ResolveModuleName } from '@hey-api/shared'; +import type { BaseOutput } from '@hey-api/shared'; import type { MaybeArray, MaybeFunc } from '@hey-api/types'; import ts from 'typescript'; @@ -37,36 +37,27 @@ export class TypeScriptRenderer implements Renderer { */ private _header?: HeaderArg; /** - * Whether `export * from 'module'` should be used when possible instead of named exports. - * - * @private - */ - private _preferExportAll: boolean; - /** - * Controls whether imports/exports include a file extension (e.g., '.ts' or '.js'). + * Options for module specifier resolution. * * @private */ - private _preferFileExtension: string; + private _module?: Partial['module']; /** - * Optional function to transform module specifiers. + * Whether `export * from 'module'` should be used when possible instead of named exports. * * @private */ - private _resolveModuleName?: ResolveModuleName; + private _preferExportAll: boolean; constructor( - args: { + args: Pick, 'module'> & { header?: HeaderArg; preferExportAll?: boolean; - preferFileExtension?: string; - resolveModuleName?: ResolveModuleName; } = {}, ) { this._header = args.header; + this._module = args.module; this._preferExportAll = args.preferExportAll ?? false; - this._preferFileExtension = args.preferFileExtension ?? ''; - this._resolveModuleName = args.resolveModuleName; } render(ctx: RenderContext): string { @@ -195,10 +186,10 @@ export class TypeScriptRenderer implements Renderer { const sortKey = moduleSortKey({ file: ctx.file, fromFile: exp.from, - preferFileExtension: this._preferFileExtension, + preferFileExtension: this._module?.extension || '', root: ctx.project.root, }); - const modulePath = this._resolveModuleName?.(sortKey[2], ctx) ?? sortKey[2]; + const modulePath = this._module?.resolve?.(sortKey[2], ctx) ?? sortKey[2]; const [groupIndex] = sortKey; if (!groups.has(groupIndex)) groups.set(groupIndex, new Map()); @@ -261,10 +252,10 @@ export class TypeScriptRenderer implements Renderer { const sortKey = moduleSortKey({ file: ctx.file, fromFile: imp.from, - preferFileExtension: this._preferFileExtension, + preferFileExtension: this._module?.extension || '', root: ctx.project.root, }); - const modulePath = this._resolveModuleName?.(sortKey[2], ctx) ?? sortKey[2]; + const modulePath = this._module?.resolve?.(sortKey[2], ctx) ?? sortKey[2]; const [groupIndex] = sortKey; if (!groups.has(groupIndex)) groups.set(groupIndex, new Map()); diff --git a/packages/shared/src/config/shared.ts b/packages/shared/src/config/shared.ts index 945139bfb..a754ed515 100644 --- a/packages/shared/src/config/shared.ts +++ b/packages/shared/src/config/shared.ts @@ -1,5 +1,5 @@ import type { NameConflictResolver, RenderContext, Symbol } from '@hey-api/codegen-core'; -import type { MaybeArray } from '@hey-api/types'; +import type { AnyString, MaybeArray } from '@hey-api/types'; import type { Plugin } from '../plugins/types'; import type { Logs } from '../types/logs'; @@ -82,7 +82,7 @@ export type NamingOptions = { /** * Base output shape all packages must satisfy. */ -export interface BaseUserOutput { +export interface BaseUserOutput { /** * Defines casing of the output fields. By default, we preserve `input` * values as data transforms incur a performance penalty at runtime. @@ -155,6 +155,25 @@ export interface BaseUserOutput { * @deprecated use `entryFile` instead */ indexFile?: boolean; + /** + * Options for module specifier resolution. + */ + module?: { + /** + * If specified, this will be the extension used when importing other + * modules. By default, we don't add an extension unless we detect that + * you're using a module resolution strategy that requires one. + * + * @default undefined + */ + extension?: TModuleExtension | AnyString | null; + /** + * Function to transform module specifiers. + * + * @default undefined + */ + resolve?: ResolveModuleFn; + }; /** * Optional name conflict resolver to customize how naming conflicts * are handled. @@ -168,8 +187,9 @@ export interface BaseUserOutput { * Optional function to transform module specifiers. * * @default undefined + * @deprecated use `module.resolve` instead */ - resolveModuleName?: ResolveModuleName; + resolveModuleName?: ResolveModuleFn; /** * Configuration for generating a copy of the input source used to produce this output. * @@ -185,7 +205,7 @@ export interface BaseUserOutput { /** * Base output shape all packages must satisfy. */ -export interface BaseOutput { +export interface BaseOutput { /** * Defines casing of the output fields. By default, we preserve `input` * values as data transforms incur a performance penalty at runtime. @@ -198,9 +218,7 @@ export interface BaseOutput { * input, or package version changes. */ clean: boolean; - /** - * Whether to generate an entry file that re-exports symbols for convenient imports. - */ + /** Whether to generate an entry file that re-exports symbols for convenient imports. */ entryFile: boolean; /** * Optional function to transform file names before they are used. @@ -221,9 +239,7 @@ export interface BaseOutput { */ suffix: string | null; }; - /** - * Text to include at the top of every generated file. - */ + /** Text to include at the top of every generated file. */ header: OutputHeader; /** * Whether to generate an entry file that re-exports symbols for convenient imports. @@ -231,26 +247,20 @@ export interface BaseOutput { * @deprecated use `entryFile` instead */ indexFile: boolean; - /** - * Optional name conflict resolver to customize how naming conflicts - * are handled. - */ + /** Options for module specifier resolution. */ + module: { + /** The extension used when importing other modules. */ + extension: TModuleExtension | AnyString | null; + /** Function to transform module specifiers. */ + resolve: ResolveModuleFn | undefined; + }; + /** Name conflict resolver to customize how naming conflicts are handled. */ nameConflictResolver: NameConflictResolver | undefined; - /** - * The absolute path to the output folder. - */ + /** The absolute path to the output folder. */ path: string; - /** - * Post-processing commands to run on the output folder, executed in order. - */ + /** Post-processing commands to run on the output folder, executed in order. */ postProcess: ReadonlyArray; - /** - * Optional function to transform module specifiers. - */ - resolveModuleName: ResolveModuleName | undefined; - /** - * Configuration for generating a copy of the input source used to produce this output. - */ + /** Configuration for generating a copy of the input source used to produce this output. */ source: SourceConfig; } @@ -350,4 +360,4 @@ export type AnyConfig = BaseConfig, BaseOutput>; /** * Function to transform module specifiers. */ -export type ResolveModuleName = (moduleName: string, ctx: RenderContext) => string | undefined; +export type ResolveModuleFn = (path: string, ctx: RenderContext) => string | undefined; diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 3656961ed..096883981 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -23,7 +23,7 @@ export type { FeatureToggle, IndexExportOption, NamingOptions, - ResolveModuleName, + ResolveModuleFn, UserCommentsOption, UserIndexExportOption, } from './config/shared';