feat: support installing Bun runtime (#9815)

* feat: support installing Bun runtime

* feat: support installing Bun runtime

* fix: cache libc resolution

* refactor: shasum file fetching

* docs: add changesets

* feat: installing the right artifact

* test: supported architectures

* test: fix on Windows
This commit is contained in:
Zoltan Kochan
2025-07-31 13:46:13 +02:00
committed by GitHub
parent 98dd75a5d9
commit 86b33e91ea
27 changed files with 706 additions and 58 deletions

View File

@@ -0,0 +1,11 @@
---
"@pnpm/resolving.bun-resolver": major
"@pnpm/read-project-manifest": minor
"@pnpm/default-resolver": minor
"@pnpm/resolver-base": minor
"@pnpm/link-bins": minor
"@pnpm/constants": minor
"pnpm": minor
---
Added support for installing Bun runtime.

View File

@@ -0,0 +1,5 @@
---
"@pnpm/crypto.shasums-file": major
---
fetchShasumsFile returns an array of shasum file items.

View File

@@ -1,29 +1,43 @@
import { createHash } from '@pnpm/crypto.hash'
import { PnpmError } from '@pnpm/error'
import {
type FetchFromRegistry,
} from '@pnpm/fetching-types'
export interface ShasumsFileItem {
integrity: string
fileName: string
}
export async function fetchShasumsFile (
fetch: FetchFromRegistry,
shasumsUrl: string,
expectedVersionIntegrity?: string
shasumsUrl: string
): Promise<ShasumsFileItem[]> {
const shasumsFileContent = await fetchShasumsFileRaw(fetch, shasumsUrl)
const lines = shasumsFileContent.split('\n')
const items: ShasumsFileItem[] = []
for (const line of lines) {
if (!line) continue
const [sha256, fileName] = line.trim().split(/\s+/)
items.push({
integrity: `sha256-${Buffer.from(sha256, 'hex').toString('base64')}`,
fileName,
})
}
return items
}
export async function fetchShasumsFileRaw (
fetch: FetchFromRegistry,
shasumsUrl: string
): Promise<string> {
const res = await fetch(shasumsUrl)
if (!res.ok) {
throw new PnpmError(
'NODE_FETCH_INTEGRITY_FAILED',
'FAILED_DOWNLOAD_SHASUM_FILE',
`Failed to fetch integrity file: ${shasumsUrl} (status: ${res.status})`
)
}
const body = await res.text()
if (expectedVersionIntegrity) {
const actualVersionIntegrity = createHash(body)
if (expectedVersionIntegrity !== actualVersionIntegrity) {
throw new PnpmError('NODE_VERSION_INTEGRITY_MISMATCH', `The integrity of ${shasumsUrl} failed. Expected: ${expectedVersionIntegrity}. Actual: ${actualVersionIntegrity}`)
}
}
return body
}

View File

@@ -234,6 +234,7 @@ async function buildGraphFromPackages (
lockfileDir: opts.lockfileDir,
ignoreScripts: opts.ignoreScripts,
pkg: { name: pkgName, version: pkgVersion, id: packageId, resolution },
supportedArchitectures: opts.supportedArchitectures,
})
} catch (err) {
if (pkgSnapshot.optional) return

View File

@@ -1,6 +1,6 @@
import path from 'path'
import { PnpmError } from '@pnpm/error'
import { fetchShasumsFile, pickFileChecksumFromShasumsFile } from '@pnpm/crypto.shasums-file'
import { fetchShasumsFileRaw, pickFileChecksumFromShasumsFile } from '@pnpm/crypto.shasums-file'
import {
type FetchFromRegistry,
type RetryTimeoutOptions,
@@ -119,10 +119,6 @@ async function getNodeArtifactInfo (
}
}
interface LoadArtifactIntegrityOptions {
expectedVersionIntegrity?: string
}
/**
* Loads and extracts the integrity hash for a specific Node.js artifact.
*
@@ -136,10 +132,9 @@ interface LoadArtifactIntegrityOptions {
async function loadArtifactIntegrity (
fetch: FetchFromRegistry,
fileName: string,
shasumsUrl: string,
options?: LoadArtifactIntegrityOptions
shasumsUrl: string
): Promise<string> {
const body = await fetchShasumsFile(fetch, shasumsUrl, options?.expectedVersionIntegrity)
const body = await fetchShasumsFileRaw(fetch, shasumsUrl)
return pickFileChecksumFromShasumsFile(body, fileName)
}

View File

@@ -60,21 +60,14 @@ export async function resolveNodeRuntime (
async function readNodeAssets (fetch: FetchFromRegistry, nodeMirrorBaseUrl: string, version: string): Promise<PlatformAssetResolution[]> {
const integritiesFileUrl = `${nodeMirrorBaseUrl}/v${version}/SHASUMS256.txt`
const shasumsFileContent = await fetchShasumsFile(fetch, integritiesFileUrl)
const lines = shasumsFileContent.split('\n')
const shasumsFileItems = await fetchShasumsFile(fetch, integritiesFileUrl)
const escaped = version.replace(/\\/g, '\\\\').replace(/\./g, '\\.')
const pattern = new RegExp(`^node-v${escaped}-([^-.]+)-([^.]+)\\.(?:tar\\.gz|zip)$`)
const assets: PlatformAssetResolution[] = []
for (const line of lines) {
if (!line) continue
const [sha256, file] = line.trim().split(/\s+/)
const match = pattern.exec(file)
for (const { integrity, fileName } of shasumsFileItems) {
const match = pattern.exec(fileName)
if (!match) continue
const buffer = Buffer.from(sha256, 'hex')
const base64 = buffer.toString('base64')
const integrity = `sha256-${base64}`
let [, platform, arch] = match
if (platform === 'win') {
platform = 'win32'

View File

@@ -26,3 +26,7 @@ export function getNodeBinLocationForCurrentOS (platform: string = process.platf
export function getDenoBinLocationForCurrentOS (platform: string = process.platform): string {
return platform === 'win32' ? 'deno.exe' : 'deno'
}
export function getBunBinLocationForCurrentOS (platform: string = process.platform): string {
return platform === 'win32' ? 'bun.exe' : 'bun'
}

View File

@@ -0,0 +1,272 @@
import { LOCKFILE_VERSION, WANTED_LOCKFILE } from '@pnpm/constants'
import { prepareEmpty } from '@pnpm/prepare'
import { addDependenciesToPackage, install } from '@pnpm/core'
import { getIntegrity } from '@pnpm/registry-mock'
import { sync as rimraf } from '@zkochan/rimraf'
import { sync as writeYamlFile } from 'write-yaml-file'
import { testDefaults } from '../utils'
const RESOLUTIONS = [
{
targets: [
{
os: 'darwin',
cpu: 'arm64',
},
],
resolution: {
type: 'binary',
archive: 'zip',
url: 'https://github.com/oven-sh/bun/releases/download/bun-v1.2.19/bun-darwin-aarch64.zip',
integrity: 'sha256-Z0pIN4NC76rcPCkVlrVzAQ88I4iVj3xEZ42H9vt1mZE=',
prefix: 'bun-darwin-aarch64',
bin: 'bun',
},
},
{
targets: [
{
os: 'darwin',
cpu: 'x64',
},
],
resolution: {
type: 'binary',
archive: 'zip',
url: 'https://github.com/oven-sh/bun/releases/download/bun-v1.2.19/bun-darwin-x64.zip',
integrity: 'sha256-39fkxHMRtdvTgjCzz9NX9dC+ro75eZYsW0EAj8QcJaA=',
prefix: 'bun-darwin-x64',
bin: 'bun',
},
},
{
targets: [
{
os: 'linux',
cpu: 'arm64',
libc: 'musl',
},
],
resolution: {
type: 'binary',
archive: 'zip',
url: 'https://github.com/oven-sh/bun/releases/download/bun-v1.2.19/bun-linux-aarch64-musl.zip',
integrity: 'sha256-ECBLT4ZeQCUI1pVr75O+Y11qek3cl0lCGxY2qseZZbY=',
prefix: 'bun-linux-aarch64-musl',
bin: 'bun',
},
},
{
targets: [
{
os: 'linux',
cpu: 'arm64',
},
],
resolution: {
type: 'binary',
archive: 'zip',
url: 'https://github.com/oven-sh/bun/releases/download/bun-v1.2.19/bun-linux-aarch64.zip',
integrity: 'sha256-/P1HHNvVp4/Uo5DinMzSu3AEpJ01K6A3rzth1P1dC4M=',
prefix: 'bun-linux-aarch64',
bin: 'bun',
},
},
{
targets: [
{
os: 'linux',
cpu: 'x64',
libc: 'musl',
},
],
resolution: {
type: 'binary',
archive: 'zip',
url: 'https://github.com/oven-sh/bun/releases/download/bun-v1.2.19/bun-linux-x64-musl.zip',
integrity: 'sha256-3M13Zi0KtkLSgO704yFtYCru4VGfdTXKHYOsqRjo/os=',
prefix: 'bun-linux-x64-musl',
bin: 'bun',
},
},
{
targets: [
{
os: 'linux',
cpu: 'x64',
},
],
resolution: {
type: 'binary',
archive: 'zip',
url: 'https://github.com/oven-sh/bun/releases/download/bun-v1.2.19/bun-linux-x64.zip',
integrity: 'sha256-w9PBTppeyD/2fQrP525DFa0G2p809Z/HsTgTeCyvH2Y=',
prefix: 'bun-linux-x64',
bin: 'bun',
},
},
{
targets: [
{
os: 'win32',
cpu: 'x64',
},
],
resolution: {
type: 'binary',
archive: 'zip',
url: 'https://github.com/oven-sh/bun/releases/download/bun-v1.2.19/bun-windows-x64.zip',
integrity: 'sha256-pIj0ZM5nsw4Ayw6lay9i5JuBw/zqe6kkYdNiJLBvdfg=',
prefix: 'bun-windows-x64',
bin: 'bun.exe',
},
},
]
test('installing Bun runtime', async () => {
const project = prepareEmpty()
const { updatedManifest: manifest } = await addDependenciesToPackage({}, ['bun@runtime:1.2.19'], testDefaults({ fastUnpack: false }))
project.isExecutable('.bin/bun')
expect(project.readLockfile()).toStrictEqual({
settings: {
autoInstallPeers: true,
excludeLinksFromLockfile: false,
},
importers: {
'.': {
dependencies: {
bun: {
specifier: 'runtime:1.2.19',
version: 'runtime:1.2.19',
},
},
},
},
lockfileVersion: LOCKFILE_VERSION,
packages: {
'bun@runtime:1.2.19': {
hasBin: true,
resolution: {
type: 'variations',
variants: RESOLUTIONS,
},
version: '1.2.19',
},
},
snapshots: {
'bun@runtime:1.2.19': {},
},
})
rimraf('node_modules')
await install(manifest, testDefaults({ frozenLockfile: true }, {
offline: true, // We want to verify that Bun is resolved from cache.
}))
project.isExecutable('.bin/bun')
await addDependenciesToPackage(manifest, ['@pnpm.e2e/dep-of-pkg-with-1-dep@100.1.0'], testDefaults({ fastUnpack: false }))
project.has('@pnpm.e2e/dep-of-pkg-with-1-dep')
expect(project.readLockfile()).toStrictEqual({
settings: {
autoInstallPeers: true,
excludeLinksFromLockfile: false,
},
importers: {
'.': {
dependencies: {
bun: {
specifier: 'runtime:1.2.19',
version: 'runtime:1.2.19',
},
'@pnpm.e2e/dep-of-pkg-with-1-dep': {
specifier: '100.1.0',
version: '100.1.0',
},
},
},
},
lockfileVersion: LOCKFILE_VERSION,
packages: {
'bun@runtime:1.2.19': {
hasBin: true,
resolution: {
type: 'variations',
variants: RESOLUTIONS,
},
version: '1.2.19',
},
'@pnpm.e2e/dep-of-pkg-with-1-dep@100.1.0': {
resolution: {
integrity: getIntegrity('@pnpm.e2e/dep-of-pkg-with-1-dep', '100.1.0'),
},
},
},
snapshots: {
'bun@runtime:1.2.19': {},
'@pnpm.e2e/dep-of-pkg-with-1-dep@100.1.0': {},
},
})
})
test('installing Bun runtime fails if offline mode is used and Bun not found locally', async () => {
prepareEmpty()
await expect(
addDependenciesToPackage({}, ['bun@runtime:1.2.19'], testDefaults({ fastUnpack: false }, { offline: true }))
).rejects.toThrow(/Failed to resolve bun@1.2.19 in package mirror/)
})
test('installing Bun runtime fails if integrity check fails', async () => {
prepareEmpty()
writeYamlFile(WANTED_LOCKFILE, {
settings: {
autoInstallPeers: true,
excludeLinksFromLockfile: false,
},
importers: {
'.': {
devDependencies: {
bun: {
specifier: 'runtime:1.2.19',
version: 'runtime:1.2.19',
},
},
},
},
lockfileVersion: LOCKFILE_VERSION,
packages: {
'bun@runtime:1.2.19': {
hasBin: true,
resolution: {
type: 'variations',
variants: RESOLUTIONS.map((resolutionVariant) => ({
...resolutionVariant,
resolution: {
...resolutionVariant.resolution,
integrity: 'sha256-0000000000000000000000000000000000000000000=',
},
})),
},
version: '1.2.19',
},
},
snapshots: {
'bun@runtime:1.2.19': {},
},
}, {
lineWidth: -1,
})
const manifest = {
devDependencies: {
bun: 'runtime:1.2.19',
},
}
await expect(install(manifest, testDefaults({ frozenLockfile: true }, {
retry: {
retries: 0,
},
}))).rejects.toThrow(/Got unexpected checksum for/)
})

View File

@@ -331,3 +331,25 @@ test('installing Node.js runtime fails if integrity check fails', async () => {
},
}))).rejects.toThrow(/Got unexpected checksum for/)
})
test('installing Node.js runtime for the given supported architecture', async () => {
const isWindows = process.platform === 'win32'
const supportedArchitectures = {
os: [isWindows ? 'linux' : 'win32'],
cpu: ['x64'],
}
const expectedBinLocation = isWindows ? 'node/bin/node' : 'node/node.exe'
const project = prepareEmpty()
const { updatedManifest: manifest } = await addDependenciesToPackage(
{},
['node@runtime:22.0.0'],
testDefaults({
fastUnpack: false,
supportedArchitectures,
})
)
project.has(expectedBinLocation)
rimraf('node_modules')
await install(manifest, testDefaults({ frozenLockfile: true, supportedArchitectures }))
project.has(expectedBinLocation)
})

View File

@@ -229,6 +229,7 @@ async function fetchDeps (
lockfileDir: opts.lockfileDir,
ignoreScripts: opts.ignoreScripts,
pkg: pkgResolution,
supportedArchitectures: opts.supportedArchitectures,
}) as any // eslint-disable-line
if (fetchResponse instanceof Promise) fetchResponse = await fetchResponse
} catch (err: any) { // eslint-disable-line

View File

@@ -1,7 +1,7 @@
import { promises as fs, existsSync } from 'fs'
import Module from 'module'
import path from 'path'
import { getNodeBinLocationForCurrentOS, getDenoBinLocationForCurrentOS } from '@pnpm/constants'
import { getNodeBinLocationForCurrentOS, getDenoBinLocationForCurrentOS, getBunBinLocationForCurrentOS } from '@pnpm/constants'
import { PnpmError } from '@pnpm/error'
import { logger, globalWarn } from '@pnpm/logger'
import { getAllDependenciesFromManifest } from '@pnpm/manifest-utils'
@@ -227,6 +227,15 @@ async function getPackageBins (
pkgVersion: '',
makePowerShellShim: false,
}]
case 'bun':
return [{
name: 'bun',
path: path.join(target, getBunBinLocationForCurrentOS()),
ownName: true,
pkgName: '',
pkgVersion: '',
makePowerShellShim: false,
}]
}
// There's a directory in node_modules without package.json: ${target}.
// This used to be a warning but it didn't really cause any issues.

View File

@@ -46,6 +46,7 @@
"@pnpm/store-controller-types": "workspace:*",
"@pnpm/store.cafs": "workspace:*",
"@pnpm/types": "workspace:*",
"detect-libc": "catalog:",
"p-defer": "catalog:",
"p-limit": "catalog:",
"p-queue": "catalog:",

View File

@@ -42,9 +42,10 @@ import {
type RequestPackageOptions,
type WantedDependency,
} from '@pnpm/store-controller-types'
import { type DependencyManifest } from '@pnpm/types'
import { type DependencyManifest, type SupportedArchitectures } from '@pnpm/types'
import { depPathToFilename } from '@pnpm/dependency-path'
import { readPkgFromCafs as _readPkgFromCafs } from '@pnpm/worker'
import { familySync } from 'detect-libc'
import PQueue from 'p-queue'
import pDefer from 'p-defer'
import pShare from 'promise-share'
@@ -53,6 +54,13 @@ import semver from 'semver'
import ssri from 'ssri'
import { equalOrSemverEqual } from './equalOrSemverEqual'
let currentLibc: 'glibc' | 'musl' | undefined | null
function getLibcFamilySync () {
if (currentLibc === undefined) {
currentLibc = familySync() as unknown as typeof currentLibc
}
return currentLibc
}
const TARBALL_INTEGRITY_FILENAME = 'tarball-integrity'
const packageRequestLogger = logger('package-requester')
@@ -303,6 +311,7 @@ async function resolveAndFetch (
resolution,
},
onFetchError: options.onFetchError,
supportedArchitectures: options.supportedArchitectures,
})
if (!manifest) {
@@ -345,7 +354,7 @@ function getFilesIndexFilePath (
storeDir: string
virtualStoreDirMaxLength: number
},
opts: Pick<FetchPackageToStoreOptions, 'pkg' | 'ignoreScripts'>
opts: Pick<FetchPackageToStoreOptions, 'pkg' | 'ignoreScripts' | 'supportedArchitectures'>
): GetFilesIndexFilePathResult {
const targetRelative = depPathToFilename(opts.pkg.id, ctx.virtualStoreDirMaxLength)
const target = path.join(ctx.storeDir, targetRelative)
@@ -358,7 +367,7 @@ function getFilesIndexFilePath (
}
let resolution!: AtomicResolution
if (opts.pkg.resolution.type === 'variations') {
resolution = findResolution(opts.pkg.resolution.variants)
resolution = findResolution(opts.pkg.resolution.variants, opts.supportedArchitectures)
if ((resolution as TarballResolution).integrity) {
return {
target,
@@ -373,9 +382,17 @@ function getFilesIndexFilePath (
return { filesIndexFile, target, resolution }
}
function findResolution (resolutionVariants: PlatformAssetResolution[]): AtomicResolution {
function findResolution (resolutionVariants: PlatformAssetResolution[], supportedArchitectures?: SupportedArchitectures): AtomicResolution {
const platform = getOneIfNonCurrent(supportedArchitectures?.os) ?? process.platform
const cpu = getOneIfNonCurrent(supportedArchitectures?.cpu) ?? process.arch
const libc = getOneIfNonCurrent(supportedArchitectures?.libc) ?? getLibcFamilySync()
const resolutionVariant = resolutionVariants
.find((resolutionVariant) => resolutionVariant.targets.some((target) => target.os === process.platform && target.cpu === process.arch))
.find((resolutionVariant) => resolutionVariant.targets.some(
(target) =>
target.os === platform &&
target.cpu === cpu &&
(target.libc == null || target.libc === libc)
))
if (!resolutionVariant) {
const resolutionTargets = resolutionVariants.map((variant) => variant.targets)
throw new PnpmError('NO_RESOLUTION_MATCHED', `Cannot find a resolution variant for the current platform in these resolutions: ${JSON.stringify(resolutionTargets)}`)
@@ -383,6 +400,13 @@ function findResolution (resolutionVariants: PlatformAssetResolution[]): AtomicR
return resolutionVariant.resolution
}
function getOneIfNonCurrent (requirements: string[] | undefined): string | undefined {
if (requirements?.length && requirements[0] !== 'current') {
return requirements[0]
}
return undefined
}
function fetchToStore (
ctx: {
readPkgFromCafs: (

View File

@@ -223,27 +223,17 @@ function createManifestWriter (
}
function convertManifestAfterRead (manifest: ProjectManifest): ProjectManifest {
if (manifest.devEngines?.runtime && !manifest.devDependencies?.['node']) {
const runtimes = Array.isArray(manifest.devEngines.runtime) ? manifest.devEngines.runtime : [manifest.devEngines.runtime]
const nodeRuntime = runtimes.find((runtime) => runtime.name === 'node')
if (nodeRuntime && nodeRuntime.onFail === 'download') {
if ('webcontainer' in process.versions) {
globalWarn('Installation of Node.js versions is not supported in WebContainer')
} else {
manifest.devDependencies ??= {}
manifest.devDependencies['node'] = `runtime:${nodeRuntime.version}`
}
}
}
if (manifest.devEngines?.runtime && !manifest.devDependencies?.['deno']) {
const runtimes = Array.isArray(manifest.devEngines.runtime) ? manifest.devEngines.runtime : [manifest.devEngines.runtime]
const denoRuntime = runtimes.find((runtime) => runtime.name === 'deno')
if (denoRuntime && denoRuntime.onFail === 'download') {
if ('webcontainer' in process.versions) {
globalWarn('Installation of Deno versions is not supported in WebContainer')
} else {
manifest.devDependencies ??= {}
manifest.devDependencies['deno'] = `runtime:${denoRuntime.version}`
for (const runtimeName of ['node', 'deno', 'bun']) {
if (manifest.devEngines?.runtime && !manifest.devDependencies?.[runtimeName]) {
const runtimes = Array.isArray(manifest.devEngines.runtime) ? manifest.devEngines.runtime : [manifest.devEngines.runtime]
const runtime = runtimes.find((runtime) => runtime.name === runtimeName)
if (runtime && runtime.onFail === 'download') {
if ('webcontainer' in process.versions) {
globalWarn(`Installation of ${runtimeName} versions is not supported in WebContainer`)
} else {
manifest.devDependencies ??= {}
manifest.devDependencies[runtimeName] = `runtime:${runtime.version}`
}
}
}
}
@@ -251,7 +241,7 @@ function convertManifestAfterRead (manifest: ProjectManifest): ProjectManifest {
}
function convertManifestBeforeWrite (manifest: ProjectManifest): ProjectManifest {
for (const runtimeName of ['node', 'deno']) {
for (const runtimeName of ['node', 'deno', 'bun']) {
const nodeDep = manifest.devDependencies?.[runtimeName]
if (typeof nodeDep === 'string' && nodeDep.startsWith('runtime:')) {
const version = nodeDep.replace(/^runtime:/, '')

61
pnpm-lock.yaml generated
View File

@@ -5368,6 +5368,9 @@ importers:
'@pnpm/worker':
specifier: workspace:^
version: link:../../worker
detect-libc:
specifier: 'catalog:'
version: 2.0.3
p-defer:
specifier: 'catalog:'
version: 3.0.0
@@ -6733,6 +6736,58 @@ importers:
specifier: 'catalog:'
version: 5.0.0
resolving/bun-resolver:
dependencies:
'@pnpm/constants':
specifier: workspace:*
version: link:../../packages/constants
'@pnpm/crypto.shasums-file':
specifier: workspace:*
version: link:../../crypto/shasums-file
'@pnpm/error':
specifier: workspace:*
version: link:../../packages/error
'@pnpm/fetcher-base':
specifier: workspace:*
version: link:../../fetching/fetcher-base
'@pnpm/fetching-types':
specifier: workspace:*
version: link:../../network/fetching-types
'@pnpm/fetching.binary-fetcher':
specifier: workspace:*
version: link:../../fetching/binary-fetcher
'@pnpm/node.fetcher':
specifier: workspace:*
version: link:../../env/node.fetcher
'@pnpm/npm-resolver':
specifier: workspace:*
version: link:../npm-resolver
'@pnpm/resolver-base':
specifier: workspace:*
version: link:../resolver-base
'@pnpm/types':
specifier: workspace:*
version: link:../../packages/types
'@pnpm/util.lex-comparator':
specifier: 'catalog:'
version: 3.0.2
'@pnpm/worker':
specifier: workspace:*
version: link:../../worker
semver:
specifier: 'catalog:'
version: 7.7.1
devDependencies:
'@pnpm/resolving.bun-resolver':
specifier: workspace:*
version: 'link:'
'@pnpm/resolving.deno-resolver':
specifier: workspace:*
version: link:../deno-resolver
'@types/semver':
specifier: 'catalog:'
version: 7.5.3
resolving/default-resolver:
dependencies:
'@pnpm/error':
@@ -6756,6 +6811,9 @@ importers:
'@pnpm/resolver-base':
specifier: workspace:*
version: link:../resolver-base
'@pnpm/resolving.bun-resolver':
specifier: workspace:*
version: link:../bun-resolver
'@pnpm/resolving.deno-resolver':
specifier: workspace:*
version: link:../deno-resolver
@@ -6775,6 +6833,9 @@ importers:
'@pnpm/constants':
specifier: workspace:*
version: link:../../packages/constants
'@pnpm/crypto.shasums-file':
specifier: workspace:*
version: link:../../crypto/shasums-file
'@pnpm/error':
specifier: workspace:*
version: link:../../packages/error

View File

@@ -0,0 +1,17 @@
# @pnpm/resolving.bun-resolver
> Resolves the Bun runtime
[![npm version](https://img.shields.io/npm/v/@pnpm/resolving.bun-resolver.svg)](https://www.npmjs.com/package/@pnpm/resolving.bun-resolver)
## Installation
```sh
pnpm add @pnpm/resolving.bun-resolver
```
## License
MIT

View File

@@ -0,0 +1,60 @@
{
"name": "@pnpm/resolving.bun-resolver",
"version": "1000.0.0-0",
"description": "Resolves the Bun runtime",
"keywords": [
"pnpm",
"pnpm10",
"bun",
"runtime"
],
"license": "MIT",
"funding": "https://opencollective.com/pnpm",
"repository": "https://github.com/pnpm/pnpm/blob/main/resolving/bun-resolver",
"homepage": "https://github.com/pnpm/pnpm/blob/main/resolving/bun-resolver#readme",
"bugs": {
"url": "https://github.com/pnpm/pnpm/issues"
},
"type": "commonjs",
"main": "lib/index.js",
"types": "lib/index.d.ts",
"exports": {
".": "./lib/index.js"
},
"files": [
"lib",
"!*.map"
],
"scripts": {
"lint": "eslint \"src/**/*.ts\"",
"test": "pnpm run compile",
"prepublishOnly": "pnpm run compile",
"compile": "tsc --build && pnpm run lint --fix"
},
"dependencies": {
"@pnpm/constants": "workspace:*",
"@pnpm/crypto.shasums-file": "workspace:*",
"@pnpm/error": "workspace:*",
"@pnpm/fetcher-base": "workspace:*",
"@pnpm/fetching-types": "workspace:*",
"@pnpm/fetching.binary-fetcher": "workspace:*",
"@pnpm/node.fetcher": "workspace:*",
"@pnpm/npm-resolver": "workspace:*",
"@pnpm/resolver-base": "workspace:*",
"@pnpm/types": "workspace:*",
"@pnpm/util.lex-comparator": "catalog:",
"@pnpm/worker": "workspace:*",
"semver": "catalog:"
},
"devDependencies": {
"@pnpm/resolving.bun-resolver": "workspace:*",
"@pnpm/resolving.deno-resolver": "workspace:*",
"@types/semver": "catalog:"
},
"engines": {
"node": ">=18.12"
},
"jest": {
"preset": "@pnpm/jest-config"
}
}

