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.
- package/dist/__tests__/unit/deps.test.js +184 -0
- package/dist/__tests__/unit/deps.test.js.map +1 -1
- package/dist/__tests__/unit/dev-spinner-ownership.test.d.ts +2 -0
- package/dist/__tests__/unit/dev-spinner-ownership.test.d.ts.map +1 -0
- package/dist/__tests__/unit/dev-spinner-ownership.test.js +37 -0
- package/dist/__tests__/unit/dev-spinner-ownership.test.js.map +1 -0
- 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/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/__tests__/unit/spinner.test.d.ts +2 -0
- package/dist/__tests__/unit/spinner.test.d.ts.map +1 -0
- package/dist/__tests__/unit/spinner.test.js +83 -0
- package/dist/__tests__/unit/spinner.test.js.map +1 -0
- package/dist/commands/dev.d.ts.map +1 -1
- package/dist/commands/dev.js +13 -3
- package/dist/commands/dev.js.map +1 -1
- package/dist/lib/deps.d.ts +22 -0
- package/dist/lib/deps.d.ts.map +1 -1
- package/dist/lib/deps.js +121 -27
- package/dist/lib/deps.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/dist/lib/spinner.d.ts +24 -0
- package/dist/lib/spinner.d.ts.map +1 -1
- package/dist/lib/spinner.js +59 -0
- package/dist/lib/spinner.js.map +1 -1
- package/package.json +3 -3
- package/vendored/app/[[...slug]]/page.tsx +12 -4
- package/vendored/app/layout.tsx +25 -10
- package/vendored/components/mdx/ApiPage.tsx +10 -2
- package/vendored/components/mdx/OpenApiEndpoint.tsx +41 -44
- package/vendored/components/mdx/YouTube.tsx +8 -0
- package/vendored/components/navigation/Sidebar.tsx +32 -17
- package/vendored/components/navigation/TabsNav.tsx +22 -30
- package/vendored/components/ui/CodePanel.tsx +48 -3
- package/vendored/hooks/useIsNavigationSettled.ts +74 -0
- package/vendored/lib/layout-helpers.tsx +27 -0
- package/vendored/lib/middleware-helpers.ts +79 -8
- package/vendored/lib/page-isr-helpers.ts +14 -9
- package/vendored/lib/prefetch-batcher.ts +51 -0
- package/vendored/lib/prefetch-rsc.ts +19 -0
- package/vendored/lib/project-resolver.ts +21 -5
- package/vendored/lib/r2-content.ts +16 -0
- package/vendored/lib/r2-feature-flags.ts +7 -0
- package/vendored/lib/render-doc-page-openapi-helpers.ts +110 -0
- package/vendored/lib/render-doc-page-parallel-helpers.ts +60 -0
- package/vendored/lib/render-doc-page.tsx +101 -52
- package/vendored/lib/sidebar-prefetch-walker.ts +50 -0
- package/vendored/lib/static-artifacts.ts +2 -1
- 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
|
-
...
|
|
237
|
-
|
|
238
|
-
alternates
|
|
239
|
-
|
|
240
|
-
|
|
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 (
|
|
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
|
|
314
|
+
const openApiSpecField = typeof data.openapi === 'string' && data.openapi ? data.openapi : null;
|
|
289
315
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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 =
|
|
364
|
-
const resolveSpec = useIsr
|
|
365
|
-
? (await
|
|
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
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
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
|
-
|
|
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
|
+
}
|