jamdesk 1.1.28 → 1.1.30

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 (35) hide show
  1. package/README.md +3 -1
  2. package/dist/lib/deps.js +3 -3
  3. package/package.json +3 -3
  4. package/vendored/app/[[...slug]]/page.tsx +36 -109
  5. package/vendored/app/api/assets/[...path]/route.ts +2 -2
  6. package/vendored/app/api/chat/[project]/route.ts +206 -138
  7. package/vendored/app/api/isr-health/route.ts +2 -3
  8. package/vendored/app/layout.tsx +2 -0
  9. package/vendored/components/JdReadySentinel.tsx +25 -0
  10. package/vendored/components/chat/ChatEmptyState.tsx +13 -1
  11. package/vendored/components/chat/ChatPanel.tsx +43 -9
  12. package/vendored/components/navigation/Breadcrumb.tsx +2 -2
  13. package/vendored/components/navigation/Header.tsx +23 -17
  14. package/vendored/components/navigation/LanguageSelector.tsx +7 -4
  15. package/vendored/components/navigation/Sidebar.tsx +28 -37
  16. package/vendored/components/navigation/TabsNav.tsx +1 -1
  17. package/vendored/hooks/useChat.ts +113 -60
  18. package/vendored/hooks/useDelayedNavigationSpinner.ts +94 -0
  19. package/vendored/hooks/useTextStreamPacer.ts +152 -0
  20. package/vendored/lib/chat-prompt.ts +69 -29
  21. package/vendored/lib/chat-tools.ts +111 -0
  22. package/vendored/lib/crisp-bridge.ts +91 -0
  23. package/vendored/lib/docs-types.ts +4 -0
  24. package/vendored/lib/embedding-chunker.ts +85 -11
  25. package/vendored/lib/find-first-nav-page.ts +40 -0
  26. package/vendored/lib/hedge-strip.ts +29 -0
  27. package/vendored/lib/middleware-helpers.ts +2 -1
  28. package/vendored/lib/openapi/lang-spec-path.ts +16 -0
  29. package/vendored/lib/page-isr-helpers.ts +4 -1
  30. package/vendored/lib/public-paths-resolver.ts +3 -42
  31. package/vendored/lib/query-rewriter.ts +91 -0
  32. package/vendored/lib/ui-strings.ts +52 -0
  33. package/vendored/lib/vector-store.ts +5 -3
  34. package/vendored/schema/docs-schema.json +15 -0
  35. package/vendored/workspace-package-lock.json +88 -88
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  <p align="center">
2
2
  <a href="https://www.jamdesk.com">
3
- <img src="https://www.jamdesk.com/logo-light.png" width="280" alt="Jamdesk" />
3
+ <img src="https://www.jamdesk.com/logo-universal.png" width="280" alt="Jamdesk" />
4
4
  </a>
5
5
  </p>
6
6
 
