jamdesk 1.1.38 → 1.1.40

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 (30) hide show
  1. package/dist/__tests__/integration/init.integration.test.js +14 -11
  2. package/dist/__tests__/integration/init.integration.test.js.map +1 -1
  3. package/dist/__tests__/unit/dev-cache-cleanup.test.d.ts +2 -0
  4. package/dist/__tests__/unit/dev-cache-cleanup.test.d.ts.map +1 -0
  5. package/dist/__tests__/unit/dev-cache-cleanup.test.js +74 -0
  6. package/dist/__tests__/unit/dev-cache-cleanup.test.js.map +1 -0
  7. package/dist/commands/dev.d.ts +8 -0
  8. package/dist/commands/dev.d.ts.map +1 -1
  9. package/dist/commands/dev.js +56 -5
  10. package/dist/commands/dev.js.map +1 -1
  11. package/package.json +1 -1
  12. package/templates/components/callouts.mdx +56 -0
  13. package/templates/components/cards.mdx +80 -0
  14. package/templates/components/steps.mdx +39 -0
  15. package/templates/components/tabs-and-accordions.mdx +65 -0
  16. package/templates/docs.json +21 -0
  17. package/templates/introduction.mdx +40 -10
  18. package/templates/quickstart.mdx +98 -9
  19. package/templates/writing/code-blocks.mdx +80 -0
  20. package/templates/writing/components.mdx +78 -0
  21. package/templates/writing/pages.mdx +59 -0
  22. package/vendored/app/api/chat/[project]/route.ts +2 -2
  23. package/vendored/app/api/docs-search/[project]/search/route.ts +15 -50
  24. package/vendored/app/layout.tsx +4 -4
  25. package/vendored/components/navigation/Sidebar.tsx +9 -4
  26. package/vendored/components/search/SearchModal.tsx +6 -6
  27. package/vendored/lib/language-utils.ts +6 -0
  28. package/vendored/lib/search-client.ts +72 -24
  29. package/vendored/lib/static-file-route.ts +13 -0
  30. package/vendored/scripts/build-search-index.cjs +34 -26
@@ -1,6 +1,7 @@
1
1
  import type { Metadata } from 'next';
2
2
  import { Inter, JetBrains_Mono } from 'next/font/google';
3
3
  import { headers } from 'next/headers';
4
+ import Script from 'next/script';
4
5
  import './globals.css';
5
6
  import { ThemeProvider } from '@/components/theme/ThemeProvider';
6
7
  import { LayoutWrapper } from '@/components/layout/LayoutWrapper';
