mirror of
https://github.com/hanzo-docs/docs.git
synced 2026-10-26 23:49:51 +00:00
OpenAPI: Unify input id and output documents
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'fumadocs-openapi': patch
|
||||
---
|
||||
|
||||
Unify input id and output documents
|
||||
@@ -234,7 +234,7 @@ The server to render pages.
|
||||
|
||||
### Input
|
||||
|
||||
- File Paths (including wildcards)
|
||||
- File Paths
|
||||
- External URLs
|
||||
- A function (see below)
|
||||
|
||||
|
||||
@@ -111,10 +111,19 @@ interface IndexItem {
|
||||
path: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
|
||||
/**
|
||||
* Only include items from specific input schema ids
|
||||
* Specify linked pages:
|
||||
* - items in `inputs` to include all generated pages of a specific schema.
|
||||
* - specific Markdown/MDX files.
|
||||
*/
|
||||
only?: string[];
|
||||
only?: (string | OutputFile)[];
|
||||
}
|
||||
|
||||
interface HookContext {
|
||||
files: OutputFile[];
|
||||
readonly generated: Record<string, OutputFile[]>;
|
||||
readonly documents: Record<string, ProcessedDocument>;
|
||||
}
|
||||
|
||||
interface BaseConfig extends GenerateOptions {
|
||||
@@ -139,7 +148,7 @@ interface BaseConfig extends GenerateOptions {
|
||||
* Generate index files with cards linking to generated pages.
|
||||
*/
|
||||
index?: {
|
||||
items: IndexItem[];
|
||||
items: IndexItem[] | ((ctx: HookContext) => IndexItem[]);
|
||||
|
||||
/**
|
||||
* Generate URLs for cards
|
||||
@@ -158,7 +167,10 @@ interface BaseConfig extends GenerateOptions {
|
||||
/**
|
||||
* Can add/change/remove output files before writing to file system
|
||||
**/
|
||||
beforeWrite?: (files: OutputFile[]) => void | Promise<void>;
|
||||
beforeWrite?: (
|
||||
this: HookContext,
|
||||
files: OutputFile[],
|
||||
) => void | Promise<void>;
|
||||
}
|
||||
|
||||
export async function generateFiles(options: Config): Promise<void> {
|
||||
@@ -181,46 +193,59 @@ export async function generateFilesOnly(
|
||||
typeof options.input === 'string' ? [options.input] : options.input;
|
||||
let schemas: Record<string, ProcessedDocument> = {};
|
||||
|
||||
if (Array.isArray(input)) {
|
||||
const targets: string[] = [];
|
||||
const patterns: string[] = [];
|
||||
|
||||
for (const item of input) {
|
||||
if (isUrl(item)) targets.push(item);
|
||||
else patterns.push(item);
|
||||
async function resolveInput(item: string) {
|
||||
if (isUrl(item)) {
|
||||
schemas[item] = await processDocumentCached(item);
|
||||
return;
|
||||
}
|
||||
|
||||
if (patterns.length > 0) targets.push(...(await glob(patterns, { cwd })));
|
||||
const resolved = await glob(item, { cwd, absolute: true });
|
||||
if (resolved.length > 1) {
|
||||
console.warn(
|
||||
'glob patterns in `input` are deprecated, please specify your schemas explicitly.',
|
||||
);
|
||||
|
||||
await Promise.all(
|
||||
targets.map(async (item) => {
|
||||
schemas[item] = await processDocumentCached(path.join(cwd, item));
|
||||
}),
|
||||
);
|
||||
for (let i = 0; i < resolved.length; i++) {
|
||||
schemas[`${item}[${i}]`] = await processDocumentCached(item);
|
||||
}
|
||||
} else if (resolved.length === 1) {
|
||||
schemas[item] = await processDocumentCached(resolved[0]);
|
||||
} else {
|
||||
throw new Error(`input not found: ${item}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(input)) {
|
||||
await Promise.all(input.map(resolveInput));
|
||||
} else {
|
||||
schemas = await input.getSchemas();
|
||||
}
|
||||
|
||||
const resolvedSchemas = Object.entries(schemas);
|
||||
if (resolvedSchemas.length === 0) {
|
||||
throw new Error('No input files found.');
|
||||
}
|
||||
|
||||
const documentFiles = new Map<string, OutputFile[]>();
|
||||
const generated: Record<string, OutputFile[]> = {};
|
||||
const files: OutputFile[] = [];
|
||||
|
||||
for (const [id, schema] of resolvedSchemas) {
|
||||
const entries = Object.entries(schemas);
|
||||
if (entries.length === 0) {
|
||||
throw new Error('No input files found.');
|
||||
}
|
||||
for (const [id, schema] of entries) {
|
||||
const result = generateFromDocument(id, schema, options);
|
||||
files.push(...result);
|
||||
documentFiles.set(id, result);
|
||||
generated[id] = result;
|
||||
}
|
||||
|
||||
const context: HookContext = {
|
||||
files,
|
||||
generated,
|
||||
documents: schemas,
|
||||
};
|
||||
|
||||
if (options.index) {
|
||||
files.push(...generateIndexFiles(documentFiles, options));
|
||||
writeIndexFiles(context, options);
|
||||
}
|
||||
|
||||
beforeWrite?.(files);
|
||||
return files;
|
||||
await beforeWrite?.call(context, context.files);
|
||||
return context.files;
|
||||
}
|
||||
|
||||
function generateFromDocument(
|
||||
@@ -411,13 +436,9 @@ function getOutputPathFromRoute(path: string): string {
|
||||
);
|
||||
}
|
||||
|
||||
function generateIndexFiles(
|
||||
generatedFiles: Map<string, OutputFile[]>,
|
||||
options: Config,
|
||||
): OutputFile[] {
|
||||
const files: OutputFile[] = [];
|
||||
function writeIndexFiles(context: HookContext, options: Config) {
|
||||
const { index, output, cwd = process.cwd() } = options;
|
||||
if (!index) return files;
|
||||
if (!index) return;
|
||||
|
||||
const { items, url } = index;
|
||||
let urlFn: (path: string) => string;
|
||||
@@ -430,26 +451,31 @@ function generateIndexFiles(
|
||||
urlFn = url;
|
||||
}
|
||||
|
||||
function fileContent(item: IndexItem): string {
|
||||
function fileContent(index: IndexItem): string {
|
||||
const generatedPages = context.generated;
|
||||
const content: string[] = [];
|
||||
content.push('<Cards>');
|
||||
const files: OutputFile[] = [];
|
||||
if (item.only) {
|
||||
for (let id of item.only) {
|
||||
if (id.startsWith('./')) id = id.slice(2);
|
||||
const files = new Map<string, OutputFile>();
|
||||
const only = index.only ?? Object.keys(context.generated);
|
||||
|
||||
const result = generatedFiles.get(id);
|
||||
if (!result)
|
||||
throw new Error(
|
||||
`${id} does not exist on "input", available: ${Array.from(generatedFiles.keys())}.`,
|
||||
);
|
||||
files.push(...result);
|
||||
for (const item of only) {
|
||||
if (typeof item === 'object') {
|
||||
files.set(item.path, item);
|
||||
continue;
|
||||
}
|
||||
|
||||
const result = generatedPages[item];
|
||||
if (!result)
|
||||
throw new Error(
|
||||
`${item} does not exist on "input", available: ${Object.keys(generatedPages).join(', ')}.`,
|
||||
);
|
||||
|
||||
for (const file of result) {
|
||||
files.set(file.path, file);
|
||||
}
|
||||
} else {
|
||||
for (const value of generatedFiles.values()) files.push(...value);
|
||||
}
|
||||
|
||||
for (const file of files) {
|
||||
for (const file of files.values()) {
|
||||
const isContent = file.path.endsWith('.mdx') || file.path.endsWith('.md');
|
||||
if (!isContent) continue;
|
||||
const { data } = matter(file.content);
|
||||
@@ -463,8 +489,8 @@ function generateIndexFiles(
|
||||
content.push('</Cards>');
|
||||
return generateDocument(
|
||||
{
|
||||
title: item.title ?? 'Overview',
|
||||
description: item.description,
|
||||
title: index.title ?? 'Overview',
|
||||
description: index.description,
|
||||
},
|
||||
content.join('\n'),
|
||||
options,
|
||||
@@ -473,17 +499,17 @@ function generateIndexFiles(
|
||||
|
||||
const outputDir = path.join(cwd, output);
|
||||
|
||||
for (const item of items) {
|
||||
files.push({
|
||||
path: path.join(
|
||||
outputDir,
|
||||
path.extname(item.path).length === 0 ? `${item.path}.mdx` : item.path,
|
||||
),
|
||||
for (const item of typeof items === 'function' ? items(context) : items) {
|
||||
const outPath = path.join(
|
||||
outputDir,
|
||||
path.extname(item.path).length === 0 ? `${item.path}.mdx` : item.path,
|
||||
);
|
||||
|
||||
context.files.push({
|
||||
path: outPath,
|
||||
content: fileContent(item),
|
||||
});
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
function defaultSlugify(s: string): string {
|
||||
|
||||
@@ -85,7 +85,7 @@ describe('Generate documents', () => {
|
||||
cwd,
|
||||
}),
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`[Error: No input files found.]`,
|
||||
`[Error: input not found: ./fixtures/non-existent-*.yaml]`,
|
||||
);
|
||||
|
||||
await expect(
|
||||
@@ -102,7 +102,7 @@ describe('Generate documents', () => {
|
||||
},
|
||||
}),
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`[Error: No input files found.]`,
|
||||
`[Error: input not found: ./fixtures/non-existent-1.yaml]`,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
[
|
||||
{
|
||||
"path": "~out/museum.mdx",
|
||||
"content": "---\ntitle: Redocly Museum API\ndescription: >-\n An imaginary, but delightful Museum API for interacting with museum services\n and information. Built with love by Redocly.\nfull: true\n_openapi:\n toc:\n - depth: 2\n title: Get museum hours\n url: '#get-museum-hours'\n - depth: 2\n title: List special events\n url: '#list-special-events'\n - depth: 2\n title: Create special events\n url: '#create-special-events'\n - depth: 2\n title: Get special event\n url: '#get-special-event'\n - depth: 2\n title: Update special event\n url: '#update-special-event'\n - depth: 2\n title: Delete special event\n url: '#delete-special-event'\n - depth: 2\n title: Buy museum tickets\n url: '#buy-museum-tickets'\n - depth: 2\n title: Get ticket QR code\n url: '#get-ticket-qr-code'\n structuredData:\n headings:\n - content: Get museum hours\n id: get-museum-hours\n - content: List special events\n id: list-special-events\n - content: Create special events\n id: create-special-events\n - content: Get special event\n id: get-special-event\n - content: Update special event\n id: update-special-event\n - content: Delete special event\n id: delete-special-event\n - content: Buy museum tickets\n id: buy-museum-tickets\n - content: Get ticket QR code\n id: get-ticket-qr-code\n contents:\n - content: Get upcoming museum operating hours\n heading: get-museum-hours\n - content: Return a list of upcoming special events at the museum.\n heading: list-special-events\n - content: Creates a new special event for the museum.\n heading: create-special-events\n - content: Get details about a special event.\n heading: get-special-event\n - content: Update the details of a special event\n heading: update-special-event\n - content: >-\n Delete a special event from the collection. Allows museum to cancel\n planned events.\n heading: delete-special-event\n - content: Purchase museum tickets for general entry or special events.\n heading: buy-museum-tickets\n - content: >-\n Return an image of your ticket with scannable QR code. Used for event\n entry.\n heading: get-ticket-qr-code\n---\n\n{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}\n\n<APIPage document={\"fixtures/museum.yaml\"} operations={[{\"path\":\"/museum-hours\",\"method\":\"get\"},{\"path\":\"/special-events\",\"method\":\"get\"},{\"path\":\"/special-events\",\"method\":\"post\"},{\"path\":\"/special-events/{eventId}\",\"method\":\"get\"},{\"path\":\"/special-events/{eventId}\",\"method\":\"patch\"},{\"path\":\"/special-events/{eventId}\",\"method\":\"delete\"},{\"path\":\"/tickets\",\"method\":\"post\"},{\"path\":\"/tickets/{ticketId}/qr\",\"method\":\"get\"}]} webhooks={[]} hasHead={true} />"
|
||||
"content": "---\ntitle: Redocly Museum API\ndescription: >-\n An imaginary, but delightful Museum API for interacting with museum services\n and information. Built with love by Redocly.\nfull: true\n_openapi:\n toc:\n - depth: 2\n title: Get museum hours\n url: '#get-museum-hours'\n - depth: 2\n title: List special events\n url: '#list-special-events'\n - depth: 2\n title: Create special events\n url: '#create-special-events'\n - depth: 2\n title: Get special event\n url: '#get-special-event'\n - depth: 2\n title: Update special event\n url: '#update-special-event'\n - depth: 2\n title: Delete special event\n url: '#delete-special-event'\n - depth: 2\n title: Buy museum tickets\n url: '#buy-museum-tickets'\n - depth: 2\n title: Get ticket QR code\n url: '#get-ticket-qr-code'\n structuredData:\n headings:\n - content: Get museum hours\n id: get-museum-hours\n - content: List special events\n id: list-special-events\n - content: Create special events\n id: create-special-events\n - content: Get special event\n id: get-special-event\n - content: Update special event\n id: update-special-event\n - content: Delete special event\n id: delete-special-event\n - content: Buy museum tickets\n id: buy-museum-tickets\n - content: Get ticket QR code\n id: get-ticket-qr-code\n contents:\n - content: Get upcoming museum operating hours\n heading: get-museum-hours\n - content: Return a list of upcoming special events at the museum.\n heading: list-special-events\n - content: Creates a new special event for the museum.\n heading: create-special-events\n - content: Get details about a special event.\n heading: get-special-event\n - content: Update the details of a special event\n heading: update-special-event\n - content: >-\n Delete a special event from the collection. Allows museum to cancel\n planned events.\n heading: delete-special-event\n - content: Purchase museum tickets for general entry or special events.\n heading: buy-museum-tickets\n - content: >-\n Return an image of your ticket with scannable QR code. Used for event\n entry.\n heading: get-ticket-qr-code\n---\n\n{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}\n\n<APIPage document={\"./fixtures/museum.yaml\"} operations={[{\"path\":\"/museum-hours\",\"method\":\"get\"},{\"path\":\"/special-events\",\"method\":\"get\"},{\"path\":\"/special-events\",\"method\":\"post\"},{\"path\":\"/special-events/{eventId}\",\"method\":\"get\"},{\"path\":\"/special-events/{eventId}\",\"method\":\"patch\"},{\"path\":\"/special-events/{eventId}\",\"method\":\"delete\"},{\"path\":\"/tickets\",\"method\":\"post\"},{\"path\":\"/tickets/{ticketId}/qr\",\"method\":\"get\"}]} webhooks={[]} hasHead={true} />"
|
||||
},
|
||||
{
|
||||
"path": "~out/petstore.mdx",
|
||||
"content": "---\ntitle: Swagger Petstore\nfull: true\n_openapi:\n toc:\n - depth: 2\n title: List all pets\n url: '#list-all-pets'\n - depth: 2\n title: Create a pet\n url: '#create-a-pet'\n - depth: 2\n title: Info for a specific pet\n url: '#info-for-a-specific-pet'\n structuredData:\n headings:\n - content: List all pets\n id: list-all-pets\n - content: Create a pet\n id: create-a-pet\n - content: Info for a specific pet\n id: info-for-a-specific-pet\n contents: []\n---\n\n{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}\n\n<APIPage document={\"fixtures/petstore.yaml\"} operations={[{\"path\":\"/pets\",\"method\":\"get\"},{\"path\":\"/pets\",\"method\":\"post\"},{\"path\":\"/pets/{petId}\",\"method\":\"get\"}]} webhooks={[]} hasHead={true} />"
|
||||
"content": "---\ntitle: Swagger Petstore\nfull: true\n_openapi:\n toc:\n - depth: 2\n title: List all pets\n url: '#list-all-pets'\n - depth: 2\n title: Create a pet\n url: '#create-a-pet'\n - depth: 2\n title: Info for a specific pet\n url: '#info-for-a-specific-pet'\n structuredData:\n headings:\n - content: List all pets\n id: list-all-pets\n - content: Create a pet\n id: create-a-pet\n - content: Info for a specific pet\n id: info-for-a-specific-pet\n contents: []\n---\n\n{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}\n\n<APIPage document={\"./fixtures/petstore.yaml\"} operations={[{\"path\":\"/pets\",\"method\":\"get\"},{\"path\":\"/pets\",\"method\":\"post\"},{\"path\":\"/pets/{petId}\",\"method\":\"get\"}]} webhooks={[]} hasHead={true} />"
|
||||
}
|
||||
]
|
||||
@@ -1,14 +1,14 @@
|
||||
[
|
||||
{
|
||||
"path": "~out/products/products/productid.mdx",
|
||||
"content": "---\ntitle: Get product details\ndescription: Retrieve details of a product by its ID.\nfull: true\n_openapi:\n method: GET\n route: /products/{productId}\n toc: []\n structuredData:\n headings: []\n contents:\n - content: Retrieve details of a product by its ID.\n---\n\n{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}\n\n<APIPage document={\"fixtures/products.yaml\"} operations={[{\"path\":\"/products/{productId}\",\"method\":\"get\"}]} webhooks={[]} hasHead={false} />"
|
||||
"content": "---\ntitle: Get product details\ndescription: Retrieve details of a product by its ID.\nfull: true\n_openapi:\n method: GET\n route: /products/{productId}\n toc: []\n structuredData:\n headings: []\n contents:\n - content: Retrieve details of a product by its ID.\n---\n\n{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}\n\n<APIPage document={\"./fixtures/products.yaml\"} operations={[{\"path\":\"/products/{productId}\",\"method\":\"get\"}]} webhooks={[]} hasHead={false} />"
|
||||
},
|
||||
{
|
||||
"path": "~out/inventory/inventory/productid.mdx",
|
||||
"content": "---\ntitle: Get product inventory\ndescription: Retrieve inventory details for a specific product.\nfull: true\n_openapi:\n method: GET\n route: /inventory/{productId}\n toc: []\n structuredData:\n headings: []\n contents:\n - content: Retrieve inventory details for a specific product.\n---\n\n{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}\n\n<APIPage document={\"fixtures/products.yaml\"} operations={[{\"path\":\"/inventory/{productId}\",\"method\":\"get\"}]} webhooks={[]} hasHead={false} />"
|
||||
"content": "---\ntitle: Get product inventory\ndescription: Retrieve inventory details for a specific product.\nfull: true\n_openapi:\n method: GET\n route: /inventory/{productId}\n toc: []\n structuredData:\n headings: []\n contents:\n - content: Retrieve inventory details for a specific product.\n---\n\n{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}\n\n<APIPage document={\"./fixtures/products.yaml\"} operations={[{\"path\":\"/inventory/{productId}\",\"method\":\"get\"}]} webhooks={[]} hasHead={false} />"
|
||||
},
|
||||
{
|
||||
"path": "~out/products/inventory/productid.mdx",
|
||||
"content": "---\ntitle: Get product inventory\ndescription: Retrieve inventory details for a specific product.\nfull: true\n_openapi:\n method: GET\n route: /inventory/{productId}\n toc: []\n structuredData:\n headings: []\n contents:\n - content: Retrieve inventory details for a specific product.\n---\n\n{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}\n\n<APIPage document={\"fixtures/products.yaml\"} operations={[{\"path\":\"/inventory/{productId}\",\"method\":\"get\"}]} webhooks={[]} hasHead={false} />"
|
||||
"content": "---\ntitle: Get product inventory\ndescription: Retrieve inventory details for a specific product.\nfull: true\n_openapi:\n method: GET\n route: /inventory/{productId}\n toc: []\n structuredData:\n headings: []\n contents:\n - content: Retrieve inventory details for a specific product.\n---\n\n{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}\n\n<APIPage document={\"./fixtures/products.yaml\"} operations={[{\"path\":\"/inventory/{productId}\",\"method\":\"get\"}]} webhooks={[]} hasHead={false} />"
|
||||
}
|
||||
]
|
||||
@@ -1,11 +1,11 @@
|
||||
[
|
||||
{
|
||||
"path": "~out/products/productid.mdx",
|
||||
"content": "---\ntitle: Get product details\ndescription: Retrieve details of a product by its ID.\nfull: true\n_openapi:\n method: GET\n route: /products/{productId}\n toc: []\n structuredData:\n headings: []\n contents:\n - content: Retrieve details of a product by its ID.\n---\n\n{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}\n\n<APIPage document={\"fixtures/products.yaml\"} operations={[{\"path\":\"/products/{productId}\",\"method\":\"get\"}]} webhooks={[]} hasHead={false} />"
|
||||
"content": "---\ntitle: Get product details\ndescription: Retrieve details of a product by its ID.\nfull: true\n_openapi:\n method: GET\n route: /products/{productId}\n toc: []\n structuredData:\n headings: []\n contents:\n - content: Retrieve details of a product by its ID.\n---\n\n{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}\n\n<APIPage document={\"./fixtures/products.yaml\"} operations={[{\"path\":\"/products/{productId}\",\"method\":\"get\"}]} webhooks={[]} hasHead={false} />"
|
||||
},
|
||||
{
|
||||
"path": "~out/inventory/productid.mdx",
|
||||
"content": "---\ntitle: Get product inventory\ndescription: Retrieve inventory details for a specific product.\nfull: true\n_openapi:\n method: GET\n route: /inventory/{productId}\n toc: []\n structuredData:\n headings: []\n contents:\n - content: Retrieve inventory details for a specific product.\n---\n\n{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}\n\n<APIPage document={\"fixtures/products.yaml\"} operations={[{\"path\":\"/inventory/{productId}\",\"method\":\"get\"}]} webhooks={[]} hasHead={false} />"
|
||||
"content": "---\ntitle: Get product inventory\ndescription: Retrieve inventory details for a specific product.\nfull: true\n_openapi:\n method: GET\n route: /inventory/{productId}\n toc: []\n structuredData:\n headings: []\n contents:\n - content: Retrieve inventory details for a specific product.\n---\n\n{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}\n\n<APIPage document={\"./fixtures/products.yaml\"} operations={[{\"path\":\"/inventory/{productId}\",\"method\":\"get\"}]} webhooks={[]} hasHead={false} />"
|
||||
},
|
||||
{
|
||||
"path": "~out/index.mdx",
|
||||
|
||||
Reference in New Issue
Block a user