fix: linking commands of engines (#10354)

close #10244
This commit is contained in:
Zoltan Kochan
2025-12-23 12:26:59 +01:00
committed by GitHub
parent 6bbdbe87ba
commit 3bf5e218a6
15 changed files with 89 additions and 26 deletions

View File

@@ -0,0 +1,5 @@
---
"@pnpm/fetching.binary-fetcher": patch
---
Runtime dependencies (node, bun, deno) are now added to the store with a package.json file.

View File

@@ -0,0 +1,6 @@
---
"@pnpm/cafs-types": minor
"@pnpm/store.cafs": minor
---
Export a new function to add a new file to the CAFS.

View File

@@ -0,0 +1,5 @@
---
"pnpm": patch
---
Binaries of runtime engines (Node.js, Deno, Bun) are written to `node_modules/.bin` before lifecycle scripts (install, postinstall, prepare) are executed [#10244](https://github.com/pnpm/pnpm/issues/10244).

View File

@@ -0,0 +1,8 @@
---
"@pnpm/tarball-fetcher": minor
"@pnpm/fetcher-base": minor
"@pnpm/worker": minor
"@pnpm/crypto.object-hasher": minor
---
Added a way to append a manifest to a package with no package.json file.

View File

@@ -19,13 +19,23 @@ export function createBinaryFetcher (ctx: {
if (ctx.offline) { if (ctx.offline) {
throw new PnpmError('CANNOT_DOWNLOAD_BINARY_OFFLINE', `Cannot download binary "${resolution.url}" because offline mode is enabled.`) throw new PnpmError('CANNOT_DOWNLOAD_BINARY_OFFLINE', `Cannot download binary "${resolution.url}" because offline mode is enabled.`)
} }
const manifest = {
name: opts.pkg.name!,
version: opts.pkg.version!,
bin: resolution.bin,
}
let fetchResult!: FetchResult let fetchResult!: FetchResult
switch (resolution.archive) { switch (resolution.archive) {
case 'tarball': { case 'tarball': {
fetchResult = await ctx.fetchFromRemoteTarball(cafs, { fetchResult = await ctx.fetchFromRemoteTarball(cafs, {
tarball: resolution.url, tarball: resolution.url,
integrity: resolution.integrity, integrity: resolution.integrity,
}, opts) }, {
appendManifest: manifest,
...opts,
})
break break
} }
case 'zip': { case 'zip': {
@@ -40,6 +50,7 @@ export function createBinaryFetcher (ctx: {
dir: tempLocation, dir: tempLocation,
filesIndexFile: opts.filesIndexFile, filesIndexFile: opts.filesIndexFile,
readManifest: false, readManifest: false,
appendManifest: manifest,
}) })
break break
} }
@@ -47,11 +58,6 @@ export function createBinaryFetcher (ctx: {
throw new PnpmError('NOT_SUPPORTED_ARCHIVE', `The binary fetcher doesn't support archive type ${resolution.archive as string}`) throw new PnpmError('NOT_SUPPORTED_ARCHIVE', `The binary fetcher doesn't support archive type ${resolution.archive as string}`)
} }
} }
const manifest = {
name: opts.pkg.name!,
version: opts.pkg.version!,
bin: resolution.bin,
}
return { return {
...fetchResult, ...fetchResult,
manifest, manifest,

View File

@@ -20,6 +20,7 @@ export interface FetchOptions {
onProgress?: (downloaded: number) => void onProgress?: (downloaded: number) => void
readManifest?: boolean readManifest?: boolean
pkg: PkgNameVersion pkg: PkgNameVersion
appendManifest?: DependencyManifest
} }
export type FetchFunction<FetcherResolution = Resolution, Options = FetchOptions, Result = FetchResult> = ( export type FetchFunction<FetcherResolution = Resolution, Options = FetchOptions, Result = FetchResult> = (

View File

@@ -93,5 +93,6 @@ async function fetchFromTarball (
registry: resolution.registry, registry: resolution.registry,
filesIndexFile: opts.filesIndexFile, filesIndexFile: opts.filesIndexFile,
pkg: opts.pkg, pkg: opts.pkg,
appendManifest: opts.appendManifest,
}) })
} }

View File

@@ -24,6 +24,7 @@ export function createLocalTarballFetcher (): FetchFunction {
readManifest: opts.readManifest, readManifest: opts.readManifest,
url: tarball, url: tarball,
pkg: opts.pkg, pkg: opts.pkg,
appendManifest: opts.appendManifest,
}) })
} }

View File