View File

@@ -0,0 +1,97 @@
import { getBunBinLocationForCurrentOS } from '@pnpm/constants'
import { fetchShasumsFile } from '@pnpm/crypto.shasums-file'
import { PnpmError } from '@pnpm/error'
import { type FetchFromRegistry } from '@pnpm/fetching-types'
import {
type BinaryResolution,
type PlatformAssetResolution,
type PlatformAssetTarget,
type ResolveResult,
type VariationsResolution,
type WantedDependency,
} from '@pnpm/resolver-base'
import { type PkgResolutionId } from '@pnpm/types'
import { type NpmResolver } from '@pnpm/npm-resolver'
import { lexCompare } from '@pnpm/util.lex-comparator'
export interface BunRuntimeResolveResult extends ResolveResult {
resolution: VariationsResolution
resolvedVia: 'github.com/oven-sh/bun'
}
export async function resolveBunRuntime (
ctx: {
fetchFromRegistry: FetchFromRegistry
rawConfig: Record<string, string>
offline?: boolean
resolveFromNpm: NpmResolver
},
wantedDependency: WantedDependency
): Promise<BunRuntimeResolveResult | null> {
if (wantedDependency.alias !== 'bun' || !wantedDependency.bareSpecifier?.startsWith('runtime:')) return null
const versionSpec = wantedDependency.bareSpecifier.substring('runtime:'.length)
// We use the npm registry for version resolution as it is easier than using the GitHub API for releases,
// which uses pagination (e.g. https://api.github.com/repos/oven-sh/bun/releases?per_page=100).
const npmResolution = await ctx.resolveFromNpm({ ...wantedDependency, bareSpecifier: versionSpec }, {})
if (npmResolution == null) {
throw new PnpmError('BUN_RESOLUTION_FAILURE', `Could not resolve Bun version specified as ${versionSpec}`)
}
const version = npmResolution.manifest.version
const assets = await readBunAssets(ctx.fetchFromRegistry, version)
assets.sort((asset1, asset2) => lexCompare((asset1.resolution as BinaryResolution).url, (asset2.resolution as BinaryResolution).url))
return {
id: `bun@runtime:${version}` as PkgResolutionId,
normalizedBareSpecifier: `runtime:${versionSpec}`,
resolvedVia: 'github.com/oven-sh/bun',
manifest: {
name: 'bun',
version,
bin: getBunBinLocationForCurrentOS(),
},
resolution: {
type: 'variations',
variants: assets,
},
}
}
async function readBunAssets (fetch: FetchFromRegistry, version: string): Promise<PlatformAssetResolution[]> {
const integritiesFileUrl = `https://github.com/oven-sh/bun/releases/download/bun-v${version}/SHASUMS256.txt`
const shasumsFileItems = await fetchShasumsFile(fetch, integritiesFileUrl)
const pattern = /^bun-([^-.]+)-([^-.]+)(-musl)?\.zip$/
const assets: PlatformAssetResolution[] = []
for (const { integrity, fileName } of shasumsFileItems) {
const match = pattern.exec(fileName)
if (!match) continue
let [, platform, arch, musl] = match
if (platform === 'windows') {
platform = 'win32'
}
if (arch === 'aarch64') {
arch = 'arm64'
}
const url = `https://github.com/oven-sh/bun/releases/download/bun-v${version}/${fileName}`
const resolution: BinaryResolution = {
type: 'binary',
archive: 'zip',
bin: getBunBinLocationForCurrentOS(platform),
integrity,
url,
prefix: fileName.replace(/\.zip$/, ''),
}
const target: PlatformAssetTarget = {
os: platform,
cpu: arch,
}
if (musl != null) {
target.libc = 'musl'
}
assets.push({
targets: [target],
resolution,
})
}
return assets
}

