zudoku 0.0.0-zacd7778d → 0.0.0-zce59fc03

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 (180) hide show
  1. package/dist/config/loader.js +3 -1
  2. package/dist/config/loader.js.map +1 -1
  3. package/dist/config/validators/InputNavigationSchema.d.ts +119 -119
  4. package/dist/config/validators/ProtectedRoutesSchema.d.ts +1 -1
  5. package/dist/config/validators/validate.d.ts +28 -10
  6. package/dist/config/validators/validate.js +6 -3
  7. package/dist/config/validators/validate.js.map +1 -1
  8. package/dist/flat-config.d.ts +28 -27
  9. package/dist/lib/components/Heading.d.ts +1 -1
  10. package/dist/lib/components/index.d.ts +18 -74
  11. package/dist/lib/components/index.js +19 -36
  12. package/dist/lib/components/index.js.map +1 -1
  13. package/dist/lib/core/plugins.d.ts +11 -1
  14. package/dist/lib/core/plugins.js +1 -0
  15. package/dist/lib/core/plugins.js.map +1 -1
  16. package/dist/lib/core/transform-config.d.ts +2 -0
  17. package/dist/lib/core/transform-config.js +22 -0
  18. package/dist/lib/core/transform-config.js.map +1 -0
  19. package/dist/lib/hooks/index.d.ts +7 -30
  20. package/dist/lib/hooks/index.js +7 -15
  21. package/dist/lib/hooks/index.js.map +1 -1
  22. package/dist/lib/oas/graphql/circular.d.ts +1 -1
  23. package/dist/lib/oas/graphql/circular.js +18 -35
  24. package/dist/lib/oas/graphql/circular.js.map +1 -1
  25. package/dist/lib/oas/graphql/circular.test.js +33 -2
  26. package/dist/lib/oas/graphql/circular.test.js.map +1 -1
  27. package/dist/lib/oas/parser/index.js +14 -5
  28. package/dist/lib/oas/parser/index.js.map +1 -1
  29. package/dist/lib/plugins/openapi/OasProvider.js +6 -2
  30. package/dist/lib/plugins/openapi/OasProvider.js.map +1 -1
  31. package/dist/lib/plugins/openapi/OperationList.js +4 -8
  32. package/dist/lib/plugins/openapi/OperationList.js.map +1 -1
  33. package/dist/lib/plugins/openapi/interfaces.d.ts +3 -0
  34. package/dist/lib/plugins/openapi/util/getRoutes.d.ts +3 -0
  35. package/dist/lib/plugins/openapi/util/getRoutes.js +2 -1
  36. package/dist/lib/plugins/openapi/util/getRoutes.js.map +1 -1
  37. package/dist/lib/ui/Command.d.ts +3 -3
  38. package/dist/lib/util/flattenAllOf.d.ts +0 -2
  39. package/dist/lib/util/flattenAllOf.js +0 -46
  40. package/dist/lib/util/flattenAllOf.js.map +1 -1
  41. package/dist/lib/util/flattenAllOf.test.js +2 -1
  42. package/dist/lib/util/flattenAllOf.test.js.map +1 -1
  43. package/dist/lib/util/flattenAllOfProcessor.d.ts +2 -0
  44. package/dist/lib/util/flattenAllOfProcessor.js +48 -0
  45. package/dist/lib/util/flattenAllOfProcessor.js.map +1 -0
  46. package/dist/lib/util/readFrontmatter.js +2 -1
  47. package/dist/lib/util/readFrontmatter.js.map +1 -1
  48. package/dist/vite/api/SchemaManager.d.ts +11 -1
  49. package/dist/vite/api/SchemaManager.js +29 -18
  50. package/dist/vite/api/SchemaManager.js.map +1 -1
  51. package/dist/vite/api/SchemaManager.test.js +45 -1
  52. package/dist/vite/api/SchemaManager.test.js.map +1 -1
  53. package/dist/vite/build.js +91 -73
  54. package/dist/vite/build.js.map +1 -1
  55. package/dist/vite/error-handler.js +1 -5
  56. package/dist/vite/error-handler.js.map +1 -1
  57. package/dist/vite/mdx/remark-inject-filepath.js +5 -1
  58. package/dist/vite/mdx/remark-inject-filepath.js.map +1 -1
  59. package/dist/vite/mdx/remark-link-rewrite.js +3 -2
  60. package/dist/vite/mdx/remark-link-rewrite.js.map +1 -1
  61. package/dist/vite/plugin-api.js +5 -3
  62. package/dist/vite/plugin-api.js.map +1 -1
  63. package/dist/vite/plugin-docs.js +9 -7
  64. package/dist/vite/plugin-docs.js.map +1 -1
  65. package/dist/vite/plugin-markdown-export.js +4 -2
  66. package/dist/vite/plugin-markdown-export.js.map +1 -1
  67. package/lib/{ClaudeLogo-DHxJUhN_.js → ClaudeLogo-C6q-Xn_l.js} +26 -22
  68. package/lib/{ClaudeLogo-DHxJUhN_.js.map → ClaudeLogo-C6q-Xn_l.js.map} +1 -1
  69. package/lib/{MdxPage-CD36PJ17.js → MdxPage-B1G4W1TK.js} +8 -8
  70. package/lib/{MdxPage-CD36PJ17.js.map → MdxPage-B1G4W1TK.js.map} +1 -1
  71. package/lib/{Mermaid-Koc3z8mU.js → Mermaid-B1xNo-pf.js} +3 -2
  72. package/lib/{Mermaid-Koc3z8mU.js.map → Mermaid-B1xNo-pf.js.map} +1 -1
  73. package/lib/{OAuthErrorPage-4mN5DA86.js → OAuthErrorPage-01Ke086W.js} +20 -18
  74. package/lib/{OAuthErrorPage-4mN5DA86.js.map → OAuthErrorPage-01Ke086W.js.map} +1 -1
  75. package/lib/OasProvider-oHPiMJZg.js +48 -0
  76. package/lib/OasProvider-oHPiMJZg.js.map +1 -0
  77. package/lib/{OperationList-CaknPbvq.js → OperationList-CZ4OK8Pm.js} +1284 -1284
  78. package/lib/OperationList-CZ4OK8Pm.js.map +1 -0
  79. package/lib/{RouteGuard--A04ESy8.js → RouteGuard-B1lCR0C_.js} +5 -5
  80. package/lib/{RouteGuard--A04ESy8.js.map → RouteGuard-B1lCR0C_.js.map} +1 -1
  81. package/lib/{SchemaList-Dw3-CJPb.js → SchemaList-DoQFkJgM.js} +7 -7
  82. package/lib/{SchemaList-Dw3-CJPb.js.map → SchemaList-DoQFkJgM.js.map} +1 -1
  83. package/lib/{SchemaView-DyJkiQkD.js → SchemaView-D2k6ZJck.js} +3 -3
  84. package/lib/{SchemaView-DyJkiQkD.js.map → SchemaView-D2k6ZJck.js.map} +1 -1
  85. package/lib/{SignUp-DRvN-8cq.js → SignUp-8kDBaLbO.js} +31 -26
  86. package/lib/{SignUp-DRvN-8cq.js.map → SignUp-8kDBaLbO.js.map} +1 -1
  87. package/lib/{SyntaxHighlight-klTH8c6-.js → SyntaxHighlight-hZOFnYl0.js} +6 -5
  88. package/lib/SyntaxHighlight-hZOFnYl0.js.map +1 -0
  89. package/lib/{Toc-PbuF-u9x.js → Toc-qEIii_-W.js} +2 -2
  90. package/lib/{Toc-PbuF-u9x.js.map → Toc-qEIii_-W.js.map} +1 -1
  91. package/lib/{index-BDsEwofZ.js → Zudoku-DUsdmPME.js} +2250 -2268
  92. package/lib/Zudoku-DUsdmPME.js.map +1 -0
  93. package/lib/{ZudokuContext-BZB1TWdT.js → ZudokuContext-BBI06sOx.js} +5 -5
  94. package/lib/{ZudokuContext-BZB1TWdT.js.map → ZudokuContext-BBI06sOx.js.map} +1 -1
  95. package/lib/{circular-DFquXeY2.js → circular-D9tSKG2c.js} +1327 -1346
  96. package/lib/{circular-DFquXeY2.js.map → circular-D9tSKG2c.js.map} +1 -1
  97. package/lib/createServer-BprC4n85.js +13036 -0
  98. package/lib/createServer-BprC4n85.js.map +1 -0
  99. package/lib/{errors-rWHkzVTd.js → errors-7hgPDs1h.js} +3 -3
  100. package/lib/{errors-rWHkzVTd.js.map → errors-7hgPDs1h.js.map} +1 -1
  101. package/lib/{firebase-BmGU1FuD.js → firebase-Dwn-2ju-.js} +13 -13
  102. package/lib/firebase-Dwn-2ju-.js.map +1 -0
  103. package/lib/{hook-BGlHBdET.js → hook-ZEd1Es7D.js} +2 -2
  104. package/lib/{hook-BGlHBdET.js.map → hook-ZEd1Es7D.js.map} +1 -1
  105. package/lib/{index-BQB9hb6n.js → index-CyIW9rHv.js} +485 -480
  106. package/lib/{index-BQB9hb6n.js.map → index-CyIW9rHv.js.map} +1 -1
  107. package/lib/index-DAWHN3cH.js +86 -0
  108. package/lib/index-DAWHN3cH.js.map +1 -0
  109. package/lib/{index-DRBOFufT.js → index-Dxdhrp-I.js} +2 -2
  110. package/lib/{index-DRBOFufT.js.map → index-Dxdhrp-I.js.map} +1 -1
  111. package/lib/{index.esm-B_0dvNjB.js → index.esm-Ca5zvoff.js} +20 -20
  112. package/lib/{index.esm-B_0dvNjB.js.map → index.esm-Ca5zvoff.js.map} +1 -1
  113. package/lib/{index.esm-Cx8B1YJQ.js → index.esm-DG4KaDKR.js} +2 -2
  114. package/lib/index.esm-DG4KaDKR.js.map +1 -0
  115. package/lib/{invariant-BJAl77rw.js → invariant-B_t_F2s_.js} +3 -3
  116. package/lib/{invariant-BJAl77rw.js.map → invariant-B_t_F2s_.js.map} +1 -1
  117. package/lib/ui/SyntaxHighlight.js +3 -3
  118. package/lib/useExposedProps-CzTDfXfq.js +30 -0
  119. package/lib/useExposedProps-CzTDfXfq.js.map +1 -0
  120. package/lib/zudoku.__internal.js +1492 -1030
  121. package/lib/zudoku.__internal.js.map +1 -1
  122. package/lib/zudoku.auth-auth0.js +1 -1
  123. package/lib/zudoku.auth-azureb2c.js +4 -4
  124. package/lib/zudoku.auth-clerk.js +2 -2
  125. package/lib/zudoku.auth-firebase.js +5 -5
  126. package/lib/zudoku.auth-openid.js +5 -5
  127. package/lib/zudoku.auth-supabase.js +4 -4
  128. package/lib/zudoku.components.js +31 -29
  129. package/lib/zudoku.components.js.map +1 -1
  130. package/lib/zudoku.hooks.js +24 -11
  131. package/lib/zudoku.hooks.js.map +1 -1
  132. package/lib/zudoku.mermaid.js +4 -3
  133. package/lib/zudoku.mermaid.js.map +1 -1
  134. package/lib/zudoku.plugin-api-catalog.js +36 -32
  135. package/lib/zudoku.plugin-api-catalog.js.map +1 -1
  136. package/lib/zudoku.plugin-api-keys.js +131 -130
  137. package/lib/zudoku.plugin-api-keys.js.map +1 -1
  138. package/lib/zudoku.plugin-custom-pages.js +1 -1
  139. package/lib/zudoku.plugin-markdown.js +1 -1
  140. package/lib/zudoku.plugin-openapi.js +2 -2
  141. package/lib/zudoku.plugin-search-pagefind.js +2 -2
  142. package/lib/zudoku.plugins.js +9 -8
  143. package/lib/zudoku.plugins.js.map +1 -1
  144. package/package.json +11 -9
  145. package/src/lib/components/index.ts +19 -39
  146. package/src/lib/core/plugins.ts +21 -1
  147. package/src/lib/core/transform-config.ts +29 -0
  148. package/src/lib/hooks/index.ts +7 -16
  149. package/src/lib/oas/graphql/circular.test.ts +37 -2
  150. package/src/lib/oas/graphql/circular.ts +25 -51
  151. package/src/lib/oas/parser/index.ts +17 -6
  152. package/src/lib/plugins/openapi/OasProvider.tsx +10 -2
  153. package/src/lib/plugins/openapi/OperationList.tsx +4 -9
  154. package/src/lib/plugins/openapi/interfaces.ts +6 -1
  155. package/src/lib/plugins/openapi/util/getRoutes.tsx +4 -1
  156. package/src/lib/util/flattenAllOf.test.ts +2 -1
  157. package/src/lib/util/flattenAllOf.ts +0 -57
  158. package/src/lib/util/flattenAllOfProcessor.ts +58 -0
  159. package/src/lib/util/readFrontmatter.ts +2 -1
  160. package/src/shiki/langs/c3.js +1 -0
  161. package/src/shiki/langs/gn.js +1 -0
  162. package/src/shiki/langs/moonbit.js +1 -0
  163. package/src/zuplo/enrich-with-zuplo-mcp.ts +168 -0
  164. package/src/zuplo/enrich-with-zuplo.ts +254 -0
  165. package/src/zuplo/policy-types.ts +46 -0
  166. package/src/zuplo/with-zuplo-processors.ts +35 -0
  167. package/src/zuplo/with-zuplo.ts +14 -0
  168. package/lib/OasProvider-DSe-hk5Y.js +0 -40
  169. package/lib/OasProvider-DSe-hk5Y.js.map +0 -1
  170. package/lib/OperationList-CaknPbvq.js.map +0 -1
  171. package/lib/SyntaxHighlight-klTH8c6-.js.map +0 -1
  172. package/lib/___vite-browser-external_commonjs-proxy-BttVsNON.js +0 -9
  173. package/lib/___vite-browser-external_commonjs-proxy-BttVsNON.js.map +0 -1
  174. package/lib/createServer-BXZ0CAUn.js +0 -16693
  175. package/lib/createServer-BXZ0CAUn.js.map +0 -1
  176. package/lib/firebase-BmGU1FuD.js.map +0 -1
  177. package/lib/index-BDsEwofZ.js.map +0 -1
  178. package/lib/index-DBjOT2H1.js +0 -133
  179. package/lib/index-DBjOT2H1.js.map +0 -1
  180. package/lib/index.esm-Cx8B1YJQ.js.map +0 -1
