From c0ef5fa33a22e2dc9f0ca3f4c7030892738aab92 Mon Sep 17 00:00:00 2001 From: Jure Rotar Date: Thu, 19 Mar 2026 15:35:34 +0100 Subject: [PATCH 1/9] feat: replaced excludeTags with includeTags --- README.md | 4 ++-- openapi-codegen.config.mjs | 2 +- src/commands/check.command.ts | 4 ++-- src/commands/check.ts | 4 ++-- src/commands/generate.command.ts | 4 ++-- src/commands/generate.ts | 2 +- src/generators/const/options.const.ts | 2 +- src/generators/core/resolveConfig.ts | 8 ++++---- src/generators/run/generate.runner.ts | 4 ++-- src/generators/types/options.ts | 2 +- src/generators/utils/object.utils.test.ts | 4 ++-- src/generators/utils/operation.utils.ts | 6 +++--- src/generators/utils/tag.utils.ts | 7 +++++-- test/config.ts | 2 +- 14 files changed, 29 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index b5b270a6..3ee7a8c8 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,7 @@ yarn openapi-codegen generate --config my-config.ts --splitByTags Organize output into separate folders based on OpenAPI operation tags (default: true) --defaultTag (Requires `--splitByTags`) Default tag for shared code across multiple tags (default: 'Common') - --excludeTags Comma-separated list of tags to exclude from generation + --includeTags Comma-separated list of tags to include in generation --excludePathRegex Exclude operations whose paths match the given regular expression --excludeRedundantZodSchemas Exclude any redundant Zod schemas (default: true) @@ -115,7 +115,7 @@ yarn openapi-codegen generate --config my-config.ts --splitByTags Organize output into separate folders based on OpenAPI operation tags (default: true) --defaultTag (Requires `--splitByTags`) Default tag for shared code across multiple tags (default: 'Common') - --excludeTags Comma-separated list of tags to exclude from generation + --includeTags Comma-separated list of tags to include in generation --excludePathRegex Exclude operations whose paths match the given regular expression --excludeRedundantZodSchemas Exclude any redundant Zod schemas (default: true) ``` diff --git a/openapi-codegen.config.mjs b/openapi-codegen.config.mjs index b108e6ed..71b80682 100644 --- a/openapi-codegen.config.mjs +++ b/openapi-codegen.config.mjs @@ -2,7 +2,7 @@ const config = { input: "http://127.0.0.1:4000/docs-json", output: "./test/generated/next", - excludeTags: ["auth"], + includeTags: ["auth"], replaceOptionalWithNullish: true, builderConfigs: true, infiniteQueries: true, diff --git a/src/commands/check.command.ts b/src/commands/check.command.ts index 086bfda0..bbe719d2 100644 --- a/src/commands/check.command.ts +++ b/src/commands/check.command.ts @@ -19,8 +19,8 @@ class CheckOptions implements CheckParams { @YargOption({ envAlias: "defaultTag" }) defaultTag?: string; - @YargOption({ envAlias: "excludeTags" }) - excludeTags?: string; + @YargOption({ envAlias: "includeTags" }) + includeTags?: string; @YargOption({ envAlias: "excludePathRegex" }) excludePathRegex?: string; diff --git a/src/commands/check.ts b/src/commands/check.ts index 7d33b5ff..09959c61 100644 --- a/src/commands/check.ts +++ b/src/commands/check.ts @@ -9,11 +9,11 @@ import SwaggerParser from "@apidevtools/swagger-parser"; export type CheckParams = { config?: string; - excludeTags?: string; + includeTags?: string; verbose?: boolean; } & Partial>; -export async function check({ verbose, config: configParam, excludeTags: _excludeTagsParam, ...params }: CheckParams) { +export async function check({ verbose, config: configParam, includeTags: _includeTagsParam, ...params }: CheckParams) { const start = Date.now(); if (verbose) { diff --git a/src/commands/generate.command.ts b/src/commands/generate.command.ts index f3181adf..31367ffc 100644 --- a/src/commands/generate.command.ts +++ b/src/commands/generate.command.ts @@ -31,8 +31,8 @@ class GenerateOptions implements GenerateParams { @YargOption({ envAlias: "defaultTag" }) defaultTag?: string; - @YargOption({ envAlias: "excludeTags" }) - excludeTags?: string; + @YargOption({ envAlias: "includeTags" }) + includeTags?: string; @YargOption({ envAlias: "excludePathRegex" }) excludePathRegex?: string; diff --git a/src/commands/generate.ts b/src/commands/generate.ts index 421b0790..d6689c4c 100644 --- a/src/commands/generate.ts +++ b/src/commands/generate.ts @@ -7,7 +7,7 @@ import { Profiler } from "@/helpers/profile.helper"; export type GenerateParams = { config?: string; - excludeTags?: string; + includeTags?: string; inlineEndpointsExcludeModules?: string; prettier?: boolean; verbose?: boolean; diff --git a/src/generators/const/options.const.ts b/src/generators/const/options.const.ts index fcf5d473..51b509a1 100644 --- a/src/generators/const/options.const.ts +++ b/src/generators/const/options.const.ts @@ -11,7 +11,7 @@ export const DEFAULT_GENERATE_OPTIONS: GenerateOptions = { incremental: true, splitByTags: true, defaultTag: "Common", - excludeTags: [], + includeTags: [], excludePathRegex: "", excludeRedundantZodSchemas: true, tsNamespaces: true, diff --git a/src/generators/core/resolveConfig.ts b/src/generators/core/resolveConfig.ts index 13927846..13040e4d 100644 --- a/src/generators/core/resolveConfig.ts +++ b/src/generators/core/resolveConfig.ts @@ -4,19 +4,19 @@ import { deepMerge } from "@/generators/utils/object.utils"; export function resolveConfig({ fileConfig = {}, - params: { excludeTags, inlineEndpointsExcludeModules, ...options }, + params: { includeTags, inlineEndpointsExcludeModules, ...options }, }: { fileConfig?: Partial | null; params: Partial< - Omit & { - excludeTags: string; + Omit & { + includeTags: string; inlineEndpointsExcludeModules: string; } >; }) { const resolvedConfig = deepMerge(DEFAULT_GENERATE_OPTIONS, fileConfig ?? {}, { ...options, - excludeTags: excludeTags?.split(","), + includeTags: includeTags?.split(","), inlineEndpointsExcludeModules: inlineEndpointsExcludeModules?.split(","), }); resolvedConfig.checkAcl = resolvedConfig.acl && resolvedConfig.checkAcl; diff --git a/src/generators/run/generate.runner.ts b/src/generators/run/generate.runner.ts index e51f55e1..11293340 100644 --- a/src/generators/run/generate.runner.ts +++ b/src/generators/run/generate.runner.ts @@ -31,8 +31,8 @@ export async function runGenerate({ }: { fileConfig?: Partial | null; params?: Partial< - Omit & { - excludeTags: string; + Omit & { + includeTags: string; inlineEndpointsExcludeModules: string; } >; diff --git a/src/generators/types/options.ts b/src/generators/types/options.ts index 5b2e38a7..07569ce4 100644 --- a/src/generators/types/options.ts +++ b/src/generators/types/options.ts @@ -68,7 +68,7 @@ interface BaseGenerateOptions { incremental?: boolean; splitByTags: boolean; defaultTag: string; - excludeTags: string[]; + includeTags: string[]; excludePathRegex: string; excludeRedundantZodSchemas: boolean; tsNamespaces: boolean; diff --git a/src/generators/utils/object.utils.test.ts b/src/generators/utils/object.utils.test.ts index ad888243..5acc9c33 100644 --- a/src/generators/utils/object.utils.test.ts +++ b/src/generators/utils/object.utils.test.ts @@ -162,7 +162,7 @@ describe("Utils: object", () => { output: "output", splitByTags: true, defaultTag: "Common", - excludeTags: [], + includeTags: [], excludePathRegex: "", excludeRedundantZodSchemas: true, tsNamespaces: true, @@ -200,7 +200,7 @@ describe("Utils: object", () => { tsNamespaces: undefined, splitByTags: undefined, defaultTag: undefined, - excludeTags: undefined, + includeTags: undefined, excludePathRegex: undefined, excludeRedundantZodSchemas: undefined, importPath: undefined, diff --git a/src/generators/utils/operation.utils.ts b/src/generators/utils/operation.utils.ts index 857cf6a8..97479d5a 100644 --- a/src/generators/utils/operation.utils.ts +++ b/src/generators/utils/operation.utils.ts @@ -9,13 +9,13 @@ import { invalidVariableNameCharactersToCamel } from "./js.utils"; import { pick } from "./object.utils"; import { isPathExcluded, pathToVariableName } from "./openapi.utils"; import { capitalize, removeWord } from "./string.utils"; -import { getOperationTag, isTagExcluded } from "./tag.utils"; +import { getOperationTag, isTagIncluded } from "./tag.utils"; export function isOperationExcluded(operation: OperationObject, options: GenerateOptions) { const isDeprecated = operation.deprecated && !options.withDeprecatedEndpoints; const tag = getOperationTag(operation, options); - const isExcluded = isTagExcluded(tag, options); - return isDeprecated || isExcluded; + const isIncluded = isTagIncluded(tag, options); + return isDeprecated || !isIncluded; } export function getOperationName({ diff --git a/src/generators/utils/tag.utils.ts b/src/generators/utils/tag.utils.ts index d46cdcbe..a9a52bb2 100644 --- a/src/generators/utils/tag.utils.ts +++ b/src/generators/utils/tag.utils.ts @@ -18,8 +18,11 @@ export function getEndpointTag(endpoint: Endpoint, options: GenerateOptions) { return formatTag(tag ?? options.defaultTag); } -export function isTagExcluded(tag: string, options: GenerateOptions) { - return options.excludeTags.some((excludeTag) => excludeTag.toLowerCase() === tag.toLowerCase()); +export function isTagIncluded(tag: string, options: GenerateOptions) { + if (options.includeTags.length === 0) { + return true; + } + return options.includeTags.some((includeTag) => includeTag.toLowerCase() === tag.toLowerCase()); } export function shouldInlineEndpointsForTag(tag: string, options: GenerateOptions) { diff --git a/test/config.ts b/test/config.ts index f4b09b72..531b4c21 100644 --- a/test/config.ts +++ b/test/config.ts @@ -3,7 +3,7 @@ import { OpenAPICodegenConfig } from "../src/generators/types/config"; export const config: OpenAPICodegenConfig = { input: "http://127.0.0.1:4000/docs-json", output: "./output", - excludeTags: ["auth"], + includeTags: ["auth"], replaceOptionalWithNullish: true, builderConfigs: true, infiniteQueries: true, From 2726fd7ec71d94611b631e8f888304d94fc43a5b Mon Sep 17 00:00:00 2001 From: Jure Rotar Date: Thu, 19 Mar 2026 15:41:15 +0100 Subject: [PATCH 2/9] feat: added both includeTags and excludeTags --- README.md | 2 ++ src/commands/check.command.ts | 3 ++ src/commands/check.ts | 9 ++++- src/commands/generate.command.ts | 3 ++ src/commands/generate.ts | 1 + src/generators/const/options.const.ts | 1 + src/generators/core/resolveConfig.ts | 6 ++-- src/generators/run/generate.runner.ts | 3 +- src/generators/types/options.ts | 1 + src/generators/utils/tag.utils.test.ts | 46 ++++++++++++++++++++++++++ src/generators/utils/tag.utils.ts | 7 ++-- 11 files changed, 76 insertions(+), 6 deletions(-) create mode 100644 src/generators/utils/tag.utils.test.ts diff --git a/README.md b/README.md index 3ee7a8c8..2d69d188 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,7 @@ yarn openapi-codegen generate --config my-config.ts --defaultTag (Requires `--splitByTags`) Default tag for shared code across multiple tags (default: 'Common') --includeTags Comma-separated list of tags to include in generation + --excludeTags Comma-separated list of tags to exclude from generation --excludePathRegex Exclude operations whose paths match the given regular expression --excludeRedundantZodSchemas Exclude any redundant Zod schemas (default: true) @@ -116,6 +117,7 @@ yarn openapi-codegen generate --config my-config.ts --defaultTag (Requires `--splitByTags`) Default tag for shared code across multiple tags (default: 'Common') --includeTags Comma-separated list of tags to include in generation + --excludeTags Comma-separated list of tags to exclude from generation --excludePathRegex Exclude operations whose paths match the given regular expression --excludeRedundantZodSchemas Exclude any redundant Zod schemas (default: true) ``` diff --git a/src/commands/check.command.ts b/src/commands/check.command.ts index bbe719d2..22a25880 100644 --- a/src/commands/check.command.ts +++ b/src/commands/check.command.ts @@ -22,6 +22,9 @@ class CheckOptions implements CheckParams { @YargOption({ envAlias: "includeTags" }) includeTags?: string; + @YargOption({ envAlias: "excludeTags" }) + excludeTags?: string; + @YargOption({ envAlias: "excludePathRegex" }) excludePathRegex?: string; diff --git a/src/commands/check.ts b/src/commands/check.ts index 09959c61..74ac6f90 100644 --- a/src/commands/check.ts +++ b/src/commands/check.ts @@ -10,10 +10,17 @@ import SwaggerParser from "@apidevtools/swagger-parser"; export type CheckParams = { config?: string; includeTags?: string; + excludeTags?: string; verbose?: boolean; } & Partial>; -export async function check({ verbose, config: configParam, includeTags: _includeTagsParam, ...params }: CheckParams) { +export async function check({ + verbose, + config: configParam, + includeTags: _includeTagsParam, + excludeTags: _excludeTagsParam, + ...params +}: CheckParams) { const start = Date.now(); if (verbose) { diff --git a/src/commands/generate.command.ts b/src/commands/generate.command.ts index 31367ffc..6ff2da7e 100644 --- a/src/commands/generate.command.ts +++ b/src/commands/generate.command.ts @@ -34,6 +34,9 @@ class GenerateOptions implements GenerateParams { @YargOption({ envAlias: "includeTags" }) includeTags?: string; + @YargOption({ envAlias: "excludeTags" }) + excludeTags?: string; + @YargOption({ envAlias: "excludePathRegex" }) excludePathRegex?: string; diff --git a/src/commands/generate.ts b/src/commands/generate.ts index d6689c4c..773feee2 100644 --- a/src/commands/generate.ts +++ b/src/commands/generate.ts @@ -8,6 +8,7 @@ import { Profiler } from "@/helpers/profile.helper"; export type GenerateParams = { config?: string; includeTags?: string; + excludeTags?: string; inlineEndpointsExcludeModules?: string; prettier?: boolean; verbose?: boolean; diff --git a/src/generators/const/options.const.ts b/src/generators/const/options.const.ts index 51b509a1..41a6101a 100644 --- a/src/generators/const/options.const.ts +++ b/src/generators/const/options.const.ts @@ -12,6 +12,7 @@ export const DEFAULT_GENERATE_OPTIONS: GenerateOptions = { splitByTags: true, defaultTag: "Common", includeTags: [], + excludeTags: [], excludePathRegex: "", excludeRedundantZodSchemas: true, tsNamespaces: true, diff --git a/src/generators/core/resolveConfig.ts b/src/generators/core/resolveConfig.ts index 13040e4d..e8035c31 100644 --- a/src/generators/core/resolveConfig.ts +++ b/src/generators/core/resolveConfig.ts @@ -4,12 +4,13 @@ import { deepMerge } from "@/generators/utils/object.utils"; export function resolveConfig({ fileConfig = {}, - params: { includeTags, inlineEndpointsExcludeModules, ...options }, + params: { includeTags, excludeTags, inlineEndpointsExcludeModules, ...options }, }: { fileConfig?: Partial | null; params: Partial< - Omit & { + Omit & { includeTags: string; + excludeTags: string; inlineEndpointsExcludeModules: string; } >; @@ -17,6 +18,7 @@ export function resolveConfig({ const resolvedConfig = deepMerge(DEFAULT_GENERATE_OPTIONS, fileConfig ?? {}, { ...options, includeTags: includeTags?.split(","), + excludeTags: excludeTags?.split(","), inlineEndpointsExcludeModules: inlineEndpointsExcludeModules?.split(","), }); resolvedConfig.checkAcl = resolvedConfig.acl && resolvedConfig.checkAcl; diff --git a/src/generators/run/generate.runner.ts b/src/generators/run/generate.runner.ts index 11293340..ed5ca39d 100644 --- a/src/generators/run/generate.runner.ts +++ b/src/generators/run/generate.runner.ts @@ -31,8 +31,9 @@ export async function runGenerate({ }: { fileConfig?: Partial | null; params?: Partial< - Omit & { + Omit & { includeTags: string; + excludeTags: string; inlineEndpointsExcludeModules: string; } >; diff --git a/src/generators/types/options.ts b/src/generators/types/options.ts index 07569ce4..2a2aa029 100644 --- a/src/generators/types/options.ts +++ b/src/generators/types/options.ts @@ -69,6 +69,7 @@ interface BaseGenerateOptions { splitByTags: boolean; defaultTag: string; includeTags: string[]; + excludeTags: string[]; excludePathRegex: string; excludeRedundantZodSchemas: boolean; tsNamespaces: boolean; diff --git a/src/generators/utils/tag.utils.test.ts b/src/generators/utils/tag.utils.test.ts new file mode 100644 index 00000000..6cb23df9 --- /dev/null +++ b/src/generators/utils/tag.utils.test.ts @@ -0,0 +1,46 @@ +import { describe, expect, test } from "vitest"; +import { DEFAULT_GENERATE_OPTIONS } from "@/generators/const/options.const"; +import { isTagIncluded } from "./tag.utils"; + +describe("Utils: tag", () => { + describe("isTagIncluded", () => { + const options = DEFAULT_GENERATE_OPTIONS; + + test("includes all when both are empty", () => { + expect(isTagIncluded("auth", { ...options, includeTags: [], excludeTags: [] })).toBe(true); + }); + + test("includes only includeTags when specified", () => { + const config = { ...options, includeTags: ["auth", "user"], excludeTags: [] }; + expect(isTagIncluded("auth", config)).toBe(true); + expect(isTagIncluded("user", config)).toBe(true); + expect(isTagIncluded("other", config)).toBe(false); + }); + + test("excludes excludeTags when includeTags is empty", () => { + const config = { ...options, includeTags: [], excludeTags: ["auth", "user"] }; + expect(isTagIncluded("auth", config)).toBe(false); + expect(isTagIncluded("user", config)).toBe(false); + expect(isTagIncluded("other", config)).toBe(true); + }); + + test("includeTags has high priority over excludeTags", () => { + const config = { ...options, includeTags: ["auth"], excludeTags: ["auth"] }; + // If it's in includeTags, it should be included regardless of excludeTags + expect(isTagIncluded("auth", config)).toBe(true); + }); + + test("includeTags overrides excludeTags (only includeTags are considered)", () => { + const config = { ...options, includeTags: ["auth"], excludeTags: ["user"] }; + expect(isTagIncluded("auth", config)).toBe(true); + expect(isTagIncluded("user", config)).toBe(false); // Not in includeTags + expect(isTagIncluded("other", config)).toBe(false); // Not in includeTags + }); + + test("case insensitive matching", () => { + expect(isTagIncluded("AUTH", { ...options, includeTags: ["auth"], excludeTags: [] })).toBe(true); + expect(isTagIncluded("auth", { ...options, includeTags: ["AUTH"], excludeTags: [] })).toBe(true); + expect(isTagIncluded("AUTH", { ...options, includeTags: [], excludeTags: ["auth"] })).toBe(false); + }); + }); +}); diff --git a/src/generators/utils/tag.utils.ts b/src/generators/utils/tag.utils.ts index a9a52bb2..05eb2056 100644 --- a/src/generators/utils/tag.utils.ts +++ b/src/generators/utils/tag.utils.ts @@ -19,10 +19,13 @@ export function getEndpointTag(endpoint: Endpoint, options: GenerateOptions) { } export function isTagIncluded(tag: string, options: GenerateOptions) { - if (options.includeTags.length === 0) { + if (options.includeTags.some((includeTag) => includeTag.toLowerCase() === tag.toLowerCase())) { return true; } - return options.includeTags.some((includeTag) => includeTag.toLowerCase() === tag.toLowerCase()); + if (options.excludeTags.some((excludeTag) => excludeTag.toLowerCase() === tag.toLowerCase())) { + return false; + } + return options.includeTags.length === 0; } export function shouldInlineEndpointsForTag(tag: string, options: GenerateOptions) { From 365073153988da149fa460081cd19174abfec7a7 Mon Sep 17 00:00:00 2001 From: Jure Rotar Date: Thu, 19 Mar 2026 15:42:23 +0100 Subject: [PATCH 3/9] chore: bumped version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2843d676..9ae019ec 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@povio/openapi-codegen-cli", - "version": "2.0.8-rc.26", + "version": "2.0.8-rc.27", "keywords": [ "codegen", "openapi", From 3c24f7eca1ba5605a8f39337992fcecfc1040a4f Mon Sep 17 00:00:00 2001 From: Jure Rotar Date: Fri, 20 Mar 2026 07:15:42 +0100 Subject: [PATCH 4/9] chore: bumped vite peer dep version --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 9ae019ec..49d8ea71 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@povio/openapi-codegen-cli", - "version": "2.0.8-rc.27", + "version": "2.0.8-rc.28", "keywords": [ "codegen", "openapi", @@ -107,7 +107,7 @@ "@tanstack/react-query": "^5.90.21", "axios": "^1.13.1", "react": "^19.1.0", - "vite": "^6.0.0 || ^7.0.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "zod": "^4.1.12" }, "peerDependenciesMeta": { From f63edf4b7357fa9aef4a15dcda298d05ddd330a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Urban=20Lavbi=C4=8D?= Date: Sat, 21 Mar 2026 19:51:44 +0100 Subject: [PATCH 5/9] release: v2.0.8-rc.29 --- package.json | 2 +- src/generators/core/SchemaResolver.class.ts | 4 ++++ .../core/getMetadataFromOpenAPIDoc.test.ts | 19 +++++++++++++++++++ 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 49d8ea71..21abc389 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@povio/openapi-codegen-cli", - "version": "2.0.8-rc.28", + "version": "2.0.8-rc.29", "keywords": [ "codegen", "openapi", diff --git a/src/generators/core/SchemaResolver.class.ts b/src/generators/core/SchemaResolver.class.ts index 0612642f..5c66829e 100644 --- a/src/generators/core/SchemaResolver.class.ts +++ b/src/generators/core/SchemaResolver.class.ts @@ -179,6 +179,10 @@ export class SchemaResolver { return this.options.defaultTag; } + if (this.options.modelsInCommon) { + return formatTag(this.options.defaultTag); + } + const extractedEnumZodSchema = this.extractedEnumZodSchemaData.find((data) => data.zodSchemaName === zodSchemaName); if (extractedEnumZodSchema) { return formatTag(extractedEnumZodSchema.tag ?? this.options.defaultTag); diff --git a/src/generators/core/getMetadataFromOpenAPIDoc.test.ts b/src/generators/core/getMetadataFromOpenAPIDoc.test.ts index bd2a4db7..673adce1 100644 --- a/src/generators/core/getMetadataFromOpenAPIDoc.test.ts +++ b/src/generators/core/getMetadataFromOpenAPIDoc.test.ts @@ -428,4 +428,23 @@ describe("getMetadataFromOpenAPIDoc", () => { expect(metadata.models).toEqual(models(extractEnums)); expect(metadata.queries).toEqual(queries); }); + + test("uses common model namespace and import path when modelsInCommon is enabled with includeTags", async () => { + const openApiDoc = (await SwaggerParser.bundle("./test/petstore.yaml")) as OpenAPIV3.Document; + + const metadata = await getMetadataFromOpenAPIDoc(openApiDoc, { + ...DEFAULT_GENERATE_OPTIONS, + includeTags: ["pet"], + modelsInCommon: true, + excludeRedundantZodSchemas: false, + }); + + expect(metadata.models.length).toBeGreaterThan(0); + expect(metadata.models.every((model) => model.namespace === "CommonModels")).toBe(true); + expect(metadata.models.every((model) => model.importPath === "common/common.models")).toBe(true); + + const petQuery = metadata.queries.find((query) => query.name === "useGetById"); + expect(petQuery?.response.namespace).toBe("CommonModels"); + expect(petQuery?.response.importPath).toBe("common/common.models"); + }); }); From 915f9fd5dc69d44cfb0ae74f7b32ffa4c17a9aef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Urban=20Lavbi=C4=8D?= Date: Sat, 21 Mar 2026 20:00:02 +0100 Subject: [PATCH 6/9] release: v2.0.8-rc.30 --- package.json | 2 +- src/generators/utils/tag.utils.test.ts | 20 ++++++++++++++++++++ src/generators/utils/tag.utils.ts | 5 +++-- 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 21abc389..d43c022d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@povio/openapi-codegen-cli", - "version": "2.0.8-rc.29", + "version": "2.0.8-rc.30", "keywords": [ "codegen", "openapi", diff --git a/src/generators/utils/tag.utils.test.ts b/src/generators/utils/tag.utils.test.ts index 6cb23df9..928253a7 100644 --- a/src/generators/utils/tag.utils.test.ts +++ b/src/generators/utils/tag.utils.test.ts @@ -42,5 +42,25 @@ describe("Utils: tag", () => { expect(isTagIncluded("auth", { ...options, includeTags: ["AUTH"], excludeTags: [] })).toBe(true); expect(isTagIncluded("AUTH", { ...options, includeTags: [], excludeTags: ["auth"] })).toBe(false); }); + + test("matches human-readable includeTags against normalized operation tags", () => { + expect( + isTagIncluded("WorkingDocumentsTemplatedDocument", { + ...options, + includeTags: ["WorkingDocuments - templated-document"], + excludeTags: [], + }), + ).toBe(true); + }); + + test("matches human-readable excludeTags against normalized operation tags", () => { + expect( + isTagIncluded("WorkingDocumentsTemplatedDocument", { + ...options, + includeTags: [], + excludeTags: ["WorkingDocuments - templated-document"], + }), + ).toBe(false); + }); }); }); diff --git a/src/generators/utils/tag.utils.ts b/src/generators/utils/tag.utils.ts index 05eb2056..1e670116 100644 --- a/src/generators/utils/tag.utils.ts +++ b/src/generators/utils/tag.utils.ts @@ -19,10 +19,11 @@ export function getEndpointTag(endpoint: Endpoint, options: GenerateOptions) { } export function isTagIncluded(tag: string, options: GenerateOptions) { - if (options.includeTags.some((includeTag) => includeTag.toLowerCase() === tag.toLowerCase())) { + const normalizedTag = formatTag(tag).toLowerCase(); + if (options.includeTags.some((includeTag) => formatTag(includeTag).toLowerCase() === normalizedTag)) { return true; } - if (options.excludeTags.some((excludeTag) => excludeTag.toLowerCase() === tag.toLowerCase())) { + if (options.excludeTags.some((excludeTag) => formatTag(excludeTag).toLowerCase() === normalizedTag)) { return false; } return options.includeTags.length === 0; From 5f43b321decd6394e77ea49aff8c1d3cf793c469 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Urban=20Lavbi=C4=8D?= Date: Sat, 21 Mar 2026 20:08:52 +0100 Subject: [PATCH 7/9] release: v2.0.8-rc.31 --- package.json | 2 +- src/commands/generate.command.ts | 3 ++ src/commands/generate.ts | 1 + src/generators/const/options.const.ts | 1 + src/generators/run/generate.runner.ts | 76 ++------------------------- src/generators/types/options.ts | 1 + 6 files changed, 12 insertions(+), 72 deletions(-) diff --git a/package.json b/package.json index d43c022d..3370cd8b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@povio/openapi-codegen-cli", - "version": "2.0.8-rc.30", + "version": "2.0.8-rc.31", "keywords": [ "codegen", "openapi", diff --git a/src/commands/generate.command.ts b/src/commands/generate.command.ts index 6ff2da7e..39b60156 100644 --- a/src/commands/generate.command.ts +++ b/src/commands/generate.command.ts @@ -16,6 +16,9 @@ class GenerateOptions implements GenerateParams { @YargOption({ envAlias: "output" }) output?: string; + @YargOption({ envAlias: "clearOutput", type: "boolean" }) + clearOutput?: boolean; + @YargOption({ envAlias: "incremental", type: "boolean" }) incremental?: boolean; diff --git a/src/commands/generate.ts b/src/commands/generate.ts index 773feee2..1f81e08c 100644 --- a/src/commands/generate.ts +++ b/src/commands/generate.ts @@ -17,6 +17,7 @@ export type GenerateParams = { GenerateOptions, | "input" | "output" + | "clearOutput" | "incremental" | "tsNamespaces" | "tsPath" diff --git a/src/generators/const/options.const.ts b/src/generators/const/options.const.ts index 41a6101a..9fca246e 100644 --- a/src/generators/const/options.const.ts +++ b/src/generators/const/options.const.ts @@ -8,6 +8,7 @@ export const DEFAULT_GENERATE_OPTIONS: GenerateOptions = { // Base options input: "http://localhost:4000/docs-json/", output: "output", + clearOutput: false, incremental: true, splitByTags: true, defaultTag: "Common", diff --git a/src/generators/run/generate.runner.ts b/src/generators/run/generate.runner.ts index ed5ca39d..6cc653aa 100644 --- a/src/generators/run/generate.runner.ts +++ b/src/generators/run/generate.runner.ts @@ -1,6 +1,4 @@ import fs from "fs"; -import path from "path"; - import SwaggerParser from "@apidevtools/swagger-parser"; import { OpenAPIV3 } from "openapi-types"; @@ -11,13 +9,6 @@ import { GenerateOptions } from "@/generators/types/options"; import { writeGenerateFileData } from "@/generators/utils/file.utils"; import { Profiler } from "@/helpers/profile.helper"; -const CACHE_FILE_NAME = ".openapi-codegen-cache.json"; - -type CacheData = { - openApiHash: string; - optionsHash: string; -}; - type GenerateStats = { generatedFilesCount: number; generatedModulesCount: number; @@ -43,27 +34,17 @@ export async function runGenerate({ const config = profiler.runSync("config.resolve", () => resolveConfig({ fileConfig, params: params ?? {} })); const openApiDoc = await getOpenApiDoc(config.input, profiler); - const openApiHash = hashString(stableStringify(openApiDoc)); - const optionsHash = hashString(stableStringify(getCacheableConfig(config))); - const cacheFilePath = path.resolve(config.output, CACHE_FILE_NAME); - - if (config.incremental) { - const cached = readCache(cacheFilePath); - if (cached && cached.openApiHash === openApiHash && cached.optionsHash === optionsHash) { - return { skipped: true, config, stats: { generatedFilesCount: 0, generatedModulesCount: 0 } }; - } - } - const filesData = profiler.runSync("generate.total", () => generateCodeFromOpenAPIDoc(openApiDoc, config, profiler)); + if (config.clearOutput) { + profiler.runSync("files.clearOutput", () => { + fs.rmSync(config.output, { force: true, recursive: true }); + }); + } await profiler.runAsync("files.write", async () => { await writeGenerateFileData(filesData, { formatGeneratedFile }); }); const stats = getGenerateStats(filesData, config); - if (config.incremental) { - await writeCache(cacheFilePath, { openApiHash, optionsHash }, formatGeneratedFile); - } - return { skipped: false, config, stats }; } @@ -116,53 +97,6 @@ function hasExternalRef(value: unknown): boolean { return false; } -function getCacheableConfig(config: GenerateOptions) { - const { output, incremental, ...cacheableConfig } = config; - void output; - void incremental; - return cacheableConfig; -} - -function readCache(filePath: string): CacheData | null { - if (!fs.existsSync(filePath)) { - return null; - } - try { - return JSON.parse(fs.readFileSync(filePath, "utf-8")) as CacheData; - } catch { - return null; - } -} - -async function writeCache(filePath: string, data: CacheData, formatGeneratedFile?: GenerateFileFormatter) { - await writeGenerateFileData([{ fileName: filePath, content: JSON.stringify(data) }], { - formatGeneratedFile, - }); -} - -function hashString(input: string) { - let hash = 2166136261; - for (let i = 0; i < input.length; i += 1) { - hash ^= input.charCodeAt(i); - hash = Math.imul(hash, 16777619); - } - return (hash >>> 0).toString(16); -} - -function stableStringify(input: unknown): string { - if (input === null || typeof input !== "object") { - return JSON.stringify(input); - } - - if (Array.isArray(input)) { - return `[${input.map((item) => stableStringify(item)).join(",")}]`; - } - - const obj = input as Record; - const keys = Object.keys(obj).sort((a, b) => a.localeCompare(b)); - return `{${keys.map((key) => `${JSON.stringify(key)}:${stableStringify(obj[key])}`).join(",")}}`; -} - function getGenerateStats(filesData: { fileName: string }[], config: GenerateOptions): GenerateStats { const generatedFilesCount = filesData.length; if (generatedFilesCount === 0) { diff --git a/src/generators/types/options.ts b/src/generators/types/options.ts index 2a2aa029..e4cf9a55 100644 --- a/src/generators/types/options.ts +++ b/src/generators/types/options.ts @@ -65,6 +65,7 @@ interface GenerateConfig { interface BaseGenerateOptions { input: string; output: string; + clearOutput?: boolean; incremental?: boolean; splitByTags: boolean; defaultTag: string; From 9fcdd897f957ed95970b9df27f72bfc5004316a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Urban=20Lavbi=C4=8D?= Date: Sat, 21 Mar 2026 20:12:19 +0100 Subject: [PATCH 8/9] release: v2.0.8-rc.32 --- package.json | 2 +- src/generators/run/generate.runner.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 3370cd8b..57cca498 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@povio/openapi-codegen-cli", - "version": "2.0.8-rc.31", + "version": "2.0.8-rc.32", "keywords": [ "codegen", "openapi", diff --git a/src/generators/run/generate.runner.ts b/src/generators/run/generate.runner.ts index 6cc653aa..d6dca3e0 100644 --- a/src/generators/run/generate.runner.ts +++ b/src/generators/run/generate.runner.ts @@ -1,4 +1,5 @@ import fs from "fs"; +import path from "path"; import SwaggerParser from "@apidevtools/swagger-parser"; import { OpenAPIV3 } from "openapi-types"; From 040123b829ce2e0db729f5e6f0a918a1ee701567 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Urban=20Lavbi=C4=8D?= Date: Sat, 21 Mar 2026 20:23:50 +0100 Subject: [PATCH 9/9] release: v2.0.8-rc.33 --- package.json | 2 +- src/generators/run/generate.runner.ts | 7 +-- src/generators/utils/file.utils.ts | 89 +++++++++++++++++++++++++++ src/vite/openapi-codegen.plugin.ts | 8 ++- 4 files changed, 98 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 57cca498..ecda0eef 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@povio/openapi-codegen-cli", - "version": "2.0.8-rc.32", + "version": "2.0.8-rc.33", "keywords": [ "codegen", "openapi", diff --git a/src/generators/run/generate.runner.ts b/src/generators/run/generate.runner.ts index d6dca3e0..6706f762 100644 --- a/src/generators/run/generate.runner.ts +++ b/src/generators/run/generate.runner.ts @@ -1,4 +1,3 @@ -import fs from "fs"; import path from "path"; import SwaggerParser from "@apidevtools/swagger-parser"; import { OpenAPIV3 } from "openapi-types"; @@ -7,7 +6,7 @@ import { resolveConfig } from "@/generators/core/resolveConfig"; import { generateCodeFromOpenAPIDoc } from "@/generators/generateCodeFromOpenAPIDoc"; import { GenerateFileFormatter } from "@/generators/types/generate"; import { GenerateOptions } from "@/generators/types/options"; -import { writeGenerateFileData } from "@/generators/utils/file.utils"; +import { removeStaleGeneratedFiles, writeGenerateFileData } from "@/generators/utils/file.utils"; import { Profiler } from "@/helpers/profile.helper"; type GenerateStats = { @@ -37,8 +36,8 @@ export async function runGenerate({ const filesData = profiler.runSync("generate.total", () => generateCodeFromOpenAPIDoc(openApiDoc, config, profiler)); if (config.clearOutput) { - profiler.runSync("files.clearOutput", () => { - fs.rmSync(config.output, { force: true, recursive: true }); + profiler.runSync("files.removeStaleGenerated", () => { + removeStaleGeneratedFiles({ output: config.output, filesData, options: config }); }); } await profiler.runAsync("files.write", async () => { diff --git a/src/generators/utils/file.utils.ts b/src/generators/utils/file.utils.ts index 0a037d7f..cbb16b51 100644 --- a/src/generators/utils/file.utils.ts +++ b/src/generators/utils/file.utils.ts @@ -3,6 +3,7 @@ import path from "path"; import { fileURLToPath } from "url"; import { GenerateFileData, GenerateFileFormatter } from "@/generators/types/generate"; +import { GenerateOptions } from "@/generators/types/options"; function readFileSync(filePath: string) { const moduleDir = path.dirname(fileURLToPath(import.meta.url)); @@ -75,3 +76,91 @@ export async function writeGenerateFileData(filesData: GenerateFileData[], optio await writeFile(file, options); } } + +export function removeStaleGeneratedFiles({ + output, + filesData, + options, +}: { + output: string; + filesData: GenerateFileData[]; + options: Pick; +}) { + if (!fs.existsSync(output)) { + return; + } + + const expectedFiles = new Set(filesData.map((file) => path.resolve(file.fileName))); + const generatedSuffixes = new Set(Object.values(options.configs).map((config) => config.outputFileNameSuffix)); + const staleFiles: string[] = []; + + const visit = (dirPath: string) => { + for (const dirent of fs.readdirSync(dirPath, { withFileTypes: true })) { + const entryPath = path.join(dirPath, dirent.name); + if (dirent.isDirectory()) { + visit(entryPath); + continue; + } + + if (isGeneratedFile(entryPath, output, generatedSuffixes) && !expectedFiles.has(path.resolve(entryPath))) { + staleFiles.push(entryPath); + } + } + }; + + visit(output); + + staleFiles.forEach((filePath) => fs.rmSync(filePath, { force: true })); + removeEmptyDirectories(output); +} + +function isGeneratedFile(filePath: string, output: string, generatedSuffixes: Set) { + const relativePath = path.relative(output, filePath); + if (relativePath === ".openapi-codegen-cache.json") { + return true; + } + + const normalizedRelativePath = relativePath.split(path.sep).join("/"); + if (["app-rest-client.ts", "queryModules.ts", "acl/app.ability.ts"].includes(normalizedRelativePath)) { + return true; + } + + const parsedPath = path.parse(filePath); + if (parsedPath.ext !== ".ts") { + return false; + } + + const segments = relativePath.split(path.sep).filter(Boolean); + if (segments.length < 2) { + return false; + } + + const moduleName = segments[0]; + const fileName = segments[segments.length - 1]; + if (!fileName.startsWith(`${moduleName}.`)) { + return false; + } + + const suffix = fileName.slice(moduleName.length + 1).replace(/\.tsx?$/, ""); + return generatedSuffixes.has(suffix); +} + +function removeEmptyDirectories(root: string) { + if (!fs.existsSync(root)) { + return; + } + + const removeIfEmpty = (dirPath: string) => { + for (const dirent of fs.readdirSync(dirPath, { withFileTypes: true })) { + if (dirent.isDirectory()) { + removeIfEmpty(path.join(dirPath, dirent.name)); + } + } + + if (dirPath !== root && fs.readdirSync(dirPath).length === 0) { + fs.rmdirSync(dirPath); + } + }; + + removeIfEmpty(root); +} diff --git a/src/vite/openapi-codegen.plugin.ts b/src/vite/openapi-codegen.plugin.ts index 108f1299..ae923564 100644 --- a/src/vite/openapi-codegen.plugin.ts +++ b/src/vite/openapi-codegen.plugin.ts @@ -28,6 +28,7 @@ export function openApiCodegen(config: OpenApiCodegenViteConfig): Plugin { const profiler = new Profiler(process.env.OPENAPI_CODEGEN_PROFILE === "1"); await runGenerate({ fileConfig, formatGeneratedFile, profiler }); }); + return queue; }; const setupWatcher = (server: ViteDevServer) => { @@ -49,10 +50,11 @@ export function openApiCodegen(config: OpenApiCodegenViteConfig): Plugin { configResolved(config) { resolvedViteConfig = config; }, - buildStart() { - enqueueGenerate(); + async buildStart() { + await enqueueGenerate(); }, - configureServer(server) { + async configureServer(server) { + await enqueueGenerate(); setupWatcher(server); }, };