zudoku 0.35.6 → 0.37.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.
Files changed (190) hide show
  1. package/README.md +1 -1
  2. package/dist/app/entry.server.js +5 -1
  3. package/dist/app/entry.server.js.map +1 -1
  4. package/dist/config/validators/common.d.ts +428 -428
  5. package/dist/config/validators/common.js +12 -7
  6. package/dist/config/validators/common.js.map +1 -1
  7. package/dist/config/validators/validate.d.ts +158 -158
  8. package/dist/lib/components/InlineCode.d.ts +2 -1
  9. package/dist/lib/components/InlineCode.js +1 -1
  10. package/dist/lib/components/InlineCode.js.map +1 -1
  11. package/dist/lib/components/Layout.js +3 -14
  12. package/dist/lib/components/Layout.js.map +1 -1
  13. package/dist/lib/components/MobileTopNavigation.js +1 -1
  14. package/dist/lib/components/MobileTopNavigation.js.map +1 -1
  15. package/dist/lib/components/TopNavigation.d.ts +2 -2
  16. package/dist/lib/components/TopNavigation.js +9 -12
  17. package/dist/lib/components/TopNavigation.js.map +1 -1
  18. package/dist/lib/components/Zudoku.js +3 -1
  19. package/dist/lib/components/Zudoku.js.map +1 -1
  20. package/dist/lib/components/cache.d.ts +7 -0
  21. package/dist/lib/components/cache.js +7 -0
  22. package/dist/lib/components/cache.js.map +1 -1
  23. package/dist/lib/components/context/ViewportAnchorContext.js +3 -6
  24. package/dist/lib/components/context/ViewportAnchorContext.js.map +1 -1
  25. package/dist/lib/components/context/ZudokuContext.d.ts +2 -2
  26. package/dist/lib/components/context/ZudokuContext.js +13 -7
  27. package/dist/lib/components/context/ZudokuContext.js.map +1 -1
  28. package/dist/lib/components/navigation/SidebarCategory.d.ts +2 -2
  29. package/dist/lib/components/navigation/SidebarCategory.js +10 -6
  30. package/dist/lib/components/navigation/SidebarCategory.js.map +1 -1
  31. package/dist/lib/components/navigation/SidebarItem.js +2 -2
  32. package/dist/lib/components/navigation/SidebarItem.js.map +1 -1
  33. package/dist/lib/components/navigation/SidebarWrapper.js +1 -1
  34. package/dist/lib/components/navigation/SidebarWrapper.js.map +1 -1
  35. package/dist/lib/core/ZudokuContext.d.ts +8 -6
  36. package/dist/lib/core/ZudokuContext.js +4 -2
  37. package/dist/lib/core/ZudokuContext.js.map +1 -1
  38. package/dist/lib/core/plugins.d.ts +3 -3
  39. package/dist/lib/hooks/useEvent.test.js +1 -1
  40. package/dist/lib/hooks/useEvent.test.js.map +1 -1
  41. package/dist/lib/oas/graphql/index.d.ts +13 -2
  42. package/dist/lib/oas/graphql/index.js +59 -39
  43. package/dist/lib/oas/graphql/index.js.map +1 -1
  44. package/dist/lib/plugins/openapi/OperationList.js +19 -5
  45. package/dist/lib/plugins/openapi/OperationList.js.map +1 -1
  46. package/dist/lib/plugins/openapi/OperationListItem.js +1 -1
  47. package/dist/lib/plugins/openapi/OperationListItem.js.map +1 -1
  48. package/dist/lib/plugins/openapi/ParamInfos.js +12 -4
  49. package/dist/lib/plugins/openapi/ParamInfos.js.map +1 -1
  50. package/dist/lib/plugins/openapi/ParameterListItem.js +1 -1
  51. package/dist/lib/plugins/openapi/ParameterListItem.js.map +1 -1
  52. package/dist/lib/plugins/openapi/Sidecar.js +2 -2
  53. package/dist/lib/plugins/openapi/Sidecar.js.map +1 -1
  54. package/dist/lib/plugins/openapi/client/useCreateQuery.d.ts +1 -1
  55. package/dist/lib/plugins/openapi/client/useCreateQuery.js +2 -1
  56. package/dist/lib/plugins/openapi/client/useCreateQuery.js.map +1 -1
  57. package/dist/lib/plugins/openapi/graphql/gql.d.ts +4 -4
  58. package/dist/lib/plugins/openapi/graphql/gql.js +3 -3
  59. package/dist/lib/plugins/openapi/graphql/gql.js.map +1 -1
  60. package/dist/lib/plugins/openapi/graphql/graphql.d.ts +33 -44
  61. package/dist/lib/plugins/openapi/graphql/graphql.js +19 -29
  62. package/dist/lib/plugins/openapi/graphql/graphql.js.map +1 -1
  63. package/dist/lib/plugins/openapi/index.d.ts +5 -10
  64. package/dist/lib/plugins/openapi/index.js +29 -60
  65. package/dist/lib/plugins/openapi/index.js.map +1 -1
  66. package/dist/lib/plugins/openapi/interfaces.d.ts +3 -1
  67. package/dist/lib/plugins/openapi/schema/SchemaPropertyItem.js +3 -3
  68. package/dist/lib/plugins/openapi/schema/SchemaPropertyItem.js.map +1 -1
  69. package/dist/lib/plugins/openapi/schema/SchemaView.js +13 -6
  70. package/dist/lib/plugins/openapi/schema/SchemaView.js.map +1 -1
  71. package/dist/lib/plugins/openapi/util/createSidebarCategory.js +5 -7
  72. package/dist/lib/plugins/openapi/util/createSidebarCategory.js.map +1 -1
  73. package/dist/lib/ui/Badge.d.ts +1 -1
  74. package/dist/lib/ui/Button.d.ts +1 -1
  75. package/dist/lib/ui/Button.js +1 -1
  76. package/dist/lib/ui/Button.js.map +1 -1
  77. package/dist/lib/ui/Command.d.ts +6 -6
  78. package/dist/lib/util/joinPath.d.ts +3 -0
  79. package/dist/lib/util/joinPath.js +3 -0
  80. package/dist/lib/util/joinPath.js.map +1 -1
  81. package/dist/lib/util/traverse.js +2 -2
  82. package/dist/lib/util/traverse.js.map +1 -1
  83. package/dist/lib/util/useScrollToAnchor.js +2 -0
  84. package/dist/lib/util/useScrollToAnchor.js.map +1 -1
  85. package/dist/vite/api/schema-codegen.js +19 -4
  86. package/dist/vite/api/schema-codegen.js.map +1 -1
  87. package/dist/vite/api/schema-codegen.test.js +61 -0
  88. package/dist/vite/api/schema-codegen.test.js.map +1 -1
  89. package/dist/vite/config.js +1 -1
  90. package/dist/vite/config.js.map +1 -1
  91. package/dist/vite/plugin-api.js +4 -12
  92. package/dist/vite/plugin-api.js.map +1 -1
  93. package/dist/vite/plugin-docs.d.ts +1 -1
  94. package/dist/vite/plugin-docs.js +18 -1
  95. package/dist/vite/plugin-docs.js.map +1 -1
  96. package/lib/{AuthenticationPlugin-4ip08maU.js → AuthenticationPlugin-Cij2tPWa.js} +2 -2
  97. package/lib/{AuthenticationPlugin-4ip08maU.js.map → AuthenticationPlugin-Cij2tPWa.js.map} +1 -1
  98. package/lib/{Spinner-C6n4eOvh.js → Button-Fp19CMUr.js} +15 -18
  99. package/lib/Button-Fp19CMUr.js.map +1 -0
  100. package/lib/{Markdown-C0eXdzGn.js → Markdown-DT5Rrq8_.js} +3526 -3264
  101. package/lib/Markdown-DT5Rrq8_.js.map +1 -0
  102. package/lib/{MdxPage-BKkG1cm1.js → MdxPage-D2rD1vC4.js} +3 -3
  103. package/lib/{MdxPage-BKkG1cm1.js.map → MdxPage-D2rD1vC4.js.map} +1 -1
  104. package/lib/{OasProvider-CwhKwrwl.js → OasProvider-DdEBf2qS.js} +3 -3
  105. package/lib/{OasProvider-CwhKwrwl.js.map → OasProvider-DdEBf2qS.js.map} +1 -1
  106. package/lib/{OperationList-DGYoFitT.js → OperationList-DT4-gm_S.js} +1122 -1093
  107. package/lib/OperationList-DT4-gm_S.js.map +1 -0
  108. package/lib/{Select-FAYHOYTy.js → Select-z1Lwl0-J.js} +3 -3
  109. package/lib/{Select-FAYHOYTy.js.map → Select-z1Lwl0-J.js.map} +1 -1
  110. package/lib/{SlotletProvider-BJC58V32.js → SlotletProvider-D8OBnr77.js} +2 -2
  111. package/lib/{SlotletProvider-BJC58V32.js.map → SlotletProvider-D8OBnr77.js.map} +1 -1
  112. package/lib/Spinner-CE68iCm0.js +7 -0
  113. package/lib/Spinner-CE68iCm0.js.map +1 -0
  114. package/lib/{circular-v7K6lDDh.js → circular-ByJI6Mci.js} +4887 -4419
  115. package/lib/circular-ByJI6Mci.js.map +1 -0
  116. package/lib/{createServer-CbL1Uh2Q.js → createServer-DjgKDpGV.js} +3301 -3747
  117. package/lib/createServer-DjgKDpGV.js.map +1 -0
  118. package/lib/{hook-CfCFKZ-2.js → hook-DzQC8PzJ.js} +78 -73
  119. package/lib/hook-DzQC8PzJ.js.map +1 -0
  120. package/lib/{index-Dm1QJHVl.js → index-DdQSV2RF.js} +593 -633
  121. package/lib/index-DdQSV2RF.js.map +1 -0
  122. package/lib/{useQuery-CQUwWR9i.js → joinUrl-BjDooT-T.js} +240 -223
  123. package/lib/joinUrl-BjDooT-T.js.map +1 -0
  124. package/lib/{mutation-B81DztCT.js → mutation-_Z5C2wFZ.js} +2 -2
  125. package/lib/{mutation-B81DztCT.js.map → mutation-_Z5C2wFZ.js.map} +1 -1
  126. package/lib/post-processors/traverse.js +2 -2
  127. package/lib/post-processors/traverse.js.map +1 -1
  128. package/lib/ui/ActionButton.js +11 -10
  129. package/lib/ui/ActionButton.js.map +1 -1
  130. package/lib/ui/Button.js +1 -1
  131. package/lib/ui/Button.js.map +1 -1
  132. package/lib/zudoku.auth-auth0.js +1 -1
  133. package/lib/zudoku.auth-clerk.js +2 -2
  134. package/lib/zudoku.auth-openid.js +3 -3
  135. package/lib/zudoku.components.js +438 -444
  136. package/lib/zudoku.components.js.map +1 -1
  137. package/lib/zudoku.hooks.js +1 -1
  138. package/lib/zudoku.plugin-api-catalog.js +3 -3
  139. package/lib/zudoku.plugin-api-keys.js +4 -4
  140. package/lib/zudoku.plugin-custom-pages.js +1 -1
  141. package/lib/zudoku.plugin-markdown.js +1 -1
  142. package/lib/zudoku.plugin-openapi.js +5 -6
  143. package/lib/zudoku.plugin-openapi.js.map +1 -1
  144. package/lib/zudoku.plugin-search-pagefind.js +15 -16
  145. package/lib/zudoku.plugin-search-pagefind.js.map +1 -1
  146. package/lib/zudoku.plugins.js.map +1 -1
  147. package/package.json +3 -1
  148. package/src/app/entry.server.tsx +7 -1
  149. package/src/lib/components/InlineCode.tsx +3 -1
  150. package/src/lib/components/Layout.tsx +3 -16
  151. package/src/lib/components/MobileTopNavigation.tsx +1 -1
  152. package/src/lib/components/TopNavigation.tsx +12 -16
  153. package/src/lib/components/Zudoku.tsx +5 -1
  154. package/src/lib/components/cache.ts +8 -0
  155. package/src/lib/components/context/ViewportAnchorContext.tsx +3 -6
  156. package/src/lib/components/context/ZudokuContext.ts +17 -8
  157. package/src/lib/components/navigation/SidebarCategory.tsx +15 -12
  158. package/src/lib/components/navigation/SidebarItem.tsx +2 -2
  159. package/src/lib/components/navigation/SidebarWrapper.tsx +2 -2
  160. package/src/lib/core/ZudokuContext.ts +11 -8
  161. package/src/lib/core/plugins.ts +4 -4
  162. package/src/lib/hooks/useEvent.test.tsx +1 -1
  163. package/src/lib/oas/graphql/index.ts +104 -64
  164. package/src/lib/plugins/openapi/OperationList.tsx +30 -36
  165. package/src/lib/plugins/openapi/OperationListItem.tsx +1 -1
  166. package/src/lib/plugins/openapi/ParamInfos.tsx +27 -4
  167. package/src/lib/plugins/openapi/ParameterListItem.tsx +5 -1
  168. package/src/lib/plugins/openapi/Sidecar.tsx +2 -2
  169. package/src/lib/plugins/openapi/client/useCreateQuery.ts +2 -1
  170. package/src/lib/plugins/openapi/graphql/gql.ts +17 -17
  171. package/src/lib/plugins/openapi/graphql/graphql.ts +57 -75
  172. package/src/lib/plugins/openapi/index.tsx +40 -84
  173. package/src/lib/plugins/openapi/interfaces.ts +4 -1
  174. package/src/lib/plugins/openapi/schema/SchemaPropertyItem.tsx +5 -2
  175. package/src/lib/plugins/openapi/schema/SchemaView.tsx +48 -35
  176. package/src/lib/plugins/openapi/util/createSidebarCategory.tsx +5 -7
  177. package/src/lib/ui/Button.tsx +1 -1
  178. package/src/lib/util/joinPath.tsx +3 -0
  179. package/src/lib/util/traverse.ts +2 -2
  180. package/src/lib/util/useScrollToAnchor.ts +2 -0
  181. package/lib/Markdown-C0eXdzGn.js.map +0 -1
  182. package/lib/OperationList-DGYoFitT.js.map +0 -1
  183. package/lib/Spinner-C6n4eOvh.js.map +0 -1
  184. package/lib/circular-v7K6lDDh.js.map +0 -1
  185. package/lib/createServer-CbL1Uh2Q.js.map +0 -1
  186. package/lib/hook-CfCFKZ-2.js.map +0 -1
  187. package/lib/index-Dm1QJHVl.js.map +0 -1
  188. package/lib/joinUrl-10po2Jdj.js +0 -20
  189. package/lib/joinUrl-10po2Jdj.js.map +0 -1
  190. package/lib/useQuery-CQUwWR9i.js.map +0 -1
