Skip to content
Merged
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
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ export default {
};
```

## JSDoc generation

JSDoc comments are omitted by default while schemas, inferred types, and runtime behavior are preserved. Set `generateJsDoc: true` in your generator configuration or pass `--generateJsDoc` to include comments. This applies to both TypeScript and native generation. Combine with `modelsOnly: true` if you only want model files.

## Lazy schemas

With `tsNamespaces` enabled, every schema of a models namespace is built the moment the module is imported, even when the importer only needs one of them. Set `lazySchemas: true` (or pass `--lazySchemas`) to build each schema on first access instead:
Expand Down Expand Up @@ -127,6 +131,7 @@ bunx openapi-codegen generate --config my-config.ts
--tsPath (Requires `--importPath` to be 'ts') Typescript import path (default: '@/data')
--removeOperationPrefixEndingWith Remove operation name prefixes that end with the specified string (default: 'Controller_')
--compileSchemas Eagerly compile non-recursive Zod schemas (default: false)
--generateJsDoc Emit JSDoc comments (default: false; enable with --generateJsDoc)
--lazySchemas Build namespace model schemas on first access instead of at import; requires `--tsNamespaces` (default: false)
--extractEnums Extract enums into separate Zod schemas (default: true)
--modelsInCommon Keep all schema declarations in defaultTag models and emit per-module proxy exports (default: false)
Expand Down
2 changes: 2 additions & 0 deletions native/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ pub struct GenerateOptions {
pub compile_schemas: bool,
#[serde(default)]
pub lazy_schemas: bool,
#[serde(default)]
pub generate_js_doc: bool,
#[serde(default = "default_true")]
pub split_by_tags: bool,
#[serde(default = "default_tag")]
Expand Down
75 changes: 45 additions & 30 deletions native/src/render.rs
Original file line number Diff line number Diff line change
Expand Up @@ -562,6 +562,7 @@ fn render_common_schema_lines(
generated_objects,
enum_objects,
property_ref_names,
options.generate_js_doc,
);
let type_name = remove_suffix(name, suffix);
let is_circular = circular_schemas.iter().any(|schema| schema == name);
Expand Down Expand Up @@ -600,6 +601,7 @@ fn render_schema_docs<'a>(
generated_objects: &'a Map<String, Value>,
enum_objects: &'a HashMap<String, Value>,
property_ref_names: &HashMap<&str, String>,
generate_js_doc: bool,
) -> (Vec<String>, Option<&'a Value>) {
let schema_object = generated_objects
.get(name)
Expand All @@ -610,6 +612,9 @@ fn render_schema_docs<'a>(
.and_then(|reference| resolve_document_ref(document, reference))
})
.or_else(|| enum_objects.get(code));
if !generate_js_doc {
return (Vec::new(), schema_object);
}
let mut lines = vec!["/** ".into(), format!(" * {name} ")];
let schema_type = if code.starts_with("z.enum(") {
"enum"
Expand Down Expand Up @@ -746,6 +751,7 @@ fn render_lazy_schema_parts(
generated_objects,
enum_objects,
property_ref_names,
options.generate_js_doc,
);
let type_name = remove_suffix(name, suffix);
let is_circular = circular_schemas.iter().any(|schema| schema == name);
Expand Down Expand Up @@ -2313,7 +2319,7 @@ pub fn render_shared(endpoints: &[Value], options: &GenerateOptions) -> Map<Stri
"queryModules".into(),
Value::String(render_query_modules(endpoints, options)),
);
if let Some(content) = render_domain_errors(endpoints) {
if let Some(content) = render_domain_errors(endpoints, options) {
rendered.insert("domainErrors".into(), Value::String(content));
}
if let Some(content) = render_app_acl(endpoints) {
Expand Down Expand Up @@ -2443,7 +2449,7 @@ fn render_app_acl(endpoints: &[Value]) -> Option<String> {
Some(lines.join("\n"))
}

fn render_domain_errors(endpoints: &[Value]) -> Option<String> {
fn render_domain_errors(endpoints: &[Value], options: &GenerateOptions) -> Option<String> {
let mut domains: HashMap<String, Vec<(Value, String, Option<String>)>> = HashMap::default();
for endpoint in endpoints {
for error in endpoint
Expand Down Expand Up @@ -2503,6 +2509,7 @@ fn render_domain_errors(endpoints: &[Value]) -> Option<String> {
.into_iter()
.map(|(code, key, description)| {
let prefix = description
.filter(|_| options.generate_js_doc)
.map(|description| format!(" /** {description} */\n "))
.unwrap_or_else(|| " ".into());
format!("{prefix}{key}: {}", value_text(&code))
Expand Down Expand Up @@ -2703,37 +2710,39 @@ fn render_ability(
} else {
format!("`{query_name}` mutation")
};
lines.push("/**".into());
lines.push(format!(
" * Use for {ability_query} ability. {}",
if has_conditions {
"For global ability, omit the object parameter."
} else {
""
}
));
if let Some(description) = endpoint
.pointer("/acl/0/description")
.and_then(Value::as_str)
{
lines.push(format!(" * @description {description}"));
}
for condition in &conditions {
let name = condition
.get("name")
.and_then(Value::as_str)
.unwrap_or_default();
let info = condition
.get("info")
.and_then(Value::as_str)
.unwrap_or_default();
if options.generate_js_doc {
lines.push("/**".into());
lines.push(format!(
" * @param {{ {} }} object.{name} {name} from {info}",
condition_type(condition, models_namespace, options)
" * Use for {ability_query} ability. {}",
if has_conditions {
"For global ability, omit the object parameter."
} else {
""
}
));
if let Some(description) = endpoint
.pointer("/acl/0/description")
.and_then(Value::as_str)
{
lines.push(format!(" * @description {description}"));
}
for condition in &conditions {
let name = condition
.get("name")
.and_then(Value::as_str)
.unwrap_or_default();
let info = condition
.get("info")
.and_then(Value::as_str)
.unwrap_or_default();
lines.push(format!(
" * @param {{ {} }} object.{name} {name} from {info}",
condition_type(condition, models_namespace, options)
));
}
lines.push(format!(" * @returns {{ AbilityTuple }} An ability tuple indicating the user's ability to use {ability_query}"));
lines.push(" */".into());
}
lines.push(format!(" * @returns {{ AbilityTuple }} An ability tuple indicating the user's ability to use {ability_query}"));
lines.push(" */".into());
lines.push(format!("export const {ability_name} = ("));
if has_conditions {
let object = conditions
Expand Down Expand Up @@ -3737,6 +3746,9 @@ fn render_query_docs(
models_namespace: &str,
infinite: bool,
) {
if !options.generate_js_doc {
return;
}
let (_, cap) = query_names(endpoint);
lines.push("/** ".into());
lines.push(if infinite {
Expand Down Expand Up @@ -4305,6 +4317,9 @@ fn render_mutation_docs(
options: &GenerateOptions,
models_namespace: &str,
) {
if !options.generate_js_doc {
return;
}
let (_, cap) = query_names(endpoint);
let mutation_cap = if endpoint.get("method").and_then(Value::as_str) == Some("get") {
format!("{cap}Mutation")
Expand Down
1 change: 1 addition & 0 deletions scripts/renderer-parity-configs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ export const optionCases = {
})),
abilityContextImportPath: [{ acl: true, abilityContextImportPath: "@test/ability-context" }],
aclCheckImportPath: [{ acl: true, aclCheckImportPath: "@test/acl" }],
generateJsDoc: bool("generateJsDoc"),
builderConfigs: bool("builderConfigs"),
filterParamName: [{ builderConfigs: true, filterParamName: "filters" }],
dataResponseParamNames: [{ builderConfigs: true, dataResponseParamNames: ["results"] }],
Expand Down
3 changes: 3 additions & 0 deletions src/commands/generate.command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ class GenerateOptions implements GenerateParams {
@YargOption({ envAlias: "lazySchemas", type: "boolean" })
lazySchemas?: boolean;

@YargOption({ envAlias: "generateJsDoc", type: "boolean" })
generateJsDoc?: boolean;

@YargOption({ envAlias: "extractEnums", type: "boolean" })
extractEnums?: boolean;

Expand Down
1 change: 1 addition & 0 deletions src/generators/const/options.const.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export const DEFAULT_GENERATE_OPTIONS: GenerateOptions = {
// Zod options
compileSchemas: false,
lazySchemas: false,
generateJsDoc: false,
schemaSuffix: SCHEMA_SUFFIX,
enumSuffix: ENUM_SUFFIX,
modelsInCommon: false,
Expand Down
10 changes: 10 additions & 0 deletions src/generators/core/resolveConfig.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,16 @@ import { describe, expect, it } from "vitest";
import { resolveConfig } from "@/generators/core/resolveConfig";

describe("resolveConfig", () => {
it("omits JSDoc by default and honors file and CLI overrides", () => {
expect(resolveConfig({ params: {} }).generateJsDoc).toBe(false);
expect(resolveConfig({ fileConfig: { generateJsDoc: true }, params: {} }).generateJsDoc).toBe(true);
expect(resolveConfig({ fileConfig: { generateJsDoc: true }, params: { generateJsDoc: false } }).generateJsDoc).toBe(
false,
);
expect(resolveConfig({ fileConfig: { generateJsDoc: false }, params: { generateJsDoc: true } }).generateJsDoc).toBe(
true,
);
});
it("disables compilation by default and honors file and CLI overrides", () => {
expect(resolveConfig({ params: {} }).compileSchemas).toBe(false);
expect(resolveConfig({ fileConfig: { compileSchemas: true }, params: {} }).compileSchemas).toBe(true);
Expand Down
30 changes: 16 additions & 14 deletions src/generators/generate/generateAcl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,22 +202,24 @@ function renderAbilityFunction({
? `\`${getQueryName(endpoint)}\` query or \`${getQueryName(endpoint, true)}\` mutation`
: `\`${getQueryName(endpoint)}\` query`
: `\`${getQueryName(endpoint)}\` mutation`;
lines.push("/**");
lines.push(
` * Use for ${abilityQuery} ability. ${hasConditions ? "For global ability, omit the object parameter." : ""}${getAbilityDescription(endpoint) ? "" : ""}`,
);
if (getAbilityDescription(endpoint)) {
lines.push(` * @description ${getAbilityDescription(endpoint)}`);
}
if (hasConditions) {
for (const propertyType of abilityConditionsTypes) {
lines.push(
` * @param { ${renderConditionType(resolver, endpoint, propertyType)} } object.${propertyType.name} ${propertyType.name} from ${propertyType.info}`,
);
if (resolver.options.generateJsDoc === true) {
lines.push("/**");
lines.push(
` * Use for ${abilityQuery} ability. ${hasConditions ? "For global ability, omit the object parameter." : ""}${getAbilityDescription(endpoint) ? "" : ""}`,
);
if (getAbilityDescription(endpoint)) {
lines.push(` * @description ${getAbilityDescription(endpoint)}`);
}
if (hasConditions) {
for (const propertyType of abilityConditionsTypes) {
lines.push(
` * @param { ${renderConditionType(resolver, endpoint, propertyType)} } object.${propertyType.name} ${propertyType.name} from ${propertyType.info}`,
);
}
}
lines.push(` * @returns { AbilityTuple } An ability tuple indicating the user's ability to use ${abilityQuery}`);
lines.push(" */");
}
lines.push(` * @returns { AbilityTuple } An ability tuple indicating the user's ability to use ${abilityQuery}`);
lines.push(" */");
lines.push(`export const ${getAbilityFunctionName(endpoint)} = (`);
if (hasConditions) {
lines.push(
Expand Down
8 changes: 7 additions & 1 deletion src/generators/generate/generateDomainErrors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,15 @@ function makeEndpointWithDomainError(
};
}

const stubResolver = {} as never;
const stubResolver = { options: { generateJsDoc: true } } as never;

describe("generateDomainErrors", () => {
it("omits descriptions when JSDoc is disabled", () => {
const data = makeData([[makeEndpointWithDomainError("rocket", 1, "Launch failed")]]);
const result = generateDomainErrors({ resolver: { options: { generateJsDoc: false } } as never, data })!;
expect(result).not.toContain("/**");
expect(result).toContain("ERROR_1: 1");
});
it("returns undefined when no domain errors exist", () => {
const data = makeData([[{ errors: [{ status: 400, zodSchema: "z.void()" }] }]]);
expect(generateDomainErrors({ resolver: stubResolver, data })).toBeUndefined();
Expand Down
2 changes: 1 addition & 1 deletion src/generators/generate/generateDomainErrors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export function generateDomainErrors({

const entries = sortedCodes
.map(({ code, name, description }) => {
const comment = description ? ` /** ${description} */\n ` : " ";
const comment = resolver.options.generateJsDoc === true && description ? ` /** ${description} */\n ` : " ";
const key = name ?? (typeof code === "string" ? code : `ERROR_${code}`);
if (!VALID_IDENTIFIER.test(key)) {
throw new Error(
Expand Down
36 changes: 36 additions & 0 deletions src/generators/generate/generateModels.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,42 @@ import { getNativeBindings } from "@/native/native-bindings";
import { generateModels } from "./generateModels";

describe("generateModels", () => {
test.each([false, true])("can omit JSDoc while preserving schemas (lazy: %s)", (lazySchemas) => {
const doc = {
openapi: "3.0.3",
info: { title: "Docs", version: "1.0.0" },
paths: {},
components: {
schemas: {
Pet: {
type: "object",
description: "A pet",
properties: { name: { type: "string", description: "Pet name" } },
},
},
},
} as OpenAPIV3.Document;
const render = (generateJsDoc: boolean) => {
const options = {
...DEFAULT_GENERATE_OPTIONS,
generateJsDoc,
lazySchemas,
splitByTags: false,
excludeRedundantZodSchemas: false,
};
const { resolver, data } = getDataFromOpenAPIDoc(doc, options);
return generateModels({ resolver, data, tag: options.defaultTag })!;
};
const documented = render(true);
const plain = render(false);
expect(documented).toContain("@description A pet");
expect(plain).not.toContain("/**");
expect(plain).toBe(documented.replace(/^[ \t]*\/\*\*[\s\S]*?\*\/\n/gm, ""));
expect(plain).toContain("PetSchema");
expect(plain).toContain("export type Pet =");
expect(plain.replace(/\s/g, "")).toBe(documented.replace(/\/\*\*[\s\S]*?\*\//g, "").replace(/\s/g, ""));
});

test("adds explicit Zod object annotations to mutually recursive schemas", () => {
const doc = {
openapi: "3.0.3",
Expand Down
9 changes: 7 additions & 2 deletions src/generators/generate/generateModels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,9 @@ export function generateModels({ resolver, data, tag }: GenerateTypeParams) {
}

for (const [name, zodSchema] of Object.entries(zodSchemasData)) {
lines.push(renderModelJsDocs({ name, zodSchema, tag, resolver }));
if (resolver.options.generateJsDoc === true) {
lines.push(renderModelJsDocs({ name, zodSchema, tag, resolver }));
}
const inferredTypeName = getZodSchemaInferedTypeName(name, resolver.options);
if (zodSchema.isCircular && zodSchema.schemaObj) {
lines.push(`export type ${inferredTypeName} = ${renderSchemaType(zodSchema.schemaObj, resolver)};`);
Expand Down Expand Up @@ -132,7 +134,9 @@ function renderLazyModels({
const schemaCode =
resolver.options.compileSchemas && !zodSchema.isCircular ? `z.compile(${zodSchema.code})` : zodSchema.code;
lines.push(`const ${name}${typeAnnotation} = once(() => ${callLazySchemaReferences(schemaCode, schemaNames)});`);
members.push(...indent(renderModelJsDocs({ name, zodSchema, tag, resolver }).split("\n")));
if (resolver.options.generateJsDoc === true) {
members.push(...indent(renderModelJsDocs({ name, zodSchema, tag, resolver }).split("\n")));
}
members.push(...renderLazyMember(name, `${name}()`));
if (zodSchema.isCircular && zodSchema.schemaObj) {
types.push(` export type ${inferredTypeName} = ${renderSchemaType(zodSchema.schemaObj, resolver)};`);
Expand Down Expand Up @@ -371,6 +375,7 @@ function renderModelJsDocs({
tag: string;
resolver: GenerateTypeParams["resolver"];
}) {
if (resolver.options.generateJsDoc !== true) return "";
const lines = [`/** `, ` * ${name} `, ` * @type { ${getZodSchemaType(zodSchema)} }`];

const description = getZodSchemaDescription(zodSchema);
Expand Down
17 changes: 17 additions & 0 deletions src/generators/generate/generateQueries.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,23 @@ const openApiDoc = {
} as OpenAPIV3.Document;

describe("generateQueries workspaceContext", () => {
it("omits JSDoc without leaving extra blank lines", () => {
const render = (generateJsDoc: boolean) =>
generateCodeFromOpenAPIDoc(openApiDoc, {
...DEFAULT_GENERATE_OPTIONS,
generateJsDoc,
output: "test-output",
}).filter((file) => file.fileName.endsWith(".queries.ts"));
const documented = render(true);
const plain = render(false);
expect(plain.length).toBeGreaterThan(0);
expect(plain).toEqual(
documented.map((file) => ({
...file,
content: file.content.replace(/^[ \t]*\/\*\*[\s\S]*?\*\/\n/gm, ""),
})),
);
});
it("passes runtime allowInvalidResponseData config through generated GET query hooks", () => {
const files = generateCodeFromOpenAPIDoc(openApiDoc, {
...DEFAULT_GENERATE_OPTIONS,
Expand Down
Loading
Loading