jamdesk 1.1.75 → 1.1.77

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 (61) hide show
  1. package/dist/__tests__/unit/deps.test.js +184 -0
  2. package/dist/__tests__/unit/deps.test.js.map +1 -1
  3. package/dist/__tests__/unit/dev-spinner-ownership.test.d.ts +2 -0
  4. package/dist/__tests__/unit/dev-spinner-ownership.test.d.ts.map +1 -0
  5. package/dist/__tests__/unit/dev-spinner-ownership.test.js +37 -0
  6. package/dist/__tests__/unit/dev-spinner-ownership.test.js.map +1 -0
  7. package/dist/__tests__/unit/dev-workspace-symlinks.test.d.ts +2 -0
  8. package/dist/__tests__/unit/dev-workspace-symlinks.test.d.ts.map +1 -0
  9. package/dist/__tests__/unit/dev-workspace-symlinks.test.js +112 -0
  10. package/dist/__tests__/unit/dev-workspace-symlinks.test.js.map +1 -0
  11. package/dist/__tests__/unit/language-filter.test.d.ts +2 -0
  12. package/dist/__tests__/unit/language-filter.test.d.ts.map +1 -0
  13. package/dist/__tests__/unit/language-filter.test.js +166 -0
  14. package/dist/__tests__/unit/language-filter.test.js.map +1 -0
  15. package/dist/__tests__/unit/output.test.d.ts +2 -0
  16. package/dist/__tests__/unit/output.test.d.ts.map +1 -0
  17. package/dist/__tests__/unit/output.test.js +61 -0
  18. package/dist/__tests__/unit/output.test.js.map +1 -0
  19. package/dist/__tests__/unit/spinner.test.d.ts +2 -0
  20. package/dist/__tests__/unit/spinner.test.d.ts.map +1 -0
  21. package/dist/__tests__/unit/spinner.test.js +83 -0
  22. package/dist/__tests__/unit/spinner.test.js.map +1 -0
  23. package/dist/commands/dev.d.ts.map +1 -1
  24. package/dist/commands/dev.js +13 -3
  25. package/dist/commands/dev.js.map +1 -1
  26. package/dist/lib/deps.d.ts +22 -0
  27. package/dist/lib/deps.d.ts.map +1 -1
  28. package/dist/lib/deps.js +121 -27
  29. package/dist/lib/deps.js.map +1 -1
  30. package/dist/lib/language-filter.d.ts +31 -0
  31. package/dist/lib/language-filter.d.ts.map +1 -0
  32. package/dist/lib/language-filter.js +14 -0
  33. package/dist/lib/language-filter.js.map +1 -0
  34. package/dist/lib/spinner.d.ts +24 -0
  35. package/dist/lib/spinner.d.ts.map +1 -1
  36. package/dist/lib/spinner.js +59 -0
  37. package/dist/lib/spinner.js.map +1 -1
  38. package/package.json +3 -3
  39. package/vendored/app/[[...slug]]/page.tsx +12 -4
  40. package/vendored/app/layout.tsx +25 -10
  41. package/vendored/components/mdx/ApiPage.tsx +10 -2
  42. package/vendored/components/mdx/OpenApiEndpoint.tsx +41 -44
  43. package/vendored/components/mdx/YouTube.tsx +8 -0
  44. package/vendored/components/navigation/Sidebar.tsx +32 -17
  45. package/vendored/components/navigation/TabsNav.tsx +22 -30
  46. package/vendored/components/ui/CodePanel.tsx +48 -3
  47. package/vendored/hooks/useIsNavigationSettled.ts +74 -0
  48. package/vendored/lib/layout-helpers.tsx +27 -0
  49. package/vendored/lib/middleware-helpers.ts +79 -8
  50. package/vendored/lib/page-isr-helpers.ts +14 -9
  51. package/vendored/lib/prefetch-batcher.ts +51 -0
  52. package/vendored/lib/prefetch-rsc.ts +19 -0
  53. package/vendored/lib/project-resolver.ts +21 -5
  54. package/vendored/lib/r2-content.ts +16 -0
  55. package/vendored/lib/r2-feature-flags.ts +7 -0
  56. package/vendored/lib/render-doc-page-openapi-helpers.ts +110 -0
  57. package/vendored/lib/render-doc-page-parallel-helpers.ts +60 -0
  58. package/vendored/lib/render-doc-page.tsx +101 -52
  59. package/vendored/lib/sidebar-prefetch-walker.ts +50 -0
  60. package/vendored/lib/static-artifacts.ts +2 -1
  61. package/vendored/workspace-package-lock.json +101 -99
