mirror of
https://github.com/pnpm/pnpm.git
synced 2026-07-19 20:22:21 -04:00
Once the lockfile pins integrity for git-hosted tarballs, every
post-fetch reader (pkg-finder, store status, FUSE mounter,
skipIfHasSideEffectsCache in building/after-install) looks the package
up under storeIndexKey(integrity, pkgId). Three issues were exposed:
1. pkg-finder ran parse(packageId) and rebuilt name@version, but
packageIdFromSnapshot returns the URL alone for git-hosted (the
`:` in `https://` makes tryGetPackageId strip the `name@`
prefix). parse(URL) returns {} and the lookup key became
`…\tundefined@undefined`. Use packageId directly — that's the
form the writer in package-requester uses.
2. The FUSE handler and the after-install side-effects-cache lookup
used `${name}@${version}`, which doesn't match the resolver's
pkg.id for git-hosted (URL). Use
`nonSemverVersion ?? ${name}@${version}` so the readers compute
the same key the writer uses.
3. On the first install, integrity isn't known when
getFilesIndexFilePath runs, so the writer parks the index under
gitHostedStoreIndexKey. Once the worker computes integrity, the
git-hosted fetcher now also registers the entry under
storeIndexKey(integrity, pkgId), so subsequent integrity-based
lookups (the same install or any later one) resolve to the same
data without a refetch.
The license test that walks the lockfile right after install now
passes locally; that's the case that was failing in CI.
---
Written by an agent (Claude Code, claude-opus-4-7).
164 lines
5.4 KiB
TypeScript
164 lines
5.4 KiB
TypeScript
import fs from 'node:fs'
|
|
import os from 'node:os'
|
|
import path from 'node:path'
|
|
|
|
import { afterAll, beforeAll, describe, expect, it } from '@jest/globals'
|
|
import type { GitResolution, Resolution, TarballResolution } from '@pnpm/resolving.resolver-base'
|
|
import type { PackageFilesIndex } from '@pnpm/store.cafs'
|
|
import { gitHostedStoreIndexKey, StoreIndex, storeIndexKey } from '@pnpm/store.index'
|
|
import { readPackageFileMap } from '@pnpm/store.pkg-finder'
|
|
|
|
function createFilesIndex (): PackageFilesIndex {
|
|
return {
|
|
algo: 'sha256',
|
|
files: new Map([
|
|
['package.json', { digest: 'abc123', mode: 0o644, size: 0 }],
|
|
['index.js', { digest: 'def456', mode: 0o644, size: 0 }],
|
|
]),
|
|
}
|
|
}
|
|
|
|
function writeCafsFile (storeDir: string, digest: string, content: string): void {
|
|
const dir = path.join(storeDir, 'files', digest.slice(0, 2))
|
|
fs.mkdirSync(dir, { recursive: true })
|
|
fs.writeFileSync(path.join(dir, digest.slice(2)), content)
|
|
}
|
|
|
|
describe('readPackageFileMap', () => {
|
|
let storeDir: string
|
|
let storeIndex: StoreIndex
|
|
|
|
beforeAll(() => {
|
|
storeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pnpm-pkg-finder-test-'))
|
|
storeIndex = new StoreIndex(storeDir)
|
|
})
|
|
|
|
afterAll(() => {
|
|
storeIndex.close()
|
|
fs.rmSync(storeDir, { recursive: true, force: true })
|
|
})
|
|
|
|
const defaultOpts = () => ({
|
|
storeDir,
|
|
storeIndex,
|
|
lockfileDir: '/tmp/project',
|
|
virtualStoreDirMaxLength: 120,
|
|
})
|
|
|
|
it('should resolve registry packages by integrity hash', async () => {
|
|
const integrity = 'sha512-abc123registry'
|
|
const pkgId = 'express@4.18.2'
|
|
const key = storeIndexKey(integrity, pkgId)
|
|
|
|
storeIndex.set(key, createFilesIndex())
|
|
|
|
const resolution: TarballResolution = {
|
|
integrity,
|
|
tarball: 'https://registry.npmjs.org/express/-/express-4.18.2.tgz',
|
|
}
|
|
|
|
const result = await readPackageFileMap(resolution, pkgId, defaultOpts())
|
|
|
|
expect(result).toBeDefined()
|
|
expect(result!.has('package.json')).toBe(true)
|
|
expect(result!.has('index.js')).toBe(true)
|
|
})
|
|
|
|
it('should resolve git-hosted tarball packages with integrity', async () => {
|
|
// After we started pinning integrity for git-hosted tarballs, the
|
|
// packageId here is the URL alone (tryGetPackageId strips the `name@`
|
|
// because the URL contains `:`). The reader must therefore key by the
|
|
// URL — the same key the writer used.
|
|
const integrity = 'sha512-abc123gitHosted'
|
|
const pkgId = 'https://codeload.github.com/stevemao/left-pad/tar.gz/cafe1234'
|
|
const key = storeIndexKey(integrity, pkgId)
|
|
|
|
storeIndex.set(key, createFilesIndex())
|
|
|
|
const resolution: TarballResolution = {
|
|
integrity,
|
|
tarball: pkgId,
|
|
}
|
|
|
|
const result = await readPackageFileMap(resolution, pkgId, defaultOpts())
|
|
|
|
expect(result).toBeDefined()
|
|
expect(result!.has('package.json')).toBe(true)
|
|
expect(result!.has('index.js')).toBe(true)
|
|
})
|
|
|
|
it('should resolve git-hosted tarball packages (no type, has tarball)', async () => {
|
|
const pkgId = 'left-pad@https://codeload.github.com/stevemao/left-pad/tar.gz/abc123'
|
|
const key = gitHostedStoreIndexKey(pkgId, { built: true })
|
|
|
|
storeIndex.set(key, createFilesIndex())
|
|
|
|
const resolution = {
|
|
tarball: 'https://codeload.github.com/stevemao/left-pad/tar.gz/abc123',
|
|
} as TarballResolution
|
|
|
|
const result = await readPackageFileMap(resolution, pkgId, defaultOpts())
|
|
|
|
expect(result).toBeDefined()
|
|
expect(result!.has('package.json')).toBe(true)
|
|
expect(result!.has('index.js')).toBe(true)
|
|
})
|
|
|
|
it('should resolve git dependencies with type "git" and return readable file paths', async () => {
|
|
const digest = 'aabbccdd001122'
|
|
const manifestContent = JSON.stringify({
|
|
name: 'left-pad',
|
|
version: '1.3.0',
|
|
license: 'MIT',
|
|
})
|
|
writeCafsFile(storeDir, digest, manifestContent)
|
|
|
|
const pkgId = 'left-pad@git+https://github.com/stevemao/left-pad.git#2fca6157fcca165438e0f9495cf0e5a4e6f71349'
|
|
const filesIndex: PackageFilesIndex = {
|
|
algo: 'sha256',
|
|
files: new Map([
|
|
['package.json', { digest, mode: 0o644, size: 0 }],
|
|
]),
|
|
}
|
|
storeIndex.set(gitHostedStoreIndexKey(pkgId, { built: true }), filesIndex)
|
|
|
|
const resolution: GitResolution = {
|
|
type: 'git',
|
|
repo: 'https://github.com/stevemao/left-pad.git',
|
|
commit: '2fca6157fcca165438e0f9495cf0e5a4e6f71349',
|
|
}
|
|
|
|
const result = await readPackageFileMap(resolution, pkgId, defaultOpts())
|
|
|
|
expect(result).toBeDefined()
|
|
const manifestPath = result!.get('package.json')!
|
|
expect(fs.existsSync(manifestPath)).toBe(true)
|
|
|
|
const parsed = JSON.parse(fs.readFileSync(manifestPath, 'utf8'))
|
|
expect(parsed.name).toBe('left-pad')
|
|
expect(parsed.license).toBe('MIT')
|
|
})
|
|
|
|
it('should throw ENOENT when store index has no entry for a git dependency', async () => {
|
|
const pkgId = 'missing-pkg@git+https://github.com/user/missing-pkg.git#deadbeef'
|
|
|
|
const resolution: GitResolution = {
|
|
type: 'git',
|
|
repo: 'https://github.com/user/missing-pkg.git',
|
|
commit: 'deadbeef',
|
|
}
|
|
|
|
await expect(
|
|
readPackageFileMap(resolution, pkgId, defaultOpts())
|
|
).rejects.toThrow(/package index not found/)
|
|
})
|
|
|
|
it('should return undefined for unknown resolution types', async () => {
|
|
const resolution = { type: 'unknown-type' } as unknown as Resolution
|
|
|
|
const result = await readPackageFileMap(resolution, 'some-pkg@1.0.0', defaultOpts())
|
|
|
|
expect(result).toBeUndefined()
|
|
})
|
|
})
|