@@ -1,46 +1,26 @@
1
- import { useMDXComponents as useMDXComponentsImport } from "@mdx-js/react";
2
- import { Helmet } from "@zudoku/react-helmet-async";
3
- import { useTheme as useThemeImport } from "next-themes";
4
- import { Link as LinkImport } from "react-router";
5
- import { useAuth as useAuthImport } from "../authentication/hook.js";
6
- import { Button as ButtonImport } from "../ui/Button.js";
7
- import { Callout as CalloutImport } from "../ui/Callout.js";
8
- import { ZudokuError as ZudokuErrorImport } from "../util/invariant.js";
9
- import { ClientOnly as ClientOnlyImport } from "./ClientOnly.js";
10
- import {
11
- CACHE_KEYS as CACHE_KEYS_IMPORT,
12
- useCache as useCacheImport,
13
- } from "./cache.js";
14
- import { useZudoku as useZudokuImport } from "./context/ZudokuContext.js";
15
- import { Heading as HeadingImport } from "./Heading.js";
16
- import { Markdown as MarkdownImport } from "./Markdown.js";
17
- import { Slot as SlotImport } from "./Slot.js";
18
- import { Spinner as SpinnerImport } from "./Spinner.js";
19
- import { Typography as TypographyImport } from "./Typography.js";
20
- import { Zudoku as ZudokuImport } from "./Zudoku.js";
1
+ export { Helmet as Head } from "@zudoku/react-helmet-async";
2
+ export { Link } from "react-router";
3
+ export { Button } from "../ui/Button.js";
4
+ export { Callout } from "../ui/Callout.js";
5
+ export { ZudokuError } from "../util/invariant.js";
6
+ export { ClientOnly } from "./ClientOnly.js";
7
+ export { Heading } from "./Heading.js";
8
+ export { Markdown } from "./Markdown.js";
9
+ export { Search } from "./Search.js";
10
+ export { Slot } from "./Slot.js";
11
+ export { Spinner } from "./Spinner.js";
12
+ export { Typography } from "./Typography.js";
13
+ export { Zudoku } from "./Zudoku.js";
21
14
 
