jamdesk 1.1.90 → 1.1.91

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 (46) hide show
  1. package/package.json +1 -1
  2. package/vendored/app/api/r2/[project]/[...path]/route.ts +14 -9
  3. package/vendored/app/layout.tsx +2 -2
  4. package/vendored/components/HtmlLangSync.tsx +3 -2
  5. package/vendored/components/mdx/Accordion.tsx +1 -1
  6. package/vendored/components/mdx/Card.tsx +1 -1
  7. package/vendored/components/mdx/CodeGroup.tsx +18 -23
  8. package/vendored/components/mdx/Color.tsx +0 -1
  9. package/vendored/components/mdx/Icon.tsx +1 -1
  10. package/vendored/components/mdx/MDXComponents.tsx +92 -66
  11. package/vendored/components/mdx/OpenApiEndpoint.tsx +0 -1
  12. package/vendored/components/mdx/ParamField.tsx +0 -1
  13. package/vendored/components/mdx/RequestExample.tsx +0 -1
  14. package/vendored/components/mdx/ResponseExample.tsx +0 -1
  15. package/vendored/components/mdx/Steps.tsx +12 -3
  16. package/vendored/components/mdx/Table.tsx +8 -2
  17. package/vendored/components/mdx/Tabs.tsx +1 -1
  18. package/vendored/components/mdx/Tree.tsx +6 -4
  19. package/vendored/components/navigation/Header.tsx +7 -5
  20. package/vendored/components/navigation/LanguageSelector.tsx +32 -7
  21. package/vendored/components/navigation/TableOfContents.tsx +1 -1
  22. package/vendored/components/navigation/TabsNav.tsx +17 -5
  23. package/vendored/components/search/SearchModal.tsx +41 -36
  24. package/vendored/components/ui/CodePanel.tsx +2 -2
  25. package/vendored/hooks/useChat.ts +1 -1
  26. package/vendored/hooks/useShikiHighlight.ts +7 -1
  27. package/vendored/lib/code-utils.ts +6 -2
  28. package/vendored/lib/health-checks.ts +2 -2
  29. package/vendored/lib/language-utils.ts +53 -2
  30. package/vendored/lib/layout-helpers.tsx +2 -1
  31. package/vendored/lib/mdx-inline-components.ts +1 -1
  32. package/vendored/lib/navigation-resolver.ts +0 -69
  33. package/vendored/lib/normalize-config.ts +1 -1
  34. package/vendored/lib/openapi/generator.ts +3 -3
  35. package/vendored/lib/openapi/parser.ts +14 -6
  36. package/vendored/lib/openapi/validator.ts +2 -2
  37. package/vendored/lib/openapi-isr.ts +4 -1
  38. package/vendored/lib/public-paths-resolver.ts +7 -6
  39. package/vendored/lib/redis.ts +2 -2
  40. package/vendored/lib/rehype-code-meta.ts +2 -2
  41. package/vendored/lib/render-doc-page.tsx +2 -2
  42. package/vendored/lib/seo.ts +21 -6
  43. package/vendored/lib/shiki-highlighter.ts +1 -1
  44. package/vendored/lib/snippet-loader-isr.ts +1 -1
  45. package/vendored/lib/validate-config.ts +1 -0
  46. package/vendored/workspace-package-lock.json +10 -10
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import { useState, useEffect, useMemo, useRef } from 'react';
3
+ import { useState, useEffect, useMemo, useRef, useCallback } from 'react';
4
4
  import { useOnClickOutside } from '@/hooks/useOnClickOutside';
5
5
  import Link from 'next/link';
6
6
  import { usePathname } from 'next/navigation';
@@ -95,8 +95,10 @@ export function Header({ config, layout = 'header-logo', tabsPosition: tabsPosit
95
95
  return resolveNavigation(config, pathname);
96
96
  }, [config, pathname, hasTabs, layout, hasMultipleLanguages]);
97
97
 