@@ -1,14 +1,16 @@
1
1
  import * as Collapsible from "@radix-ui/react-collapsible";
2
+ import { deepEqual } from "fast-equals";
2
3
  import { ChevronRightIcon } from "lucide-react";
3
- import { useEffect, useState } from "react";
4
+ import { memo, useEffect, useState } from "react";
4
5
  import { NavLink, useMatch } from "react-router";
6
+ import { Button } from "zudoku/ui/Button.js";
5
7
  import type { SidebarItemCategory } from "../../../config/validators/SidebarSchema.js";
6
8
  import { cn } from "../../util/cn.js";
7
9
  import { joinPath } from "../../util/joinPath.js";
8
10
  import { navigationListItem, SidebarItem } from "./SidebarItem.js";
9
11
  import { useIsCategoryOpen } from "./utils.js";
10
12
 
11
- export const SidebarCategory = ({
13
+ const SidebarCategoryInner = ({
12
14
  category,
13
15
  onRequestClose,
14
16
  }: {
@@ -35,13 +37,15 @@ export const SidebarCategory = ({
35
37
  }, [isCategoryOpen]);
36
38
 
37
39
  const ToggleButton = isCollapsible && (
38
- <button
39
- type="button"
40
+ <Button
40
41
  onClick={(e) => {
41
42
  e.preventDefault();
42
43
  setOpen((prev) => !prev);
43
44
  setHasInteracted(true);
44
45
  }}
46
+ variant="ghost"
47
+ size="icon"
48
+ className="size-6 hover:bg-[hsl(from_hsl(var(--accent))_h_s_calc(l-5))] hover:dark:bg-[hsl(from_hsl(var(--accent))_h_s_calc(l+5))]"
45
49
  >
46
50
  <ChevronRightIcon
47
51
  size={16}
@@ -50,7 +54,7 @@ export const SidebarCategory = ({
50
54
  "shrink-0 group-data-[state=open]:rotate-90",
51
55
  )}
52
56
  />
53
- </button>
57
+ </Button>
54
58
  );
55
59
 
56
60
  const icon = category.icon && (
@@ -62,7 +66,7 @@ export const SidebarCategory = ({
62
66
 
63
67
  const styles = navigationListItem({
64
68
  className: [
65
- "text-start font-medium",
69
+ "group text-start font-medium",
66
70
  isCollapsible || typeof category.link !== "undefined"
67
71
  ? "cursor-pointer"
68
72
  : "cursor-default hover:bg-transparent",
@@ -90,12 +94,7 @@ export const SidebarCategory = ({
90
94
  }}
91
95
  >
92
96
  {icon}
93
- <div
94
- className={cn(
95
- "flex items-center gap-2 justify-between w-full",
96
- isActive ? "text-primary" : "text-foreground/80",
97
- )}
98
- >
97
+ <div className="flex items-center gap-2 justify-between w-full text-foreground/80 group-aria-[current='page']:text-primary">
99
98
  <div className="truncate">{category.label}</div>
100
99
  {ToggleButton}
101
100
  </div>
@@ -135,3 +134,7 @@ export const SidebarCategory = ({
135
134
  </Collapsible.Root>
136
135
  );
137
136
  };
137
+
138
+ export const SidebarCategory = memo(SidebarCategoryInner, deepEqual);
139
+
140
+ SidebarCategory.displayName = "SidebarCategory";
@@ -10,11 +10,11 @@ import { SidebarBadge } from "./SidebarBadge.js";
10
10
  import { SidebarCategory } from "./SidebarCategory.js";
11
11
 
12
12
  export const navigationListItem = cva(
13
- "flex items-center gap-2 px-[--padding-nav-item] py-1.5 rounded-lg hover:bg-accent",
13
+ "flex items-center gap-2 px-[--padding-nav-item] my-0.5 py-1.5 rounded-lg hover:bg-accent",
14
14
  {
15
15
  variants: {
16
16
  isActive: {
17
- true: "text-primary font-medium",
17
+ true: "bg-accent font-medium",
18
18
  false: "text-foreground/80",
19
19
  },
20
20
  isMuted: {
@@ -11,9 +11,9 @@ export const SidebarWrapper = ({
11
11
  }>) => (
12
12
  <nav
13
13
  className={cn(
14
- "hidden lg:flex h-full scrollbar peer flex-col overflow-y-auto shrink-0 text-sm border-r pr-6",
14
+ "hidden lg:flex h-full scrollbar flex-col overflow-y-auto shrink-0 text-sm border-r pr-6",
15
15
  "sticky top-[--header-height] h-[calc(100vh-var(--header-height))]",
16
- "-mx-[--padding-nav-item] max-w-[--side-nav-width] pb-20 pt-[--padding-content-top] scroll-pt-2 gap-2",
16
+ "-mx-[--padding-nav-item] max-w-[--side-nav-width] pb-6 pt-[--padding-content-top] scroll-pt-2 gap-2",
17
17
  className,
18
18
  )}
19
19
  ref={ref}
@@ -1,11 +1,12 @@
1
+ import type { QueryClient } from "@tanstack/react-query";
1
2
  import { createNanoEvents } from "nanoevents";
2
- import { ReactNode } from "react";
3
- import { Location } from "react-router";
4
- import { TopNavigationItem } from "../../config/validators/common.js";
3
+ import type { ReactNode } from "react";
4
+ import type { Location } from "react-router";
5
+ import type { TopNavigationItem } from "../../config/validators/common.js";
5
6
  import type { SidebarConfig } from "../../config/validators/SidebarSchema.js";
6
- import { type AuthenticationProvider } from "../authentication/authentication.js";
7
+ import type { AuthenticationProvider } from "../authentication/authentication.js";
7
8
  import type { ComponentsContextType } from "../components/context/ComponentsContext.js";
8
- import { Slotlets } from "../components/SlotletProvider.js";
9
+ import type { Slotlets } from "../components/SlotletProvider.js";
9
10
  import { joinPath } from "../util/joinPath.js";
10
11
  import type { MdxComponentsType } from "../util/MdxComponents.js";
11
12
  import { objectEntries } from "../util/objectEntries.js";
@@ -90,7 +91,10 @@ export class ZudokuContext {
90
91
  private readonly navigationPlugins: NavigationPlugin[];
91
92
  private emitter = createNanoEvents<ZudokuEvents>();
92
93
 
93
- constructor(public readonly options: ZudokuContextOptions) {
94
+ constructor(
95
+ public readonly options: ZudokuContextOptions,
96
+ public readonly queryClient: QueryClient,
97
+ ) {
94
98
  this.plugins = options.plugins ?? [];
95
99
  this.topNavigation = options.topNavigation ?? [];
96
100
  this.sidebars = options.sidebars ?? {};
@@ -98,7 +102,6 @@ export class ZudokuContext {
98
102
  this.authentication = options.authentication;
99
103
  this.meta = options.metadata;
100
104
  this.page = options.page;
101
-
102
105
  this.plugins.forEach((plugin) => {
103
106
  if (!isEventConsumerPlugin(plugin)) return;
104
107
 
@@ -143,7 +146,7 @@ export class ZudokuContext {
143
146
  getPluginSidebar = async (path: string) => {
144
147
  const navigations = await Promise.all(
145
148
  this.navigationPlugins.map((plugin) =>
146
- plugin.getSidebar?.(joinPath(path)),
149
+ plugin.getSidebar?.(joinPath(path), this),
147
150
  ),
148
151
  );
149
152
 
@@ -2,11 +2,11 @@ import type { LucideIcon } from "lucide-react";
2
2
  import { type ReactElement } from "react";
3
3
  import { type RouteObject } from "react-router";
4
4
  import type { Sidebar } from "../../config/validators/SidebarSchema.js";
5
- import { MdxComponentsType } from "../util/MdxComponents.js";
5
+ import { type MdxComponentsType } from "../util/MdxComponents.js";
6
6
  import {
7
- ZudokuContext,
8
- ZudokuEvents,
9
7
  type ApiIdentity,
8
+ type ZudokuContext,
9
+ type ZudokuEvents,
10
10
  } from "./ZudokuContext.js";
11
11
 
12
12
  export type ZudokuPlugin =
@@ -21,7 +21,7 @@ export type { RouteObject };
21
21
 
22
22
  export interface NavigationPlugin {
23
23
  getRoutes: () => RouteObject[];
24
- getSidebar?: (path: string) => Promise<Sidebar>;
24
+ getSidebar?: (path: string, context: ZudokuContext) => Promise<Sidebar>;
25
25
  }
26
26
 
27
27
  export const createApiIdentityPlugin = (
@@ -12,8 +12,8 @@ import { useEvent } from "./useEvent.js";
12
12
  */
13
13
 
14
14
  const createTestContext = () => {
15
- const context = new ZudokuContext({});
16
15
  const queryClient = new QueryClient();
16
+ const context = new ZudokuContext({}, queryClient);
17
17
  const wrapper = ({ children }: PropsWithChildren) => (
18
18
  <QueryClientProvider client={queryClient}>
19
19
  <ZudokuProvider context={context}>{children}</ZudokuProvider>
@@ -51,17 +51,19 @@ export const createOperationSlug = (
51
51
  return slugify(summary);
52
52
  };
53
53
 
54
- type SchemaImport = () => Promise<{ schema: OpenAPIDocument }>;
54
+ export type SchemaImport = () => Promise<{
55
+ schema: OpenAPIDocument;
56
+ slugs: ReturnType<typeof getAllSlugs>;
57
+ }>;
55
58
 
56
59
  export type SchemaImports = Record<string, SchemaImport>;
57
60
 
58
61
  type Context = {
59
62
  schema: OpenAPIDocument;
60
63
  operations: GraphQLOperationObject[];
61
- tags: TagObject[];
62
64
  schemaImports?: SchemaImports;
63
- slugify: CountableSlugify;
64
- slugs: Record<string, string>;
65
+ tags: ReturnType<typeof getAllTags>;
66
+ slugs: ReturnType<typeof getAllSlugs>;
65
67
  };
66
68
 
67
69
  const builder = new SchemaBuilder<{
@@ -87,17 +89,15 @@ const JSONScalar = builder.addScalarType("JSON", GraphQLJSON);
87
89
  const JSONObjectScalar = builder.addScalarType("JSONObject", GraphQLJSONObject);
88
90
  const JSONSchemaScalar = builder.addScalarType("JSONSchema", GraphQLJSONSchema);
89
91
 
90
- const resolveExtensions = (obj: Record<string, any>) => {
91
- const extensions: Record<string, any> = {};
92
- for (const [key, value] of Object.entries(obj)) {
93
- if (key.startsWith("x-")) {
94
- extensions[key] = value;
95
- }
96
- }
97
- return extensions;
98
- };
92
+ const resolveExtensions = (obj: Record<string, any>) =>
93
+ Object.fromEntries(
94
+ Object.entries(obj).filter(([key]) => key.startsWith("x-")),
95
+ );
99
96
 
100
- export const getAllTags = (schema: OpenAPIDocument): TagObject[] => {
97
+ export const getAllTags = (
98
+ schema: OpenAPIDocument,
99
+ slugs: ReturnType<typeof getAllSlugs>["tags"],
100
+ ): Array<TagObject & { slug?: string }> => {
101
101
  const rootTags = schema.tags ?? [];
102
102
  const operationTags = new Set(
103
103
  Object.values(schema.paths ?? {})
@@ -107,24 +107,42 @@ export const getAllTags = (schema: OpenAPIDocument): TagObject[] => {
107
107
 
108
108
  return [
109
109
  // Keep root tags that are actually used in operations
110
- ...rootTags.filter((tag) => operationTags.has(tag.name)),
110
+ ...rootTags
111
+ .filter((tag) => operationTags.has(tag.name))
112
+ .map((tag) => ({ ...tag, slug: slugs[tag.name] })),
111
113
  // Add tags found in operations but not defined in root tags
112
114
  ...[...operationTags]
113
115
  .filter((tag) => !rootTags.some((rt) => rt.name === tag))
114
- .map((tag) => ({ name: tag })),
116
+ .map((tag) => ({ name: tag, slug: slugs[tag] })),
115
117
  ];
116
118
  };
117
119
 
118
- const getAllSlugs = (operations: GraphQLOperationObject[]) =>
119
- Object.fromEntries(
120
- operations.map((op) => [
121
- getSlugName(op),
122
- createOperationSlug(slugifyWithCounter(), op),
120
+ export const getAllSlugs = (
121
+ ops: GraphQLOperationObject[],
122
+ schemaTags: TagObject[] = [],
123
+ ) => {
124
+ const slugify = slugifyWithCounter();
125
+
126
+ const tags = Array.from(
127
+ new Set([
128
+ ...ops.flatMap((op) => op.tags ?? []),
129
+ ...schemaTags.map((tag) => tag.name),
123
130
  ]),
124
131
  );
125
132
 
126
- const getSlugName = (op: GraphQLOperationObject) =>
127
- `${op.path}-${op.method}-${op.operationId}-${op.summary}`;
133
+ return {
134
+ operations: Object.fromEntries(
135
+ ops.map((op) => [
136
+ getOperationSlugKey(op),
137
+ createOperationSlug(slugify, op),
138
+ ]),
139
+ ),
140
+ tags: Object.fromEntries(tags.map((tag) => [tag, slugify(tag)])),
141
+ };
142
+ };
143
+
144
+ const getOperationSlugKey = (op: GraphQLOperationObject) =>
145
+ [op.path, op.method, op.operationId, op.summary].filter(Boolean).join("-");
128
146
 
129
147
  export const getAllOperations = (
130
148
  paths?: PathsObject,
@@ -163,30 +181,38 @@ export const getAllOperations = (
163
181
  return operations;
164
182
  };
165
183
 
166
- const SchemaTag = builder.objectRef<TagObject>("SchemaTag").implement({
167
- fields: (t) => ({
168
- name: t.exposeString("name"),
169
- description: t.exposeString("description", { nullable: true }),
170
- operations: t.field({
171
- type: [OperationItem],
172
- resolve: (parent, _args, ctx) => {
173
- const rootTags = ctx.tags.map((tag) => tag.name);
174
- return ctx.operations
175
- .filter((item) =>
176
- parent.name
177
- ? item.tags?.includes(parent.name)
178
- : item.tags?.length === 0 ||
179
- // If none of the tags are present in the root tags, then show them here
180
- item.tags?.every((tag) => !rootTags.includes(tag)),
181
- )
182
- .map((item) => ({
183
- ...item,
184
- parentTag: parent.name,
185
- }));
186
- },
184
+ const SchemaTag = builder
185
+ .objectRef<
186
+ Omit<TagObject, "name"> & { name?: string; slug?: string }
187
+ >("SchemaTag")
188
+ .implement({
189
+ fields: (t) => ({
190
+ name: t.exposeString("name", { nullable: true }),
191
+ slug: t.exposeString("slug", { nullable: true }),
192
+ description: t.exposeString("description", { nullable: true }),
193
+ operations: t.field({
194
+ type: [OperationItem],
195
+ resolve: (parent, _args, ctx) => {
196
+ const rootTags = ctx.tags.map((tag) => tag.name);
197
+
198
+ return ctx.operations
199
+ .filter((item) =>
200
+ parent.name
201
+ ? item.tags?.includes(parent.name)
202
+ : item.tags?.length === 0 ||
203
+ // If none of the tags are present in the root tags, then show them here
204
+ item.tags?.every((tag) => !rootTags.includes(tag)),
205
+ )
206
+ .map((item) => ({ ...item, parentTag: parent.name }));
207
+ },
208
+ }),
209
+ extensions: t.field({
210
+ type: JSONObjectScalar,
211
+ resolve: (parent) => resolveExtensions(parent),
212
+ nullable: true,
213
+ }),
187
214
  }),
188
- }),
189
- });
215
+ });
190
216
 
191
217
  const ServerItem = builder.objectRef<ServerObject>("Server").implement({
192
218
  fields: (t) => ({
@@ -350,7 +376,16 @@ const OperationItem = builder
350
376
  fields: (t) => ({
351
377
  slug: t.field({
352
378
  type: "String",
353
- resolve: (parent, _, ctx) => ctx.slugs[getSlugName(parent)]!,
379
+ resolve: (parent, _, ctx) => {
380
+ const slug = ctx.slugs.operations[getOperationSlugKey(parent)];
381
+
382
+ if (!slug) {
383
+ throw new Error(
384
+ `No slug found for operation: ${getOperationSlugKey(parent)}`,
385
+ );
386
+ }
387
+ return slug;
388
+ },
354
389
  }),
355
390
  path: t.exposeString("path"),
356
391
  method: t.exposeString("method"),
@@ -465,10 +500,17 @@ const Schema = builder.objectRef<OpenAPIDocument>("Schema").implement({
465
500
  name: t.arg.string(),
466
501
  },
467
502
  type: [SchemaTag],
468
- resolve: (root, args, ctx) => {
469
- return args.name
470
- ? ctx.tags.filter((tag) => tag.name === args.name)
471
- : ctx.tags;
503
+ resolve: (_root, args, ctx) => {
504
+ if (args.name) {
505
+ return ctx.tags.filter((tag) => tag.name === args.name);
506
+ }
507
+
508
+ // Append empty tag which will be used to display untagged operations
509
+ if (ctx.operations.some((op) => !op.tags?.length)) {
510
+ return [...ctx.tags, { name: undefined, slug: undefined }];
511
+ }
512
+
513
+ return ctx.tags;
472
514
  },
473
515
  }),
474
516
  operations: t.field({
@@ -512,27 +554,25 @@ builder.queryType({
512
554
  input: t.arg({ type: JSONScalar, required: true }),
513
555
  },
514
556
  resolve: async (_, args, ctx) => {
515
- let schema: OpenAPIDocument;
516
-
517
557
  if (args.type === "file" && typeof args.input === "string") {
518
558
  const loadSchema = ctx.schemaImports?.[args.input];
519
559
 
520
560
  if (!loadSchema) {
521
561
  throw new Error(`No schema loader found for path: ${args.input}`);
522
562
  }
523
- const module = await loadSchema();
524
- schema = module.schema;
563
+ const { schema, slugs } = await loadSchema();
564
+ ctx.schema = schema;
565
+ ctx.operations = getAllOperations(schema.paths);
566
+ ctx.slugs = slugs;
567
+ ctx.tags = getAllTags(schema, ctx.slugs.tags);
525
568
  } else {
526
- schema = await validate(args.input as string);
569
+ ctx.schema = await validate(args.input as string);
570
+ ctx.operations = getAllOperations(ctx.schema.paths);
571
+ ctx.slugs = getAllSlugs(ctx.operations);
572
+ ctx.tags = getAllTags(ctx.schema, ctx.slugs.tags);
527
573
  }
528
574
 
529
- ctx.schema = schema;
530
- ctx.operations = getAllOperations(schema.paths);
531
- ctx.slugify = slugifyWithCounter();
532
- ctx.tags = getAllTags(schema);
533
- ctx.slugs = getAllSlugs(ctx.operations);
534
-
535
- return schema;
575
+ return ctx.schema;
536
576
  },
537
577
  }),
538
578
  }),
@@ -542,4 +582,4 @@ export const schema = builder.toSchema();
542
582
 
543
583
  export const createGraphQLServer = (
544
584
  options?: Omit<YogaServerOptions<any, any>, "schema">,
545
- ) => createYoga({ schema, ...options });
585
+ ) => createYoga({ schema, batching: true, ...options });
@@ -1,5 +1,5 @@
1
1
  import { type ResultOf } from "@graphql-typed-document-node/core";
2
- import { useSuspenseQuery } from "@tanstack/react-query";
2
+ import { useQuery, useSuspenseQuery } from "@tanstack/react-query";
3
3
  import { Helmet } from "@zudoku/react-helmet-async";
4
4
  import { ChevronsDownUpIcon, ChevronsUpDownIcon } from "lucide-react";
5
5
  import { useNavigate } from "react-router";
@@ -97,8 +97,16 @@ export const OperationsFragment = graphql(/* GraphQL */ `
97
97
 
98
98
  export type OperationListItemResult = ResultOf<typeof OperationsFragment>;
99
99
 
100
- const AllOperationsQuery = graphql(/* GraphQL */ `
101
- query AllOperations(
100
+ const SchemaWarmupQuery = graphql(/* GraphQL */ `
101
+ query SchemaWarmup($input: JSON!, $type: SchemaType!) {
102
+ schema(input: $input, type: $type) {
103
+ openapi
104
+ }
105
+ }
106
+ `);
107
+
108
+ const OperationsForTagQuery = graphql(/* GraphQL */ `
109
+ query OperationsForTag(
102
110
  $input: JSON!
103
111
  $type: SchemaType!
104
112
  $tag: String
@@ -133,7 +141,7 @@ export const OperationList = ({
133
141
  untagged?: boolean;
134
142
  }) => {
135
143
  const { input, type, versions, version, options } = useOasConfig();
136
- const query = useCreateQuery(AllOperationsQuery, {
144
+ const query = useCreateQuery(OperationsForTagQuery, {
137
145
  input,
138
146
  type,
139
147
  tag,
@@ -151,6 +159,14 @@ export const OperationList = ({
151
159
  const operations = schema.operations;
152
160
  const tagDescription = schema.tags.find((t) => t.name === tag)?.description;
153
161
 
162
+ // This is to warmup (i.e. load the schema in the background) the schema on the client, if the page has been rendered on the server
163
+ const warmupQuery = useCreateQuery(SchemaWarmupQuery, { input, type });
164
+ useQuery({
165
+ ...warmupQuery,
166
+ enabled: typeof window !== "undefined",
167
+ notifyOnChangeProps: [],
168
+ });
169
+
154
170
  // Prefetch for Playground
155
171
  useApiIdentities();
156
172
 
@@ -269,38 +285,16 @@ export const OperationList = ({
269
285
  <div className="my-4 flex items-center justify-end gap-4">
270
286
  <Endpoint />
271
287
  </div>
272
- {operations.map((fragment) => (
273
- <OperationListItem
274
- serverUrl={selectedServer}
275
- key={fragment.slug}
276
- operationFragment={fragment}
277
- />
278
- ))}
279
- {/* {schema.tags
280
- .filter((tag) => tag.operations.length > 0)
281
- .map((tag) => (
282
- // px, -mx is so that `content-visibility` doesn't cut off overflown heading anchor links '#'
283
- <div key={tag.name} className="px-6 -mx-6 [content-visibility:auto]">
284
- {tag.name && <CategoryHeading>{tag.name}</CategoryHeading>}
285
- {tag.description && (
286
- <Markdown
287
- className={`${ProseClasses} max-w-full prose-img:max-w-prose w-full mt-2 mb-12`}
288
- content={tag.description}
289
- />
290
- )}
291
- <div className="operation mb-12">
292
- <StaggeredRender>
293
- {tag.operations.map((fragment) => (
294
- <OperationListItem
295
- serverUrl={selectedServer ?? schema.url}
296
- key={fragment.slug}
297
- operationFragment={fragment}
298
- />
299
- ))}
300
- </StaggeredRender>
301
- </div>
302
- </div>
303
- ))} */}
288
+ {/* px, -mx is so that `content-visibility` doesn't cut off overflown heading anchor links '#' */}
289
+ <div className="px-6 -mx-6 [content-visibility:auto]">
290
+ {operations.map((fragment) => (
291
+ <OperationListItem
292
+ serverUrl={selectedServer}
293
+ key={fragment.slug}
294
+ operationFragment={fragment}
295
+ />
296
+ ))}
297
+ </div>
304
298
  </div>
305
299
  );
306
300
  };
@@ -63,7 +63,7 @@ export const OperationListItem = ({
63
63
  <SelectOnClick className="max-w-full truncate flex cursor-pointer">
64
64
  {serverUrl && (
65
65
  <div className="text-neutral-400 dark:text-neutral-500 truncate">
66
- {serverUrl}
66
+ {serverUrl.replace(/\/$/, "")}
67
67
  </div>
68
68
  )}
69
69
  <div className="text-neutral-900 dark:text-neutral-200">
@@ -1,6 +1,29 @@
1
- import { isValidElement } from "react";
1
+ import { ChevronsLeftRightIcon } from "lucide-react";
2
+ import { isValidElement, useState } from "react";
2
3
  import { InlineCode } from "../../components/InlineCode.js";
3
4
  import { type SchemaObject } from "../../oas/parser/index.js";
5
+ import { cn } from "../../util/cn.js";
6
+
7
+ const Pattern = ({ pattern }: { pattern: string }) => {
8
+ const [isExpanded, setIsExpanded] = useState(false);
9
+ const isExpandable = pattern.length > 20;
10
+ const shortPattern = isExpandable ? `${pattern.slice(0, 20)}…` : pattern;
11
+
12
+ return (
13
+ <InlineCode
14
+ className={cn("text-xs", isExpandable && "cursor-pointer")}
15
+ onClick={() => setIsExpanded(!isExpanded)}
16
+ selectOnClick={false}
17
+ >
18
+ {isExpanded ? pattern : shortPattern}
19
+ {isExpandable && (
20
+ <button type="button" className="p-1 translate-y-[2px]">
21
+ {!isExpanded && <ChevronsLeftRightIcon size={12} />}
22
+ </button>
23
+ )}
24
+ </InlineCode>
25
+ );
26
+ };
4
27
 
5
28
  const getSchemaInfos = (schema?: SchemaObject) => {
6
29
  if (!schema) return [];
@@ -28,7 +51,7 @@ const getSchemaInfos = (schema?: SchemaObject) => {
28
51
  schema.deprecated && "deprecated",
29
52
  schema.pattern && (
30
53
  <>
31
- pattern: <InlineCode className="text-xs">{schema.pattern}</InlineCode>
54
+ pattern: <Pattern pattern={schema.pattern} />
32
55
  </>
33
56
  ),
34
57
  ];
@@ -48,7 +71,7 @@ export const ParamInfos = ({
48
71
  );
49
72
 
50
73
  return (
51
- <div className={className}>
74
+ <span className={className}>
52
75
  {filteredItems.map((item, index) => (
53
76
  <span className="text-muted-foreground" key={index}>
54
77
  {item}
@@ -59,6 +82,6 @@ export const ParamInfos = ({
59
82
  )}
60
83
  </span>
61
84
  ))}
62
- </div>
85
+ </span>
63
86
  );
64
87
  };
@@ -67,7 +67,11 @@ export const ParameterListItem = ({
67
67
  className="text-sm prose-p:my-1 prose-code:whitespace-pre-line"
68
68
  />
69
69
  )}
70
- {paramSchema.enum && <EnumValues values={paramSchema.enum} />}
70
+ {paramSchema.type === "array" && paramSchema.items.enum ? (
71
+ <EnumValues values={paramSchema.items.enum} />
72
+ ) : (
73
+ paramSchema.enum && <EnumValues values={paramSchema.enum} />
74
+ )}
71
75
  </li>
72
76
  );
73
77
  };
@@ -173,9 +173,9 @@ export const Sidecar = ({
173
173
  const showPlayground =
174
174
  isOnScreen &&
175
175
  (operation.extensions["x-explorer-enabled"] === true ||
176
- operation.extensions["x-playground-enabled"] === true ||
176
+ operation.extensions["x-zudoku-playground-enabled"] === true ||
177
177
  (operation.extensions["x-explorer-enabled"] === undefined &&
178
- operation.extensions["x-playground-enabled"] === undefined &&
178
+ operation.extensions["x-zudoku-playground-enabled"] === undefined &&
179
179
  !options?.disablePlayground));
180
180
 
181
181
  return (
@@ -1,3 +1,4 @@
1
+ import { stripIgnoredCharacters } from "graphql";
1
2
  import { useContext } from "react";
2
3
  import type { TypedDocumentString } from "../graphql/graphql.js";
3
4
  import { GraphQLContext } from "./GraphQLContext.js";
@@ -13,6 +14,6 @@ export const useCreateQuery = <TResult, TVariables>(
13
14
 
14
15
  return {
15
16
  queryFn: () => graphQLClient.fetch(query, ...variables),
16
- queryKey: [query, variables[0]],
17
+ queryKey: [stripIgnoredCharacters(query.toString()), variables[0]],
17
18
  } as const;
18
19
  };