Skip to content
This repository was archived by the owner on May 5, 2024. It is now read-only.

Commit 53ed431

Browse files
feat(handlers): add npm
1 parent 9bfad85 commit 53ed431

File tree

1 file changed

+145
-0
lines changed

1 file changed

+145
-0
lines changed

handlers/npm.ts

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import semver from 'semver'
2+
import { respondWithError } from '../respondWithError.ts'
3+
import { rewriteContent } from '../rewriteContent.ts'
4+
import { Handler, HandlerContext } from '../types.ts'
5+
6+
function parseUrl(url: URL) {
7+
const pieces = url.pathname.split('/').slice(2)
8+
const isScoped = pieces[0].startsWith('@')
9+
10+
let name: string
11+
let version: string | null = null
12+
let submodulePath: string
13+
14+
if (isScoped) {
15+
const includesVersion = pieces[1].includes('@')
16+
17+
if (includesVersion) {
18+
version = pieces[1].split('@')[1]
19+
pieces[1] = pieces[1].split('@')[0]
20+
name = pieces.slice(0, 2).join('/')
21+
} else {
22+
name = pieces.slice(0, 2).join('/')
23+
}
24+
25+
submodulePath = '/' + pieces.slice(2).join('/')
26+
} else {
27+
const includesVersion = pieces[0].includes('@')
28+
29+
if (includesVersion) {
30+
name = pieces[0].split('@')[0]
31+
version = pieces[0].split('@')[1]
32+
} else {
33+
name = pieces[0]
34+
}
35+
36+
submodulePath = '/' + pieces.slice(1).join('/')
37+
}
38+
39+
return {
40+
name,
41+
version,
42+
submodulePath,
43+
}
44+
}
45+
46+
async function fetchVersions(
47+
ctx: HandlerContext,
48+
data: ReturnType<typeof parseUrl>,
49+
): Promise<string[]> {
50+
const cachedVersions = await ctx.versionCache.get(`npm:${data.name}`)
51+
52+
if (cachedVersions) {
53+
return cachedVersions
54+
}
55+
56+
const res = await fetch(`https://registry.npmjs.org/${data.name}`)
57+
58+
if (!res.ok) {
59+
return []
60+
}
61+
62+
let { versions } = await res.json() as { versions: string[] }
63+
64+
versions = Object.keys(versions)
65+
66+
await ctx.versionCache.set(`npm:${data.name}`, versions)
67+
68+
return versions.filter((version) => semver.valid(version))
69+
}
70+
71+
export const handle: Handler = async (ctx) => {
72+
const data = parseUrl(ctx.url)
73+
74+
// if there's no version tag
75+
if (data.version === null) {
76+
const versions = await fetchVersions(ctx, data)
77+
78+
if (versions.length === 0) {
79+
return respondWithError('BAD_MODULE')
80+
}
81+
82+
data.version = semver.rsort(versions)[0]
83+
// if there's an invalid version tag or range
84+
} else if (semver.valid(data.version) === null) {
85+
if (semver.validRange(data.version) === null) {
86+
return respondWithError('BAD_VERSION')
87+
}
88+
89+
const versions = await fetchVersions(ctx, data)
90+
91+
if (versions.length === 0) {
92+
return respondWithError('BAD_MODULE')
93+
}
94+
95+
const suggestedVersion = semver.maxSatisfying(versions, data.version)
96+
97+
if (suggestedVersion === null) {
98+
return respondWithError('BAD_VERSION')
99+
}
100+
101+
data.version = suggestedVersion
102+
}
103+
104+
if (ctx.url.searchParams.has('tgz') || ctx.url.searchParams.has('tar.gz')) {
105+
return Response.redirect(
106+
`https://registry.npmjs.org/${data.name}/-/${data.name}-${data.version}.tgz`,
107+
307,
108+
)
109+
}
110+
111+
const res = await fetch(
112+
`https://cdn.jsdelivr.net/npm/${data.name}@${data.version}${
113+
data.submodulePath === '/' ? '' : data.submodulePath
114+
}/+esm`,
115+
)
116+
117+
if (!res.ok) {
118+
return respondWithError('UNINDEXED_MODULE')
119+
}
120+
121+
let content = await res.text()
122+
123+
content = rewriteContent(content, ({ url }) => {
124+
// e.g. /npm/[email protected]/+esm
125+
if (/^\/npm(\/[^\/]+)+\/\+esm$/.test(url)) {
126+
url = url.replace(
127+
'/npm',
128+
ctx.url.protocol + '//' + ctx.url.hostname + '/npm',
129+
)
130+
url = url.slice(0, -5) // remove /+esm
131+
}
132+
133+
return url
134+
})
135+
136+
return new Response(content, {
137+
headers: {
138+
'cache-control': `public, max-age=${ctx.cacheDurationInHours * 3600}`,
139+
'content-type': 'text/javascript; charset=utf-8',
140+
'x-typescript-types': `https://esm.sh/${data.name}@${data.version}${
141+
data.submodulePath === '/' ? '' : data.submodulePath
142+
}?target=es2022`,
143+
},
144+
})
145+
}

0 commit comments

Comments
 (0)