Skip to content

Commit 40d0fa3

Browse files
committed
OpenAPI: Expect OpenAPI server to use generateFiles()
1 parent aa4e1ad commit 40d0fa3

File tree

10 files changed

+87
-92
lines changed

10 files changed

+87
-92
lines changed

.changeset/dry-schools-flash.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
---
2+
'fumadocs-openapi': major
3+
---
4+
5+
**Expect OpenAPI server to use `generateFiles()`**
6+
7+
File generation is now part of OpenAPI server, the `input` field requires the server instead of string array.
8+
9+
Before:
10+
11+
```ts
12+
import { openapi } from '@/lib/openapi';
13+
14+
void generateFiles({
15+
input: ['./products.yaml'],
16+
output: './content/docs',
17+
});
18+
```
19+
20+
After:
21+
22+
```ts
23+
import { generateFiles } from 'fumadocs-openapi';
24+
import { openapi } from '@/lib/openapi';
25+
26+
void generateFiles({
27+
input: openapi,
28+
output: './content/docs',
29+
});
30+
```

packages/openapi/src/generate-file.ts

Lines changed: 7 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,18 @@
11
import { mkdir, writeFile } from 'node:fs/promises';
22
import * as path from 'node:path';
3-
import { glob } from 'tinyglobby';
43
import {
54
generateDocument,
65
type PagesToTextOptions,
76
toText,
87
} from './utils/pages/to-text';
9-
import {
10-
processDocumentCached,
11-
type ProcessedDocument,
12-
} from '@/utils/process-document';
8+
import type { ProcessedDocument } from '@/utils/process-document';
139
import type { OpenAPIServer } from '@/server';
1410
import { createGetUrl, getSlugs } from 'fumadocs-core/source';
1511
import matter from 'gray-matter';
1612
import {
1713
createAutoPreset,
1814
type SchemaToPagesOptions,
1915
} from '@/utils/pages/preset-auto';
20-
import { isUrl } from '@/utils/url';
2116
import { fromSchema } from '@/utils/pages/builder';
2217