22
- export const Head = /*@__PURE__*/ Helmet;
23
- export const Heading = /*@__PURE__*/ HeadingImport;
24
- export const Callout = /*@__PURE__*/ CalloutImport;
25
- export const Markdown = /*@__PURE__*/ MarkdownImport;
26
- export const Spinner = /*@__PURE__*/ SpinnerImport;
27
- export const ClientOnly = /*@__PURE__*/ ClientOnlyImport;
28
- export const Button = /*@__PURE__*/ ButtonImport;
29
- export const Link = /*@__PURE__*/ LinkImport;
30
- export const Zudoku = /*@__PURE__*/ ZudokuImport;
31
- export const Typography = /*@__PURE__*/ TypographyImport;
32
- export const Slot = /*@__PURE__*/ SlotImport;
33
- export const ZudokuError = /*@__PURE__*/ ZudokuErrorImport;
15
+ //
34
16
 
35
17
  /** @deprecated Import from `zudoku/hooks` instead */
36
- export const useMDXComponents = /*@__PURE__*/ useMDXComponentsImport;
18
+ export { useMDXComponents } from "@mdx-js/react";
37
19
  /** @deprecated Import from `zudoku/hooks` instead */
38
- export const useZudoku = /*@__PURE__*/ useZudokuImport;
20
+ export { useTheme } from "next-themes";
39
21
  /** @deprecated Import from `zudoku/hooks` instead */
