@@ -4,6 +4,36 @@ import { NpmInfo } from '@devtools/kit';
44import { execSync } from 'child_process' ;
55import path from 'path' ;
66
7+ // In-memory cache for npm package information
8+ interface CacheEntry {
9+ data : any ;
10+ timestamp : number ;
11+ }
12+
13+ const packageCache = new Map < string , CacheEntry > ( ) ;
14+ const CACHE_TTL = 1000 * 60 * 30 ; // 30 minutes cache TTL
15+
16+ // Preloaded dependencies cache - loaded at server startup
17+ let preloadedDependencies : any [ ] | null = null ;
18+ let isPreloading = false ;
19+ let preloadPromise : Promise < any [ ] > | null = null ;
20+
21+ function getCachedPackage ( name : string ) : any | null {
22+ const cached = packageCache . get ( name ) ;
23+ if ( cached && Date . now ( ) - cached . timestamp < CACHE_TTL ) {
24+ return cached . data ;
25+ }
26+ packageCache . delete ( name ) ;
27+ return null ;
28+ }
29+
30+ function setCachedPackage ( name : string , data : any ) : void {
31+ packageCache . set ( name , {
32+ data,
33+ timestamp : Date . now ( ) ,
34+ } ) ;
35+ }
36+
737export async function detectPackageManager (
838 projectRoot : string ,
939) : Promise < 'npm' | 'pnpm' | 'yarn' > {
@@ -38,6 +68,194 @@ export async function detectPackageManager(
3868 }
3969}
4070
71+ // Preload dependencies function - moved to module scope
72+ const preloadDependencies = async ( config : any ) : Promise < any [ ] > => {
73+ if ( preloadedDependencies ) {
74+ console . log ( '[Qwik DevTools] Dependencies already preloaded' ) ;
75+ return preloadedDependencies ;
76+ }
77+
78+ if ( isPreloading && preloadPromise ) {
79+ console . log ( '[Qwik DevTools] Preloading already in progress...' ) ;
80+ return preloadPromise ;
81+ }
82+
83+ isPreloading = true ;
84+ console . log ( '[Qwik DevTools] Starting to preload dependencies...' ) ;
85+
86+ preloadPromise = ( async ( ) => {
87+ const pathToPackageJson = config . configFileDependencies . find (
88+ ( file : string ) => file . endsWith ( 'package.json' ) ,
89+ ) ;
90+
91+ if ( ! pathToPackageJson ) {
92+ preloadedDependencies = [ ] ;
93+ isPreloading = false ;
94+ console . log ( '[Qwik DevTools] No package.json found' ) ;
95+ return [ ] ;
96+ }
97+
98+ try {
99+ const pkgJson = await fsp . readFile ( pathToPackageJson , 'utf-8' ) ;
100+ const pkg = JSON . parse ( pkgJson ) ;
101+
102+ const allDeps = {
103+ ...pkg . dependencies || { } ,
104+ ...pkg . devDependencies || { } ,
105+ ...pkg . peerDependencies || { } ,
106+ } ;
107+
108+ const dependencies = Object . entries < string > ( allDeps ) ;
109+
110+ // Check cache first
111+ const cachedPackages : any [ ] = [ ] ;
112+ const uncachedDependencies : [ string , string ] [ ] = [ ] ;
113+
114+ for ( const [ name , version ] of dependencies ) {
115+ const cached = getCachedPackage ( name ) ;
116+ if ( cached ) {
117+ cachedPackages . push ( { ...cached , version } ) ;
118+ } else {
119+ uncachedDependencies . push ( [ name , version ] ) ;
120+ }
121+ }
122+
123+ if ( uncachedDependencies . length === 0 ) {
124+ preloadedDependencies = cachedPackages ;
125+ isPreloading = false ;
126+ return cachedPackages ;
127+ }
128+
129+ // Load all dependencies - use larger batch for initial preload
130+ const batchSize = 100 ;
131+ const batches = [ ] ;
132+ for ( let i = 0 ; i < uncachedDependencies . length ; i += batchSize ) {
133+ batches . push ( uncachedDependencies . slice ( i , i + batchSize ) ) ;
134+ }
135+
136+ const fetchedPackages : any [ ] = [ ] ;
137+
138+ console . log ( `[Qwik DevTools] Fetching ${ uncachedDependencies . length } packages in parallel...` ) ;
139+
140+ const allBatchPromises = batches . map ( async ( batch ) => {
141+ const batchPromises = batch . map ( async ( [ name , version ] ) => {
142+ try {
143+ const controller = new AbortController ( ) ;
144+ const timeoutId = setTimeout ( ( ) => controller . abort ( ) , 5000 ) ; // Longer timeout for initial load
145+
146+ const response = await fetch ( `https://registry.npmjs.org/${ name } ` , {
147+ headers : {
148+ 'Accept' : 'application/json' ,
149+ } ,
150+ signal : controller . signal ,
151+ } ) ;
152+
153+ clearTimeout ( timeoutId ) ;
154+
155+ if ( ! response . ok ) {
156+ throw new Error ( `HTTP ${ response . status } ` ) ;
157+ }
158+
159+ const packageData = await response . json ( ) ;
160+
161+ const latestVersion = packageData [ 'dist-tags' ] ?. latest || version ;
162+ const versionData = packageData . versions ?. [ latestVersion ] || packageData . versions ?. [ version ] ;
163+
164+ let repositoryUrl = versionData ?. repository ?. url || packageData . repository ?. url ;
165+ if ( repositoryUrl ) {
166+ repositoryUrl = repositoryUrl
167+ . replace ( / ^ g i t \+ / , '' )
168+ . replace ( / ^ s s h : \/ \/ g i t @ / , 'https://' )
169+ . replace ( / \. g i t $ / , '' ) ;
170+ }
171+
172+ let iconUrl = null ;
173+
174+ if ( packageData . logo ) {
175+ iconUrl = packageData . logo ;
176+ } else if ( name . startsWith ( '@' ) ) {
177+ const scope = name . split ( '/' ) [ 0 ] . substring ( 1 ) ;
178+ iconUrl = `https://avatars.githubusercontent.com/${ scope } ?size=64` ;
179+ } else if ( repositoryUrl ?. includes ( 'github.com' ) ) {
180+ const repoMatch = repositoryUrl . match ( / g i t h u b \. c o m \/ ( [ ^ \/ ] + ) / ) ;
181+ if ( repoMatch ) {
182+ iconUrl = `https://avatars.githubusercontent.com/${ repoMatch [ 1 ] } ?size=64` ;
183+ }
184+ }
185+
186+ const packageInfo = {
187+ name,
188+ version,
189+ description : versionData ?. description || packageData . description || 'No description available' ,
190+ author : versionData ?. author || packageData . author ,
191+ homepage : versionData ?. homepage || packageData . homepage ,
192+ repository : repositoryUrl ,
193+ npmUrl : `https://www.npmjs.com/package/${ name } ` ,
194+ iconUrl,
195+ } ;
196+
197+ setCachedPackage ( name , packageInfo ) ;
198+ return packageInfo ;
199+ } catch ( error ) {
200+ const basicInfo = {
201+ name,
202+ version,
203+ description : 'No description available' ,
204+ npmUrl : `https://www.npmjs.com/package/${ name } ` ,
205+ iconUrl : null ,
206+ } ;
207+
208+ setCachedPackage ( name , basicInfo ) ;
209+ return basicInfo ;
210+ }
211+ } ) ;
212+
213+ const batchResults = await Promise . allSettled ( batchPromises ) ;
214+ return batchResults
215+ . filter ( ( result ) : result is PromiseFulfilledResult < any > => result . status === 'fulfilled' )
216+ . map ( result => result . value ) ;
217+ } ) ;
218+
219+ const allBatchResults = await Promise . all ( allBatchPromises ) ;
220+ for ( const batchResult of allBatchResults ) {
221+ fetchedPackages . push ( ...batchResult ) ;
222+ }
223+
224+ const allPackages = [ ...cachedPackages , ...fetchedPackages ] ;
225+ preloadedDependencies = allPackages ;
226+ isPreloading = false ;
227+
228+ console . log ( `[Qwik DevTools] ✓ Successfully preloaded ${ allPackages . length } dependencies` ) ;
229+
230+ return allPackages ;
231+ } catch ( error ) {
232+ console . error ( '[Qwik DevTools] ✗ Failed to preload dependencies:' , error ) ;
233+ preloadedDependencies = [ ] ;
234+ isPreloading = false ;
235+ return [ ] ;
236+ }
237+ } ) ( ) ;
238+
239+ return preloadPromise ;
240+ } ;
241+
242+ // Export function to start preloading from plugin initialization
243+ export async function startPreloading ( { config } : { config : any } ) {
244+ const startTime = Date . now ( ) ;
245+ console . log ( '[Qwik DevTools] 🚀 Initiating dependency preload (background)...' ) ;
246+
247+ // Start preloading in background, don't wait for it
248+ preloadDependencies ( config ) . then ( ( ) => {
249+ const duration = ( ( Date . now ( ) - startTime ) / 1000 ) . toFixed ( 2 ) ;
250+ console . log ( `[Qwik DevTools] ⚡ Preload completed in ${ duration } s` ) ;
251+ } ) . catch ( ( err ) => {
252+ console . error ( '[Qwik DevTools] ✗ Preload failed:' , err ) ;
253+ } ) ;
254+
255+ // Return immediately, don't block
256+ return Promise . resolve ( ) ;
257+ }
258+
41259export function getNpmFunctions ( { config } : ServerContext ) {
42260 return {
43261 async getQwikPackages ( ) : Promise < NpmInfo > {
@@ -57,6 +275,24 @@ export function getNpmFunctions({ config }: ServerContext) {
57275 }
58276 } ,
59277
278+ async getAllDependencies ( ) : Promise < any [ ] > {
279+ // Return preloaded data immediately if available
280+ if ( preloadedDependencies ) {
281+ console . log ( '[Qwik DevTools] Returning preloaded dependencies' ) ;
282+ return preloadedDependencies ;
283+ }
284+
285+ // If preloading is in progress, wait for it
286+ if ( isPreloading && preloadPromise ) {
287+ console . log ( '[Qwik DevTools] Waiting for preload to complete...' ) ;
288+ return preloadPromise ;
289+ }
290+
291+ // If preloading hasn't started (shouldn't happen), start it now
292+ console . log ( '[Qwik DevTools] Warning: Preload not started, starting now...' ) ;
293+ return preloadDependencies ( config ) ;
294+ } ,
295+
60296 async installPackage (
61297 packageName : string ,
62298 isDev = true ,
0 commit comments