@@ -18,16 +18,16 @@ export interface HttpResponse {
body: string body: string
} }
export type DownloadFunction = (url: string, opts: { export type DownloadOptions = {
getAuthHeaderByURI: (registry: string) => string | undefined getAuthHeaderByURI: (registry: string) => string | undefined
cafs: Cafs cafs: Cafs
readManifest?: boolean
registry?: string registry?: string
onStart?: (totalSize: number | null, attempt: number) => void onStart?: (totalSize: number | null, attempt: number) => void
onProgress?: (downloaded: number) => void onProgress?: (downloaded: number) => void
integrity?: string integrity?: string
filesIndexFile: string } & Pick<FetchOptions, 'pkg' | 'appendManifest' | 'readManifest' | 'filesIndexFile'>
} & Pick<FetchOptions, 'pkg'>) => Promise<FetchResult>
export type DownloadFunction = (url: string, opts: DownloadOptions) => Promise<FetchResult>
export interface NpmRegistryClient { export interface NpmRegistryClient {
get: (url: string, getOpts: object, cb: (err: Error, data: object, raw: object, res: HttpResponse) => void) => void get: (url: string, getOpts: object, cb: (err: Error, data: object, raw: object, res: HttpResponse) => void) => void
@@ -60,16 +60,7 @@ export function createDownloader (
} }
const fetchMinSpeedKiBps = gotOpts.fetchMinSpeedKiBps ?? 50 // 50 KiB/s const fetchMinSpeedKiBps = gotOpts.fetchMinSpeedKiBps ?? 50 // 50 KiB/s
return async function download (url: string, opts: { return async function download (url: string, opts: DownloadOptions): Promise<FetchResult> {
getAuthHeaderByURI: (registry: string) => string | undefined
cafs: Cafs
readManifest?: boolean
registry?: string
onStart?: (totalSize: number | null, attempt: number) => void
onProgress?: (downloaded: number) => void
integrity?: string
filesIndexFile: string
} & Pick<FetchOptions, 'pkg'>): Promise<FetchResult> {
const authHeaderValue = opts.getAuthHeaderByURI(url) const authHeaderValue = opts.getAuthHeaderByURI(url)
const op = retry.operation(retryOpts) const op = retry.operation(retryOpts)
@@ -179,6 +170,7 @@ export function createDownloader (
filesIndexFile: opts.filesIndexFile, filesIndexFile: opts.filesIndexFile,
url, url,
pkg: opts.pkg, pkg: opts.pkg,
appendManifest: opts.appendManifest,
}) })
} }
} }

View File