View File

@@ -0,0 +1,49 @@
{
"extends": "@pnpm/tsconfig",
"compilerOptions": {
"outDir": "lib",
"rootDir": "src"
},
"include": [
"src/**/*.ts",
"../../__typings__/**/*.d.ts"
],
"references": [
{
"path": "../../crypto/shasums-file"
},
{
"path": "../../env/node.fetcher"
},
{
"path": "../../fetching/binary-fetcher"
},
{
"path": "../../fetching/fetcher-base"
},
{
"path": "../../network/fetching-types"
},
{
"path": "../../packages/constants"
},
{
"path": "../../packages/error"
},
{
"path": "../../packages/types"
},
{
"path": "../../worker"
},
{
"path": "../deno-resolver"
},
{
"path": "../npm-resolver"
},
{
"path": "../resolver-base"
}
]
}

View File

@@ -0,0 +1,8 @@
{
"extends": "./tsconfig.json",
"include": [
"src/**/*.ts",
"test/**/*.ts",
"../../__typings__/**/*.d.ts"
]
}

View File

@@ -40,6 +40,7 @@
"@pnpm/node.resolver": "workspace:*",
"@pnpm/npm-resolver": "workspace:*",
"@pnpm/resolver-base": "workspace:*",
"@pnpm/resolving.bun-resolver": "workspace:*",
"@pnpm/resolving.deno-resolver": "workspace:*",
"@pnpm/tarball-resolver": "workspace:*"
},

