Skip to content

Commit 20c2698

Browse files
committed
feat: pkg-manager.factory improvements
1 parent 72a0cea commit 20c2698

File tree

10 files changed

+146
-28
lines changed

10 files changed

+146
-28
lines changed

package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
"devDependencies": {
5555
"@antfu/ni": "^0.21.12",
5656
"@types/node": "^20.10.7",
57+
"@types/which": "^3.0.3",
5758
"bumpp": "^9.2.1",
5859
"eslint": "^8.56.0",
5960
"esno": "^4.0.0",
@@ -80,8 +81,10 @@
8081
"@nestjs/common": "^10.3.3",
8182
"@nestjs/core": "^10.3.3",
8283
"@nestjs/platform-express": "^10.3.3",
84+
"chalk": "^5.3.0",
8385
"eslint-kit": "^10.19.0",
8486
"execa": "^8.0.1",
85-
"nest-commander": "^3.12.5"
87+
"nest-commander": "^3.12.5",
88+
"which": "^4.0.0"
8689
}
8790
}

pnpm-lock.yaml

Lines changed: 13 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/commands/init.command.ts

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,7 @@ export class InitCommand extends CommandRunner {
1818
}
1919

2020
public async run(param: string[], options: AnyRecord) {
21-
console.log({
22-
param,
23-
options,
24-
manager: this.manager
25-
})
26-
27-
// console.log(this.manager.agent)
2821
const workspaces = await this.manager.getWorkspaces()
29-
3022
console.log(workspaces)
3123
}
3224
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { parseJson } from '@neodx/fs'
2+
import { hasOwn, isObject, isTruthy } from '@neodx/std'
3+
import { execaCommand as $ } from 'execa'
4+
import { AbstractPackageManager } from '@/pkg-manager/managers/abstract.pkg-manager'
5+
import { PackageManager } from '@/pkg-manager/pkg-manager.consts'
6+
import type { WorkspaceProject } from '@/pkg-manager/pkg-manager.types'
7+
8+
export class YarnBerryPackageManager extends AbstractPackageManager {
9+
constructor() {
10+
super(PackageManager.YARN_BERRY)
11+
}
12+
13+
public async getWorkspaces(): Promise<WorkspaceProject[]> {
14+
const cwd = process.cwd()
15+
16+
const output = await $('yarn workspaces list --json', {
17+
cwd
18+
})
19+
20+
const serializedLines = output.stdout.trim().split('\n')
21+
22+
const workspaces = await Promise.all(
23+
serializedLines.map(async (serializedMeta) => {
24+
const isWorkspaceProject = (val: unknown): val is WorkspaceProject =>
25+
isObject(project) && hasOwn(project, 'location')
26+
27+
const project = await parseJson(serializedMeta)
28+
29+
if (isWorkspaceProject(project)) {
30+
return project
31+
}
32+
33+
return null
34+
})
35+
)
36+
37+
return workspaces.filter(isTruthy)
38+
}
39+
}

src/pkg-manager/managers/yarn.pkg-manager.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,6 @@ export class YarnPackageManager extends AbstractPackageManager {
2727
cwd
2828
})
2929

30-
console.log({
31-
output: output.stdout
32-
})
33-
3430
const stdout = output.stdout
3531

3632
const jsonStartIndex = stdout.indexOf('{')

src/pkg-manager/pkg-manager.consts.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,29 +10,35 @@ export enum PackageManager {
1010
NPM = 'npm',
1111
YARN = 'yarn',
1212
PNPM = 'pnpm',
13-
BUN = 'bun'
13+
BUN = 'bun',
14+
YARN_BERRY = 'yarn@berry'
1415
}
1516

1617
interface PackageManagerMatcher {
1718
lockFile: string
19+
name: PackageManager
1820
manager: new () => AbstractPackageManager
1921
}
2022

