Skip to content

Commit

Permalink
[build-utils] Add zero config detection for bun package manager (#10486)
Browse files Browse the repository at this point in the history
> [!IMPORTANT]  
> This PR will only support Bun as a package manager at build time. 
> Bun will **not** work at runtime with Serverless Functions or Edge
Functions at this time.

- Depends on vercel/api#21869
- Fixes https://github.com/orgs/vercel/discussions/2021
- Closes #10244
- Related nodejs/corepack#295
- Docs https://bun.sh/docs/install
  • Loading branch information
styfle committed Sep 11, 2023
1 parent a732d30 commit 45b73c7
Show file tree
Hide file tree
Showing 9 changed files with 106 additions and 21 deletions.
5 changes: 5 additions & 0 deletions .changeset/poor-cobras-deliver.md
@@ -0,0 +1,5 @@
---
"@vercel/build-utils": minor
---

[build-utils] Add zero config detection for bun package manager
70 changes: 49 additions & 21 deletions packages/build-utils/src/fs/run-user-scripts.ts
Expand Up @@ -16,7 +16,7 @@ import { cloneEnv } from '../clone-env';
// Only allow one `runNpmInstall()` invocation to run concurrently
const runNpmInstallSema = new Sema(1);

export type CliType = 'yarn' | 'npm' | 'pnpm';
export type CliType = 'yarn' | 'npm' | 'pnpm' | 'bun';

export interface ScanParentDirsResult {
/**
Expand Down Expand Up @@ -284,26 +284,34 @@ export async function scanParentDirs(
readPackageJson && pkgJsonPath
? JSON.parse(await fs.readFile(pkgJsonPath, 'utf8'))
: undefined;
const [yarnLockPath, npmLockPath, pnpmLockPath] = await walkParentDirsMulti({
base: '/',
start: destPath,
filenames: ['yarn.lock', 'package-lock.json', 'pnpm-lock.yaml'],
});
const [yarnLockPath, npmLockPath, pnpmLockPath, bunLockPath] =
await walkParentDirsMulti({
base: '/',
start: destPath,
filenames: [
'yarn.lock',
'package-lock.json',
'pnpm-lock.yaml',
'bun.lockb',
],
});
let lockfilePath: string | undefined;
let lockfileVersion: number | undefined;
let cliType: CliType = 'yarn';

const [hasYarnLock, packageLockJson, pnpmLockYaml] = await Promise.all([
Boolean(yarnLockPath),
npmLockPath
? readConfigFile<{ lockfileVersion: number }>(npmLockPath)
: null,
pnpmLockPath
? readConfigFile<{ lockfileVersion: number }>(pnpmLockPath)
: null,
]);

// Priority order is Yarn > pnpm > npm
const [hasYarnLock, packageLockJson, pnpmLockYaml, bunLockBin] =
await Promise.all([
Boolean(yarnLockPath),
npmLockPath
? readConfigFile<{ lockfileVersion: number }>(npmLockPath)
: null,
pnpmLockPath
? readConfigFile<{ lockfileVersion: number }>(pnpmLockPath)
: null,
bunLockPath ? fs.readFile(bunLockPath, 'utf8') : null,
]);

// Priority order is Yarn > pnpm > npm > bun
if (hasYarnLock) {
cliType = 'yarn';
lockfilePath = yarnLockPath;
Expand All @@ -315,6 +323,11 @@ export async function scanParentDirs(
cliType = 'npm';
lockfilePath = npmLockPath;
lockfileVersion = packageLockJson.lockfileVersion;
} else if (bunLockBin) {
cliType = 'bun';
lockfilePath = bunLockPath;
// TODO: read "bun-lockfile-format-v0"
lockfileVersion = 0;
}

const packageJsonPath = pkgJsonPath || undefined;
Expand Down Expand Up @@ -451,6 +464,10 @@ export async function runNpmInstall(
commandArgs = args
.filter(a => a !== '--prefer-offline')
.concat(['install', '--unsafe-perm']);
} else if (cliType === 'bun') {
// @see options https://bun.sh/docs/cli/install
opts.prettyCommand = 'bun install';
commandArgs = ['install', ...args];
} else {
opts.prettyCommand = 'yarn install';
commandArgs = ['install', ...args];
Expand Down Expand Up @@ -505,6 +522,7 @@ export function getEnvForPackageManager({
const npm7 = '/node16/bin-npm7';
const pnpm7 = '/pnpm7/node_modules/.bin';
const pnpm8 = '/pnpm8/node_modules/.bin';
const bun1 = '/bun1';
const corepackEnabled = env.ENABLE_EXPERIMENTAL_COREPACK === '1';
if (cliType === 'npm') {
if (
Expand All @@ -516,7 +534,7 @@ export function getEnvForPackageManager({
) {
// Ensure that npm 7 is at the beginning of the `$PATH`
newEnv.PATH = `${npm7}${path.delimiter}${oldPath}`;
console.log('Detected `package-lock.json` generated by npm 7+...');
console.log('Detected `package-lock.json` generated by npm 7+');
}
} else if (cliType === 'pnpm') {
if (
Expand All @@ -528,7 +546,7 @@ export function getEnvForPackageManager({
// Ensure that pnpm 7 is at the beginning of the `$PATH`
newEnv.PATH = `${pnpm7}${path.delimiter}${oldPath}`;
console.log(
`Detected \`pnpm-lock.yaml\` version ${lockfileVersion} generated by pnpm 7...`
`Detected \`pnpm-lock.yaml\` version ${lockfileVersion} generated by pnpm 7`
);
} else if (
typeof lockfileVersion === 'number' &&
Expand All @@ -539,7 +557,16 @@ export function getEnvForPackageManager({
// Ensure that pnpm 8 is at the beginning of the `$PATH`
newEnv.PATH = `${pnpm8}${path.delimiter}${oldPath}`;
console.log(
`Detected \`pnpm-lock.yaml\` version ${lockfileVersion} generated by pnpm 8...`
`Detected \`pnpm-lock.yaml\` version ${lockfileVersion} generated by pnpm 8`
);
}
} else if (cliType === 'bun') {
if (!oldPath.includes(bun1) && !corepackEnabled) {
// Ensure that Bun 1 is at the beginning of the `$PATH`
newEnv.PATH = `${bun1}${path.delimiter}${oldPath}`;
console.log('Detected `bun.lockb` generated by Bun');
console.warn(
'Warning: Bun is used as a package manager at build time only, not at runtime with Functions'
);
}
} else {
Expand All @@ -548,7 +575,6 @@ export function getEnvForPackageManager({
newEnv.YARN_NODE_LINKER = 'node-modules';
}
}

return newEnv;
}

Expand Down Expand Up @@ -614,6 +640,8 @@ export async function runPackageJsonScript(
opts.prettyCommand = `npm run ${scriptName}`;
} else if (cliType === 'pnpm') {
opts.prettyCommand = `pnpm run ${scriptName}`;
} else if (cliType === 'bun') {
opts.prettyCommand = `bun run ${scriptName}`;
} else {
opts.prettyCommand = `yarn run ${scriptName}`;
}
Expand Down
2 changes: 2 additions & 0 deletions packages/build-utils/test/fixtures/30-bun-v1/.gitignore
@@ -0,0 +1,2 @@
.vercel
public
8 changes: 8 additions & 0 deletions packages/build-utils/test/fixtures/30-bun-v1/build.js
@@ -0,0 +1,8 @@
import { mkdir, rm, writeFile } from 'node:fs/promises'
import { say } from 'cowsay'

const text = say({ text: `bun version: ${process.versions.bun}` })
const content = say({ text })
await rm('./public', { recursive: true, force: true })
await mkdir('./public', { recursive: true })
await writeFile('./public/index.txt', content)
Binary file not shown.
9 changes: 9 additions & 0 deletions packages/build-utils/test/fixtures/30-bun-v1/package.json
@@ -0,0 +1,9 @@
{
"private": true,
"scripts": {
"build": "bun build.js"
},
"dependencies": {
"cowsay": "1.5.0"
}
}
8 changes: 8 additions & 0 deletions packages/build-utils/test/fixtures/30-bun-v1/probes.json
@@ -0,0 +1,8 @@
{
"probes": [
{
"path": "/",
"mustContain": "bun version: 1"
}
]
}
16 changes: 16 additions & 0 deletions packages/build-utils/test/unit.get-env-for-package-manager.test.ts
Expand Up @@ -133,6 +133,22 @@ describe('Test `getEnvForPackageManager()`', () => {
PATH: `/pnpm7/node_modules/.bin${delimiter}foo`,
},
},
{
name: 'should set path if bun v1 is detected',
args: {
cliType: 'bun',
nodeVersion: { major: 18, range: '18.x', runtime: 'nodejs18.x' },
lockfileVersion: 0,
env: {
FOO: 'bar',
PATH: '/usr/local/bin',
},
},
want: {
FOO: 'bar',
PATH: `/bun1${delimiter}/usr/local/bin`,
},
},
{
name: 'should not set pnpm path if corepack is enabled',
args: {
Expand Down
9 changes: 9 additions & 0 deletions packages/build-utils/test/unit.test.ts
Expand Up @@ -540,6 +540,15 @@ it(
ms('1m')
);

it('should return cliType bun and correct lock file for bun v1', async () => {
const fixture = path.join(__dirname, 'fixtures', '30-bun-v1');
const result = await scanParentDirs(fixture);
expect(result.cliType).toEqual('bun');
expect(result.lockfileVersion).toEqual(0);
expect(result.lockfilePath).toEqual(path.join(fixture, 'bun.lockb'));
expect(result.packageJsonPath).toEqual(path.join(fixture, 'package.json'));
});

it('should return lockfileVersion 2 with npm7', async () => {
const fixture = path.join(__dirname, 'fixtures', '20-npm-7');
const result = await scanParentDirs(fixture);
Expand Down

0 comments on commit 45b73c7

Please sign in to comment.