@@ -216,6 +216,9 @@ test('installing Node.js runtime', async () => {
}, },
}) })
// Verify that package.json is created
expect(fs.existsSync(path.resolve('node_modules/node/package.json'))).toBeTruthy()
rimraf('node_modules') rimraf('node_modules')
await install(manifest, testDefaults({ frozenLockfile: true }, { await install(manifest, testDefaults({ frozenLockfile: true }, {
offline: true, // We want to verify that Node.js is resolved from cache. offline: true, // We want to verify that Node.js is resolved from cache.

View File

@@ -73,6 +73,7 @@ export interface Cafs {
storeDir: string storeDir: string
addFilesFromDir: (dir: string) => AddToStoreResult addFilesFromDir: (dir: string) => AddToStoreResult
addFilesFromTarball: (buffer: Buffer) => AddToStoreResult addFilesFromTarball: (buffer: Buffer) => AddToStoreResult
addFile: (buffer: Buffer, mode: number) => FileWriteResult
getIndexFilePathInCafs: (integrity: string | IntegrityLike, fileType: FileType) => string getIndexFilePathInCafs: (integrity: string | IntegrityLike, fileType: FileType) => string
getFilePathByModeInCafs: (integrity: string | IntegrityLike, mode: number) => string getFilePathByModeInCafs: (integrity: string | IntegrityLike, mode: number) => string
importPackage: ImportPackageFunction importPackage: ImportPackageFunction

View File

@@ -43,6 +43,7 @@ export interface CreateCafsOpts {
export interface CafsFunctions { export interface CafsFunctions {
addFilesFromDir: (dirname: string, opts?: { files?: string[], readManifest?: boolean }) => AddToStoreResult addFilesFromDir: (dirname: string, opts?: { files?: string[], readManifest?: boolean }) => AddToStoreResult
addFilesFromTarball: (tarballBuffer: Buffer, readManifest?: boolean) => AddToStoreResult addFilesFromTarball: (tarballBuffer: Buffer, readManifest?: boolean) => AddToStoreResult
addFile: (buffer: Buffer, mode: number) => FileWriteResult
getIndexFilePathInCafs: (integrity: string | ssri.IntegrityLike, fileType: FileType) => string getIndexFilePathInCafs: (integrity: string | ssri.IntegrityLike, fileType: FileType) => string
getFilePathByModeInCafs: (integrity: string | ssri.IntegrityLike, mode: number) => string getFilePathByModeInCafs: (integrity: string | ssri.IntegrityLike, mode: number) => string
} }
@@ -53,6 +54,7 @@ export function createCafs (storeDir: string, { ignoreFile, cafsLocker }: Create
return { return {
addFilesFromDir: addFilesFromDir.bind(null, addBuffer), addFilesFromDir: addFilesFromDir.bind(null, addBuffer),
addFilesFromTarball: addFilesFromTarball.bind(null, addBuffer, ignoreFile ?? null), addFilesFromTarball: addFilesFromTarball.bind(null, addBuffer, ignoreFile ?? null),
addFile: addBuffer,
getIndexFilePathInCafs: getIndexFilePathInCafs.bind(null, storeDir), getIndexFilePathInCafs: getIndexFilePathInCafs.bind(null, storeDir),
getFilePathByModeInCafs: getFilePathByModeInCafs.bind(null, storeDir), getFilePathByModeInCafs: getFilePathByModeInCafs.bind(null, storeDir),
} }

View File

@@ -73,7 +73,7 @@ interface AddFilesResult {
integrity?: string integrity?: string
} }
type AddFilesFromDirOptions = Pick<AddDirToStoreMessage, 'storeDir' | 'dir' | 'filesIndexFile' | 'sideEffectsCacheKey' | 'readManifest' | 'pkg' | 'files'> type AddFilesFromDirOptions = Pick<AddDirToStoreMessage, 'storeDir' | 'dir' | 'filesIndexFile' | 'sideEffectsCacheKey' | 'readManifest' | 'pkg' | 'files' | 'appendManifest'>
export async function addFilesFromDir (opts: AddFilesFromDirOptions): Promise<AddFilesResult> { export async function addFilesFromDir (opts: AddFilesFromDirOptions): Promise<AddFilesResult> {
if (!workerPool) { if (!workerPool) {
@@ -97,6 +97,7 @@ export async function addFilesFromDir (opts: AddFilesFromDirOptions): Promise<Ad
sideEffectsCacheKey: opts.sideEffectsCacheKey, sideEffectsCacheKey: opts.sideEffectsCacheKey,
readManifest: opts.readManifest, readManifest: opts.readManifest,
pkg: opts.pkg, pkg: opts.pkg,
appendManifest: opts.appendManifest,
files: opts.files, files: opts.files,
}) })
}) })
@@ -136,7 +137,7 @@ If you think that this is the case, then run "pnpm store prune" and rerun the co
} }
} }
type AddFilesFromTarballOptions = Pick<TarballExtractMessage, 'buffer' | 'storeDir' | 'filesIndexFile' | 'integrity' | 'readManifest' | 'pkg'> & { type AddFilesFromTarballOptions = Pick<TarballExtractMessage, 'buffer' | 'storeDir' | 'filesIndexFile' | 'integrity' | 'readManifest' | 'pkg' | 'appendManifest'> & {
url: string url: string
} }
@@ -169,6 +170,7 @@ export async function addFilesFromTarball (opts: AddFilesFromTarballOptions): Pr
filesIndexFile: opts.filesIndexFile, filesIndexFile: opts.filesIndexFile,
readManifest: opts.readManifest, readManifest: opts.readManifest,
pkg: opts.pkg, pkg: opts.pkg,
appendManifest: opts.appendManifest,
}) })
}) })
} }

View File

