diff --git a/.editorconfig b/.editorconfig index 903a3b5f0..0f1786729 100755 --- a/.editorconfig +++ b/.editorconfig @@ -7,7 +7,3 @@ indent_size = 2 indent_style = space insert_final_newline = true trim_trailing_whitespace = true - -[*.hbs] -indent_size = 2 -indent_style = space -- 2.51.2 From 701a69cae24d3068235d80561e2cf9d18d7ccac9 Mon Sep 17 00:00:00 2001 From: Lubos Date: Sun, 11 Jan 2026 16:46:18 +0800 Subject: [PATCH 2/2] feat(config): add oxfmt preset --- .changeset/large-plums-sleep.md | 5 + dev/openapi-ts.config.ts | 3 +- docs/openapi-ts/configuration/output.md | 63 ++++----- docs/openapi-ts/migrating.md | 2 +- .../openapi-ts/src/config/output/config.ts | 78 ++++++++++- .../openapi-ts/src/config/output/index.ts | 2 +- .../src/config/output/postprocess.ts | 121 ++++++++++++------ .../openapi-ts/src/config/output/types.d.ts | 29 ++++- packages/openapi-ts/src/createClient.ts | 4 +- 9 files changed, 224 insertions(+), 83 deletions(-) create mode 100644 .changeset/large-plums-sleep.md diff --git a/.changeset/large-plums-sleep.md b/.changeset/large-plums-sleep.md new file mode 100644 index 000000000..be3af70a9 --- /dev/null +++ b/.changeset/large-plums-sleep.md @@ -0,0 +1,5 @@ +--- +'@hey-api/openapi-ts': patch +--- + +**output**: add `oxfmt` preset diff --git a/dev/openapi-ts.config.ts b/dev/openapi-ts.config.ts index 4d609e25c..428f24cfe 100644 --- a/dev/openapi-ts.config.ts +++ b/dev/openapi-ts.config.ts @@ -116,7 +116,6 @@ export default defineConfig(() => { // // name: '{{name}}.renamed', // suffix: '.meh', // }, - // format: 'prettier', // header: null, header: [ '/* eslint-disable */', @@ -124,12 +123,12 @@ export default defineConfig(() => { ], // importFileExtension: '.js', // indexFile: false, - // lint: 'eslint', // nameConflictResolver({ attempt, baseName }) { // // console.log('resolving conflict for:', { attempt, baseName }); // return attempt === 0 ? baseName : `${baseName}_N${attempt + 1}`; // }, path: path.resolve(__dirname, '.gen'), + postProcess: ['eslint'], // preferExportAll: true, resolveModuleName: (moduleName) => { if (moduleName === 'valibot') { diff --git a/docs/openapi-ts/configuration/output.md b/docs/openapi-ts/configuration/output.md index 77d4b0d0b..b9964d0fc 100644 --- a/docs/openapi-ts/configuration/output.md +++ b/docs/openapi-ts/configuration/output.md @@ -214,95 +214,100 @@ export default { }; ``` -## Format +## Post Process -To format your output folder contents, set `format` to a valid formatter. +Post-processing allows you to run commands on the generated output folder after files are written. This is typically used to run formatters, linters, or other cleanup tools. + +Commands are executed in order, and each command receives the output path via the `path` placeholder. + +### Presets + +You can use built-in presets for common tools: ::: code-group -```js [disabled] +```js [biome:format] export default { input: 'hey-api/backend', // sign up at app.heyapi.dev output: { - format: null, // [!code ++] path: 'src/client', + postProcess: ['biome:format'], // [!code ++] }, }; ``` -```js [prettier] +```js [biome:lint] export default { input: 'hey-api/backend', // sign up at app.heyapi.dev output: { - format: 'prettier', // [!code ++] path: 'src/client', + postProcess: ['biome:lint'], // [!code ++] }, }; ``` -```js [biome] +```js [eslint] export default { input: 'hey-api/backend', // sign up at app.heyapi.dev output: { - format: 'biome', // [!code ++] path: 'src/client', + postProcess: ['eslint'], // [!code ++] }, }; ``` -::: - -You can also prevent your output from being formatted by adding your output path to the formatter's ignore file. - -## Lint - -To lint your output folder contents, set `lint` to a valid linter. - -::: code-group - -```js [disabled] +```js [oxfmt] export default { input: 'hey-api/backend', // sign up at app.heyapi.dev output: { - lint: null, // [!code ++] path: 'src/client', + postProcess: ['oxfmt'], // [!code ++] }, }; ``` -```js [eslint] +```js [oxlint] export default { input: 'hey-api/backend', // sign up at app.heyapi.dev output: { - lint: 'eslint', // [!code ++] path: 'src/client', + postProcess: ['oxlint'], // [!code ++] }, }; ``` -```js [biome] +```js [prettier] export default { input: 'hey-api/backend', // sign up at app.heyapi.dev output: { - lint: 'biome', // [!code ++] path: 'src/client', + postProcess: ['prettier'], // [!code ++] }, }; ``` -```js [oxlint] +::: + +### Custom + +You can also provide custom post processors: + + +```js export default { input: 'hey-api/backend', // sign up at app.heyapi.dev output: { - lint: 'oxlint', // [!code ++] path: 'src/client', + postProcess: [{ // [!code ++] + command: 'dprint', // [!code ++] + args: ['fmt', '{{path}}'], // [!code ++] + }], // [!code ++] }, }; ``` + -::: - -You can also prevent your output from being linted by adding your output path to the linter's ignore file. +You can skip processing by adding the output path to the tool’s ignore file (for example `.eslintignore` or `.prettierignore`). ## Name Conflicts diff --git a/docs/openapi-ts/migrating.md b/docs/openapi-ts/migrating.md index 305430b4c..bafd728d2 100644 --- a/docs/openapi-ts/migrating.md +++ b/docs/openapi-ts/migrating.md @@ -1506,7 +1506,7 @@ This command is now called `openapi-ts`. ### Removed `indent` -This config option has been removed. Use a [code formatter](/openapi-ts/configuration#formatting) to modify the generated files code style according to your preferences. +This config option has been removed. Use a [code formatter](/openapi-ts/configuration/output#post-process) to modify the generated files code style according to your preferences. ## v0.27.24 diff --git a/packages/openapi-ts/src/config/output/config.ts b/packages/openapi-ts/src/config/output/config.ts index 66bb3798a..1002bf550 100644 --- a/packages/openapi-ts/src/config/output/config.ts +++ b/packages/openapi-ts/src/config/output/config.ts @@ -1,10 +1,14 @@ +import { log } from '@hey-api/codegen-core'; import ts from 'typescript'; import { findTsConfigPath, loadTsConfig } from '~/generate/tsConfig'; import type { Config, UserConfig } from '~/types/config'; import { valueToObject } from '../utils/config'; +import type { PostProcessor, UserPostProcessor } from './postprocess'; +import { postProcessors } from './postprocess'; import { resolveSource } from './source/config'; +import type { UserOutput } from './types'; export function getOutput(userConfig: UserConfig): Config['output'] { if (userConfig.output instanceof Array) { @@ -13,6 +17,13 @@ export function getOutput(userConfig: UserConfig): Config['output'] { ); } + const userOutput = + typeof userConfig.output === 'string' + ? { path: userConfig.output } + : (userConfig.output ?? {}); + + const legacyPostProcess = resolveLegacyPostProcess(userOutput); + const output = valueToObject({ defaultValue: { clean: true, @@ -26,6 +37,7 @@ export function getOutput(userConfig: UserConfig): Config['output'] { indexFile: true, lint: null, path: '', + postProcess: [], preferExportAll: false, }, mappers: { @@ -45,9 +57,8 @@ export function getOutput(userConfig: UserConfig): Config['output'] { value: fields.fileName, }), }), - string: (path) => ({ path }), }, - value: userConfig.output, + value: userOutput, }) as Config['output']; output.tsConfig = loadTsConfig(findTsConfigPath(output.tsConfigPath)); if ( @@ -65,6 +76,69 @@ export function getOutput(userConfig: UserConfig): Config['output'] { ) { output.importFileExtension = `.${output.importFileExtension}`; } + output.postProcess = normalizePostProcess( + userOutput.postProcess ?? legacyPostProcess, + ); output.source = resolveSource(output); return output; } + +function resolveLegacyPostProcess( + config: Partial, +): ReadonlyArray { + const result: Array = []; + + if (config.lint !== undefined) { + let processor: PostProcessor | undefined; + let preset: keyof typeof postProcessors | undefined; + if (config.lint) { + preset = config.lint === 'biome' ? 'biome:lint' : config.lint; + processor = postProcessors[preset]; + if (processor) result.push(processor); + } + + log.warnDeprecated({ + context: 'output', + field: 'lint', + replacement: `postProcess: [${processor && preset ? `'${preset}'` : ''}]`, + }); + } + + if (config.format !== undefined) { + let processor: PostProcessor | undefined; + let preset: keyof typeof postProcessors | undefined; + if (config.format) { + preset = config.format === 'biome' ? 'biome:format' : config.format; + processor = postProcessors[preset]; + if (processor) result.push(processor); + } + + log.warnDeprecated({ + context: 'output', + field: 'format', + replacement: `postProcess: [${processor && preset ? `'${preset}'` : ''}]`, + }); + } + + return result; +} + +function normalizePostProcess( + input: UserOutput['postProcess'], +): ReadonlyArray { + if (!input) return []; + + return input.map((item) => { + if (typeof item === 'string') { + const preset = postProcessors[item]; + if (!preset) { + throw new Error(`Unknown post-processor preset: "${item}"`); + } + return preset; + } + return { + name: item.name ?? item.command, + ...item, + }; + }); +} diff --git a/packages/openapi-ts/src/config/output/index.ts b/packages/openapi-ts/src/config/output/index.ts index 80f9f1049..84b2508cd 100644 --- a/packages/openapi-ts/src/config/output/index.ts +++ b/packages/openapi-ts/src/config/output/index.ts @@ -1,3 +1,3 @@ export { getOutput } from './config'; export { postprocessOutput } from './postprocess'; -export type { Formatters, Linters, Output, UserOutput } from './types'; +export type { Output, UserOutput } from './types'; diff --git a/packages/openapi-ts/src/config/output/postprocess.ts b/packages/openapi-ts/src/config/output/postprocess.ts index 1f07eb7ea..e05a838e1 100644 --- a/packages/openapi-ts/src/config/output/postprocess.ts +++ b/packages/openapi-ts/src/config/output/postprocess.ts @@ -1,66 +1,103 @@ +import colors from 'ansi-colors'; import { sync } from 'cross-spawn'; -import type { Formatters, Linters, Output } from './types'; +import type { Output } from './types'; -type OutputProcessor = { - args: (path: string) => ReadonlyArray; +/** + * @deprecated Use `PostProcessorPreset` instead. + */ +export type Formatters = 'biome' | 'prettier'; + +/** + * @deprecated Use `PostProcessorPreset` instead. + */ +export type Linters = 'biome' | 'eslint' | 'oxlint'; + +export type UserPostProcessor = { + /** + * Arguments to pass to the command. Use `{{path}}` as a placeholder + * for the output directory path. + * + * @example ['format', '--write', '{{path}}'] + */ + args: ReadonlyArray; + /** + * The command to run (e.g., 'biome', 'prettier', 'eslint'). + */ command: string; + /** + * Display name for logging. Defaults to the command name. + */ + name?: string; +}; + +export type PostProcessor = { + /** + * Arguments to pass to the command. + */ + args: ReadonlyArray; + /** + * The command to run. + */ + command: string; + /** + * Display name for logging. + */ name: string; }; -/** - * Map of supported formatters - */ -const formatters: Record = { - biome: { - args: (path) => ['format', '--write', path], +export const postProcessors = { + 'biome:format': { + args: ['format', '--write', '{{path}}'], command: 'biome', name: 'Biome (Format)', }, - prettier: { - args: (path) => [ - '--ignore-unknown', - path, - '--write', - '--ignore-path', - './.prettierignore', - ], - command: 'prettier', - name: 'Prettier', - }, -}; - -/** - * Map of supported linters - */ -const linters: Record = { - biome: { - args: (path) => ['lint', '--apply', path], + 'biome:lint': { + args: ['lint', '--apply', '{{path}}'], command: 'biome', name: 'Biome (Lint)', }, eslint: { - args: (path) => [path, '--fix'], + args: ['{{path}}', '--fix'], command: 'eslint', name: 'ESLint', }, + oxfmt: { + args: ['{{path}}'], + command: 'oxfmt', + name: 'Oxfmt', + }, oxlint: { - args: (path) => ['--fix', path], + args: ['--fix', '{{path}}'], command: 'oxlint', - name: 'oxlint', + name: 'Oxlint', }, -}; + prettier: { + args: [ + '--ignore-unknown', + '{{path}}', + '--write', + '--ignore-path', + './.prettierignore', + ], + command: 'prettier', + name: 'Prettier', + }, +} as const satisfies Record; -export const postprocessOutput = (config: Output): void => { - if (config.lint) { - const module = linters[config.lint]; - console.log(`✨ Running ${module.name}`); - sync(module.command, module.args(config.path)); - } +export type PostProcessorPreset = keyof typeof postProcessors; + +export const postprocessOutput = (config: Output, jobPrefix: string): void => { + for (const processor of config.postProcess) { + const resolved = + typeof processor === 'string' ? postProcessors[processor] : processor; + + const name = resolved.name ?? resolved.command; + const args = resolved.args.map((arg) => + arg.replace('{{path}}', config.path), + ); - if (config.format) { - const module = formatters[config.format]; - console.log(`✨ Running ${module.name}`); - sync(module.command, module.args(config.path)); + console.log(`${jobPrefix}🧹 Running ${colors.cyanBright(name)}`); + sync(resolved.command, args); } }; diff --git a/packages/openapi-ts/src/config/output/types.d.ts b/packages/openapi-ts/src/config/output/types.d.ts index c73e5acef..84cd72525 100644 --- a/packages/openapi-ts/src/config/output/types.d.ts +++ b/packages/openapi-ts/src/config/output/types.d.ts @@ -8,12 +8,15 @@ import type { MaybeArray, MaybeFunc } from '~/types/utils'; import type { Casing, NameTransformer } from '~/utils/naming'; import type { NamingOptions } from '../shared'; +import type { + Formatters, + Linters, + PostProcessor, + PostProcessorPreset, + UserPostProcessor, +} from './postprocess'; import type { SourceConfig, UserSourceConfig } from './source/types'; -export type Formatters = 'biome' | 'prettier'; - -export type Linters = 'biome' | 'eslint' | 'oxlint'; - type ImportFileExtensions = '.js' | '.ts'; type Header = MaybeFunc< @@ -76,6 +79,7 @@ export type UserOutput = { * Which formatter to use to process output folder? * * @default null + * @deprecated Use `postProcess` instead. */ format?: Formatters | null; /** @@ -103,6 +107,7 @@ export type UserOutput = { * Which linter to use to process output folder? * * @default null + * @deprecated Use `postProcess` instead. */ lint?: Linters | null; /** @@ -114,6 +119,18 @@ export type UserOutput = { * The absolute path to the output folder. */ path: string; + /** + * Post-processing commands to run on the output folder, executed in order. + * + * Use preset strings for common tools, or provide custom configurations. + * + * @example ['biome:lint', 'prettier'] + * @example [{ command: 'dprint', args: ['fmt', '{{path}}'] }] + * @example ['eslint', { command: 'prettier', args: ['{{path}}', '--write'] }] + * + * @default [] + */ + postProcess?: ReadonlyArray; /** * Whether `export * from 'module'` should be used when possible * instead of named exports. @@ -214,6 +231,10 @@ export type Output = { * The absolute path to the output folder. */ path: string; + /** + * Post-processing commands to run on the output folder, executed in order. + */ + postProcess: ReadonlyArray; /** * Whether `export * from 'module'` should be used when possible * instead of named exports. diff --git a/packages/openapi-ts/src/createClient.ts b/packages/openapi-ts/src/createClient.ts index 4534a4b54..ea4ca80bc 100644 --- a/packages/openapi-ts/src/createClient.ts +++ b/packages/openapi-ts/src/createClient.ts @@ -327,13 +327,13 @@ export const createClient = async ({ const eventPostprocess = logger.timeEvent('postprocess'); if (!config.dryRun) { - postprocessOutput(config.output); + const jobPrefix = colors.gray(`[Job ${jobIndex + 1}] `); + postprocessOutput(config.output, jobPrefix); if (config.logs.level !== 'silent') { const outputPath = process.env.INIT_CWD ? `./${path.relative(process.env.INIT_CWD, config.output.path)}` : config.output.path; - const jobPrefix = colors.gray(`[Job ${jobIndex + 1}] `); console.log( `${jobPrefix}${colors.green('✅ Done!')} Your output is in ${colors.cyanBright(outputPath)}`, );