1717 * required, same pattern as socket-sdk-js's `check-new-deps` hook.
1818 */
1919
20- import { readdirSync , readFileSync } from 'node:fs'
20+ import { promises as fs } from 'node:fs'
2121import path from 'node:path'
2222
2323import { SOCKET_PUBLIC_API_TOKEN } from '@socketsecurity/lib/constants/socket'
@@ -54,53 +54,68 @@ const sdk = new SocketSdk(SOCKET_PUBLIC_API_TOKEN, { timeout: API_TIMEOUT_MS })
5454// --- specifier extraction ---
5555
5656/**
57- * Parse `npm:` specifiers out of every `.ts` file in `dir`. Deduped
58- * by `name@version` string. Only direct deps — transitives come from
59- * the pacote walk.
57+ * Scan every file in `dir` matching `suffix`, read each in parallel,
58+ * and collect matches of `pattern` as `NpmDep`s tagged `source`. The
59+ * shared pipeline for both extractors below so all dedupe/parallelism
60+ * logic lives in one place. Reads are `Promise.allSettled` so a single
61+ * unreadable file doesn't abort the scan — failures collect into a
62+ * composite error and throw after the successful results are drained.
6063 */
61- function extractNpmDepsFromDir ( dir : string ) : NpmDep [ ] {
64+ async function extractDepsFromDir (
65+ dir : string ,
66+ suffix : string ,
67+ pattern : RegExp ,
68+ source : NpmDep [ 'source' ] ,
69+ ) : Promise < NpmDep [ ] > {
70+ const entries = await fs . readdir ( dir )
71+ const matching = entries . filter ( e => e . endsWith ( suffix ) )
72+ const reads = await Promise . allSettled (
73+ matching . map ( entry =>
74+ fs
75+ . readFile ( path . join ( dir , entry ) , 'utf8' )
76+ . then ( text => ( { entry, text } ) ) ,
77+ ) ,
78+ )
79+ const failures : string [ ] = [ ]
6280 const seen = new Map < string , NpmDep > ( )
63- for ( const entry of readdirSync ( dir ) ) {
64- if ( ! entry . endsWith ( '.ts' ) ) {
81+ for ( const r of reads ) {
82+ if ( r . status === 'rejected' ) {
83+ failures . push ( String ( ( r . reason as Error ) ?. message ?? r . reason ) )
6584 continue
6685 }
67- const source = readFileSync ( path . join ( dir , entry ) , 'utf8' )
68- for ( const m of source . matchAll ( NPM_SPECIFIER_RE ) ) {
86+ for ( const m of r . value . text . matchAll ( pattern ) ) {
6987 const spec = m . groups ! [ 'spec' ] !
7088 const atIndex = spec . lastIndexOf ( '@' )
7189 const name = spec . slice ( 0 , atIndex )
7290 const version = spec . slice ( atIndex + 1 )
7391 const key = `${ name } @${ version } `
7492 if ( ! seen . has ( key ) ) {
75- seen . set ( key , { name, version, source : 'direct' } )
93+ seen . set ( key , { name, version, source } )
7694 }
7795 }
7896 }
97+ if ( failures . length > 0 ) {
98+ throw new Error (
99+ `audit-deps: failed to read ${ failures . length } file(s) in ${ dir } :\n - ${ failures . join ( '\n - ' ) } ` ,
100+ )
101+ }
79102 return [ ...seen . values ( ) ]
80103}
81104
105+ /**
106+ * Parse `npm:` specifiers out of every `.ts` file in `dir`. Deduped
107+ * by `name@version` string. Only direct deps — transitives come from
108+ * the pacote walk.
109+ */
110+ async function extractNpmDepsFromDir ( dir : string ) : Promise < NpmDep [ ] > {
111+ return extractDepsFromDir ( dir , '.ts' , NPM_SPECIFIER_RE , 'direct' )
112+ }
113+
82114/**
83115 * Extract unpkg script deps from generated HTML files.
84116 */
85- function extractCdnDepsFromDir ( dir : string ) : NpmDep [ ] {
86- const seen = new Map < string , NpmDep > ( )
87- for ( const entry of readdirSync ( dir ) ) {
88- if ( ! entry . endsWith ( '.html' ) ) {
89- continue
90- }
91- const source = readFileSync ( path . join ( dir , entry ) , 'utf8' )
92- for ( const m of source . matchAll ( UNPKG_SCRIPT_RE ) ) {
93- const spec = m . groups ! [ 'spec' ] !
94- const atIndex = spec . lastIndexOf ( '@' )
95- const name = spec . slice ( 0 , atIndex )
96- const version = spec . slice ( atIndex + 1 )
97- const key = `${ name } @${ version } `
98- if ( ! seen . has ( key ) ) {
99- seen . set ( key , { name, version, source : 'cdn' } )
100- }
101- }
102- }
103- return [ ...seen . values ( ) ]
117+ async function extractCdnDepsFromDir ( dir : string ) : Promise < NpmDep [ ] > {
118+ return extractDepsFromDir ( dir , '.html' , UNPKG_SCRIPT_RE , 'cdn' )
104119}
105120
106121// --- transitive closure via pacote ---
@@ -215,7 +230,7 @@ async function checkMalwareBatched(
215230 */
216231export async function auditValDeps ( repoRoot : string ) : Promise < void > {
217232 const valDir = path . join ( repoRoot , 'val' )
218- const directs = extractNpmDepsFromDir ( valDir )
233+ const directs = await extractNpmDepsFromDir ( valDir )
219234 if ( directs . length === 0 ) {
220235 console . log ( '[audit-deps] no npm specifiers found in val/' )
221236 return
@@ -237,7 +252,7 @@ export async function auditValDeps(repoRoot: string): Promise<void> {
237252 * bundles are preflight-built, their deps don't ship separately.
238253 */
239254export async function auditCdnScripts ( walkthroughDir : string ) : Promise < void > {
240- const cdnDeps = extractCdnDepsFromDir ( walkthroughDir )
255+ const cdnDeps = await extractCdnDepsFromDir ( walkthroughDir )
241256 if ( cdnDeps . length === 0 ) {
242257 console . log ( '[audit-deps] no unpkg CDN scripts in walkthrough HTML' )
243258 return
0 commit comments