@@ -143,7 +143,7 @@ async function handleMessage (
} }
} }
function addTarballToStore ({ buffer, storeDir, integrity, filesIndexFile }: TarballExtractMessage) { function addTarballToStore ({ buffer, storeDir, integrity, filesIndexFile, appendManifest }: TarballExtractMessage) {
if (integrity) { if (integrity) {
const [, algo, integrityHash] = integrity.match(INTEGRITY_REGEX)! const [, algo, integrityHash] = integrity.match(INTEGRITY_REGEX)!
// Compensate for the possibility of non-uniform Base64 padding // Compensate for the possibility of non-uniform Base64 padding
@@ -166,7 +166,11 @@ function addTarballToStore ({ buffer, storeDir, integrity, filesIndexFile }: Tar
cafsCache.set(storeDir, createCafs(storeDir)) cafsCache.set(storeDir, createCafs(storeDir))
} }
const cafs = cafsCache.get(storeDir)! const cafs = cafsCache.get(storeDir)!
const { filesIndex, manifest } = cafs.addFilesFromTarball(buffer, true) let { filesIndex, manifest } = cafs.addFilesFromTarball(buffer, true)
if (appendManifest && manifest == null) {
manifest = appendManifest
addManifestToCafs(cafs, filesIndex, appendManifest)
}
const { filesIntegrity, filesMap } = processFilesIndex(filesIndex) const { filesIntegrity, filesMap } = processFilesIndex(filesIndex)
const requiresBuild = writeFilesIndexFile(filesIndexFile, { manifest: manifest ?? {}, files: filesIntegrity }) const requiresBuild = writeFilesIndexFile(filesIndexFile, { manifest: manifest ?? {}, files: filesIntegrity })
return { return {
@@ -214,15 +218,28 @@ function initStore ({ storeDir }: InitStoreMessage): { status: string } {
return { status: 'success' } return { status: 'success' }
} }
function addFilesFromDir ({ dir, storeDir, filesIndexFile, sideEffectsCacheKey, files }: AddDirToStoreMessage): AddFilesFromDirResult { function addFilesFromDir (
{
appendManifest,
dir,
files,
filesIndexFile,
sideEffectsCacheKey,
storeDir,
}: AddDirToStoreMessage
): AddFilesFromDirResult {
if (!cafsCache.has(storeDir)) { if (!cafsCache.has(storeDir)) {
cafsCache.set(storeDir, createCafs(storeDir)) cafsCache.set(storeDir, createCafs(storeDir))
} }
const cafs = cafsCache.get(storeDir)! const cafs = cafsCache.get(storeDir)!
const { filesIndex, manifest } = cafs.addFilesFromDir(dir, { let { filesIndex, manifest } = cafs.addFilesFromDir(dir, {
files, files,
readManifest: true, readManifest: true,
}) })
if (appendManifest && manifest == null) {
manifest = appendManifest
addManifestToCafs(cafs, filesIndex, appendManifest)
}
const { filesIntegrity, filesMap } = processFilesIndex(filesIndex) const { filesIntegrity, filesMap } = processFilesIndex(filesIndex)
let requiresBuild: boolean let requiresBuild: boolean
if (sideEffectsCacheKey) { if (sideEffectsCacheKey) {
@@ -254,6 +271,16 @@ function addFilesFromDir ({ dir, storeDir, filesIndexFile, sideEffectsCacheKey,
return { status: 'success', value: { filesIndex: filesMap, manifest, requiresBuild } } return { status: 'success', value: { filesIndex: filesMap, manifest, requiresBuild } }
} }
function addManifestToCafs (cafs: CafsFunctions, filesIndex: FilesIndex, manifest: DependencyManifest): void {
const fileBuffer = Buffer.from(JSON.stringify(manifest, null, 2), 'utf8')
const mode = 0o644
filesIndex.set('package.json', {
mode,
size: fileBuffer.length,
...cafs.addFile(fileBuffer, mode),
})
}
function calculateDiff (baseFiles: PackageFiles, sideEffectsFiles: PackageFiles): SideEffectsDiff { function calculateDiff (baseFiles: PackageFiles, sideEffectsFiles: PackageFiles): SideEffectsDiff {
const deleted: string[] = [] const deleted: string[] = []
const added: PackageFiles = new Map() const added: PackageFiles = new Map()

View File

@@ -1,4 +1,5 @@
import { type PackageFilesResponse } from '@pnpm/cafs-types' import { type PackageFilesResponse } from '@pnpm/cafs-types'
import { type DependencyManifest } from '@pnpm/types'
export interface PkgNameVersion { export interface PkgNameVersion {
name?: string name?: string
@@ -18,6 +19,7 @@ export interface TarballExtractMessage {
filesIndexFile: string filesIndexFile: string
readManifest?: boolean readManifest?: boolean
pkg?: PkgNameVersion pkg?: PkgNameVersion
appendManifest?: DependencyManifest
} }
export interface LinkPkgMessage { export interface LinkPkgMessage {
@@ -50,6 +52,7 @@ export interface AddDirToStoreMessage {
sideEffectsCacheKey?: string sideEffectsCacheKey?: string
readManifest?: boolean readManifest?: boolean
pkg?: PkgNameVersion pkg?: PkgNameVersion
appendManifest?: DependencyManifest
files?: string[] files?: string[]
} }