40
- export const useAuth = /*@__PURE__*/ useAuthImport;
22
+ export { useAuth } from "../authentication/hook.js";
41
23
  /** @deprecated Import from `zudoku/hooks` instead */
42
- export const useCache = /*@__PURE__*/ useCacheImport;
24
+ export { CACHE_KEYS, useCache } from "./cache.js";
43
25
  /** @deprecated Import from `zudoku/hooks` instead */
44
- export const CACHE_KEYS = /*@__PURE__*/ CACHE_KEYS_IMPORT;
45
- /** @deprecated Import from `zudoku/hooks` instead */
46
- export const useTheme = /*@__PURE__*/ useThemeImport;
26
+ export { useZudoku } from "./context/ZudokuContext.js";
@@ -3,6 +3,7 @@ import type { ReactNode } from "react";
3
3
  import type { Location, RouteObject } from "react-router";
4
4
  import type { Navigation } from "../../config/validators/NavigationSchema.js";
5
5
  import type { ProtectedRoutesInput } from "../../config/validators/ProtectedRoutesSchema.js";
6
+ import type { ZudokuConfig } from "../../config/validators/validate.js";
6
7
  import type { AuthenticationPlugin } from "../authentication/authentication.js";
7
8
  import type { MdxComponentsType } from "../util/MdxComponents.js";
