@@ -17,6 +17,10 @@ export type PkgManagerFields = {
1717 installCommand : string
1818 /** The package managers run command prefix */
1919 runCommand : string
20+ /** The package managers command prefix for running a command in a locally installed package */
21+ localPackageCommand : string
22+ /** The package managers command prefix(s) for running a command in a non-installed package. This is sometimes the same as `localPackageCommand` */
23+ remotePackageCommand : string [ ]
2024 /** The lock files a package manager is using */
2125 lockFiles : string [ ]
2226 /** Environment variable that can be used to force the usage of a package manager even though there is no lock file or a different lock file */
@@ -34,30 +38,73 @@ export const AVAILABLE_PACKAGE_MANAGERS: Record<PkgManager, PkgManagerFields> =
3438 name : PkgManager . YARN ,
3539 installCommand : 'yarn install' ,
3640 runCommand : 'yarn run' ,
41+ localPackageCommand : 'yarn' ,
42+ remotePackageCommand : [ 'yarn' , 'dlx' ] ,
3743 lockFiles : [ 'yarn.lock' ] ,
3844 forceEnvironment : 'NETLIFY_USE_YARN' ,
3945 } ,
4046 [ PkgManager . PNPM ] : {
4147 name : PkgManager . PNPM ,
4248 installCommand : 'pnpm install' ,
4349 runCommand : 'pnpm run' ,
50+ localPackageCommand : 'pnpm' ,
51+ remotePackageCommand : [ 'pnpm' , 'dlx' ] ,
4452 lockFiles : [ 'pnpm-lock.yaml' ] ,
4553 forceEnvironment : 'NETLIFY_USE_PNPM' ,
4654 } ,
4755 [ PkgManager . NPM ] : {
4856 name : PkgManager . NPM ,
4957 installCommand : 'npm install' ,
5058 runCommand : 'npm run' ,
59+ localPackageCommand : 'npx' ,
60+ remotePackageCommand : [ 'npx' ] ,
5161 lockFiles : [ 'package-lock.json' ] ,
5262 } ,
5363 [ PkgManager . BUN ] : {
5464 name : PkgManager . BUN ,
5565 installCommand : 'bun install' ,
5666 runCommand : 'bun run' ,
67+ localPackageCommand : 'bunx' ,
68+ remotePackageCommand : [ 'bunx' ] ,
5769 lockFiles : [ 'bun.lockb' , 'bun.lock' ] ,
5870 } ,
5971}
6072
73+ /**
74+ * The environment variable `npm_config_user_agent` can be used to
75+ * guess the package manager that was used to execute wrangler.
76+ * It's imperfect (just like regular user agent sniffing!)
77+ * but the package managers we support all set this property:
78+ *
79+ * - [npm](https://github.com/npm/cli/blob/1415b4bdeeaabb6e0ba12b6b1b0cc56502bd64ab/lib/utils/config/definitions.js#L1945-L1979)
80+ * - [pnpm](https://github.com/pnpm/pnpm/blob/cd4f9341e966eb8b411462b48ff0c0612e0a51a7/packages/plugin-commands-script-runners/src/makeEnv.ts#L14)
81+ * - [yarn](https://yarnpkg.com/advanced/lifecycle-scripts#environment-variables)
82+ * - [bun](https://github.com/oven-sh/bun/blob/550522e99b303d8172b7b16c5750d458cb056434/src/Global.zig#L205)
83+ */
84+ export function sniffUserAgent ( ) : PkgManager | undefined {
85+ const userAgent = process . env . npm_config_user_agent
86+ if ( userAgent === undefined ) {
87+ return undefined
88+ }
89+
90+ if ( userAgent . includes ( 'yarn' ) ) {
91+ return PkgManager . YARN
92+ }
93+
94+ if ( userAgent . includes ( 'pnpm' ) ) {
95+ return PkgManager . PNPM
96+ }
97+
98+ if ( userAgent . includes ( 'bun' ) ) {
99+ return PkgManager . BUN
100+ }
101+
102+ // npm should come last as it is included in the user agent strings of other package managers
103+ if ( userAgent . includes ( 'npm' ) ) {
104+ return PkgManager . NPM
105+ }
106+ }
107+
61108/**
62109 * generate a map out of key is lock file and value the package manager
63110 * this is to reduce the complexity in loops
@@ -74,6 +121,8 @@ const lockFileMap = Object.values(AVAILABLE_PACKAGE_MANAGERS).reduce(
74121 * 3. a lock file that is present in this directory or up in the tree for workspaces
75122 */
76123export const detectPackageManager = async ( project : Project ) : Promise < PkgManagerFields | null > => {
124+ const sniffedPkgManager = sniffUserAgent ( )
125+
77126 try {
78127 const pkgPaths = await project . fs . findUpMultiple ( 'package.json' , {
79128 cwd : project . baseDirectory ,
@@ -82,7 +131,7 @@ export const detectPackageManager = async (project: Project): Promise<PkgManager
82131
83132 // if there is no package json than there is no package manager to detect
84133 if ( ! pkgPaths . length ) {
85- return null
134+ return sniffedPkgManager ? AVAILABLE_PACKAGE_MANAGERS [ sniffedPkgManager ] : null
86135 }
87136
88137 for ( const pkgPath of pkgPaths ) {
@@ -122,7 +171,10 @@ export const detectPackageManager = async (project: Project): Promise<PkgManager
122171 } catch ( error ) {
123172 project . report ( error )
124173 }
125- // always default to npm
126- // TODO: add some reporting here to log that we fall backed
174+ if ( sniffedPkgManager ) {
175+ return AVAILABLE_PACKAGE_MANAGERS [ sniffedPkgManager ]
176+ }
177+
178+ // TODO: add some reporting here to log that we fall backe to NPM
127179 return AVAILABLE_PACKAGE_MANAGERS [ PkgManager . NPM ]
128180}
0 commit comments