fumadocs-openapi 9.2.2 → 9.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,6 +8,10 @@ interface GenerateFileOutput {
8
8
  pathOrUrl: string;
9
9
  content: string;
10
10
  }
11
+ export interface OutputFile {
12
+ path: string;
13
+ content: string;
14
+ }
11
15
  interface OperationConfig extends BaseConfig {
12
16
  /**
13
17
  * Generate a page for each API endpoint/operation (default).
@@ -59,6 +63,15 @@ interface BaseName {
59
63
  */
60
64
  algorithm?: 'v2' | 'v1';
61
65
  }
66
+ interface IndexItem {
67
+ path: string;
68
+ title?: string;
69
+ description?: string;
70
+ /**
71
+ * Only include items from specific input schema ids
72
+ */
73
+ only?: string[];
74
+ }
62
75
  interface BaseConfig extends GenerateOptions {
63
76
  /**
64
77
  * Schema files, or the OpenAPI server object
@@ -74,7 +87,28 @@ interface BaseConfig extends GenerateOptions {
74
87
  * By default, it only escapes whitespaces and upper case (English) characters
75
88
  */
76
89
  slugify?: (name: string) => string;
90
+ /**
91
+ * Generate index files with cards linking to generated pages.
92
+ */
93
+ index?: {
94
+ items: IndexItem[];
95
+ /**
96
+ * Generate URLs for cards
97
+ */
98
+ url: ((filePath: string) => string) | {
99
+ baseUrl: string;
100
+ /**
101
+ * Base content directory
102
+ */
103
+ contentDir: string;
104
+ };
105
+ };
106
+ /**
107
+ * Can add/change/remove output files before writing to file system
108
+ **/
109
+ beforeWrite?: (files: OutputFile[]) => void | Promise<void>;
77
110
  }
78
111
  export declare function generateFiles(options: Config): Promise<void>;
112
+ export declare function generateFilesOnly(options: Config): Promise<OutputFile[]>;
79
113
  export {};
80
114
  //# sourceMappingURL=generate-file.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"generate-file.d.ts","sourceRoot":"","sources":["../src/generate-file.ts"],"names":[],"mappings":"AAGA,OAAO,EAEL,KAAK,eAAe,EACpB,KAAK,kBAAkB,EAEvB,KAAK,iBAAiB,EAEvB,MAAM,YAAY,CAAC;AACpB,OAAO,EAEL,KAAK,iBAAiB,EACvB,MAAM,0BAA0B,CAAC;AAClC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAE9C,UAAU,kBAAkB;IAC1B;;OAEG;IACH,SAAS,EAAE,MAAM,CAAC;IAElB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,UAAU,eAAgB,SAAQ,UAAU;IAC1C;;OAEG;IACH,GAAG,CAAC,EAAE,WAAW,CAAC;IAElB;;;;;;;OAOG;IACH,OAAO,CAAC,EAAE,KAAK,GAAG,OAAO,GAAG,MAAM,CAAC;IAEnC;;OAEG;IACH,IAAI,CAAC,EACD,CAAC,CACC,MAAM,EAAE,kBAAkB,EAC1B,QAAQ,EAAE,iBAAiB,CAAC,UAAU,CAAC,KACpC,MAAM,CAAC,GACZ,QAAQ,CAAC;CACd;AAED,UAAU,SAAU,SAAQ,UAAU;IACpC;;OAEG;IACH,GAAG,EAAE,KAAK,CAAC;IAEX;;OAEG;IACH,IAAI,CAAC,EACD,CAAC,CACC,MAAM,EAAE,iBAAiB,EACzB,QAAQ,EAAE,iBAAiB,CAAC,UAAU,CAAC,KACpC,MAAM,CAAC,GACZ,QAAQ,CAAC;CACd;AAED,UAAU,UAAW,SAAQ,UAAU;IACrC;;OAEG;IACH,GAAG,EAAE,MAAM,CAAC;IAEZ;;OAEG;IACH,IAAI,CAAC,EACD,CAAC,CACC,MAAM,EAAE,kBAAkB,EAC1B,QAAQ,EAAE,iBAAiB,CAAC,UAAU,CAAC,KACpC,MAAM,CAAC,GACZ,QAAQ,CAAC;CACd;AAED,MAAM,MAAM,MAAM,GAAG,UAAU,GAAG,SAAS,GAAG,eAAe,CAAC;AAE9D,UAAU,QAAQ;IAChB;;;;;;;OAOG;IACH,SAAS,CAAC,EAAE,IAAI,GAAG,IAAI,CAAC;CACzB;AAED,UAAU,UAAW,SAAQ,eAAe;IAC1C;;OAEG;IACH,KAAK,EAAE,MAAM,EAAE,GAAG,MAAM,GAAG,aAAa,CAAC;IAEzC;;OAEG;IACH,MAAM,EAAE,MAAM,CAAC;IAEf;;;;OAIG;IACH,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;CACpC;AAED,wBAAsB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAqClE"}
1
+ {"version":3,"file":"generate-file.d.ts","sourceRoot":"","sources":["../src/generate-file.ts"],"names":[],"mappings":"AAGA,OAAO,EAGL,KAAK,eAAe,EACpB,KAAK,kBAAkB,EAEvB,KAAK,iBAAiB,EAEvB,MAAM,YAAY,CAAC;AACpB,OAAO,EAEL,KAAK,iBAAiB,EACvB,MAAM,0BAA0B,CAAC;AAClC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAI9C,UAAU,kBAAkB;IAC1B;;OAEG;IACH,SAAS,EAAE,MAAM,CAAC;IAElB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,UAAU,eAAgB,SAAQ,UAAU;IAC1C;;OAEG;IACH,GAAG,CAAC,EAAE,WAAW,CAAC;IAElB;;;;;;;OAOG;IACH,OAAO,CAAC,EAAE,KAAK,GAAG,OAAO,GAAG,MAAM,CAAC;IAEnC;;OAEG;IACH,IAAI,CAAC,EACD,CAAC,CACC,MAAM,EAAE,kBAAkB,EAC1B,QAAQ,EAAE,iBAAiB,CAAC,UAAU,CAAC,KACpC,MAAM,CAAC,GACZ,QAAQ,CAAC;CACd;AAED,UAAU,SAAU,SAAQ,UAAU;IACpC;;OAEG;IACH,GAAG,EAAE,KAAK,CAAC;IAEX;;OAEG;IACH,IAAI,CAAC,EACD,CAAC,CACC,MAAM,EAAE,iBAAiB,EACzB,QAAQ,EAAE,iBAAiB,CAAC,UAAU,CAAC,KACpC,MAAM,CAAC,GACZ,QAAQ,CAAC;CACd;AAED,UAAU,UAAW,SAAQ,UAAU;IACrC;;OAEG;IACH,GAAG,EAAE,MAAM,CAAC;IAEZ;;OAEG;IACH,IAAI,CAAC,EACD,CAAC,CACC,MAAM,EAAE,kBAAkB,EAC1B,QAAQ,EAAE,iBAAiB,CAAC,UAAU,CAAC,KACpC,MAAM,CAAC,GACZ,QAAQ,CAAC;CACd;AAED,MAAM,MAAM,MAAM,GAAG,UAAU,GAAG,SAAS,GAAG,eAAe,CAAC;AAE9D,UAAU,QAAQ;IAChB;;;;;;;OAOG;IACH,SAAS,CAAC,EAAE,IAAI,GAAG,IAAI,CAAC;CACzB;AAED,UAAU,SAAS;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;OAEG;IACH,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;CACjB;AAED,UAAU,UAAW,SAAQ,eAAe;IAC1C;;OAEG;IACH,KAAK,EAAE,MAAM,EAAE,GAAG,MAAM,GAAG,aAAa,CAAC;IAEzC;;OAEG;IACH,MAAM,EAAE,MAAM,CAAC;IAEf;;;;OAIG;IACH,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IAEnC;;OAEG;IACH,KAAK,CAAC,EAAE;QACN,KAAK,EAAE,SAAS,EAAE,CAAC;QAEnB;;WAEG;QACH,GAAG,EACC,CAAC,CAAC,QAAQ,EAAE,MAAM,KAAK,MAAM,CAAC,GAC9B;YACE,OAAO,EAAE,MAAM,CAAC;YAChB;;eAEG;YACH,UAAU,EAAE,MAAM,CAAC;SACpB,CAAC;KACP,CAAC;IAEF;;QAEI;IACJ,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,EAAE,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC7D;AAED,wBAAsB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAUlE;AAED,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,UAAU,EAAE,CAAC,CA8CvB"}
@@ -1,10 +1,20 @@
1
1
  import { mkdir, writeFile } from 'node:fs/promises';
