1+ import * as v from 'valibot'
2+ import { PackageFileQuerySchema } from '#shared/schemas/package'
3+ import {
4+ CACHE_MAX_AGE_ONE_YEAR ,
5+ ERROR_PACKAGE_VERSION_AND_FILE_FAILED ,
6+ } from '#shared/utils/constants'
7+
18const CACHE_VERSION = 2
29
310// Maximum file size to fetch and highlight (500KB)
@@ -50,7 +57,10 @@ async function fetchFileContent(
5057 if ( response . status === 404 ) {
5158 throw createError ( { statusCode : 404 , message : 'File not found' } )
5259 }
53- throw createError ( { statusCode : 502 , message : 'Failed to fetch file from jsDelivr' } )
60+ throw createError ( {
61+ statusCode : 502 ,
62+ message : 'Failed to fetch file from jsDelivr' ,
63+ } )
5464 }
5565
5666 // Check content-length header if available
@@ -84,38 +94,33 @@ async function fetchFileContent(
8494 */
8595export default defineCachedEventHandler (
8696 async event => {
87- const segments = getRouterParam ( event , 'pkg' ) ?. split ( '/' ) ?? [ ]
88- if ( segments . length === 0 ) {
89- throw createError ( {
90- statusCode : 400 ,
91- message : 'Package name, version, and file path are required' ,
92- } )
93- }
94-
9597 // Parse: [pkg, 'v', version, ...filePath] or [@scope, pkg, 'v', version, ...filePath]
96- const vIndex = segments . indexOf ( 'v' )
97- if ( vIndex === - 1 || vIndex >= segments . length - 2 ) {
98- throw createError ( { statusCode : 400 , message : 'Version and file path are required' } )
99- }
98+ const pkgParamSegments = getRouterParam ( event , 'pkg' ) ?. split ( '/' ) ?? [ ]
10099
101- const packageName = segments . slice ( 0 , vIndex ) . join ( '/' )
102- // Find where version ends (next segment after 'v') and file path begins
103- // Version could be like "1.2.3" or "1.2.3-beta.1"
104- const versionAndPath = segments . slice ( vIndex + 1 )
100+ const { rawPackageName, rawVersion : fullPathAfterV } = parsePackageParams ( pkgParamSegments )
105101
106- // The version is the first segment after 'v', and everything else is the file path
107- const version = versionAndPath [ 0 ]
108- const filePath = versionAndPath . slice ( 1 ) . join ( '/' )
102+ // Since version AND path route are required, we split the remainder
103+ // fullPathAfterV => "1.2.3/dist/index.mjs"
104+ const versionSegments = fullPathAfterV ?. split ( '/' ) ?? [ ]
109105
110- if ( ! packageName || ! version || ! filePath ) {
106+ if ( versionSegments . length < 2 ) {
111107 throw createError ( {
112108 statusCode : 400 ,
113- message : 'Package name, version, and file path are required' ,
109+ message : ERROR_PACKAGE_VERSION_AND_FILE_FAILED ,
114110 } )
115111 }
116- assertValidPackageName ( packageName )
112+
113+ // The version is the first segment after 'v', and everything else is the file path
114+ const rawVersion = versionSegments [ 0 ]
115+ const rawFilePath = versionSegments . slice ( 1 ) . join ( '/' )
117116
118117 try {
118+ const { packageName, version, filePath } = v . parse ( PackageFileQuerySchema , {
119+ packageName : rawPackageName ,
120+ version : rawVersion ,
121+ filePath : rawFilePath ,
122+ } )
123+
119124 const content = await fetchFileContent ( packageName , version , filePath )
120125 const language = getLanguageFromPath ( filePath )
121126
@@ -156,7 +161,10 @@ export default defineCachedEventHandler(
156161 }
157162 }
158163
159- const html = await highlightCode ( content , language , { dependencies, resolveRelative } )
164+ const html = await highlightCode ( content , language , {
165+ dependencies,
166+ resolveRelative,
167+ } )
160168
161169 return {
162170 package : packageName ,
@@ -167,19 +175,19 @@ export default defineCachedEventHandler(
167175 html,
168176 lines : content . split ( '\n' ) . length ,
169177 }
170- } catch ( error ) {
171- if ( error && typeof error === 'object' && 'statusCode' in error ) {
172- throw error
173- }
174- throw createError ( { statusCode : 502 , message : 'Failed to fetch file content' } )
178+ } catch ( error : unknown ) {
179+ handleApiError ( error , {
180+ statusCode : 502 ,
181+ message : 'Failed to fetch file content' ,
182+ } )
175183 }
176184 } ,
177185 {
178186 // File content for a specific version never changes - cache permanently
179- maxAge : 60 * 60 * 24 * 365 , // 1 year
187+ maxAge : CACHE_MAX_AGE_ONE_YEAR , // 1 year
180188 getKey : event => {
181189 const pkg = getRouterParam ( event , 'pkg' ) ?? ''
182- return `file:v${ CACHE_VERSION } :${ pkg } `
190+ return `file:v${ CACHE_VERSION } :${ pkg . replace ( / \/ + $ / , '' ) . trim ( ) } `
183191 } ,
184192 } ,
185193)
0 commit comments