Files
pnpm/store/pkg-finder/test/readPackageFileMap.test.ts
Allan Kimmer Jensen bcc88a1239 fix(sbom): resolve licenses for git-sourced dependencies (#11310)
* fix(sbom): resolve licenses for git-sourced dependencies

`readPackageFileMap` did not handle `type: 'git'` resolutions, causing
`pnpm sbom` to emit NOASSERTION and `pnpm licenses` to throw for any
dependency installed from a git URL.

Closes #11260

* fix: add missing store.cafs devDep, test tsconfigs, and size field

- Add @pnpm/store.cafs devDependency and tsconfig reference to
  license-scanner so CI typecheck resolves the PackageFilesIndex import
- Add test/tsconfig.json to pkg-finder so CI typechecks the new tests
- Add required `size` field to PackageFileInfo test fixtures

* fix: replace spellcheck-failing test strings

* fix: use spellcheck-safe integrity string in test

* style: fix import sort in pkg-finder test

* fix(sbom): use packageIdFromSnapshot to match store index keys

The SBOM used `snapshot.id ?? depPath` as the package ID, which
includes the package name prefix (e.g. `left-pad@git+https://...`).
The store index stores git packages under just the git URL without
the name prefix. Use `packageIdFromSnapshot` which strips the prefix,
matching how the licenses command already does it.

Also fixes test store keys to match the real installer layout so the
mismatch would have been caught by tests.

* refactor: move git resolution check after tarball check

Tarball resolutions are more common than type: 'git', so check them
first. Per review feedback from @zkochan.
2026-04-20 14:29:35 +02:00

140 lines
4.5 KiB
TypeScript

import fs from 'node:fs'
import os from 'node:os'
import path from 'node:path'
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 (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()
})
})