@@ -411,10 +412,9 @@ export default async function RootLayout({
411
412
  {/* Preload FA font files to avoid waterfall: CSS → font discovery → download */}
412
413
  <link rel="preload" href="/_jd/fonts/fontawesome/webfonts/fa-light-300.woff2" as="font" type="font/woff2" crossOrigin="anonymous" />
413
414
  <link rel="preload" href="/_jd/fonts/fontawesome/webfonts/fa-brands-400.woff2" as="font" type="font/woff2" crossOrigin="anonymous" />
414
- <script
415
- // eslint-disable-next-line react/no-danger -- static trusted content, no user input
416
- dangerouslySetInnerHTML={{ __html: `var l=document.createElement('link');l.rel='stylesheet';l.href='${FA_CSS_HREF}';document.head.appendChild(l);window.__FA_CSS_LOADED__=true;` }}
417
- />
415
+ <Script id="fa-css-async-loader" strategy="beforeInteractive">
416
+ {`var l=document.createElement('link');l.rel='stylesheet';l.href='${FA_CSS_HREF}';document.head.appendChild(l);window.__FA_CSS_LOADED__=true;`}
417
+ </Script>
418
418
  <noscript>
419
419
  <link rel="stylesheet" href={FA_CSS_HREF} />
420
420
  </noscript>
@@ -278,20 +278,25 @@ export function Sidebar({ config, layout = 'header-logo', tabsPosition: tabsPosi
278
278
  // Prevent body scroll when sidebar is open on mobile
279
279
  useBodyScrollLock(isOpen);
280
280
 
281
- // Toggle group expansion; if expanding, navigate to first page
281
+ // Toggle group expansion; if expanding, navigate to first page.
282
+ // The router.push must run AFTER the setState commits — calling it inside the
283
+ // updater triggers "Cannot update a component (Router) while rendering Sidebar".
282
284
  const handleGroupClick = useCallback((group: ResolvedGroup) => {
285
+ const willExpand = !expandedGroups.has(group.name);
283
286
  setExpandedGroups(prev => {
284
287
  const newSet = new Set(prev);
285
288
  if (prev.has(group.name)) {
286
289
  newSet.delete(group.name);
287
290
  } else {
288
- const firstPagePath = findFirstPageInGroups([group]);
289
- if (firstPagePath) router.push(`${linkPrefix}/${firstPagePath}`);
290
291
  newSet.add(group.name);
291
292
  }
292
293
  return newSet;
293
294
  });
294
- }, [router, linkPrefix]);
295
+ if (willExpand) {
296
+ const firstPagePath = findFirstPageInGroups([group]);
297
+ if (firstPagePath) router.push(`${linkPrefix}/${firstPagePath}`);
298
+ }
299
+ }, [router, linkPrefix, expandedGroups]);
295
300
 
296
301
  // Render a navigation group (supports nesting)
297
302
  function renderGroup(group: ResolvedGroup, level: number = 0) {
@@ -74,7 +74,7 @@ function NoResultsState({ query, onSuggestionClick }: { query: string; onSuggest
74
74
  <button
75
75
  key={suggestion}
76
76
  onClick={() => onSuggestionClick(suggestion)}
77
- className="px-2.5 py-1 text-xs bg-[var(--color-bg-secondary)] hover:bg-[var(--color-bg-tertiary)] text-[var(--color-text-primary)] rounded-full transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--color-accent)]"
77
+ className="px-2.5 py-1 text-xs cursor-pointer bg-[var(--color-bg-secondary)] hover:bg-[var(--color-bg-tertiary)] text-[var(--color-text-primary)] rounded-full transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--color-accent)]"
78
78
  >
79
79
  {suggestion}
80
80
  </button>
@@ -410,7 +410,7 @@ export function SearchModal({ isOpen, onClose, popularPages, onNavigate }: Searc
410
410
  />
411
411
  <button
412
412
  onClick={onClose}
413
- className="p-1.5 hover:bg-[var(--color-bg-tertiary)] rounded-lg transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--color-accent)]"
413
+ className="p-1.5 cursor-pointer hover:bg-[var(--color-bg-tertiary)] rounded-lg transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--color-accent)]"
414
414
  aria-label="Close search"
415
415
  >
416
416
  <i className="fa-solid fa-xmark h-4 w-4 text-[var(--color-text-muted)]" aria-hidden="true" />
@@ -448,7 +448,7 @@ export function SearchModal({ isOpen, onClose, popularPages, onNavigate }: Searc
448
448
  onClick={() => handleResultClick(result, index)}
449
449
  role="option"
450
450
  aria-selected={index === selectedIndex}
451
- className={`w-full px-4 py-2.5 flex items-start gap-3 transition-colors outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-accent)] focus-visible:ring-inset ${
451
+ className={`w-full px-4 py-2.5 flex items-start gap-3 cursor-pointer transition-colors outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-accent)] focus-visible:ring-inset ${
452
452
  index === selectedIndex
453
453
  ? 'bg-[var(--color-bg-secondary)]'
454
454
  : 'hover:bg-[var(--color-bg-secondary)]'
@@ -490,7 +490,7 @@ export function SearchModal({ isOpen, onClose, popularPages, onNavigate }: Searc
490
490
  </span>
491
491
  <button
492
492
  onClick={handleClearRecentSearches}
493
- className="text-xs text-[var(--color-text-muted)] hover:text-[var(--color-text-primary)] transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--color-accent)]"
493
+ className="text-xs cursor-pointer text-[var(--color-text-muted)] hover:text-[var(--color-text-primary)] transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--color-accent)]"
494
494
  >
495
495
  Clear
496
496
  </button>
@@ -499,7 +499,7 @@ export function SearchModal({ isOpen, onClose, popularPages, onNavigate }: Searc
499
499
  <button
500
500
  key={term}
501
501
  onClick={() => handleRecentSearchClick(term)}
502
- className="w-full px-4 py-2 flex items-center gap-3 hover:bg-[var(--color-bg-secondary)] transition-colors group focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--color-accent)]"
502
+ className="w-full px-4 py-2 flex items-center gap-3 cursor-pointer hover:bg-[var(--color-bg-secondary)] transition-colors group focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--color-accent)]"
503
503
  >
504
504
  {/* Keyboard shortcut badge */}
505
505
  <kbd className="w-4 h-4 flex items-center justify-center bg-[var(--color-bg-tertiary)] border border-[var(--color-border)] rounded text-[9px] font-medium text-[var(--color-text-muted)] group-hover:border-[var(--color-accent)] transition-colors">
@@ -532,7 +532,7 @@ export function SearchModal({ isOpen, onClose, popularPages, onNavigate }: Searc
532
532
  router.push(url);
533
533
  onClose();
534
534
  }}
535
- className={`w-full px-4 py-2.5 flex items-center gap-3 transition-colors outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-accent)] focus-visible:ring-inset ${
535
+ className={`w-full px-4 py-2.5 flex items-center gap-3 cursor-pointer transition-colors outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-accent)] focus-visible:ring-inset ${
536
536
  index === popularIndex
537
537
  ? 'bg-[var(--color-bg-secondary)]'
538
538
  : 'hover:bg-[var(--color-bg-secondary)]'
@@ -15,6 +15,12 @@ import LANGUAGE_CODES_JSON from './language-codes.json';
15
15
  */
16
16
  export const LANGUAGE_CODES = LANGUAGE_CODES_JSON as readonly LanguageCode[];
17
17
 
18
+ /** BCP-47 syntax check (2-3 letter primary tag + optional 2-4 letter
19
+ * region/script). Shared by the chat and docs-search REST endpoints to
20
+ * keep their request-validation contracts identical. Limitation:
21
+ * 3-segment tags like `zh-Hant-HK` are rejected. */
22
+ export const BCP47_LANGUAGE_RE = /^[a-zA-Z]{2,3}(?:[-_][a-zA-Z]{2,4})?$/;
23
+
18
24
  /**
19
25
  * Display names for each supported language code
20
26
  * Uses native language names where appropriate
@@ -1,6 +1,12 @@
1
1
  // Client-side search with Orama (BM25 ranking)
2
2
  import { create, insertMultiple, search as oramaSearch, type Orama } from '@orama/orama';
3
- import { resolveLocaleWithLoweredSet } from './language-utils';
3
+ import { LANGUAGE_CODES, resolveLocaleWithLoweredSet } from './language-utils';
4
+
5
+ // Lowercased canonical language codes — used to detect slug-prefix locales when
6
+ // the search-data.json predates the locale field (legacy fallback).
7
+ const KNOWN_LANGUAGE_CODES_LOWERED: ReadonlySet<string> = new Set(
8
+ LANGUAGE_CODES.map((c) => c.toLowerCase()),
9
+ );
4
10
 
5
11
  export interface SearchResult {
6
12
  id: string;
@@ -38,16 +44,17 @@ let lastEtag = '';
38
44
  let lastParsedData: SearchResult[] | null = null;
39
45
 
40
46
  /**
41
- * Non-empty locale values present in the currently-committed index.
42
- * Empty Set means either (a) the index has zero translated docs (single-
43
- * language project) or (b) the index is a legacy build that predates the
44
- * locale field. In either case the language filter is a no-op.
47
+ * Locales the URL-resolver treats as real translations for the current
48
+ * project. Populated from the index's `locale` field when present, falling
49
+ * back to slug-prefix derivation for legacy (pre-feature) search-data.json
50
+ * so French pages don't leak English results while customers rebuild.
45
51
  *
46
- * `projectLocalesLowered` mirrors the same set lowercased kept around so
47
- * `resolveActiveLocale` doesn't re-lowercase per keystroke.
52
+ * `indexHasLocaleField` is the separate gate that controls whether the Orama
53
+ * `where` clause is used — set only when the new index format is detected.
54
+ * Slug-prefix fallback runs in `search()` whenever the where clause is off.
48
55
  */
49
- let projectLocales: Set<string> = new Set();
50
56
  let projectLocalesLowered: ReadonlySet<string> = new Set();
57
+ let indexHasLocaleField = false;
51
58
 
52
59
  /**
53
60
  * Cheap fingerprint: count + first/last IDs + a sample of content lengths.
@@ -80,13 +87,26 @@ async function buildIndex(data: SearchResult[], etag: string): Promise<void> {
80
87
  },
81
88
  });
82
89
 
83
- // Detect a legacy (pre-feature) index by inspecting the RAW input — once the
84
- // ?? '' fallback runs in the map below, we can't tell "doc had locale=''" from
85
- // "doc was missing the locale field entirely".
90
+ // Distinguish new-format docs (have a `locale` field, possibly '') from
91
+ // legacy ones (field missing entirely). Once the `?? ''` fallback runs in
92
+ // the map below the two are indistinguishable, so check the raw input first.
93
+ // A single doc with the field is enough — the build script always emits the
94
+ // field for every doc when it emits any.
95
+ indexHasLocaleField = data.some(
96
+ (d) => typeof (d as { locale?: unknown }).locale === 'string',
97
+ );
98
+
86
99
  const seenLocales = new Set<string>();
100
+ const slugPrefixLocales = new Set<string>();
87
101
  const normalizedData = data.map(item => {
88
102
  const raw = item.locale;
89
103
  if (typeof raw === 'string' && raw.length > 0) seenLocales.add(raw);
104
+ // Track first-segment slug prefixes that match a known language code,
105
+ // used as the URL-to-locale whitelist when the index lacks the field.
106
+ const firstSeg = item.slug.split('/', 1)[0]?.toLowerCase();
107
+ if (firstSeg && KNOWN_LANGUAGE_CODES_LOWERED.has(firstSeg)) {
108
+ slugPrefixLocales.add(firstSeg);
109
+ }
90
110
  return {
91
111
  id: item.id,
92
112
  title: item.title,
@@ -101,8 +121,13 @@ async function buildIndex(data: SearchResult[], etag: string): Promise<void> {
101
121
 
102
122
  await insertMultiple(db, normalizedData);
103
123
 
104
- projectLocales = seenLocales;
105
- projectLocalesLowered = new Set(Array.from(seenLocales, (l) => l.toLowerCase()));
124
+ // Trust the index's own locale set when present (handles the dodo case
125
+ // where `de/` is a directory, not a translation — `seenLocales` won't
126
+ // include `de`). Fall back to slug-prefix derivation for legacy indexes so
127
+ // the URL still maps to a sensible locale during the rebuild window.
128
+ projectLocalesLowered = indexHasLocaleField
129
+ ? new Set(Array.from(seenLocales, (l) => l.toLowerCase()))
130
+ : slugPrefixLocales;
106
131
 
107
132
  committedFingerprint = buildingFingerprint;
108
133
  lastParsedData = data;
@@ -148,15 +173,15 @@ export async function search(
148
173
  return [];
149
174
  }
150
175
 
151
- // Legacy-index safety: if the committed index has zero translated docs
152
- // (single-language project OR pre-feature search-data.json), skip the where
153
- // clause so un-rebuilt R2 files behave identically to before this feature.
154
- const useFilter = projectLocales.size > 0;
155
176
  const targetLocale = language ?? '';
156
177
 
178
+ // Fetch extra when post-filtering by slug prefix so we can still reach
179
+ // `limit` after dropping cross-locale hits.
180
+ const fetchLimit = indexHasLocaleField ? limit : Math.min(limit * 5, 50);
181
+
157
182
  const results = await oramaSearch(db, {
158
183
  term: query,
159
- limit,
184
+ limit: fetchLimit,
160
185
  tolerance: 1, // Allow 1 typo for fuzzy matching
161
186
  boost: {
162
187
  title: 2,
@@ -164,10 +189,32 @@ export async function search(
164
189
  description: 1,
165
190
  content: 0.5,
166
191
  },
167
- ...(useFilter ? { where: { locale: { eq: targetLocale } } } : {}),
192
+ ...(indexHasLocaleField ? { where: { locale: { eq: targetLocale } } } : {}),
168
193
  });
169
194
 
170
- return results.hits.map(hit => hit.document as unknown as SearchResult);
195
+ const docs = results.hits.map(hit => hit.document as unknown as SearchResult);
196
+
197
+ // Slug-prefix fallback for un-rebuilt indexes: the index has no locale
198
+ // field, so the where clause was skipped above. Filter by slug prefix
199
+ // instead so French pages don't see English results during the rebuild
200
+ // window. Inverted on default-language pages: drop slugs whose prefix
201
+ // matches any known project locale.
202
+ if (!indexHasLocaleField) {
203
+ if (targetLocale) {
204
+ const prefix = `${targetLocale}/`;
205
+ return docs.filter(d => d.slug.startsWith(prefix)).slice(0, limit);
206
+ }
207
+ if (projectLocalesLowered.size > 0) {
208
+ return docs
209
+ .filter(d => {
210
+ const firstSeg = d.slug.split('/', 1)[0]?.toLowerCase();
211
+ return !firstSeg || !projectLocalesLowered.has(firstSeg);
212
+ })
213
+ .slice(0, limit);
214
+ }
215
+ }
216
+
217
+ return docs.slice(0, limit);
171
218
  }
172
219
 
173
220
  /** @internal Used by tests only */
@@ -177,12 +224,13 @@ export function isInitialized(): boolean {
177
224
 
178
225
  /**
179
226
  * Resolve the locale that the search filter should target for a given pathname,
180
- * gated by the locales actually present in the currently-committed index.
227
+ * gated by the locales actually present in the currently-committed index
228
+ * (or, in legacy mode, by slug-prefix derivation).
181
229
  *
182
230
  * Returns:
183
- * - `''` when there is no committed index yet, OR no translations exist, OR
184
- * the pathname's prefix is not a translated locale.
185
- * - The canonical language code when the prefix matches an indexed locale.
231
+ * - `''` when there is no committed index yet, OR the pathname has no
232
+ * language prefix, OR the prefix is not a known project locale.
233
+ * - The canonical language code when the prefix matches.
186
234
  *
187
235
  * Caller (SearchModal) should pass the result to `search()`. Empty string and
188
236
  * undefined are equivalent — both target the default-language doc set.
@@ -6,6 +6,9 @@
6
6
  * duplication across 12 route files (6 at root + 6 at /docs).
7
7
  */
8
8
 
9
+ import fs from 'fs';
10
+ import path from 'path';
11
+
9
12
  import { NextRequest, NextResponse } from 'next/server';
10
13
 
11
14
  import { log } from '@/lib/logger';
@@ -55,6 +58,16 @@ export function createStaticFileHandler(
55
58
 
56
59
  return async function GET(request: NextRequest): Promise<NextResponse> {
57
60
  if (!isIsrMode()) {
61
+ // Dev fallback: `dev-project.cjs` writes static artifacts (search-data.json,
62
+ // sitemap.xml, ...) into `public/`. Next.js prefers route handlers over
63
+ // public/ files when both exist, so without this branch the dev server
64
+ // 404s on these paths and breaks the SearchModal init.
65
+ const localPath = path.join(process.cwd(), 'public', filename);
66
+ if (fs.existsSync(localPath)) {
67
+ return new NextResponse(fs.readFileSync(localPath), {
68
+ headers: { 'Content-Type': contentType, 'Cache-Control': 'no-cache' },
69
+ });
70
+ }
58
71
  return new NextResponse('Not found', { status: 404 });
59
72
  }
60
73
 
@@ -310,32 +310,40 @@ async function buildSearchIndex() {
310
310
  async function processFile({ filePath, slug }) {
311
311
  await semaphore.acquire();
312
312
  try {
313
- const fileContents = await fsPromises.readFile(filePath, 'utf8');
314
- const { data, content } = parseFrontmatterLenient(fileContents);
315
- // Filter for="agents" content out of the search index.
316
- const visibleContent = filterVisibility(content, 'humans');
317
- const sections = extractSections(visibleContent);
318
-
319
- const docs = [];
320
- const normalizedSlug = slug.replace(/\\/g, '/');
321
- const pageType = inferPageType(normalizedSlug);
322
- const locale = resolveLocaleFromPath(normalizedSlug, projectLanguages);
323
- sections.forEach((section, idx) => {
324
- const cleanContent = stripMarkdown(section.content);
325
- if (cleanContent.trim()) {
326
- docs.push({
327
- id: `${slug}-${idx}`,
328
- title: data.title || slug.split('/').pop() || '',
329
- description: data.description,
330
- content: cleanContent.substring(0, 300),
331
- slug: normalizedSlug,
332
- section: section.heading || undefined,
333
- type: pageType,
334
- locale,
335
- });
336
- }
337
- });
338
- return docs;
313
+ try {
314
+ const fileContents = await fsPromises.readFile(filePath, 'utf8');
315
+ const { data, content } = parseFrontmatterLenient(fileContents);
316
+ // Filter for="agents" content out of the search index.
317
+ const visibleContent = filterVisibility(content, 'humans');
318
+ const sections = extractSections(visibleContent);
319
+
320
+ const docs = [];
321
+ const normalizedSlug = slug.replace(/\\/g, '/');
322
+ const pageType = inferPageType(normalizedSlug);
323
+ const locale = resolveLocaleFromPath(normalizedSlug, projectLanguages);
324
+ sections.forEach((section, idx) => {
325
+ const cleanContent = stripMarkdown(section.content);
326
+ if (cleanContent.trim()) {
327
+ docs.push({
328
+ id: `${slug}-${idx}`,
329
+ title: data.title || slug.split('/').pop() || '',
330
+ description: data.description,
331
+ content: cleanContent.substring(0, 300),
332
+ slug: normalizedSlug,
333
+ section: section.heading || undefined,
334
+ type: pageType,
335
+ locale,
336
+ });
337
+ }
338
+ });
339
+ return docs;
340
+ } catch (err) {
341
+ // One malformed MDX file (bad YAML, etc.) used to abort the entire index
342
+ // build — leaving the project searchless. Skip the file with a warning
343
+ // so the rest of the docs remain searchable.
344
+ console.warn(`⚠ Skipping ${filePath} in search index: ${err.message}`);
345
+ return [];
346
+ }
339
347
  } finally {
340
348
  semaphore.release();
341
349
  }