zudoku 0.0.0-z6bcd96da → 0.0.0-z9d382b4a

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 (52) 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 +75 -75
  4. package/dist/config/validators/validate.d.ts +6 -6
  5. package/dist/lib/core/plugins.d.ts +11 -1
  6. package/dist/lib/core/plugins.js +1 -0
  7. package/dist/lib/core/plugins.js.map +1 -1
  8. package/dist/lib/core/transform-config.d.ts +2 -0
  9. package/dist/lib/core/transform-config.js +22 -0
  10. package/dist/lib/core/transform-config.js.map +1 -0
  11. package/dist/lib/oas/graphql/circular.d.ts +1 -1
  12. package/dist/lib/oas/graphql/circular.js +18 -35
  13. package/dist/lib/oas/graphql/circular.js.map +1 -1
  14. package/dist/lib/oas/graphql/circular.test.js +33 -2
  15. package/dist/lib/oas/graphql/circular.test.js.map +1 -1
  16. package/dist/lib/ui/Command.d.ts +3 -3
  17. package/dist/lib/util/readFrontmatter.js +2 -1
  18. package/dist/lib/util/readFrontmatter.js.map +1 -1
  19. package/dist/vite/build.js +91 -73
  20. package/dist/vite/build.js.map +1 -1
  21. package/dist/vite/mdx/remark-inject-filepath.js +5 -1
  22. package/dist/vite/mdx/remark-inject-filepath.js.map +1 -1
  23. package/dist/vite/mdx/remark-link-rewrite.js +3 -2
  24. package/dist/vite/mdx/remark-link-rewrite.js.map +1 -1
  25. package/dist/vite/plugin-docs.js +9 -7
  26. package/dist/vite/plugin-docs.js.map +1 -1
  27. package/dist/vite/plugin-markdown-export.js +4 -2
  28. package/dist/vite/plugin-markdown-export.js.map +1 -1
  29. package/lib/{OasProvider-CS_ASmBB.js → OasProvider-B2KxIBsI.js} +2 -2
  30. package/lib/{OasProvider-CS_ASmBB.js.map → OasProvider-B2KxIBsI.js.map} +1 -1
  31. package/lib/{OperationList-Dq_AB4W9.js → OperationList-C2tAfThO.js} +3 -3
  32. package/lib/{OperationList-Dq_AB4W9.js.map → OperationList-C2tAfThO.js.map} +1 -1
  33. package/lib/{SchemaList-BJZJv1gD.js → SchemaList-Ep8DleP_.js} +3 -3
  34. package/lib/{SchemaList-BJZJv1gD.js.map → SchemaList-Ep8DleP_.js.map} +1 -1
  35. package/lib/{SchemaView-U4JMYB3N.js → SchemaView-BpaEKRYx.js} +2 -2
  36. package/lib/{SchemaView-U4JMYB3N.js.map → SchemaView-BpaEKRYx.js.map} +1 -1
  37. package/lib/{circular-BmMJjG1v.js → circular-CG3e0_Uz.js} +1327 -1346
  38. package/lib/{circular-BmMJjG1v.js.map → circular-CG3e0_Uz.js.map} +1 -1
  39. package/lib/{createServer-CLSZ7hWJ.js → createServer-CNeRqj98.js} +3 -3
  40. package/lib/{createServer-CLSZ7hWJ.js.map → createServer-CNeRqj98.js.map} +1 -1
  41. package/lib/firebase-BCXX7Qv5.js.map +1 -1
  42. package/lib/{index-O9RHI87z.js → index-I3kmZ7tG.js} +6 -6
  43. package/lib/{index-O9RHI87z.js.map → index-I3kmZ7tG.js.map} +1 -1
  44. package/lib/zudoku.plugin-openapi.js +1 -1
  45. package/lib/zudoku.plugins.js +9 -8
  46. package/lib/zudoku.plugins.js.map +1 -1
  47. package/package.json +3 -2
  48. package/src/lib/core/plugins.ts +21 -1
  49. package/src/lib/core/transform-config.ts +29 -0
  50. package/src/lib/oas/graphql/circular.test.ts +37 -2
  51. package/src/lib/oas/graphql/circular.ts +25 -51
  52. package/src/lib/util/readFrontmatter.ts +2 -1
@@ -3,7 +3,7 @@ import "lucide-react";
3
3
  import "./chunk-EPOLDU6W-C6C8jAwd.js";