21-
export const lockFileMatchers = [
23+
export const packageManagerMatchers = [
2224
{
2325
lockFile: 'package-lock.json',
26+
name: PackageManager.NPM,
2427
manager: NpmPackageManager
2528
},
2629
{
2730
lockFile: 'yarn.lock',
31+
name: PackageManager.YARN,
2832
manager: YarnPackageManager
2933
},
3034
{
3135
lockFile: 'pnpm-lock.yaml',
36+
name: PackageManager.PNPM,
3237
manager: PnpmPackageManager
3338
},
3439
{
3540
lockFile: 'bun.lockb',
41+
name: PackageManager.BUN,
3642
manager: BunPackageManager
3743
}
3844
] satisfies PackageManagerMatcher[]
Lines changed: 72 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,83 @@
1-
import { readdir } from '@neodx/fs'
1+
import { cmdExists } from '@antfu/ni'
2+
import { ensureFile, scan } from '@neodx/fs'
3+
import { isTypeOfString, uniq, values } from '@neodx/std'
4+
import chalk from 'chalk'
5+
import { execaCommand as $ } from 'execa'
6+
import { basename, resolve } from 'node:path'
27
import type { AbstractPackageManager } from '@/pkg-manager/managers/abstract.pkg-manager'
3-
import { lockFileMatchers } from '@/pkg-manager/pkg-manager.consts'
8+
import { YarnBerryPackageManager } from '@/pkg-manager/managers/yarn-berry.pkg-manager'
9+
import {
10+
PackageManager,
11+
packageManagerMatchers
12+
} from '@/pkg-manager/pkg-manager.consts'
13+
import type { PackageJson } from '@/shared/json'
14+
import { readJson } from '@/shared/json'
15+
import { addLibraryPrefix } from '@/shared/misc'
416

517
export class PackageManagerFactory {
6-
// TODO: extend
718
public static async detect(): Promise<AbstractPackageManager> {
8-
const files = await readdir(process.cwd())
19+
let programmaticAgent: PackageManager
920

10-
const match = lockFileMatchers.find((matcher) =>
11-
files.includes(matcher.lockFile)
21+
const agents = values(PackageManager)
22+
23+
const lockFilePatterns = await scan(
24+
process.cwd(),
25+
uniq(packageManagerMatchers.map(({ lockFile }) => lockFile))
1226
)
1327

14-
if (match) {
15-
return new match.manager()
28+
const lockPath = lockFilePatterns.shift() as string
29+
30+
const packageJsonPath = lockPath
31+
? resolve(lockPath, '../package.json')
32+
: resolve(process.cwd(), 'package.json')
33+
34+
await ensureFile(lockPath)
35+
await ensureFile(packageJsonPath)
36+
37+
const pkg = (await readJson(packageJsonPath)) as PackageJson
38+
39+
if (isTypeOfString(pkg.packageManager)) {
40+
const [name, version] = pkg.packageManager.replace(/^\^/, '').split('@')
41+
const agent = name as PackageManager
42+
43+
const isYarnBerry =
44+
agent === PackageManager.YARN &&
45+
version &&
46+
Number.parseInt(version, 10) > 1
47+
48+
if (isYarnBerry) {
49+
return new YarnBerryPackageManager()
50+
}
51+
52+
if (agents.includes(agent)) {
53+
programmaticAgent = agent
54+
}
55+
}
56+
57+
const match = packageManagerMatchers.find(
58+
(matcher) =>
59+
matcher.lockFile === basename(lockPath) ||
60+
programmaticAgent === matcher.name
61+
)
62+
63+
if (!match) {
64+
throw new Error('Unable to detect a package manager.')
65+
}
66+
67+
if (!cmdExists(match.name)) {
68+
console.warn(addLibraryPrefix(`${match.name} is not installed.`))
69+
console.info(
70+
addLibraryPrefix(
71+
`Attempting to install ${chalk.cyan(match.name)} globally...`
72+
)
73+
)
74+
75+
await $(`npm i -g ${match.name}`, {
76+
stdio: 'inherit',
77+
cwd: process.cwd()
78+
})
1679
}
1780

18-
// TODO better errors
19-
throw new Error('never')
81+
return new match.manager()
2082
}
2183
}

src/shared/json.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export interface PackageJson {
99
version: string
1010
type?: 'module' | 'commonjs'
1111
dependencies?: Record<string, string>
12+
packageManager?: string
1213
peerDependencies?: Record<string, string>
1314
devDependencies?: Record<string, string>
1415
scripts?: Record<string, string>

src/shared/misc.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import chalk from 'chalk'
2+
3+
export const addLibraryPrefix = (message: string) =>
4+
`[${chalk.green('gx')}]: ${message}`

src/shared/shell.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import which from 'which'
2+
3+
export function cmdExists(cmd: string) {
4+
return which.sync(cmd, { nothrow: true }) !== null
5+
}

0 commit comments

Comments
 (0)