8
9
  import type {
@@ -18,7 +19,8 @@ export type ZudokuPlugin =
18
19
  | ApiIdentityPlugin
19
20
  | SearchProviderPlugin
20
21
  | EventConsumerPlugin
21
- | AuthenticationPlugin;
22
+ | AuthenticationPlugin
23
+ | TransformConfigPlugin;
22
24
 
23
25
  export type { AuthenticationPlugin, RouteObject };
24
26
 
@@ -60,6 +62,19 @@ export type ProfileNavigationItem = {
60
62
  icon?: LucideIcon;
61
63
  };
62
64
 
65
+ export interface ConfigHookContext {
66
+ mode: typeof process.env.ZUDOKU_ENV;
67
+ rootDir: string;
68
+ configPath: string;
69
+ }
70
+
71
+ export interface TransformConfigPlugin {
72
+ transformConfig?: (
73
+ config: ZudokuConfig,
74
+ ctx: ConfigHookContext,
75
+ ) => Partial<ZudokuConfig> | void | Promise<Partial<ZudokuConfig> | void>;
76
+ }
77
+
63
78
  export interface CommonPlugin {
64
79
  initialize?: (
65
80
  context: ZudokuContext,
@@ -110,3 +125,8 @@ export const isApiIdentityPlugin = (
110
125
  obj: ZudokuPlugin,
111
126
  ): obj is ApiIdentityPlugin =>
112
127
  "getIdentities" in obj && typeof obj.getIdentities === "function";
128
+
129
+ export const isTransformConfigPlugin = (
130
+ obj: ZudokuPlugin,
131
+ ): obj is TransformConfigPlugin =>
132
+ "transformConfig" in obj && typeof obj.transformConfig === "function";
@@ -0,0 +1,29 @@
1
+ import createDeepmerge from "@fastify/deepmerge";
2
+ import type { ConfigWithMeta } from "../../config/loader.js";
3
+ import { type ConfigHookContext, isTransformConfigPlugin } from "./plugins.js";
4
+
5
+ const mergeConfig = createDeepmerge({
6
+ mergeArray: (opt) => (_, source) => opt.clone(source),
7
+ });
8
+
9
+ export const runTransformConfigHooks = async (
10
+ config: ConfigWithMeta,
11
+ ): Promise<ConfigWithMeta> => {
12
+ const ctx = {
13
+ mode: config.__meta.mode,
14
+ rootDir: config.__meta.rootDir,
15
+ configPath: config.__meta.configPath,
16
+ } satisfies ConfigHookContext;
17
+ const plugins = config.plugins ?? [];
18
+
19
+ let result = config;
20
+
21
+ for (const plugin of plugins.filter(isTransformConfigPlugin)) {
22
+ const partial = await plugin.transformConfig?.(result, ctx);
23
+ if (!partial) continue;
24
+
25
+ result = mergeConfig(result, partial) as ConfigWithMeta;
26
+ }
27
+
28
+ return result;
29
+ };
@@ -1,16 +1,7 @@
1
- import { useMDXComponents as useMDXComponentsImport } from "@mdx-js/react";
2
- import { useTheme as useThemeImport } from "next-themes";
3
- import { useAuth as useAuthImport } from "../authentication/hook.js";
4
- import { CACHE_KEYS, useCache as useCacheImport } from "../components/cache.js";
5
- import { useZudoku as useZudokuImport } from "../components/context/ZudokuContext.js";
6
- import { useExposedProps as useExposedPropsImport } from "../util/useExposedProps.js";
7
- import { useEvent as useEventImport } from "./useEvent.js";
8
-
9
- export const useEvent = /*@__PURE__*/ useEventImport;
10
- export const useTheme = /*@__PURE__*/ useThemeImport;
11
- export const useExposedProps = /*@__PURE__*/ useExposedPropsImport;
12
- export const useMDXComponents = /*@__PURE__*/ useMDXComponentsImport;
13
- export const useAuth = /*@__PURE__*/ useAuthImport;
14
- export const useZudoku = /*@__PURE__*/ useZudokuImport;
15
- export const useCache = /*@__PURE__*/ useCacheImport;
16
- export { CACHE_KEYS };
1
+ export { useMDXComponents } from "@mdx-js/react";
2
+ export { useTheme } from "next-themes";
3
+ export { useAuth } from "../authentication/hook.js";
4
+ export { CACHE_KEYS, useCache } from "../components/cache.js";
5
+ export { useZudoku } from "../components/context/ZudokuContext.js";
6
+ export { useExposedProps } from "../util/useExposedProps.js";
7
+ export { useEvent } from "./useEvent.js";
@@ -156,16 +156,20 @@ describe("handleCircularRefs", () => {
156
156
  });
157
157
  });
158
158
 
159
- it("should deduplicate shared object instances with __$ref", () => {
159
+ it("should handle shared object instances with __$ref without marking circular", () => {
160
160
  const shared = { __$ref: "#/components/schemas/Foo", type: "string" };
161
161
  const obj = { a: shared, b: shared };
162
162
  const result = handleCircularRefs(obj);
163
163
 
164
+ // Both should return the cached result, not mark as circular
164
165
  expect(result.a).toEqual({
165
166
  __$ref: "#/components/schemas/Foo",
166
167
  type: "string",
167
168
  });
168
- expect(result.b).toBe(`${SCHEMA_REF_PREFIX}#/components/schemas/Foo`);
169
+ expect(result.b).toEqual({
170
+ __$ref: "#/components/schemas/Foo",
171
+ type: "string",
172
+ });
169
173
  });
170
174
 
171
175
  it("should mark circular ref with property name from path", () => {
@@ -183,4 +187,35 @@ describe("handleCircularRefs", () => {
183
187
 
184
188
  expect(result.properties.child.properties.back).toContain(CIRCULAR_REF);
185
189
  });
190
+
191
+ // Exact reproduction of #1869 - shared object instances with __$ref
192
+ it("should NOT mark shared object instances with __$ref as circular (issue #1869)", () => {
193
+ // When dereferencing, the SAME object instance is returned for all refs to the same schema
194
+ const timestampSchema = {
195
+ __$ref: "#/components/schemas/timestamp",
196
+ type: "string",
197
+ format: "date-time",
198
+ };
199
+
200
+ // Both created_at and updated_at point to the SAME object instance
201
+ const obj = {
202
+ type: "object",
203
+ properties: {
204
+ created_at: timestampSchema,
205
+ updated_at: timestampSchema,
206
+ },
207
+ };
208
+
209
+ const result = handleCircularRefs(obj);
210
+
211
+ // The first one should be fully expanded
212
+ expect(result.properties.created_at).toEqual({
213
+ __$ref: "#/components/schemas/timestamp",
214
+ type: "string",
215
+ format: "date-time",
216
+ });
217
+ // The second one should ALSO be fully expanded (not marked as circular)
218
+ expect(typeof result.properties.updated_at).toBe("object");
219
+ expect(result.properties.updated_at).not.toContain(CIRCULAR_REF);
220
+ });
186
221
  });
@@ -1,6 +1,5 @@
1
1
  import { GraphQLScalarType } from "graphql/index.js";
2
2
  import { GraphQLJSON } from "graphql-type-json";
3
- import type { RecordAny } from "../../util/traverse.js";
4
3
 
5
4
  export const CIRCULAR_REF = "$[Circular Reference]";
6
5
  export const SCHEMA_REF_PREFIX = "$ref:";
@@ -17,73 +16,48 @@ const OPENAPI_PROPS = new Set([
17
16
  export const handleCircularRefs = (
18
17
  // biome-ignore lint/suspicious/noExplicitAny: Allow any type
19
18
  obj: any,
20
- visited = new WeakSet(),
19
+ currentPath = new WeakSet(),
21
20
  refs = new WeakMap(),
22
21
  path: string[] = [],
23
- seenRefPaths = new Set<string>(),
22
+ currentRefPaths = new Set<string>(),
24
23
  // biome-ignore lint/suspicious/noExplicitAny: Allow any type
25
24
  ): any => {
26
25
  if (obj === null || typeof obj !== "object") return obj;
27
26
 
28
27
  const refPath = obj.__$ref;
28
+ const isCircular =
29
+ currentPath.has(obj) ||
30
+ (typeof refPath === "string" && currentRefPaths.has(refPath));
29
31
 
30
- // Check if this object has a __$ref marker (set during schema code generation)
31
- // If we've already fully processed this ref path, return a reference marker
32
- // instead of the full data to avoid JSON.stringify serializing duplicates
33
- if (typeof refPath === "string" && seenRefPaths.has(refPath)) {
34
- return SCHEMA_REF_PREFIX + refPath;
35
- }
36
-
37
- if (visited.has(obj)) {
38
- const cached = refs.get(obj);
39
- if (cached) {
40
- return typeof refPath === "string"
41
- ? // If already processed, return ref marker to avoid duplicate serialization
42
- SCHEMA_REF_PREFIX + refPath
43
- : cached;
44
- }
32
+ if (isCircular) {
33
+ if (typeof refPath === "string") return SCHEMA_REF_PREFIX + refPath;
45
34
  const circularProp = path.find((p) => !OPENAPI_PROPS.has(p)) || path[0];
46
-
47
35
  return [CIRCULAR_REF, circularProp].filter(Boolean).join(":");
48
36
  }
49
37
 
50
- visited.add(obj);
38
+ if (refs.has(obj)) return refs.get(obj);
51
39
 
52
- // Add refPath BEFORE recursing to detect cycles within this branch
53
- // This will be removed after processing to allow siblings with the same ref
54
- if (typeof refPath === "string") {
55
- seenRefPaths.add(refPath);
56
- }
40
+ currentPath.add(obj);
41
+ if (typeof refPath === "string") currentRefPaths.add(refPath);
57
42
 
58
- let result: RecordAny | RecordAny[];
59
- if (Array.isArray(obj)) {
60
- result = obj.map((item, index) =>
61
- handleCircularRefs(
62
- item,
63
- visited,
64
- refs,
65
- [...path, index.toString()],
66
- seenRefPaths,
67
- ),
43
+ const recurse = (value: unknown, key: string) =>
44
+ handleCircularRefs(
45
+ value,
46
+ currentPath,
47
+ refs,
48
+ [...path, key],
49
+ currentRefPaths,
68
50
  );
69
- } else {
70
- result = {};
71
- for (const [key, value] of Object.entries(obj)) {
72
- result[key] = handleCircularRefs(
73
- value,
74
- visited,
75
- refs,
76
- [...path, key],
77
- seenRefPaths,
51
+
52
+ const result = Array.isArray(obj)
53
+ ? obj.map((item, i) => recurse(item, i.toString()))
54
+ : Object.fromEntries(
55
+ Object.entries(obj).map(([k, v]) => [k, recurse(v, k)]),
78
56
  );
79
- }
80
- }
81
- refs.set(obj, result);
82
57
 
83
- // Remove refPath after processing so sibling refs aren't incorrectly marked
84
- if (typeof refPath === "string") {
85
- seenRefPaths.delete(refPath);
86
- }
58
+ refs.set(obj, result);
59
+ currentPath.delete(obj);
60
+ if (typeof refPath === "string") currentRefPaths.delete(refPath);
87
61
 
88
62
  return result;
89
63
  };
@@ -1,6 +1,7 @@
1
1
  import { GraphQLError } from "graphql/error/index.js";
2
2
  import { OpenAPIV3, type OpenAPIV3_1 } from "openapi-types";
3
- import { flattenAllOfProcessor } from "../../util/flattenAllOf.js";
3
+ import { flattenAllOf } from "../../util/flattenAllOf.js";
4
+ import { traverse } from "../../util/traverse.js";
4
5
  import { dereference, type JSONSchema } from "./dereference/index.js";
5
6
  import { upgradeSchema } from "./upgrade/index.js";
6
7
 
@@ -104,11 +105,21 @@ export const validate = async (schemaInput: unknown) => {
104
105
  const dereferenced = await dereference(schema);
105
106
  const upgraded = upgradeSchema(dereferenced);
106
107
 
107
- const flattened = await flattenAllOfProcessor({
108
- schema: upgraded,
109
- file: "schema.json",
110
- dereference: async (schema) => schema,
111
- });
108
+ const flattened = traverse(upgraded, (spec) => {
109
+ if (!spec || typeof spec !== "object" || Array.isArray(spec)) {
110
+ return spec;
111
+ }
112
+ const isSchemaObject =
113
+ "type" in spec ||
114
+ "properties" in spec ||
115
+ "allOf" in spec ||
116
+ "anyOf" in spec ||
117
+ "oneOf" in spec;
118
+
119
+ if (!isSchemaObject) return spec;
120
+
121
+ return flattenAllOf(spec) as typeof spec;
122
+ }) as OpenAPIDocument;
112
123
 
113
124
  return flattened;
114
125
  };
@@ -19,13 +19,21 @@ export const OasProvider = ({
19
19
  client: GraphQLClient;
20
20
  }) => {
21
21
  const value = useMemo(() => {
22
- const { versions: availableVersions, labels } = getVersionMetadata(config);
22
+ const {
23
+ versions: availableVersions,
24
+ labels,
25
+ downloadUrls,
26
+ } = getVersionMetadata(config);
23
27
  const currentVersion = version ?? availableVersions.at(0);
24
28
 
25
29
  const versionLinks = Object.fromEntries(
26
30
  availableVersions.map((id) => [
27
31
  id,
28
- { path: joinUrl(basePath, id), label: labels[id] ?? id },
32
+ {
33
+ path: joinUrl(basePath, id),
34
+ label: labels[id] ?? id,
35
+ downloadUrl: downloadUrls[id],
36
+ },
29
37
  ]),
30
38
  );
31
39
 
@@ -20,7 +20,6 @@ import { Heading } from "../../components/Heading.js";
20
20
  import { Markdown } from "../../components/Markdown.js";
21
21
  import { PagefindSearchMeta } from "../../components/PagefindSearchMeta.js";
22
22
  import { Pagination } from "../../components/Pagination.js";
23
- import { joinUrl } from "../../util/joinUrl.js";
24
23
  import { useCreateQuery } from "./client/useCreateQuery.js";
25
24
  import { useOasConfig } from "./context.js";
26
25
  import { DownloadSchemaButton } from "./DownloadSchemaButton.js";
@@ -151,11 +150,6 @@ const OperationsForTagQuery = graphql(/* GraphQL */ `
151
150
 
152
151
  const LAZY_OPERATION_LIST_THRESHOLD = 30;
153
152
 
154
- const getFileExtension = (filename: string): string => {
155
- const lastDotIndex = filename.lastIndexOf(".");
156
- return lastDotIndex !== -1 ? filename.slice(lastDotIndex) : "";
157
- };
158
-
159
153
  export const OperationList = ({
160
154
  tag,
161
155
  untagged,
@@ -163,7 +157,7 @@ export const OperationList = ({
163
157
  tag?: string;
164
158
  untagged?: boolean;
165
159
  }) => {
166
- const { path, input, type, versions, version, options } = useOasConfig();
160
+ const { input, type, versions, version, options } = useOasConfig();
167
161
  const { tag: tagFromParams } = useParams<"tag">();
168
162
  const query = useCreateQuery(OperationsForTagQuery, {
169
163
  input,
@@ -252,11 +246,12 @@ export const OperationList = ({
252
246
  const tagTitle = schema.tag.extensions?.["x-displayName"] ?? schema.tag.name;
253
247
  const helmetTitle = [tagTitle, title].filter(Boolean).join(" - ");
254
248
 
249
+ const currentVersion = version != null ? versions[version] : undefined;
255
250
  const downloadUrl =
256
251
  typeof input === "string"
257
252
  ? type === "url"
258
253
  ? input
259
- : joinUrl(path, version, `schema${getFileExtension(input)}`)
254
+ : currentVersion?.downloadUrl
260
255
  : undefined;
261
256
 
262
257
  return (
@@ -291,7 +286,7 @@ export const OperationList = ({
291
286
  {showVersions && (
292
287
  <span className="text-xl text-muted-foreground ms-1.5">
293
288
  {" "}
294
- ({version})
289
+ ({schema.version})
295
290
  </span>
296
291
  )}
297
292
  </Heading>
@@ -7,6 +7,8 @@ type DynamicInput = () => Promise<unknown>;
7
7
 
8
8
  export type VersionedInput<T> = Array<{
9
9
  path: string;
10
+ version?: string;
11
+ downloadUrl?: string;
10
12
  label?: string;
11
13
  input: T;
12
14
  }>;
@@ -85,5 +87,8 @@ export type OasPluginConfig = BaseOasConfig & OasSource;
85
87
  export type OasPluginContext = BaseOasConfig &
86
88
  ContextOasSource & {
87
89
  version?: string;
88
- versions: Record<string, { path: string; label: string }>;
90
+ versions: Record<
91
+ string,
92
+ { path: string; label: string; downloadUrl?: string }
93
+ >;
89
94
  };
@@ -150,7 +150,7 @@ const createVersionRoutes = (
150
150
 
151
151
  export const getVersionMetadata = (config: OasPluginConfig) => {
152
152
  if (config.type === "raw" || !Array.isArray(config.input)) {
153
- return { versions: [], labels: {} };
153
+ return { versions: [], labels: {}, downloadUrls: {} };
154
154
  }
155
155
 
156
156
  return {
@@ -158,6 +158,9 @@ export const getVersionMetadata = (config: OasPluginConfig) => {
158
158
  labels: Object.fromEntries(
159
159
  config.input.map((v) => [v.path, v.label ?? v.path]),
160
160
  ),
161
+ downloadUrls: Object.fromEntries(
162
+ config.input.map((v) => [v.path, v.downloadUrl]),
163
+ ),
161
164
  };
162
165
  };
163
166
 
@@ -1,7 +1,8 @@
1
1
  import type { JSONSchema7 } from "json-schema";
2
2
  import { describe, expect, it, vi } from "vitest";
3
3
  import type { OpenAPIDocument } from "../oas/parser/index.js";
4
- import { flattenAllOf, flattenAllOfProcessor } from "./flattenAllOf.js";
4
+ import { flattenAllOf } from "./flattenAllOf.js";
5
+ import { flattenAllOfProcessor } from "./flattenAllOfProcessor.js";
5
6
  import invariant from "./invariant.js";
6
7
 
7
8
  describe("flattenAllOf", () => {
@@ -1,4 +1,3 @@
1
- import { $RefParser } from "@apidevtools/json-schema-ref-parser";
2
1
  import {
3
2
  createComparator,
4
3
  createMerger,
@@ -9,62 +8,6 @@ import {
9
8
  createIntersector,
10
9
  } from "@x0k/json-schema-merge/lib/array";
11
10
  import type { JSONSchema7Definition } from "json-schema";
12
- import type { Processor } from "../../config/validators/BuildSchema.js";
13
- import type { OpenAPIDocument } from "../oas/parser/index.js";
14
- import { type RecordAny, traverse } from "./traverse.js";
15
-
16
- export const flattenAllOfProcessor: Processor = async ({ schema, file }) => {
17
- try {
18
- // Resolve refs once - creates a lookup table without modifying the schema
19
- const parser = new $RefParser();
20
- await parser.resolve(schema);
21
- const $refs = parser.$refs;
22
-
23
- const flattened = traverse(schema, (spec) => {
24
- if (!spec || typeof spec !== "object" || Array.isArray(spec)) {
25
- return spec;
26
- }
27
-
28
- const isSchemaObject =
29
- "type" in spec ||
30
- "properties" in spec ||
31
- "allOf" in spec ||
32
- "anyOf" in spec ||
33
- "oneOf" in spec;
34
-
35
- if (!isSchemaObject) return spec;
36
-
37
- if ("allOf" in spec && Array.isArray(spec.allOf)) {
38
- const resolvedAllOf = spec.allOf.map((item) => {
39
- if (
40
- item &&
41
- typeof item === "object" &&
42
- "$ref" in item &&
43
- typeof item.$ref === "string"
44
- ) {
45
- try {
46
- return $refs.get(item.$ref) ?? item;
47
- } catch {
48
- return item;
49
- }
50
- }
51
- return item;
52
- });
53
- return flattenAllOf({ ...spec, allOf: resolvedAllOf }) as RecordAny;
54
- }
55
-
56
- return flattenAllOf(spec) as RecordAny;
57
- }) as OpenAPIDocument;
58
-
59
- return flattened;
60
- } catch (error) {
61
- // biome-ignore lint/suspicious/noConsole: Logging allowed here
62
- console.warn(
63
- `Failed to flatten \`allOf\` in ${file}: ${error instanceof Error ? error.message : error}`,
64
- );
65
- return schema;
66
- }
67
- };
68
11
 
69
12
  const { compareSchemaDefinitions, compareSchemaValues } = createComparator();
70
13
  const { mergeArrayOfSchemaDefinitions } = createMerger({
@@ -0,0 +1,58 @@
1
+ import { $RefParser } from "@apidevtools/json-schema-ref-parser";
2
+ import type { Processor } from "../../config/validators/BuildSchema.js";
3
+ import type { OpenAPIDocument } from "../oas/parser/index.js";
4
+ import { flattenAllOf } from "./flattenAllOf.js";
5
+ import { type RecordAny, traverse } from "./traverse.js";
6
+
7
+ export const flattenAllOfProcessor: Processor = async ({ schema, file }) => {
8
+ try {
9
+ // Resolve refs once - creates a lookup table without modifying the schema
10
+ const parser = new $RefParser();
11
+ await parser.resolve(schema);
12
+ const $refs = parser.$refs;
13
+
14
+ const flattened = traverse(schema, (spec) => {
15
+ if (!spec || typeof spec !== "object" || Array.isArray(spec)) {
16
+ return spec;
17
+ }
18
+
19
+ const isSchemaObject =
20
+ "type" in spec ||
21
+ "properties" in spec ||
22
+ "allOf" in spec ||
23
+ "anyOf" in spec ||
24
+ "oneOf" in spec;
25
+
26
+ if (!isSchemaObject) return spec;
27
+
28
+ if ("allOf" in spec && Array.isArray(spec.allOf)) {
29
+ const resolvedAllOf = spec.allOf.map((item) => {
30
+ if (
31
+ item &&
32
+ typeof item === "object" &&
33
+ "$ref" in item &&
34
+ typeof item.$ref === "string"
35
+ ) {
36
+ try {
37
+ return $refs.get(item.$ref) ?? item;
38
+ } catch {
39
+ return item;
40
+ }
41
+ }
42
+ return item;
43
+ });
44
+ return flattenAllOf({ ...spec, allOf: resolvedAllOf }) as RecordAny;
45
+ }
46
+
47
+ return flattenAllOf(spec) as RecordAny;
48
+ }) as OpenAPIDocument;
49
+
50
+ return flattened;
51
+ } catch (error) {
52
+ // biome-ignore lint/suspicious/noConsole: Logging allowed here
53
+ console.warn(
54
+ `Failed to flatten \`allOf\` in ${file}: ${error instanceof Error ? error.message : error}`,
55
+ );
56
+ return schema;
57
+ }
58
+ };
@@ -9,5 +9,6 @@ export const yaml = {
9
9
 
10
10
  export const readFrontmatter = async (filePath: string) => {
11
11
  const content = await readFile(filePath, "utf-8");
12
- return matter(content, { engines: { yaml } });
12
+ const normalizedContent = content.replace(/\r\n/g, "\n");
13
+ return matter(normalizedContent, { engines: { yaml } });
13
14
  };
@@ -0,0 +1 @@
1
+ export { default } from "@shikijs/langs/c3";
@@ -0,0 +1 @@
1
+ export { default } from "@shikijs/langs/gn";
@@ -0,0 +1 @@
1
+ export { default } from "@shikijs/langs/moonbit";