fumadocs-openapi 10.6.5 → 10.6.6

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.
@@ -17,14 +17,17 @@ import ServerSelect from "./components/server-select.js";
17
17
  import { FieldInput, FieldSet, JsonInput, ObjectInput } from "./components/inputs.js";
18
18
  import { ClientCodeBlock } from "../ui/components/codeblock.js";
19
19
  import { useOperationContext } from "../ui/operation/client.js";
20
- import { OauthDialog, OauthDialogTrigger } from "./components/oauth-dialog.js";
20
+ import { useAuth } from "./auth.js";
21
+ import { OAuthDialog, OAuthDialogContent, OAuthDialogTrigger } from "./components/oauth-dialog.js";
22
+ import { Spinner } from "./components/spinner.js";
21
23
  import { Fragment, useEffect, useMemo, useRef, useState } from "react";
22
24
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
23
25
  import { ChevronDown, LoaderCircle } from "lucide-react";
24
26
  import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "fumadocs-ui/components/ui/collapsible";
25
27
  import { buttonVariants } from "fumadocs-ui/components/ui/button";
26
28
  import { StfProvider, useDataEngine, useFieldValue, useListener, useStf } from "@fumari/stf";
27
- import { objectGet, objectSet, stringifyFieldKey } from "@fumari/stf/lib/utils";
29
+ import { arrayStartsWith, objectGet, objectSet, stringifyFieldKey } from "@fumari/stf/lib/utils";
30
+ import { useOnChange } from "fumadocs-core/utils/use-on-change";
28
31
  //#region src/playground/client.tsx
