66import { existsSync , promises as fs } from 'node:fs'
77import os from 'node:os'
88import path from 'node:path'
9- import util from 'node:util '
9+ import { parseArgs } from '../registry/dist/lib/parse-args.js '
1010
1111import constants from './constants.mjs'
12- import ENV from '@socketsecurity /registry/lib/constants/env '
13- import WIN32 from '@socketsecurity /registry/lib/constants/win32 '
12+ import ENV from '.. /registry/dist/ lib/constants/ENV.js '
13+ import WIN32 from '.. /registry/dist/ lib/constants/WIN32.js '
1414import {
1515 readPackageJson ,
1616 resolveOriginalPackageName ,
17- } from '@socketsecurity/registry/lib/packages'
18- import { pEach } from '@socketsecurity/registry/lib/promises'
19- import { LOG_SYMBOLS , logger } from '@socketsecurity/registry/lib/logger'
17+ } from '../registry/dist/lib/packages.js'
18+ import { pEach } from '../registry/dist/lib/promises.js'
19+ import { LOG_SYMBOLS , logger } from '../registry/dist/lib/logger.js'
20+ import { pluralize } from '../registry/dist/lib/words.js'
2021
21- const { values : cliArgs } = util . parseArgs ( {
22+ const { values : cliArgs } = parseArgs ( {
2223 options : {
23- package : {
24- type : 'string' ,
25- multiple : true ,
26- } ,
2724 concurrency : {
2825 type : 'string' ,
29- // Reduce concurrency in CI to avoid memory issues, especially on Windows.
30- default : ENV . CI ? ( WIN32 ? '5' : '10' ) : '50' ,
26+ default : ENV . CI ? ( WIN32 ? '10' : '20' ) : '50' ,
3127 } ,
3228 'temp-dir' : {
3329 type : 'string' ,
3430 default : path . join ( os . tmpdir ( ) , 'npm-package-tests' ) ,
3531 } ,
32+ package : {
33+ type : 'string' ,
34+ multiple : true ,
35+ } ,
36+ 'clear-cache' : {
37+ type : 'boolean' ,
38+ default : false ,
39+ } ,
3640 } ,
41+ strict : false ,
3742} )
3843
3944const concurrency = Math . max ( 1 , parseInt ( cliArgs . concurrency , 10 ) )
40- const tempBaseDir = cliArgs [ 'temp-dir' ]
41- const MAX_PROGRESS_WIDTH = 80
42-
43- // Progress tracking for line wrapping.
44- let currentLinePosition = 0
45- let completedPackages = 0
46- let totalPackagesCount = 0
47-
48- function writeProgress ( symbol ) {
49- completedPackages += 1
50- if ( currentLinePosition >= MAX_PROGRESS_WIDTH ) {
51- process . stdout . write ( `\n(${ completedPackages } /${ totalPackagesCount } ) ` )
52- currentLinePosition = `(${ completedPackages } /${ totalPackagesCount } ) ` . length
53- }
54- process . stdout . write ( symbol )
55- currentLinePosition += 1
45+ const tempBaseDir = cliArgs . tempDir
46+
47+ function writeProgress ( ) {
48+ // Don't output progress dots, too noisy.
5649}
5750
5851async function downloadPackage ( socketPkgName ) {
@@ -62,7 +55,7 @@ async function downloadPackage(socketPkgName) {
6255 const skipTestsMap = constants . skipTestsByEcosystem
6356 const skipSet = skipTestsMap . get ( 'npm' )
6457 if ( skipSet && ( skipSet . has ( socketPkgName ) || skipSet . has ( origPkgName ) ) ) {
65- writeProgress ( LOG_SYMBOLS . warn )
58+ writeProgress ( )
6659 return {
6760 package : origPkgName ,
6861 socketPackage : socketPkgName ,
@@ -120,49 +113,158 @@ async function downloadPackage(socketPkgName) {
120113 }
121114}
122115
123- void ( async ( ) => {
116+ async function main ( ) {
124117 const packages = cliArgs . package ?. length
125118 ? cliArgs . package
126119 : constants . npmPackageNames
127120
128- logger . log (
129- `Processing package info for ${ packages . length } packages with concurrency ${ concurrency } ...` ,
130- )
131- logger . log ( `Temp directory: ${ tempBaseDir } ` )
132- logger . log (
133- `Progress: ${ LOG_SYMBOLS . success } = success, ${ LOG_SYMBOLS . fail } = failed, ${ LOG_SYMBOLS . warn } = skipped\n` ,
134- )
135-
136- // Initialize progress tracking.
137- totalPackagesCount = packages . length
138- completedPackages = 0
139- currentLinePosition = 0
140- process . stdout . write ( '(0/' + totalPackagesCount + ') ' )
141- currentLinePosition = ( '(0/' + totalPackagesCount + ') ' ) . length
142-
143121 // Ensure base temp directory exists.
144122 await fs . mkdir ( tempBaseDir , { recursive : true } )
145123
146- const results = [ ]
124+ // Check if download results already exist and are fresh.
125+ const resultsFile = path . join ( tempBaseDir , 'download-results.json' )
126+ const clearCache = cliArgs . clearCache
127+
128+ // Clear cache if requested.
129+ if ( clearCache && existsSync ( resultsFile ) ) {
130+ await fs . unlink ( resultsFile )
131+ logger . log ( '🗑️ Cleared download cache' )
132+ }
133+
134+ // Load existing cache if available.
135+ let cachedResults = [ ]
136+ if ( ! clearCache && existsSync ( resultsFile ) ) {
137+ try {
138+ cachedResults = JSON . parse ( await fs . readFile ( resultsFile , 'utf8' ) )
139+ } catch {
140+ logger . warn ( 'Could not read cache, starting fresh...' )
141+ }
142+ }
143+
144+ // Determine which packages need processing.
145+ let packagesToProcess = packages
146+ let usedCache = false
147+
148+ if ( cachedResults . length > 0 ) {
149+ if ( cliArgs . package ?. length ) {
150+ // For specific packages, check which ones are already cached.
151+ const cachedPackageNames = new Set (
152+ cachedResults . map ( r => r . socketPackage || r . package ) ,
153+ )
154+ const missingPackages = packages . filter (
155+ pkg => ! cachedPackageNames . has ( pkg ) ,
156+ )
157+
158+ if ( missingPackages . length === 0 ) {
159+ // All requested packages are cached.
160+ const relevantResults = cachedResults . filter (
161+ r =>
162+ packages . includes ( r . socketPackage ) || packages . includes ( r . package ) ,
163+ )
164+ logger . log (
165+ `💾 Using cached download results (${ relevantResults . length } ${ pluralize ( 'package' , relevantResults . length ) } )` ,
166+ )
167+ await fs . writeFile (
168+ resultsFile ,
169+ JSON . stringify ( relevantResults , null , 2 ) ,
170+ )
171+ process . exitCode = 0
172+ return
173+ } else if ( missingPackages . length < packages . length ) {
174+ // Some packages are cached, only process missing ones.
175+ packagesToProcess = missingPackages
176+ usedCache = true
177+ logger . log (
178+ `💾 Found ${ packages . length - missingPackages . length } cached, processing ${ missingPackages . length } new ${ pluralize ( 'package' , missingPackages . length ) } ...` ,
179+ )
180+ }
181+ } else {
182+ // For full run, check if cache has all packages.
183+ const cachedPackageNames = cachedResults
184+ . map ( r => r . socketPackage || r . package )
185+ . sort ( )
186+ const requestedPackages = packages . slice ( ) . sort ( )
187+ if (
188+ JSON . stringify ( cachedPackageNames ) === JSON . stringify ( requestedPackages )
189+ ) {
190+ logger . log (
191+ `💾 Using cached download results (${ cachedResults . length } ${ pluralize ( 'package' , cachedResults . length ) } )` ,
192+ )
193+ process . exitCode = 0
194+ return
195+ }
196+ }
197+ }
198+
199+ logger . log (
200+ `Processing ${ packagesToProcess . length } ${ pluralize ( 'package' , packagesToProcess . length ) } ...` ,
201+ )
202+
203+ // Start with cached results if doing incremental update.
204+ const results =
205+ usedCache && cliArgs . package ?. length
206+ ? cachedResults . filter (
207+ r =>
208+ ! packagesToProcess . includes ( r . socketPackage ) &&
209+ ! packagesToProcess . includes ( r . package ) ,
210+ )
211+ : [ ]
147212
148213 await pEach (
149- packages ,
214+ packagesToProcess ,
150215 async pkgName => {
151216 const result = await downloadPackage ( pkgName )
152217 results . push ( result )
153218 } ,
154219 { concurrency } ,
155220 )
156221
157- // Add newline after progress indicators.
158- process . stdout . write ( '\n' )
159-
160222 const failed = results . filter ( r => ! r . downloaded && r . reason !== 'Skipped' )
223+ const skipped = results . filter ( r => r . reason === 'Skipped' )
224+ const succeeded = results . filter ( r => r . downloaded )
225+
226+ // Summary.
227+ logger . log (
228+ `✅ ${ clearCache ? 'Redownloaded' : 'Processed' } : ${ succeeded . length } ` ,
229+ )
230+ if ( skipped . length > 0 ) {
231+ logger . log ( `⏭️ Skipped: ${ skipped . length } ` )
232+ }
233+ if ( failed . length > 0 ) {
234+ logger . fail ( `Failed: ${ failed . length } ` )
235+ }
236+
237+ // Merge new results with existing cache for full dataset.
238+ const finalResults =
239+ cliArgs . package ?. length && usedCache
240+ ? [
241+ ...cachedResults . filter (
242+ r =>
243+ ! packages . includes ( r . socketPackage ) &&
244+ ! packages . includes ( r . package ) ,
245+ ) ,
246+ ...results ,
247+ ]
248+ : results
161249
162250 // Write results to file for the install phase.
163- const resultsFile = path . join ( tempBaseDir , 'download-results.json' )
164- await fs . writeFile ( resultsFile , JSON . stringify ( results , null , 2 ) )
251+ await fs . writeFile (
252+ resultsFile ,
253+ JSON . stringify (
254+ cliArgs . package ?. length
255+ ? finalResults . filter (
256+ r =>
257+ packages . includes ( r . socketPackage ) ||
258+ packages . includes ( r . package ) ,
259+ )
260+ : finalResults ,
261+ null ,
262+ 2 ,
263+ ) ,
264+ )
165265
166266 // Set exit code for process termination.
167267 process . exitCode = failed . length ? 1 : 0
168- } ) ( )
268+ }
269+
270+ main ( ) . catch ( console . error )
0 commit comments