Skip to content

Commit 7efd05f

Browse files
committed
scripts(audit-deps): migrate fs calls to async + Promise.allSettled
Replaces readdirSync/readFileSync with fs.promises. Extracts the shared scan-and-match pipeline into extractDepsFromDir so both the npm-specifier scan and the unpkg CDN scan parallelize reads through Promise.allSettled. Failures collect into one composite error with the file-by-file details rather than aborting on the first.
1 parent 5d3da0c commit 7efd05f

1 file changed

Lines changed: 46 additions & 31 deletions

File tree

scripts/audit-deps.mts

Lines changed: 46 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
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'
2121
import path from 'node:path'
2222

2323
import { 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
*/
216231
export 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
*/
239254
export 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

Comments
 (0)