@@ -18,6 +18,46 @@ export interface TimelineResponse {
1818 total : number
1919}
2020
21+ const fetchTimeline = defineCachedFunction (
22+ async ( packageName : string ) : Promise < TimelineVersion [ ] > => {
23+ const packument = await fetchNpmPackage ( packageName )
24+
25+ const tagsByVersion = new Map < string , string [ ] > ( )
26+ for ( const [ tag , ver ] of Object . entries ( packument [ 'dist-tags' ] ?? { } ) ) {
27+ const list = tagsByVersion . get ( ver )
28+ if ( list ) list . push ( tag )
29+ else tagsByVersion . set ( ver , [ tag ] )
30+ }
31+
32+ return Object . keys ( packument . versions )
33+ . filter ( v => packument . time [ v ] )
34+ . map ( v => {
35+ const version = packument . versions [ v ] !
36+ let license = version . license
37+ if ( license && typeof license === 'object' && 'type' in license ) {
38+ license = ( license as { type : string } ) . type
39+ }
40+
41+ return {
42+ version : v ,
43+ time : packument . time [ v ] ! ,
44+ license : typeof license === 'string' ? license : undefined ,
45+ type : typeof version . type === 'string' ? version . type : undefined ,
46+ hasTypes : hasBuiltInTypes ( version ) || undefined ,
47+ hasTrustedPublisher : version . _npmUser ?. trustedPublisher ? true : undefined ,
48+ hasProvenance : version . dist ?. attestations ? true : undefined ,
49+ tags : tagsByVersion . get ( v ) ?? [ ] ,
50+ }
51+ } )
52+ . sort ( ( a , b ) => Date . parse ( b . time ) - Date . parse ( a . time ) )
53+ } ,
54+ {
55+ maxAge : CACHE_MAX_AGE_FIVE_MINUTES ,
56+ swr : true ,
57+ getKey : ( packageName : string ) => `timeline:v1:${ packageName } ` ,
58+ } ,
59+ )
60+
2161/**
2262 * Returns paginated version timeline data for a package.
2363 *
@@ -28,76 +68,34 @@ export interface TimelineResponse {
2868 * - /api/registry/timeline/packageName?offset=0&limit=25
2969 * - /api/registry/timeline/@scope/packageName?offset=0&limit=25
3070 */
31- export default defineCachedEventHandler (
32- async event => {
33- const pkgParam = getRouterParam ( event , 'pkg' )
34- if ( ! pkgParam ) {
35- throw createError ( { statusCode : 404 , message : 'Package name is required' } )
36- }
71+ export default defineEventHandler ( async event => {
72+ const pkgParam = getRouterParam ( event , 'pkg' )
73+ if ( ! pkgParam ) {
74+ throw createError ( { statusCode : 404 , message : 'Package name is required' } )
75+ }
3776
38- let packageName : string
39- try {
40- packageName = decodeURIComponent ( pkgParam )
41- } catch {
42- throw createError ( { statusCode : 400 , message : 'Invalid package name encoding' } )
43- }
44-
45- const query = getQuery ( event )
46- const offset = Math . max ( 0 , Number ( query . offset ) || 0 )
47- const limit = Math . max ( 1 , Math . min ( 100 , Number ( query . limit ) || DEFAULT_LIMIT ) )
48-
49- try {
50- const packument = await fetchNpmPackage ( packageName )
77+ let packageName : string
78+ try {
79+ packageName = decodeURIComponent ( pkgParam )
80+ } catch {
81+ throw createError ( { statusCode : 400 , message : 'Invalid package name encoding' } )
82+ }
5183
52- const tagsByVersion = new Map < string , string [ ] > ( )
53- for ( const [ tag , ver ] of Object . entries ( packument [ 'dist-tags' ] ?? { } ) ) {
54- const list = tagsByVersion . get ( ver )
55- if ( list ) list . push ( tag )
56- else tagsByVersion . set ( ver , [ tag ] )
57- }
84+ const query = getQuery ( event )
85+ const offset = Math . max ( 0 , Number ( query . offset ) || 0 )
86+ const limit = Math . max ( 1 , Math . min ( 100 , Number ( query . limit ) || DEFAULT_LIMIT ) )
5887
59- // Build full sorted list
60- const allVersions = Object . keys ( packument . versions )
61- . filter ( v => packument . time [ v ] )
62- . map ( v => {
63- const version = packument . versions [ v ] !
64- let license = version . license
65- if ( license && typeof license === 'object' && 'type' in license ) {
66- license = ( license as { type : string } ) . type
67- }
88+ try {
89+ const allVersions = await fetchTimeline ( packageName )
6890
69- return {
70- version : v ,
71- time : packument . time [ v ] ! ,
72- license : typeof license === 'string' ? license : undefined ,
73- type : typeof version . type === 'string' ? version . type : undefined ,
74- hasTypes : hasBuiltInTypes ( version ) || undefined ,
75- hasTrustedPublisher : version . _npmUser ?. trustedPublisher ? true : undefined ,
76- hasProvenance : version . dist ?. attestations ? true : undefined ,
77- tags : tagsByVersion . get ( v ) ?? [ ] ,
78- }
79- } )
80- . sort ( ( a , b ) => Date . parse ( b . time ) - Date . parse ( a . time ) )
81-
82- return {
83- versions : allVersions . slice ( offset , offset + limit ) ,
84- total : allVersions . length ,
85- } satisfies TimelineResponse
86- } catch ( error : unknown ) {
87- handleApiError ( error , {
88- statusCode : 502 ,
89- message : `Failed to fetch timeline for ${ packageName } ` ,
90- } )
91- }
92- } ,
93- {
94- maxAge : CACHE_MAX_AGE_FIVE_MINUTES ,
95- swr : true ,
96- getKey : event => {
97- const query = getQuery ( event )
98- const offset = Math . max ( 0 , Number ( query . offset ) || 0 )
99- const limit = Math . max ( 1 , Math . min ( 100 , Number ( query . limit ) || DEFAULT_LIMIT ) )
100- return `timeline:v1:${ getRouterParam ( event , 'pkg' ) } :${ offset } :${ limit } `
101- } ,
102- } ,
103- )
91+ return {
92+ versions : allVersions . slice ( offset , offset + limit ) ,
93+ total : allVersions . length ,
94+ } satisfies TimelineResponse
95+ } catch ( error : unknown ) {
96+ handleApiError ( error , {
97+ statusCode : 502 ,
98+ message : `Failed to fetch timeline for ${ packageName } ` ,
99+ } )
100+ }
101+ } )
0 commit comments