29
32
  function PlaygroundClient({ route, method, securities, doc, proxyUrl, writeOnly, readOnly, ...rest }) {
30
33
  const t = useTranslations();
@@ -51,13 +54,7 @@ function PlaygroundClient({ route, method, securities, doc, proxyUrl, writeOnly,
51
54
  ]);
52
55
  const { example: exampleId, examples, setExampleData } = useOperationContext();
53
56
  const { server } = useServerContext();
54
- const storageKeys = useStorageKey();
55
- const { mediaAdapters, client: { playground: { components: { ResultDisplay = DefaultResultDisplay, CollapsiblePanel = DefaultCollapsiblePanel } = {}, requestTimeout, fetchOptions = { requestTimeout }, transformAuthInputs, renderBodyField } = {} } } = useApiContext();
56
- const [securityId, setSecurityId] = useState(() => {
57
- const idx = securities.findIndex((s) => s.every((entry) => !entry.deprecated));
58
- return idx === -1 ? 0 : idx;
59
- });
60
- const { inputs, mapInputs, initAuthValues } = useAuthInputs(securities[securityId], transformAuthInputs);
57
+ const { mediaAdapters, client: { playground: { components: { ResultDisplay = DefaultResultDisplay, CollapsiblePanel = DefaultCollapsiblePanel } = {}, requestTimeout, fetchOptions = { requestTimeout }, renderBodyField } = {} } } = useApiContext();
61
58
  const defaultValues = useMemo(() => {
62
59
  const requestData = examples.find((example) => example.id === exampleId)?.data;
63
60
  return {
@@ -69,6 +66,7 @@ function PlaygroundClient({ route, method, securities, doc, proxyUrl, writeOnly,
69
66
  };
70
67
  }, [examples, exampleId]);
71
68
  const stf = useStf({ defaultValues });
69
+ const { inputs, requirementId, setRequirementId, mapInputs, initAuthInputs } = useAuthInputs(stf.dataEngine, securities);
72
70
  const testQuery = useQuery(async (input) => {
73
71
  const fetcher = await import("./fetcher.js").then((mod) => mod.createBrowserFetcher(mediaAdapters, {
74
72
  proxyUrl,
@@ -82,23 +80,21 @@ function PlaygroundClient({ route, method, securities, doc, proxyUrl, writeOnly,
82
80
  return fetcher.fetch(joinURL(withBase(server ? resolveServerUrl(server.url, server.variables) : "/", window.location.origin), resolveRequestData(route, encoded)), encoded);
83
81
  });
84
82
  const timerRef = useRef(null);
83
+ const stfSync = useRef(false);
84
+ function triggerExampleUpdate() {
85
+ const data = {
86
+ ...mapInputs(stf.dataEngine.getData()),
87
+ method,
88
+ bodyMediaType: body?.mediaType
89
+ };
90
+ setExampleData(data, encodeRequestData(data, mediaAdapters, parameters));
91
+ }
85
92
  useListener({
86
93
  stf,
87
94
  onUpdate() {
95
+ if (!stfSync.current) return;
88
96
  if (timerRef.current) window.clearTimeout(timerRef.current);
89
- timerRef.current = window.setTimeout(() => {
90
- const values = stf.dataEngine.getData();
91
- for (const item of inputs) {
92
- const value = stf.dataEngine.get(item.fieldName);
93
- if (value) localStorage.setItem(storageKeys.AuthField(item), JSON.stringify(value));
94
- }
95
- const data = {
96
- ...mapInputs(values),
97
- method,
98
- bodyMediaType: body?.mediaType
99
- };
100
- setExampleData(data, encodeRequestData(data, mediaAdapters, parameters));
101
- }, timerRef.current ? 400 : 0);
97
+ timerRef.current = window.setTimeout(triggerExampleUpdate, 400);
102
98
  }
103
99
  });
104
100
  useEffect(() => {
@@ -106,7 +102,13 @@ function PlaygroundClient({ route, method, securities, doc, proxyUrl, writeOnly,
106
102
  stf.dataEngine.reset(defaultValues);
107
103
  }, [defaultValues]);
108
104
  useEffect(() => {
109
- return initAuthValues(stf);
105
+ const reset = initAuthInputs();
106
+ triggerExampleUpdate();
107
+ stfSync.current = true;
108
+ return () => {
109
+ stfSync.current = false;
110
+ reset();
111
+ };
110
112
  }, [defaultValues, inputs]);
111
113
  return /* @__PURE__ */ jsx(StfProvider, {
112
114
  value: stf,
@@ -118,7 +120,7 @@ function PlaygroundClient({ route, method, securities, doc, proxyUrl, writeOnly,
118
120
  ...rest,
119
121
  className: cn("not-prose flex flex-col rounded-xl border shadow-md overflow-hidden bg-fd-card text-fd-card-foreground", rest.className),
120
122
  onSubmit: (e) => {
121
- testQuery.start(mapInputs(stf.dataEngine.getData()));
123
+ testQuery.start(stf.dataEngine.getData());
122
124
  e.preventDefault();
123
125
  },
124
126
  children: [
@@ -146,10 +148,10 @@ function PlaygroundClient({ route, method, securities, doc, proxyUrl, writeOnly,
146
148
  data: testQuery.data,
147
149
  reset: testQuery.reset
148
150
  }) : null,
149
- securities.length > 0 && /* @__PURE__ */ jsx(SecurityTabs, {
151
+ securities.length > 0 && /* @__PURE__ */ jsx(SecurityRequirements, {
150
152
  securities,
151
- securityId,
152
- setSecurityId,
153
+ securityId: requirementId,
154
+ setSecurityId: setRequirementId,
153
155
  children: inputs.map((input) => /* @__PURE__ */ jsx(Fragment, { children: input.children }, stringifyFieldKey(input.fieldName)))
154
156
  }),
155
157
  /* @__PURE__ */ jsx(ParametersForm, { parameters }),
@@ -163,50 +165,62 @@ function PlaygroundClient({ route, method, securities, doc, proxyUrl, writeOnly,
163
165
  })
164
166
  });
165
167
  }
166
- function SecurityTabsSelectItem({ security }) {
167
- return /* @__PURE__ */ jsx("div", {
168
- className: "flex flex-col gap-2 max-w-[600px]",
169
- children: security.map((item) => /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("p", {
170
- className: cn("font-mono font-medium", item.deprecated && "text-fd-muted-foreground line-through"),
171
- children: item.id
172
- }), /* @__PURE__ */ jsx("p", {
173
- className: "text-fd-muted-foreground whitespace-pre-wrap",
174
- children: item.description
175
- })] }, item.id))
176
- });
177
- }
178
- function SecurityTabs({ securities, setSecurityId, securityId, children }) {
179
- const [open, setOpen] = useState(false);
180
- const engine = useDataEngine();
168
+ function SecurityRequirements({ securities, setSecurityId, securityId, children }) {
181
169
  const t = useTranslations();
170
+ const { isLoading, error } = useAuth();
171
+ const defaultOpen = isLoading || error != null;
172
+ const [open, setOpen] = useState(defaultOpen);
182
173
  const { CollapsiblePanel = DefaultCollapsiblePanel } = useApiContext().client.playground?.components ?? {};
183
- const result = /* @__PURE__ */ jsxs(CollapsiblePanel, {
184
- title: t.authorization,
174
+ useOnChange(defaultOpen, () => {
175
+ if (defaultOpen) setOpen(true);
176
+ });
177
+ return /* @__PURE__ */ jsxs(CollapsiblePanel, {
178
+ title: /* @__PURE__ */ jsxs(Fragment$1, { children: [t.authorization, isLoading && /* @__PURE__ */ jsxs("span", {
179
+ className: "border-s ps-2 inline-flex items-center gap-1.5 text-fd-muted-foreground text-xs font-mono",
180
+ children: [
181
+ /* @__PURE__ */ jsx(Spinner, {}),
182
+ " ",
183
+ t.fetchingToken
184
+ ]
185
+ })] }),
185
186
  "data-type": "authorization",
186
- children: [/* @__PURE__ */ jsxs(Select, {
187
- value: securityId.toString(),
188
- onValueChange: (v) => setSecurityId(Number(v)),
189
- children: [/* @__PURE__ */ jsx(SelectTrigger, { children: /* @__PURE__ */ jsx(SelectValue, { children: /* @__PURE__ */ jsx(SecurityTabsSelectItem, { security: securities[securityId] }) }) }), /* @__PURE__ */ jsx(SelectContent, { children: securities.map((security, i) => /* @__PURE__ */ jsx(SelectItem, {
190
- value: i.toString(),
191
- children: /* @__PURE__ */ jsx(SecurityTabsSelectItem, { security })
192
- }, i)) })]
193
- }), children]
187
+ open,
188
+ onOpenChange: setOpen,
189
+ children: [
190
+ error != null && /* @__PURE__ */ jsxs("div", {
191
+ className: "p-2 border rounded-lg bg-fd-secondary",
192
+ children: [/* @__PURE__ */ jsx("p", {
193
+ className: "text-fd-muted-foreground font-medium mb-1",
194
+ children: t.fetchTokenError
195
+ }), /* @__PURE__ */ jsx("p", { children: String(error) })]
196
+ }),
197
+ /* @__PURE__ */ jsxs(Select, {
198
+ value: securityId.toString(),
199
+ onValueChange: (v) => setSecurityId(Number(v)),
200
+ children: [/* @__PURE__ */ jsx(SelectTrigger, { children: /* @__PURE__ */ jsx(SelectValue, { children: /* @__PURE__ */ jsx(SecurityRequirement, { requirement: securities[securityId] }) }) }), /* @__PURE__ */ jsx(SelectContent, { children: securities.map((security, i) => /* @__PURE__ */ jsx(SelectItem, {
201
+ value: i.toString(),
202
+ children: /* @__PURE__ */ jsx(SecurityRequirement, { requirement: security })
203
+ }, i)) })]
204
+ }),
205
+ children
206
+ ]
207
+ });
208
+ }
209
+ function SecurityRequirement({ requirement }) {
210
+ const { schemes } = useApiContext();
211
+ return /* @__PURE__ */ jsx("div", {
212
+ className: "flex flex-col gap-2 max-w-[600px]",
213
+ children: requirement.map((item) => {
214
+ const scheme = schemes[item.id];
215
+ return /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("p", {
216
+ className: cn("font-mono font-medium", scheme.deprecated && "text-fd-muted-foreground line-through"),
217
+ children: item.id
218
+ }), /* @__PURE__ */ jsx("p", {
219
+ className: "text-fd-muted-foreground whitespace-pre-wrap",
220
+ children: scheme.description
221
+ })] }, item.id);
222
+ })
194
223
  });
195
- for (let i = 0; i < securities.length; i++) {
196
- const security = securities[i];
197
- for (const item of security) if (item.type === "oauth2") return /* @__PURE__ */ jsx(OauthDialog, {
198
- scheme: item,
199
- scopes: item.scopes,
200
- open,
201
- setOpen: (v) => {
202
- setOpen(v);
203
- if (v) setSecurityId(i);
204
- },
205
- setToken: (token) => engine.update(["header", "Authorization"], token),
206
- children: result
207
- });
208
- }
209
- return result;
210
224
  }
211
225
  const ParamTypes = [
212
226
  "path",
@@ -287,98 +301,97 @@ function BodyInput({ field: _field }) {
287
301
  })
288
302
  });
289
303
  }
290
- function useAuthInputs(securities, transform) {
304
+ function useAuthInputs(engine, requirements) {
305
+ const authCtx = useAuth();
291
306
  const storageKeys = useStorageKey();
292
307
  const t = useTranslations();
293
- const inputs = useMemo(() => {
294
- const result = [];
295
- if (!securities) return result;
296
- for (const security of securities) if (security.type === "http" && security.scheme === "basic") {
297
- const fieldName = ["header", "Authorization"];
298
- result.push({
299
- fieldName,
300
- original: security,
301
- defaultValue: {
302
- username: "",
303
- password: ""
304
- },
305
- mapOutput(out) {
306
- if (out && typeof out === "object") {
307
- const obj = out;
308
- return `Basic ${btoa(`${obj.username ?? ""}:${obj.password ?? ""}`)}`;
309
- }
310
- return out;
311
- },
312
- children: /* @__PURE__ */ jsx(ObjectInput, {
313
- field: {
314
- type: "object",
315
- properties: {
316
- username: { type: "string" },
317
- password: { type: "string" }
308
+ const { schemes, client: { playground: { transformAuthInputs } = {} } } = useApiContext();
309
+ const [requirementId, setRequirementId] = useState(() => {
310
+ if (requirements.length === 0) return -1;
311
+ const idx = requirements.findIndex((s) => s.every((item) => !schemes[item.id].deprecated));
312
+ return idx !== -1 ? idx : 0;
313
+ });
314
+ const requirement = requirementId === -1 ? null : requirements[requirementId];
315
+ let inputs = useMemo(() => {
316
+ if (!requirement) return [];
317
+ return requirement.map((item) => {
318
+ const scheme = schemes[item.id];
319
+ if (scheme.type === "http" && scheme.scheme === "basic") {
320
+ const fieldName = ["header", "Authorization"];
321
+ return {
322
+ fieldName,
323
+ schemeId: item.id,
324
+ storageKey: storageKeys.AuthField(item.id),
325
+ defaultValue: {
326
+ username: "",
327
+ password: ""
328
+ },
329
+ mapOutput(out) {
330
+ if (out && typeof out === "object") {
331
+ const obj = out;
332
+ return `Basic ${btoa(`${obj.username ?? ""}:${obj.password ?? ""}`)}`;
318
333
  }
334
+ return out;
319
335
  },
320
- fieldName
321
- })
322
- });
323
- } else if (security.type === "oauth2") {
324
- const fieldName = ["header", "Authorization"];
325
- result.push({
326
- fieldName,
327
- original: security,
328
- defaultValue: "Bearer ",
329
- children: /* @__PURE__ */ jsxs("fieldset", {
330
- className: "flex flex-col gap-2",
331
- children: [/* @__PURE__ */ jsx("label", {
332
- htmlFor: stringifyFieldKey(fieldName),
333
- className: cn(labelVariants()),
334
- children: t.accessToken
335
- }), /* @__PURE__ */ jsxs("div", {
336
- className: "flex gap-2",
337
- children: [/* @__PURE__ */ jsx(FieldInput, {
338
- fieldName,
339
- field: { type: "string" },
340
- className: "flex-1"
341
- }), /* @__PURE__ */ jsx(OauthDialogTrigger, {
342
- type: "button",
343
- className: cn(buttonVariants({
344
- size: "sm",
345
- color: "secondary"
346
- })),
347
- children: t.authorize
348
- })]
349
- })]
350
- })
351
- });
352
- } else if (security.type === "http") {
353
- const fieldName = ["header", "Authorization"];
354
- result.push({
355
- fieldName,
356
- original: security,
357
- defaultValue: "Bearer ",
358
- children: /* @__PURE__ */ jsx(FieldSet, {
359
- name: `${t.authorization} (${t.header})`,
336
+ children: /* @__PURE__ */ jsx(ObjectInput, {
337
+ field: {
338
+ type: "object",
339
+ properties: {
340
+ username: { type: "string" },
341
+ password: { type: "string" }
342
+ }
343
+ },
344
+ fieldName
345
+ })
346
+ };
347
+ }
348
+ if (scheme.type === "oauth2") {
349
+ const fieldName = ["header", "Authorization"];
350
+ return {
360
351
  fieldName,
361
- field: { type: "string" }
362
- })
363
- });
364
- } else if (security.type === "apiKey") {
365
- const fieldName = [security.in, security.name];
366
- result.push({
367
- fieldName,
368
- defaultValue: "",
369
- original: security,
370
- children: /* @__PURE__ */ jsx(FieldSet, {
352
+ schemeId: item.id,
353
+ storageKey: storageKeys.AuthField(item.id),
354
+ defaultValue: "Bearer ",
355
+ children: /* @__PURE__ */ jsx(OAuth2Input, {
356
+ fieldName,
357
+ security: item
358
+ })
359
+ };
360
+ }
361
+ if (scheme.type === "http") {
362
+ const fieldName = ["header", "Authorization"];
363
+ return {
371
364
  fieldName,
372
- name: `${security.name} (${security.in})`,
373
- field: { type: "string" }
374
- })
375
- });
376
- } else {
365
+ schemeId: item.id,
366
+ storageKey: storageKeys.AuthField(item.id),
367
+ defaultValue: "Bearer ",
368
+ children: /* @__PURE__ */ jsx(FieldSet, {
369
+ name: `${t.authorization} (${t.header})`,
370
+ fieldName,
371
+ field: { type: "string" }
372
+ })
373
+ };
374
+ }
375
+ if (scheme.type === "apiKey") {
376
+ const fieldName = [scheme.in, scheme.name];
377
+ return {
378
+ fieldName,
379
+ schemeId: item.id,
380
+ defaultValue: "",
381
+ storageKey: storageKeys.AuthField(item.id),
382
+ children: /* @__PURE__ */ jsx(FieldSet, {
383
+ fieldName,
384
+ name: `${scheme.name} (${scheme.in})`,
385
+ field: { type: "string" }
386
+ })
387
+ };
388
+ }
377
389
  const fieldName = ["header", "Authorization"];
378
- result.push({
390
+ return {
379
391
  fieldName,
392
+ schemeId: item.id,
380
393
  defaultValue: "",
381
- original: security,
394
+ storageKey: storageKeys.AuthField(item.id),
382
395
  children: /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(FieldSet, {
383
396
  name: `${t.authorization} (${t.header})`,
384
397
  fieldName,
@@ -387,45 +400,106 @@ function useAuthInputs(securities, transform) {
387
400
  className: "text-fd-muted-foreground text-xs",
388
401
  children: t.openIdUnsupported
389
402
  })] })
390
- });
391
- }
392
- return transform ? transform(result) : result;
403
+ };
404
+ });
393
405
  }, [
394
- securities,
395
- transform,
406
+ requirement,
407
+ storageKeys,
408
+ schemes,
396
409
  t
397
410
  ]);
398
- const mapInputs = (values) => {
399
- const cloned = structuredClone(values);
400
- for (const item of inputs) {
401
- if (!item.mapOutput) continue;
402
- objectSet(cloned, item.fieldName, item.mapOutput(objectGet(cloned, item.fieldName)));
403
- }
404
- return cloned;
405
- };
406
- const initAuthValues = (stf) => {
407
- const { dataEngine } = stf;
408
- for (const item of inputs) {
409
- const stored = localStorage.getItem(storageKeys.AuthField(item));
410
- if (stored) {
411
- const parsed = JSON.parse(stored);
412
- if (typeof parsed === typeof item.defaultValue) {
413
- dataEngine.init(item.fieldName, parsed);
414
- continue;
415
- }
411
+ if (transformAuthInputs) inputs = transformAuthInputs(inputs);
412
+ useListener({
413
+ stf: engine,
414
+ onUpdate(key) {
415
+ for (const item of inputs) {
416
+ if (!arrayStartsWith(item.fieldName, key)) continue;
417
+ const value = engine.get(item.fieldName);
418
+ if (value != null) localStorage.setItem(item.storageKey, JSON.stringify(value));
416
419
  }
417
- dataEngine.init(item.fieldName, item.defaultValue);
418
420
  }
419
- return () => {
420
- for (const item of inputs) stf.dataEngine.delete(item.fieldName);
421
- };
422
- };
421
+ });
422
+ useOnChange(authCtx.updatedSchemeId, () => {
423
+ const { updatedSchemeId } = authCtx;
424
+ if (!updatedSchemeId) return;
425
+ const { token } = authCtx.store[updatedSchemeId];
426
+ const input = inputs.find((input) => input.schemeId === updatedSchemeId);
427
+ if (input) {
428
+ engine.update(input.fieldName, token);
429
+ return;
430
+ }
431
+ const idx = requirements.findIndex((requirement) => requirement.some((item) => item.id === updatedSchemeId));
432
+ if (idx !== -1) {
433
+ localStorage.setItem(storageKeys.AuthField(updatedSchemeId), JSON.stringify(token));
434
+ setRequirementId(idx);
435
+ }
436
+ });
423
437
  return {
424
438
  inputs,
425
- mapInputs,
426
- initAuthValues
439
+ requirementId,
440
+ setRequirementId,
441
+ mapInputs(values) {
442
+ const cloned = structuredClone(values);
443
+ for (const item of inputs) {
444
+ if (!item.mapOutput) continue;
445
+ objectSet(cloned, item.fieldName, item.mapOutput(objectGet(cloned, item.fieldName)));
446
+ }
447
+ return cloned;
448
+ },
449
+ initAuthInputs() {
450
+ for (const item of inputs) {
451
+ const stored = localStorage.getItem(item.storageKey);
452
+ if (stored) {
453
+ const parsed = JSON.parse(stored);
454
+ if (typeof parsed === typeof item.defaultValue) {
455
+ engine.init(item.fieldName, parsed);
456
+ continue;
457
+ }
458
+ }
459
+ engine.init(item.fieldName, item.defaultValue);
460
+ }
461
+ return () => {
462
+ for (const item of inputs) engine.delete(item.fieldName);
463
+ };
464
+ }
427
465
  };
428
466
  }
467
+ function OAuth2Input({ fieldName, security }) {
468
+ const [open, setOpen] = useState(false);
469
+ const engine = useDataEngine();
470
+ const t = useTranslations();
471
+ return /* @__PURE__ */ jsxs("fieldset", {
472
+ className: "flex flex-col gap-2",
473
+ children: [/* @__PURE__ */ jsx("label", {
474
+ htmlFor: stringifyFieldKey(fieldName),
475
+ className: cn(labelVariants()),
476
+ children: t.accessToken
477
+ }), /* @__PURE__ */ jsxs("div", {
478
+ className: "flex gap-2",
479
+ children: [/* @__PURE__ */ jsx(FieldInput, {
480
+ fieldName,
481
+ field: { type: "string" },
482
+ className: "flex-1"
483
+ }), /* @__PURE__ */ jsxs(OAuthDialog, {
484
+ open,
485
+ onOpenChange: setOpen,
486
+ children: [/* @__PURE__ */ jsx(OAuthDialogTrigger, {
487
+ type: "button",
488
+ className: cn(buttonVariants({
489
+ size: "sm",
490
+ color: "secondary"
491
+ })),
492
+ children: t.authorize
493
+ }), /* @__PURE__ */ jsx(OAuthDialogContent, {
494
+ setOpen,
495
+ schemeId: security.id,
496
+ scopes: security.scopes,
497
+ setToken: (token) => engine.update(["header", "Authorization"], token)
498
+ })]
499
+ })]
500
+ })]
501
+ });
502
+ }
429
503
  function Route({ route, ...props }) {
430
504
  return /* @__PURE__ */ jsx("div", {
431
505
  ...props,