jamdesk 1.1.89 → 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 (85) hide show
  1. package/dist/__tests__/integration/validate.integration.test.js +2 -1
  2. package/dist/__tests__/integration/validate.integration.test.js.map +1 -1
  3. package/dist/__tests__/unit/dev-workspace-symlinks.test.d.ts +2 -0
  4. package/dist/__tests__/unit/dev-workspace-symlinks.test.d.ts.map +1 -0
  5. package/dist/__tests__/unit/dev-workspace-symlinks.test.js +112 -0
  6. package/dist/__tests__/unit/dev-workspace-symlinks.test.js.map +1 -0
  7. package/dist/__tests__/unit/docs-config-discovery.test.d.ts +2 -0
  8. package/dist/__tests__/unit/docs-config-discovery.test.d.ts.map +1 -0
  9. package/dist/__tests__/unit/docs-config-discovery.test.js +190 -0
  10. package/dist/__tests__/unit/docs-config-discovery.test.js.map +1 -0
  11. package/dist/__tests__/unit/docs-config.test.js +2 -1
  12. package/dist/__tests__/unit/docs-config.test.js.map +1 -1
  13. package/dist/__tests__/unit/language-filter.test.d.ts +2 -0
  14. package/dist/__tests__/unit/language-filter.test.d.ts.map +1 -0
  15. package/dist/__tests__/unit/language-filter.test.js +166 -0
  16. package/dist/__tests__/unit/language-filter.test.js.map +1 -0
  17. package/dist/__tests__/unit/output.test.d.ts +2 -0
  18. package/dist/__tests__/unit/output.test.d.ts.map +1 -0
  19. package/dist/__tests__/unit/output.test.js +61 -0
  20. package/dist/__tests__/unit/output.test.js.map +1 -0
  21. package/dist/commands/dev.d.ts.map +1 -1
  22. package/dist/commands/dev.js +4 -1
  23. package/dist/commands/dev.js.map +1 -1
  24. package/dist/commands/doctor.d.ts.map +1 -1
  25. package/dist/commands/doctor.js +14 -12
  26. package/dist/commands/doctor.js.map +1 -1
  27. package/dist/commands/validate.d.ts.map +1 -1
  28. package/dist/commands/validate.js +14 -2
  29. package/dist/commands/validate.js.map +1 -1
  30. package/dist/lib/docs-config.d.ts +54 -3
  31. package/dist/lib/docs-config.d.ts.map +1 -1
  32. package/dist/lib/docs-config.js +126 -8
  33. package/dist/lib/docs-config.js.map +1 -1
  34. package/dist/lib/language-filter.d.ts +31 -0
  35. package/dist/lib/language-filter.d.ts.map +1 -0
  36. package/dist/lib/language-filter.js +14 -0
  37. package/dist/lib/language-filter.js.map +1 -0
  38. package/package.json +1 -1
  39. package/vendored/app/api/r2/[project]/[...path]/route.ts +14 -9
  40. package/vendored/app/layout.tsx +2 -2
  41. package/vendored/components/HtmlLangSync.tsx +3 -2
  42. package/vendored/components/mdx/Accordion.tsx +1 -1
  43. package/vendored/components/mdx/Card.tsx +1 -1
  44. package/vendored/components/mdx/CodeGroup.tsx +18 -23
  45. package/vendored/components/mdx/Color.tsx +0 -1
  46. package/vendored/components/mdx/Icon.tsx +1 -1
  47. package/vendored/components/mdx/MDXComponents.tsx +92 -66
  48. package/vendored/components/mdx/OpenApiEndpoint.tsx +0 -1
  49. package/vendored/components/mdx/ParamField.tsx +0 -1
  50. package/vendored/components/mdx/RequestExample.tsx +0 -1
  51. package/vendored/components/mdx/ResponseExample.tsx +0 -1
  52. package/vendored/components/mdx/Steps.tsx +12 -3
  53. package/vendored/components/mdx/Table.tsx +8 -2
  54. package/vendored/components/mdx/Tabs.tsx +1 -1
  55. package/vendored/components/mdx/Tree.tsx +6 -4
  56. package/vendored/components/navigation/Header.tsx +7 -5
  57. package/vendored/components/navigation/LanguageSelector.tsx +32 -7
  58. package/vendored/components/navigation/TableOfContents.tsx +1 -1
  59. package/vendored/components/navigation/TabsNav.tsx +17 -5
  60. package/vendored/components/search/SearchModal.tsx +41 -36
  61. package/vendored/components/ui/CodePanel.tsx +2 -2
  62. package/vendored/hooks/useChat.ts +1 -1
  63. package/vendored/hooks/useShikiHighlight.ts +7 -1
  64. package/vendored/lib/build/error-parser.ts +38 -12
  65. package/vendored/lib/code-utils.ts +6 -2
  66. package/vendored/lib/health-checks.ts +2 -2
  67. package/vendored/lib/language-utils.ts +53 -2
  68. package/vendored/lib/layout-helpers.tsx +2 -1
  69. package/vendored/lib/mdx-inline-components.ts +1 -1
  70. package/vendored/lib/navigation-resolver.ts +0 -69
  71. package/vendored/lib/normalize-config.ts +1 -1
  72. package/vendored/lib/openapi/generator.ts +3 -3
  73. package/vendored/lib/openapi/parser.ts +14 -6
  74. package/vendored/lib/openapi/validator.ts +2 -2
  75. package/vendored/lib/openapi-isr.ts +4 -1
  76. package/vendored/lib/public-paths-resolver.ts +7 -6
  77. package/vendored/lib/redis.ts +2 -2
  78. package/vendored/lib/rehype-code-meta.ts +2 -2
  79. package/vendored/lib/render-doc-page.tsx +2 -2
  80. package/vendored/lib/seo.ts +21 -6
  81. package/vendored/lib/shiki-highlighter.ts +1 -1
  82. package/vendored/lib/snippet-loader-isr.ts +1 -1
  83. package/vendored/lib/validate-config.ts +136 -8
  84. package/vendored/shared/status-reporter.ts +12 -0
  85. package/vendored/workspace-package-lock.json +16 -16
