Repository: tailwindlabs/tailwindcss
File: packages/@tailwindcss-upgrade/src/utils/packages.ts (TypeScript)

Code before:
import { dirname, resolve } from 'node:path'
import { promisify } from 'node:util'
import { DefaultMap } from '../../../tailwindcss/src/utils/default-map'
import { warn } from './renderer'

const exec = promisify(execCb)

class PackageManager {
  constructor(private base: string) {}

  async exec(command: string) {
    return exec(command, { cwd: this.base })
  }

  async add(
    packages: string[],
    location: 'dependencies' | 'devDependencies',
  ): ReturnType<typeof this.exec> {
    throw new Error('Method not implemented.')
  }

  async remove(packages: string[]): ReturnType<typeof this.exec> {
    throw new Error('Method not implemented.')
  }
}

class BunPackageManager extends PackageManager {
  add(packages: string[], location: 'dependencies' | 'devDependencies') {
    let args = packages.slice()

    if (location === 'devDependencies') {
      args.unshift('--development')
    }

    return this.exec(`bun add ${args.join(' ')}`)
  }

  remove(packages: string[]) {
    return this.exec(`bun remove ${packages.join(' ')}`)
  }
}

class YarnPackageManager extends PackageManager {
  add(packages: string[], location: 'dependencies' | 'devDependencies') {
    let args = packages.slice()

    if (location === 'devDependencies') {
      args.unshift('--dev')
    }

    return this.exec(`yarn add ${args.join(' ')}`)

Review comment (suggestion):
We could return the packageManager directly, but then the API usage would look like this:
```diff
- pkg(base).add(['@tailwindcss/postcss@next'])
+ pkg(base).then(pm => pm.add(['@tailwindcss/postcss@next']))
```

Which isn't as nice to work with.

Code after:
import { dirname, resolve } from 'node:path'
import { promisify } from 'node:util'
import { DefaultMap } from '../../../tailwindcss/src/utils/default-map'
import { warn } from './renderer'

const exec = promisify(execCb)

const SAVE_DEV: Record<string, string> = {
  default: '-D',
  bun: '-d',
}

export function pkg(base: string) {
  return {
    async add(packages: string[], location: 'dependencies' | 'devDependencies' = 'dependencies') {
      let packageManager = await packageManagerForBase.get(base)
      let args = packages.slice()
      if (location === 'devDependencies') {
        args.push(SAVE_DEV[packageManager] || SAVE_DEV.default)
      }
      return exec(`${packageManager} add ${args.join(' ')}`, { cwd: base })
    },
    async remove(packages: string[]) {
      let packageManager = await packageManagerForBase.get(base)
      return exec(`${packageManager} remove ${packages.join(' ')}`, { cwd: base })
    },
  }
}

let didWarnAboutPackageManager = false
let packageManagerForBase = new DefaultMap(async (base) => {
  do {
    // 1. Check package.json for a `packageManager` field
    let packageJsonPath = resolve(base, 'package.json')
    try {
      let packageJsonContent = await fs.readFile(packageJsonPath, 'utf-8')
      let packageJson = JSON.parse(packageJsonContent)
      if (packageJson.packageManager) {
        if (packageJson.packageManager.includes('bun')) {
          return 'bun'
        }
        if (packageJson.packageManager.includes('yarn')) {
          return 'yarn'
        }
        if (packageJson.packageManager.includes('pnpm')) {
          return 'pnpm'
        }
        if (packageJson.packageManager.includes('npm')) {
          return 'npm'
        }
      }