mirror of
https://github.com/hanzo-docs/docs.git
synced 2026-10-25 04:20:02 +00:00
OpenAPI: introduce input API on createOpenAPI()
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
---
|
||||
'fumadocs-openapi': minor
|
||||
---
|
||||
|
||||
**Introduce `input` API on `createOpenAPI()`, unify `generateFiles()`**
|
||||
|
||||
Migration: Move the server object from `lib/source` to `lib/openapi`
|
||||
|
||||
```ts
|
||||
import { createOpenAPI } from 'fumadocs-openapi/server';
|
||||
|
||||
export const openapi = createOpenAPI({
|
||||
input: ['./my-schema.json'],
|
||||
});
|
||||
```
|
||||
|
||||
Use the server object for `generateFiles()`:
|
||||
|
||||
```ts
|
||||
import { generateFiles } from 'fumadocs-openapi';
|
||||
import { openapi } from '@/lib/openapi';
|
||||
|
||||
void generateFiles({
|
||||
input: openapi,
|
||||
output: './content/docs',
|
||||
// we recommend to enable it
|
||||
// make sure your endpoint description doesn't break MDX syntax.
|
||||
includeDescription: true,
|
||||
});
|
||||
```
|
||||
@@ -1,3 +1,3 @@
|
||||
import { openapi } from '@/lib/source';
|
||||
import { openapi } from '@/lib/openapi';
|
||||
|
||||
export const { GET, HEAD, PUT, POST, PATCH, DELETE } = openapi.createProxy();
|
||||
|
||||
@@ -25,7 +25,7 @@ import { Callout } from 'fumadocs-ui/components/callout';
|
||||
import { TypeTable } from 'fumadocs-ui/components/type-table';
|
||||
import * as Preview from '@/components/preview';
|
||||
import { createMetadata } from '@/lib/metadata';
|
||||
import { openapi, source } from '@/lib/source';
|
||||
import { source } from '@/lib/source';
|
||||
import { Wrapper } from '@/components/preview/wrapper';
|
||||
import { Mermaid } from '@/components/mdx/mermaid';
|
||||
import { Rate } from '@/components/rate';
|
||||
@@ -46,6 +46,7 @@ import { APIPage } from 'fumadocs-openapi/ui';
|
||||
import { LLMCopyButton, ViewOptions } from '@/components/ai/page-actions';
|
||||
import * as path from 'node:path';
|
||||
import { Banner } from 'fumadocs-ui/components/banner';
|
||||
import { openapi } from '@/lib/openapi';
|
||||
|
||||
function PreviewRenderer({ preview }: { preview: string }): ReactNode {
|
||||
if (preview && preview in Preview) {
|
||||
|
||||
@@ -9,12 +9,8 @@ Pass options to the `generateFiles` function.
|
||||
|
||||
### Input
|
||||
|
||||
An array of input files.
|
||||
Allowed:
|
||||
|
||||
- File Paths
|
||||
- External URLs
|
||||
- Wildcard
|
||||
- OpenAPI Server object.
|
||||
- anything supported by [`input`](#input-1).
|
||||
|
||||
```ts
|
||||
import { generateFiles } from 'fumadocs-openapi';
|
||||
@@ -40,7 +36,10 @@ void generateFiles({
|
||||
|
||||
Customise how the page is generated, default to `operation`.
|
||||
|
||||
> Operation in OpenAPI schema refers to an API endpoint with specific method like `/api/weather:GET`.
|
||||
<Callout>
|
||||
Operation refers to an API endpoint with specific method like
|
||||
`/api/weather:GET`.
|
||||
</Callout>
|
||||
|
||||
| mode | | output |
|
||||
| --------- | --------------------------- | ------------------------------------- |
|
||||
@@ -162,6 +161,35 @@ tags:
|
||||
|
||||
The server to render pages.
|
||||
|
||||
### Input
|
||||
|
||||
- File Paths (including wildcards)
|
||||
- External URLs
|
||||
- A function (see below)
|
||||
|
||||
```ts tab="Basic"
|
||||
import { createOpenAPI } from 'fumadocs-openapi/server';
|
||||
|
||||
export const openapi = createOpenAPI({
|
||||
input: ['./unkey.json'],
|
||||
});
|
||||
```
|
||||
|
||||
```ts tab="Function"
|
||||
import { createOpenAPI } from 'fumadocs-openapi/server';
|
||||
|
||||
export const openapi = createOpenAPI({
|
||||
async input() {
|
||||
return {
|
||||
// [id]: downloaded OpenAPI Schema
|
||||
my_schema: await fetch(
|
||||
'https://registry.scalar.com/@scalar/apis/galaxy/latest?format=json',
|
||||
).then((res) => res.json()),
|
||||
};
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Generate Code Samples
|
||||
|
||||
Generate custom code samples for each API endpoint. Make sure to install the types package to give you type-safety when customising it:
|
||||
@@ -235,32 +263,3 @@ export const openapi = createOpenAPI({
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Advanced
|
||||
|
||||
### Using API Page
|
||||
|
||||
> This is not a public API, use it carefully.
|
||||
|
||||
To use the `APIPage` component in your MDX files:
|
||||
|
||||
```mdx
|
||||
---
|
||||
title: Delete Api
|
||||
full: true
|
||||
---
|
||||
|
||||
<APIPage
|
||||
document="./unkey.json"
|
||||
operations={[{ path: '/v1/apis.deleteApi', method: 'post' }]}
|
||||
hasHead={false}
|
||||
/>
|
||||
```
|
||||
|
||||
Unlike using the `generateFiles()` function, this supports revalidation of the OpenAPI schema if given an URL.
|
||||
|
||||
| Prop | Description |
|
||||
| ------------ | ----------------------------------------- |
|
||||
| `document` | OpenAPI Schema |
|
||||
| `operations` | Operations (API endpoints) to be rendered |
|
||||
| `hasHead` | Enable to render the heading of operation |
|
||||
|
||||
@@ -25,10 +25,19 @@ Please note that you must have Tailwind CSS v4 configured.
|
||||
|
||||
### Configure Pages
|
||||
|
||||
Create an OpenAPI instance on the server. Fumadocs OpenAPI renders the pages on server-side.
|
||||
Create an OpenAPI instance on the server.
|
||||
|
||||
```ts title="lib/openapi.ts"
|
||||
import { createOpenAPI } from 'fumadocs-openapi/server';
|
||||
|
||||
export const openapi = createOpenAPI({
|
||||
// the OpenAPI schema, you can also give it an external URL.
|
||||
input: ['./unkey.json'],
|
||||
});
|
||||
```
|
||||
|
||||
```ts title="lib/source.ts"
|
||||
import { createOpenAPI, attachFile } from 'fumadocs-openapi/server';
|
||||
import { attachFile } from 'fumadocs-openapi/server';
|
||||
import { loader } from 'fumadocs-core/source';
|
||||
|
||||
export const source = loader({
|
||||
@@ -37,9 +46,6 @@ export const source = loader({
|
||||
attachFile,
|
||||
},
|
||||
});
|
||||
|
||||
// [!code ++]
|
||||
export const openapi = createOpenAPI();
|
||||
```
|
||||
|
||||
Add `APIPage` to your MDX Components, so that you can use it in MDX files.
|
||||
@@ -47,7 +53,7 @@ Add `APIPage` to your MDX Components, so that you can use it in MDX files.
|
||||
```tsx title="mdx-components.tsx"
|
||||
import defaultComponents from 'fumadocs-ui/mdx';
|
||||
import { APIPage } from 'fumadocs-openapi/ui';
|
||||
import { openapi } from '@/lib/source';
|
||||
import { openapi } from '@/lib/openapi';
|
||||
import type { MDXComponents } from 'mdx/types';
|
||||
|
||||
export function getMDXComponents(components?: MDXComponents): MDXComponents {
|
||||
@@ -59,7 +65,7 @@ export function getMDXComponents(components?: MDXComponents): MDXComponents {
|
||||
}
|
||||
```
|
||||
|
||||
> It is a React Server Component.
|
||||
> `APIPage` is a React Server Component.
|
||||
|
||||
### Generate Files
|
||||
|
||||
@@ -67,12 +73,12 @@ You can generate MDX files directly from your OpenAPI schema.
|
||||
|
||||
Create a script:
|
||||
|
||||
```js title="scripts/generate-docs.mjs"
|
||||
```js title="scripts/generate-docs.ts"
|
||||
import { generateFiles } from 'fumadocs-openapi';
|
||||
import { openapi } from '@/lib/openapi';
|
||||
|
||||
void generateFiles({
|
||||
// the OpenAPI schema, you can also give it an external URL.
|
||||
input: ['./unkey.json'],
|
||||
input: openapi,
|
||||
output: './content/docs',
|
||||
// we recommend to enable it
|
||||
// make sure your endpoint description doesn't break MDX syntax.
|
||||
@@ -85,7 +91,7 @@ void generateFiles({
|
||||
Generate docs with the script:
|
||||
|
||||
```bash
|
||||
node ./scripts/generate-docs.mjs
|
||||
bun ./scripts/generate-docs.ts
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
@@ -19,7 +19,7 @@ We can use it for executing HTTP requests on the OpenAPI playground, when the ta
|
||||
Create a route handler for proxy server.
|
||||
|
||||
```ts title="/api/proxy/route.ts"
|
||||
import { openapi } from '@/lib/source';
|
||||
import { openapi } from '@/lib/openapi';
|
||||
|
||||
export const { GET, HEAD, PUT, POST, PATCH, DELETE } = openapi.createProxy({
|
||||
// optional, we recommend to set a list of allowed origins for proxied requests
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { createOpenAPI } from 'fumadocs-openapi/server';
|
||||
|
||||
export const openapi = createOpenAPI({
|
||||
input: ['./scalar.yaml'],
|
||||
|
||||
proxyUrl: '/api/proxy',
|
||||
shikiOptions: {
|
||||
themes: {
|
||||
dark: 'vesper',
|
||||
light: 'vitesse-light',
|
||||
},
|
||||
},
|
||||
});
|
||||
+1
-11
@@ -2,7 +2,7 @@ import { createMDXSource } from 'fumadocs-mdx';
|
||||
import type { InferMetaType, InferPageType } from 'fumadocs-core/source';
|
||||
import { loader } from 'fumadocs-core/source';
|
||||
import { icons } from 'lucide-react';
|
||||
import { attachFile, createOpenAPI } from 'fumadocs-openapi/server';
|
||||
import { attachFile } from 'fumadocs-openapi/server';
|
||||
import { createElement } from 'react';
|
||||
import { blog as blogPosts, docs } from '@/.source';
|
||||
|
||||
@@ -23,15 +23,5 @@ export const blog = loader({
|
||||
source: createMDXSource(blogPosts),
|
||||
});
|
||||
|
||||
export const openapi = createOpenAPI({
|
||||
proxyUrl: '/api/proxy',
|
||||
shikiOptions: {
|
||||
themes: {
|
||||
dark: 'vesper',
|
||||
light: 'vitesse-light',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export type Page = InferPageType<typeof source>;
|
||||
export type Meta = InferMetaType<typeof source>;
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "pnpm build:pre && next build && pnpm build:post",
|
||||
"build:pre": "tsx ./scripts/pre-build.mts",
|
||||
"build:post": "tsx ./scripts/post-build.mts",
|
||||
"build:pre": "bun ./scripts/pre-build.ts",
|
||||
"build:post": "bun ./scripts/post-build.ts",
|
||||
"clean": "rimraf .next",
|
||||
"dev": "next dev --turbo",
|
||||
"lint": "fumadocs-mdx && tsx ./scripts/lint.mts && eslint .",
|
||||
@@ -79,7 +79,6 @@
|
||||
"tinyglobby": "^0.2.14",
|
||||
"ts-morph": "^26.0.0",
|
||||
"tsconfig": "workspace:*",
|
||||
"tsx": "^4.20.4",
|
||||
"typescript": "^5.9.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { buildRegistry } from '@/scripts/build-registry.mjs';
|
||||
import { buildRegistry } from '@/scripts/build-registry';
|
||||
import * as OpenAPI from 'fumadocs-openapi';
|
||||
import { rimraf } from 'rimraf';
|
||||
import { openapi } from '@/lib/openapi';
|
||||
|
||||
export async function generateDocs() {
|
||||
await rimraf('./content/docs/openapi/(generated)');
|
||||
|
||||
await Promise.all([
|
||||
OpenAPI.generateFiles({
|
||||
input: ['./scalar.yaml'],
|
||||
input: openapi,
|
||||
output: './content/docs/openapi/(generated)',
|
||||
per: 'operation',
|
||||
includeDescription: true,
|
||||
@@ -0,0 +1,6 @@
|
||||
import { createOpenAPI } from 'fumadocs-openapi/server';
|
||||
|
||||
export const openapi = createOpenAPI({
|
||||
// input files
|
||||
input: ['./openapi.json'],
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createMDXSource } from 'fumadocs-mdx';
|
||||
import { loader } from 'fumadocs-core/source';
|
||||
import { attachFile, createOpenAPI } from 'fumadocs-openapi/server';
|
||||
import { attachFile } from 'fumadocs-openapi/server';
|
||||
import { docs, meta } from '@/.source';
|
||||
|
||||
export const source = loader({
|
||||
@@ -10,5 +10,3 @@ export const source = loader({
|
||||
attachFile,
|
||||
},
|
||||
});
|
||||
|
||||
export const openapi = createOpenAPI();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import defaultMdxComponents from 'fumadocs-ui/mdx';
|
||||
import type { MDXComponents } from 'mdx/types';
|
||||
import { openapi } from '@/lib/source';
|
||||
import { openapi } from '@/lib/openapi';
|
||||
import { APIPage } from 'fumadocs-openapi/ui';
|
||||
|
||||
export function getMDXComponents(components?: MDXComponents): MDXComponents {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "next build",
|
||||
"build:docs": "node ./scripts/generate-docs.mjs",
|
||||
"build:docs": "bun ./scripts/generate-docs.ts",
|
||||
"dev": "next dev --turbo",
|
||||
"start": "next start",
|
||||
"types:check": "fumadocs-mdx && tsc --noEmit"
|
||||
|
||||
+2
-2
@@ -1,5 +1,6 @@
|
||||
import * as OpenAPI from 'fumadocs-openapi';
|
||||
import { rimraf } from 'rimraf';
|
||||
import { openapi } from '@/lib/openapi';
|
||||
|
||||
const out = './content/docs/(api)';
|
||||
|
||||
@@ -12,8 +13,7 @@ async function generate() {
|
||||
});
|
||||
|
||||
await OpenAPI.generateFiles({
|
||||
// input files
|
||||
input: ['./openapi.json'],
|
||||
input: openapi,
|
||||
output: out,
|
||||
});
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import { mkdir, writeFile } from 'node:fs/promises';
|
||||
import * as path from 'node:path';
|
||||
import { resolve } from 'node:path';
|
||||
import { glob } from 'tinyglobby';
|
||||
import {
|
||||
generateAll,
|
||||
@@ -11,10 +10,10 @@ import {
|
||||
generateTags,
|
||||
} from './generate';
|
||||
import {
|
||||
type DocumentInput,
|
||||
processDocument,
|
||||
type ProcessedDocument,
|
||||
} from '@/utils/process-document';
|
||||
import type { OpenAPIServer } from '@/server';
|
||||
|
||||
interface GenerateFileOutput {
|
||||
/**
|
||||
@@ -102,9 +101,9 @@ interface BaseName {
|
||||
|
||||
interface BaseConfig extends GenerateOptions {
|
||||
/**
|
||||
* Schema files
|
||||
* Schema files, or the OpenAPI server object
|
||||
*/
|
||||
input: string[] | string;
|
||||
input: string[] | string | OpenAPIServer;
|
||||
|
||||
/**
|
||||
* Output directory
|
||||
@@ -120,38 +119,52 @@ interface BaseConfig extends GenerateOptions {
|
||||
}
|
||||
|
||||
export async function generateFiles(options: Config): Promise<void> {
|
||||
const { input, cwd = process.cwd() } = options;
|
||||
const urlInputs: string[] = [];
|
||||
const fileInputs: string[] = [];
|
||||
const { cwd = process.cwd() } = options;
|
||||
const input =
|
||||
typeof options.input === 'string' ? [options.input] : options.input;
|
||||
let schemas: Record<string, ProcessedDocument> = {};
|
||||
|
||||
for (const v of typeof input === 'string' ? [input] : input) {
|
||||
if (isUrl(v)) {
|
||||
urlInputs.push(v);
|
||||
} else {
|
||||
fileInputs.push(v);
|
||||
if (Array.isArray(input)) {
|
||||
const targets: string[] = [];
|
||||
|
||||
for (const item of input) {
|
||||
if (isUrl(item)) {
|
||||
targets.push(item);
|
||||
} else {
|
||||
targets.push(...(await glob(item, { cwd, absolute: true })));
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
targets.map(async (item) => {
|
||||
schemas[item] = await processDocument(item);
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
schemas = await input.getSchemas();
|
||||
}
|
||||
|
||||
const resolvedInputs = [
|
||||
...(await glob(fileInputs, { cwd, absolute: false })),
|
||||
...urlInputs,
|
||||
];
|
||||
const resolvedSchemas = Object.entries(schemas);
|
||||
|
||||
if (resolvedInputs.length === 0) {
|
||||
throw new Error(
|
||||
`No input files found. Tried resolving: ${
|
||||
typeof input === 'string' ? input : input.join(', ')
|
||||
}`,
|
||||
);
|
||||
if (resolvedSchemas.length === 0) {
|
||||
throw new Error('No input files found.');
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
resolvedInputs.map((input) => generateFromDocument(input, options)),
|
||||
resolvedSchemas.map(([id, document]) =>
|
||||
generateFromDocument(id, document, options),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
async function generateFromDocument(pathOrUrl: string, options: Config) {
|
||||
async function generateFromDocument(
|
||||
schemaId: string,
|
||||
document: ProcessedDocument,
|
||||
options: Config,
|
||||
) {
|
||||
const { output, cwd = process.cwd(), slugify = defaultSlugify } = options;
|
||||
const outputDir = path.join(cwd, output);
|
||||
|
||||
let nameFn: (
|
||||
output: GeneratePageOutput | GenerateTagOutput | GenerateFileOutput,
|
||||
document: ProcessedDocument['document'],
|
||||
@@ -168,9 +181,9 @@ async function generateFromDocument(pathOrUrl: string, options: Config) {
|
||||
}
|
||||
|
||||
if (options.per === 'file') {
|
||||
return isUrl(pathOrUrl)
|
||||
return isUrl(schemaId)
|
||||
? 'index'
|
||||
: path.basename(pathOrUrl, path.extname(pathOrUrl));
|
||||
: path.basename(schemaId, path.extname(schemaId));
|
||||
}
|
||||
|
||||
const result = output as GeneratePageOutput;
|
||||
@@ -201,9 +214,6 @@ async function generateFromDocument(pathOrUrl: string, options: Config) {
|
||||
nameFn = options.name as typeof nameFn;
|
||||
}
|
||||
|
||||
const document = await dereference(pathOrUrl, options);
|
||||
const outputDir = path.join(cwd, output);
|
||||
|
||||
async function write(file: string, content: string) {
|
||||
await mkdir(path.dirname(file), { recursive: true });
|
||||
await writeFile(file, content);
|
||||
@@ -248,10 +258,10 @@ async function generateFromDocument(pathOrUrl: string, options: Config) {
|
||||
}
|
||||
|
||||
if (options.per === 'file') {
|
||||
const result = await generateAll(pathOrUrl, document, options);
|
||||
const result = await generateAll(schemaId, document, options);
|
||||
const filename = nameFn(
|
||||
{
|
||||
pathOrUrl,
|
||||
pathOrUrl: schemaId,
|
||||
content: result,
|
||||
},
|
||||
document.document,
|
||||
@@ -262,7 +272,7 @@ async function generateFromDocument(pathOrUrl: string, options: Config) {
|
||||
await write(outPath, result);
|
||||
console.log(`Generated: ${outPath}`);
|
||||
} else if (options.per === 'tag') {
|
||||
const results = await generateTags(pathOrUrl, document, options);
|
||||
const results = await generateTags(schemaId, document, options);
|
||||
|
||||
for (const result of results) {
|
||||
const filename = nameFn(result, document.document);
|
||||
@@ -271,7 +281,7 @@ async function generateFromDocument(pathOrUrl: string, options: Config) {
|
||||
console.log(`Generated: ${outPath}`);
|
||||
}
|
||||
} else {
|
||||
const results = await generatePages(pathOrUrl, document, options);
|
||||
const results = await generatePages(schemaId, document, options);
|
||||
const mapping = new Map<string, GeneratePageOutput>();
|
||||
|
||||
for (const result of results) {
|
||||
@@ -323,17 +333,3 @@ function getOutputPathFromRoute(path: string): string {
|
||||
function defaultSlugify(s: string): string {
|
||||
return s.replace(/\s+/g, '-').toLowerCase();
|
||||
}
|
||||
|
||||
async function dereference(
|
||||
pathOrDocument: DocumentInput,
|
||||
options: GenerateOptions,
|
||||
) {
|
||||
return processDocument(
|
||||
// resolve paths
|
||||
typeof pathOrDocument === 'string' &&
|
||||
!pathOrDocument.startsWith('http://') &&
|
||||
!pathOrDocument.startsWith('https://')
|
||||
? resolve(options.cwd ?? process.cwd(), pathOrDocument)
|
||||
: pathOrDocument,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,10 +5,7 @@ import {
|
||||
} from '@/utils/generate-document';
|
||||
import { idToTitle } from '@/utils/id-to-title';
|
||||
import type { OperationItem, WebhookItem } from '@/render/api-page';
|
||||
import type {
|
||||
DocumentInput,
|
||||
ProcessedDocument,
|
||||
} from '@/utils/process-document';
|
||||
import type { ProcessedDocument } from '@/utils/process-document';
|
||||
|
||||
export interface GenerateOptions {
|
||||
/**
|
||||
@@ -54,6 +51,7 @@ export interface GenerateOptions {
|
||||
/**
|
||||
* Inline the entire OpenAPI document into the MDX file.
|
||||
*
|
||||
* @deprecated Use the new `input` API on `createOpenAPI()` instead.
|
||||
* @defaultValue false
|
||||
*/
|
||||
inlineDocument?: boolean;
|
||||
@@ -77,7 +75,7 @@ export type GeneratePageOutput =
|
||||
};
|
||||
|
||||
export async function generateAll(
|
||||
input: DocumentInput,
|
||||
schemaId: string,
|
||||
processed: ProcessedDocument,
|
||||
options: GenerateOptions = {},
|
||||
): Promise<string> {
|
||||
@@ -85,7 +83,7 @@ export async function generateAll(
|
||||
const items = getAPIPageItems(document);
|
||||
|
||||
return generateDocument(
|
||||
input,
|
||||
schemaId,
|
||||
processed,
|
||||
{
|
||||
operations: items.operations,
|
||||
@@ -104,7 +102,7 @@ export async function generateAll(
|
||||
}
|
||||
|
||||
export async function generatePages(
|
||||
input: DocumentInput,
|
||||
schemaId: string,
|
||||
processed: ProcessedDocument,
|
||||
options: GenerateOptions = {},
|
||||
): Promise<GeneratePageOutput[]> {
|
||||
@@ -122,7 +120,7 @@ export async function generatePages(
|
||||
type: 'operation',
|
||||
item,
|
||||
content: generateDocument(
|
||||
input,
|
||||
schemaId,
|
||||
processed,
|
||||
{
|
||||
operations: [item],
|
||||
@@ -153,7 +151,7 @@ export async function generatePages(
|
||||
type: 'webhook',
|
||||
item,
|
||||
content: generateDocument(
|
||||
input,
|
||||
schemaId,
|
||||
processed,
|
||||
{
|
||||
webhooks: [item],
|
||||
@@ -175,7 +173,7 @@ export async function generatePages(
|
||||
}
|
||||
|
||||
export async function generateTags(
|
||||
input: DocumentInput,
|
||||
schemaId: string,
|
||||
processed: ProcessedDocument,
|
||||
options: GenerateOptions = {},
|
||||
): Promise<GenerateTagOutput[]> {
|
||||
@@ -199,7 +197,7 @@ export async function generateTags(
|
||||
return {
|
||||
tag: tag.name,
|
||||
content: generateDocument(
|
||||
input,
|
||||
schemaId,
|
||||
processed,
|
||||
{
|
||||
operations,
|
||||
|
||||
@@ -2,43 +2,25 @@ import Slugger from 'github-slugger';
|
||||
import { Operation } from '@/render/operation';
|
||||
import type { RenderContext } from '@/types';
|
||||
import { createMethod } from '@/server/create-method';
|
||||
import { createRenders, type Renderer } from '@/render/renderer';
|
||||
import { createRenders } from '@/render/renderer';
|
||||
import type { OpenAPIV3_1 } from 'openapi-types';
|
||||
import {
|
||||
type DocumentInput,
|
||||
processDocument,
|
||||
type ProcessedDocument,
|
||||
} from '@/utils/process-document';
|
||||
import { defaultAdapters } from '@/media/adapter';
|
||||
import type { SharedOpenAPIOptions } from '@/server';
|
||||
|
||||
type ApiPageContextProps = Pick<
|
||||
Partial<RenderContext>,
|
||||
| 'shikiOptions'
|
||||
| 'generateTypeScriptSchema'
|
||||
| 'generateCodeSamples'
|
||||
| 'proxyUrl'
|
||||
| 'showResponseSchema'
|
||||
| 'disablePlayground'
|
||||
| 'mediaAdapters'
|
||||
>;
|
||||
|
||||
export interface ApiPageProps extends ApiPageContextProps {
|
||||
document: DocumentInput;
|
||||
export interface ApiPageProps extends SharedOpenAPIOptions {
|
||||
document: Promise<ProcessedDocument> | string | ProcessedDocument;
|
||||
hasHead: boolean;
|
||||
|
||||
renderer?: Partial<Renderer>;
|
||||
|
||||
/**
|
||||
* An array of operations
|
||||
*/
|
||||
operations?: OperationItem[];
|
||||
|
||||
webhooks?: WebhookItem[];
|
||||
|
||||
/**
|
||||
* By default, it is disabled on dev mode
|
||||
*/
|
||||
disableCache?: boolean;
|
||||
}
|
||||
|
||||
export interface WebhookItem {
|
||||
@@ -52,13 +34,11 @@ export interface OperationItem {
|
||||
}
|
||||
|
||||
export async function APIPage(props: ApiPageProps) {
|
||||
const {
|
||||
operations,
|
||||
hasHead = true,
|
||||
webhooks,
|
||||
disableCache = process.env.NODE_ENV === 'development',
|
||||
} = props;
|
||||
const processed = await processDocument(props.document, disableCache);
|
||||
const { operations, hasHead = true, webhooks } = props;
|
||||
const processed =
|
||||
typeof props.document === 'string'
|
||||
? await processDocument(props.document)
|
||||
: await props.document;
|
||||
const ctx = await getContext(processed, props);
|
||||
const { document } = processed;
|
||||
|
||||
@@ -121,9 +101,7 @@ export async function APIPage(props: ApiPageProps) {
|
||||
|
||||
export async function getContext(
|
||||
schema: ProcessedDocument,
|
||||
options: ApiPageContextProps & {
|
||||
renderer?: Partial<Renderer>;
|
||||
} = {},
|
||||
options: SharedOpenAPIOptions = {},
|
||||
): Promise<RenderContext> {
|
||||
const document = schema.document;
|
||||
const servers =
|
||||
|
||||
@@ -1,21 +1,140 @@
|
||||
import type { ApiPageProps } from '@/render/api-page';
|
||||
import { createProxy } from '@/server/proxy';
|
||||
import type { CodeSample } from '@/render/operation';
|
||||
import type { Renderer } from '@/render/renderer';
|
||||
import type { NoReference } from '@/utils/schema';
|
||||
import type {
|
||||
BuiltinTheme,
|
||||
CodeOptionsThemes,
|
||||
CodeToHastOptionsCommon,
|
||||
} from 'shiki';
|
||||
import type { MediaAdapter } from '@/media/adapter';
|
||||
import type { MethodInformation } from '@/types';
|
||||
import type { OpenAPIV3, OpenAPIV3_1 } from 'openapi-types';
|
||||
import {
|
||||
processDocument,
|
||||
type ProcessedDocument,
|
||||
} from '@/utils/process-document';
|
||||
|
||||
export type OpenAPIOptions = Omit<Partial<ApiPageProps>, 'document'>;
|
||||
type Awaitable<T> = T | Promise<T>;
|
||||
/**
|
||||
* schema id -> downloaded schema object
|
||||
*/
|
||||
type SchemaMap = Record<string, OpenAPIV3_1.Document | OpenAPIV3.Document>;
|
||||
type ProcessedSchemaMap = Record<string, ProcessedDocument>;
|
||||
|
||||
export interface SharedOpenAPIOptions {
|
||||
/**
|
||||
* The url of proxy to avoid CORS issues
|
||||
*/
|
||||
proxyUrl?: string;
|
||||
|
||||
renderer?: Partial<Renderer>;
|
||||
|
||||
/**
|
||||
* Disable API Playground
|
||||
*
|
||||
* @defaultValue false
|
||||
*/
|
||||
disablePlayground?: boolean;
|
||||
|
||||
/**
|
||||
* Generate TypeScript definitions from response schema.
|
||||
*
|
||||
* Pass `false` to disable it.
|
||||
*
|
||||
* @param method - the operation object
|
||||
* @param statusCode - status code
|
||||
*/
|
||||
generateTypeScriptSchema?:
|
||||
| ((
|
||||
method: NoReference<MethodInformation>,
|
||||
statusCode: string,
|
||||
) => Awaitable<string>)
|
||||
| false;
|
||||
|
||||
/**
|
||||
* Generate code samples for endpoint.
|
||||
*/
|
||||
generateCodeSamples?: (method: MethodInformation) => Awaitable<CodeSample[]>;
|
||||
|
||||
shikiOptions?: Omit<CodeToHastOptionsCommon, 'lang'> &
|
||||
CodeOptionsThemes<BuiltinTheme>;
|
||||
|
||||
/**
|
||||
* Show full response schema instead of only example response & Typescript definitions
|
||||
*
|
||||
* @default true
|
||||
*/
|
||||
showResponseSchema?: boolean;
|
||||
|
||||
mediaAdapters?: Record<string, MediaAdapter>;
|
||||
}
|
||||
|
||||
export interface OpenAPIOptions extends SharedOpenAPIOptions {
|
||||
/**
|
||||
* Schema files, can be:
|
||||
* - URL
|
||||
* - file path
|
||||
* - a function returning records of downloaded schemas.
|
||||
*/
|
||||
input?: string[] | (() => Promise<SchemaMap>);
|
||||
|
||||
/**
|
||||
* By default, it is disabled on dev mode
|
||||
*/
|
||||
disableCache?: boolean;
|
||||
}
|
||||
|
||||
export interface OpenAPIServer {
|
||||
getAPIPageProps: (from: ApiPageProps) => ApiPageProps;
|
||||
createProxy: typeof createProxy;
|
||||
getSchemas: () => Promise<ProcessedSchemaMap>;
|
||||
}
|
||||
|
||||
export function createOpenAPI(options: OpenAPIOptions = {}): OpenAPIServer {
|
||||
const {
|
||||
input = [],
|
||||
disableCache = process.env.NODE_ENV === 'development',
|
||||
...shared
|
||||
} = options;
|
||||
let schemas: Promise<ProcessedSchemaMap> | undefined;
|
||||
|
||||
async function getSchemas() {
|
||||
const out: ProcessedSchemaMap = {};
|
||||
|
||||
if (Array.isArray(input)) {
|
||||
await Promise.all(
|
||||
input.map(async (item) => {
|
||||
out[item] = await processDocument(item, disableCache);
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
await Promise.all(
|
||||
Object.entries(await input()).map(async ([k, v]) => {
|
||||
out[k] = await processDocument(v, disableCache);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
return {
|
||||
createProxy,
|
||||
getAPIPageProps(props) {
|
||||
async getSchemas() {
|
||||
return (schemas ??= getSchemas());
|
||||
},
|
||||
getAPIPageProps({ document, ...props }) {
|
||||
return {
|
||||
...options,
|
||||
...shared,
|
||||
...props,
|
||||
document:
|
||||
typeof document === 'string'
|
||||
? this.getSchemas().then((map) => {
|
||||
return map[document] ?? processDocument(document, disableCache);
|
||||
})
|
||||
: document,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,15 +1,10 @@
|
||||
import type { OpenAPIV3_1 as V3_1 } from 'openapi-types';
|
||||
import type { default as Slugger } from 'github-slugger';
|
||||
import { type Renderer } from '@/render/renderer';
|
||||
import type { CodeSample } from '@/render/operation';
|
||||
import type {
|
||||
BuiltinTheme,
|
||||
CodeOptionsThemes,
|
||||
CodeToHastOptionsCommon,
|
||||
} from 'shiki';
|
||||
import type { NoReference } from '@/utils/schema';
|
||||
import type { ProcessedDocument } from '@/utils/process-document';
|
||||
import type { MediaAdapter } from '@/media/adapter';
|
||||
import type { SharedOpenAPIOptions } from '@/server';
|
||||
|
||||
export type Document = V3_1.Document;
|
||||
export type OperationObject = V3_1.OperationObject;
|
||||
@@ -27,28 +22,14 @@ export type MethodInformation = NoReference<OperationObject> & {
|
||||
method: string;
|
||||
};
|
||||
|
||||
type Awaitable<T> = T | Promise<T>;
|
||||
|
||||
/**
|
||||
* Dereferenced value and its original `$ref` value
|
||||
*/
|
||||
export type DereferenceMap = Map<unknown, string>;
|
||||
|
||||
export interface RenderContext {
|
||||
/**
|
||||
* The url of proxy to avoid CORS issues
|
||||
*/
|
||||
proxyUrl?: string;
|
||||
|
||||
export interface RenderContext extends SharedOpenAPIOptions {
|
||||
renderer: Renderer;
|
||||
|
||||
/**
|
||||
* Disable API Playground
|
||||
*/
|
||||
disablePlayground?: boolean;
|
||||
|
||||
servers: ServerObject[];
|
||||
|
||||
slugger: Slugger;
|
||||
|
||||
/**
|
||||
@@ -56,35 +37,5 @@ export interface RenderContext {
|
||||
*/
|
||||
schema: ProcessedDocument;
|
||||
|
||||
/**
|
||||
* Generate TypeScript definitions from response schema.
|
||||
*
|
||||
* Pass `false` to disable it.
|
||||
*
|
||||
* @param method - the operation object
|
||||
* @param statusCode - status code
|
||||
*/
|
||||
generateTypeScriptSchema?:
|
||||
| ((
|
||||
method: NoReference<MethodInformation>,
|
||||
statusCode: string,
|
||||
) => Awaitable<string>)
|
||||
| false;
|
||||
|
||||
/**
|
||||
* Generate code samples for endpoint.
|
||||
*/
|
||||
generateCodeSamples?: (method: MethodInformation) => Awaitable<CodeSample[]>;
|
||||
|
||||
shikiOptions?: Omit<CodeToHastOptionsCommon, 'lang'> &
|
||||
CodeOptionsThemes<BuiltinTheme>;
|
||||
|
||||
/**
|
||||
* Show full response schema instead of only example response & Typescript definitions
|
||||
*
|
||||
* @default true
|
||||
*/
|
||||
showResponseSchema?: boolean;
|
||||
|
||||
mediaAdapters: Record<string, MediaAdapter>;
|
||||
}
|
||||
|
||||
@@ -11,10 +11,7 @@ import type { GenerateOptions } from '@/generate';
|
||||
import { idToTitle } from '@/utils/id-to-title';
|
||||
import type { Document, TagObject } from '@/types';
|
||||
import type { NoReference } from '@/utils/schema';
|
||||
import type {
|
||||
DocumentInput,
|
||||
ProcessedDocument,
|
||||
} from '@/utils/process-document';
|
||||
import type { ProcessedDocument } from '@/utils/process-document';
|
||||
|
||||
interface StaticData {
|
||||
toc: TableOfContents;
|
||||
@@ -34,7 +31,7 @@ export type DocumentContext =
|
||||
};
|
||||
|
||||
export function generateDocument(
|
||||
input: DocumentInput,
|
||||
schemaId: string,
|
||||
processed: ProcessedDocument,
|
||||
pageProps: Omit<ApiPageProps, 'document'>,
|
||||
options: GenerateOptions & {
|
||||
@@ -52,7 +49,7 @@ export function generateDocument(
|
||||
const extend = frontmatter?.(options.title, options.description, context);
|
||||
const page: ApiPageProps = {
|
||||
...pageProps,
|
||||
document: options.inlineDocument ? processed.downloaded : input,
|
||||
document: schemaId,
|
||||
};
|
||||
|
||||
let meta: object | undefined;
|
||||
|
||||
@@ -4,8 +4,6 @@ import type { OpenAPIV3, OpenAPIV3_1 } from 'openapi-types';
|
||||
import { bundle, dereference, upgrade } from '@scalar/openapi-parser';
|
||||
import { fetchUrls, readFiles } from '@scalar/openapi-parser/plugins';
|
||||
|
||||
export type DocumentInput = string | OpenAPIV3_1.Document | OpenAPIV3.Document;
|
||||
|
||||
export type ProcessedDocument = {
|
||||
document: NoReference<Document>;
|
||||
dereferenceMap: DereferenceMap;
|
||||
@@ -18,7 +16,7 @@ const cache = new Map<string, ProcessedDocument>();
|
||||
* process & reference input document to a Fumadocs OpenAPI compatible format
|
||||
*/
|
||||
export async function processDocument(
|
||||
input: DocumentInput,
|
||||
input: string | OpenAPIV3_1.Document | OpenAPIV3.Document,
|
||||
disableCache = false,
|
||||
): Promise<ProcessedDocument> {
|
||||
const cached =
|
||||
|
||||
@@ -106,8 +106,8 @@ describe('Generate documents', () => {
|
||||
per: 'file',
|
||||
cwd,
|
||||
}),
|
||||
).rejects.toThrow(
|
||||
'No input files found. Tried resolving: ./fixtures/non-existent-*.yaml',
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`[Error: No input files found.]`,
|
||||
);
|
||||
|
||||
await expect(
|
||||
@@ -123,8 +123,8 @@ describe('Generate documents', () => {
|
||||
algorithm: 'v1',
|
||||
},
|
||||
}),
|
||||
).rejects.toThrow(
|
||||
'No input files found. Tried resolving: ./fixtures/non-existent-1.yaml, ./fixtures/non-existent-2.yaml',
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`[Error: No input files found.]`,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
Generated
-3
@@ -252,9 +252,6 @@ importers:
|
||||
tsconfig:
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/tsconfig
|
||||
tsx:
|
||||
specifier: ^4.20.4
|
||||
version: 4.20.4
|
||||
typescript:
|
||||
specifier: ^5.9.2
|
||||
version: 5.9.2
|
||||
|
||||
Reference in New Issue
Block a user