OpenAPI: fix typescript schema generation

This commit is contained in:
Fuma Nama
2025-11-05 20:37:01 +08:00
parent d7304219cf
commit c1026b82e7
25 changed files with 397 additions and 344 deletions
+7
View File
@@ -0,0 +1,7 @@
---
'fumadocs-openapi': patch
---
Fix TypeScript schema wrong output.
Note: code formatting has been disabled to improve performance.
@@ -52,10 +52,6 @@ When set to `custom`, you can pass a function to fully customise the generation
import { generateFiles, OperationOutput } from 'fumadocs-openapi';
void generateFiles({
// ---cut-start---
input: {} as any,
output: {} as any,
// ---cut-end---
per: 'custom',
toPages(builder) {
const { dereferenced } = builder.document;
@@ -82,6 +78,15 @@ void generateFiles({
});
```
When set to `tag`, adding `x-displayName` to the tag definition can control the title of each page.
```yaml title="openapi.yaml"
tags:
- name: test
description: this is a tag.
x-displayName: My Test Name
```
### `groupBy`
In `operation` mode, you can group output files with folders.
@@ -217,14 +222,3 @@ void generateFiles({
addGeneratedComment: false,
});
```
### Tag Display Name
Adding `x-displayName` to OpenAPI Schema can control the display name of your tags.
```yaml title="openapi.yaml"
tags:
- name: test
description: this is a tag.
x-displayName: My Test Name
```
+2 -2
View File
@@ -15,7 +15,7 @@
"@ai-sdk/openai-compatible": "^1.0.26",
"@ai-sdk/react": "^2.0.87",
"@fumadocs/mdx-remote": "workspace:*",
"@mixedbread/sdk": "^0.37.0",
"@mixedbread/sdk": "^0.38.0",
"@orama/core": "^1.2.13",
"@orama/orama": "^3.1.16",
"@radix-ui/react-dialog": "^1.1.15",
@@ -88,7 +88,7 @@
"shadcn": "3.5.0",
"tailwindcss": "^4.1.16",
"tailwindcss-animate": "^1.0.7",
"tinyexec": "^1.0.1",
"tinyexec": "^1.0.2",
"tinyglobby": "^0.2.15",
"ts-morph": "^27.0.2",
"tsconfig": "workspace:*",
+2 -2
View File
@@ -18,7 +18,7 @@
"react-router": "^7.9.5"
},
"devDependencies": {
"@remix-run/node-fetch-server": "^0.11.0",
"@remix-run/node-fetch-server": "^0.12.0",
"@tailwindcss/vite": "^4.1.16",
"@types/mdx": "^2.0.13",
"@types/node": "^24.10.0",
@@ -28,6 +28,6 @@
"react-router-devtools": "^5.1.3",
"tailwindcss": "^4.1.16",
"typescript": "^5.9.3",
"vite": "^7.1.12"
"vite": "^7.2.0"
}
}
+1 -1
View File
@@ -30,7 +30,7 @@
"react-router-devtools": "^5.1.3",
"tailwindcss": "^4.1.16",
"typescript": "^5.9.3",
"vite": "^7.1.12",
"vite": "^7.2.0",
"vite-tsconfig-paths": "^5.1.4"
}
}
+1 -1
View File
@@ -30,7 +30,7 @@
"react-router-devtools": "^5.1.3",
"tailwindcss": "^4.1.16",
"typescript": "^5.9.3",
"vite": "^7.1.12",
"vite": "^7.2.0",
"vite-tsconfig-paths": "^5.1.4"
}
}
+1 -1
View File
@@ -30,7 +30,7 @@
"serve": "^14.2.5",
"tailwindcss": "^4.1.16",
"typescript": "^5.9.3",
"vite": "^7.1.12",
"vite": "^7.2.0",
"vite-tsconfig-paths": "^5.1.4"
}
}
+1 -1
View File
@@ -29,7 +29,7 @@
"react-router-devtools": "^5.1.3",
"tailwindcss": "^4.1.16",
"typescript": "^5.9.3",
"vite": "^7.1.12",
"vite": "^7.2.0",
"vite-tsconfig-paths": "^5.1.4"
}
}
+1 -1
View File
@@ -20,7 +20,7 @@
"react": "^19.2.0",
"react-dom": "^19.2.0",
"tailwind-merge": "^3.3.1",
"vite": "^7.1.12"
"vite": "^7.2.0"
},
"devDependencies": {
"@tailwindcss/vite": "^4.1.16",
+1 -1
View File
@@ -21,7 +21,7 @@
"react": "^19.2.0",
"react-dom": "^19.2.0",
"tailwind-merge": "^3.3.1",
"vite": "^7.1.12"
"vite": "^7.2.0"
},
"devDependencies": {
"@tailwindcss/vite": "^4.1.16",
+1 -1
View File
@@ -19,7 +19,7 @@
"react": "^19.2.0",
"react-dom": "^19.2.0",
"tailwind-merge": "^3.3.1",
"vite": "^7.1.12"
"vite": "^7.2.0"
},
"devDependencies": {
"@tailwindcss/vite": "^4.1.16",
+1 -1
View File
@@ -26,7 +26,7 @@
"@types/react-dom": "^19.2.2",
"tailwindcss": "^4.1.16",
"typescript": "^5.9.3",
"vite": "^7.1.12",
"vite": "^7.2.0",
"vite-tsconfig-paths": "^5.1.4"
}
}
+1 -1
View File
@@ -25,7 +25,7 @@
"@types/react-dom": "^19.2.2",
"tailwindcss": "^4.1.16",
"typescript": "^5.9.3",
"vite": "^7.1.12",
"vite": "^7.2.0",
"vite-tsconfig-paths": "^5.1.4"
}
}
+1 -1
View File
@@ -32,7 +32,7 @@
"turbo": "2.6.0",
"typescript": "^5.9.3",
"typescript-eslint": "^8.46.3",
"vitest": "^4.0.6"
"vitest": "^4.0.7"
},
"packageManager": "[email protected]",
"engines": {
+1 -1
View File
@@ -37,7 +37,7 @@
"commander": "^14.0.2",
"package-manager-detector": "^1.5.0",
"picocolors": "^1.1.1",
"tinyexec": "^1.0.1",
"tinyexec": "^1.0.2",
"ts-morph": "^27.0.2",
"zod": "^4.1.12"
},
+1 -1
View File
@@ -129,7 +129,7 @@
},
"devDependencies": {
"@mdx-js/mdx": "^3.1.1",
"@mixedbread/sdk": "^0.37.0",
"@mixedbread/sdk": "^0.38.0",
"@orama/core": "^1.2.13",
"@tanstack/react-router": "^1.134.12",
"@types/estree-jsx": "^1.0.5",
+1 -1
View File
@@ -43,7 +43,7 @@
"@commander-js/extra-typings": "^14.0.0",
"commander": "^14.0.2",
"picocolors": "^1.1.1",
"tinyexec": "^1.0.1",
"tinyexec": "^1.0.2",
"ts-morph": "^27.0.2"
},
"devDependencies": {
+2 -2
View File
@@ -94,7 +94,7 @@
"picocolors": "^1.1.1",
"picomatch": "^4.0.3",
"remark-mdx": "^3.1.1",
"tinyexec": "^1.0.1",
"tinyexec": "^1.0.2",
"tinyglobby": "^0.2.15",
"unified": "^11.0.5",
"unist-util-remove-position": "^5.0.0",
@@ -123,7 +123,7 @@
"rollup": "^4.52.5",
"tsconfig": "workspace:*",
"vfile": "^6.0.3",
"vite": "^7.1.12",
"vite": "^7.2.0",
"webpack": "^5.102.1"
},
"peerDependencies": {
+5 -5
View File
@@ -56,13 +56,13 @@
"types:check": "tsc --noEmit"
},
"dependencies": {
"@fumari/json-schema-to-typescript": "^1.1.3",
"@fumari/json-schema-to-typescript": "^2.0.0",
"@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-slot": "^1.2.3",
"@scalar/json-magic": "^0.6.1",
"@scalar/openapi-parser": "0.22.3",
"@radix-ui/react-slot": "^1.2.4",
"@scalar/json-magic": "^0.7.0",
"@scalar/openapi-parser": "0.23.0",
"ajv": "^8.17.1",
"class-variance-authority": "^0.7.1",
"fumadocs-core": "workspace:*",
@@ -80,7 +80,7 @@
"xml-js": "^1.6.11"
},
"devDependencies": {
"@scalar/api-client-react": "^1.3.46",
"@scalar/api-client-react": "^1.3.48",
"@types/js-yaml": "^4.0.9",
"@types/node": "24.10.0",
"@types/openapi-sampler": "^1.0.3",
+1 -1
View File
@@ -336,7 +336,7 @@ async function ResponseAccordion({
if (generateTypeScriptSchema) {
ts = await generateTypeScriptSchema(operation, status);
} else if (generateTypeScriptSchema === undefined && schema) {
ts = await getTypescriptSchema(ctx.schema);
ts = await getTypescriptSchema(schema, ctx);
}
return (
@@ -1,17 +1,27 @@
import type { ProcessedDocument } from '@/utils/process-document';
import type { SchemaObject } from 'ajv';
import type { RenderContext } from '@/types';
export async function getTypescriptSchema(
processed: ProcessedDocument,
schema: SchemaObject,
ctx: RenderContext,
): Promise<string | undefined> {
try {
const { compile } = await import('@fumari/json-schema-to-typescript');
const { compile } = await import('@fumari/json-schema-to-typescript');
const cloned = structuredClone(processed.bundled);
return await compile(cloned, 'Response', {
$refOptions: false,
bannerComment: '',
additionalProperties: false,
try {
const input = structuredClone({
schema,
idToSchema: ctx.schema._internal_idToSchema(),
});
const schemaToId = new WeakMap<object, string>();
for (const [k, v] of input.idToSchema) {
schemaToId.set(v, k);
}
return await compile(input.schema, 'Response', {
enableConstEnums: false,
schemaToId,
});
} catch (e) {
console.warn('Failed to generate typescript schema:', e);
+14 -8
View File
@@ -11,6 +11,8 @@ export type ProcessedDocument = {
*/
dereferenced: NoReference<Document>;
_internal_idToSchema: () => Map<string, object>;
/**
* Get raw object from dereference object
*/
@@ -61,19 +63,23 @@ export async function processDocument(
* Dereferenced value and its original `$ref` value
*/
const dereferenceMap = new WeakMap<object, string>();
const serializable = new Map<string, object>();
return {
dereferenced: (
await dereference(document, {
throwOnError: true,
onDereference({ schema, ref }) {
dereferenceMap.set(schema, ref);
},
})
).schema as NoReference<Document>,
dereferenced: dereference(document, {
throwOnError: true,
onDereference({ schema, ref }) {
serializable.set(ref, schema);
dereferenceMap.set(schema, ref);
},
}).schema as NoReference<Document>,
getRawRef(obj) {
return dereferenceMap.get(obj);
},
_internal_idToSchema() {
return serializable;
},
bundled: document as Document,
};
}
+1 -1
View File
@@ -29,7 +29,7 @@
"picocolors": "^1.1.1",
"react-router": "^7.9.5",
"tinyglobby": "^0.2.15",
"vite": "^7.1.12",
"vite": "^7.2.0",
"zod": "^4.1.12"
},
"devDependencies": {
+1 -1
View File
@@ -122,7 +122,7 @@
"@radix-ui/react-popover": "^1.1.15",
"@radix-ui/react-presence": "^1.1.5",
"@radix-ui/react-scroll-area": "^1.2.10",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-tabs": "^1.1.13",
"class-variance-authority": "^0.7.1",
"fumadocs-core": "workspace:*",
+321 -285
View File
File diff suppressed because it is too large Load Diff