Skip to content

Commit 678f306

Browse files
authored
perf: load markdown source code on copy instead of on main request (#1386)
1 parent ffd9c9c commit 678f306

File tree

8 files changed

+461
-124
lines changed

8 files changed

+461
-124
lines changed

app/pages/package/[[org]]/[name].vue

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type {
55
PackumentVersion,
66
ProvenanceDetails,
77
ReadmeResponse,
8+
ReadmeMarkdownResponse,
89
SkillsListResponse,
910
} from '#shared/types'
1011
import type { JsrPackageInfo } from '#shared/types/jsr'
@@ -106,15 +107,47 @@ const { data: readmeData } = useLazyFetch<ReadmeResponse>(
106107
const version = requestedVersion.value
107108
return version ? `${base}/v/${version}` : base
108109
},
109-
{ default: () => ({ html: '', md: '', playgroundLinks: [], toc: [] }) },
110+
{ default: () => ({ html: '', mdExists: false, playgroundLinks: [], toc: [] }) },
111+
)
112+
113+
const {
114+
data: readmeMarkdownData,
115+
status: readmeMarkdownStatus,
116+
execute: fetchReadmeMarkdown,
117+
} = useLazyFetch<ReadmeMarkdownResponse>(
118+
() => {
119+
const base = `/api/registry/readme/markdown/${packageName.value}`
120+
const version = requestedVersion.value
121+
return version ? `${base}/v/${version}` : base
122+
},
123+
{
124+
server: false,
125+
immediate: false,
126+
default: () => ({}),
127+
},
110128
)
111129
112130
//copy README file as Markdown
113131
const { copied: copiedReadme, copy: copyReadme } = useClipboard({
114-
source: () => readmeData.value?.md ?? '',
132+
source: () => '',
115133
copiedDuring: 2000,
116134
})
117135
136+
function prefetchReadmeMarkdown() {
137+
if (readmeMarkdownStatus.value === 'idle') {
138+
fetchReadmeMarkdown()
139+
}
140+
}
141+
142+
async function copyReadmeHandler() {
143+
await fetchReadmeMarkdown()
144+
145+
const markdown = readmeMarkdownData.value?.markdown
146+
if (!markdown) return
147+
148+
await copyReadme(markdown)
149+
}
150+
118151
// Track active TOC item based on scroll position
119152
const tocItems = computed(() => readmeData.value?.toc ?? [])
120153
const { activeId: activeTocId } = useActiveTocItem(tocItems)
@@ -1238,12 +1271,14 @@ const showSkeleton = shallowRef(false)
12381271
<div class="flex gap-2">
12391272
<!-- Copy readme as Markdown button -->
12401273
<TooltipApp
1241-
v-if="readmeData?.md"
1274+
v-if="readmeData?.mdExists"
12421275
:text="$t('package.readme.copy_as_markdown')"
12431276
position="bottom"
12441277
>
12451278
<ButtonBase
1246-
@click="copyReadme()"
1279+
@mouseenter="prefetchReadmeMarkdown"
1280+
@focus="prefetchReadmeMarkdown"
1281+
@click="copyReadmeHandler()"
12471282
:aria-pressed="copiedReadme"
12481283
:aria-label="
12491284
copiedReadme ? $t('common.copied') : $t('package.readme.copy_as_markdown')
Lines changed: 7 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,5 @@
1-
import * as v from 'valibot'
2-
import { PackageRouteParamsSchema } from '#shared/schemas/package'
3-
import {
4-
CACHE_MAX_AGE_ONE_HOUR,
5-
NPM_MISSING_README_SENTINEL,
6-
ERROR_NPM_FETCH_FAILED,
7-
} from '#shared/utils/constants'
8-
9-
/** Standard README filenames to try when fetching from jsdelivr (case-sensitive CDN) */
10-
const standardReadmeFilenames = [
11-
'README.md',
12-
'readme.md',
13-
'Readme.md',
14-
'README',
15-
'readme',
16-
'README.markdown',
17-
'readme.markdown',
18-
]
19-
20-
/** Matches standard README filenames (case-insensitive, for checking registry metadata) */
21-
const standardReadmePattern = /^readme(?:\.md|\.markdown)?$/i
22-
23-
/**
24-
* Fetch README from jsdelivr CDN for a specific package version.
25-
* Falls back through common README filenames.
26-
*/
27-
async function fetchReadmeFromJsdelivr(
28-
packageName: string,
29-
readmeFilenames: string[],
30-
version?: string,
31-
): Promise<string | null> {
32-
const versionSuffix = version ? `@${version}` : ''
33-
34-
for (const filename of readmeFilenames) {
35-
try {
36-
const url = `https://cdn.jsdelivr.net/npm/${packageName}${versionSuffix}/${filename}`
37-
const response = await fetch(url)
38-
if (response.ok) {
39-
return await response.text()
40-
}
41-
} catch {
42-
// Try next filename
43-
}
44-
}
45-
46-
return null
47-
}
1+
import { CACHE_MAX_AGE_ONE_HOUR, ERROR_NPM_FETCH_FAILED } from '#shared/utils/constants'
2+
import { resolvePackageReadmeSource } from '#server/utils/readme-loaders'
483

