@@ -8,7 +8,6 @@ import which from 'which'
8
8
9
9
import { parse as parseBunLockb } from '@socketregistry/hyrious__bun.lockb/index.cjs'
10
10
import { Logger } from '@socketsecurity/registry/lib/logger'
11
- import { isObjectObject } from '@socketsecurity/registry/lib/objects'
12
11
import { readPackageJson } from '@socketsecurity/registry/lib/packages'
13
12
import { naturalCompare } from '@socketsecurity/registry/lib/sorts'
14
13
import { spawn } from '@socketsecurity/registry/lib/spawn'
@@ -41,18 +40,17 @@ export const AGENTS = [BUN, NPM, PNPM, YARN_BERRY, YARN_CLASSIC, VLT] as const
41
40
export type Agent = ( typeof AGENTS ) [ number ]
42
41
export type StringKeyValueObject = { [ key : string ] : string }
43
42
44
- const binByAgent = {
45
- __proto__ : null ,
46
- [ BUN ] : BUN ,
47
- [ NPM ] : NPM ,
48
- [ PNPM ] : PNPM ,
49
- [ YARN_BERRY ] : YARN ,
50
- [ YARN_CLASSIC ] : YARN ,
51
- [ VLT ] : VLT
52
- }
43
+ const binByAgent = new Map < Agent , string > ( [
44
+ [ BUN , BUN ] ,
45
+ [ NPM , NPM ] ,
46
+ [ PNPM , PNPM ] ,
47
+ [ YARN_BERRY , YARN ] ,
48
+ [ YARN_CLASSIC , YARN ] ,
49
+ [ VLT , VLT ]
50
+ ] )
53
51
54
52
async function getAgentExecPath ( agent : Agent ) : Promise < string > {
55
- const binName = binByAgent [ agent ]
53
+ const binName = binByAgent . get ( agent ) !
56
54
return ( await which ( binName , { nothrow : true } ) ) ?? binName
57
55
}
58
56
@@ -63,6 +61,9 @@ async function getAgentVersion(
63
61
let result
64
62
try {
65
63
result =
64
+ // Coerce version output into a valid semver version by passing it through
65
+ // semver.coerce which strips leading v's, carets (^), comparators (<,<=,>,>=,=),
66
+ // and tildes (~).
66
67
semver . coerce (
67
68
// All package managers support the "--version" flag.
68
69
( await spawn ( agentExecPath , [ '--version' ] , { cwd } ) ) . stdout
@@ -97,7 +98,7 @@ type ReadLockFile =
97
98
| ( ( lockPath : string ) => Promise < string | undefined > )
98
99
| ( ( lockPath : string , agentExecPath : string ) => Promise < string | undefined > )
99
100
100
- const readLockFileByAgent : Record < Agent , ReadLockFile > = ( ( ) => {
101
+ const readLockFileByAgent : Map < Agent , ReadLockFile > = ( ( ) => {
101
102
function wrapReader < T extends ( ...args : any [ ] ) => Promise < any > > (
102
103
reader : T
103
104
) : ( ...args : Parameters < T > ) => Promise < Awaited < ReturnType < T > > | undefined > {
@@ -115,32 +116,35 @@ const readLockFileByAgent: Record<Agent, ReadLockFile> = (() => {
115
116
async ( lockPath : string ) => await readFileUtf8 ( lockPath )
116
117
)
117
118
118
- return {
119
- [ BUN ] : wrapReader ( async ( lockPath : string , agentExecPath : string ) => {
120
- const ext = path . extname ( lockPath )
121
- if ( ext === LOCK_EXT ) {
122
- return await defaultReader ( lockPath )
123
- }
124
- if ( ext === BINARY_LOCK_EXT ) {
125
- const lockBuffer = await binaryReader ( lockPath )
126
- if ( lockBuffer ) {
127
- try {
128
- return parseBunLockb ( lockBuffer )
129
- } catch { }
119
+ return new Map ( [
120
+ [
121
+ BUN ,
122
+ wrapReader ( async ( lockPath : string , agentExecPath : string ) => {
123
+ const ext = path . extname ( lockPath )
124
+ if ( ext === LOCK_EXT ) {
125
+ return await defaultReader ( lockPath )
130
126
}
131
- // To print a Yarn lockfile to your console without writing it to disk
132
- // use `bun bun.lockb`.
133
- // https://bun.sh/guides/install/yarnlock
134
- return ( await spawn ( agentExecPath , [ lockPath ] ) ) . stdout . trim ( )
135
- }
136
- return undefined
137
- } ) ,
138
- [ NPM ] : defaultReader ,
139
- [ PNPM ] : defaultReader ,
140
- [ VLT ] : defaultReader ,
141
- [ YARN_BERRY ] : defaultReader ,
142
- [ YARN_CLASSIC ] : defaultReader
143
- }
127
+ if ( ext === BINARY_LOCK_EXT ) {
128
+ const lockBuffer = await binaryReader ( lockPath )
129
+ if ( lockBuffer ) {
130
+ try {
131
+ return parseBunLockb ( lockBuffer )
132
+ } catch { }
133
+ }
134
+ // To print a Yarn lockfile to your console without writing it to disk
135
+ // use `bun bun.lockb`.
136
+ // https://bun.sh/guides/install/yarnlock
137
+ return ( await spawn ( agentExecPath , [ lockPath ] ) ) . stdout . trim ( )
138
+ }
139
+ return undefined
140
+ } )
141
+ ] ,
142
+ [ NPM , defaultReader ] ,
143
+ [ PNPM , defaultReader ] ,
144
+ [ VLT , defaultReader ] ,
145
+ [ YARN_BERRY , defaultReader ] ,
146
+ [ YARN_CLASSIC , defaultReader ]
147
+ ] )
144
148
} ) ( )
145
149
146
150
export type DetectOptions = {
@@ -151,17 +155,17 @@ export type DetectOptions = {
151
155
type EnvBase = {
152
156
agent : Agent
153
157
agentExecPath : string
158
+ agentSupported : boolean
154
159
features : {
155
160
// Fixed by https://github.com/npm/cli/pull/8089.
156
161
// Landed in npm v11.2.0.
157
162
npmBuggyOverrides : boolean
158
163
}
159
- minimumNodeVersion : string
160
164
npmExecPath : string
161
165
pkgSupported : boolean
162
- targets : {
163
- browser : boolean
164
- node : boolean
166
+ pkgRequirements : {
167
+ agent : string
168
+ node : string
165
169
}
166
170
}
167
171
@@ -221,13 +225,14 @@ export async function detectPackageEnvironment({
221
225
let agent : Agent | undefined
222
226
let agentVersion : SemVer | undefined
223
227
if ( pkgManager ) {
228
+ // A valid "packageManager" field value is "<package manager name>@<version>".
229
+ // https://nodejs.org/api/packages.html#packagemanager
224
230
const atSignIndex = pkgManager . lastIndexOf ( '@' )
225
231
if ( atSignIndex !== - 1 ) {
226
232
const name = pkgManager . slice ( 0 , atSignIndex ) as Agent
227
233
const version = pkgManager . slice ( atSignIndex + 1 )
228
234
if ( version && AGENTS . includes ( name ) ) {
229
235
agent = name
230
- agentVersion = semver . coerce ( version ) ?? undefined
231
236
}
232
237
}
233
238
}
@@ -244,7 +249,6 @@ export async function detectPackageEnvironment({
244
249
onUnknown ?.( pkgManager )
245
250
}
246
251
const agentExecPath = await getAgentExecPath ( agent )
247
-
248
252
const npmExecPath =
249
253
agent === NPM ? agentExecPath : await getAgentExecPath ( NPM )
250
254
if ( agentVersion === undefined ) {
@@ -253,23 +257,31 @@ export async function detectPackageEnvironment({
253
257
if ( agent === YARN_CLASSIC && ( agentVersion ?. major ?? 0 ) > 1 ) {
254
258
agent = YARN_BERRY
255
259
}
256
- const targets = {
257
- browser : false ,
258
- node : true
259
- }
260
- let lockSrc : string | undefined
261
260
// Lazily access constants.maintainedNodeVersions.
262
- let minimumNodeVersion = constants . maintainedNodeVersions . last
261
+ const { maintainedNodeVersions } = constants
262
+ // Lazily access constants.minimumVersionByAgent.
263
+ const minSupportedAgentVersion = constants . minimumVersionByAgent . get ( agent ) !
264
+ const minSupportedNodeVersion = maintainedNodeVersions . last
265
+ let lockSrc : string | undefined
266
+ let pkgMinAgentVersion = minSupportedAgentVersion
267
+ let pkgMinNodeVersion = minSupportedNodeVersion
263
268
if ( pkgJson ) {
264
- const browserField = pkgJson . browser
265
- if ( isNonEmptyString ( browserField ) || isObjectObject ( browserField ) ) {
266
- targets . browser = true
267
- }
269
+ const agentRange = pkgJson . engines ?. [ agent ]
268
270
const nodeRange = pkgJson . engines ?. [ 'node' ]
271
+ if ( isNonEmptyString ( agentRange ) ) {
272
+ // Roughly check agent range as semver.coerce will strip leading
273
+ // v's, carets (^), comparators (<,<=,>,>=,=), and tildes (~).
274
+ const coerced = semver . coerce ( agentRange )
275
+ if ( coerced && semver . lt ( coerced , pkgMinAgentVersion ) ) {
276
+ pkgMinAgentVersion = coerced . version
277
+ }
278
+ }
269
279
if ( isNonEmptyString ( nodeRange ) ) {
280
+ // Roughly check Node range as semver.coerce will strip leading
281
+ // v's, carets (^), comparators (<,<=,>,>=,=), and tildes (~).
270
282
const coerced = semver . coerce ( nodeRange )
271
- if ( coerced && semver . lt ( coerced , minimumNodeVersion ) ) {
272
- minimumNodeVersion = coerced . version
283
+ if ( coerced && semver . lt ( coerced , pkgMinNodeVersion ) ) {
284
+ pkgMinNodeVersion = coerced . version
273
285
}
274
286
}
275
287
const browserslistQuery = pkgJson [ 'browserslist' ] as string [ ] | undefined
@@ -280,50 +292,56 @@ export async function detectPackageEnvironment({
280
292
const browserslistNodeTargets = browserslistTargets
281
293
. filter ( v => v . startsWith ( 'node ' ) )
282
294
. map ( v => v . slice ( 5 /*'node '.length*/ ) )
283
- if ( ! targets . browser && browserslistTargets . length ) {
284
- targets . browser =
285
- browserslistTargets . length !== browserslistNodeTargets . length
286
- }
287
295
if ( browserslistNodeTargets . length ) {
288
296
const coerced = semver . coerce ( browserslistNodeTargets [ 0 ] )
289
- if ( coerced && semver . lt ( coerced , minimumNodeVersion ) ) {
290
- minimumNodeVersion = coerced . version
297
+ if ( coerced && semver . lt ( coerced , pkgMinNodeVersion ) ) {
298
+ pkgMinNodeVersion = coerced . version
291
299
}
292
300
}
293
301
}
294
- // Lazily access constants.maintainedNodeVersions.
295
- targets . node = constants . maintainedNodeVersions . some ( v =>
296
- semver . satisfies ( v , `>=${ minimumNodeVersion } ` )
297
- )
298
302
lockSrc =
299
303
typeof lockPath === 'string'
300
- ? await readLockFileByAgent [ agent ] ( lockPath , agentExecPath )
304
+ ? await readLockFileByAgent . get ( agent ) ! ( lockPath , agentExecPath )
301
305
: undefined
302
306
} else {
303
307
lockName = undefined
304
308
lockPath = undefined
305
309
}
306
- const pkgSupported = targets . browser || targets . node
310
+ // Does system agent version meet our minimum supported agent version?
311
+ const agentSupported =
312
+ ! ! agentVersion &&
313
+ semver . satisfies ( agentVersion , `>=${ minSupportedAgentVersion } ` )
314
+
315
+ // Does our minimum supported agent version meet the package's requirements?
316
+ // Does our supported Node versions meet the package's requirements?
317
+ const pkgSupported =
318
+ semver . satisfies ( minSupportedAgentVersion , `>=${ pkgMinAgentVersion } ` ) &&
319
+ maintainedNodeVersions . some ( v =>
320
+ semver . satisfies ( v , `>=${ pkgMinNodeVersion } ` )
321
+ )
322
+
307
323
const npmBuggyOverrides =
308
324
agent === NPM &&
309
325
! ! agentVersion &&
310
326
semver . lt ( agentVersion , NPM_BUGGY_OVERRIDES_PATCHED_VERSION )
327
+
311
328
return {
312
329
agent,
313
330
agentExecPath,
331
+ agentSupported,
314
332
agentVersion,
333
+ features : { npmBuggyOverrides } ,
315
334
lockName,
316
335
lockPath,
317
336
lockSrc,
318
- minimumNodeVersion,
319
337
npmExecPath,
320
338
pkgJson : editablePkgJson ,
321
339
pkgPath,
322
340
pkgSupported,
323
- features : {
324
- npmBuggyOverrides
325
- } ,
326
- targets
341
+ pkgRequirements : {
342
+ agent : pkgMinAgentVersion ,
343
+ node : pkgMinNodeVersion
344
+ }
327
345
}
328
346
}
329
347
0 commit comments