2
2
  import * as path from 'node:path';
3
3
  import { glob } from 'tinyglobby';
4
- import { generateAll, generatePages, generateTags, } from './generate.js';
5
- import { processDocument, } from './utils/process-document.js';
4
+ import { generateAll, generateDocument, generatePages, generateTags, } from './generate.js';
5
+ import { processDocumentCached, } from './utils/process-document.js';
6
+ import { createGetUrl, getSlugs } from 'fumadocs-core/source';
7
+ import matter from 'gray-matter';
6
8
  export async function generateFiles(options) {
7
- const { cwd = process.cwd() } = options;
9
+ const files = await generateFilesOnly(options);
10
+ await Promise.all(files.map(async (file) => {
11
+ await mkdir(path.dirname(file.path), { recursive: true });
12
+ await writeFile(file.path, file.content);
13
+ console.log(`Generated: ${file.path}`);
14
+ }));
15
+ }
16
+ export async function generateFilesOnly(options) {
17
+ const { cwd = process.cwd(), beforeWrite } = options;
8
18
  const input = typeof options.input === 'string' ? [options.input] : options.input;
9
19
  let schemas = {};
10
20
  if (Array.isArray(input)) {
@@ -19,7 +29,7 @@ export async function generateFiles(options) {
19
29
  if (patterns.length > 0)
20
30
  targets.push(...(await glob(patterns, { cwd })));
21
31
  await Promise.all(targets.map(async (item) => {
22
- schemas[item] = await processDocument(path.join(cwd, item));
32
+ schemas[item] = await processDocumentCached(path.join(cwd, item));
23
33
  }));
24
34
  }
25
35
  else {
@@ -29,59 +39,43 @@ export async function generateFiles(options) {
29
39
  if (resolvedSchemas.length === 0) {
30
40
  throw new Error('No input files found.');
31
41
  }
32
- await Promise.all(resolvedSchemas.map(([id, document]) => generateFromDocument(id, document, options)));
42
+ const documentFiles = new Map();
43
+ const files = [];
44
+ for (const [id, schema] of resolvedSchemas) {
45
+ const result = generateFromDocument(id, schema, options);
46
+ files.push(...result);
47
+ documentFiles.set(id, result);
48
+ }
49
+ if (options.index) {
50
+ files.push(...generateIndexFiles(documentFiles, options));
51
+ }
52
+ beforeWrite?.(files);
53
+ return files;
33
54
  }
34
- async function generateFromDocument(schemaId, document, options) {
55
+ function generateFromDocument(schemaId, processed, options) {
56
+ const files = [];
57
+ const { document } = processed;
35
58
  const { output, cwd = process.cwd(), slugify = defaultSlugify } = options;
36
59
  const outputDir = path.join(cwd, output);
37
60
  let nameFn;
38
61
  if (!options.name || typeof options.name !== 'function') {
39
- const { algorithm = 'v2' } = options.name ?? {};
40
- nameFn = (output, document) => {
41
- if (options.per === 'tag') {
42
- const result = output;
43
- return slugify(result.tag);
44
- }
45
- if (options.per === 'file') {
46
- return isUrl(schemaId)
47
- ? 'index'
48
- : path.basename(schemaId, path.extname(schemaId));
49
- }
50
- const result = output;
51
- if (result.type === 'operation') {
52
- const operation = document.paths[result.item.path][result.item.method];
53
- if (algorithm === 'v2' && operation.operationId) {
54
- return operation.operationId;
55
- }
56
- return path.join(getOutputPathFromRoute(result.item.path), result.item.method.toLowerCase());
57
- }
58
- const hook = document.webhooks[result.item.name][result.item.method];
59
- if (algorithm === 'v2' && hook.operationId) {
60
- return hook.operationId;
61
- }
62
- return slugify(result.item.name);
63
- };
62
+ const algorithm = options.name?.algorithm;
63
+ nameFn = (out, doc) => defaultNameFn(schemaId, out, doc, options, algorithm);
64
64
  }
65
65
  else {
66
66
  nameFn = options.name;
67
67
  }
68
- async function write(file, content) {
69
- await mkdir(path.dirname(file), { recursive: true });
70
- await writeFile(file, content);
71
- }
72
68
  function getOutputPaths(groupBy = 'none', result) {
73
69
  if (groupBy === 'route') {
74
70
  return [
75
71
  path.join(getOutputPathFromRoute(result.type === 'operation' ? result.item.path : result.item.name), `${result.item.method.toLowerCase()}.mdx`),
76
72
  ];
77
73
  }
78
- const file = nameFn(result, document.document);
74
+ const file = nameFn(result, document);
79
75
  if (groupBy === 'tag') {
80
76
  let tags = result.type === 'operation'
81
- ? document.document.paths[result.item.path][result.item.method]
82
- .tags
83
- : document.document.webhooks[result.item.name][result.item.method]
84
- .tags;
77
+ ? document.paths[result.item.path][result.item.method].tags
78
+ : document.webhooks[result.item.name][result.item.method].tags;
85
79
  if (!tags || tags.length === 0) {
86
80
  console.warn('When `groupBy` is set to `tag`, make sure a `tags` is defined for every operation schema.');
87
81
  tags = ['unknown'];
@@ -91,46 +85,76 @@ async function generateFromDocument(schemaId, document, options) {
91
85
  return [`${file}.mdx`];
92
86
  }
93
87
  if (options.per === 'file') {
94
- const result = await generateAll(schemaId, document, options);
88
+ const result = generateAll(schemaId, processed, options);
95
89
  const filename = nameFn({
96
90
  pathOrUrl: schemaId,
97
91
  content: result,
98
- }, document.document);
99
- const outPath = path.join(outputDir, `${filename}.mdx`);
100
- await write(outPath, result);
101
- console.log(`Generated: ${outPath}`);
92
+ }, document);
93
+ files.push({
94
+ path: path.join(outputDir, `${filename}.mdx`),
95
+ content: result,
96
+ });
97
+ return files;
102
98
  }
103
- else if (options.per === 'tag') {
104
- const results = await generateTags(schemaId, document, options);
99
+ if (options.per === 'tag') {
100
+ const results = generateTags(schemaId, processed, options);
105
101
  for (const result of results) {
106
- const filename = nameFn(result, document.document);
107
- const outPath = path.join(outputDir, `${filename}.mdx`);
108
- await write(outPath, result.content);
109
- console.log(`Generated: ${outPath}`);
102
+ const filename = nameFn(result, document);
103
+ files.push({
104
+ path: path.join(outputDir, `${filename}.mdx`),
105
+ content: result.content,
106
+ });
110
107
  }
108
+ return files;
111
109
  }
112
- else {
113
- const results = await generatePages(schemaId, document, options);
114
- const mapping = new Map();
115
- for (const result of results) {
116
- for (const outputPath of getOutputPaths(options.groupBy, result)) {
117
- mapping.set(outputPath, result);
118
- }
110
+ const results = generatePages(schemaId, processed, options);
111
+ const mapping = new Map();
112
+ for (const result of results) {
113
+ for (const outputPath of getOutputPaths(options.groupBy, result)) {
114
+ mapping.set(outputPath, result);
119
115
  }
120
- for (const [key, output] of mapping.entries()) {
121
- let outputPath = key;
122
- // v1 will remove nested directories
123
- if (typeof options.name === 'object' && options.name.algorithm === 'v1') {
124
- const isSharedDir = Array.from(mapping.keys()).some((item) => item !== outputPath &&
125
- path.dirname(item) === path.dirname(outputPath));
126
- if (!isSharedDir && path.dirname(outputPath) !== '.') {
127
- outputPath = path.join(path.dirname(outputPath) + '.mdx');
128
- }
116
+ }
117
+ for (const [key, output] of mapping.entries()) {
118
+ let outputPath = key;
119
+ // v1 will remove nested directories
120
+ if (typeof options.name === 'object' && options.name.algorithm === 'v1') {
121
+ const isSharedDir = Array.from(mapping.keys()).some((item) => item !== outputPath &&
122
+ path.dirname(item) === path.dirname(outputPath));
123
+ if (!isSharedDir && path.dirname(outputPath) !== '.') {
124
+ outputPath = path.join(path.dirname(outputPath) + '.mdx');
129
125
  }
130
- await write(path.join(outputDir, outputPath), output.content);
131
- console.log(`Generated: ${outputPath}`);
132
126
  }
127
+ files.push({
128
+ path: path.join(outputDir, outputPath),
129
+ content: output.content,
130
+ });
133
131
  }
132
+ return files;
133
+ }
134
+ function defaultNameFn(schemaId, output, document, options, algorithm = 'v2') {
135
+ const { slugify = defaultSlugify } = options;
136
+ if (options.per === 'tag') {
137
+ const result = output;
138
+ return slugify(result.tag);
139
+ }
140
+ if (options.per === 'file') {
141
+ return isUrl(schemaId)
142
+ ? 'index'
143
+ : path.basename(schemaId, path.extname(schemaId));
144
+ }
145
+ const result = output;
146
+ if (result.type === 'operation') {
147
+ const operation = document.paths[result.item.path][result.item.method];
148
+ if (algorithm === 'v2' && operation.operationId) {
149
+ return operation.operationId;
150
+ }
151
+ return path.join(getOutputPathFromRoute(result.item.path), result.item.method.toLowerCase());
152
+ }
153
+ const hook = document.webhooks[result.item.name][result.item.method];
154
+ if (algorithm === 'v2' && hook.operationId) {
155
+ return hook.operationId;
156
+ }
157
+ return slugify(result.item.name);
134
158
  }
135
159
  function isUrl(input) {
136
160
  return input.startsWith('https://') || input.startsWith('http://');
@@ -147,6 +171,63 @@ function getOutputPathFromRoute(path) {
147
171
  })
148
172
  .join('/') ?? '');
149
173
  }
174
+ function generateIndexFiles(generatedFiles, options) {
175
+ const files = [];
176
+ const { index, output, cwd = process.cwd() } = options;
177
+ if (!index)
178
+ return files;
179
+ const { items, url } = index;
180
+ let urlFn;
181
+ if (typeof url === 'object') {
182
+ const getUrl = createGetUrl(url.baseUrl);
183
+ const contentDir = path.resolve(cwd, url.contentDir);
184
+ urlFn = (file) => getUrl(getSlugs(path.relative(contentDir, file)));
185
+ }
186
+ else {
187
+ urlFn = url;
188
+ }
189
+ function fileContent(item) {
190
+ const content = [];
191
+ content.push('<Cards>');
192
+ const files = [];
193
+ if (item.only) {
194
+ for (let id of item.only) {
195
+ if (id.startsWith('./'))
196
+ id = id.slice(2);
197
+ const result = generatedFiles.get(id);
198
+ if (!result)
199
+ throw new Error(`${id} does not exist on "input", available: ${Array.from(generatedFiles.keys())}.`);
200
+ files.push(...result);
201
+ }
202
+ }
203
+ else {
204
+ for (const value of generatedFiles.values())
205
+ files.push(...value);
206
+ }
207
+ for (const file of files) {
208
+ const isContent = file.path.endsWith('.mdx') || file.path.endsWith('.md');
209
+ if (!isContent)
210
+ continue;
211
+ const { data } = matter(file.content);
212
+ if (typeof data.title !== 'string')
213
+ continue;
214
+ content.push(`<Card href="${urlFn(file.path)}" title=${JSON.stringify(data.title)} description=${JSON.stringify(data.description)} />`);
215
+ }
216
+ content.push('</Cards>');
217
+ return generateDocument({
218
+ title: item.title ?? 'Overview',
219
+ description: item.description,
220
+ }, content.join('\n'), options);
221
+ }
222
+ const outputDir = path.join(cwd, output);
223
+ for (const item of items) {
224
+ files.push({
225
+ path: path.join(outputDir, path.extname(item.path).length === 0 ? `${item.path}.mdx` : item.path),
226
+ content: fileContent(item),
227
+ });
228
+ }
229
+ return files;
230
+ }
150
231
  function defaultSlugify(s) {
151
232
  return s.replace(/\s+/g, '-').toLowerCase();
152
233
  }
@@ -1,6 +1,6 @@
1
- import { type DocumentContext } from './utils/generate-document.js';
2
1
  import type { OperationItem, WebhookItem } from './render/api-page.js';
3
2
  import type { ProcessedDocument } from './utils/process-document.js';
3
+ import type { TagObject } from './types.js';
4
4
  export interface GenerateOptions {
5
5
  /**
6
6
  * Additional imports of your MDX components.
@@ -54,7 +54,16 @@ export type GeneratePageOutput = {
54
54
  item: WebhookItem;
55
55
  content: string;
56
56
  };
57
- export declare function generateAll(schemaId: string, processed: ProcessedDocument, options?: GenerateOptions): Promise<string>;
58
- export declare function generatePages(schemaId: string, processed: ProcessedDocument, options?: GenerateOptions): Promise<GeneratePageOutput[]>;
59
- export declare function generateTags(schemaId: string, processed: ProcessedDocument, options?: GenerateOptions): Promise<GenerateTagOutput[]>;
57
+ export declare function generateAll(schemaId: string, processed: ProcessedDocument, options?: GenerateOptions): string;
58
+ export declare function generatePages(schemaId: string, processed: ProcessedDocument, options?: GenerateOptions): GeneratePageOutput[];
59
+ export declare function generateTags(schemaId: string, processed: ProcessedDocument, options?: GenerateOptions): GenerateTagOutput[];
60
+ export declare function generateDocument(frontmatter: unknown, content: string, options: Pick<GenerateOptions, 'addGeneratedComment' | 'imports'>): string;
61
+ export type DocumentContext = {
62
+ type: 'tag';
63
+ tag: TagObject | undefined;
64
+ } | {
65
+ type: 'operation';
66
+ } | {
67
+ type: 'file';
68
+ };
60
69
  //# sourceMappingURL=generate.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"generate.d.ts","sourceRoot":"","sources":["../src/generate.ts"],"names":[],"mappings":"AACA,OAAO,EACL,KAAK,eAAe,EAErB,MAAM,2BAA2B,CAAC;AAEnC,OAAO,KAAK,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACpE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAElE,MAAM,WAAW,eAAe;IAC9B;;OAEG;IACH,OAAO,CAAC,EAAE;QACR,KAAK,EAAE,MAAM,EAAE,CAAC;QAChB,IAAI,EAAE,MAAM,CAAC;KACd,EAAE,CAAC;IAEJ;;;;OAIG;IACH,WAAW,CAAC,EAAE,CACZ,KAAK,EAAE,MAAM,EACb,WAAW,EAAE,MAAM,GAAG,SAAS,EAC/B,OAAO,EAAE,eAAe,KACrB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAE7B;;;;;;OAMG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAE7B;;;;;;;OAOG;IACH,mBAAmB,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;IAEvC,GAAG,CAAC,EAAE,MAAM,CAAC;IAEb;;;;;OAKG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,MAAM,WAAW,iBAAiB;IAChC,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,MAAM,kBAAkB,GAC1B;IACE,IAAI,EAAE,WAAW,CAAC;IAClB,IAAI,EAAE,aAAa,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;CACjB,GACD;IACE,IAAI,EAAE,SAAS,CAAC;IAChB,IAAI,EAAE,WAAW,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAEN,wBAAsB,WAAW,CAC/B,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,iBAAiB,EAC5B,OAAO,GAAE,eAAoB,GAC5B,OAAO,CAAC,MAAM,CAAC,CAqBjB;AAED,wBAAsB,aAAa,CACjC,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,iBAAiB,EAC5B,OAAO,GAAE,eAAoB,GAC5B,OAAO,CAAC,kBAAkB,EAAE,CAAC,CAiE/B;AAED,wBAAsB,YAAY,CAChC,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,iBAAiB,EAC5B,OAAO,GAAE,eAAoB,GAC5B,OAAO,CAAC,iBAAiB,EAAE,CAAC,CAwC9B"}
1
+ {"version":3,"file":"generate.d.ts","sourceRoot":"","sources":["../src/generate.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAEV,aAAa,EACb,WAAW,EACZ,MAAM,mBAAmB,CAAC;AAC3B,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAGlE,OAAO,KAAK,EAAY,SAAS,EAAE,MAAM,SAAS,CAAC;AAKnD,MAAM,WAAW,eAAe;IAC9B;;OAEG;IACH,OAAO,CAAC,EAAE;QACR,KAAK,EAAE,MAAM,EAAE,CAAC;QAChB,IAAI,EAAE,MAAM,CAAC;KACd,EAAE,CAAC;IAEJ;;;;OAIG;IACH,WAAW,CAAC,EAAE,CACZ,KAAK,EAAE,MAAM,EACb,WAAW,EAAE,MAAM,GAAG,SAAS,EAC/B,OAAO,EAAE,eAAe,KACrB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAE7B;;;;;;OAMG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAE7B;;;;;;;OAOG;IACH,mBAAmB,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;IAEvC,GAAG,CAAC,EAAE,MAAM,CAAC;IAEb;;;;;OAKG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,MAAM,WAAW,iBAAiB;IAChC,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,MAAM,kBAAkB,GAC1B;IACE,IAAI,EAAE,WAAW,CAAC;IAClB,IAAI,EAAE,aAAa,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;CACjB,GACD;IACE,IAAI,EAAE,SAAS,CAAC;IAChB,IAAI,EAAE,WAAW,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAEN,wBAAgB,WAAW,CACzB,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,iBAAiB,EAC5B,OAAO,GAAE,eAAoB,GAC5B,MAAM,CAqBR;AAED,wBAAgB,aAAa,CAC3B,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,iBAAiB,EAC5B,OAAO,GAAE,eAAoB,GAC5B,kBAAkB,EAAE,CAiEtB;AAED,wBAAgB,YAAY,CAC1B,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,iBAAiB,EAC5B,OAAO,GAAE,eAAoB,GAC5B,iBAAiB,EAAE,CAwCrB;AAED,wBAAgB,gBAAgB,CAC9B,WAAW,EAAE,OAAO,EACpB,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,IAAI,CAAC,eAAe,EAAE,qBAAqB,GAAG,SAAS,CAAC,GAChE,MAAM,CA+BR;AAOD,MAAM,MAAM,eAAe,GACvB;IACE,IAAI,EAAE,KAAK,CAAC;IACZ,GAAG,EAAE,SAAS,GAAG,SAAS,CAAC;CAC5B,GACD;IACE,IAAI,EAAE,WAAW,CAAC;CACnB,GACD;IACE,IAAI,EAAE,MAAM,CAAC;CACd,CAAC"}
package/dist/generate.js CHANGED
@@ -1,10 +1,11 @@
1
1
  import { getAPIPageItems } from './build-routes.js';
2
- import { generateDocument, } from './utils/generate-document.js';
3
2
  import { idToTitle } from './utils/id-to-title.js';
4
- export async function generateAll(schemaId, processed, options = {}) {
3
+ import { dump } from 'js-yaml';
4
+ import Slugger from 'github-slugger';
5
+ export function generateAll(schemaId, processed, options = {}) {
5
6
  const { document } = processed;
6
7
  const items = getAPIPageItems(document);
7
- return generateDocument(schemaId, processed, {
8
+ return generatePage(schemaId, processed, {
8
9
  operations: items.operations,
9
10
  webhooks: items.webhooks,
10
11
  hasHead: true,
@@ -16,7 +17,7 @@ export async function generateAll(schemaId, processed, options = {}) {
16
17
  type: 'file',
17
18
  });
18
19
  }
19
- export async function generatePages(schemaId, processed, options = {}) {
20
+ export function generatePages(schemaId, processed, options = {}) {
20
21
  const { document } = processed;
21
22
  const items = getAPIPageItems(document);
22
23
  const result = [];
@@ -30,7 +31,7 @@ export async function generatePages(schemaId, processed, options = {}) {
30
31
  result.push({
31
32
  type: 'operation',
32
33
  item,
33
- content: generateDocument(schemaId, processed, {
34
+ content: generatePage(schemaId, processed, {
34
35
  operations: [item],
35
36
  hasHead: false,
36
37
  }, {
@@ -54,7 +55,7 @@ export async function generatePages(schemaId, processed, options = {}) {
54
55
  result.push({
55
56
  type: 'webhook',
56
57
  item,
57
- content: generateDocument(schemaId, processed, {
58
+ content: generatePage(schemaId, processed, {
58
59
  webhooks: [item],
59
60
  hasHead: false,
60
61
  }, {
@@ -68,7 +69,7 @@ export async function generatePages(schemaId, processed, options = {}) {
68
69
  }
69
70
  return result;
70
71
  }
71
- export async function generateTags(schemaId, processed, options = {}) {
72
+ export function generateTags(schemaId, processed, options = {}) {
72
73
  const { document } = processed;
73
74
  if (!document.tags)
74
75
  return [];
@@ -81,7 +82,7 @@ export async function generateTags(schemaId, processed, options = {}) {
81
82
  : idToTitle(tag.name);
82
83
  return {
83
84
  tag: tag.name,
84
- content: generateDocument(schemaId, processed, {
85
+ content: generatePage(schemaId, processed, {
85
86
  operations,
86
87
  webhooks,
87
88
  hasHead: true,
@@ -96,3 +97,99 @@ export async function generateTags(schemaId, processed, options = {}) {
96
97
  };
97
98
  });
98
99
  }
100
+ export function generateDocument(frontmatter, content, options) {
101
+ const { addGeneratedComment = true, imports } = options;
102
+ const out = [];
103
+ const banner = dump(frontmatter).trim();
104
+ if (banner.length > 0)
105
+ out.push(`---\n${banner}\n---`);
106
+ if (addGeneratedComment) {
107
+ let commentContent = 'This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again.';
108
+ if (typeof addGeneratedComment === 'string') {
109
+ commentContent = addGeneratedComment;
110
+ }
111
+ commentContent = commentContent.replaceAll('/', '\\/');
112
+ out.push(`{/* ${commentContent} */}`);
113
+ }
114
+ if (imports) {
115
+ out.push(...imports
116
+ .map((item) => `import { ${item.names.join(', ')} } from ${JSON.stringify(item.from)};`)
117
+ .join('\n'));
118
+ }
119
+ out.push(content);
120
+ return out.join('\n\n');
121
+ }
122
+ function generatePage(schemaId, processed, pageProps, options, context) {
123
+ const { frontmatter, includeDescription = false } = options;
124
+ const extend = frontmatter?.(options.title, options.description, context);
125
+ const page = {
126
+ ...pageProps,
127
+ document: schemaId,
128
+ };
129
+ let meta;
130
+ if (page.operations?.length === 1) {
131
+ const operation = page.operations[0];
132
+ meta = {
133
+ method: operation.method.toUpperCase(),
134
+ route: operation.path,
135
+ };
136
+ }
137
+ const data = generateStaticData(processed.document, page);
138
+ const content = [];
139
+ if (options.description && includeDescription)
140
+ content.push(options.description);
141
+ content.push(pageContent(page));
142
+ return generateDocument({
143
+ title: options.title,
144
+ description: !includeDescription ? options.description : undefined,
145
+ full: true,
146
+ ...extend,
147
+ _openapi: {
148
+ ...meta,
149
+ ...data,
150
+ ...extend?._openapi,
151
+ },
152
+ }, content.join('\n\n'), options);
153
+ }
154
+ function generateStaticData(dereferenced, props) {
155
+ const slugger = new Slugger();
156
+ const toc = [];
157
+ const structuredData = { headings: [], contents: [] };
158
+ for (const item of props.operations ?? []) {
159
+ const operation = dereferenced.paths?.[item.path]?.[item.method];
160
+ if (!operation)
161
+ continue;
162
+ if (props.hasHead && operation.operationId) {
163
+ const title = operation.summary ??
164
+ (operation.operationId ? idToTitle(operation.operationId) : item.path);
165
+ const id = slugger.slug(title);
166
+ toc.push({
167
+ depth: 2,
168
+ title,
169
+ url: `#${id}`,
170
+ });
171
+ structuredData.headings.push({
172
+ content: title,
173
+ id,
174
+ });
175
+ }
176
+ if (operation.description)
177
+ structuredData.contents.push({
178
+ content: operation.description,
179
+ heading: structuredData.headings.at(-1)?.id,
180
+ });
181
+ }
182
+ return { toc, structuredData };
183
+ }
184
+ function pageContent(props) {
185
+ // filter extra properties in props
186
+ const operations = (props.operations ?? []).map((item) => ({
187
+ path: item.path,
188
+ method: item.method,
189
+ }));
190
+ const webhooks = (props.webhooks ?? []).map((item) => ({
191
+ name: item.name,
192
+ method: item.method,
193
+ }));
194
+ return `<APIPage document={${JSON.stringify(props.document)}} operations={${JSON.stringify(operations)}} webhooks={${JSON.stringify(webhooks)}} hasHead={${JSON.stringify(props.hasHead)}} />`;
195
+ }
@@ -3,12 +3,12 @@ import Slugger from 'github-slugger';
3
3
  import { Operation } from '../render/operation/index.js';
4
4
  import { createMethod } from '../server/create-method.js';
5
5
  import { createRenders } from '../render/renderer.js';
6
- import { processDocument, } from '../utils/process-document.js';
6
+ import { processDocumentCached, } from '../utils/process-document.js';
7
7
  import { defaultAdapters } from '../media/adapter.js';
8
8
  export async function APIPage(props) {
9
9
  const { operations, hasHead = true, webhooks } = props;
10
10
  const processed = typeof props.document === 'string'
11
- ? await processDocument(props.document)
11
+ ? await processDocumentCached(props.document)
12
12
  : await props.document;
13
13
  const ctx = await getContext(processed, props);
14
14
  const { document } = processed;
@@ -1 +1 @@
1
- {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/render/schema.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAY,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AACjD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AACrD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAe7C,wBAAgB,MAAM,CAAC,EACrB,IAAI,EACJ,MAAM,EACN,QAAgB,EAChB,QAAgB,EAChB,SAAiB,EACjB,EAAe,EACf,GAAG,EAAE,aAAa,GACnB,EAAE;IACD,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,MAAM,EAAE,cAAc,CAAC;IACvB,EAAE,CAAC,EAAE,UAAU,GAAG,MAAM,CAAC;IAEzB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,GAAG,EAAE,aAAa,CAAC;CACpB,GAAG,SAAS,CAoTZ"}
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/render/schema.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAY,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AACjD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AACrD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAe7C,wBAAgB,MAAM,CAAC,EACrB,IAAI,EACJ,MAAM,EACN,QAAgB,EAChB,QAAgB,EAChB,SAAiB,EACjB,EAAe,EACf,GAAG,EAAE,aAAa,GACnB,EAAE;IACD,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,MAAM,EAAE,cAAc,CAAC;IACvB,EAAE,CAAC,EAAE,UAAU,GAAG,MAAM,CAAC;IAEzB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,GAAG,EAAE,aAAa,CAAC;CACpB,GAAG,SAAS,CA+TZ"}
@@ -7,6 +7,12 @@ import { Tabs, TabsContent, TabsList, TabsTrigger, } from 'fumadocs-ui/component
7
7
  export function Schema({ name, schema, required = false, readOnly = false, writeOnly = false, as = 'property', ctx: renderContext, }) {
8
8
  const { renderer } = renderContext;
9
9
  function propertyBody(schema, renderPrimitive, ctx) {
10
+ if (ctx.stack.has(schema))
11
+ return;
12
+ const next = {
13
+ ...ctx,
14
+ stack: ctx.stack.next(schema),
15
+ };
10
16
  if (Array.isArray(schema.type)) {
11
17
  const items = schema.type.flatMap((type) => {
12
18
  const composed = {
@@ -20,24 +26,25 @@ export function Schema({ name, schema, required = false, readOnly = false, write
20
26
  if (items.length === 0)
21
27
  return;
22
28
  if (items.length === 1)
23
- return propertyBody(items[0], renderPrimitive, ctx);
29
+ return propertyBody(items[0], renderPrimitive, next);
24
30
  return (_jsxs(Tabs, { defaultValue: items[0].type, children: [_jsx(TabsList, { children: items.map((item) => (_jsx(TabsTrigger, { value: item.type, children: schemaToString(item, renderContext.schema) }, item.type))) }), items.map((item) => (_jsxs(TabsContent, { value: item.type, children: [item.description && _jsx(Markdown, { text: item.description }), propertyInfo(item), renderPrimitive(item, ctx)] }, item.type)))] }));
25
31
  }
26
32
  if (schema.oneOf) {
27
33
  const oneOf = schema.oneOf.filter((item) => isComplexType(item));
28
- if (oneOf.length === 0)
34
+ if (oneOf.length === 0 || oneOf.some((item) => ctx.stack.has(item)))
29
35
  return;
30
36
  if (oneOf.length === 1) {
31
- return propertyBody(oneOf[0], renderPrimitive, ctx);
37
+ return propertyBody(oneOf[0], renderPrimitive, next);
32
38
  }
33
- return (_jsxs(Tabs, { defaultValue: "0", children: [_jsx(TabsList, { children: oneOf.map((item, i) => (_jsx(TabsTrigger, { value: i.toString(), children: schemaToString(item, renderContext.schema) }, i))) }), oneOf.map((item, i) => (_jsxs(TabsContent, { value: i.toString(), children: [item.description && _jsx(Markdown, { text: item.description }), propertyInfo(item), propertyBody(item, (child, ctx) => primitiveBody(child, ctx, false, true), ctx)] }, i)))] }));
39
+ return (_jsxs(Tabs, { defaultValue: "0", children: [_jsx(TabsList, { children: oneOf.map((item, i) => (_jsx(TabsTrigger, { value: i.toString(), children: schemaToString(item, renderContext.schema) }, i))) }), oneOf.map((item, i) => (_jsxs(TabsContent, { value: i.toString(), children: [item.description && _jsx(Markdown, { text: item.description }), propertyInfo(item), propertyBody(item, (child, ctx) => primitiveBody(child, ctx, false, true), next)] }, i)))] }));
34
40
  }
35
41
  const of = schema.allOf ?? schema.anyOf;
36
42
  if (of) {
37
- const arr = of.filter((item) => !ctx.stack.has(item));
38
- if (arr.length === 0)
43
+ if (of.length === 0)
44
+ return;
45
+ if (of.some((item) => typeof item === 'object' && ctx.stack.has(item)))
39
46
  return;
40
- const combined = combineSchema(arr);
47
+ const combined = combineSchema(of);
41
48
  if (typeof combined === 'boolean')
42
49
  return;
43
50
  return renderPrimitive(combined, ctx);
@@ -99,9 +106,9 @@ export function Schema({ name, schema, required = false, readOnly = false, write
99
106
  return (_jsx("div", { className: "flex flex-wrap gap-2 not-prose", children: fields.map((field) => (_jsxs("div", { className: "bg-fd-secondary border rounded-lg text-xs p-1.5 shadow-md", children: [_jsx("span", { className: "font-medium me-2", children: field.key }), _jsx("code", { className: "text-fd-muted-foreground", children: field.value })] }, field.key))) }));
100
107
  }
101
108
  function primitiveBody(schema, ctx, collapsible, nested) {
109
+ if (ctx.stack.has(schema))
110
+ return _jsx("p", { children: "Recursive" });
102
111
  if (schema.type === 'object') {
103
- if (ctx.stack.has(schema))
104
- return _jsx("p", { children: "Recursive" });
105
112
  const props = Object.entries(schema.properties ?? {});
106
113
  const patternProps = Object.entries(schema.patternProperties ?? {});
107
114
  const next = {
@@ -138,12 +145,14 @@ export function Schema({ name, schema, required = false, readOnly = false, write
138
145
  else if (schema === false) {
139
146
  return _jsx(renderer.Property, { name: key, type: "never", ...props });
140
147
  }
148
+ if (ctx.stack.has(schema))
149
+ return;
141
150
  if ((schema.readOnly && !readOnly) || (schema.writeOnly && !writeOnly))
142
151
  return;
143
152
  return (_jsxs(renderer.Property, { name: key, type: schemaToString(schema, renderContext.schema), deprecated: schema.deprecated, ...props, children: [schema.description && _jsx(Markdown, { text: schema.description }), propertyInfo(schema), propertyBody(schema, (child, ctx) => primitiveBody(child, ctx, true, true), ctx)] }));
144
153
  }
145
154
  const context = {
146
- stack: schemaStack(),
155
+ stack: schemaStack(renderContext),
147
156
  };
148
157
  if (typeof schema === 'boolean' ||
149
158
  as === 'property' ||
@@ -151,12 +160,17 @@ export function Schema({ name, schema, required = false, readOnly = false, write
151
160
  return property(name, schema, context, { required });
152
161
  return propertyBody(schema, (child, ctx) => primitiveBody(child, ctx, false, false), context);
153
162
  }
154
- function schemaStack(parent) {
155
- const titles = new Set();
156
- const history = new WeakSet();
163
+ function schemaStack(renderContext, parent) {
164
+ const ids = new Set();
165
+ function getId(schema) {
166
+ if (typeof schema !== 'object')
167
+ return;
168
+ return schema.title ?? renderContext.schema.dereferenceMap.get(schema);
169
+ }
157
170
  return {
171
+ history: parent ? [...parent.history] : [],
158
172
  next(...schemas) {
159
- const child = schemaStack(this);
173
+ const child = schemaStack(renderContext, this);
160
174
  for (const item of schemas) {
161
175
  child.add(item);
162
176
  }
@@ -165,18 +179,23 @@ function schemaStack(parent) {
165
179
  add(schema) {
166
180
  if (typeof schema !== 'object')
167
181
  return;
168
- if (schema.title)
169
- titles.add(schema.title);
170
- history.add(schema);
182
+ const id = getId(schema);
183
+ if (id)
184
+ ids.add(id);
185
+ this.history.push(schema);
171
186
  },
172
187
  has(schema) {
173
- if (typeof schema !== 'object')
174
- return false;
175
- if (parent && parent.has(schema))
188
+ if (this.history.length > 30) {
189
+ console.warn(`[Fumadocs OpenAPI] schema depth exceeded 30, this might be unexpected.`);
190
+ // stopping at here
176
191
  return true;
177
- if (schema.title && titles.has(schema.title))
192
+ }
193
+ if (parent && parent.has(schema))
178
194
  return true;
179
- return history.has(schema);
195
+ const id = getId(schema);
196
+ if (id)
197
+ return ids.has(id);
198
+ return this.history.includes(schema);
180
199
  },
181
200
  };
182
201
  }
@@ -56,9 +56,6 @@ export interface OpenAPIOptions extends SharedOpenAPIOptions {
56
56
  * - a function returning records of downloaded schemas.
57
57
  */
58
58
  input?: string[] | (() => Promise<SchemaMap>);
59
- /**
60
- * By default, it is disabled on dev mode
61
- */
62
59
  disableCache?: boolean;
63
60
  }
64
61
  export interface OpenAPIServer {
@@ -1 +1 @@
1
- {"version":3,"file":"create.d.ts","sourceRoot":"","sources":["../../src/server/create.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACtD,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAC7C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAClD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAClD,OAAO,KAAK,EACV,YAAY,EACZ,iBAAiB,EACjB,uBAAuB,EACxB,MAAM,OAAO,CAAC;AACf,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AACpD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC;AACjD,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5D,OAAO,EAEL,KAAK,iBAAiB,EACvB,MAAM,0BAA0B,CAAC;AAElC,KAAK,SAAS,CAAC,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;AACnC;;GAEG;AACH,KAAK,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,QAAQ,GAAG,SAAS,CAAC,QAAQ,CAAC,CAAC;AAC3E,KAAK,kBAAkB,GAAG,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC;AAE5D,MAAM,WAAW,oBAAoB;IACnC;;OAEG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB,QAAQ,CAAC,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAC;IAE7B;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAE5B;;;;;;;OAOG;IACH,wBAAwB,CAAC,EACrB,CAAC,CACC,MAAM,EAAE,WAAW,CAAC,iBAAiB,CAAC,EACtC,UAAU,EAAE,MAAM,KACf,SAAS,CAAC,MAAM,CAAC,CAAC,GACvB,KAAK,CAAC;IAEV;;OAEG;IACH,mBAAmB,CAAC,EAAE,CAAC,MAAM,EAAE,iBAAiB,KAAK,SAAS,CAAC,UAAU,EAAE,CAAC,CAAC;IAE7E,YAAY,CAAC,EAAE,IAAI,CAAC,uBAAuB,EAAE,MAAM,CAAC,GAClD,iBAAiB,CAAC,YAAY,CAAC,CAAC;IAElC;;;;OAIG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAE7B,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;CAC9C;AAED,MAAM,WAAW,cAAe,SAAQ,oBAAoB;IAC1D;;;;;OAKG;IACH,KAAK,CAAC,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC;IAE9C;;OAEG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,aAAa;IAC5B,eAAe,EAAE,CAAC,IAAI,EAAE,YAAY,KAAK,YAAY,CAAC;IACtD,WAAW,EAAE,OAAO,WAAW,CAAC;IAChC,UAAU,EAAE,MAAM,OAAO,CAAC,kBAAkB,CAAC,CAAC;CAC/C;AAED,wBAAgB,aAAa,CAAC,OAAO,GAAE,cAAmB,GAAG,aAAa,CA8CzE;AAED,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,OAAO,EAAE,UAAU,CAAC,CAAC,CAAC,GAAG,UAAU,CAEtE"}
1
+ {"version":3,"file":"create.d.ts","sourceRoot":"","sources":["../../src/server/create.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACtD,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAC7C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAClD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAClD,OAAO,KAAK,EACV,YAAY,EACZ,iBAAiB,EACjB,uBAAuB,EACxB,MAAM,OAAO,CAAC;AACf,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AACpD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC;AACjD,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5D,OAAO,EAGL,KAAK,iBAAiB,EACvB,MAAM,0BAA0B,CAAC;AAElC,KAAK,SAAS,CAAC,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;AACnC;;GAEG;AACH,KAAK,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,QAAQ,GAAG,SAAS,CAAC,QAAQ,CAAC,CAAC;AAC3E,KAAK,kBAAkB,GAAG,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC;AAE5D,MAAM,WAAW,oBAAoB;IACnC;;OAEG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB,QAAQ,CAAC,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAC;IAE7B;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAE5B;;;;;;;OAOG;IACH,wBAAwB,CAAC,EACrB,CAAC,CACC,MAAM,EAAE,WAAW,CAAC,iBAAiB,CAAC,EACtC,UAAU,EAAE,MAAM,KACf,SAAS,CAAC,MAAM,CAAC,CAAC,GACvB,KAAK,CAAC;IAEV;;OAEG;IACH,mBAAmB,CAAC,EAAE,CAAC,MAAM,EAAE,iBAAiB,KAAK,SAAS,CAAC,UAAU,EAAE,CAAC,CAAC;IAE7E,YAAY,CAAC,EAAE,IAAI,CAAC,uBAAuB,EAAE,MAAM,CAAC,GAClD,iBAAiB,CAAC,YAAY,CAAC,CAAC;IAElC;;;;OAIG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAE7B,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;CAC9C;AAED,MAAM,WAAW,cAAe,SAAQ,oBAAoB;IAC1D;;;;;OAKG;IACH,KAAK,CAAC,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC;IAE9C,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,aAAa;IAC5B,eAAe,EAAE,CAAC,IAAI,EAAE,YAAY,KAAK,YAAY,CAAC;IACtD,WAAW,EAAE,OAAO,WAAW,CAAC;IAChC,UAAU,EAAE,MAAM,OAAO,CAAC,kBAAkB,CAAC,CAAC;CAC/C;AAED,wBAAgB,aAAa,CAAC,OAAO,GAAE,cAAmB,GAAG,aAAa,CA4CzE;AAED,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,OAAO,EAAE,UAAU,CAAC,CAAC,CAAC,GAAG,UAAU,CAEtE"}
@@ -1,18 +1,18 @@
1
1
  import { createProxy } from '../server/proxy.js';
2
- import { processDocument, } from '../utils/process-document.js';
2
+ import { processDocument, processDocumentCached, } from '../utils/process-document.js';
3
3
  export function createOpenAPI(options = {}) {
4
- const { input = [], disableCache = process.env.NODE_ENV === 'development', ...shared } = options;
4
+ const { input = [], disableCache = false, ...shared } = options;
5
5
  let schemas;
6
6
  async function getSchemas() {
7
7
  const out = {};
8
8
  if (Array.isArray(input)) {
9
9
  await Promise.all(input.map(async (item) => {
10
- out[item] = await processDocument(item, disableCache);
10
+ out[item] = await processDocument(item);
11
11
  }));
12
12
  }
13
13
  else {
14
14
  await Promise.all(Object.entries(await input()).map(async ([k, v]) => {
15
- out[k] = await processDocument(v, disableCache);
15
+ out[k] = await processDocument(v);
16
16
  }));
17
17
  }
18
18
  return out;
@@ -20,6 +20,8 @@ export function createOpenAPI(options = {}) {
20
20
  return {
21
21
  createProxy,
22
22
  async getSchemas() {
23
+ if (disableCache)
24
+ return getSchemas();
23
25
  return (schemas ?? (schemas = getSchemas()));
24
26
  },
25
27
  getAPIPageProps({ document, ...props }) {
@@ -28,7 +30,7 @@ export function createOpenAPI(options = {}) {
28
30
  ...props,
29
31
  document: typeof document === 'string'
30
32
  ? this.getSchemas().then((map) => {
31
- return map[document] ?? processDocument(document, disableCache);
33
+ return map[document] ?? processDocumentCached(document);
32
34
  })
33
35
  : document,
34
36
  };
@@ -6,8 +6,9 @@ export type ProcessedDocument = {
6
6
  dereferenceMap: DereferenceMap;
7
7
  downloaded: Document;
8
8
  };
9
+ export declare function processDocumentCached(input: string | OpenAPIV3_1.Document | OpenAPIV3.Document): Promise<ProcessedDocument>;
9
10
  /**
10
11
  * process & reference input document to a Fumadocs OpenAPI compatible format
11
12
  */
12
- export declare function processDocument(input: string | OpenAPIV3_1.Document | OpenAPIV3.Document, disableCache?: boolean): Promise<ProcessedDocument>;
13
+ export declare function processDocument(input: string | OpenAPIV3_1.Document | OpenAPIV3.Document): Promise<ProcessedDocument>;
13
14
  //# sourceMappingURL=process-document.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"process-document.d.ts","sourceRoot":"","sources":["../../src/utils/process-document.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACxD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAClD,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAI5D,MAAM,MAAM,iBAAiB,GAAG;IAC9B,QAAQ,EAAE,WAAW,CAAC,QAAQ,CAAC,CAAC;IAChC,cAAc,EAAE,cAAc,CAAC;IAC/B,UAAU,EAAE,QAAQ,CAAC;CACtB,CAAC;AAIF;;GAEG;AACH,wBAAsB,eAAe,CACnC,KAAK,EAAE,MAAM,GAAG,WAAW,CAAC,QAAQ,GAAG,SAAS,CAAC,QAAQ,EACzD,YAAY,UAAQ,GACnB,OAAO,CAAC,iBAAiB,CAAC,CA+B5B"}
1
+ {"version":3,"file":"process-document.d.ts","sourceRoot":"","sources":["../../src/utils/process-document.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACxD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAClD,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAI5D,MAAM,MAAM,iBAAiB,GAAG;IAC9B,QAAQ,EAAE,WAAW,CAAC,QAAQ,CAAC,CAAC;IAChC,cAAc,EAAE,cAAc,CAAC;IAC/B,UAAU,EAAE,QAAQ,CAAC;CACtB,CAAC;AAIF,wBAAsB,qBAAqB,CACzC,KAAK,EAAE,MAAM,GAAG,WAAW,CAAC,QAAQ,GAAG,SAAS,CAAC,QAAQ,GACxD,OAAO,CAAC,iBAAiB,CAAC,CAS5B;AAED;;GAEG;AACH,wBAAsB,eAAe,CACnC,KAAK,EAAE,MAAM,GAAG,WAAW,CAAC,QAAQ,GAAG,SAAS,CAAC,QAAQ,GACxD,OAAO,CAAC,iBAAiB,CAAC,CAoB5B"}
@@ -1,13 +1,20 @@
1
1
  import { bundle, dereference, upgrade } from '@scalar/openapi-parser';
2
2
  import { fetchUrls, readFiles } from '@scalar/openapi-parser/plugins';
3
3
  const cache = new Map();
4
+ export async function processDocumentCached(input) {
5
+ if (typeof input !== 'string')
6
+ return processDocument(input);
7
+ const cached = cache.get(input);
8
+ if (cached)
9
+ return cached;
10
+ const processed = await processDocument(input);
11
+ cache.set(input, processed);
12
+ return processed;
13
+ }
4
14
  /**
5
15
  * process & reference input document to a Fumadocs OpenAPI compatible format
6
16
  */
7
- export async function processDocument(input, disableCache = false) {
8
- const cached = !disableCache && typeof input === 'string' ? cache.get(input) : null;
9
- if (cached)
10
- return cached;
17
+ export async function processDocument(input) {
11
18
  const dereferenceMap = new Map();
12
19
  let document = await bundle(input, {
13
20
  plugins: [fetchUrls(), readFiles()],
@@ -20,13 +27,9 @@ export async function processDocument(input, disableCache = false) {
20
27
  dereferenceMap.set(schema, ref);
21
28
  },
22
29
  });
23
- const processed = {
30
+ return {
24
31
  document: dereferenced,
25
32
  dereferenceMap,
26
33
  downloaded: document,
27
34
  };
28
- if (!disableCache && typeof input === 'string') {
29
- cache.set(input, processed);
30
- }
31
- return processed;
32
35
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fumadocs-openapi",
3
- "version": "9.2.2",
3
+ "version": "9.3.0",
4
4
  "description": "Generate MDX docs for your OpenAPI spec",
5
5
  "keywords": [
6
6
  "NextJs",
@@ -54,6 +54,7 @@
54
54
  "ajv": "^8.17.1",
55
55
  "class-variance-authority": "^0.7.1",
56
56
  "github-slugger": "^2.0.0",
57
+ "gray-matter": "^4.0.3",
57
58
  "hast-util-to-jsx-runtime": "^2.3.6",
58
59
  "js-yaml": "^4.1.0",
59
60
  "next-themes": "^0.4.6",
@@ -64,15 +65,15 @@
64
65
  "shiki": "^3.11.0",
65
66
  "tinyglobby": "^0.2.14",
66
67
  "xml-js": "^1.6.11",
67
- "fumadocs-core": "15.7.1",
68
- "fumadocs-ui": "15.7.1"
68
+ "fumadocs-core": "15.7.3",
69
+ "fumadocs-ui": "15.7.3"
69
70
  },
70
71
  "devDependencies": {
71
- "@scalar/api-client-react": "^1.3.27",
72
+ "@scalar/api-client-react": "^1.3.29",
72
73
  "@types/js-yaml": "^4.0.9",
73
74
  "@types/node": "24.3.0",
74
75
  "@types/openapi-sampler": "^1.0.3",
75
- "@types/react": "^19.1.10",
76
+ "@types/react": "^19.1.11",
76
77
  "json-schema-typed": "^8.0.1",
77
78
  "openapi-types": "^12.1.3",
78
79
  "tailwindcss": "^4.1.12",
@@ -1,17 +0,0 @@
1
- import type { ApiPageProps } from '../render/api-page.js';
2
- import type { GenerateOptions } from '../generate.js';
3
- import type { TagObject } from '../types.js';
4
- import type { ProcessedDocument } from '../utils/process-document.js';
5
- export type DocumentContext = {
6
- type: 'tag';
7
- tag: TagObject | undefined;
8
- } | {
9
- type: 'operation';
10
- } | {
11
- type: 'file';
12
- };
13
- export declare function generateDocument(schemaId: string, processed: ProcessedDocument, pageProps: Omit<ApiPageProps, 'document'>, options: GenerateOptions & {
14
- title: string;
15
- description?: string;
16
- }, context: DocumentContext): string;
17
- //# sourceMappingURL=generate-document.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"generate-document.d.ts","sourceRoot":"","sources":["../../src/utils/generate-document.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,YAAY,EAGb,MAAM,mBAAmB,CAAC;AAC3B,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAElD,OAAO,KAAK,EAAY,SAAS,EAAE,MAAM,SAAS,CAAC;AAEnD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAOlE,MAAM,MAAM,eAAe,GACvB;IACE,IAAI,EAAE,KAAK,CAAC;IACZ,GAAG,EAAE,SAAS,GAAG,SAAS,CAAC;CAC5B,GACD;IACE,IAAI,EAAE,WAAW,CAAC;CACnB,GACD;IACE,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEN,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,iBAAiB,EAC5B,SAAS,EAAE,IAAI,CAAC,YAAY,EAAE,UAAU,CAAC,EACzC,OAAO,EAAE,eAAe,GAAG;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,EACD,OAAO,EAAE,eAAe,GACvB,MAAM,CAkER"}
@@ -1,94 +0,0 @@
1
- import { dump } from 'js-yaml';
2
- import Slugger from 'github-slugger';
3
- import { idToTitle } from '../utils/id-to-title.js';
4
- export function generateDocument(schemaId, processed, pageProps, options, context) {
5
- const { frontmatter, includeDescription = false, addGeneratedComment = true, } = options;
6
- const out = [];
7
- const extend = frontmatter?.(options.title, options.description, context);
8
- const page = {
9
- ...pageProps,
10
- document: schemaId,
11
- };
12
- let meta;
13
- if (page.operations?.length === 1) {
14
- const operation = page.operations[0];
15
- meta = {
16
- method: operation.method.toUpperCase(),
17
- route: operation.path,
18
- };
19
- }
20
- const data = generateStaticData(processed.document, page);
21
- const banner = dump({
22
- title: options.title,
23
- description: !includeDescription ? options.description : undefined,
24
- full: true,
25
- ...extend,
26
- _openapi: {
27
- ...meta,
28
- ...data,
29
- ...extend?._openapi,
30
- },
31
- }).trim();
32
- if (banner.length > 0)
33
- out.push(`---\n${banner}\n---`);
34
- if (addGeneratedComment !== false) {
35
- let commentContent = 'This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again.';
36
- if (typeof addGeneratedComment === 'string') {
37
- commentContent = addGeneratedComment;
38
- }
39
- commentContent = commentContent.replaceAll('/', '\\/');
40
- out.push(`{/* ${commentContent} */}`);
41
- }
42
- const imports = options.imports
43
- ?.map((item) => `import { ${item.names.join(', ')} } from ${JSON.stringify(item.from)};`)
44
- .join('\n');
45
- if (imports) {
46
- out.push(imports);
47
- }
48
- if (options.description && includeDescription)
49
- out.push(options.description);
50
- out.push(pageContent(page));
51
- return out.join('\n\n');
52
- }
53
- function generateStaticData(dereferenced, props) {
54
- const slugger = new Slugger();
55
- const toc = [];
56
- const structuredData = { headings: [], contents: [] };
57
- for (const item of props.operations ?? []) {
58
- const operation = dereferenced.paths?.[item.path]?.[item.method];
59
- if (!operation)
60
- continue;
61
- if (props.hasHead && operation.operationId) {
62
- const title = operation.summary ??
63
- (operation.operationId ? idToTitle(operation.operationId) : item.path);
64
- const id = slugger.slug(title);
65
- toc.push({
66
- depth: 2,
67
- title,
68
- url: `#${id}`,
69
- });
70
- structuredData.headings.push({
71
- content: title,
72
- id,
73
- });
74
- }
75
- if (operation.description)
76
- structuredData.contents.push({
77
- content: operation.description,
78
- heading: structuredData.headings.at(-1)?.id,
79
- });
80
- }
81
- return { toc, structuredData };
82
- }
83
- function pageContent(props) {
84
- // filter extra properties in props
85
- const operations = (props.operations ?? []).map((item) => ({
86
- path: item.path,
87
- method: item.method,
88
- }));
89
- const webhooks = (props.webhooks ?? []).map((item) => ({
90
- name: item.name,
91
- method: item.method,
92
- }));
93
- return `<APIPage document={${JSON.stringify(props.document)}} operations={${JSON.stringify(operations)}} webhooks={${JSON.stringify(webhooks)}} hasHead={${JSON.stringify(props.hasHead)}} />`;
94
- }