98
- // Simple check if current path belongs to a tab (without full navigation resolution)
99
- const pathBelongsToTab = (tab: typeof navigationTabs[0], currentPath: string): boolean => {
98
+ // Simple check if current path belongs to a tab (without full navigation resolution).
99
+ // Pure over its arguments wrapped in useCallback so it's stable for the
100
+ // tab list useMemo below.
101
+ const pathBelongsToTab = useCallback((tab: typeof navigationTabs[0], currentPath: string): boolean => {
100
102
  const path = currentPath.replace(/^\/docs\/?/, '');
101
103
 
102
104
  // Check tab's groups
@@ -124,7 +126,7 @@ export function Header({ config, layout = 'header-logo', tabsPosition: tabsPosit
124
126
  }
125
127
 
126
128
  return false;
127
- };
129
+ }, []);
128
130
 
129
131
  // Build tabs data for header
130
132
  const headerTabs = useMemo(() => {
@@ -175,7 +177,7 @@ export function Header({ config, layout = 'header-logo', tabsPosition: tabsPosit
175
177
  isExternal: false,
176
178
  };
177
179
  });
178
- }, [navigationTabs, resolved, hasTabs, pathname, linkPrefix]);
180
+ }, [navigationTabs, resolved, hasTabs, pathname, linkPrefix, pathBelongsToTab]);
179
181
 
180
182
  // Detect dark mode for logo switching
