fumadocs-openapi 9.2.3 → 9.3.1

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,22 @@ 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
+ * Specify linked pages:
72
+ * - items in `inputs` to include all generated pages of a specific schema.
73
+ * - specific Markdown/MDX files.
74
+ */
75
+ only?: (string | OutputFile)[];
76
+ }
77
+ interface HookContext {
78
+ files: OutputFile[];
79
+ readonly generated: Record<string, OutputFile[]>;
80
+ readonly documents: Record<string, ProcessedDocument>;
81
+ }
62
82
  interface BaseConfig extends GenerateOptions {
63
83
  /**
64
84
  * Schema files, or the OpenAPI server object
@@ -74,7 +94,28 @@ interface BaseConfig extends GenerateOptions {
74
94
  * By default, it only escapes whitespaces and upper case (English) characters
75
95
  */
76
96
  slugify?: (name: string) => string;
97
+ /**
98
+ * Generate index files with cards linking to generated pages.
99
+ */
100
+ index?: {
101
+ items: IndexItem[] | ((ctx: HookContext) => IndexItem[]);
102
+ /**
103
+ * Generate URLs for cards
104
+ */
105
+ url: ((filePath: string) => string) | {
106
+ baseUrl: string;
107
+ /**
108
+ * Base content directory
109
+ */
110
+ contentDir: string;
111
+ };
112
+ };
113
+ /**
114
+ * Can add/change/remove output files before writing to file system
115
+ **/
116
+ beforeWrite?: (this: HookContext, files: OutputFile[]) => void | Promise<void>;
77
117
  }
78
118
  export declare function generateFiles(options: Config): Promise<void>;
119
+ export declare function generateFilesOnly(options: Config): Promise<OutputFile[]>;
79
120
  export {};
80
121
  //# 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;IAErB;;;;OAIG;IACH,IAAI,CAAC,EAAE,CAAC,MAAM,GAAG,UAAU,CAAC,EAAE,CAAC;CAChC;AAED,UAAU,WAAW;IACnB,KAAK,EAAE,UAAU,EAAE,CAAC;IACpB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,EAAE,CAAC,CAAC;IACjD,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC;CACvD;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,GAAG,CAAC,CAAC,GAAG,EAAE,WAAW,KAAK,SAAS,EAAE,CAAC,CAAC;QAEzD;;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,CACZ,IAAI,EAAE,WAAW,EACjB,KAAK,EAAE,UAAU,EAAE,KAChB,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC3B;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,CA2DvB"}
