fumadocs-openapi 3.3.0 → 4.0.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.
@@ -0,0 +1,115 @@
1
+ // src/ui/shared.ts
2
+ import { cva } from "class-variance-authority";
3
+ var badgeVariants = cva(
4
+ "rounded border px-1.5 py-1 text-xs font-medium leading-[12px]",
5
+ {
6
+ variants: {
7
+ color: {
8
+ green: "border-green-400/50 bg-green-400/20 text-green-600 dark:text-green-400",
9
+ yellow: "border-yellow-400/50 bg-yellow-400/20 text-yellow-600 dark:text-yellow-400",
10
+ red: "border-red-400/50 bg-red-400/20 text-red-600 dark:text-red-400",
11
+ blue: "border-blue-400/50 bg-blue-400/20 text-blue-600 dark:text-blue-400",
12
+ orange: "border-orange-400/50 bg-orange-400/20 text-orange-600 dark:text-orange-400"
13
+ }
14
+ }
15
+ }
16
+ );
17
+ function getBadgeColor(method) {
18
+ switch (method) {
19
+ case "PUT":
20
+ return "yellow";
21
+ case "PATCH":
22
+ return "orange";
23
+ case "POST":
24
+ return "blue";
25
+ case "DELETE":
26
+ return "red";
27
+ default:
28
+ return "green";
29
+ }
30
+ }
31
+ function getDefaultValue(item, references) {
32
+ if (item.type === "object")
33
+ return Object.fromEntries(
34
+ Object.entries(item.properties).map(([key, prop]) => [
35
+ key,
36
+ getDefaultValue(references[prop.schema], references)
37
+ ])
38
+ );
39
+ if (item.type === "array") return [];
40
+ if (item.type === "null") return null;
41
+ if (item.type === "switcher")
42
+ return getDefaultValue(
43
+ resolve(Object.values(item.items)[0], references),
44
+ references
45
+ );
46
+ return String(item.defaultValue);
47
+ }
48
+ function getDefaultValues(field, context) {
49
+ return Object.fromEntries(
50
+ field.map((p) => [p.name, getDefaultValue(p, context)])
51
+ );
52
+ }
53
+ function resolve(schema, references) {
54
+ if (typeof schema === "string") return references[schema];
55
+ if (schema.type !== "ref") return schema;
56
+ return {
57
+ ...references[schema.schema],
58
+ description: schema.description,
59
+ isRequired: schema.isRequired
60
+ };
61
+ }
62
+
63
+ // src/ui/contexts/api.tsx
64
+ import { createContext, useContext, useEffect, useState } from "react";
65
+ import { jsx } from "react/jsx-runtime";
66
+ var ApiContext = createContext({
67
+ baseUrl: void 0,
68
+ setBaseUrl: () => void 0,
69
+ highlighter: null
70
+ });
71
+ function useApiContext() {
72
+ return useContext(ApiContext);
73
+ }
74
+ async function initHighlighter() {
75
+ const { createHighlighterCore } = await import("shiki/core");
76
+ const getWasm = await import("shiki/wasm");
77
+ return createHighlighterCore({
78
+ themes: [
79
+ import("shiki/themes/github-light.mjs"),
80
+ import("shiki/themes/github-dark.mjs")
81
+ ],
82
+ langs: [import("shiki/langs/json.mjs")],
83
+ loadWasm: getWasm
84
+ });
85
+ }
86
+ var highlighterInstance;
87
+ function ApiProvider({
88
+ defaultBaseUrl,
89
+ children
90
+ }) {
91
+ const [highlighter, setHighlighter] = useState(null);
92
+ const [baseUrl, setBaseUrl] = useState(defaultBaseUrl);
93
+ useEffect(() => {
94
+ setBaseUrl((prev) => localStorage.getItem("apiBaseUrl") ?? prev);
95
+ if (highlighterInstance) setHighlighter(highlighterInstance);
96
+ else
97
+ void initHighlighter().then((res) => {
98
+ setHighlighter(res);
99
+ });
100
+ }, []);
101
+ useEffect(() => {
102
+ if (baseUrl) localStorage.setItem("apiBaseUrl", baseUrl);
103
+ }, [baseUrl]);
104
+ return /* @__PURE__ */ jsx(ApiContext.Provider, { value: { baseUrl, setBaseUrl, highlighter }, children });
105
+ }
106
+
107
+ export {
108
+ badgeVariants,
109
+ getBadgeColor,
110
+ getDefaultValue,
111
+ getDefaultValues,
112
+ resolve,
113
+ useApiContext,
114
+ ApiProvider
115
+ };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  import { OpenAPIV3 } from 'openapi-types';
2
+ import { A as APIPlaygroundProps } from './playground-D8pYn13F.js';
3
+ export { P as PrimitiveRequestField, a as ReferenceSchema, R as RequestSchema } from './playground-D8pYn13F.js';
2
4
 
