Skip to content

Commit 9e61037

Browse files
committed
fix: more resilience for scoped packages
1 parent a88644f commit 9e61037

3 files changed

Lines changed: 93 additions & 29 deletions

File tree

modules/runtime/server/cache.ts

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,42 @@ function getFixturePath(type: FixtureType, name: string): string {
6262
return `${dir}:${filename.replace(/\//g, ':')}`
6363
}
6464

65+
/**
66+
* Parse a scoped package name with optional version.
67+
* Handles formats like: @scope/name, @scope/name@version, name, name@version
68+
*/
69+
function parseScopedPackageWithVersion(input: string): { name: string; version?: string } {
70+
if (input.startsWith('@')) {
71+
// Scoped package: @scope/name or @scope/name@version
72+
const slashIndex = input.indexOf('/')
73+
if (slashIndex === -1) {
74+
// Invalid format like just "@scope"
75+
return { name: input }
76+
}
77+
const afterSlash = input.slice(slashIndex + 1)
78+
const atIndex = afterSlash.indexOf('@')
79+
if (atIndex === -1) {
80+
// @scope/name (no version)
81+
return { name: input }
82+
}
83+
// @scope/name@version
84+
return {
85+
name: input.slice(0, slashIndex + 1 + atIndex),
86+
version: afterSlash.slice(atIndex + 1),
87+
}
88+
}
89+
90+
// Unscoped package: name or name@version
91+
const atIndex = input.indexOf('@')
92+
if (atIndex === -1) {
93+
return { name: input }
94+
}
95+
return {
96+
name: input.slice(0, atIndex),
97+
version: input.slice(atIndex + 1),
98+
}
99+
}
100+
65101
function getMockForUrl(url: string): MockResult | null {
66102
let urlObj: URL
67103
try {
@@ -136,12 +172,12 @@ function getMockForUrl(url: string): MockResult | null {
136172
const packageMatch = decodeURIComponent(pathname).match(/^\/v1\/packages\/npm\/(.+)$/)
137173
if (packageMatch?.[1]) {
138174
const pkgWithVersion = packageMatch[1]
139-
const [name] = pkgWithVersion.split('@')
175+
const parsed = parseScopedPackageWithVersion(pkgWithVersion)
140176
return {
141177
data: {
142178
type: 'npm',
143-
name,
144-
version: pkgWithVersion.includes('@') ? pkgWithVersion.split('@')[1] : 'latest',
179+
name: parsed.name,
180+
version: parsed.version || 'latest',
145181
files: [
146182
{ name: 'package.json', hash: 'abc123', size: 1000 },
147183
{ name: 'index.js', hash: 'def456', size: 500 },

scripts/generate-fixtures.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,14 @@ async function generateEsmTypesFixture(packageName: string, version: string): Pr
332332
try {
333333
// First, get the types URL from the header
334334
const headResponse = await fetch(baseUrl, { method: 'HEAD' })
335+
336+
if (!headResponse.ok) {
337+
console.log(
338+
` esm.sh HEAD request failed for ${packageName}@${version}: HTTP ${headResponse.status}`,
339+
)
340+
return
341+
}
342+
335343
const typesUrl = headResponse.headers.get('x-typescript-types')
336344

337345
if (!typesUrl) {

test/e2e/test-utils.ts

Lines changed: 46 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,49 @@ function readFixture(relativePath: string): unknown | null {
1717
}
1818
}
1919

20+
/**
21+
* Parse a scoped package name into its components.
22+
* Handles formats like: @scope/name, @scope/name@version, name, name@version
23+
*/
24+
function parseScopedPackage(input: string): { name: string; version?: string } {
25+
if (input.startsWith('@')) {
26+
// Scoped package: @scope/name or @scope/name@version
27+
const slashIndex = input.indexOf('/')
28+
if (slashIndex === -1) {
29+
// Invalid format like just "@scope"
30+
return { name: input }
31+
}
32+
const afterSlash = input.slice(slashIndex + 1)
33+
const atIndex = afterSlash.indexOf('@')
34+
if (atIndex === -1) {
35+
// @scope/name (no version)
36+
return { name: input }
37+
}
38+
// @scope/name@version
39+
return {
40+
name: input.slice(0, slashIndex + 1 + atIndex),
41+
version: afterSlash.slice(atIndex + 1),
42+
}
43+
}
44+
45+
// Unscoped package: name or name@version
46+
const atIndex = input.indexOf('@')
47+
if (atIndex === -1) {
48+
return { name: input }
49+
}
50+
return {
51+
name: input.slice(0, atIndex),
52+
version: input.slice(atIndex + 1),
53+
}
54+
}
55+
2056
function packageToFixturePath(packageName: string): string {
2157
if (packageName.startsWith('@')) {
2258
const [scope, name] = packageName.slice(1).split('/')
59+
if (!name) {
60+
// Guard against invalid scoped package format like just "@scope"
61+
return `npm-registry/packuments/${packageName}.json`
62+
}
2363
return `npm-registry/packuments/@${scope}/${name}.json`
2464
}
2565
return `npm-registry/packuments/${packageName}.json`
@@ -294,12 +334,13 @@ async function handleJsdelivrDataApi(route: Route): Promise<boolean> {
294334
// Package file listing: /v1/packages/npm/{package}@{version}
295335
const packageMatch = pathname.match(/^\/v1\/packages\/npm\/(.+)$/)
296336
if (packageMatch?.[1]) {
337+
const parsed = parseScopedPackage(packageMatch[1])
297338
// Return a minimal file tree
298339
await route.fulfill({
299340
json: {
300341
type: 'npm',
301-
name: packageMatch[1].split('@')[0],
302-
version: packageMatch[1].split('@')[1] || 'latest',
342+
name: parsed.name,
343+
version: parsed.version || 'latest',
303344
files: [
304345
{ name: 'package.json', hash: 'abc123', size: 1000 },
305346
{ name: 'index.js', hash: 'def456', size: 500 },
@@ -324,7 +365,7 @@ async function handleGravatarApi(route: Route): Promise<boolean> {
324365

325366
/**
326367
* Handle GitHub API requests.
327-
* Returns mock contributor data for the contributors endpoint.
368+
* Returns mock contributor data from fixtures for the contributors endpoint.
328369
*/
329370
async function handleGitHubApi(route: Route): Promise<boolean> {
330371
const url = new URL(route.request().url())
@@ -333,30 +374,9 @@ async function handleGitHubApi(route: Route): Promise<boolean> {
333374
// Contributors endpoint: /repos/{owner}/{repo}/contributors
334375
const contributorsMatch = pathname.match(/^\/repos\/([^/]+)\/([^/]+)\/contributors$/)
335376
if (contributorsMatch) {
377+
const fixture = readFixture('github/contributors.json')
336378
await route.fulfill({
337-
json: [
338-
{
339-
login: 'danielroe',
340-
id: 28706372,
341-
avatar_url: 'https://avatars.githubusercontent.com/u/28706372?v=4',
342-
html_url: 'https://github.com/danielroe',
343-
contributions: 150,
344-
},
345-
{
346-
login: 'antfu',
347-
id: 11247099,
348-
avatar_url: 'https://avatars.githubusercontent.com/u/11247099?v=4',
349-
html_url: 'https://github.com/antfu',
350-
contributions: 120,
351-
},
352-
{
353-
login: 'pi0',
354-
id: 5158436,
355-
avatar_url: 'https://avatars.githubusercontent.com/u/5158436?v=4',
356-
html_url: 'https://github.com/pi0',
357-
contributions: 100,
358-
},
359-
],
379+
json: fixture || [],
360380
})
361381
return true
362382
}

0 commit comments

Comments
 (0)