181
183
  useEffect(() => {
@@ -11,6 +11,7 @@ import {
11
11
  saveLanguagePreference,
12
12
  getLanguagePreference,
13
13
  extractLanguageFromPath,
14
+ toHreflang,
14
15
  } from '@/lib/language-utils';
15
16
  import { useLinkPrefix } from '@/lib/link-prefix-context';
16
17
  import { getUiStrings } from '@/lib/ui-strings';
@@ -51,13 +52,36 @@ export function LanguageSelector({
51
52
  const isNavigating = pendingPathname !== null;
52
53
  const showSpinner = spinnerPathname !== null;
53
54
 
55
+ // Dedupe entries that collapse onto the same BCP 47 tag (e.g. customer
56
+ // declared both `cn` and `zh-Hans`, which would render two identical
57
+ // "简体中文" rows). Prefer the entry whose code already IS the BCP 47 form;
58
+ // fall back to the first occurrence. The server-side warning in
59
+ // buildHreflangAlternates is the authoritative signal — this is the UX
60
+ // safety net so users don't see duplicate rows in the meantime.
61
+ const displayedLanguages = (() => {
62
+ const byTag = new Map<string, ResolvedLanguage>();
63
+ for (const lang of languages) {
64
+ const tag = toHreflang(lang.code);
65
+ const existing = byTag.get(tag);
66
+ if (!existing) {
67
+ byTag.set(tag, lang);
68
+ continue;
69
+ }
70
+ if (existing.code !== tag && lang.code === tag) byTag.set(tag, lang);
71
+ }
72
+ return [...byTag.values()];
73
+ })();
74
+
54
75
  // Find current language
55
- const currentLanguage = languages.find((l) => l.isActive) || languages[0];
76
+ const currentLanguage = displayedLanguages.find((l) => l.isActive) || displayedLanguages[0];
56
77
 
57
78
  // Find actual default language from config
58
- const actualDefault = languages.find((l) => l.isDefault)?.code || defaultLanguage;
79
+ const actualDefault = displayedLanguages.find((l) => l.isDefault)?.code || defaultLanguage;
59
80
 
60
- // Check localStorage preference on mount and redirect if needed
81
+ // Check localStorage preference on mount and redirect if needed.
82
+ // Intentionally mount-only: re-running on pathname/language changes would
83
+ // fight the user's in-session navigation choices (they may have explicitly
84
+ // switched languages this session, which we don't want to revert).
61
85
  useEffect(() => {
62
86
  const savedPref = getLanguagePreference();
63
87
  if (savedPref && currentLanguage && savedPref !== currentLanguage.code) {
@@ -68,18 +92,20 @@ export function LanguageSelector({
68
92
  if (!hasLangInPath) {
69
93
  const basePath = transformLanguagePath(
70
94
  pathname || '/docs',
71
- actualDefault,
72
95
  savedPref,
73
96
  actualDefault
74
97
  );
75
98
  router.replace(`${linkPrefix}${basePath}`);
76
99
  }
77
100
  }
78
- }, []); // Only run once on mount
101
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- mount-only by design; see comment above the effect
102
+ }, []);
79
103
 
80
104
  // Handle click outside to close
81
105
  useOnClickOutside(containerRef, () => setIsOpen(false), isOpen);
82
106
 
107
+ // Defined before handleKeyDown so handleKeyDown can include it in its
108
+ // dependency array without TDZ issues.
83
109
  const handleSelectLanguage = useCallback(
84
110
  (lang: ResolvedLanguage) => {
85
111
  // Block double-fire while a previous switch is still navigating —
@@ -95,7 +121,6 @@ export function LanguageSelector({
95
121
 
96
122
  const basePath = transformLanguagePath(
97
123
  pathname || '/docs',
98
- currentLanguage?.code || actualDefault,
99
124
  lang.code,
100
125
  actualDefault
101
126
  );
@@ -228,7 +253,7 @@ export function LanguageSelector({
228
253
  `}
229
254
  style={{ boxShadow: 'var(--shadow-lg)' }}
230
255
  >
231
- {languages.map((lang, index) => (
256
+ {displayedLanguages.map((lang, index) => (
232
257
  <li key={lang.code}>
233
258
  <button
234
259
  role="option"
@@ -494,7 +494,7 @@ export function TableOfContents({ content, className = '' }: TableOfContentsProp
494
494
  return () => {
495
495
  scrollTarget.removeEventListener('scroll', computeActive);
496
496
  };
497
- // eslint-disable-next-line react-hooks/exhaustive-deps
497
+
498
498
  }, [headings, isHidden]);
499
499
 
500
500
  if (headings.length === 0 || isHidden) {
@@ -4,7 +4,12 @@ import { useMemo } from 'react';
4
4
  import Link from 'next/link';
5
5
  import { usePathname } from 'next/navigation';
6
6
  // Icons use Font Awesome CSS classes for lightweight rendering
7
- import type { DocsConfig, TabsPosition } from '@/lib/docs-types';
7
+ import type {
8
+ DocsConfig,
9
+ GroupConfig,
10
+ NavigationPage,
11
+ TabsPosition,
12
+ } from '@/lib/docs-types';
8
13
  import { resolveNavigation } from '@/lib/navigation-resolver';
9
14
  import { getIconClass } from '@/lib/icon-utils';
10
15
  import { getTheme } from '@/themes';
@@ -87,14 +92,21 @@ export function TabsNav({ config, className = '' }: TabsNavProps) {
87
92
  const currentPath = pathname.replace(/^\/docs\/?/, '').replace(/^\//, '');
88
93
  let isActive = false;
89
94
 
90
- const checkPages = (pages: any[]): boolean => {
95
+ const checkPages = (pages: (NavigationPage | GroupConfig)[]): boolean => {
91
96
  for (const page of pages) {
92
- const pagePath = typeof page === 'string' ? page : page.page || page.group;
93
- if (pagePath === currentPath || currentPath.startsWith(pagePath + '/')) {
97
+ const pagePath =
98
+ typeof page === 'string'
99
+ ? page
100
+ : 'page' in page
101
+ ? page.page
102
+ : 'group' in page
103
+ ? page.group
104
+ : undefined;
105
+ if (pagePath && (pagePath === currentPath || currentPath.startsWith(pagePath + '/'))) {
94
106
  return true;
95
107
  }
96
108
  // Check nested groups
97
- if (page.pages) {
109
+ if (typeof page !== 'string' && 'pages' in page && page.pages) {
98
110
  if (checkPages(page.pages)) return true;
99
111
  }
100
112
  }
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import { useEffect, useState, useRef, type ReactElement } from 'react';
3
+ import { useEffect, useState, useRef, useCallback, type ReactElement } from 'react';
4
4
  import { usePathname, useRouter } from 'next/navigation';
5
5
  import { getRecentSearches, addRecentSearch, clearRecentSearches } from '@/lib/recent-searches';
6
6
  import { useFocusTrap } from '@/hooks/useFocusTrap';
@@ -237,6 +237,45 @@ export function SearchModal({ isOpen, onClose, popularPages, onNavigate }: Searc
237
237
  el?.scrollIntoView({ block: 'nearest' });
238
238
  }, [selectedIndex, results.length]);
239
239
 
240
+ // Click handler for a result row. Defined before the keyboard-navigation
241
+ // effect so it can be a stable dep without TDZ issues.
242
+ const handleResultClick = useCallback(
243
+ (result: SearchResult, index: number) => {
244
+ if (query.trim()) {
245
+ // Track search_query event (the search itself)
246
+ trackSearch(projectSlug, {
247
+ type: 'search_query',
248
+ query: query.trim(),
249
+ resultsCount: results.length,
250
+ });
251
+
252
+ // Track search_click event (the result they clicked)
253
+ trackSearch(projectSlug, {
254
+ type: 'search_click',
255
+ query: query.trim(),
256
+ resultsCount: results.length,
257
+ clickedResult: {
258
+ slug: result.slug,
259
+ position: index + 1, // 1-indexed for analytics
260
+ title: result.title,
261
+ },
262
+ });
263
+ hasTrackedRef.current = true; // Mark as tracked to avoid duplicate on modal close
264
+
265
+ // Save to recent searches
266
+ addRecentSearch(projectSlug, query.trim());
267
+ }
268
+
269
+ const url = `${linkPrefix}/${result.slug}${result.section ? `#${result.section.toLowerCase().replace(/\s+/g, '-')}` : ''}`;
270
+ onNavigate?.(url);
271
+ router.push(url);
272
+ onClose();
273
+ setQuery('');
274
+ setResults([]);
275
+ },
276
+ [query, projectSlug, results, linkPrefix, onNavigate, router, onClose]
277
+ );
278
+
240
279
  // Keyboard navigation
241
280
  useEffect(() => {
242
281
  if (!isOpen) return;
@@ -297,41 +336,7 @@ export function SearchModal({ isOpen, onClose, popularPages, onNavigate }: Searc
297
336
 
298
337
  document.addEventListener('keydown', handleKeyDown);
299
338
  return () => document.removeEventListener('keydown', handleKeyDown);
300
- }, [isOpen, query, recentSearches, results, selectedIndex, effectivePopularPages, popularIndex, router, onClose, onNavigate, projectSlug]);
301
-
302
- const handleResultClick = (result: SearchResult, index: number) => {
303
- if (query.trim()) {
304
- // Track search_query event (the search itself)
305
- trackSearch(projectSlug, {
306
- type: 'search_query',
307
- query: query.trim(),
308
- resultsCount: results.length,
309
- });
310
-
311
- // Track search_click event (the result they clicked)
312
- trackSearch(projectSlug, {
313
- type: 'search_click',
314
- query: query.trim(),
315
- resultsCount: results.length,
316
- clickedResult: {
317
- slug: result.slug,
318
- position: index + 1, // 1-indexed for analytics
319
- title: result.title,
320
- },
321
- });
322
- hasTrackedRef.current = true; // Mark as tracked to avoid duplicate on modal close
323
-
324
- // Save to recent searches
325
- addRecentSearch(projectSlug, query.trim());
326
- }
327
-
328
- const url = `${linkPrefix}/${result.slug}${result.section ? `#${result.section.toLowerCase().replace(/\s+/g, '-')}` : ''}`;
329
- onNavigate?.(url);
330
- router.push(url);
331
- onClose();
332
- setQuery('');
333
- setResults([]);
334
- };
339
+ }, [isOpen, query, recentSearches, results, selectedIndex, effectivePopularPages, popularIndex, router, onClose, onNavigate, projectSlug, handleResultClick, isSearching, linkPrefix]);
335
340
 
336
341
  const handleClearRecentSearches = () => {
337
342
  clearRecentSearches(projectSlug);
@@ -83,12 +83,12 @@ export function extractTextContent(node: ReactNode): string {
83
83
  if (!node) return '';
84
84
 
85
85
  if (isValidElement(node)) {
86
- const props = node.props as any;
86
+ const props = node.props as { children?: ReactNode };
87
87
  // If it's a pre element, get the code element's children
88
88
  if (node.type === 'pre') {
89
89
  const codeElement = props?.children;
90
90
  if (isValidElement(codeElement)) {
91
- const codeProps = codeElement.props as any;
91
+ const codeProps = codeElement.props as { children?: ReactNode };
92
92
  return extractTextContent(codeProps?.children);
93
93
  }
94
94
  }
@@ -105,7 +105,7 @@ export function useChat(endpoint = '/_chat'): {
105
105
  useEffect(() => {
106
106
  const lastUser = messagesRef.current.filter(m => m.role === 'user').at(-1);
107
107
  if (lastUser) lastUserMessageRef.current = lastUser.content;
108
- // eslint-disable-next-line react-hooks/exhaustive-deps
108
+
109
109
  }, []);
110
110
 
111
111
  /** Unlock the input: the request is done (or errored out). The assistant
@@ -57,6 +57,11 @@ export function useShikiHighlightMultiple(
57
57
  );
58
58
  const [isLoading, setIsLoading] = useState(true);
59
59
 
60
+ // Serialize items for the effect dep array. Using a stable string key
61
+ // avoids re-running the highlighter on every parent render that produces
62
+ // a new (but value-equal) `items` array.
63
+ const itemsKey = JSON.stringify(items);
64
+
60
65
  useEffect(() => {
61
66
  let cancelled = false;
62
67
 
@@ -85,7 +90,8 @@ export function useShikiHighlightMultiple(
85
90
  return () => {
86
91
  cancelled = true;
87
92
  };
88
- }, [JSON.stringify(items)]);
93
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- itemsKey is the stable serialization of items; using items directly would re-fire on every parent render
94
+ }, [itemsKey]);
89
95
 
90
96
  return { results, isLoading };
91
97
  }
@@ -34,9 +34,13 @@ export function formatLanguage(lang: string): string {
34
34
  }
35
35
 
36
36
  /**
37
- * Helper for accessing React element props
38
- * Uses `any` to allow property access without type guards
37
+ * Helper for accessing React element props.
38
+ * Many MDX call sites read .className/.children with string ops without
39
+ * narrowing first; widening the value type to `unknown` would force a
40
+ * type-narrow at every site. Keep `any` here only.
39
41
  */
42
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- MDX consumer call sites do unchecked string ops on prop values; narrowing here would require touching dozens of components
40
43
  export function getElementProps(element: ReactElement): Record<string, any> {
44
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- see function-level comment
41
45
  return (element.props || {}) as Record<string, any>;
42
46
  }
@@ -45,7 +45,7 @@ export async function checkMemoryHealth(): Promise<MemoryHealth> {
45
45
  percentUsed: stats.peakPercentage !== null ? Math.round(stats.peakPercentage) : null,
46
46
  available,
47
47
  };
48
- } catch (error) {
48
+ } catch {
49
49
  return {
50
50
  status: 'critical',
51
51
  heapUsedMB: 0,
@@ -119,7 +119,7 @@ export async function checkDiskHealth(path: string): Promise<DiskHealth> {
119
119
  percentUsed,
120
120
  available: isUnlimited ? `${available} (tmpfs)` : available,
121
121
  };
122
- } catch (error) {
122
+ } catch {
123
123
  return {
124
124
  status: 'critical',
125
125
  path,
@@ -199,6 +199,59 @@ export const LANGUAGE_FLAGS: Record<LanguageCode, string> = {
199
199
  he: '🇮🇱',
200
200
  };
201
201
 
202
+ /**
203
+ * Maps internal LanguageCode values to valid IETF BCP 47 tags for use in
204
+ * `<link rel="alternate" hreflang>` and `<html lang>` attributes.
205
+ *
206
+ * Why: Customer projects use directory-aligned codes like `cn/`, `jp/` that
207
+ * are first-class LanguageCode values for internal routing, but `cn` and `jp`
208
+ * are not valid BCP 47 language tags. Google silently drops alternates with
209
+ * invalid tags, so the Chinese (or `jp`-named Japanese) page gets no hreflang
210
+ * benefit. Remap at emit time to keep file layouts stable.
211
+ *
212
+ * One-way map only — never add the canonical form (e.g. `'zh-Hans': 'cn'`)
213
+ * as a key; downstream callers assume the output is BCP 47-valid.
214
+ */
215
+ const HREFLANG_REMAP: Partial<Record<LanguageCode, string>> = {
216
+ cn: 'zh-Hans', // not a BCP 47 tag; Chinese Simplified is zh-Hans
217
+ jp: 'ja', // not a BCP 47 tag; Japanese is ja
218
+ 'ja-jp': 'ja-JP', // lowercase region subtag is not valid BCP 47
219
+ 'fr-ca': 'fr-CA', // lowercase region subtag is not valid BCP 47
220
+ };
221
+
222
+ /**
223
+ * Convert an internal LanguageCode to a valid BCP 47 tag for SEO emission.
224
+ * Codes not in HREFLANG_REMAP pass through unchanged.
225
+ */
226
+ export function toHreflang(code: LanguageCode): string {
227
+ return HREFLANG_REMAP[code] ?? code;
228
+ }
229
+
230
+ /**
231
+ * Detect customer-config errors where two distinct LanguageCode entries
232
+ * collapse to the same BCP 47 tag after toHreflang — e.g. declaring both
233
+ * `cn` and `zh-Hans`, or both `ja` and `jp`. When this happens, the
234
+ * second entry silently overwrites the first in the hreflang alternates
235
+ * map, dropping a translation from search-engine discovery.
236
+ *
237
+ * Returns one entry per colliding tag, listing the internal codes that
238
+ * collapsed onto it. Empty array means no collision.
239
+ */
240
+ export function findHreflangAliasCollisions(
241
+ codes: LanguageCode[]
242
+ ): Array<{ tag: string; codes: LanguageCode[] }> {
243
+ const byTag = new Map<string, LanguageCode[]>();
244
+ for (const code of codes) {
245
+ const tag = toHreflang(code);
246
+ const existing = byTag.get(tag);
247
+ if (existing) existing.push(code);
248
+ else byTag.set(tag, [code]);
249
+ }
250
+ return [...byTag.entries()]
251
+ .filter(([, group]) => group.length > 1)
252
+ .map(([tag, group]) => ({ tag, codes: group }));
253
+ }
254
+
202
255
  /**
203
256
  * RTL (Right-to-Left) languages
204
257
  */
@@ -246,14 +299,12 @@ export function getLanguageDisplayInfo(code: LanguageCode): LanguageDisplayInfo
246
299
  * - transformPath('/docs/introduction', 'en', 'es') → '/es/introduction' (strips /docs prefix)
247
300
  *
248
301
  * @param currentPath - Current pathname
249
- * @param fromLang - Current language code (or 'en' for default)
250
302
  * @param toLang - Target language code
251
303
  * @param defaultLang - The default language (typically 'en')
252
304
  * @returns Transformed path for the target language (relative to app root)
253
305
  */
254
306
  export function transformLanguagePath(
255
307
  currentPath: string,
256
- fromLang: LanguageCode,
257
308
  toLang: LanguageCode,
258
309
  defaultLang: LanguageCode = 'en'
259
310
  ): string {
@@ -26,6 +26,7 @@ import { LinkPrefixProvider } from '@/lib/link-prefix-context';
26
26
  import { ProjectSlugProvider } from '@/lib/project-slug-context';
27
27
  import { getAnalyticsScript } from '@/lib/analytics-script';
28
28
  import { fetchCustomCss, fetchCustomJs } from '@/lib/r2-content';
29
+ import { toHreflang } from '@/lib/language-utils';
29
30
 
30
31
  const scrollLockBootstrap = `
31
32
  (function() {
@@ -314,7 +315,7 @@ export async function DocsChrome({
314
315
  preload('/_jd/fonts/fontawesome/webfonts/fa-brands-400.woff2', { as: 'font', type: 'font/woff2', crossOrigin: 'anonymous' });
315
316
 
316
317
  return (
317
- <html lang={lang} dir={dir} suppressHydrationWarning data-scroll-behavior="smooth" data-scroll-locked="true">
318
+ <html lang={toHreflang(lang)} dir={dir} suppressHydrationWarning data-scroll-behavior="smooth" data-scroll-locked="true">
318
319
  <head>
319
320
  {/*
320
321
  SSR scroll lock — prevents Chrome's same-tab cross-origin "preserve
@@ -121,7 +121,7 @@ export function compileInlineComponents(
121
121
  // IMPORTANT: React hooks are intentionally NOT provided here. Inline components
122
122
  // are server-rendered during Next.js static export, and hooks only work client-side.
123
123
  // Users who need hooks should use snippet files with 'use client' directive instead.
124
- // eslint-disable-next-line @typescript-eslint/no-implied-eval
124
+
125
125
  const createComponent = new Function(
126
126
  'React',
127
127
  '_jsx',
@@ -9,11 +9,9 @@
9
9
 
10
10
  import type {
11
11
  DocsConfig,
12
- NavigationConfig,
13
12
  NavigationPage,
14
13
  NavigationPageObject,
15
14
  GroupConfig,
16
- AnchorConfig,
17
15
  TabConfig,
18
16
  LanguageCode,
19
17
  LanguageConfig,
@@ -228,50 +226,6 @@ function resolveGroup(group: GroupConfig): ResolvedGroup {
228
226
  };
229
227
  }
230
228
 
231
- /**
232
- * Resolve an anchor configuration
233
- */
234
- function resolveAnchor(anchor: AnchorConfig): ResolvedAnchor {
235
- const groups: ResolvedGroup[] = [];
236
-
237
- // If anchor has groups, resolve them
238
- if (anchor.groups) {
239
- for (const group of anchor.groups) {
240
- groups.push(resolveGroup(group));
241
- }
242
- }
243
-
244
- // If anchor has pages directly, create a default group
245
- if (anchor.pages && anchor.pages.length > 0) {
246
- const { resolvedPages, nestedGroups, items } = resolvePages(anchor.pages);
247
- if (resolvedPages.length > 0 || nestedGroups.length > 0) {
248
- groups.push({
249
- name: '', // No group name for direct pages
250
- pages: resolvedPages,
251
- nested: nestedGroups.length > 0 ? nestedGroups : undefined,
252
- items: items.length > 0 ? items : undefined,
253
- });
254
- }
255
- }
256
-
257
- // If anchor has tabs, we need to resolve those too
258
- // For now, we flatten tabs into groups
259
- if (anchor.tabs) {
260
- for (const tab of anchor.tabs) {
261
- const tabGroups = resolveTabGroups(tab);
262
- groups.push(...tabGroups);
263
- }
264
- }
265
-
266
- return {
267
- name: anchor.anchor,
268
- icon: getIconName(anchor.icon),
269
- href: anchor.href,
270
- isExternal: !!anchor.href && !anchor.groups && !anchor.pages && !anchor.tabs,
271
- groups,
272
- };
273
- }
274
-
275
229
  /**
276
230
  * Resolve groups from a tab configuration
277
231
  */
@@ -311,29 +265,6 @@ function resolveTab(tab: TabConfig): ResolvedTab {
311
265
  };
312
266
  }
313
267
 
314
- /**
315
- * Find active anchor based on pathname
316
- */
317
- function findActiveAnchor(anchors: ResolvedAnchor[], pathname: string): string | undefined {
318
- // Remove /docs prefix and leading slash to match page paths (stored without leading slash)
319
- const path = pathname.replace(/^\/docs\/?/, '').replace(/^\//, '');
320
-
321
- // Find anchor whose groups contain a page matching the path
322
- for (const anchor of anchors) {
323
- if (anchor.isExternal) continue;
324
-
325
- for (const group of anchor.groups) {
326
- if (groupContainsPath(group, path)) {
327
- return anchor.name;
328
- }
329
- }
330
- }
331
-
332
- // Default to first non-external anchor
333
- const firstAnchor = anchors.find(a => !a.isExternal);
334
- return firstAnchor?.name;
335
- }
336
-
337
268
  /**
338
269
  * Find active tab based on pathname
339
270
  */
@@ -84,7 +84,7 @@ export function normalizeConfig(config: DocsConfigInput): NormalizeResult {
84
84
  }
85
85
 
86
86
  // 4. Warn about navbar.style
87
- if ((rest.navbar as any)?.style) {
87
+ if ((rest.navbar as { style?: unknown } | undefined)?.style) {
88
88
  warnings.push(
89
89
  'navbar.style is ignored. All navbars use the default style.'
90
90
  );
@@ -5,8 +5,8 @@
5
5
  * Supports generating pages from navigation openapi field.
6
6
  */
7
7
 
8
- import type { OpenAPI, OpenAPIV3 } from 'openapi-types';
9
- import type { GeneratedPage, HttpMethod, OpenApiEndpointData } from './types';
8
+ import type { OpenAPIV3 } from 'openapi-types';
9
+ import type { GeneratedPage, OpenApiEndpointData } from './types';
10
10
  import { parseEndpoint, getAllEndpoints, getSpecInfo } from './parser';
11
11
  import { getCachedSpec } from './cache';
12
12
 
@@ -94,7 +94,7 @@ export async function generatePagesFromSpec(
94
94
 
95
95
  for (const { method, path, operation, tags } of endpoints) {
96
96
  // Skip hidden endpoints (x-hidden extension)
97
- if ((operation as any)['x-hidden']) {
97
+ if ((operation as { 'x-hidden'?: unknown })['x-hidden']) {
98
98
  continue;
99
99
  }
100
100
 
@@ -193,8 +193,9 @@ function parseServers(api: OpenAPI.Document): ServerInfo[] {
193
193
 
194
194
  // Swagger 2.0
195
195
  if ('host' in api && typeof api.host === 'string') {
196
- const scheme = (api as any).schemes?.[0] || 'https';
197
- const basePath = (api as any).basePath || '';
196
+ const swagger2 = api as { schemes?: string[]; basePath?: string };
197
+ const scheme = swagger2.schemes?.[0] || 'https';
198
+ const basePath = swagger2.basePath || '';
198
199
  return [{
199
200
  url: `${scheme}://${api.host}${basePath}`,
200
201
  }];
@@ -229,9 +230,16 @@ function parseSecuritySchemes(api: OpenAPI.Document): Map<string, SecurityRequir
229
230
 
230
231
  // Swagger 2.0
231
232
  if ('securityDefinitions' in api) {
232
- const defs = (api as any).securityDefinitions;
233
+ type Swagger2SecurityScheme = {
234
+ type: SecurityRequirement['type'];
235
+ scheme?: string;
236
+ in?: SecurityRequirement['in'];
237
+ name?: string;
238
+ };
239
+ const defs = (api as { securityDefinitions?: Record<string, Swagger2SecurityScheme> })
240
+ .securityDefinitions;
233
241
  for (const [name, scheme] of Object.entries(defs || {})) {
234
- const s = scheme as any;
242
+ const s = scheme as Swagger2SecurityScheme;
235
243
  schemes.set(name, {
236
244
  name,
237
245
  type: s.type,
@@ -319,7 +327,7 @@ function parseRequestBody(
319
327
  content[mediaType] = {
320
328
  schema: toJsonSchema(mediaObj.schema),
321
329
  example: mediaObj.example,
322
- examples: mediaObj.examples as any,
330
+ examples: mediaObj.examples as Record<string, { value: unknown; summary?: string }> | undefined,
323
331
  };
324
332
  }
325
333
 
@@ -346,7 +354,7 @@ function parseResponses(
346
354
  content[mediaType] = {
347
355
  schema: toJsonSchema(mediaObj.schema),
348
356
  example: mediaObj.example,
349
- examples: mediaObj.examples as any,
357
+ examples: mediaObj.examples as Record<string, { value: unknown; summary?: string }> | undefined,
350
358
  };
351
359
  }
352
360
 
@@ -41,12 +41,12 @@ function checkDuplicateOperationIds(
41
41
  specPath: string
42
42
  ): OpenApiValidationError | null {
43
43
  const seen = new Map<string, string>(); // operationId → "METHOD /path"
44
- const paths = (api as any).paths || {};
44
+ const paths = (api as { paths?: Record<string, unknown> }).paths || {};
45
45
  const methods = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options'];
46
46
 
47
47
  for (const [pathStr, pathItem] of Object.entries(paths)) {
48
48
  for (const method of methods) {
49
- const operation = (pathItem as Record<string, any>)?.[method];
49
+ const operation = (pathItem as Record<string, { operationId?: string } | undefined> | undefined)?.[method];
50
50
  if (!operation?.operationId) continue;
51
51
 
52
52
  const id = operation.operationId;
@@ -69,7 +69,10 @@ export async function fetchOpenApiSpecFromR2(
69
69
  const isYaml = /\.ya?ml$/i.test(specPath);
70
70
  const raw = isYaml ? yaml.load(content) : JSON.parse(content);
71
71
 
72
- // Dereference all $ref pointers (matching static mode's SwaggerParser.validate behavior)
72
+ // Dereference all $ref pointers (matching static mode's SwaggerParser.validate behavior).
73
+ // SwaggerParser.dereference accepts a loose Document type that does not line up with
74
+ // our internal OpenApiSpec; intermediate `any` cast is required at this library boundary.
75
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- SwaggerParser type interop (see CLAUDE.md memory)
73
76
  const spec = await SwaggerParser.dereference(raw as any) as unknown as OpenApiSpec;
74
77
 
75
78
  // Cache it