2318
export interface OutputFile {
@@ -56,12 +51,10 @@ interface IndexItem {
5651
}
5752

5853
interface GenerateFilesConfig extends PagesToTextOptions {
59-
cwd?: string;
60-
6154
/**
62-
* Schema files, or the OpenAPI server object
55+
* the OpenAPI server object
6356
*/
64-
input: string[] | string | OpenAPIServer;
57+
input: OpenAPIServer;
6558

6659
/**
6760
* Output directory
@@ -92,12 +85,11 @@ interface HookContext {
9285

9386
export async function generateFiles(options: Config): Promise<void> {
9487
const files = await generateFilesOnly(options);
95-
const { output, cwd = process.cwd() } = options;
96-
const baseDir = path.join(cwd, output);
88+
const { output } = options;
9789

9890
await Promise.all(
9991
files.map(async (file) => {
100-
const filePath = path.join(baseDir, file.path);
92+
const filePath = path.join(output, file.path);
10193

10294
await mkdir(path.dirname(filePath), { recursive: true });
10395
await writeFile(filePath, file.content);
@@ -109,38 +101,8 @@ export async function generateFiles(options: Config): Promise<void> {
109101
export async function generateFilesOnly(
110102
options: SchemaToPagesOptions & Omit<GenerateFilesConfig, 'output'>,
111103
): Promise<OutputFile[]> {
112-
const { cwd = process.cwd(), beforeWrite } = options;
113-
const input =
114-
typeof options.input === 'string' ? [options.input] : options.input;
115-
let schemas: Record<string, ProcessedDocument> = {};
116-
117-
async function resolveInput(item: string) {
118-
if (isUrl(item)) {
119-
schemas[item] = await processDocumentCached(item);
120-
return;
121-
}
122-
123-
const resolved = await glob(item, { cwd, absolute: true });
124-
if (resolved.length > 1) {
125-
console.warn(
126-
'glob patterns in `input` are deprecated, please specify your schemas explicitly.',
127-
);
128-
129-
for (let i = 0; i < resolved.length; i++) {
130-
schemas[`${item}[${i}]`] = await processDocumentCached(item);
131-
}
132-
} else if (resolved.length === 1) {
133-
schemas[item] = await processDocumentCached(resolved[0]);
134-
} else {
135-
throw new Error(`input not found: ${item}`);
136-
}
137-
}
138-
139-
if (Array.isArray(input)) {
140-
await Promise.all(input.map(resolveInput));
141-
} else {
142-
schemas = await input.getSchemas();
143-
}
104+
const { beforeWrite } = options;
105+
const schemas = await options.input.getSchemas();
144106

145107
const generated: Record<string, OutputFile[]> = {};
146108
const files: OutputFile[] = [];

packages/openapi/src/server/create.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,12 @@ import {
77
} from '@/utils/process-document';
88

99
/**
10-
* schema id -> downloaded schema object
10+
* schema id -> file path, URL, or downloaded schema object
1111
*/
12-
type SchemaMap = Record<string, OpenAPIV3_1.Document | OpenAPIV3.Document>;
12+
type SchemaMap = Record<
13+
string,
14+
string | OpenAPIV3_1.Document | OpenAPIV3.Document
15+
>;
1316
type ProcessedSchemaMap = Record<string, ProcessedDocument>;
1417

1518
export interface OpenAPIOptions {
@@ -19,7 +22,7 @@ export interface OpenAPIOptions {
1922
* - file path
2023
* - a function returning records of downloaded schemas.
2124
*/
22-
input?: string[] | (() => Promise<SchemaMap>);
25+
input?: string[] | (() => SchemaMap | Promise<SchemaMap>);
2326

2427
disableCache?: boolean;
2528

packages/openapi/test/index.test.ts

Lines changed: 30 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { fileURLToPath } from 'node:url';
22
import { afterEach, describe, expect, test, vi } from 'vitest';
33
import { idToTitle } from '@/utils/id-to-title';
44
import { generateFilesOnly, type OutputFile } from '@/generate-file';
5+
import { createOpenAPI } from '@/server';
6+
import path from 'node:path';
57

68
describe('Utilities', () => {
79
test('Operation ID to Title', () => {
@@ -20,8 +22,9 @@ describe('Generate documents', () => {
2022

2123
test('Pet Store (Per Operation)', async () => {
2224
const out = await generateFilesOnly({
23-
input: './fixtures/petstore.yaml',
24-
cwd,
25+
input: createOpenAPI({
26+
input: [path.join(cwd, './fixtures/petstore.yaml')],
27+
}),
2528
per: 'operation',
2629
});
2730

@@ -32,8 +35,9 @@ describe('Generate documents', () => {
3235

3336
test('Museum (Per Tag)', async () => {
3437
const out = await generateFilesOnly({
35-
cwd,
36-
input: './fixtures/museum.yaml',
38+
input: createOpenAPI({
39+
input: [path.join(cwd, './fixtures/museum.yaml')],
40+
}),
3741
per: 'tag',
3842
});
3943

@@ -44,8 +48,9 @@ describe('Generate documents', () => {
4448

4549
test('Unkey (Per File)', async () => {
4650
const out = await generateFilesOnly({
47-
cwd,
48-
input: './fixtures/unkey.json',
51+
input: createOpenAPI({
52+
input: [path.join(cwd, './fixtures/unkey.json')],
53+
}),
4954
per: 'file',
5055
});
5156

@@ -56,9 +61,13 @@ describe('Generate documents', () => {
5661

5762
test('Generate Files', async () => {
5863
const out = await generateFilesOnly({
59-
input: ['./fixtures/museum.yaml', './fixtures/petstore.yaml'],
64+
input: createOpenAPI({
65+
input: [
66+
path.join(cwd, './fixtures/museum.yaml'),
67+
path.join(cwd, './fixtures/petstore.yaml'),
68+
],
69+
}),
6070
per: 'file',
61-
cwd,
6271
});
6372

6473
await expect(stringifyOutput(out)).toMatchFileSnapshot(
@@ -69,38 +78,26 @@ describe('Generate documents', () => {
6978
test('Generate Files - throws error when no input files found', async () => {
7079
await expect(
7180
generateFilesOnly({
72-
input: ['./fixtures/non-existent-*.yaml'],
81+
input: createOpenAPI({
82+
input: [path.join(cwd, './fixtures/non-existent.yaml')],
83+
}),
7384
per: 'file',
74-
cwd,
7585
}),
7686
).rejects.toThrowErrorMatchingInlineSnapshot(
77-
`[Error: input not found: ./fixtures/non-existent-*.yaml]`,
87+
`[Error: [OpenAPI] Failed to resolve input: /Users/xred/dev/fumadocs/packages/openapi/test/fixtures/non-existent.yaml]`,
7888
);
79-
80-
await expect(
81-
generateFilesOnly({
82-
input: [
83-
'./fixtures/non-existent-1.yaml',
84-
'./fixtures/non-existent-2.yaml',
85-
],
86-
per: 'file',
87-
cwd,
88-
name: {
89-
algorithm: 'v1',
90-
},
91-
}),
92-
).rejects.toThrow(/input not found/);
9389
});
9490

9591
test('Generate Files - groupBy tag per operation', async () => {
9692
const out = await generateFilesOnly({
97-
input: ['./fixtures/products.yaml'],
93+
input: createOpenAPI({
94+
input: [path.join(cwd, './fixtures/products.yaml')],
95+
}),
9896
per: 'operation',
9997
groupBy: 'tag',
10098
name: {
10199
algorithm: 'v1',
102100
},
103-
cwd,
104101
});
105102

106103
await expect(stringifyOutput(out)).toMatchFileSnapshot(
@@ -110,7 +107,11 @@ describe('Generate documents', () => {
110107

111108
test('Generate Files - with index', async () => {
112109
const out = await generateFilesOnly({
113-
input: ['./fixtures/products.yaml'],
110+
input: createOpenAPI({
111+
input: () => ({
112+
products: path.join(cwd, './fixtures/products.yaml'),
113+
}),
114+
}),
114115
per: 'operation',
115116
name: {
116117
algorithm: 'v1',
@@ -124,11 +125,10 @@ describe('Generate documents', () => {
124125
{
125126
description: 'all available pages',
126127
path: 'index.mdx',
127-
only: ['./fixtures/products.yaml'],
128+
only: ['products'],
128129
},
129130
],
130131
},
131-
cwd,
132132
});
133133

134134
await expect(stringifyOutput(out)).toMatchFileSnapshot(
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
[
22
{
33
"path": "museum.mdx",
4-
"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} />"
4+
"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} />"
55
},
66
{
77
"path": "petstore.mdx",
8-
"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} />"
8+
"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} />"
99
}
1010
]

0 commit comments

Comments
 (0)