@@ -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
  }
@@ -96,14 +96,16 @@ export function formatErrorLocation(source: {
96
96
 
97
97
  /**
98
98
  * Parse build error output into user-friendly error information.
99
- * @param pageToFileMap - Optional mapping of URL paths to MDX files
99
+ * @param opts.pageToFileMap - Optional mapping of URL paths to MDX files
100
+ * @param opts.discoveryHint - Optional remediation hint from docs.json discovery walk
100
101
  */
101
102
  export function parseErrorDetails(
102
103
  output: string,
103
104
  message: string,
104
105
  phase: string,
105
- pageToFileMap?: Record<string, string>
106
+ opts: { pageToFileMap?: Record<string, string>; discoveryHint?: string } = {},
106
107
  ): ErrorDetails {
108
+ const { pageToFileMap, discoveryHint } = opts;
107
109
  const lowerOutput = output.toLowerCase();
108
110
 
109
111
  // Extract error source information upfront - used by multiple error types
@@ -136,19 +138,43 @@ export function parseErrorDetails(
136
138
 
137
139
  // Configuration validation errors (from validate phase)
138
140
  if (message.includes('Missing docs.json') || message.includes('Invalid docs.json')) {
141
+ const monorepoMatch = message.match(/Missing docs.json at '([^']+)'/);
142
+ const expectedPath = monorepoMatch?.[1];
143
+
144
+ const headline = expectedPath
145
+ ? `Missing docs.json at '${expectedPath}'.`
146
+ : message.split('. ')[0] + '.';
147
+
148
+ const intro = expectedPath
149
+ ? `We could not find '${expectedPath}' in the commit that was built.`
150
+ : 'Your repository must have a valid docs.json file in the root directory.';
151
+
152
+ const requiredFields =
153
+ 'Required fields:\n' +
154
+ '• "name": Your site name\n' +
155
+ '• "theme": One of "jam", "nebula", or "pulsar"\n' +
156
+ '• "colors": { "primary": "#hexcolor" }\n' +
157
+ '• "navigation": { "anchors": [...] } or { "groups": [...] }\n\n';
158
+
159
+ let suggestion = `${intro}\n\n`;
160
+ if (discoveryHint) {
161
+ // Discovery hint is the most actionable info — put it just below intro,
162
+ // before the required-fields recap.
163
+ suggestion += `${discoveryHint}\n\n`;
164
+ } else if (expectedPath) {
165
+ // No discovery (e.g. CLI didn't pass repoRoot) but we still know docsPath.
166
+ suggestion += 'If your docs.json lives somewhere else in the repo, update the docs path setting in the dashboard.\n\n';
167
+ }
168
+ suggestion += requiredFields;
169
+ suggestion +=
170
+ 'Tip: Use JSON5 format to add comments to your docs.json.\n' +
171
+ 'See https://www.jamdesk.com/docs/configuration for examples.';
172
+
139
173
  return {
140
174
  type: 'config_error',
141
- message: message.split('. ')[0] + '.', // Get first sentence with period
175
+ message: headline,
142
176
  details: message,
143
- suggestion:
144
- 'Check your repository has a valid docs.json file in the root directory.\n\n' +
145
- 'Required fields:\n' +
146
- '• "name": Your site name\n' +
147
- '• "theme": One of "jam", "nebula", or "pulsar"\n' +
148
- '• "colors": { "primary": "#hexcolor" }\n' +
149
- '• "navigation": { "anchors": [...] } or { "groups": [...] }\n\n' +
150
- 'Tip: Use JSON5 format to add comments to your docs.json.\n' +
151
- 'See https://www.jamdesk.com/docs/configuration for examples.',
177
+ suggestion,
152
178
  };
153
179
  }
154
180
 
@@ -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
@@ -22,10 +22,11 @@ function collectPublicGroupPages(
22
22
  if (!nav || typeof nav !== 'object') return out;
23
23
 
24
24
  const node = nav as Record<string, unknown>;
25
+ const pages = node.pages;
25
26
 
26
- if ('group' in node && Array.isArray((node as any).pages)) {
27
+ if ('group' in node && Array.isArray(pages)) {
27
28
  const isPublic = inPublicGroup || node.public === true;
28
- for (const p of (node as any).pages) {
29
+ for (const p of pages) {
29
30
  if (typeof p === 'string') {
30
31
  if (isPublic) out.push(`/${p}`);
31
32
  } else {
@@ -35,14 +36,14 @@ function collectPublicGroupPages(
35
36
  return out;
36
37
  }
37
38
 
38
- if (Array.isArray((node as any).pages)) {
39
- for (const p of (node as any).pages) {
39
+ if (Array.isArray(pages)) {
40
+ for (const p of pages) {
40
41
  collectPublicGroupPages(p, inPublicGroup, out);
41
42
  }
42
43
  }
43
44
 
44
45
  for (const key of ['groups', 'tabs', 'anchors', 'versions', 'languages']) {
45
- const arr = (node as any)[key];
46
+ const arr = node[key];
46
47
  if (Array.isArray(arr)) {
47
48
  for (const item of arr) {
48
49
  collectPublicGroupPages(item, inPublicGroup, out);
@@ -72,7 +73,7 @@ export function resolvePublicPaths(input: Input): string[] {
72
73
  set.add(path);
73
74
  }
74
75
 
75
- const globs = (input.docsConfig as any).auth?.password?.public;
76
+ const globs = input.docsConfig.auth?.password?.public;
76
77
  if (Array.isArray(globs)) {
77
78
  for (const g of globs) {
78
79
  if (typeof g === 'string' && g.startsWith('/')) {
@@ -44,9 +44,9 @@ async function upstashCommand(
44
44
  headers: { Authorization: `Bearer ${kvToken}` },
45
45
  });
46
46
  const bodyText = await response.text();
47
- let data: any = null;
47
+ let data: { error?: string; result?: unknown } | null = null;
48
48
  try {
49
- data = bodyText ? JSON.parse(bodyText) : null;
49
+ data = bodyText ? (JSON.parse(bodyText) as { error?: string; result?: unknown }) : null;
50
50
  } catch {
51
51
  data = { result: bodyText };
52
52
  }
@@ -154,7 +154,7 @@ export function rehypeCodeMeta() {
154
154
 
155
155
  // Store parsed data in node.data so Shiki transformers can access it
156
156
  node.data = node.data || {};
157
- (node.data as any).parsedMeta = parsed;
157
+ (node.data as { parsedMeta?: unknown }).parsedMeta = parsed;
158
158
 
159
159
  if (parsed.icon) {
160
160
  node.properties['data-icon'] = parsed.icon;
@@ -182,7 +182,7 @@ export function rehypeCodeMeta() {
182
182
  preNode.properties['data-title'] = parsed.title;
183
183
  // Store in parent data as well
184
184
  preNode.data = preNode.data || {};
185
- (preNode.data as any).parsedTitle = parsed.title;
185
+ (preNode.data as { parsedTitle?: string }).parsedTitle = parsed.title;
186
186
  }
187
187
  }
188
188
  });
@@ -13,7 +13,7 @@
13
13
  import { notFound } from 'next/navigation';
14
14
  import { MDXRemote } from 'next-mdx-remote/rsc';
15
15
  import type { Metadata } from 'next';
16
- import type { ReactElement } from 'react';
16
+ import type { AnchorHTMLAttributes, ReactElement } from 'react';
17
17
  import { MDXComponents } from '@/components/mdx/MDXComponents';
18
18
  import { Breadcrumb } from '@/components/navigation/Breadcrumb';
19
19
  import { TableOfContents } from '@/components/navigation/TableOfContents';
@@ -360,7 +360,7 @@ export async function renderDocPage(input: RenderInput): Promise<ReactElement> {
360
360
  ...snippetAliases,
361
361
  ...inlineComponents,
362
362
  ...(hostAtDocs ? {
363
- a: ({ ariaLabel, href, ...props }: any) => {
363
+ a: ({ ariaLabel, href, ...props }: AnchorHTMLAttributes<HTMLAnchorElement> & { ariaLabel?: string }) => {
364
364
  const needsPrefix = href?.startsWith('/') && !href.startsWith('/docs/') && href !== '/docs';
365
365
  return (
366
366
  <a