@@ -1,87 +1,93 @@
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
- if (Array.isArray(input)) {
11
- const targets = [];
12
- const patterns = [];
13
- for (const item of input) {
14
- if (isUrl(item))
15
- targets.push(item);
16
- else
17
- patterns.push(item);
20
+ async function resolveInput(item) {
21
+ if (isUrl(item)) {
22
+ schemas[item] = await processDocumentCached(item);
23
+ return;
24
+ }
25
+ const resolved = await glob(item, { cwd, absolute: true });
26
+ if (resolved.length > 1) {
27
+ console.warn('glob patterns in `input` are deprecated, please specify your schemas explicitly.');
28
+ for (let i = 0; i < resolved.length; i++) {
29
+ schemas[`${item}[${i}]`] = await processDocumentCached(item);
30
+ }
31
+ }
32
+ else if (resolved.length === 1) {
33
+ schemas[item] = await processDocumentCached(resolved[0]);
18
34
  }
19
- if (patterns.length > 0)
20
- targets.push(...(await glob(patterns, { cwd })));
21
- await Promise.all(targets.map(async (item) => {
22
- schemas[item] = await processDocument(path.join(cwd, item));
23
- }));
35
+ else {
36
+ throw new Error(`input not found: ${item}`);
37
+ }
38
+ }
39
+ if (Array.isArray(input)) {
40
+ await Promise.all(input.map(resolveInput));
24
41
  }
25
42
  else {
26
43
  schemas = await input.getSchemas();
27
44
  }
28
- const resolvedSchemas = Object.entries(schemas);
29
- if (resolvedSchemas.length === 0) {
45
+ const generated = {};
46
+ const files = [];
47
+ const entries = Object.entries(schemas);
48
+ if (entries.length === 0) {
30
49
  throw new Error('No input files found.');
31
50
  }
32
- await Promise.all(resolvedSchemas.map(([id, document]) => generateFromDocument(id, document, options)));
51
+ for (const [id, schema] of entries) {
52
+ const result = generateFromDocument(id, schema, options);
53
+ files.push(...result);
54
+ generated[id] = result;
55
+ }
56
+ const context = {
57
+ files,
58
+ generated,
59
+ documents: schemas,
60
+ };
61
+ if (options.index) {
62
+ writeIndexFiles(context, options);
63
+ }
64
+ await beforeWrite?.call(context, context.files);
65
+ return context.files;
33
66
  }
34
- async function generateFromDocument(schemaId, document, options) {
67
+ function generateFromDocument(schemaId, processed, options) {
68
+ const files = [];
69
+ const { document } = processed;
35
70
  const { output, cwd = process.cwd(), slugify = defaultSlugify } = options;
36
71
  const outputDir = path.join(cwd, output);
37
72
  let nameFn;
38
73
  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
- };
74
+ const algorithm = options.name?.algorithm;
75
+ nameFn = (out, doc) => defaultNameFn(schemaId, out, doc, options, algorithm);
64
76
  }
65
77
  else {
66
78
  nameFn = options.name;
67
79
  }
68
- async function write(file, content) {
69
- await mkdir(path.dirname(file), { recursive: true });
70
- await writeFile(file, content);
71
- }
72
80
  function getOutputPaths(groupBy = 'none', result) {
73
81
  if (groupBy === 'route') {
74
82
  return [
75
83
  path.join(getOutputPathFromRoute(result.type === 'operation' ? result.item.path : result.item.name), `${result.item.method.toLowerCase()}.mdx`),
76
84
  ];
77
85
  }
78
- const file = nameFn(result, document.document);
86
+ const file = nameFn(result, document);
79
87
  if (groupBy === 'tag') {
80
88
  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;
89
+ ? document.paths[result.item.path][result.item.method].tags
90
+ : document.webhooks[result.item.name][result.item.method].tags;
85
91
  if (!tags || tags.length === 0) {
86
92
  console.warn('When `groupBy` is set to `tag`, make sure a `tags` is defined for every operation schema.');
87
93
  tags = ['unknown'];
@@ -91,46 +97,76 @@ async function generateFromDocument(schemaId, document, options) {
91
97
  return [`${file}.mdx`];
92
98
  }
93
99
  if (options.per === 'file') {
94
- const result = await generateAll(schemaId, document, options);
100
+ const result = generateAll(schemaId, processed, options);
95
101
  const filename = nameFn({
96
102
  pathOrUrl: schemaId,
97
103
  content: result,
98
- }, document.document);
99
- const outPath = path.join(outputDir, `${filename}.mdx`);
100
- await write(outPath, result);
101
- console.log(`Generated: ${outPath}`);
104
+ }, document);
105
+ files.push({
106
+ path: path.join(outputDir, `${filename}.mdx`),
107
+ content: result,
108
+ });
109
+ return files;
102
110
  }
103
- else if (options.per === 'tag') {
104
- const results = await generateTags(schemaId, document, options);
111
+ if (options.per === 'tag') {
112
+ const results = generateTags(schemaId, processed, options);
105
113
  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}`);
114
+ const filename = nameFn(result, document);
115
+ files.push({
116
+ path: path.join(outputDir, `${filename}.mdx`),
117
+ content: result.content,
118
+ });
110
119
  }
120
+ return files;
111
121
  }
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
- }
122
+ const results = generatePages(schemaId, processed, options);
123
+ const mapping = new Map();
124
+ for (const result of results) {
125
+ for (const outputPath of getOutputPaths(options.groupBy, result)) {
126
+ mapping.set(outputPath, result);
119
127
  }
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
- }
128
+ }
129
+ for (const [key, output] of mapping.entries()) {
130
+ let outputPath = key;
131
+ // v1 will remove nested directories
132
+ if (typeof options.name === 'object' && options.name.algorithm === 'v1') {
133
+ const isSharedDir = Array.from(mapping.keys()).some((item) => item !== outputPath &&
134
+ path.dirname(item) === path.dirname(outputPath));
135
+ if (!isSharedDir && path.dirname(outputPath) !== '.') {
136
+ outputPath = path.join(path.dirname(outputPath) + '.mdx');
129
137
  }
130
- await write(path.join(outputDir, outputPath), output.content);
131
- console.log(`Generated: ${outputPath}`);
132
138
  }
139
+ files.push({
140
+ path: path.join(outputDir, outputPath),
141
+ content: output.content,
142
+ });
143
+ }
144
+ return files;
145
+ }
146
+ function defaultNameFn(schemaId, output, document, options, algorithm = 'v2') {
147
+ const { slugify = defaultSlugify } = options;
148
+ if (options.per === 'tag') {
149
+ const result = output;
150
+ return slugify(result.tag);
151
+ }
152
+ if (options.per === 'file') {
153
+ return isUrl(schemaId)
154
+ ? 'index'
155
+ : path.basename(schemaId, path.extname(schemaId));
156
+ }
157
+ const result = output;
158
+ if (result.type === 'operation') {
159
+ const operation = document.paths[result.item.path][result.item.method];
160
+ if (algorithm === 'v2' && operation.operationId) {
161
+ return operation.operationId;
162
+ }
163
+ return path.join(getOutputPathFromRoute(result.item.path), result.item.method.toLowerCase());
164
+ }
165
+ const hook = document.webhooks[result.item.name][result.item.method];
166
+ if (algorithm === 'v2' && hook.operationId) {
167
+ return hook.operationId;
133
168
  }
169
+ return slugify(result.item.name);
134
170
  }
135
171
  function isUrl(input) {
136
172
  return input.startsWith('https://') || input.startsWith('http://');
@@ -147,6 +183,62 @@ function getOutputPathFromRoute(path) {
147
183
  })
148
184
  .join('/') ?? '');
149
185
  }
186
+ function writeIndexFiles(context, options) {
187
+ const { index, output, cwd = process.cwd() } = options;
188
+ if (!index)
189
+ return;
190
+ const { items, url } = index;
191
+ let urlFn;
192
+ if (typeof url === 'object') {
193
+ const getUrl = createGetUrl(url.baseUrl);
194
+ const contentDir = path.resolve(cwd, url.contentDir);
195
+ urlFn = (file) => getUrl(getSlugs(path.relative(contentDir, file)));
196
+ }
197
+ else {
198
+ urlFn = url;
199
+ }
200
+ function fileContent(index) {
201
+ const generatedPages = context.generated;
202
+ const content = [];
203
+ content.push('<Cards>');
204
+ const files = new Map();
205
+ const only = index.only ?? Object.keys(context.generated);
206
+ for (const item of only) {
207
+ if (typeof item === 'object') {
208
+ files.set(item.path, item);
209
+ continue;
210
+ }
211
+ const result = generatedPages[item];
212
+ if (!result)
213
+ throw new Error(`${item} does not exist on "input", available: ${Object.keys(generatedPages).join(', ')}.`);
214
+ for (const file of result) {
215
+ files.set(file.path, file);
216
+ }
217
+ }
218
+ for (const file of files.values()) {
219
+ const isContent = file.path.endsWith('.mdx') || file.path.endsWith('.md');
220
+ if (!isContent)
221
+ continue;
222
+ const { data } = matter(file.content);
223
+ if (typeof data.title !== 'string')
224
+ continue;
225
+ content.push(`<Card href="${urlFn(file.path)}" title=${JSON.stringify(data.title)} description=${JSON.stringify(data.description)} />`);
226
+ }
227
+ content.push('</Cards>');
228
+ return generateDocument({
229
+ title: index.title ?? 'Overview',
230
+ description: index.description,
231
+ }, content.join('\n'), options);
232
+ }
233
+ const outputDir = path.join(cwd, output);
234
+ for (const item of typeof items === 'function' ? items(context) : items) {
235
+ const outPath = path.join(outputDir, path.extname(item.path).length === 0 ? `${item.path}.mdx` : item.path);
236
+ context.files.push({
237
+ path: outPath,
238
+ content: fileContent(item),
239
+ });
240
+ }
241
+ }
150
242
  function defaultSlugify(s) {
151
243
  return s.replace(/\s+/g, '-').toLowerCase();
152
244
  }
@@ -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
  }
@@ -1 +1 @@
1
- {"version":3,"file":"url.d.ts","sourceRoot":"","sources":["../../src/utils/url.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAEtD,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAM9D;AAED;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAM1D;AAED,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAChC,MAAM,CAMR;AAED,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,MAAM,EAChB,EAAE,IAAI,EAAE,KAAK,EAAE,EAAE,WAAW,GAC3B,MAAM,CAyBR"}
1
+ {"version":3,"file":"url.d.ts","sourceRoot":"","sources":["../../src/utils/url.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAEtD,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAM9D;AAED;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAM1D;AAED,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAChC,MAAM,CAMR;AAED,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,MAAM,EAChB,EAAE,IAAI,EAAE,KAAK,EAAE,EAAE,WAAW,GAC3B,MAAM,CAoCR"}
package/dist/utils/url.js CHANGED
@@ -25,6 +25,7 @@ export function resolveServerUrl(template, variables) {
25
25
  return template;
26
26
  }
27
27
  export function resolveRequestData(pathname, { path, query }) {
28
+ // First, resolve path parameters in the pathname
28
29
  for (const key in path) {
29
30
  const param = path[key];
30
31
  if (Array.isArray(param.value)) {
@@ -34,17 +35,25 @@ export function resolveRequestData(pathname, { path, query }) {
34
35
  pathname = pathname.replace(`{${key}}`, param.value);
35
36
  }
36
37
  }
37
- const searchParams = new URLSearchParams();
38
+ // Check if pathname already contains query parameters (legacy API support)
39
+ const [pathPart, existingQueryString] = pathname.split('?', 2);
40
+ // Parse existing query parameters from the pathname if they exist
41
+ const searchParams = new URLSearchParams(existingQueryString || '');
42
+ // Add new query parameters from the RequestData
38
43
  for (const key in query) {
39
44
  const param = query[key];
40
45
  if (Array.isArray(param.value)) {
46
+ // Remove existing parameter first to avoid duplicates
47
+ searchParams.delete(key);
41
48
  for (const item of param.value) {
42
49
  searchParams.append(key, item);
43
50
  }
44
51
  }
45
52
  else {
46
- searchParams.append(key, param.value);
53
+ // Set (replace if exists) the parameter value
54
+ searchParams.set(key, param.value);
47
55
  }
48
56
  }
49
- return searchParams.size > 0 ? `${pathname}?${searchParams}` : pathname;
57
+ // Return the reconstructed URL
58
+ return searchParams.size > 0 ? `${pathPart}?${searchParams}` : pathPart;
50
59
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fumadocs-openapi",
3
- "version": "9.2.3",
3
+ "version": "9.3.1",
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,8 +65,8 @@
64
65
  "shiki": "^3.11.0",
65
66
  "tinyglobby": "^0.2.14",
66
67
  "xml-js": "^1.6.11",
67
- "fumadocs-core": "15.7.2",
68
- "fumadocs-ui": "15.7.2"
68
+ "fumadocs-core": "15.7.4",
69
+ "fumadocs-ui": "15.7.4"
69
70
  },
70
71
  "devDependencies": {
71
72
  "@scalar/api-client-react": "^1.3.29",
@@ -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
- }