@@ -8,7 +8,7 @@ import which from 'which'
8
8
9
9
import { existsSync , findUp , readFileBinary , readFileUtf8 } from './fs'
10
10
import { parseJSONObject } from './json'
11
- import { getOwn , isObjectObject } from './objects'
11
+ import { isObjectObject } from './objects'
12
12
import { isNonEmptyString } from './strings'
13
13
14
14
import type { Content as PackageJsonContent } from '@npmcli/package-json'
@@ -40,9 +40,50 @@ export const LOCKS: Record<string, string> = {
40
40
'node_modules/.package-lock.json' : 'npm'
41
41
}
42
42
43
- const MAINTAINED_NODE_VERSIONS = browserslist ( 'maintained node versions' )
44
- // Trim value, e.g. 'node 22.5.0' to '22.5.0'
45
- . map ( v => v . slice ( 5 ) )
43
+ const numericCollator = new Intl . Collator ( undefined , {
44
+ numeric : true ,
45
+ sensitivity : 'base'
46
+ } )
47
+ const { compare : alphaNumericComparator } = numericCollator
48
+
49
+ const maintainedNodeVersions = ( ( ) => {
50
+ // Under the hood browserlist uses the node-releases package which is out of date:
51
+ // https://github.com/chicoxyzzy/node-releases/issues/37
52
+ // So we maintain a manual version list for now.
53
+ // https://nodejs.org/en/about/previous-releases#looking-for-latest-release-of-a-version-branch
54
+ const manualPrev = '18.20.4'
55
+ const manualCurr = '20.18.0'
56
+ const manualNext = '22.10.0'
57
+
58
+ const query = browserslist ( 'maintained node versions' )
59
+ // Trim value, e.g. 'node 22.5.0' to '22.5.0'.
60
+ . map ( s => s . slice ( 5 /*'node '.length*/ ) )
61
+ // Sort ascending.
62
+ . toSorted ( alphaNumericComparator )
63
+ const queryPrev = query . at ( 0 ) ?? manualPrev
64
+ const queryCurr = query . at ( 1 ) ?? manualCurr
65
+ const queryNext = query . at ( 2 ) ?? manualNext
66
+
67
+ const previous = semver . maxSatisfying (
68
+ [ queryPrev , manualPrev ] ,
69
+ `^${ semver . major ( queryPrev ) } `
70
+ ) !
71
+ const current = semver . maxSatisfying (
72
+ [ queryCurr , manualCurr ] ,
73
+ `^${ semver . major ( queryCurr ) } `
74
+ ) !
75
+ const next = semver . maxSatisfying (
76
+ [ queryNext , manualNext ] ,
77
+ `^${ semver . major ( queryNext ) } `
78
+ ) !
79
+ return Object . freeze (
80
+ Object . assign ( [ previous , current , next ] , {
81
+ previous,
82
+ current,
83
+ next
84
+ } )
85
+ )
86
+ } ) ( )
46
87
47
88
export type DetectOptions = {
48
89
cwd ?: string
@@ -57,6 +98,7 @@ export type DetectResult = Readonly<{
57
98
isWorkspace : boolean
58
99
lockPath : string | undefined
59
100
lockSrc : string | undefined
101
+ minimumNodeVersion : string
60
102
pkgJson : PackageJsonContent | undefined
61
103
pkgJsonPath : string | undefined
62
104
pkgJsonStr : string | undefined
@@ -69,25 +111,25 @@ export type DetectResult = Readonly<{
69
111
70
112
type ReadLockFile = (
71
113
lockPath : string ,
72
- agentExecPath ? : string
114
+ agentExecPath : string
73
115
) => Promise < string | undefined >
74
116
75
117
const readLockFileByAgent : Record < AgentPlusBun , ReadLockFile > = ( ( ) => {
76
118
const wrapReader =
77
119
(
78
120
reader : (
79
121
lockPath : string ,
80
- agentExecPath ? : string
122
+ agentExecPath : string
81
123
) => Promise < string | undefined >
82
124
) : ReadLockFile =>
83
- async ( lockPath : string , agentExecPath ? : string ) => {
125
+ async ( lockPath : string , agentExecPath : string ) => {
84
126
try {
85
127
return await reader ( lockPath , agentExecPath )
86
128
} catch { }
87
129
return undefined
88
130
}
89
131
return {
90
- bun : wrapReader ( async ( lockPath : string , agentExecPath ? : string ) => {
132
+ bun : wrapReader ( async ( lockPath : string , agentExecPath : string ) => {
91
133
let lockBuffer : Buffer | undefined
92
134
try {
93
135
lockBuffer = < Buffer > await readFileBinary ( lockPath )
@@ -99,7 +141,7 @@ const readLockFileByAgent: Record<AgentPlusBun, ReadLockFile> = (() => {
99
141
} catch { }
100
142
// To print a Yarn lockfile to your console without writing it to disk use `bun bun.lockb`.
101
143
// https://bun.sh/guides/install/yarnlock
102
- return ( await spawn ( agentExecPath ?? 'bun' , [ lockPath ] ) ) . stdout
144
+ return ( await spawn ( agentExecPath , [ lockPath ] ) ) . stdout
103
145
} ) ,
104
146
npm : wrapReader ( async ( lockPath : string ) => await readFileUtf8 ( lockPath ) ) ,
105
147
pnpm : wrapReader ( async ( lockPath : string ) => await readFileUtf8 ( lockPath ) ) ,
@@ -126,8 +168,8 @@ export async function detect({
126
168
? ( parseJSONObject ( pkgJsonStr ) ?? undefined )
127
169
: undefined
128
170
const pkgManager = < string | undefined > (
129
- ( isNonEmptyString ( getOwn ( pkgJson , 'packageManager' ) )
130
- ? pkgJson ?. [ 'packageManager' ]
171
+ ( isNonEmptyString ( pkgJson ?. [ 'packageManager' ] )
172
+ ? pkgJson [ 'packageManager' ]
131
173
: undefined )
132
174
)
133
175
@@ -156,60 +198,56 @@ export async function detect({
156
198
agent = 'npm'
157
199
onUnknown ?.( pkgManager )
158
200
}
159
- const agentExecPath = ( await which ( agent , { nothrow : true } ) ) ?? agent
160
201
161
- let lockSrc : string | undefined
202
+ const agentExecPath = ( await which ( agent , { nothrow : true } ) ) ?? agent
162
203
const targets = {
163
204
browser : false ,
164
205
node : true
165
206
}
166
-
207
+ let lockSrc : string | undefined
167
208
let isPrivate = false
168
209
let isWorkspace = false
210
+ let minimumNodeVersion = maintainedNodeVersions . previous
169
211
if ( pkgJson ) {
170
212
const pkgPath = path . dirname ( pkgJsonPath ! )
171
213
isPrivate = ! ! pkgJson [ 'private' ]
172
214
isWorkspace =
173
215
! ! pkgJson [ 'workspaces' ] ||
174
216
existsSync ( path . join ( pkgPath , `${ PNPM_WORKSPACE } .yaml` ) ) ||
175
217
existsSync ( path . join ( pkgPath , `${ PNPM_WORKSPACE } .yml` ) )
176
- let browser : boolean | undefined
177
- let node : boolean | undefined
178
- const browserField = getOwn ( pkgJson , 'browser' )
218
+ const browserField = pkgJson [ 'browser' ]
179
219
if ( isNonEmptyString ( browserField ) || isObjectObject ( browserField ) ) {
180
- browser = true
220
+ targets . browser = true
181
221
}
182
- const nodeRange = getOwn ( pkgJson [ 'engines' ] , 'node' )
222
+ const nodeRange = ( pkgJson as any ) [ 'engines' ] ?. [ 'node' ]
183
223
if ( isNonEmptyString ( nodeRange ) ) {
184
- node = MAINTAINED_NODE_VERSIONS . some ( v => {
185
- const coerced = semver . coerce ( nodeRange )
186
- return coerced && semver . satisfies ( coerced , `^ ${ semver . major ( v ) } ` )
187
- } )
224
+ const coerced = semver . coerce ( nodeRange )
225
+ if ( coerced ) {
226
+ minimumNodeVersion = coerced . version
227
+ }
188
228
}
189
- const browserslistQuery = getOwn ( pkgJson , 'browserslist' )
229
+ const browserslistQuery = < string [ ] | undefined > pkgJson [ 'browserslist' ]
190
230
if ( Array . isArray ( browserslistQuery ) ) {
191
231
const browserslistTargets = browserslist ( browserslistQuery )
232
+ . map ( s => s . toLowerCase ( ) )
233
+ . toSorted ( alphaNumericComparator )
192
234
const browserslistNodeTargets = browserslistTargets
193
235
. filter ( v => v . startsWith ( 'node ' ) )
194
- . map ( v => v . slice ( 5 ) )
195
- if ( browser === undefined && browserslistTargets . length ) {
196
- browser = browserslistTargets . length !== browserslistNodeTargets . length
236
+ . map ( v => v . slice ( 5 /*'node '.length*/ ) )
237
+ if ( ! targets . browser && browserslistTargets . length ) {
238
+ targets . browser =
239
+ browserslistTargets . length !== browserslistNodeTargets . length
197
240
}
198
- if ( node === undefined && browserslistNodeTargets . length ) {
199
- node = MAINTAINED_NODE_VERSIONS . some ( v =>
200
- browserslistNodeTargets . some ( t => {
201
- const coerced = semver . coerce ( t )
202
- return coerced && semver . satisfies ( coerced , `^${ semver . major ( v ) } ` )
203
- } )
204
- )
241
+ if ( browserslistNodeTargets . length ) {
242
+ const coerced = semver . coerce ( browserslistNodeTargets [ 0 ] )
243
+ if ( coerced && semver . lt ( coerced , minimumNodeVersion ) ) {
244
+ minimumNodeVersion = coerced . version
245
+ }
205
246
}
206
247
}
207
- if ( browser !== undefined ) {
208
- targets . browser = browser
209
- }
210
- if ( node !== undefined ) {
211
- targets . node = node
212
- }
248
+ targets . node = maintainedNodeVersions . some ( v =>
249
+ semver . satisfies ( v , `>=${ minimumNodeVersion } ` )
250
+ )
213
251
lockSrc =
214
252
typeof lockPath === 'string'
215
253
? await readLockFileByAgent [ agent ] ( lockPath , agentExecPath )
@@ -225,6 +263,7 @@ export async function detect({
225
263
isWorkspace,
226
264
lockPath,
227
265
lockSrc,
266
+ minimumNodeVersion,
228
267
pkgJson,
229
268
pkgJsonPath,
230
269
pkgJsonStr,
0 commit comments