View File

@@ -4,6 +4,7 @@ import { type GitResolveResult, createGitResolver } from '@pnpm/git-resolver'
import { type LocalResolveResult, resolveFromLocal } from '@pnpm/local-resolver'
import { resolveNodeRuntime, type NodeRuntimeResolveResult } from '@pnpm/node.resolver'
import { resolveDenoRuntime, type DenoRuntimeResolveResult } from '@pnpm/resolving.deno-resolver'
import { resolveBunRuntime, type BunRuntimeResolveResult } from '@pnpm/resolving.bun-resolver'
import {
createNpmResolver,
type JsrResolveResult,
@@ -37,6 +38,7 @@ export type DefaultResolveResult =
| WorkspaceResolveResult
| NodeRuntimeResolveResult
| DenoRuntimeResolveResult
| BunRuntimeResolveResult
export type DefaultResolver = (wantedDependency: WantedDependency, opts: ResolveOptions) => Promise<DefaultResolveResult>
@@ -54,6 +56,7 @@ export function createResolver (
})
const _resolveNodeRuntime = resolveNodeRuntime.bind(null, { fetchFromRegistry, offline: pnpmOpts.offline, rawConfig: pnpmOpts.rawConfig })
const _resolveDenoRuntime = resolveDenoRuntime.bind(null, { fetchFromRegistry, offline: pnpmOpts.offline, rawConfig: pnpmOpts.rawConfig, resolveFromNpm })
const _resolveBunRuntime = resolveBunRuntime.bind(null, { fetchFromRegistry, offline: pnpmOpts.offline, rawConfig: pnpmOpts.rawConfig, resolveFromNpm })
return {
resolve: async (wantedDependency, opts) => {
const resolution = await resolveFromNpm(wantedDependency, opts as ResolveFromNpmOptions) ??
@@ -64,7 +67,8 @@ export function createResolver (
await _resolveFromLocal(wantedDependency as { bareSpecifier: string }, opts)
)) ??
await _resolveNodeRuntime(wantedDependency) ??
await _resolveDenoRuntime(wantedDependency)
await _resolveDenoRuntime(wantedDependency) ??
await _resolveBunRuntime(wantedDependency)
if (!resolution) {
throw new PnpmError(
'SPEC_NOT_SUPPORTED_BY_ANY_RESOLVER',

View File

@@ -21,6 +21,9 @@
{
"path": "../../packages/error"
},
{
"path": "../bun-resolver"
},
{
"path": "../deno-resolver"
},

View File

@@ -33,6 +33,7 @@
},
"dependencies": {
"@pnpm/constants": "workspace:*",
"@pnpm/crypto.shasums-file": "workspace:*",
"@pnpm/error": "workspace:*",
"@pnpm/fetcher-base": "workspace:*",
"@pnpm/fetching-types": "workspace:*",

View File

@@ -9,6 +9,9 @@
"../../__typings__/**/*.d.ts"
],
"references": [
{
"path": "../../crypto/shasums-file"
},
{
"path": "../../env/node.fetcher"
},

View File

@@ -44,6 +44,7 @@ export interface GitResolution {
export interface PlatformAssetTarget {
os: string
cpu: string
libc?: 'musl'
}
export interface PlatformAssetResolution {

View File

@@ -93,6 +93,7 @@ export interface FetchPackageToStoreOptions {
resolution: Resolution
}
onFetchError?: OnFetchError
supportedArchitectures?: SupportedArchitectures
}
export type OnFetchError = (error: Error) => Error