This commit is contained in:
Fuma Nama
2025-09-10 19:48:35 +08:00
parent 1fb028cd3d
commit f30da8fac5
17 changed files with 111 additions and 111 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'fumadocs-openapi': patch
---
Migrate to `@scalar/[email protected]`
+1 -1
View File
@@ -8,7 +8,7 @@ async function generate() {
// clean generated files
await rimraf(out, {
filter(v) {
return !v.endsWith('index.mdx') && !v.endsWith('meta.json');
return !v.endsWith('meta.json');
},
});
+4 -8
View File
@@ -1,18 +1,14 @@
import { remark } from 'remark';
import remarkGfm from 'remark-gfm';
import remarkRehype from 'remark-rehype';
import type { VFile } from 'vfile';
import type { Compatible } from 'vfile';
import { type Components, toJsxRuntime } from 'hast-util-to-jsx-runtime';
import type { Root } from 'hast';
import * as JsxRuntime from 'react/jsx-runtime';
import type { PluggableList, Processor } from 'unified';
import type { ReactNode } from 'react';
function rehypeReact(
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- processor errors
this: Processor<any, any, any, any>,
options: MarkdownProps = {},
) {
function rehypeReact(this: Processor, options: MarkdownProps = {}) {
this.compiler = (tree, file) => {
return toJsxRuntime(tree as Root, {
development: false,
@@ -23,7 +19,7 @@ function rehypeReact(
};
}
interface MarkdownProps {
export interface MarkdownProps {
components?: Components;
}
@@ -35,7 +31,7 @@ export async function Markdown({
}: MarkdownProps & {
remarkPlugins?: PluggableList;
rehypePlugins?: PluggableList;
children: string | VFile;
children: Compatible;
}) {
const processor = remark()
.use(remarkGfm)
+2 -2
View File
@@ -57,7 +57,8 @@
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-slot": "^1.2.3",
"@scalar/openapi-parser": "0.18.3",
"@scalar/json-magic": "^0.4.0",
"@scalar/openapi-parser": "0.20.3",
"ajv": "^8.17.1",
"class-variance-authority": "^0.7.1",
"fumadocs-core": "workspace:*",
@@ -71,7 +72,6 @@
"react-hook-form": "^7.62.0",
"remark": "^15.0.1",
"remark-rehype": "^11.1.2",
"shiki": "^3.12.2",
"tinyglobby": "^0.2.15",
"xml-js": "^1.6.11"
},
+11 -11
View File
@@ -54,7 +54,7 @@ interface OperationConfig extends BaseConfig {
name?:
| ((
output: GeneratePageOutput,
document: ProcessedDocument['document'],
document: ProcessedDocument['dereferenced'],
) => string)
| BaseName;
}
@@ -71,7 +71,7 @@ interface TagConfig extends BaseConfig {
name?:
| ((
output: GenerateTagOutput,
document: ProcessedDocument['document'],
document: ProcessedDocument['dereferenced'],
) => string)
| BaseName;
}
@@ -88,7 +88,7 @@ interface FileConfig extends BaseConfig {
name?:
| ((
output: GenerateFileOutput,
document: ProcessedDocument['document'],
document: ProcessedDocument['dereferenced'],
) => string)
| BaseName;
}
@@ -254,13 +254,13 @@ function generateFromDocument(
options: Config,
): OutputFile[] {
const files: OutputFile[] = [];
const { document } = processed;
const { dereferenced } = processed;
const { output, cwd = process.cwd(), slugify = defaultSlugify } = options;
const outputDir = path.join(cwd, output);
let nameFn: (
output: GeneratePageOutput | GenerateTagOutput | GenerateFileOutput,
document: ProcessedDocument['document'],
document: ProcessedDocument['dereferenced'],
) => string;
if (!options.name || typeof options.name !== 'function') {
@@ -287,12 +287,12 @@ function generateFromDocument(
];
}
const file = nameFn(result, document);
const file = nameFn(result, dereferenced);
if (groupBy === 'tag') {
let tags =
result.type === 'operation'
? document.paths![result.item.path]![result.item.method]!.tags
: document.webhooks![result.item.name][result.item.method]!.tags;
? dereferenced.paths![result.item.path]![result.item.method]!.tags
: dereferenced.webhooks![result.item.name][result.item.method]!.tags;
if (!tags || tags.length === 0) {
console.warn(
@@ -315,7 +315,7 @@ function generateFromDocument(
pathOrUrl: schemaId,
content: result,
},
document,
dereferenced,
);
files.push({
@@ -329,7 +329,7 @@ function generateFromDocument(
const results = generateTags(schemaId, processed, options);
for (const result of results) {
const filename = nameFn(result, document);
const filename = nameFn(result, dereferenced);
files.push({
path: path.join(outputDir, `${filename}.mdx`),
@@ -376,7 +376,7 @@ function generateFromDocument(
function defaultNameFn(
schemaId: string,
output: GeneratePageOutput | GenerateTagOutput | GenerateFileOutput,
document: ProcessedDocument['document'],
document: ProcessedDocument['dereferenced'],
options: Config,
algorithm: 'v2' | 'v1' = 'v2',
) {
+13 -13
View File
@@ -86,8 +86,8 @@ export function generateAll(
processed: ProcessedDocument,
options: GenerateOptions = {},
): string {
const { document } = processed;
const items = getAPIPageItems(document);
const { dereferenced } = processed;
const items = getAPIPageItems(dereferenced);
return generatePage(
schemaId,
@@ -99,8 +99,8 @@ export function generateAll(
},
{
...options,
title: document.info.title,
description: document.info.description,
title: dereferenced.info.title,
description: dereferenced.info.description,
},
{
type: 'file',
@@ -113,12 +113,12 @@ export function generatePages(
processed: ProcessedDocument,
options: GenerateOptions = {},
): GeneratePageOutput[] {
const { document } = processed;
const items = getAPIPageItems(document);
const { dereferenced } = processed;
const items = getAPIPageItems(dereferenced);
const result: GeneratePageOutput[] = [];
for (const item of items.operations) {
const pathItem = document.paths?.[item.path];
const pathItem = dereferenced.paths?.[item.path];
if (!pathItem) continue;
const operation = pathItem[item.method];
if (!operation) continue;
@@ -149,7 +149,7 @@ export function generatePages(
}
for (const item of items.webhooks) {
const pathItem = document.webhooks?.[item.name];
const pathItem = dereferenced.webhooks?.[item.name];
if (!pathItem) continue;
const operation = pathItem[item.method];
if (!operation) continue;
@@ -184,11 +184,11 @@ export function generateTags(
processed: ProcessedDocument,
options: GenerateOptions = {},
): GenerateTagOutput[] {
const { document } = processed;
if (!document.tags) return [];
const items = getAPIPageItems(document);
const { dereferenced } = processed;
if (!dereferenced.tags) return [];
const items = getAPIPageItems(dereferenced);
return document.tags.map((tag) => {
return dereferenced.tags.map((tag) => {
const webhooks = items.webhooks.filter(
(v) => v.tags && v.tags.includes(tag.name),
);
@@ -306,7 +306,7 @@ function generatePage(
};
}
const data = generateStaticData(processed.document, page);
const data = generateStaticData(processed.dereferenced, page);
const content: string[] = [];
if (options.description && includeDescription)
+2 -4
View File
@@ -750,8 +750,6 @@ function Route({
route,
...props
}: HTMLAttributes<HTMLDivElement> & { route: string }) {
const segments = route.split('/').filter((part) => part.length > 0);
return (
<div
{...props}
@@ -760,9 +758,9 @@ function Route({
props.className,
)}
>
{segments.map((part, index) => (
{route.split('/').map((part, index) => (
<Fragment key={index}>
<span className="text-fd-muted-foreground">/</span>
{index > 0 && <span className="text-fd-muted-foreground">/</span>}
{part.startsWith('{') && part.endsWith('}') ? (
<code className="bg-fd-primary/10 text-fd-primary">{part}</code>
) : (
+3 -3
View File
@@ -131,17 +131,17 @@ function writeReferences(
function parseSecurities(
method: MethodInformation,
{ schema: { document } }: RenderContext,
{ schema: { dereferenced } }: RenderContext,
): ClientProps['securities'] {
const result: ClientProps['securities'] = [];
const security = method.security ?? document.security ?? [];
const security = method.security ?? dereferenced.security ?? [];
if (security.length === 0) return result;
for (const map of security) {
const list: ClientProps['securities'][number] = [];
for (const [key, scopes] of Object.entries(map)) {
const scheme = document.components?.securitySchemes?.[key];
const scheme = dereferenced.components?.securitySchemes?.[key];
if (!scheme) continue;
list.push({
+4 -4
View File
@@ -40,12 +40,12 @@ export async function APIPage(props: ApiPageProps) {
? await processDocumentCached(props.document)
: await props.document;
const ctx = await getContext(processed, props);
const { document } = processed;
const { dereferenced } = processed;
return (
<ctx.renderer.Root ctx={ctx}>
{operations?.map((item) => {
const pathItem = document.paths?.[item.path];
const pathItem = dereferenced.paths?.[item.path];
if (!pathItem)
throw new Error(
`[Fumadocs OpenAPI] Path not found in OpenAPI schema: ${item.path}`,
@@ -70,7 +70,7 @@ export async function APIPage(props: ApiPageProps) {
);
})}
{webhooks?.map((item) => {
const webhook = document.webhooks?.[item.name];
const webhook = dereferenced.webhooks?.[item.name];
if (!webhook)
throw new Error(
`[Fumadocs OpenAPI] Webhook not found in OpenAPI schema: ${item.name}`,
@@ -103,7 +103,7 @@ export async function getContext(
schema: ProcessedDocument,
options: SharedOpenAPIOptions = {},
): Promise<RenderContext> {
const document = schema.document;
const document = schema.dereferenced;
const servers =
document.servers && document.servers.length > 0
? document.servers
@@ -70,6 +70,9 @@ export function Operation({
hasHead?: boolean;
headingLevel?: number;
}): ReactElement {
const {
schema: { dereferenced },
} = ctx;
const body = method.requestBody;
let headNode: ReactNode = null;
let bodyNode: ReactNode = null;
@@ -177,14 +180,12 @@ export function Operation({
);
});
const securities = (
method.security ??
ctx.schema.document.security ??
[]
).filter((v) => Object.keys(v).length > 0);
const securities = (method.security ?? dereferenced.security ?? []).filter(
(v) => Object.keys(v).length > 0,
);
if (type === 'operation' && securities.length > 0) {
const securitySchemes = ctx.schema.document.components?.securitySchemes;
const securitySchemes = dereferenced.components?.securitySchemes;
const names = securities.map((security) =>
Object.keys(security).join(' & '),
);
@@ -289,10 +290,7 @@ async function ResponseAccordion({
ctx: RenderContext;
}) {
const response = operation.responses![status];
const {
generateTypeScriptSchema,
schema: { dereferenceMap },
} = ctx;
const { generateTypeScriptSchema } = ctx;
const contentTypes = response.content
? Object.entries(response.content)
: null;
@@ -319,7 +317,7 @@ async function ResponseAccordion({
if (generateTypeScriptSchema) {
ts = await generateTypeScriptSchema(operation, status);
} else if (generateTypeScriptSchema === undefined && schema) {
ts = await getTypescriptSchema(schema, dereferenceMap);
ts = await getTypescriptSchema(ctx.schema);
}
return (
+2 -1
View File
@@ -368,8 +368,9 @@ function schemaStack(
function getId(schema: ResolvedSchema) {
if (typeof schema !== 'object') return;
if (schema.title) return schema.title;
return schema.title ?? renderContext.schema.dereferenceMap.get(schema);
return renderContext.schema.getRawRef(schema);
}
return {
+1 -1
View File
@@ -13,6 +13,6 @@ export function APIPlayground({
ctx: RenderContext;
}) {
return (
<Client method={method.method} path={path} spec={ctx.schema.downloaded} />
<Client method={method.method} path={path} spec={ctx.schema.bundled} />
);
}
-5
View File
@@ -22,11 +22,6 @@ export type MethodInformation = NoReference<OperationObject> & {
method: string;
};
/**
* Dereferenced value and its original `$ref` value
*/
export type DereferenceMap = Map<unknown, string>;
export interface RenderContext extends SharedOpenAPIOptions {
renderer: Renderer;
servers: ServerObject[];
@@ -1,15 +1,13 @@
import { compile } from '@fumari/json-schema-to-typescript';
import type { DereferenceMap } from '@/types';
import type { ProcessedDocument } from '@/utils/process-document';
export async function getTypescriptSchema(
schema: object,
dereferenceMap: DereferenceMap,
processed: ProcessedDocument,
): Promise<string | undefined> {
try {
const cloned = structuredClone({ schema, dereferenceMap });
return await compile(cloned.schema, 'Response', {
const cloned = structuredClone(processed.bundled);
return await compile(cloned, 'Response', {
$refOptions: false,
schemaToId: cloned.dereferenceMap,
bannerComment: '',
additionalProperties: false,
enableConstEnums: false,
+40 -20
View File
@@ -1,13 +1,22 @@
import type { DereferenceMap, Document } from '@/types';
import type { Document } from '@/types';
import type { NoReference } from '@/utils/schema';
import type { OpenAPIV3, OpenAPIV3_1 } from 'openapi-types';
import { bundle, dereference, upgrade } from '@scalar/openapi-parser';
import { fetchUrls, readFiles } from '@scalar/openapi-parser/plugins';
import { dereference, upgrade } from '@scalar/openapi-parser';
import { bundle } from '@scalar/json-magic/bundle';
import { fetchUrls, readFiles } from '@scalar/json-magic/bundle/plugins/node';
export type ProcessedDocument = {
document: NoReference<Document>;
dereferenceMap: DereferenceMap;
downloaded: Document;
/**
* dereferenced document
*/
dereferenced: NoReference<Document>;
/**
* Get raw object from dereference object
*/
getRawRef: (obj: object) => string | undefined;
bundled: Document;
};
const cache = new Map<string, ProcessedDocument>();
@@ -31,23 +40,34 @@ export async function processDocumentCached(
export async function processDocument(
input: string | OpenAPIV3_1.Document | OpenAPIV3.Document,
): Promise<ProcessedDocument> {
const dereferenceMap: DereferenceMap = new Map();
let document = await bundle(input as string, {
const document = await bundle(input as string, {
plugins: [fetchUrls(), readFiles()],
treeShake: false,
});
// upgrade
document = upgrade(document).specification;
const { schema: dereferenced } = await dereference(document, {
onDereference({ ref, schema }) {
dereferenceMap.set(schema, ref);
treeShake: true,
urlMap: true,
hooks: {
onResolveError(node) {
throw new Error(`Failed to resolve ${node.$ref}`);
},
},
});
}).then((v) => upgrade(v).specification);
/**
* Dereferenced value and its original `$ref` value
*/
const dereferenceMap = new WeakMap<object, string>();
return {
document: dereferenced as NoReference<Document>,
dereferenceMap,
downloaded: document as Document,
dereferenced: (
await dereference(document, {
throwOnError: true,
onDereference({ schema, ref }) {
dereferenceMap.set(schema, ref);
},
})
).schema as NoReference<Document>,
getRawRef(obj) {
return dereferenceMap.get(obj);
},
bundled: document as Document,
};
}
@@ -27,8 +27,8 @@ export function schemaToString(
else if (schema === false) return 'never';
if (schema.title) return schema.title;
const referenceName = ctx?.dereferenceMap.get(schema);
if (referenceName) return referenceName.split('/').at(-1)!;
const rawRef = ctx?.getRawRef(schema);
if (rawRef) return rawRef.split('/').at(-1)!;
if (Array.isArray(schema.type)) {
const members = new Set();
+8 -19
View File
@@ -300,7 +300,7 @@ importers:
version: 0.2.2(@content-collections/[email protected]([email protected]))([email protected]([email protected]))([email protected])
'@content-collections/next':
specifier: ^0.2.7
version: 0.2.7(@content-collections/[email protected]([email protected]))([email protected](@babel/[email protected])(@opentelemetry/[email protected])([email protected])([email protected]([email protected]))([email protected]))
version: 0.2.7(@content-collections/[email protected]([email protected]))([email protected](@opentelemetry/[email protected])([email protected]([email protected]))([email protected]))
'@types/mdx':
specifier: ^2.0.13
version: 2.0.13
@@ -1371,7 +1371,7 @@ importers:
version: 0.2.2(@content-collections/[email protected]([email protected]))([email protected]([email protected]))([email protected])
'@content-collections/next':
specifier: ^0.2.7
version: 0.2.7(@content-collections/[email protected]([email protected]))([email protected](@babel/[email protected])(@opentelemetry/[email protected])([email protected])([email protected]([email protected]))([email protected]))
version: 0.2.7(@content-collections/[email protected]([email protected]))([email protected](@opentelemetry/[email protected])([email protected]([email protected]))([email protected]))
'@react-router/dev':
specifier: ^7.8.2
version: 7.8.2(@react-router/[email protected]([email protected]([email protected]([email protected]))([email protected]))([email protected]))(@types/[email protected])([email protected])([email protected])([email protected])([email protected]([email protected]))([email protected]([email protected]([email protected]))([email protected]))([email protected])([email protected])([email protected])([email protected])([email protected](@types/[email protected])([email protected])([email protected])([email protected])([email protected])([email protected]))([email protected])
@@ -1747,9 +1747,12 @@ importers:
'@radix-ui/react-slot':
specifier: ^1.2.3
version: 1.2.3(@types/[email protected])([email protected])
'@scalar/json-magic':
specifier: ^0.4.0
version: 0.4.0([email protected])
'@scalar/openapi-parser':
specifier: 0.18.3
version: 0.18.3
specifier: 0.20.3
version: 0.20.3([email protected])
ajv:
specifier: ^8.17.1
version: 8.17.1
@@ -4409,10 +4412,6 @@ packages:
resolution: {integrity: sha512-upE5aMXc6QGgSqVDqVGeJiSMfU2/LYcNevMuLygj3PhnOufCmpMxbjCEHvXJUeqm+V8oUcbA+PCXCOgXQo+GNA==}
engines: {node: '>=20'}
'@scalar/[email protected]':
resolution: {integrity: sha512-j0OM3Y3JTVx50m7r5ogoADe2pT/ewhAnO7knjkcm7b+vjgKRpgjUMhXsYraBemFkqd2dNa1QfTX/mDVv7wEr9A==}
engines: {node: '>=20'}
'@scalar/[email protected]':
resolution: {integrity: sha512-vjnJ9EyUza4GnYR+YK9/GLrYVWbuyH3U4NZWa51kdOg/34gh5H9Jc2Cphmrz/Unxe8EmzwqaElbeM+7qKADgXQ==}
engines: {node: '>=20'}
@@ -11516,7 +11515,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@content-collections/[email protected](@content-collections/[email protected]([email protected]))([email protected](@babel/[email protected])(@opentelemetry/[email protected])([email protected])([email protected]([email protected]))([email protected]))':
'@content-collections/[email protected](@content-collections/[email protected]([email protected]))([email protected](@opentelemetry/[email protected])([email protected]([email protected]))([email protected]))':
dependencies:
'@content-collections/core': 0.11.1([email protected])
'@content-collections/integrations': 0.3.0(@content-collections/[email protected]([email protected]))
@@ -13512,16 +13511,6 @@ snapshots:
ts-deepmerge: 7.0.3
type-fest: 4.41.0
'@scalar/[email protected]':
dependencies:
'@scalar/openapi-types': 0.3.7
ajv: 8.17.1
ajv-draft-04: 1.0.0([email protected])
ajv-formats: 3.0.1([email protected])
jsonpointer: 5.0.1
leven: 4.0.0
yaml: 2.8.0
'@scalar/[email protected]([email protected])':
dependencies:
'@scalar/json-magic': 0.4.0([email protected])