Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add preliminary support for Bun #490

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
19 changes: 16 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,13 @@ yarn 2+ have native support for patching dependencies via
[`yarn patch`](https://yarnpkg.com/cli/patch). You do not need to use
patch-package on these projects.

### bun

bun add patch-package

You can use `--dev` if you don't need to run bun in production, e.g. if you're
making a web frontend.

### pnpm

pnpm has native support for patching dependencies via
Expand Down Expand Up @@ -152,9 +159,15 @@ team.

- `--use-yarn`

By default, patch-package checks whether you use npm or yarn based on which
lockfile you have. If you have both, it uses npm by default. Set this option
to override that default and always use yarn.
By default, patch-package checks whether you use npm, yarn or bun based on
which lockfile you have. If you have multiple lockfiles, it uses npm by
default (in cases where npm is not available, it will resort to yarn). Set
this option to override that default and always use yarn.

- `--use-bun`

Similar to --use-yarn, but for bun. If both --use-yarn and --use-bun are
specified, --use-yarn takes precedence.

- `--exclude <regexp>`

Expand Down
87 changes: 69 additions & 18 deletions src/detectPackageManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import chalk from "chalk"
import process from "process"
import findWorkspaceRoot from "find-yarn-workspace-root"

export type PackageManager = "yarn" | "npm" | "npm-shrinkwrap"
export type PackageManager = "yarn" | "npm" | "npm-shrinkwrap" | "bun"

function printNoYarnLockfileError() {
console.log(`
Expand All @@ -14,12 +14,20 @@ ${chalk.red.bold("**ERROR**")} ${chalk.red(
`)
}

function printNoBunLockfileError() {
console.log(`
${chalk.red.bold("**ERROR**")} ${chalk.red(
`The --use-bun option was specified but there is no bun.lockb file`,
)}
`)
}

function printNoLockfilesError() {
console.log(`
${chalk.red.bold("**ERROR**")} ${chalk.red(
`No package-lock.json, npm-shrinkwrap.json, or yarn.lock file.
`No package-lock.json, npm-shrinkwrap.json, yarn.lock, or bun.lockb file.

You must use either npm@>=5, yarn, or npm-shrinkwrap to manage this project's
You must use either npm@>=5, yarn, npm-shrinkwrap, or bun to manage this project's
dependencies.`,
)}
`)
Expand All @@ -29,14 +37,40 @@ function printSelectingDefaultMessage() {
console.info(
`${chalk.bold(
"patch-package",
)}: you have both yarn.lock and package-lock.json
)}: you have multiple lockfiles, e.g. yarn.lock and package-lock.json
Defaulting to using ${chalk.bold("npm")}
You can override this setting by passing --use-yarn or deleting
package-lock.json if you don't need it
You can override this setting by passing --use-yarn, --use-bun, or
deleting the conflicting lockfile if you don't need it
`,
)
}

function printSelectingDefaultYarnMessage() {
console.info(
`${chalk.bold(
"patch-package",
)}: you have both yarn.lock and bun.lockb lockfiles
Defaulting to using ${chalk.bold("yarn")}
You can override this setting by passing --use-bun, or
deleting yarn.lock if you don't need it
`,
)
}

function checkForYarnOverride(overridePackageManager: PackageManager | null) {
if (overridePackageManager === "yarn") {
printNoYarnLockfileError()
process.exit(1)
}
}

function checkForBunOverride(overridePackageManager: PackageManager | null) {
if (overridePackageManager === "bun") {
printNoBunLockfileError()
process.exit(1)
}
}

export const detectPackageManager = (
appRootPath: string,
overridePackageManager: PackageManager | null,
Expand All @@ -47,23 +81,40 @@ export const detectPackageManager = (
const shrinkWrapExists = fs.existsSync(
join(appRootPath, "npm-shrinkwrap.json"),
)
const yarnLockExists = fs.existsSync(join(appRootPath, "yarn.lock"))
if ((packageLockExists || shrinkWrapExists) && yarnLockExists) {
const yarnLockExists = fs.existsSync(
join(findWorkspaceRoot() ?? appRootPath, "yarn.lock"),
)
// Bun workspaces seem to work the same as yarn workspaces - https://bun.sh/docs/install/workspaces
const bunLockbExists = fs.existsSync(
join(findWorkspaceRoot() ?? appRootPath, "bun.lockb"),
)
if (
[
packageLockExists || shrinkWrapExists,
yarnLockExists,
bunLockbExists,
].filter(Boolean).length > 1
) {
if (overridePackageManager) {
return overridePackageManager
} else {
printSelectingDefaultMessage()
return shrinkWrapExists ? "npm-shrinkwrap" : "npm"
}
} else if (packageLockExists || shrinkWrapExists) {
if (overridePackageManager === "yarn") {
printNoYarnLockfileError()
process.exit(1)
} else {
return shrinkWrapExists ? "npm-shrinkwrap" : "npm"
if (!packageLockExists && !shrinkWrapExists) {
// The only case where we don't want to default to npm is when we have both yarn and bun lockfiles.
printSelectingDefaultYarnMessage()
return "yarn"
}
} else if (yarnLockExists || findWorkspaceRoot()) {
printSelectingDefaultMessage()
return shrinkWrapExists ? "npm-shrinkwrap" : "npm"
} else if (packageLockExists || shrinkWrapExists) {
checkForYarnOverride(overridePackageManager)
checkForBunOverride(overridePackageManager)
return shrinkWrapExists ? "npm-shrinkwrap" : "npm"
} else if (yarnLockExists) {
checkForBunOverride(overridePackageManager)
return "yarn"
} else if (bunLockbExists) {
checkForYarnOverride(overridePackageManager)
return "bun"
} else {
printNoLockfilesError()
process.exit(1)
Expand Down
29 changes: 21 additions & 8 deletions src/getPackageResolution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import yaml from "yaml"
import findWorkspaceRoot from "find-yarn-workspace-root"
import { getPackageVersion } from "./getPackageVersion"
import { coerceSemVer } from "./coerceSemVer"
import { parseBunLockfile } from "./parseBunLockfile"

export function getPackageResolution({
packageDetails,
Expand All @@ -17,24 +18,32 @@ export function getPackageResolution({
packageManager: PackageManager
appPath: string
}) {
if (packageManager === "yarn") {
let lockFilePath = "yarn.lock"
if (packageManager === "yarn" || packageManager === "bun") {
const isBun = packageManager === "bun"
const lockFileName = isBun ? "bun.lockb" : "yarn.lock"
let lockFilePath = lockFileName
if (!existsSync(lockFilePath)) {
const workspaceRoot = findWorkspaceRoot()
if (!workspaceRoot) {
throw new Error("Can't find yarn.lock file")
throw new Error(`Can't find ${lockFileName} file`)
}
lockFilePath = join(workspaceRoot, "yarn.lock")
lockFilePath = join(workspaceRoot, lockFilePath)
}
if (!existsSync(lockFilePath)) {
throw new Error("Can't find yarn.lock file")
throw new Error(`Can't find ${lockFileName} file`)
}
const lockFileString = readFileSync(lockFilePath).toString()
const lockFileString = isBun
? parseBunLockfile(lockFilePath)
: readFileSync(lockFilePath).toString()
let appLockFile
if (lockFileString.includes("yarn lockfile v1")) {
const parsedYarnLockFile = parseYarnLockFile(lockFileString)
if (parsedYarnLockFile.type !== "success") {
throw new Error("Could not parse yarn v1 lock file")
throw new Error(
`Could not parse yarn v1 lock file ${
isBun ? "- was originally a bun.lockb file" : ""
}`,
)
} else {
appLockFile = parsedYarnLockFile.object
}
Expand All @@ -43,7 +52,11 @@ export function getPackageResolution({
appLockFile = yaml.parse(lockFileString)
} catch (e) {
console.log(e)
throw new Error("Could not parse yarn v2 lock file")
throw new Error(
`Could not parse yarn v2 lock file ${
isBun ? "- was originally a bun.lockb file (should not happen)" : ""
}`,
)
}
}

Expand Down
14 changes: 10 additions & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ if (argv.version || argv.v) {
)
const packageManager = detectPackageManager(
appPath,
argv["use-yarn"] ? "yarn" : null,
argv["use-yarn"] ? "yarn" : argv["use-bun"] ? "bun" : null,
)
const createIssue = argv["create-issue"]
packageNames.forEach((packagePathSpecifier: string) => {
Expand Down Expand Up @@ -198,9 +198,15 @@ Usage:

${chalk.bold("--use-yarn")}

By default, patch-package checks whether you use npm or yarn based on
which lockfile you have. If you have both, it uses npm by default.
Set this option to override that default and always use yarn.
By default, patch-package checks whether you use npm, yarn or bun based on
which lockfile you have. If you have multiple lockfiles, it uses npm by
default (in cases where npm is not available, it will resort to yarn). Set
this option to override that default and always use yarn.

${chalk.bold("--use-bun")}

Similar to --use-yarn, but for bun. If both --use-yarn and --use-bun are
specified, --use-yarn takes precedence.

${chalk.bold("--exclude <regexp>")}

Expand Down
15 changes: 15 additions & 0 deletions src/parseBunLockfile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { spawnSync } from "child_process"

// Adapted from https://github.com/oven-sh/bun/blob/ffe4f561a3af53b9f5a41c182de55d7199b5d692/packages/bun-vscode/src/features/lockfile.ts#L39,
// rewritten to use spawnSync instead of spawn.
export function parseBunLockfile(lockFilePath: string): string {
const process = spawnSync("bun", [lockFilePath], {
stdio: ["ignore", "pipe", "pipe"],
})
if (process.status !== 0) {
throw new Error(
`Bun exited with code: ${process.status}\n${process.stderr.toString()}`,
)
}
return process.stdout.toString()
}