Skip to content

Commit ba53a52

Browse files
authored
feat: support devEngines field (#50)
* feat: support `devEngines` field * test: add test
1 parent 466d7a7 commit ba53a52

File tree

14 files changed

+169
-12
lines changed

14 files changed

+169
-12
lines changed

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,13 @@ import { getUserAgent } from 'package-manager-detector/detect'
3838

3939
## Customize Detection Strategy
4040

41-
By default, the `detect` API searches through the current directory for lock files, and if none exists, the `package.json` `packageManager` field. If both strategies couldn't detect the package manager, it'll crawl upwards to the parent directory and repeat the detection process until it reaches the root directory.
41+
By default, the `detect` API searches through the current directory for lock files, and if none exists, it looks for the `packageManager` field in `package.json`. If that also doesn't exist, it will check the `devEngines.packageManager` field in `package.json`. If all strategies couldn't detect the package manager, it'll crawl upwards to the parent directory and repeat the detection process until it reaches the root directory.
4242

4343
The strategies can be configured through `detect`'s `strategies` option with the following accepted strategies:
4444

4545
- `'lockfile'`: Look up for lock files.
4646
- `'packageManager-field'`: Look up for the `packageManager` field in package.json.
47+
- `'devEngines-field'`: Look up for the `devEngines.packageManager` field in package.json.
4748
- `'install-metadata'`: Look up for installation metadata added by package managers.
4849

4950
The order of the strategies can also be changed to prioritize one strategy over another. For example, if you prefer to detect the package manager used for installation:
@@ -52,7 +53,7 @@ The order of the strategies can also be changed to prioritize one strategy over
5253
import { detect } from 'package-manager-detector/detect'
5354

5455
const pm = await detect({
55-
strategies: ['install-metadata', 'lockfile', 'packageManager-field']
56+
strategies: ['install-metadata', 'lockfile', 'packageManager-field', 'devEngines-field']
5657
})
5758
```
5859

src/detect.ts

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ async function parsePackageJson(
5555
* @returns {Promise<DetectResult | null>} The detected package manager or `null` if not found.
5656
*/
5757
export async function detect(options: DetectOptions = {}): Promise<DetectResult | null> {
58-
const { cwd, strategies = ['lockfile', 'packageManager-field'], onUnknown } = options
58+
const { cwd, strategies = ['lockfile', 'packageManager-field', 'devEngines-field'], onUnknown } = options
5959

6060
for (const directory of lookup(cwd)) {
6161
for (const strategy of strategies) {
@@ -74,7 +74,8 @@ export async function detect(options: DetectOptions = {}): Promise<DetectResult
7474
}
7575
break
7676
}
77-
case 'packageManager-field': {
77+
case 'packageManager-field':
78+
case 'devEngines-field': {
7879
// Look up for package.json
7980
const result = await parsePackageJson(path.join(directory, 'package.json'), onUnknown)
8081
if (result)
@@ -102,6 +103,21 @@ export async function detect(options: DetectOptions = {}): Promise<DetectResult
102103
return null
103104
}
104105

106+
function getNameAndVer(pkg: { packageManager?: string, devEngines?: { packageManager?: { name?: string, version?: string } } }) {
107+
const handelVer = (version: string | undefined) => version?.match(/\d+(\.\d+){0,2}/)?.[0] ?? version
108+
if (typeof pkg.packageManager === 'string') {
109+
const [name, ver] = pkg.packageManager.replace(/^\^/, '').split('@')
110+
return { name, ver: handelVer(ver) }
111+
}
112+
if (typeof pkg.devEngines?.packageManager?.name === 'string') {
113+
return {
114+
name: pkg.devEngines.packageManager.name,
115+
ver: handelVer(pkg.devEngines.packageManager.version),
116+
}
117+
}
118+
return undefined
119+
}
120+
105121
async function handlePackageManager(
106122
filepath: string,
107123
onUnknown: DetectOptions['onUnknown'],
@@ -110,16 +126,18 @@ async function handlePackageManager(
110126
try {
111127
const pkg = JSON.parse(await fs.readFile(filepath, 'utf8'))
112128
let agent: Agent | undefined
113-
if (typeof pkg.packageManager === 'string') {
114-
const [name, ver] = pkg.packageManager.replace(/^\^/, '').split('@')
129+
const nameAndVer = getNameAndVer(pkg)
130+
if (nameAndVer) {
131+
const name = nameAndVer.name as AgentName
132+
const ver = nameAndVer.ver
115133
let version = ver
116-
if (name === 'yarn' && Number.parseInt(ver) > 1) {
134+
if (name === 'yarn' && ver && Number.parseInt(ver) > 1) {
117135
agent = 'yarn@berry'
118136
// the version in packageManager isn't the actual yarn package version
119137
version = 'berry'
120138
return { name, agent, version }
121139
}
122-
else if (name === 'pnpm' && Number.parseInt(ver) < 7) {
140+
else if (name === 'pnpm' && ver && Number.parseInt(ver) < 7) {
123141
agent = 'pnpm@6'
124142
return { name, agent, version }
125143
}

src/types.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export interface ResolvedCommand {
3131
args: string[]
3232
}
3333

34-
export type DetectStrategy = 'lockfile' | 'packageManager-field' | 'install-metadata'
34+
export type DetectStrategy = 'lockfile' | 'packageManager-field' | 'devEngines-field' | 'install-metadata'
3535

3636
export interface DetectOptions {
3737
/**
@@ -46,9 +46,10 @@ export interface DetectOptions {
4646
*
4747
* - `'lockfile'`: Look up for lock files.
4848
* - `'packageManager-field'`: Look up for the `packageManager` field in package.json.
49+
* - `'devEngines-field'`: Look up for the `devEngines.packageManager` field in package.json.
4950
* - `'install-metadata'`: Look up for installation metadata added by package managers.
5051
*
51-
* @default ['lockfile', 'packageManager-field']
52+
* @default ['lockfile', 'packageManager-field', 'devEngines-field']
5253
*/
5354
strategies?: DetectStrategy[]
5455
/**

test/__snapshots__/detect.spec.ts.snap

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,71 @@
11
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
22

3+
exports[`dev-engines > bun 1`] = `
4+
{
5+
"agent": "bun",
6+
"name": "bun",
7+
"version": "0",
8+
}
9+
`;
10+
11+
exports[`dev-engines > deno 1`] = `
12+
{
13+
"agent": "deno",
14+
"name": "deno",
15+
"version": "2",
16+
}
17+
`;
18+
19+
exports[`dev-engines > npm 1`] = `
20+
{
21+
"agent": "npm",
22+
"name": "npm",
23+
"version": "7",
24+
}
25+
`;
26+
27+
exports[`dev-engines > pnpm 1`] = `
28+
{
29+
"agent": "pnpm",
30+
"name": "pnpm",
31+
"version": "8",
32+
}
33+
`;
34+
35+
exports[`dev-engines > pnpm@6 1`] = `
36+
{
37+
"agent": "pnpm@6",
38+
"name": "pnpm",
39+
"version": "6",
40+
}
41+
`;
42+
43+
exports[`dev-engines > pnpm-version-range 1`] = `
44+
{
45+
"agent": "pnpm",
46+
"name": "pnpm",
47+
"version": "8.0.0",
48+
}
49+
`;
50+
51+
exports[`dev-engines > unknown 1`] = `null`;
52+
53+
exports[`dev-engines > yarn 1`] = `
54+
{
55+
"agent": "yarn",
56+
"name": "yarn",
57+
"version": "1",
58+
}
59+
`;
60+
61+
exports[`dev-engines > yarn@berry 1`] = `
62+
{
63+
"agent": "yarn@berry",
64+
"name": "yarn",
65+
"version": "berry",
66+
}
67+
`;
68+
369
exports[`install-metadata > bun 1`] = `
470
{
571
"agent": "bun",

test/detect.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,15 +29,15 @@ afterAll(() => {
2929
vi.resetAllMocks()
3030
})
3131

32-
const fixtures = ['lockfile', 'packager', 'install-metadata']
32+
const fixtures = ['lockfile', 'packager', 'dev-engines', 'install-metadata']
3333

3434
fixtures.forEach(fixture => describe(fixture, () => {
3535
const fixtureDirs = getFixtureDirs(fixture)
3636

3737
fixtureDirs.forEach((dir) => {
3838
let options: DetectOptions | undefined
3939
if (fixture === 'install-metadata') {
40-
options = { strategies: ['install-metadata', 'lockfile', 'packageManager-field'] }
40+
options = { strategies: ['install-metadata', 'lockfile', 'packageManager-field', 'devEngines-field'] }
4141
}
4242
it(dir, detectTest(fixture, dir, options))
4343
})
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"devEngines": {
3+
"packageManager": {
4+
"name": "bun",
5+
"version": "0"
6+
}
7+
}
8+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"devEngines": {
3+
"packageManager": {
4+
"name": "deno",
5+
"version": "2"
6+
}
7+
}
8+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"devEngines": {
3+
"packageManager": {
4+
"name": "npm",
5+
"version": "7"
6+
}
7+
}
8+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"devEngines": {
3+
"packageManager": {
4+
"name": "pnpm",
5+
"version": "^8.0.0"
6+
}
7+
}
8+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"devEngines": {
3+
"packageManager": {
4+
"name": "pnpm",
5+
"version": "8"
6+
}
7+
}
8+
}

0 commit comments

Comments
 (0)