@@ -34,6 +34,9 @@ import { rehypeClassToClassName } from '@/lib/rehype-class-to-classname';
34
34
  import { remarkSvgNamespaceAttrs } from '@/lib/remark-svg-namespace-attrs';
35
35
  import { rehypeNoZoomToData } from '@/lib/rehype-nozoom-to-data';
36
36
  import { rehypeUnwrapNestedAnchors } from './rehype-unwrap-nested-anchors';
37
+ import { loadSnippetsAndInlineComponents } from './render-doc-page-parallel-helpers';
38
+ import { tryOpenApiCandidatesInParallel, makeTryOpenApiSpec, formatFallbackWarning } from './render-doc-page-openapi-helpers';
39
+ import { logger } from '../shared/logger';
37
40
  import { preprocessMdx, containsPanel, containsView, buildSnippetAliasMap } from '@/lib/preprocess-mdx';
38
41
  import { loadSnippetsForIsr } from '@/lib/snippet-loader-isr';
39
42
  import { PanelWrapper } from '@/components/mdx/PanelWrapper';
@@ -188,6 +191,13 @@ function resolveSlug(normalizedSlug: string[], config: DocsConfig): string[] {
188
191
  export async function buildDocMetadata(input: RenderInput): Promise<Metadata> {
189
192
  const { slug: slugInput, projectSlug, hostAtDocs, requestHeaders } = input;
190
193
 
194
+ // Middleware sets `x-jd-noindex: true` when serving a hostAtDocs project
195
+ // directly via *.jamdesk.app and the project has no registered custom
196
+ // domain yet. The upstream subdomain shouldn't compete with the (yet-to-
197
+ // arrive) public face in search results — emit robots noindex so Google
198
+ // skips it. See proxy.ts → projectHeaderOptsForCanonical for the source.
199
+ const noindexHeader = requestHeaders?.get('x-jd-noindex') === 'true';
200
+
191
201
  if (isIsrMode()) {
192
202
  if (!projectSlug) return { title: 'Not Found' };
193
203
  const exists = await projectExists(projectSlug);
@@ -229,26 +239,37 @@ export async function buildDocMetadata(input: RenderInput): Promise<Metadata> {
229
239
  ? (data.title === config.name ? { absolute: data.title } : data.title)
230
240
  : { absolute: buildSiteTitle(config.name) };
231
241
 
242
+ const markdownHref = `${hostAtDocs ? '/docs/' : '/'}${pagePath}.md`;
243
+
244
+ // noindexHeader (middleware) and isRoot override frontmatter robots
245
+ // (already in seoMetadata) via spread order.
246
+ const robotsOverride =
247
+ isRoot || noindexHeader
248
+ ? { robots: { index: false, follow: true } as const }
249
+ : {};
250
+
232
251
  return {
233
252
  title: titleValue,
234
253
  description: data.description || '',
235
254
  ...seoMetadata,
236
- ...(isRoot && { robots: { index: false, follow: true } }),
237
- ...(data.rss ? {
238
- alternates: {
239
- ...seoMetadata.alternates,
240
- types: {
255
+ ...robotsOverride,
256
+ alternates: {
257
+ ...seoMetadata.alternates,
258
+ types: {
259
+ 'text/markdown': markdownHref,
260
+ ...(data.rss && {
241
261
  'application/rss+xml': hostAtDocs ? '/docs/feed.xml' : '/feed.xml',
242
- },
262
+ }),
243
263
  },
244
- } : {}),
264
+ },
245
265
  };
246
266
  }
247
267
 
248
268
  export async function renderDocPage(input: RenderInput): Promise<ReactElement> {
249
269
  const { slug: slugInput, projectSlug, hostAtDocs, requestHeaders } = input;
270
+ const isIsr = isIsrMode();
250
271
 
251
- if (isIsrMode()) {
272
+ if (isIsr) {
252
273
  if (!projectSlug) notFound();
253
274
  const exists = await projectExists(projectSlug);
254
275
  if (!exists) notFound();
@@ -256,6 +277,10 @@ export async function renderDocPage(input: RenderInput): Promise<ReactElement> {
256
277
 
257
278
  const loader = getContentLoader(projectSlug ?? undefined);
258
279
  const configP = loader.getConfig();
280
+ // Shiki init takes 200-500ms on cold function instances. Kick it off
281
+ // before the content fetch so the two run concurrently. Warm instances
282
+ // resolve from the globalThis singleton instantly.
283
+ const highlighterP = getHighlighter();
259
284
 
260
285
  const normalizedSlug = normalizeSlugForContent(slugInput || [], hostAtDocs);
261
286
  const slug = needsSlugRewrite(normalizedSlug)
@@ -263,9 +288,10 @@ export async function renderDocPage(input: RenderInput): Promise<ReactElement> {
263
288
  : normalizedSlug;
264
289
  const pagePath = slug.join('/');
265
290
  const currentLang = extractLanguageFromPath(`/${pagePath}`);
266
- const [fileContents, config] = await Promise.all([
291
+ const [fileContents, config, highlighter] = await Promise.all([
267
292
  loader.getContent(pagePath).catch(() => null),
268
293
  configP,
294
+ highlighterP,
269
295
  ]);
270
296
 
271
297
  if (!fileContents) {
@@ -285,14 +311,14 @@ export async function renderDocPage(input: RenderInput): Promise<ReactElement> {
285
311
  });
286
312
  const jsonLdScript = renderJsonLdScript(jsonLd);
287
313
 
288
- const highlighter = await getHighlighter();
314
+ const openApiSpecField = typeof data.openapi === 'string' && data.openapi ? data.openapi : null;
289
315
 
290
- let snippetAliases: Record<string, React.ComponentType<unknown>> = {};
291
- if (isIsrMode() && projectSlug) {
292
- snippetAliases = await loadSnippetsForIsr(projectSlug, rawContent, MDXComponents);
293
- } else {
294
- snippetAliases = buildSnippetAliasMap(rawContent, SnippetComponents);
295
- }
316
+ // Resolve openapi-isr dynamic import concurrently with the snippet/inline
317
+ // parallel block. Win is small — one module-resolve, ~5–30ms on a true cold
318
+ // start, <1ms on warm V8. Kept because the syntactic cost is one ternary.
319
+ const openApiIsrP = openApiSpecField && isIsr && projectSlug
320
+ ? import('@/lib/openapi-isr')
321
+ : null;
296
322
 
297
323
  const content = preprocessMdx(rawContent, { assetVersion: config.assetVersion });
298
324
 
@@ -300,13 +326,29 @@ export async function renderDocPage(input: RenderInput): Promise<ReactElement> {
300
326
  .filter(h => typeof h.stepNumber === 'number')
301
327
  .map(h => ({ title: h.text, slug: h.id }));
302
328
 
303
- const { inlineComponents, paramFields } = await extractInlineComponents(content, MDXComponents);
329
+ // [render-timing] proves the snippet/inline parallel win. With Promise.all,
330
+ // wall-clock should be ~max(R2_snippets, CPU_inline). Compare against the
331
+ // per-op `[r2-timing]` for snippet R2: if snippetInlineMs ≈ snippet R2 ms,
332
+ // inline ran "for free" inside the R2 wait. Sum-of-both = parallelism failed.
333
+ const snippetInlineStart = performance.now();
334
+ const { snippetAliases, inlineComponents, paramFields } = await loadSnippetsAndInlineComponents({
335
+ isIsr,
336
+ projectSlug: projectSlug ?? null,
337
+ rawContent,
338
+ preprocessedContent: content,
339
+ mdxComponents: MDXComponents,
340
+ snippetComponents: SnippetComponents,
341
+ loadSnippets: loadSnippetsForIsr,
342
+ buildAlias: buildSnippetAliasMap,
343
+ extractInline: extractInlineComponents,
344
+ });
345
+ const snippetInlineMs = Math.round(performance.now() - snippetInlineStart);
304
346
 
305
347
  const overriddenComponents = Object.keys(inlineComponents).filter(
306
348
  (name) => name in MDXComponents,
307
349
  );
308
350
  if (overriddenComponents.length > 0) {
309
- console.warn(
351
+ logger.warn(
310
352
  `[MDX] Inline component(s) override built-in: ${overriddenComponents.join(', ')} in ${slug.join('/')}`,
311
353
  );
312
354
  }
@@ -339,9 +381,11 @@ export async function renderDocPage(input: RenderInput): Promise<ReactElement> {
339
381
  let openApiEndpointData: OpenApiEndpointData | null = null;
340
382
  let openApiCodeExamples: CodeExample[] | null = null;
341
383
  let openApiError: string | null = null;
384
+ let openApiMs: number | null = null;
385
+ let openApiCandidates = 0;
342
386
 
343
387
  let lastFailure: { err: unknown; specPath: string } | null = null;
344
- if (data.openapi && typeof data.openapi === 'string') {
388
+ if (openApiSpecField) {
345
389
  try {
346
390
  const openApiConfig = config.api?.openapi;
347
391
  const allSpecPaths: string[] = typeof openApiConfig === 'string'
@@ -351,7 +395,7 @@ export async function renderDocPage(input: RenderInput): Promise<ReactElement> {
351
395
  : [];
352
396
 
353
397
  const parsed = parseOpenApiFrontmatter(
354
- data.openapi,
398
+ openApiSpecField,
355
399
  allSpecPaths.length > 0 ? allSpecPaths : undefined,
356
400
  );
357
401
 
@@ -359,43 +403,40 @@ export async function renderDocPage(input: RenderInput): Promise<ReactElement> {
359
403
  ? allSpecPaths
360
404
  : [parsed.specPath];
361
405
  const specsToTry = baseSpecs.flatMap(p => candidateSpecPaths(p, currentLang));
406
+ openApiCandidates = specsToTry.length;
362
407
 
363
- const useIsr = isIsrMode() && !!projectSlug;
364
- const resolveSpec = useIsr
365
- ? (await import('@/lib/openapi-isr')).resolveOpenApiSpec
408
+ const useIsr = isIsr && !!projectSlug;
409
+ const resolveSpec = useIsr && openApiIsrP
410
+ ? (await openApiIsrP).resolveOpenApiSpec
366
411
  : null;
367
412
  const contentDir = useIsr ? null : getContentDir();
368
413
 
369
- for (let i = 0; i < specsToTry.length; i++) {
370
- const specPath = specsToTry[i];
371
- try {
372
- if (resolveSpec && projectSlug) {
373
- const spec = await resolveSpec(projectSlug, specPath);
374
- openApiEndpointData = parseEndpoint(
375
- spec as Parameters<typeof parseEndpoint>[0],
376
- parsed.method,
377
- parsed.path,
378
- specPath,
379
- );
380
- } else {
381
- const { api } = await getCachedSpec(specPath, contentDir!);
382
- openApiEndpointData = parseEndpoint(api, parsed.method, parsed.path, specPath);
383
- }
384
- lastFailure = null;
385
- break;
386
- } catch (err) {
387
- lastFailure = { err, specPath };
388
- const isLast = i === specsToTry.length - 1;
389
- if (!isLast) {
390
- console.warn(
391
- `[openapi] spec candidate "${specPath}" failed; trying next: ${(err as Error).message}`,
392
- );
393
- }
394
- }
414
+ const tryOne = makeTryOpenApiSpec({
415
+ projectSlug: projectSlug ?? null,
416
+ isIsr: useIsr,
417
+ parsedMethod: parsed.method,
418
+ parsedPath: parsed.path,
419
+ resolveIsrSpec: resolveSpec,
420
+ getStaticSpec: useIsr ? null : getCachedSpec,
421
+ contentDir,
422
+ parseEndpointFn: parseEndpoint,
423
+ });
424
+
425
+ // See `[render-timing]` block above snippetInlineMs for the verification
426
+ // protocol. Same idea here: openApiMs ≈ max(per-spec R2) = parallel.
427
+ const openApiStart = performance.now();
428
+ const result = await tryOpenApiCandidatesInParallel(specsToTry, tryOne);
429
+ openApiMs = Math.round(performance.now() - openApiStart);
430
+ if (result.kind === 'success') {
431
+ openApiEndpointData = result.endpoint;
432
+ lastFailure = null;
433
+ const warning = formatFallbackWarning(specsToTry, result.specPath, parsed.method, parsed.path);
434
+ if (warning) logger.warn(warning);
435
+ } else {
436
+ lastFailure = { err: result.lastError, specPath: result.specPath };
437
+ throw result.lastError;
395
438
  }
396
439
 
397
- if (lastFailure) throw lastFailure.err;
398
-
399
440
  if (openApiEndpointData) {
400
441
  const { method: authMethod, headerName: authHeaderName } = resolveAuth(openApiEndpointData, config);
401
442
  const languages = config.api?.examples?.languages;
@@ -403,13 +444,21 @@ export async function renderDocPage(input: RenderInput): Promise<ReactElement> {
403
444
  }
404
445
  } catch (err) {
405
446
  const typed = classifyOpenApiLoadError(err, lastFailure?.specPath ?? null);
406
- console.warn(formatOpenApiWarning(typed));
447
+ logger.warn(formatOpenApiWarning(typed));
407
448
  openApiError = typed.suggestion
408
449
  ? `${typed.message} — ${typed.suggestion}`
409
450
  : typed.message;
410
451
  }
411
452
  }
412
453
 
454
+ logger.info('[render-timing]', {
455
+ region: process.env.VERCEL_REGION || 'unknown',
456
+ pagePath,
457
+ snippetInlineMs,
458
+ openApiMs,
459
+ openApiCandidates,
460
+ });
461
+
413
462
  let mdxApiMethod: HttpMethod | null = null;
414
463
  let mdxApiPath: string | null = null;
415
464
  if (data.api && typeof data.api === 'string' && !data.openapi) {
@@ -0,0 +1,50 @@
1
+ import type { ResolvedGroup } from '@/lib/navigation-resolver';
2
+
3
+ /**
4
+ * Flatten a resolved navigation tree to the list of hrefs that should be
5
+ * prefetched under the existing group-engagement rule from Sidebar.tsx:
6
+ *
7
+ * shouldPrefetch = level !== 0 || isExpanded || !group.name
8
+ *
9
+ * Children of a level-0 group emit only when that group is expanded.
10
+ * Children of nameless or deeper-level groups always emit. Order matches
11
+ * the rendered sidebar so paced prefetches mirror what a user is most
12
+ * likely to click next.
13
+ */
14
+ export function getPrefetchableHrefs(
15
+ groups: ResolvedGroup[],
16
+ expandedGroups: Set<string>,
17
+ linkPrefix: string,
18
+ level: number = 0,
19
+ ): string[] {
20
+ const out: string[] = [];
21
+ const walk = (groups: ResolvedGroup[], level: number) => {
22
+ for (const group of groups) {
23
+ const isExpanded = expandedGroups.has(group.name);
24
+ const groupOpen = level !== 0 || isExpanded || !group.name;
25
+ if (!groupOpen) continue;
26
+
27
+ if (group.items) {
28
+ for (const item of group.items) {
29
+ if (item.type === 'page') {
30
+ out.push(`${linkPrefix}/${item.page.path}`);
31
+ } else if (item.type === 'group') {
32
+ walk([item.group], level + 1);
33
+ }
34
+ }
35
+ } else {
36
+ for (const page of group.pages) {
37
+ out.push(`${linkPrefix}/${page.path}`);
38
+ }
39
+ if (group.nested) walk(group.nested, level + 1);
40
+ }
41
+ }
42
+ };
43
+ walk(groups, level);
44
+
45
+ // Dedup: a docs.json config can reference the same page from two groups
46
+ // (uncommon but legal). Without this, the pacer fires router.prefetch()
47
+ // twice for the same href since scheduleNext walks the items array
48
+ // literally and `seen` is populated up-front, not within the chain.
49
+ return Array.from(new Set(out));
50
+ }
@@ -170,7 +170,8 @@ Disallow: /
170
170
  return `User-agent: *
171
171
  Allow: /
172
172
 
173
- Disallow: /_next/
173
+ Allow: /_next/static/
174
+ Allow: /_next/image
174
175
 
175
176
  Sitemap: ${sitemapUrl || defaultSitemapUrl}
176
177
  `;