Skip to content

Commit 400d5cb

Browse files
committed
Track supported node and agent versions
1 parent 7668f5d commit 400d5cb

File tree

3 files changed

+108
-75
lines changed

3 files changed

+108
-75
lines changed

src/commands/optimize/add-overrides.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -127,9 +127,13 @@ export async function addOverrides(
127127
const depAliasMap = new Map<string, string>()
128128
const depEntries = getDependencyEntries(pkgJson)
129129

130-
const nodeRange = `>=${pkgEnvDetails.minimumNodeVersion}`
131130
const manifestEntries = manifestNpmOverrides.filter(({ 1: data }) =>
132-
semver.satisfies(semver.coerce(data.engines.node)!, nodeRange)
131+
semver.satisfies(
132+
// Roughly check Node range as semver.coerce will strip leading
133+
// v's, carets (^), comparators (<,<=,>,>=,=), and tildes (~).
134+
semver.coerce(data.engines.node)!,
135+
`>=${pkgEnvDetails.pkgRequirements.node}`
136+
)
133137
)
134138

135139
// Chunk package names to process them in parallel 3 at a time.
@@ -150,11 +154,15 @@ export async function addOverrides(
150154
: undefined
151155
if (origSpec) {
152156
let thisSpec = origSpec
153-
// Add package aliases for direct dependencies to avoid npm EOVERRIDE errors.
157+
// Add package aliases for direct dependencies to avoid npm EOVERRIDE
158+
// errors...
154159
// https://docs.npmjs.com/cli/v8/using-npm/package-spec#aliases
155160
if (
161+
// ...if the spec doesn't start with a valid Socket override.
156162
!(
157163
thisSpec.startsWith(sockOverridePrefix) &&
164+
// Check the validity of the spec by passing it through npa and
165+
// seeing if it will coerce to a version.
158166
semver.coerce(npa(thisSpec).rawSpec)?.version
159167
)
160168
) {
@@ -210,6 +218,11 @@ export async function addOverrides(
210218
if (
211219
pin &&
212220
semver.major(
221+
// Check the validity of the spec by passing it through npa
222+
// and seeing if it will coerce to a version. semver.coerce
223+
// will strip leading v's, carets (^), comparators (<,<=,>,>=,=),
224+
// and tildes (~). If not coerced to a valid version then
225+
// default to the manifest entry version.
213226
semver.coerce(npa(thisSpec).rawSpec)?.version ?? version
214227
) !== major
215228
) {

src/constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import process from 'node:process'
66
import registryConstants from '@socketsecurity/registry/lib/constants'
77
import { envAsBoolean } from '@socketsecurity/registry/lib/env'
88

9+
import type { Agent } from './utils/package-environment'
910
import type { Remap } from '@socketsecurity/registry/lib/objects'
1011

1112
const {
@@ -117,6 +118,7 @@ type Constants = Remap<
117118
readonly distShadowNpmBinPath: string
118119
readonly distShadowNpmInjectPath: string
119120
readonly homePath: string
121+
readonly minimumVersionByAgent: Map<Agent, string>
120122
readonly nmBinPath: string
121123
readonly nodeHardenFlags: string[]
122124
readonly rootBinPath: string

src/utils/package-environment.ts

Lines changed: 90 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import which from 'which'
88

99
import { parse as parseBunLockb } from '@socketregistry/hyrious__bun.lockb/index.cjs'
1010
import { Logger } from '@socketsecurity/registry/lib/logger'
11-
import { isObjectObject } from '@socketsecurity/registry/lib/objects'
1211
import { readPackageJson } from '@socketsecurity/registry/lib/packages'
1312
import { naturalCompare } from '@socketsecurity/registry/lib/sorts'
1413
import { spawn } from '@socketsecurity/registry/lib/spawn'
@@ -41,18 +40,17 @@ export const AGENTS = [BUN, NPM, PNPM, YARN_BERRY, YARN_CLASSIC, VLT] as const
4140
export type Agent = (typeof AGENTS)[number]
4241
export type StringKeyValueObject = { [key: string]: string }
4342

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+
])
5351

5452
async function getAgentExecPath(agent: Agent): Promise<string> {
55-
const binName = binByAgent[agent]
53+
const binName = binByAgent.get(agent)!
5654
return (await which(binName, { nothrow: true })) ?? binName
5755
}
5856

@@ -63,6 +61,9 @@ async function getAgentVersion(
6361
let result
6462
try {
6563
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 (~).
6667
semver.coerce(
6768
// All package managers support the "--version" flag.
6869
(await spawn(agentExecPath, ['--version'], { cwd })).stdout
@@ -97,7 +98,7 @@ type ReadLockFile =
9798
| ((lockPath: string) => Promise<string | undefined>)
9899
| ((lockPath: string, agentExecPath: string) => Promise<string | undefined>)
99100

100-
const readLockFileByAgent: Record<Agent, ReadLockFile> = (() => {
101+
const readLockFileByAgent: Map<Agent, ReadLockFile> = (() => {
101102
function wrapReader<T extends (...args: any[]) => Promise<any>>(
102103
reader: T
103104
): (...args: Parameters<T>) => Promise<Awaited<ReturnType<T>> | undefined> {
@@ -115,32 +116,35 @@ const readLockFileByAgent: Record<Agent, ReadLockFile> = (() => {
115116
async (lockPath: string) => await readFileUtf8(lockPath)
116117
)
117118

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)
130126
}
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+
])
144148
})()
145149

146150
export type DetectOptions = {
@@ -151,17 +155,17 @@ export type DetectOptions = {
151155
type EnvBase = {
152156
agent: Agent
153157
agentExecPath: string
158+
agentSupported: boolean
154159
features: {
155160
// Fixed by https://github.com/npm/cli/pull/8089.
156161
// Landed in npm v11.2.0.
157162
npmBuggyOverrides: boolean
158163
}
159-
minimumNodeVersion: string
160164
npmExecPath: string
161165
pkgSupported: boolean
162-
targets: {
163-
browser: boolean
164-
node: boolean
166+
pkgRequirements: {
167+
agent: string
168+
node: string
165169
}
166170
}
167171

@@ -221,13 +225,14 @@ export async function detectPackageEnvironment({
221225
let agent: Agent | undefined
222226
let agentVersion: SemVer | undefined
223227
if (pkgManager) {
228+
// A valid "packageManager" field value is "<package manager name>@<version>".
229+
// https://nodejs.org/api/packages.html#packagemanager
224230
const atSignIndex = pkgManager.lastIndexOf('@')
225231
if (atSignIndex !== -1) {
226232
const name = pkgManager.slice(0, atSignIndex) as Agent
227233
const version = pkgManager.slice(atSignIndex + 1)
228234
if (version && AGENTS.includes(name)) {
229235
agent = name
230-
agentVersion = semver.coerce(version) ?? undefined
231236
}
232237
}
233238
}
@@ -244,7 +249,6 @@ export async function detectPackageEnvironment({
244249
onUnknown?.(pkgManager)
245250
}
246251
const agentExecPath = await getAgentExecPath(agent)
247-
248252
const npmExecPath =
249253
agent === NPM ? agentExecPath : await getAgentExecPath(NPM)
250254
if (agentVersion === undefined) {
@@ -253,23 +257,31 @@ export async function detectPackageEnvironment({
253257
if (agent === YARN_CLASSIC && (agentVersion?.major ?? 0) > 1) {
254258
agent = YARN_BERRY
255259
}
256-
const targets = {
257-
browser: false,
258-
node: true
259-
}
260-
let lockSrc: string | undefined
261260
// 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
263268
if (pkgJson) {
264-
const browserField = pkgJson.browser
265-
if (isNonEmptyString(browserField) || isObjectObject(browserField)) {
266-
targets.browser = true
267-
}
269+
const agentRange = pkgJson.engines?.[agent]
268270
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+
}
269279
if (isNonEmptyString(nodeRange)) {
280+
// Roughly check Node range as semver.coerce will strip leading
281+
// v's, carets (^), comparators (<,<=,>,>=,=), and tildes (~).
270282
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
273285
}
274286
}
275287
const browserslistQuery = pkgJson['browserslist'] as string[] | undefined
@@ -280,50 +292,56 @@ export async function detectPackageEnvironment({
280292
const browserslistNodeTargets = browserslistTargets
281293
.filter(v => v.startsWith('node '))
282294
.map(v => v.slice(5 /*'node '.length*/))
283-
if (!targets.browser && browserslistTargets.length) {
284-
targets.browser =
285-
browserslistTargets.length !== browserslistNodeTargets.length
286-
}
287295
if (browserslistNodeTargets.length) {
288296
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
291299
}
292300
}
293301
}
294-
// Lazily access constants.maintainedNodeVersions.
295-
targets.node = constants.maintainedNodeVersions.some(v =>
296-
semver.satisfies(v, `>=${minimumNodeVersion}`)
297-
)
298302
lockSrc =
299303
typeof lockPath === 'string'
300-
? await readLockFileByAgent[agent](lockPath, agentExecPath)
304+
? await readLockFileByAgent.get(agent)!(lockPath, agentExecPath)
301305
: undefined
302306
} else {
303307
lockName = undefined
304308
lockPath = undefined
305309
}
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+
307323
const npmBuggyOverrides =
308324
agent === NPM &&
309325
!!agentVersion &&
310326
semver.lt(agentVersion, NPM_BUGGY_OVERRIDES_PATCHED_VERSION)
327+
311328
return {
312329
agent,
313330
agentExecPath,
331+
agentSupported,
314332
agentVersion,
333+
features: { npmBuggyOverrides },
315334
lockName,
316335
lockPath,
317336
lockSrc,
318-
minimumNodeVersion,
319337
npmExecPath,
320338
pkgJson: editablePkgJson,
321339
pkgPath,
322340
pkgSupported,
323-
features: {
324-
npmBuggyOverrides
325-
},
326-
targets
341+
pkgRequirements: {
342+
agent: pkgMinAgentVersion,
343+
node: pkgMinNodeVersion
344+
}
327345
}
328346
}
329347

0 commit comments

Comments
 (0)