@@ -435,6 +435,7 @@ See the [docs.json reference](https://www.jamdesk.com/docs/config/docs-json-refe
435
435
 
436
436
  - Node.js v20.0.0+
437
437
  - npm v8+ (recommended)
438
+ - macOS, Linux, or Windows (all tested in CI)
438
439
 
439
440
  ## Learn More
440
441
 
@@ -446,6 +447,7 @@ See the [docs.json reference](https://www.jamdesk.com/docs/config/docs-json-refe
446
447
  - [OpenAPI](https://www.jamdesk.com/docs/api-reference/openapi-setup)
447
448
  - [Deployment](https://www.jamdesk.com/docs/deploy)
448
449
  - [npm Package](https://www.npmjs.com/package/jamdesk)
450
+ - [Release History](https://www.npmjs.com/package/jamdesk?activeTab=versions)
449
451
  - [Homepage](https://www.jamdesk.com)
450
452
  - [Pricing](https://www.jamdesk.com/pricing)
451
453
 
package/dist/lib/deps.js CHANGED
@@ -73,8 +73,8 @@ export const REQUIRED_DEPS = {
73
73
  'unified': '^11.0.0',
74
74
  'unist-util-visit': '^5.0.0',
75
75
  // CSS
76
- 'tailwindcss': '^4.2.2',
77
- '@tailwindcss/postcss': '^4.2.2',
76
+ 'tailwindcss': '^4.2.4',
77
+ '@tailwindcss/postcss': '^4.2.4',
78
78
  '@tailwindcss/typography': '^0.5.10',
79
79
  'postcss': '^8.5.10',
80
80
  'autoprefixer': '^10.4.24',
@@ -83,7 +83,7 @@ export const REQUIRED_DEPS = {
83
83
  'json5': '^2.2.3',
84
84
  'glob': '^13.0.6',
85
85
  // TypeScript (needed for Next.js to avoid auto-install breaking symlink)
86
- 'typescript': '^6.0.2',
86
+ 'typescript': '^6.0.3',
87
87
  '@types/node': '^25.5.2',
88
88
  '@types/react': '^19.2.14',
89
89
  '@types/react-dom': '^19.0.0',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jamdesk",
3
- "version": "1.1.28",
3
+ "version": "1.1.30",
4
4
  "description": "CLI for Jamdesk — build, preview, and deploy documentation sites from MDX. Dev server with hot reload, 50+ components, OpenAPI support, AI search, and Mintlify migration",
5
5
  "keywords": [
6
6
  "jamdesk",
@@ -114,7 +114,7 @@
114
114
  "json5": "^2.2.3",
115
115
  "nspell": "^2.1.5",
116
116
  "open": "^11.0.0",
117
- "ora": "^9.3.0",
117
+ "ora": "^9.4.0",
118
118
  "tar": "^7.5.9"
119
119
  },
120
120
  "devDependencies": {
@@ -122,7 +122,7 @@
122
122
  "@types/fs-extra": "^11.0.0",
123
123
  "@types/node": "^25.5.0",
124
124
  "typescript": "^6.0.2",
125
- "vitest": "^4.1.3"
125
+ "vitest": "^4.1.5"
126
126
  },
127
127
  "engines": {
128
128
  "node": ">=20.0.0"
@@ -74,6 +74,9 @@ import {
74
74
  type CodeExample,
75
75
  type AuthMethod,
76
76
  } from '@/lib/openapi';
77
+ import { extractLanguageFromPath, isValidLanguageCode } from '@/lib/language-utils';
78
+ import { findFirstNavPage } from '@/lib/find-first-nav-page';
79
+ import { candidateSpecPaths } from '@/lib/openapi/lang-spec-path';
77
80
  import { ApiEndpoint } from '@/components/mdx/ApiEndpoint';
78
81
  import { ImagePriorityProvider } from '@/components/mdx/ImagePriorityProvider';
79
82
  import { AIActionsMenu } from '@/components/AIActionsMenu';
@@ -182,100 +185,19 @@ function getAllDocPaths(): string[] {
182
185
  return paths;
183
186
  }
184
187
 
185
- /**
186
- * Find the first page in navigation (used to resolve empty root slug in place).
187
- */
188
- function findFirstPage(config: DocsConfig): string {
189
- const navigation = config.navigation;
190
-
191
- // Helper to extract page path from a page entry
192
- const getPagePath = (page: unknown): string | null => {
193
- if (typeof page === 'string') return page;
194
- if (typeof page === 'object' && page && 'page' in page) {
195
- return (page as { page: string }).page;
196
- }
197
- return null;
198
- };
199
-
200
- // Helper to extract first page from a group
201
- const extractFirstFromGroup = (group: { pages?: unknown[] }): string | null => {
202
- const itemPages = group.pages || [];
203
- for (const page of itemPages) {
204
- if (typeof page === 'object' && page && 'group' in page) {
205
- const result = extractFirstFromGroup(page as { pages?: unknown[] });
206
- if (result) return result;
207
- continue;
208
- }
209
- const pagePath = getPagePath(page);
210
- if (pagePath) return pagePath;
211
- }
212
- return null;
213
- };
214
-
215
- // Check languages first (multi-language navigation structure)
216
- if (navigation.languages && navigation.languages.length > 0) {
217
- const firstLang = navigation.languages[0];
218
- if (firstLang.tabs && firstLang.tabs.length > 0) {
219
- for (const tab of firstLang.tabs) {
220
- if (tab.groups) {
221
- for (const group of tab.groups) {
222
- const result = extractFirstFromGroup(group);
223
- if (result) return result;
224
- }
225
- }
226
- }
227
- }
228
- }
229
-
230
- // Check tabs first
231
- if (navigation.tabs && navigation.tabs.length > 0) {
232
- for (const tab of navigation.tabs) {
233
- if (tab.groups) {
234
- for (const group of tab.groups) {
235
- const result = extractFirstFromGroup(group);
236
- if (result) return result;
237
- }
238
- }
239
- }
240
- }
241
-
242
- // Check top-level groups
243
- if (navigation.groups && navigation.groups.length > 0) {
244
- for (const group of navigation.groups) {
245
- const result = extractFirstFromGroup(group);
246
- if (result) return result;
247
- }
248
- }
249
-
250
- // Check anchors
251
- if (navigation.anchors && navigation.anchors.length > 0) {
252
- for (const anchor of navigation.anchors) {
253
- if (anchor.groups) {
254
- for (const group of anchor.groups) {
255
- const result = extractFirstFromGroup(group);
256
- if (result) return result;
257
- }
258
- }
259
- }
260
- }
188
+ function findFirstPage(config: DocsConfig, lang?: string): string {
189
+ const nav = config.navigation;
190
+ const langBlock = lang ? nav.languages?.find((l) => l.language === lang) : undefined;
191
+ const result = (langBlock && findFirstNavPage(langBlock)) || findFirstNavPage(nav);
192
+ return result ? result.replace(/^\//, '') : 'introduction';
193
+ }
261
194
 
262
- // Check pages array (flat format with groups inside pages)
263
- if (navigation.pages && Array.isArray(navigation.pages)) {
264
- for (const item of navigation.pages) {
265
- // Could be a string page
266
- const directPath = getPagePath(item);
267
- if (directPath) return directPath;
268
-
269
- // Could be a group object { group: "...", pages: [...] }
270
- if (item && typeof item === 'object' && 'group' in item) {
271
- const result = extractFirstFromGroup(item as { pages?: unknown[] });
272
- if (result) return result;
273
- }
274
- }
195
+ function resolveSlug(normalizedSlug: string[], config: DocsConfig): string[] {
196
+ if (normalizedSlug.length === 0) return pathToSlug(findFirstPage(config));
197
+ if (normalizedSlug.length === 1 && isValidLanguageCode(normalizedSlug[0])) {
198
+ return pathToSlug(findFirstPage(config, normalizedSlug[0]));
275
199
  }
276
-
277
- // Fallback
278
- return 'introduction';
200
+ return normalizedSlug;
279
201
  }
280
202
 
281
203
  export async function generateStaticParams() {
@@ -342,17 +264,12 @@ export async function generateMetadata({ params }: PageProps) {
342
264
  // Normalize slug: strip /docs prefix when hostAtDocs=true.
343
265
  // Empty root → resolve to first page (see DocPage for the full rationale).
344
266
  const normalizedSlug = normalizeSlugForContent(resolvedParams.slug || [], hostAtDocs);
267
+ const config = await loader.getConfig();
268
+ const slug = resolveSlug(normalizedSlug, config);
345
269
  const isRoot = normalizedSlug.length === 0;
346
- const slug = isRoot
347
- ? pathToSlug(findFirstPage(await loader.getConfig()))
348
- : normalizedSlug;
349
270
  const pagePath = slug.join('/');
350
271
 
351
- // Fetch content and config in parallel
352
- const [fileContents, config] = await Promise.all([
353
- loader.getContent(pagePath).catch(() => null),
354
- loader.getConfig(),
355
- ]);
272
+ const fileContents = await loader.getContent(pagePath).catch(() => null);
356
273
 
357
274
  if (!fileContents) {
358
275
  return {
@@ -428,14 +345,11 @@ export default async function DocPage({ params }: PageProps) {
428
345
  // redirect() emits cache-control: private, blocking CDN caching. Canonical
429
346
  // + noindex in generateMetadata prevent duplicate indexing.
430
347
  const normalizedSlug = normalizeSlugForContent(resolvedParams.slug || [], hostAtDocs);
431
- const slug = normalizedSlug.length === 0
432
- ? pathToSlug(findFirstPage(await loader.getConfig()))
433
- : normalizedSlug;
348
+ const config = await loader.getConfig();
349
+ const slug = resolveSlug(normalizedSlug, config);
434
350
  const pagePath = slug.join('/');
435
- const [fileContents, config] = await Promise.all([
436
- loader.getContent(pagePath).catch(() => null),
437
- loader.getConfig(),
438
- ]);
351
+ const currentLang = extractLanguageFromPath(`/${pagePath}`);
352
+ const fileContents = await loader.getContent(pagePath).catch(() => null);
439
353
 
440
354
  // Check if content exists (getContent returns null via catch if not found)
441
355
  if (!fileContents) {
@@ -567,9 +481,13 @@ export default async function DocPage({ params }: PageProps) {
567
481
  allSpecPaths.length > 0 ? allSpecPaths : undefined
568
482
  );
569
483
 
570
- const specsToTry = parsed.isShortFormat && allSpecPaths.length > 1
484
+ const baseSpecs = parsed.isShortFormat && allSpecPaths.length > 1
571
485
  ? allSpecPaths
572
486
  : [parsed.specPath];
487
+ // For each base spec, expand into [<base>.<lang>.ext, <base>.ext] when
488
+ // the page is under a localized URL. Resolver tries each in order; the
489
+ // first that loads wins. Missing lang variant falls back to English.
490
+ const specsToTry = baseSpecs.flatMap(p => candidateSpecPaths(p, currentLang));
573
491
 
574
492
  // Hoist mode-dependent values before the loop
575
493
  const useIsr = isIsrMode() && !!projectSlug;
@@ -580,7 +498,8 @@ export default async function DocPage({ params }: PageProps) {
580
498
 
581
499
  let lastError: unknown = null;
582
500
 
583
- for (const specPath of specsToTry) {
501
+ for (let i = 0; i < specsToTry.length; i++) {
502
+ const specPath = specsToTry[i];
584
503
  try {
585
504
  if (resolveSpec && projectSlug) {
586
505
  const spec = await resolveSpec(projectSlug, specPath);
@@ -598,6 +517,14 @@ export default async function DocPage({ params }: PageProps) {
598
517
  break;
599
518
  } catch (err) {
600
519
  lastError = err;
520
+ const isLast = i === specsToTry.length - 1;
521
+ if (!isLast) {
522
+ // Lang variant (or intermediate candidate) failed — log so we
523
+ // notice transient R2 errors that silently fall through to English.
524
+ console.warn(
525
+ `[openapi] spec candidate "${specPath}" failed; trying next: ${(err as Error).message}`
526
+ );
527
+ }
601
528
  }
602
529
  }
603
530
 
@@ -8,6 +8,7 @@
8
8
  import { NextRequest, NextResponse } from 'next/server';
9
9
  import { fetchAsset } from '@/lib/r2-content';
10
10
  import { log } from '@/lib/logger';
11
+ import { isIsrMode } from '@/lib/page-isr-helpers';
11
12
 
12
13
  // Cache durations by content type (in seconds)
13
14
  const CACHE_DURATIONS: Record<string, number> = {
@@ -27,8 +28,7 @@ export async function GET(
27
28
  request: NextRequest,
28
29
  { params }: { params: Promise<{ path: string[] }> }
29
30
  ) {
30
- // Check if ISR mode is enabled
31
- if (process.env.ISR_MODE !== 'true') {
31
+ if (!isIsrMode()) {
32
32
  return new NextResponse('Asset serving only available in ISR mode', {
33
33
  status: 404,
34
34
  });