Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +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')

--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)
Expand Down Expand Up @@ -115,6 +116,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')

--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)
Expand Down
2 changes: 1 addition & 1 deletion openapi-codegen.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@povio/openapi-codegen-cli",
"version": "2.0.8-rc.26",
"version": "2.0.8-rc.33",
"keywords": [
"codegen",
"openapi",
Expand Down Expand Up @@ -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": {
Expand Down
3 changes: 3 additions & 0 deletions src/commands/check.command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ class CheckOptions implements CheckParams {
@YargOption({ envAlias: "defaultTag" })
defaultTag?: string;

@YargOption({ envAlias: "includeTags" })
includeTags?: string;

@YargOption({ envAlias: "excludeTags" })
excludeTags?: string;

Expand Down
9 changes: 8 additions & 1 deletion src/commands/check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,18 @@ import SwaggerParser from "@apidevtools/swagger-parser";

export type CheckParams = {
config?: string;
includeTags?: string;
excludeTags?: string;
verbose?: boolean;
} & Partial<Pick<GenerateOptions, "input" | "splitByTags" | "defaultTag">>;

export async function check({ verbose, config: configParam, excludeTags: _excludeTagsParam, ...params }: CheckParams) {
export async function check({
verbose,
config: configParam,
includeTags: _includeTagsParam,
excludeTags: _excludeTagsParam,
...params
}: CheckParams) {
const start = Date.now();

if (verbose) {
Expand Down
6 changes: 6 additions & 0 deletions src/commands/generate.command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -31,6 +34,9 @@ class GenerateOptions implements GenerateParams {
@YargOption({ envAlias: "defaultTag" })
defaultTag?: string;

@YargOption({ envAlias: "includeTags" })
includeTags?: string;

@YargOption({ envAlias: "excludeTags" })
excludeTags?: string;

Expand Down
2 changes: 2 additions & 0 deletions src/commands/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { Profiler } from "@/helpers/profile.helper";

export type GenerateParams = {
config?: string;
includeTags?: string;
excludeTags?: string;
inlineEndpointsExcludeModules?: string;
prettier?: boolean;
Expand All @@ -16,6 +17,7 @@ export type GenerateParams = {
GenerateOptions,
| "input"
| "output"
| "clearOutput"
| "incremental"
| "tsNamespaces"
| "tsPath"
Expand Down
2 changes: 2 additions & 0 deletions src/generators/const/options.const.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ export const DEFAULT_GENERATE_OPTIONS: GenerateOptions = {
// Base options
input: "http://localhost:4000/docs-json/",
output: "output",
clearOutput: false,
incremental: true,
splitByTags: true,
defaultTag: "Common",
includeTags: [],
excludeTags: [],
excludePathRegex: "",
excludeRedundantZodSchemas: true,
Expand Down
4 changes: 4 additions & 0 deletions src/generators/core/SchemaResolver.class.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
19 changes: 19 additions & 0 deletions src/generators/core/getMetadataFromOpenAPIDoc.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
});
});
6 changes: 4 additions & 2 deletions src/generators/core/resolveConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,20 @@ import { deepMerge } from "@/generators/utils/object.utils";

export function resolveConfig({
fileConfig = {},
params: { excludeTags, inlineEndpointsExcludeModules, ...options },
params: { includeTags, excludeTags, inlineEndpointsExcludeModules, ...options },
}: {
fileConfig?: Partial<GenerateOptions> | null;
params: Partial<
Omit<GenerateOptions, "excludeTags" | "inlineEndpointsExcludeModules"> & {
Omit<GenerateOptions, "includeTags" | "excludeTags" | "inlineEndpointsExcludeModules"> & {
includeTags: string;
excludeTags: string;
inlineEndpointsExcludeModules: string;
}
>;
}) {
const resolvedConfig = deepMerge(DEFAULT_GENERATE_OPTIONS, fileConfig ?? {}, {
...options,
includeTags: includeTags?.split(","),
excludeTags: excludeTags?.split(","),
inlineEndpointsExcludeModules: inlineEndpointsExcludeModules?.split(","),
});
Expand Down
81 changes: 8 additions & 73 deletions src/generators/run/generate.runner.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,14 @@
import fs from "fs";
import path from "path";

import SwaggerParser from "@apidevtools/swagger-parser";
import { OpenAPIV3 } from "openapi-types";

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";

const CACHE_FILE_NAME = ".openapi-codegen-cache.json";

type CacheData = {
openApiHash: string;
optionsHash: string;
};

type GenerateStats = {
generatedFilesCount: number;
generatedModulesCount: number;
Expand All @@ -31,7 +22,8 @@ export async function runGenerate({
}: {
fileConfig?: Partial<GenerateOptions> | null;
params?: Partial<
Omit<GenerateOptions, "excludeTags" | "inlineEndpointsExcludeModules"> & {
Omit<GenerateOptions, "includeTags" | "excludeTags" | "inlineEndpointsExcludeModules"> & {
includeTags: string;
excludeTags: string;
inlineEndpointsExcludeModules: string;
}
Expand All @@ -42,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.removeStaleGenerated", () => {
removeStaleGeneratedFiles({ output: config.output, filesData, options: config });
});
}
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 };
}

Expand Down Expand Up @@ -115,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<string, unknown>;
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) {
Expand Down
2 changes: 2 additions & 0 deletions src/generators/types/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,11 @@ interface GenerateConfig {
interface BaseGenerateOptions {
input: string;
output: string;
clearOutput?: boolean;
incremental?: boolean;
splitByTags: boolean;
defaultTag: string;
includeTags: string[];
excludeTags: string[];
excludePathRegex: string;
excludeRedundantZodSchemas: boolean;
Expand Down
Loading