494
/**
505
* Returns rendered README HTML for a package.
@@ -57,63 +12,15 @@ async function fetchReadmeFromJsdelivr(
5712
*/
5813
export default defineCachedEventHandler(
5914
async event => {
60-
// Parse package name and optional version from URL segments
61-
// Patterns: [pkg] or [pkg, 'v', version] or [@scope, pkg] or [@scope, pkg, 'v', version]
62-
const pkgParamSegments = getRouterParam(event, 'pkg')?.split('/') ?? []
63-
64-
const { rawPackageName, rawVersion } = parsePackageParams(pkgParamSegments)
65-
6615
try {
67-
// 1. Validate
68-
const { packageName, version } = v.parse(PackageRouteParamsSchema, {
69-
packageName: rawPackageName,
70-
version: rawVersion,
71-
})
72-
73-
const packageData = await fetchNpmPackage(packageName)
16+
const packagePath = getRouterParam(event, 'pkg') ?? ''
17+
const { packageName, markdown, repoInfo } = await resolvePackageReadmeSource(packagePath)
7418

75-
let readmeContent: string | undefined
76-
let readmeFilename: string | undefined
77-
78-
// If a specific version is requested, get README from that version
79-
if (version) {
80-
const versionData = packageData.versions[version]
81-
if (versionData) {
82-
readmeContent = versionData.readme
83-
readmeFilename = versionData.readmeFilename
84-
}
85-
} else {
86-
// Use the packument-level readme (from latest version)
87-
readmeContent = packageData.readme
88-
readmeFilename = packageData.readmeFilename
19+
if (!markdown) {
20+
return { html: '', mdExists: false, playgroundLinks: [], toc: [] }
8921
}
9022

91-
const hasValidNpmReadme = readmeContent && readmeContent !== NPM_MISSING_README_SENTINEL
92-
93-
// If no README in packument, or if readmeFilename is non-standard (e.g., README.zh-TW.md),
94-
// try fetching a standard README from jsdelivr (package tarball).
95-
// Note: When readmeFilename is missing, we defensively fetch from jsdelivr to ensure
96-
// we get a standard English README if one exists.
97-
if (!hasValidNpmReadme || !isStandardReadme(readmeFilename)) {
98-
const jsdelivrReadme = await fetchReadmeFromJsdelivr(
99-
packageName,
100-
standardReadmeFilenames,
101-
version,
102-
)
103-
// Only replace npm content if jsdelivr returned something
104-
if (jsdelivrReadme) {
105-
readmeContent = jsdelivrReadme
106-
}
107-
}
108-
109-
if (!readmeContent || readmeContent === NPM_MISSING_README_SENTINEL) {
110-
return { html: '', playgroundLinks: [], toc: [] }
111-
}
112-
113-
// Parse repository info for resolving relative URLs to GitHub
114-
const repoInfo = parseRepositoryInfo(packageData.repository)
115-
116-
return await renderReadmeHtml(readmeContent, packageName, repoInfo)
23+
return await renderReadmeHtml(markdown, packageName, repoInfo)
11724
} catch (error: unknown) {
11825
handleApiError(error, {
11926
statusCode: 502,
@@ -130,7 +37,3 @@ export default defineCachedEventHandler(
13037
},
13138
},
13239
)
133-
134-
function isStandardReadme(filename: string | undefined): boolean {
135-
return !!filename && standardReadmePattern.test(filename)
136-
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import type { H3Event } from 'h3'
2+
import { ERROR_NPM_FETCH_FAILED } from '#shared/utils/constants'
3+
import { resolvePackageReadmeSource } from '#server/utils/readme-loaders'
4+
5+
export default async function getMarkdownReadme(event: H3Event) {
6+
try {
7+
const packagePath = getRouterParam(event, 'pkg') ?? ''
8+
return await resolvePackageReadmeSource(packagePath)
9+
} catch (error: unknown) {
10+
handleApiError(error, {
11+
statusCode: 502,
12+
message: ERROR_NPM_FETCH_FAILED,
13+
})
14+
}
15+
}

server/utils/readme-loaders.ts

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import * as v from 'valibot'
2+
import { PackageRouteParamsSchema } from '#shared/schemas/package'
3+
import { CACHE_MAX_AGE_ONE_HOUR, NPM_MISSING_README_SENTINEL } from '#shared/utils/constants'
4+
5+
/** Standard README filenames to try when fetching from jsdelivr (case-sensitive CDN) */
6+
const standardReadmeFilenames = [
7+
'README.md',
8+
'readme.md',
9+
'Readme.md',
10+
'README',
11+
'readme',
12+
'README.markdown',
13+
'readme.markdown',
14+
]
15+
16+
/** Matches standard README filenames (case-insensitive, for checking registry metadata) */
17+
const standardReadmePattern = /^readme(?:\.md|\.markdown)?$/i
18+
19+
export function isStandardReadme(filename: string | undefined): boolean {
20+
return !!filename && standardReadmePattern.test(filename)
21+
}
22+
23+
/**
24+
* Fetch README from jsdelivr CDN for a specific package version.
25+
* Falls back through common README filenames.
26+
*/
27+
export async function fetchReadmeFromJsdelivr(
28+
packageName: string,
29+
readmeFilenames: string[],
30+
version?: string,
31+
): Promise<string | null> {
32+
const versionSuffix = version ? `@${version}` : ''
33+
34+
for (const filename of readmeFilenames) {
35+
try {
36+
const url = `https://cdn.jsdelivr.net/npm/${packageName}${versionSuffix}/${filename}`
37+
const response = await fetch(url)
38+
if (response.ok) {
39+
return await response.text()
40+
}
41+
} catch {
42+
// Try next filename
43+
}
44+
}
45+
46+
return null
47+
}
48+
49+
export const resolvePackageReadmeSource = defineCachedFunction(
50+
async (packagePath: string) => {
51+
const pkgParamSegments = packagePath.split('/')
52+
53+
const { rawPackageName, rawVersion } = parsePackageParams(pkgParamSegments)
54+
55+
const { packageName, version } = v.parse(PackageRouteParamsSchema, {
56+
packageName: rawPackageName,
57+
version: rawVersion,
58+
})
59+
60+
const packageData = await fetchNpmPackage(packageName)
61+
62+
let readmeContent: string | undefined
63+
let readmeFilename: string | undefined
64+
65+
if (version) {
66+
const versionData = packageData.versions[version]
67+
if (versionData) {
68+
readmeContent = versionData.readme
69+
readmeFilename = versionData.readmeFilename
70+
}
71+
} else {
72+
readmeContent = packageData.readme
73+
readmeFilename = packageData.readmeFilename
74+
}
75+
76+
const hasValidNpmReadme = readmeContent && readmeContent !== NPM_MISSING_README_SENTINEL
77+
78+
if (!hasValidNpmReadme || !isStandardReadme(readmeFilename)) {
79+
const jsdelivrReadme = await fetchReadmeFromJsdelivr(
80+
packageName,
81+
standardReadmeFilenames,
82+
version,
83+
)
84+
if (jsdelivrReadme) {
85+
readmeContent = jsdelivrReadme
86+
}
87+
}
88+
89+
if (!readmeContent || readmeContent === NPM_MISSING_README_SENTINEL) {
90+
return {
91+
packageName,
92+
version,
93+
markdown: undefined,
94+
repoInfo: undefined,
95+
}
96+
}
97+
98+
const repoInfo = parseRepositoryInfo(packageData.repository)
99+
100+
return {
101+
packageName,
102+
version,
103+
markdown: readmeContent,
104+
repoInfo,
105+
}
106+
},
107+
{
108+
maxAge: CACHE_MAX_AGE_ONE_HOUR,
109+
swr: true,
110+
getKey: (packagePath: string) => packagePath,
111+
},
112+
)

server/utils/readme.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -319,7 +319,7 @@ export async function renderReadmeHtml(
319319
packageName: string,
320320
repoInfo?: RepositoryInfo,
321321
): Promise<ReadmeResponse> {
322-
if (!content) return { html: '', md: '', playgroundLinks: [], toc: [] }
322+
if (!content) return { html: '', playgroundLinks: [], toc: [] }
323323

324324
const shiki = await getShikiHighlighter()
325325
const renderer = new marked.Renderer()
@@ -511,7 +511,7 @@ ${html}
511511

512512
return {
513513
html: convertToEmoji(sanitized),
514-
md: content,
514+
mdExists: Boolean(content),
515515
playgroundLinks: collectedLinks,
516516
toc,
517517
}

shared/types/readme.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,17 @@ export interface TocItem {
2828
* Response from README API endpoint
2929
*/
3030
export interface ReadmeResponse {
31+
/** Whether the README exists */
32+
mdExists?: boolean
3133
/** Rendered HTML content */
3234
html: string
33-
/** Original markdown content */
34-
md: string
3535
/** Extracted playground/demo links */
3636
playgroundLinks: PlaygroundLink[]
3737
/** Table of contents extracted from headings */
3838
toc: TocItem[]
3939
}
40+
41+
export interface ReadmeMarkdownResponse {
42+
/** Original markdown content */
43+
markdown?: string
44+
}

0 commit comments

Comments
 (0)