3
5
  interface ResponsesProps {
4
6
  items: string[];
@@ -24,8 +26,11 @@ interface RequestProps {
24
26
  name: string;
25
27
  code: string;
26
28
  }
29
+ interface RootProps {
30
+ baseUrl?: string;
31
+ }
27
32
  interface Renderer {
28
- Root: (child: string[]) => string;
33
+ Root: (props: RootProps, child: string[]) => string;
29
34
  API: (child: string[]) => string;
30
35
  APIInfo: (props: APIInfoProps, child: string[]) => string;
31
36
  APIExample: (child: string[]) => string;
@@ -41,7 +46,9 @@ interface Renderer {
41
46
  */
42
47
  ObjectCollapsible: (props: ObjectCollapsibleProps, child: string[]) => string;
43
48
  Property: (props: PropertyProps, child: string[]) => string;
49
+ APIPlayground: (props: APIPlaygroundProps) => string;
44
50
  }
51
+
45
52
  declare const defaultRenderer: Renderer;
46
53
 
47
54
  interface CodeSample {
@@ -147,4 +154,4 @@ declare function generateFiles({ input, output, name: nameFn, per, cwd, ...optio
147
154
 
148
155
  declare function createElement(name: string, props: object, ...child: string[]): string;
149
156
 
150
- export { type APIInfoProps, type Config, type GenerateOperationOutput, type GenerateOptions, type GenerateTagOutput, type MethodInformation, type ObjectCollapsibleProps, type PropertyProps, type RenderContext, type Renderer, type RequestProps, type ResponseProps, type ResponsesProps, type RouteInformation, createElement, defaultRenderer, generate, generateFiles, generateOperations, generateTags };
157
+ export { type APIInfoProps, APIPlaygroundProps, type Config, type GenerateOperationOutput, type GenerateOptions, type GenerateTagOutput, type MethodInformation, type ObjectCollapsibleProps, type PropertyProps, type RenderContext, type Renderer, type RequestProps, type ResponseProps, type ResponsesProps, type RootProps, type RouteInformation, createElement, defaultRenderer, generate, generateFiles, generateOperations, generateTags };
package/dist/index.js CHANGED
@@ -46,7 +46,7 @@ function buildOperation(method, operation) {
46
46
  };
47
47
  }
48
48
 
49
- // src/render/page.ts
49
+ // src/utils/generate-document.ts
50
50
  import { dump } from "js-yaml";
51
51
 
52
52
  // src/render/element.ts
@@ -78,7 +78,7 @@ function heading(depth, child) {
78
78
 
79
79
  // src/render/renderer.ts
80
80
  var defaultRenderer = {
81
- Root: (child) => createElement("Root", {}, ...child),
81
+ Root: (props, child) => createElement("Root", props, ...child),
82
82
  API: (child) => createElement("API", {}, ...child),
83
83
  APIInfo: (props, child) => createElement("APIInfo", props, ...child),
84
84
  APIExample: (child) => createElement("APIExample", {}, ...child),
@@ -94,11 +94,12 @@ var defaultRenderer = {
94
94
  Property: (props, child) => createElement("Property", props, ...child),
95
95
  ObjectCollapsible: (props, child) => createElement("ObjectCollapsible", props, ...child),
96
96
  Requests: (items, child) => createElement("Requests", { items }, ...child),
97
- Request: ({ language, code, name }) => createElement("Request", { value: name }, codeblock({ language }, code))
97
+ Request: ({ language, code, name }) => createElement("Request", { value: name }, codeblock({ language }, code)),
98
+ APIPlayground: (props) => createElement("APIPlayground", props)
98
99
  };
99
100
 
100
- // src/render/page.ts
101
- function renderPage(title, description, content, options) {
101
+ // src/utils/generate-document.ts
102
+ function generateDocument(title, description, content, options) {
102
103
  const banner = dump({
103
104
  title,
104
105
  description,
@@ -108,19 +109,30 @@ function renderPage(title, description, content, options) {
108
109
  const finalImports = (options.imports ?? [
109
110
  {
110
111
  names: Object.keys(defaultRenderer),
111
- from: "fumadocs-ui/components/api"
112
+ from: "fumadocs-openapi/ui"
112
113
  }
113
114
  ]).map(
114
115
  (item) => `import { ${item.names.join(", ")} } from ${JSON.stringify(item.from)};`
115
116
  ).join("\n");
116
- const Root = options.renderer?.Root ?? defaultRenderer.Root;
117
117
  return `---
118
118
  ${banner}
119
119
  ---
120
120
 
121
121
  ${finalImports}
122
122
 
123
- ${Root(content)}`;
123
+ ${content}`;
124
+ }
125
+
126
+ // src/utils/id-to-title.ts
127
+ function idToTitle(id) {
128
+ const result = [];
129
+ for (const c of id) {
130
+ if (result.length === 0) result.push(c.toLocaleUpperCase());
131
+ else if (/^[A-Z]$/.test(c) && result.at(-1) !== " ") result.push(" ", c);
132
+ else if (c === "-") result.push(" ");
133
+ else result.push(c);
134
+ }
135
+ return result.join("");
124
136
  }
125
137
 
126
138
  // src/utils/schema.ts
@@ -266,6 +278,160 @@ async function getTypescriptSchema(endpoint, code) {
266
278
  }
267
279
  }
268
280
 
281
+ // src/utils/get-security.ts
282
+ function getScheme(requirement, document) {
283
+ const results = [];
284
+ const schemas = document.components?.securitySchemes ?? {};
285
+ for (const [key, scopes] of Object.entries(requirement)) {
286
+ if (!(key in schemas)) return [];
287
+ const schema = noRef(schemas[key]);
288
+ results.push({
289
+ ...schema,
290
+ scopes
291
+ });
292
+ }
293
+ return results;
294
+ }
295
+
296
+ // src/render/playground.ts
297
+ function renderPlayground(path, method, ctx) {
298
+ let currentId = 0;
299
+ const context = {
300
+ schema: {},
301
+ nextId() {
302
+ return String(currentId++);
303
+ },
304
+ registered: /* @__PURE__ */ new WeakMap()
305
+ };
306
+ const body = method.requestBody ? getPreferredMedia(noRef(method.requestBody).content) : void 0;
307
+ return ctx.renderer.APIPlayground({
308
+ authorization: getAuthorizationField(method, ctx),
309
+ method: method.method,
310
+ route: path,
311
+ path: method.parameters.filter((v) => v.in === "path").map((v) => parameterToField(v, context)),
312
+ query: method.parameters.filter((v) => v.in === "query").map((v) => parameterToField(v, context)),
313
+ header: method.parameters.filter((v) => v.in === "header").map((v) => parameterToField(v, context)),
314
+ body: body?.schema ? toSchema(noRef(body.schema), true, context) : void 0,
315
+ schemas: context.schema
316
+ });
317
+ }
318
+ function getAuthorizationField(method, ctx) {
319
+ const security = method.security ?? ctx.document.security ?? [];
320
+ if (security.length === 0) return;
321
+ const singular = security.find(
322
+ (requirements) => Object.keys(requirements).length === 1
323
+ );
324
+ if (!singular) return;
325
+ const scheme = getScheme(singular, ctx.document)[0];
326
+ return {
327
+ type: "string",
328
+ name: "Authorization",
329
+ defaultValue: scheme.type === "oauth2" || scheme.type === "http" && scheme.scheme === "bearer" ? "Bearer" : "Basic",
330
+ isRequired: security.every(
331
+ (requirements) => Object.keys(requirements).length > 0
332
+ ),
333
+ description: "The Authorization access token"
334
+ };
335
+ }
336
+ function getIdFromSchema(schema, required, ctx) {
337
+ const registered = ctx.registered.get(schema);
338
+ if (registered === void 0) {
339
+ const id = ctx.nextId();
340
+ ctx.registered.set(schema, id);
341
+ ctx.schema[id] = toSchema(schema, required, ctx);
342
+ return id;
343
+ }
344
+ return registered;
345
+ }
346
+ function parameterToField(v, ctx) {
347
+ return {
348
+ name: v.name,
349
+ ...toSchema(
350
+ noRef(v.schema) ?? { type: "string" },
351
+ v.required ?? false,
352
+ ctx
353
+ )
354
+ };
355
+ }
356
+ function toReference(schema, required, ctx) {
357
+ return {
358
+ type: "ref",
359
+ isRequired: required,
360
+ schema: getIdFromSchema(schema, false, ctx)
361
+ };
362
+ }
363
+ function toSchema(schema, required, ctx) {
364
+ if (schema.type === "array") {
365
+ return {
366
+ type: "array",
367
+ description: schema.description ?? schema.title,
368
+ isRequired: required,
369
+ items: getIdFromSchema(noRef(schema.items), false, ctx)
370
+ };
371
+ }
372
+ if (schema.type === "object" || schema.properties !== void 0 || schema.allOf !== void 0) {
373
+ const properties = {};
374
+ Object.entries(schema.properties ?? {}).forEach(([key, prop]) => {
375
+ properties[key] = toReference(
376
+ noRef(prop),
377
+ schema.required?.includes(key) ?? false,
378
+ ctx
379
+ );
380
+ });
381
+ schema.allOf?.forEach((c) => {
382
+ const field = toSchema(noRef(c), true, ctx);
383
+ if (field.type === "object") Object.assign(properties, field.properties);
384
+ });
385
+ const additional = noRef(schema.additionalProperties);
386
+ let additionalProperties;
387
+ if (additional && typeof additional === "object") {
388
+ if (!additional.type && !additional.anyOf && !additional.allOf && !additional.oneOf) {
389
+ additionalProperties = true;
390
+ } else {
391
+ additionalProperties = getIdFromSchema(additional, false, ctx);
392
+ }
393
+ } else {
394
+ additionalProperties = additional;
395
+ }
396
+ return {
397
+ type: "object",
398
+ isRequired: required,
399
+ description: schema.description ?? schema.title,
400
+ properties,
401
+ additionalProperties
402
+ };
403
+ }
404
+ if (schema.type === void 0) {
405
+ const combine = schema.anyOf ?? schema.oneOf;
406
+ if (combine) {
407
+ return {
408
+ type: "switcher",
409
+ description: schema.description ?? schema.title,
410
+ items: Object.fromEntries(
411
+ combine.map((c, idx) => {
412
+ const item = noRef(c);
413
+ return [
414
+ item.title ?? item.type ?? `Item ${idx.toString()}`,
415
+ toReference(item, true, ctx)
416
+ ];
417
+ })
418
+ ),
419
+ isRequired: required
420
+ };
421
+ }
422
+ return {
423
+ type: "null",
424
+ isRequired: false
425
+ };
426
+ }
427
+ return {
428
+ type: schema.type === "integer" ? "number" : schema.type,
429
+ defaultValue: schema.example ?? "",
430
+ isRequired: required,
431
+ description: schema.description ?? schema.title
432
+ };
433
+ }
434
+
269
435
  // src/render/schema.ts
270
436
  var keys = {
271
437
  example: "Example",
@@ -278,7 +444,7 @@ var keys = {
278
444
  format: "Format"
279
445
  };
280
446
  function isObject(schema) {
281
- return schema.type === "object" || schema.properties !== void 0;
447
+ return schema.type === "object" || schema.properties !== void 0 || schema.additionalProperties !== void 0;
282
448
  }
283
449
  function schemaElement(name, schema, ctx) {
284
450
  return render(name, schema, {
@@ -348,9 +514,19 @@ function render(name, schema, ctx) {
348
514
  })
349
515
  ])
350
516
  );
517
+ } else if (schema.allOf) {
518
+ child.push(
519
+ renderer.ObjectCollapsible({ name }, [
520
+ render(name, combineSchema(schema.allOf.map(noRef)), {
521
+ ...ctx,
522
+ parseObject: true,
523
+ required: false
524
+ })
525
+ ])
526
+ );
351
527
  } else {
352
528
  const mentionedObjectTypes = [
353
- ...schema.anyOf ?? schema.oneOf ?? schema.allOf ?? [],
529
+ ...schema.anyOf ?? schema.oneOf ?? [],
354
530
  ...schema.not ? [schema.not] : [],
355
531
  ...schema.type === "array" ? [schema.items] : []
356
532
  ].map(noRef).filter((s) => isComplexType(s) && !ctx.stack.includes(s));
@@ -381,6 +557,28 @@ function render(name, schema, ctx) {
381
557
  child
382
558
  );
383
559
  }
560
+ function combineSchema(schema) {
561
+ const result = {
562
+ type: "object"
563
+ };
564
+ function add(s) {
565
+ result.properties ??= {};
566
+ if (s.properties) {
567
+ Object.assign(result.properties, s.properties);
568
+ }
569
+ result.additionalProperties ??= {};
570
+ if (s.additionalProperties === true) {
571
+ result.additionalProperties = true;
572
+ } else if (s.additionalProperties && typeof result.additionalProperties !== "boolean") {
573
+ Object.assign(result.additionalProperties, s.additionalProperties);
574
+ }
575
+ if (s.allOf) {
576
+ add(combineSchema(s.allOf.map(noRef)));
577
+ }
578
+ }
579
+ schema.forEach(add);
580
+ return result;
581
+ }
384
582
  function isComplexType(schema) {
385
583
  if (schema.anyOf ?? schema.oneOf ?? schema.allOf) return true;
386
584
  return isObject(schema) || schema.type === "array";
@@ -413,9 +611,13 @@ async function renderOperation(path, method, ctx, noTitle = false) {
413
611
  const security = method.security ?? ctx.document.security;
414
612
  const info = [];
415
613
  const example = [];
416
- const title = method.summary ?? method.operationId;
417
- if (title && !noTitle) {
418
- info.push(heading(level, title));
614
+ if (!noTitle) {
615
+ info.push(
616
+ heading(
617
+ level,
618
+ method.summary ?? (method.operationId ? idToTitle(method.operationId) : path)
619
+ )
620
+ );
419
621
  level++;
420
622
  }
421
623
  if (method.description) info.push(p(method.description));
@@ -474,6 +676,7 @@ async function renderOperation(path, method, ctx, noTitle = false) {
474
676
  info.push(heading(level, group), ...parameters);
475
677
  }
476
678
  info.push(getResponseTable(method));
679
+ info.push(renderPlayground(path, method, ctx));
477
680
  const samples = dedupe([
478
681
  {
479
682
  label: "cURL",
@@ -518,12 +721,9 @@ function dedupe(samples) {
518
721
  }
519
722
  function getAuthSection(requirements, { document, renderer }) {
520
723
  const info = [];
521
- const schemas = document.components?.securitySchemes ?? {};
522
724
  for (const requirement of requirements) {
523
725
  if (info.length > 0) info.push(`---`);
524
- for (const [name, scopes] of Object.entries(requirement)) {
525
- if (!(name in schemas)) continue;
526
- const schema = noRef(schemas[name]);
726
+ for (const schema of getScheme(requirement, document)) {
527
727
  if (schema.type === "http") {
528
728
  info.push(
529
729
  renderer.Property(
@@ -550,7 +750,7 @@ function getAuthSection(requirements, { document, renderer }) {
550
750
  [
551
751
  p(schema.description),
552
752
  `In: \`header\``,
553
- `Scope: \`${scopes.length > 0 ? scopes.join(", ") : "none"}\``
753
+ `Scope: \`${schema.scopes.length > 0 ? schema.scopes.join(", ") : "none"}\``
554
754
  ]
555
755
  )
556
756
  );
@@ -632,10 +832,10 @@ async function generate(pathOrDocument, options = {}) {
632
832
  child.push(await renderOperation(route.path, method, ctx));
633
833
  }
634
834
  }
635
- return renderPage(
835
+ return generateDocument(
636
836
  document.info.title,
637
837
  document.info.description,
638
- child,
838
+ ctx.renderer.Root({ baseUrl: ctx.baseUrl }, child),
639
839
  options
640
840
  );
641
841
  }
@@ -646,10 +846,12 @@ async function generateOperations(pathOrDocument, options = {}) {
646
846
  return await Promise.all(
647
847
  routes.flatMap((route) => {
648
848
  return route.methods.map(async (method) => {
649
- const content = renderPage(
849
+ const content = generateDocument(
650
850
  method.summary ?? method.method,
651
851
  method.description,
652
- [await renderOperation(route.path, method, ctx, true)],
852
+ ctx.renderer.Root({ baseUrl: ctx.baseUrl }, [
853
+ await renderOperation(route.path, method, ctx, true)
854
+ ]),
653
855
  options
654
856
  );
655
857
  if (!method.operationId)
@@ -677,7 +879,12 @@ async function generateTags(pathOrDocument, options = {}) {
677
879
  }
678
880
  return {
679
881
  tag,
680
- content: renderPage(tag, info?.description, child, options)
882
+ content: generateDocument(
883
+ idToTitle(tag),
884
+ info?.description,
885
+ ctx.renderer.Root({ baseUrl: ctx.baseUrl }, child),
886
+ options
887
+ )
681
888
  };
682
889
  })
683
890
  );
@@ -694,8 +901,8 @@ function getContext(document, options) {
694
901
  }
695
902
 
696
903
  // src/generate-file.ts
697
- import { mkdir, writeFile } from "node:fs/promises";
698
- import { dirname, join, parse } from "node:path";
904
+ import { mkdir, writeFile } from "fs/promises";
905
+ import { dirname, join, parse } from "path";
699
906
  import fg from "fast-glob";
700
907
  async function generateFiles({
701
908
  input,
@@ -0,0 +1,52 @@
1
+ interface BaseRequestField {
2
+ name: string;
3
+ description?: string;
4
+ }
5
+ interface BaseSchema {
6
+ description?: string;
7
+ isRequired: boolean;
8
+ }
9
+ type PrimitiveRequestField = BaseRequestField & PrimitiveSchema;
10
+ interface PrimitiveSchema extends BaseSchema {
11
+ type: 'boolean' | 'string' | 'number';
12
+ defaultValue: string;
13
+ }
14
+ interface ReferenceSchema extends BaseSchema {
15
+ type: 'ref';
16
+ schema: string;
17
+ }
18
+ interface ArraySchema extends BaseSchema {
19
+ type: 'array';
20
+ /**
21
+ * Reference to item schema or the schema
22
+ */
23
+ items: string | RequestSchema;
24
+ }
25
+ interface ObjectSchema extends BaseSchema {
26
+ type: 'object';
27
+ properties: Record<string, ReferenceSchema>;
28
+ /**
29
+ * Reference to schema, or true if it's `any`
30
+ */
31
+ additionalProperties?: boolean | string;
32
+ }
33
+ interface SwitcherSchema extends BaseSchema {
34
+ type: 'switcher';
35
+ items: Record<string, ReferenceSchema | RequestSchema>;
36
+ }
37
+ interface NullSchema extends BaseSchema {
38
+ type: 'null';
39
+ }
40
+ type RequestSchema = PrimitiveSchema | ArraySchema | ObjectSchema | SwitcherSchema | NullSchema;
41
+ interface APIPlaygroundProps {
42
+ route: string;
43
+ method: string;
44
+ authorization?: PrimitiveRequestField;
45
+ path?: PrimitiveRequestField[];
46
+ query?: PrimitiveRequestField[];
47
+ header?: PrimitiveRequestField[];
48
+ body?: RequestSchema;
49
+ schemas: Record<string, RequestSchema>;
50
+ }
51
+
52
+ export type { APIPlaygroundProps as A, PrimitiveRequestField as P, RequestSchema as R, ReferenceSchema as a };