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.
- package/dist/__tests__/integration/validate.integration.test.js +2 -1
- package/dist/__tests__/integration/validate.integration.test.js.map +1 -1
- package/dist/__tests__/unit/dev-workspace-symlinks.test.d.ts +2 -0
- package/dist/__tests__/unit/dev-workspace-symlinks.test.d.ts.map +1 -0
- package/dist/__tests__/unit/dev-workspace-symlinks.test.js +112 -0
- package/dist/__tests__/unit/dev-workspace-symlinks.test.js.map +1 -0
- package/dist/__tests__/unit/docs-config-discovery.test.d.ts +2 -0
- package/dist/__tests__/unit/docs-config-discovery.test.d.ts.map +1 -0
- package/dist/__tests__/unit/docs-config-discovery.test.js +190 -0
- package/dist/__tests__/unit/docs-config-discovery.test.js.map +1 -0
- package/dist/__tests__/unit/docs-config.test.js +2 -1
- package/dist/__tests__/unit/docs-config.test.js.map +1 -1
- package/dist/__tests__/unit/language-filter.test.d.ts +2 -0
- package/dist/__tests__/unit/language-filter.test.d.ts.map +1 -0
- package/dist/__tests__/unit/language-filter.test.js +166 -0
- package/dist/__tests__/unit/language-filter.test.js.map +1 -0
- package/dist/__tests__/unit/output.test.d.ts +2 -0
- package/dist/__tests__/unit/output.test.d.ts.map +1 -0
- package/dist/__tests__/unit/output.test.js +61 -0
- package/dist/__tests__/unit/output.test.js.map +1 -0
- package/dist/commands/dev.d.ts.map +1 -1
- package/dist/commands/dev.js +4 -1
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +14 -12
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/validate.d.ts.map +1 -1
- package/dist/commands/validate.js +14 -2
- package/dist/commands/validate.js.map +1 -1
- package/dist/lib/docs-config.d.ts +54 -3
- package/dist/lib/docs-config.d.ts.map +1 -1
- package/dist/lib/docs-config.js +126 -8
- package/dist/lib/docs-config.js.map +1 -1
- package/dist/lib/language-filter.d.ts +31 -0
- package/dist/lib/language-filter.d.ts.map +1 -0
- package/dist/lib/language-filter.js +14 -0
- package/dist/lib/language-filter.js.map +1 -0
- package/package.json +1 -1
- package/vendored/app/api/r2/[project]/[...path]/route.ts +14 -9
- package/vendored/app/layout.tsx +2 -2
- package/vendored/components/HtmlLangSync.tsx +3 -2
- package/vendored/components/mdx/Accordion.tsx +1 -1
- package/vendored/components/mdx/Card.tsx +1 -1
- package/vendored/components/mdx/CodeGroup.tsx +18 -23
- package/vendored/components/mdx/Color.tsx +0 -1
- package/vendored/components/mdx/Icon.tsx +1 -1
- package/vendored/components/mdx/MDXComponents.tsx +92 -66
- package/vendored/components/mdx/OpenApiEndpoint.tsx +0 -1
- package/vendored/components/mdx/ParamField.tsx +0 -1
- package/vendored/components/mdx/RequestExample.tsx +0 -1
- package/vendored/components/mdx/ResponseExample.tsx +0 -1
- package/vendored/components/mdx/Steps.tsx +12 -3
- package/vendored/components/mdx/Table.tsx +8 -2
- package/vendored/components/mdx/Tabs.tsx +1 -1
- package/vendored/components/mdx/Tree.tsx +6 -4
- package/vendored/components/navigation/Header.tsx +7 -5
- package/vendored/components/navigation/LanguageSelector.tsx +32 -7
- package/vendored/components/navigation/TableOfContents.tsx +1 -1
- package/vendored/components/navigation/TabsNav.tsx +17 -5
- package/vendored/components/search/SearchModal.tsx +41 -36
- package/vendored/components/ui/CodePanel.tsx +2 -2
- package/vendored/hooks/useChat.ts +1 -1
- package/vendored/hooks/useShikiHighlight.ts +7 -1
- package/vendored/lib/build/error-parser.ts +38 -12
- package/vendored/lib/code-utils.ts +6 -2
- package/vendored/lib/health-checks.ts +2 -2
- package/vendored/lib/language-utils.ts +53 -2
- package/vendored/lib/layout-helpers.tsx +2 -1
- package/vendored/lib/mdx-inline-components.ts +1 -1
- package/vendored/lib/navigation-resolver.ts +0 -69
- package/vendored/lib/normalize-config.ts +1 -1
- package/vendored/lib/openapi/generator.ts +3 -3
- package/vendored/lib/openapi/parser.ts +14 -6
- package/vendored/lib/openapi/validator.ts +2 -2
- package/vendored/lib/openapi-isr.ts +4 -1
- package/vendored/lib/public-paths-resolver.ts +7 -6
- package/vendored/lib/redis.ts +2 -2
- package/vendored/lib/rehype-code-meta.ts +2 -2
- package/vendored/lib/render-doc-page.tsx +2 -2
- package/vendored/lib/seo.ts +21 -6
- package/vendored/lib/shiki-highlighter.ts +1 -1
- package/vendored/lib/snippet-loader-isr.ts +1 -1
- package/vendored/lib/validate-config.ts +136 -8
- package/vendored/shared/status-reporter.ts +12 -0
- 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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
*
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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 {
|
|
9
|
-
import type { GeneratedPage,
|
|
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
|
|
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
|
|
197
|
-
const
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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(
|
|
27
|
+
if ('group' in node && Array.isArray(pages)) {
|
|
27
28
|
const isPublic = inPublicGroup || node.public === true;
|
|
28
|
-
for (const p of
|
|
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(
|
|
39
|
-
for (const p of
|
|
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 =
|
|
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 =
|
|
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('/')) {
|
package/vendored/lib/redis.ts
CHANGED
|
@@ -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:
|
|
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
|
|
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
|
|
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 }:
|
|
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
|