4
4
  import "./ui/Button.js";
5
5
  import "./ZudokuContext-BZB1TWdT.js";
6
- import { y as e, U as n, z as s } from "./index-O9RHI87z.js";
6
+ import { y as e, U as n, z as s } from "./index-I3kmZ7tG.js";
7
7
  export {
8
8
  e as GetNavigationOperationsQuery,
9
9
  n as UNTAGGED_PATH,
@@ -1,15 +1,16 @@
1
- const e = (n) => n, t = (n) => n, i = (n) => "events" in n && typeof n.events == "object", o = (n) => "getProfileMenuItems" in n && typeof n.getProfileMenuItems == "function", s = (n) => "getRoutes" in n && typeof n.getRoutes == "function", c = (n) => "signUp" in n && typeof n.signUp == "function", u = (n) => "renderSearch" in n && typeof n.renderSearch == "function", g = (n) => "initialize" in n && typeof n.initialize == "function", f = (n) => "getHead" in n && typeof n.getHead == "function", r = (n) => "getMdxComponents" in n && typeof n.getMdxComponents == "function", a = (n) => "getIdentities" in n && typeof n.getIdentities == "function";
1
+ const t = (n) => n, i = (n) => n, e = (n) => "events" in n && typeof n.events == "object", o = (n) => "getProfileMenuItems" in n && typeof n.getProfileMenuItems == "function", s = (n) => "getRoutes" in n && typeof n.getRoutes == "function", f = (n) => "signUp" in n && typeof n.signUp == "function", c = (n) => "renderSearch" in n && typeof n.renderSearch == "function", u = (n) => "initialize" in n && typeof n.initialize == "function", g = (n) => "getHead" in n && typeof n.getHead == "function", r = (n) => "getMdxComponents" in n && typeof n.getMdxComponents == "function", a = (n) => "getIdentities" in n && typeof n.getIdentities == "function", l = (n) => "transformConfig" in n && typeof n.transformConfig == "function";
2
2
  export {
3
- e as createApiIdentityPlugin,
4
- t as createProfileMenuPlugin,
5
- f as hasHead,
3
+ t as createApiIdentityPlugin,
4
+ i as createProfileMenuPlugin,
5
+ g as hasHead,
6
6
  a as isApiIdentityPlugin,
7
- c as isAuthenticationPlugin,
8
- i as isEventConsumerPlugin,
7
+ f as isAuthenticationPlugin,
8
+ e as isEventConsumerPlugin,
9
9
  r as isMdxProviderPlugin,
10
10
  s as isNavigationPlugin,
11
11
  o as isProfileMenuPlugin,
12
- u as isSearchPlugin,
13
- g as needsInitialization
12
+ c as isSearchPlugin,
13
+ l as isTransformConfigPlugin,
14
+ u as needsInitialization
14
15
  };
15
16
  //# sourceMappingURL=zudoku.plugins.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"zudoku.plugins.js","sources":["../src/lib/core/plugins.ts"],"sourcesContent":["import type { LucideIcon } from \"lucide-react\";\nimport type { ReactNode } from \"react\";\nimport type { Location, RouteObject } from \"react-router\";\nimport type { Navigation } from \"../../config/validators/NavigationSchema.js\";\nimport type { ProtectedRoutesInput } from \"../../config/validators/ProtectedRoutesSchema.js\";\nimport type { AuthenticationPlugin } from \"../authentication/authentication.js\";\nimport type { MdxComponentsType } from \"../util/MdxComponents.js\";\nimport type {\n ApiIdentity,\n ZudokuContext,\n ZudokuEvents,\n} from \"./ZudokuContext.js\";\n\nexport type ZudokuPlugin =\n | CommonPlugin\n | ProfileMenuPlugin\n | NavigationPlugin\n | ApiIdentityPlugin\n | SearchProviderPlugin\n | EventConsumerPlugin\n | AuthenticationPlugin;\n\nexport type { AuthenticationPlugin, RouteObject };\n\nexport interface NavigationPlugin {\n getRoutes: () => RouteObject[];\n getNavigation?: (path: string, context: ZudokuContext) => Promise<Navigation>;\n getProtectedRoutes?: () => ProtectedRoutesInput;\n}\n\nexport const createApiIdentityPlugin = (\n plugin: ApiIdentityPlugin,\n): ApiIdentityPlugin => plugin;\n\nexport const createProfileMenuPlugin = (\n plugin: ProfileMenuPlugin,\n): ProfileMenuPlugin => plugin;\n\nexport interface ApiIdentityPlugin {\n getIdentities: (context: ZudokuContext) => Promise<ApiIdentity[]>;\n}\n\nexport interface SearchProviderPlugin {\n renderSearch: (o: {\n isOpen: boolean;\n onClose: () => void;\n }) => React.JSX.Element | null;\n}\n\nexport interface ProfileMenuPlugin {\n getProfileMenuItems: (context: ZudokuContext) => ProfileNavigationItem[];\n}\n\nexport type ProfileNavigationItem = {\n label: string;\n path?: string;\n weight?: number;\n category?: \"top\" | \"middle\" | \"bottom\";\n children?: ProfileNavigationItem[];\n icon?: LucideIcon;\n};\n\nexport interface CommonPlugin {\n initialize?: (\n context: ZudokuContext,\n ) => Promise<void | boolean> | void | boolean;\n getHead?: (args: { location: Location }) => ReactNode | undefined;\n getMdxComponents?: () => MdxComponentsType;\n}\n\nexport type EventConsumerPlugin<Event extends ZudokuEvents = ZudokuEvents> = {\n events: { [K in keyof Event]?: Event[K] };\n};\n\nexport const isEventConsumerPlugin = (\n obj: ZudokuPlugin,\n): obj is EventConsumerPlugin =>\n \"events\" in obj && typeof obj.events === \"object\";\n\nexport const isProfileMenuPlugin = (\n obj: ZudokuPlugin,\n): obj is ProfileMenuPlugin =>\n \"getProfileMenuItems\" in obj && typeof obj.getProfileMenuItems === \"function\";\n\nexport const isNavigationPlugin = (\n obj: ZudokuPlugin,\n): obj is NavigationPlugin =>\n \"getRoutes\" in obj && typeof obj.getRoutes === \"function\";\n\nexport const isAuthenticationPlugin = (\n obj: ZudokuPlugin,\n): obj is AuthenticationPlugin =>\n \"signUp\" in obj && typeof obj.signUp === \"function\";\n\nexport const isSearchPlugin = (\n obj: ZudokuPlugin,\n): obj is SearchProviderPlugin =>\n \"renderSearch\" in obj && typeof obj.renderSearch === \"function\";\n\nexport const needsInitialization = (obj: ZudokuPlugin): obj is CommonPlugin =>\n \"initialize\" in obj && typeof obj.initialize === \"function\";\n\nexport const hasHead = (obj: ZudokuPlugin): obj is CommonPlugin =>\n \"getHead\" in obj && typeof obj.getHead === \"function\";\n\nexport const isMdxProviderPlugin = (obj: ZudokuPlugin): obj is CommonPlugin =>\n \"getMdxComponents\" in obj && typeof obj.getMdxComponents === \"function\";\n\nexport const isApiIdentityPlugin = (\n obj: ZudokuPlugin,\n): obj is ApiIdentityPlugin =>\n \"getIdentities\" in obj && typeof obj.getIdentities === \"function\";\n"],"names":["createApiIdentityPlugin","plugin","createProfileMenuPlugin","isEventConsumerPlugin","obj","isProfileMenuPlugin","isNavigationPlugin","isAuthenticationPlugin","isSearchPlugin","needsInitialization","hasHead","isMdxProviderPlugin","isApiIdentityPlugin"],"mappings":"AA8BO,MAAMA,IAA0B,CACrCC,MACsBA,GAEXC,IAA0B,CACrCD,MACsBA,GAsCXE,IAAwB,CACnCC,MAEA,YAAYA,KAAO,OAAOA,EAAI,UAAW,UAE9BC,IAAsB,CACjCD,MAEA,yBAAyBA,KAAO,OAAOA,EAAI,uBAAwB,YAExDE,IAAqB,CAChCF,MAEA,eAAeA,KAAO,OAAOA,EAAI,aAAc,YAEpCG,IAAyB,CACpCH,MAEA,YAAYA,KAAO,OAAOA,EAAI,UAAW,YAE9BI,IAAiB,CAC5BJ,MAEA,kBAAkBA,KAAO,OAAOA,EAAI,gBAAiB,YAE1CK,IAAsB,CAACL,MAClC,gBAAgBA,KAAO,OAAOA,EAAI,cAAe,YAEtCM,IAAU,CAACN,MACtB,aAAaA,KAAO,OAAOA,EAAI,WAAY,YAEhCO,IAAsB,CAACP,MAClC,sBAAsBA,KAAO,OAAOA,EAAI,oBAAqB,YAElDQ,IAAsB,CACjCR,MAEA,mBAAmBA,KAAO,OAAOA,EAAI,iBAAkB;"}
1
+ {"version":3,"file":"zudoku.plugins.js","sources":["../src/lib/core/plugins.ts"],"sourcesContent":["import type { LucideIcon } from \"lucide-react\";\nimport type { ReactNode } from \"react\";\nimport type { Location, RouteObject } from \"react-router\";\nimport type { Navigation } from \"../../config/validators/NavigationSchema.js\";\nimport type { ProtectedRoutesInput } from \"../../config/validators/ProtectedRoutesSchema.js\";\nimport type { ZudokuConfig } from \"../../config/validators/validate.js\";\nimport type { AuthenticationPlugin } from \"../authentication/authentication.js\";\nimport type { MdxComponentsType } from \"../util/MdxComponents.js\";\nimport type {\n ApiIdentity,\n ZudokuContext,\n ZudokuEvents,\n} from \"./ZudokuContext.js\";\n\nexport type ZudokuPlugin =\n | CommonPlugin\n | ProfileMenuPlugin\n | NavigationPlugin\n | ApiIdentityPlugin\n | SearchProviderPlugin\n | EventConsumerPlugin\n | AuthenticationPlugin\n | TransformConfigPlugin;\n\nexport type { AuthenticationPlugin, RouteObject };\n\nexport interface NavigationPlugin {\n getRoutes: () => RouteObject[];\n getNavigation?: (path: string, context: ZudokuContext) => Promise<Navigation>;\n getProtectedRoutes?: () => ProtectedRoutesInput;\n}\n\nexport const createApiIdentityPlugin = (\n plugin: ApiIdentityPlugin,\n): ApiIdentityPlugin => plugin;\n\nexport const createProfileMenuPlugin = (\n plugin: ProfileMenuPlugin,\n): ProfileMenuPlugin => plugin;\n\nexport interface ApiIdentityPlugin {\n getIdentities: (context: ZudokuContext) => Promise<ApiIdentity[]>;\n}\n\nexport interface SearchProviderPlugin {\n renderSearch: (o: {\n isOpen: boolean;\n onClose: () => void;\n }) => React.JSX.Element | null;\n}\n\nexport interface ProfileMenuPlugin {\n getProfileMenuItems: (context: ZudokuContext) => ProfileNavigationItem[];\n}\n\nexport type ProfileNavigationItem = {\n label: string;\n path?: string;\n weight?: number;\n category?: \"top\" | \"middle\" | \"bottom\";\n children?: ProfileNavigationItem[];\n icon?: LucideIcon;\n};\n\nexport interface ConfigHookContext {\n mode: typeof process.env.ZUDOKU_ENV;\n rootDir: string;\n configPath: string;\n}\n\nexport interface TransformConfigPlugin {\n transformConfig?: (\n config: ZudokuConfig,\n ctx: ConfigHookContext,\n ) => Partial<ZudokuConfig> | void | Promise<Partial<ZudokuConfig> | void>;\n}\n\nexport interface CommonPlugin {\n initialize?: (\n context: ZudokuContext,\n ) => Promise<void | boolean> | void | boolean;\n getHead?: (args: { location: Location }) => ReactNode | undefined;\n getMdxComponents?: () => MdxComponentsType;\n}\n\nexport type EventConsumerPlugin<Event extends ZudokuEvents = ZudokuEvents> = {\n events: { [K in keyof Event]?: Event[K] };\n};\n\nexport const isEventConsumerPlugin = (\n obj: ZudokuPlugin,\n): obj is EventConsumerPlugin =>\n \"events\" in obj && typeof obj.events === \"object\";\n\nexport const isProfileMenuPlugin = (\n obj: ZudokuPlugin,\n): obj is ProfileMenuPlugin =>\n \"getProfileMenuItems\" in obj && typeof obj.getProfileMenuItems === \"function\";\n\nexport const isNavigationPlugin = (\n obj: ZudokuPlugin,\n): obj is NavigationPlugin =>\n \"getRoutes\" in obj && typeof obj.getRoutes === \"function\";\n\nexport const isAuthenticationPlugin = (\n obj: ZudokuPlugin,\n): obj is AuthenticationPlugin =>\n \"signUp\" in obj && typeof obj.signUp === \"function\";\n\nexport const isSearchPlugin = (\n obj: ZudokuPlugin,\n): obj is SearchProviderPlugin =>\n \"renderSearch\" in obj && typeof obj.renderSearch === \"function\";\n\nexport const needsInitialization = (obj: ZudokuPlugin): obj is CommonPlugin =>\n \"initialize\" in obj && typeof obj.initialize === \"function\";\n\nexport const hasHead = (obj: ZudokuPlugin): obj is CommonPlugin =>\n \"getHead\" in obj && typeof obj.getHead === \"function\";\n\nexport const isMdxProviderPlugin = (obj: ZudokuPlugin): obj is CommonPlugin =>\n \"getMdxComponents\" in obj && typeof obj.getMdxComponents === \"function\";\n\nexport const isApiIdentityPlugin = (\n obj: ZudokuPlugin,\n): obj is ApiIdentityPlugin =>\n \"getIdentities\" in obj && typeof obj.getIdentities === \"function\";\n\nexport const isTransformConfigPlugin = (\n obj: ZudokuPlugin,\n): obj is TransformConfigPlugin =>\n \"transformConfig\" in obj && typeof obj.transformConfig === \"function\";\n"],"names":["createApiIdentityPlugin","plugin","createProfileMenuPlugin","isEventConsumerPlugin","obj","isProfileMenuPlugin","isNavigationPlugin","isAuthenticationPlugin","isSearchPlugin","needsInitialization","hasHead","isMdxProviderPlugin","isApiIdentityPlugin","isTransformConfigPlugin"],"mappings":"AAgCO,MAAMA,IAA0B,CACrCC,MACsBA,GAEXC,IAA0B,CACrCD,MACsBA,GAmDXE,IAAwB,CACnCC,MAEA,YAAYA,KAAO,OAAOA,EAAI,UAAW,UAE9BC,IAAsB,CACjCD,MAEA,yBAAyBA,KAAO,OAAOA,EAAI,uBAAwB,YAExDE,IAAqB,CAChCF,MAEA,eAAeA,KAAO,OAAOA,EAAI,aAAc,YAEpCG,IAAyB,CACpCH,MAEA,YAAYA,KAAO,OAAOA,EAAI,UAAW,YAE9BI,IAAiB,CAC5BJ,MAEA,kBAAkBA,KAAO,OAAOA,EAAI,gBAAiB,YAE1CK,IAAsB,CAACL,MAClC,gBAAgBA,KAAO,OAAOA,EAAI,cAAe,YAEtCM,IAAU,CAACN,MACtB,aAAaA,KAAO,OAAOA,EAAI,WAAY,YAEhCO,IAAsB,CAACP,MAClC,sBAAsBA,KAAO,OAAOA,EAAI,oBAAqB,YAElDQ,IAAsB,CACjCR,MAEA,mBAAmBA,KAAO,OAAOA,EAAI,iBAAkB,YAE5CS,IAA0B,CACrCT,MAEA,qBAAqBA,KAAO,OAAOA,EAAI,mBAAoB;"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zudoku",
3
- "version": "0.0.0-z6bcd96da",
3
+ "version": "0.0.0-z9d382b4a",
4
4
  "type": "module",
5
5
  "homepage": "https://zudoku.dev",
6
6
  "repository": {
@@ -154,6 +154,7 @@
154
154
  "dependencies": {
155
155
  "@apidevtools/json-schema-ref-parser": "14.1.1",
156
156
  "@envelop/core": "5.3.2",
157
+ "@fastify/deepmerge": "3.1.0",
157
158
  "@graphql-typed-document-node/core": "3.2.0",
158
159
  "@lekoarts/rehype-meta-as-attributes": "3.0.3",
159
160
  "@mdx-js/react": "3.1.1",
@@ -232,7 +233,7 @@
232
233
  "pagefind": "1.5.0-beta.1",
233
234
  "picocolors": "1.1.1",
234
235
  "piscina": "5.1.4",
235
- "posthog-node": "5.14.1",
236
+ "posthog-node": "5.21.2",
236
237
  "react-error-boundary": "6.0.0",
237
238
  "react-hook-form": "7.66.0",
238
239
  "react-is": "19.2.3",
@@ -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
+ };
@@ -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
  };
@@ -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
  };