Skip to content

Commit 5b9adad

Browse files
committed
Include user agent sniffing
1 parent a107de7 commit 5b9adad

File tree

6 files changed

+98
-5
lines changed

6 files changed

+98
-5
lines changed

packages/build-info/src/node/__snapshots__/get-build-info.test.ts.snap

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,15 @@ exports[`should retrieve the build info for providing a rootDir 1`] = `
3434
"packageManager": {
3535
"forceEnvironment": "NETLIFY_USE_PNPM",
3636
"installCommand": "pnpm install",
37+
"localPackageCommand": "pnpm",
3738
"lockFiles": [
3839
"pnpm-lock.yaml",
3940
],
4041
"name": "pnpm",
42+
"remotePackageCommand": [
43+
"pnpm",
44+
"dlx",
45+
],
4146
"runCommand": "pnpm run",
4247
"version": SemVer {
4348
"build": [],
@@ -167,10 +172,15 @@ exports[`should retrieve the build info for providing a rootDir and a nested pro
167172
"packageManager": {
168173
"forceEnvironment": "NETLIFY_USE_PNPM",
169174
"installCommand": "pnpm install",
175+
"localPackageCommand": "pnpm",
170176
"lockFiles": [
171177
"pnpm-lock.yaml",
172178
],
173179
"name": "pnpm",
180+
"remotePackageCommand": [
181+
"pnpm",
182+
"dlx",
183+
],
174184
"runCommand": "pnpm run",
175185
"version": SemVer {
176186
"build": [],
@@ -243,10 +253,15 @@ exports[`should retrieve the build info for providing a rootDir and the same pro
243253
"packageManager": {
244254
"forceEnvironment": "NETLIFY_USE_PNPM",
245255
"installCommand": "pnpm install",
256+
"localPackageCommand": "pnpm",
246257
"lockFiles": [
247258
"pnpm-lock.yaml",
248259
],
249260
"name": "pnpm",
261+
"remotePackageCommand": [
262+
"pnpm",
263+
"dlx",
264+
],
250265
"runCommand": "pnpm run",
251266
"version": SemVer {
252267
"build": [],

packages/build-info/src/node/get-build-info.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,14 @@ test('should not crash on invalid projects', async (ctx) => {
5353
expect(packageManager).toMatchInlineSnapshot(`
5454
{
5555
"installCommand": "npm install",
56+
"localPackageCommand": "npx",
5657
"lockFiles": [
5758
"package-lock.json",
5859
],
5960
"name": "npm",
61+
"remotePackageCommand": [
62+
"npx",
63+
],
6064
"runCommand": "npm run",
6165
}
6266
`)

packages/build-info/src/package-managers/detect-package-manager.test.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { join } from 'path'
22

3-
import { beforeEach, describe, expect, test } from 'vitest'
3+
import { beforeEach, describe, expect, test, vi } from 'vitest'
44

55
import { mockFileSystem } from '../../tests/mock-file-system.js'
66
import { NodeFS } from '../node/file-system.js'
@@ -80,6 +80,19 @@ test('should fallback to npm if just a package.json is present there', async ({
8080
expect(pkgManager?.name).toBe('npm')
8181
})
8282

83+
describe.each([{ pm: 'npm' }, { pm: 'yarn' }, { pm: 'pnpm' }, { pm: 'bun' }])(
84+
'should fallback to user agent if present',
85+
({ pm }) => {
86+
test(`fallback ${pm}`, async ({ fs }) => {
87+
vi.stubEnv('npm_config_user_agent', pm)
88+
const cwd = mockFileSystem({})
89+
const project = new Project(fs, cwd)
90+
const pkgManager = await detectPackageManager(project)
91+
expect(pkgManager?.name).toBe(pm)
92+
})
93+
},
94+
)
95+
8396
test('should use yarn if there is a yarn.lock in the root', async ({ fs }) => {
8497
const cwd = mockFileSystem({
8598
'package.json': '{}',

packages/build-info/src/package-managers/detect-package-manager.ts

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -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
*/
76123
export 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
}

packages/build-info/tests/bin.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,11 @@ test('CLI does not print js-workspaces if given a project without it', async (ct
5555
"name": "pnpm",
5656
"installCommand": "pnpm install",
5757
"runCommand": "pnpm run",
58+
"localPackageCommand": "pnpm",
59+
"remotePackageCommand": [
60+
"pnpm",
61+
"dlx"
62+
],
5863
"lockFiles": [
5964
"pnpm-lock.yaml"
6065
],

packages/build-info/tests/test-setup.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import fs from 'fs'
22

3-
import { afterEach, vi } from 'vitest'
3+
import { afterEach, beforeEach, vi } from 'vitest'
44

55
vi.mock('fs', async () => {
66
const unionFs: any = (await import('unionfs')).default
@@ -13,6 +13,10 @@ vi.mock('fs', async () => {
1313
return { default: united, ...united }
1414
})
1515

16+
beforeEach(() => {
17+
vi.stubEnv('npm_config_user_agent', undefined)
18+
})
19+
1620
// cleanup after each test as a fallback if someone forgot to call it
1721
afterEach(async ({ cleanup }) => {
1822
if (typeof cleanup === 'function') {

0 commit comments

Comments
 (0)