11import fs from 'node:fs/promises'
22import path from 'node:path'
33
4+ import { lineage } from './walk'
5+
46export class Runnable {
57 executable : string
68 args : string [ ]
@@ -44,7 +46,8 @@ export abstract class PackageManagerDetector {
4446 abstract get name ( ) : string
4547 abstract detectUserAgent ( userAgent : string ) : boolean
4648 abstract detectRuntime ( ) : boolean
47- abstract detectLockfile ( dir : string ) : Promise < void >
49+ abstract get representativeLockfile ( ) : string | undefined
50+ abstract detectLockfile ( dir : string ) : Promise < string >
4851 abstract detectExecutable ( lookup : PathLookup ) : Promise < void >
4952 abstract installCommand ( ) : Runnable
5053 abstract execCommand ( args : string [ ] ) : Runnable
@@ -63,8 +66,12 @@ export class NpmDetector extends PackageManagerDetector implements PackageManage
6366 return false
6467 }
6568
66- async detectLockfile ( dir : string ) : Promise < void > {
67- return await accessR ( path . join ( dir , 'package-lock.json' ) )
69+ get representativeLockfile ( ) : string {
70+ return 'package-lock.json'
71+ }
72+
73+ async detectLockfile ( dir : string ) : Promise < string > {
74+ return await accessR ( path . join ( dir , this . representativeLockfile ) )
6875 }
6976
7077 async detectExecutable ( lookup : PathLookup ) : Promise < void > {
@@ -93,8 +100,12 @@ export class CNpmDetector extends PackageManagerDetector implements PackageManag
93100 return false
94101 }
95102
103+ get representativeLockfile ( ) : undefined {
104+ return
105+ }
106+
96107 // eslint-disable-next-line require-await
97- async detectLockfile ( ) : Promise < void > {
108+ async detectLockfile ( ) : Promise < string > {
98109 throw new NotDetectedError ( )
99110 }
100111
@@ -124,8 +135,12 @@ export class PNpmDetector extends PackageManagerDetector implements PackageManag
124135 return false
125136 }
126137
127- async detectLockfile ( dir : string ) : Promise < void > {
128- return await accessR ( path . join ( dir , 'pnpm-lock.yaml' ) )
138+ get representativeLockfile ( ) : string {
139+ return 'pnpm-lock.yaml'
140+ }
141+
142+ async detectLockfile ( dir : string ) : Promise < string > {
143+ return await accessR ( path . join ( dir , this . representativeLockfile ) )
129144 }
130145
131146 async detectExecutable ( lookup : PathLookup ) : Promise < void > {
@@ -154,8 +169,12 @@ export class YarnDetector extends PackageManagerDetector implements PackageManag
154169 return false
155170 }
156171
157- async detectLockfile ( dir : string ) : Promise < void > {
158- return await accessR ( path . join ( dir , 'yarn.lock' ) )
172+ get representativeLockfile ( ) : string {
173+ return 'yarn.lock'
174+ }
175+
176+ async detectLockfile ( dir : string ) : Promise < string > {
177+ return await accessR ( path . join ( dir , this . representativeLockfile ) )
159178 }
160179
161180 async detectExecutable ( lookup : PathLookup ) : Promise < void > {
@@ -184,8 +203,12 @@ export class DenoDetector extends PackageManagerDetector implements PackageManag
184203 return process . versions . deno !== undefined
185204 }
186205
187- async detectLockfile ( dir : string ) : Promise < void > {
188- return await accessR ( path . join ( dir , 'deno.lock' ) )
206+ get representativeLockfile ( ) : string {
207+ return 'deno.lock'
208+ }
209+
210+ async detectLockfile ( dir : string ) : Promise < string > {
211+ return await accessR ( path . join ( dir , this . representativeLockfile ) )
189212 }
190213
191214 async detectExecutable ( lookup : PathLookup ) : Promise < void > {
@@ -214,8 +237,12 @@ export class BunDetector extends PackageManagerDetector implements PackageManage
214237 return process . versions . bun !== undefined
215238 }
216239
217- async detectLockfile ( dir : string ) : Promise < void > {
218- return await accessR ( path . join ( dir , 'bun.lockb' ) )
240+ get representativeLockfile ( ) : string {
241+ return 'bun.lockb'
242+ }
243+
244+ async detectLockfile ( dir : string ) : Promise < string > {
245+ return await accessR ( path . join ( dir , this . representativeLockfile ) )
219246 }
220247
221248 async detectExecutable ( lookup : PathLookup ) : Promise < void > {
@@ -231,17 +258,19 @@ export class BunDetector extends PackageManagerDetector implements PackageManage
231258 }
232259}
233260
234- async function accessR ( filePath : string ) : Promise < void > {
261+ async function accessR ( filePath : string ) : Promise < string > {
235262 try {
236263 await fs . access ( filePath , fs . constants . R_OK )
264+ return filePath
237265 } catch {
238266 throw new NotDetectedError ( )
239267 }
240268}
241269
242- async function accessX ( filePath : string ) : Promise < void > {
270+ async function accessX ( filePath : string ) : Promise < string > {
243271 try {
244272 await fs . access ( filePath , fs . constants . X_OK )
273+ return filePath
245274 } catch {
246275 throw new NotDetectedError ( )
247276 }
@@ -333,13 +362,13 @@ export const knownPackageManagers: PackageManagerDetector[] = [
333362 npmDetector ,
334363]
335364
336- export interface DetectPackageManagerOptions {
365+ export interface DetectOptions {
337366 detectors ?: PackageManagerDetector [ ]
338367}
339368
340369export async function detectPackageManager (
341370 dir : string ,
342- options ?: DetectPackageManagerOptions ,
371+ options ?: DetectOptions ,
343372) : Promise < PackageManager > {
344373 const detectors = options ?. detectors ?? knownPackageManagers
345374
@@ -362,10 +391,11 @@ export async function detectPackageManager (
362391
363392 // Next, try to find a lockfile.
364393 try {
365- return await Promise . any ( detectors . map ( async detector => {
366- await detector . detectLockfile ( dir )
367- return detector
368- } ) )
394+ const { packageManager } = await detectNearestLockfile ( dir , {
395+ detectors,
396+ } )
397+
398+ return packageManager
369399 } catch {
370400 // Nothing detected.
371401 }
@@ -387,3 +417,61 @@ export async function detectPackageManager (
387417 // If all else fails, just assume npm.
388418 return npmDetector
389419}
420+
421+ export interface NearestLockFile {
422+ packageManager : PackageManager
423+ lockfile : string
424+ }
425+
426+ export class NoLockfileFoundError extends Error {
427+ searchPaths : string [ ]
428+ lockfiles : string [ ]
429+
430+ constructor ( searchPaths : string [ ] , lockfiles : string [ ] , options ?: ErrorOptions ) {
431+ const message = `Unable to detect a lockfile in any of the following paths:`
432+ + `\n\n`
433+ + `${ searchPaths . map ( searchPath => ` ${ searchPath } ` ) . join ( '\n' ) } `
434+ + `\n\n`
435+ + `Lockfiles we looked for:`
436+ + `\n\n`
437+ + `${ lockfiles . map ( lockfile => ` ${ lockfile } ` ) . join ( '\n' ) } `
438+ super ( message , options )
439+ this . name = 'NoLockfileFoundError'
440+ this . searchPaths = searchPaths
441+ this . lockfiles = lockfiles
442+ }
443+ }
444+
445+ export async function detectNearestLockfile (
446+ dir : string ,
447+ options ?: DetectOptions ,
448+ ) : Promise < NearestLockFile > {
449+ const detectors = options ?. detectors ?? knownPackageManagers
450+
451+ const searchPaths : string [ ] = [ ]
452+
453+ // Next, try to find a lockfile.
454+ for ( const searchPath of lineage ( dir ) ) {
455+ try {
456+ searchPaths . push ( searchPath )
457+
458+ // Assume that only a single kind of lockfile exists, which means the
459+ // resolve order does not matter.
460+ return await Promise . any ( detectors . map ( async detector => {
461+ const lockfile = await detector . detectLockfile ( searchPath )
462+ return {
463+ packageManager : detector ,
464+ lockfile,
465+ }
466+ } ) )
467+ } catch {
468+ // Nothing detected.
469+ }
470+ }
471+
472+ const lockfiles = detectors . reduce < string [ ] > ( ( acc , detector ) => {
473+ return acc . concat ( detector . representativeLockfile ?? [ ] )
474+ } , [ ] )
475+
476+ throw new NoLockfileFoundError ( searchPaths , lockfiles )
477+ }
0 commit comments