mirror of
https://github.com/pnpm/pnpm.git
synced 2026-05-29 11:11:43 -04:00
fix: pin integrity of git-hosted tarballs in lockfile (#11491)
Cherry-pick of #11481 from main, adapted to the v10 layout. For git-hosted tarballs (codeload.github.com / gitlab.com / bitbucket.org) the fetcher dropped the integrity it computed while downloading, so the lockfile only stored the URL. A compromised git host or man-in-the-middle could serve a substituted tarball on subsequent installs and pnpm would install it without lockfile changes. This pins the SHA-512 SRI of the raw tarball in the lockfile in the same sha512-<base64> form npm-registry tarballs use; subsequent installs verify the download against that integrity in the worker. A new optional gitHosted: boolean field is recorded on TarballResolution so every store-key consumer can route by a single typed read instead of re-deriving the routing from the URL. Lockfiles written by older pnpm versions are enriched on load (URL fallback) so the field can be relied on uniformly. 🤖 Cherry-picked by Claude (claude-opus-4-7) on behalf of @zkochan
This commit is contained in:
19
.changeset/git-tarball-integrity.md
Normal file
19
.changeset/git-tarball-integrity.md
Normal file
@@ -0,0 +1,19 @@
|
||||
---
|
||||
"@pnpm/git-resolver": patch
|
||||
"@pnpm/license-scanner": patch
|
||||
"@pnpm/lockfile.fs": patch
|
||||
"@pnpm/lockfile.types": patch
|
||||
"@pnpm/lockfile.utils": patch
|
||||
"@pnpm/package-requester": patch
|
||||
"@pnpm/pick-fetcher": patch
|
||||
"@pnpm/plugin-commands-rebuild": patch
|
||||
"@pnpm/plugin-commands-store": patch
|
||||
"@pnpm/resolve-dependencies": patch
|
||||
"@pnpm/resolver-base": patch
|
||||
"@pnpm/tarball-fetcher": patch
|
||||
"pnpm": patch
|
||||
---
|
||||
|
||||
Pin the integrity of git-hosted tarballs (codeload.github.com, gitlab.com, bitbucket.org) in the lockfile so that subsequent installs detect a tampered or substituted tarball and refuse to install it. Previously the lockfile only stored the tarball URL for git dependencies, so a compromised git host or a man-in-the-middle could serve arbitrary code on later installs without lockfile changes.
|
||||
|
||||
A new `gitHosted: true` field is recorded on git-hosted tarball resolutions in the lockfile, letting every reader/writer route them by a single typed check instead of pattern-matching the tarball URL in each call site. Lockfiles written by older pnpm versions are enriched on load (URL fallback) so the field can be relied on uniformly across the codebase.
|
||||
@@ -351,9 +351,12 @@ async function _rebuild (
|
||||
}
|
||||
const resolution = (pkgSnapshot.resolution as TarballResolution)
|
||||
let sideEffectsCacheKey: string | undefined
|
||||
const pkgId = `${pkgInfo.name}@${pkgInfo.version}`
|
||||
if (opts.skipIfHasSideEffectsCache && resolution.integrity) {
|
||||
const filesIndexFile = getIndexFilePathInCafs(opts.storeDir, resolution.integrity!.toString(), pkgId)
|
||||
// Match the resolver-supplied pkg.id used by package-requester: the
|
||||
// tarball URL for git-hosted packages (nonSemverVersion) and
|
||||
// `name@version` for npm-hosted ones.
|
||||
const pkgId = pkgInfo.nonSemverVersion ?? `${pkgInfo.name}@${pkgInfo.version}`
|
||||
if (opts.skipIfHasSideEffectsCache && (resolution.gitHosted === true || resolution.integrity)) {
|
||||
const filesIndexFile = pickRebuildIndexFilePath(opts.storeDir, resolution, pkgId, opts.virtualStoreDirMaxLength)
|
||||
let pkgFilesIndex: PackageFilesIndex | undefined
|
||||
try {
|
||||
pkgFilesIndex = await loadJsonFile<PackageFilesIndex>(filesIndexFile)
|
||||
@@ -388,9 +391,9 @@ async function _rebuild (
|
||||
shellEmulator: opts.shellEmulator,
|
||||
unsafePerm: opts.unsafePerm || false,
|
||||
})
|
||||
if (hasSideEffects && (opts.sideEffectsCacheWrite ?? true) && resolution.integrity) {
|
||||
if (hasSideEffects && (opts.sideEffectsCacheWrite ?? true) && (resolution.gitHosted === true || resolution.integrity)) {
|
||||
builtDepPaths.add(depPath)
|
||||
const filesIndexFile = getIndexFilePathInCafs(opts.storeDir, resolution.integrity!.toString(), pkgId)
|
||||
const filesIndexFile = pickRebuildIndexFilePath(opts.storeDir, resolution, pkgId, opts.virtualStoreDirMaxLength)
|
||||
try {
|
||||
if (!sideEffectsCacheKey) {
|
||||
sideEffectsCacheKey = calcDepState(depGraph, depsStateCache, depPath, {
|
||||
@@ -471,6 +474,22 @@ async function _rebuild (
|
||||
return { pkgsThatWereRebuilt, ignoredPkgs }
|
||||
}
|
||||
|
||||
// Git-hosted resolutions are post-processed (preparePackage / packlist) on
|
||||
// extraction, so their index file lives in the per-package store path
|
||||
// keyed by built/not-built — not in the integrity-based cafs key, even
|
||||
// though integrity is now pinned in the lockfile for tamper detection.
|
||||
function pickRebuildIndexFilePath (
|
||||
storeDir: string,
|
||||
resolution: TarballResolution,
|
||||
pkgId: string,
|
||||
virtualStoreDirMaxLength: number
|
||||
): string {
|
||||
if (resolution.gitHosted === true) {
|
||||
return path.join(storeDir, dp.depPathToFilename(pkgId, virtualStoreDirMaxLength), 'integrity.json')
|
||||
}
|
||||
return getIndexFilePathInCafs(storeDir, resolution.integrity!.toString(), pkgId)
|
||||
}
|
||||
|
||||
function binDirsInAllParentDirs (pkgRoot: string, lockfileDir: string): string[] {
|
||||
const binDirs: string[] = []
|
||||
let dir = pkgRoot
|
||||
|
||||
@@ -7,7 +7,12 @@ export function pickFetcher (fetcherByHostingType: Partial<Fetchers>, resolution
|
||||
if (resolution.type == null) {
|
||||
if (resolution.tarball.startsWith('file:')) {
|
||||
fetcherType = 'localTarball'
|
||||
} else if (isGitHostedPkgUrl(resolution.tarball)) {
|
||||
} else if (
|
||||
('gitHosted' in resolution && resolution.gitHosted === true) ||
|
||||
// URL fallback for resolutions that didn't go through the resolver or
|
||||
// the lockfile loader (e.g., constructed ad-hoc).
|
||||
isGitHostedPkgUrl(resolution.tarball)
|
||||
) {
|
||||
fetcherType = 'gitHostedTarball'
|
||||
} else {
|
||||
fetcherType = 'remoteTarball'
|
||||
|
||||
@@ -27,7 +27,7 @@ export interface CreateGitHostedTarballFetcher {
|
||||
export function createGitHostedTarballFetcher (fetchRemoteTarball: FetchFunction, fetcherOpts: CreateGitHostedTarballFetcher): FetchFunction {
|
||||
const fetch = async (cafs: Cafs, resolution: Resolution, opts: FetchOptions) => {
|
||||
const tempIndexFile = pathTemp(opts.filesIndexFile)
|
||||
const { filesIndex, manifest, requiresBuild } = await fetchRemoteTarball(cafs, resolution, {
|
||||
const { filesIndex, manifest, requiresBuild, integrity } = await fetchRemoteTarball(cafs, resolution, {
|
||||
...opts,
|
||||
filesIndexFile: tempIndexFile,
|
||||
})
|
||||
@@ -40,6 +40,9 @@ export function createGitHostedTarballFetcher (fetchRemoteTarball: FetchFunction
|
||||
filesIndex: prepareResult.filesIndex,
|
||||
manifest: prepareResult.manifest ?? manifest,
|
||||
requiresBuild,
|
||||
// Propagate the raw tarball integrity so the lockfile pins it and
|
||||
// future installs detect a tampered tarball from the git host.
|
||||
integrity,
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
assert(util.types.isNativeError(err))
|
||||
|
||||
@@ -468,6 +468,26 @@ test('take only the files included in the package, when fetching a git-hosted pa
|
||||
'dist/index.js',
|
||||
'package.json',
|
||||
])
|
||||
// The fetcher must surface the integrity of the downloaded git tarball so
|
||||
// that the lockfile can pin it (CVE: malicious codeload.github.com responses).
|
||||
expect(result.integrity).toMatch(/^sha512-/)
|
||||
})
|
||||
|
||||
test('verify integrity of git-hosted tarball against the resolution', async () => {
|
||||
process.chdir(tempy.directory())
|
||||
|
||||
const resolution = {
|
||||
tarball: 'https://codeload.github.com/pnpm-e2e/pkg-with-ignored-files/tar.gz/958d6d487217512bb154d02836e9b5b922a600d8',
|
||||
// A well-formed sha512 SRI that doesn't match the actual tarball — should
|
||||
// surface as TarballIntegrityError when the buffer is verified.
|
||||
integrity: 'sha512-MRqvs50psUtGELoeBcJwDUi7lT6RUXBzTHsU3U701V/DIouBQSZo+tx5xSXDJLEcItepyZPjIncx8Xy4qPFlKw==',
|
||||
}
|
||||
|
||||
await expect(fetch.gitHostedTarball(cafs, resolution, {
|
||||
filesIndexFile,
|
||||
lockfileDir: process.cwd(),
|
||||
pkg,
|
||||
})).rejects.toThrow(TarballIntegrityError)
|
||||
})
|
||||
|
||||
test('fail when extracting a broken tarball', async () => {
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
type LockfileFileProjectResolvedDependencies,
|
||||
type LockfilePackageInfo,
|
||||
type PackageSnapshots,
|
||||
type TarballResolution,
|
||||
} from '@pnpm/lockfile.types'
|
||||
import { type DepPath, DEPENDENCIES_FIELDS } from '@pnpm/types'
|
||||
import isEmpty from 'ramda/src/isEmpty'
|
||||
@@ -18,6 +19,19 @@ import pickBy from 'ramda/src/pickBy'
|
||||
import pick from 'ramda/src/pick'
|
||||
import { LOCKFILE_VERSION } from '@pnpm/constants'
|
||||
|
||||
// Minimal duplicate of `isGitHostedPkgUrl` from `@pnpm/fetching.pick-fetcher`,
|
||||
// inlined to avoid pulling the fetcher dep into the lockfile I/O layer. Used
|
||||
// to enrich entries written by older pnpm versions (which didn't record the
|
||||
// `gitHosted` field on TarballResolution) so every downstream reader can rely
|
||||
// on the field directly.
|
||||
function isGitHostedTarballUrl (url: string): boolean {
|
||||
return (
|
||||
url.startsWith('https://codeload.github.com/') ||
|
||||
url.startsWith('https://bitbucket.org/') ||
|
||||
url.startsWith('https://gitlab.com/')
|
||||
) && url.includes('tar.gz')
|
||||
}
|
||||
|
||||
export function convertToLockfileFile (lockfile: LockfileObject): LockfileFile {
|
||||
const packages: Record<string, LockfilePackageInfo> = {}
|
||||
const snapshots: Record<string, LockfilePackageSnapshot> = {}
|
||||
@@ -151,6 +165,7 @@ export function convertToLockfileObject (lockfile: LockfileFile): LockfileObject
|
||||
for (const [depPath, pkg] of Object.entries(lockfile.snapshots ?? {})) {
|
||||
const pkgId = removeSuffix(depPath)
|
||||
packages[depPath as DepPath] = Object.assign(pkg, lockfile.packages?.[pkgId])
|
||||
enrichGitHostedFlag(packages[depPath as DepPath]?.resolution as TarballResolution | undefined)
|
||||
}
|
||||
return {
|
||||
...omit(['snapshots'], rest),
|
||||
@@ -159,6 +174,18 @@ export function convertToLockfileObject (lockfile: LockfileFile): LockfileObject
|
||||
}
|
||||
}
|
||||
|
||||
// Backfill the `gitHosted` flag for tarball resolutions written by older
|
||||
// pnpm versions. Doing it once at load time lets every downstream reader
|
||||
// rely on the typed field instead of repeating URL prefix matches.
|
||||
function enrichGitHostedFlag (resolution: TarballResolution | undefined): void {
|
||||
if (resolution == null) return
|
||||
if (resolution.type !== undefined) return
|
||||
if (resolution.gitHosted != null) return
|
||||
if (resolution.tarball != null && isGitHostedTarballUrl(resolution.tarball)) {
|
||||
resolution.gitHosted = true
|
||||
}
|
||||
}
|
||||
|
||||
function convertProjectSnapshotToInlineSpecifiersFormat (
|
||||
projectSnapshot: ProjectSnapshot
|
||||
): LockfileFileProjectSnapshot {
|
||||
|
||||
@@ -91,6 +91,19 @@ export interface TarballResolution {
|
||||
tarball: string
|
||||
integrity?: string
|
||||
path?: string
|
||||
/**
|
||||
* True for tarballs sourced from a git host (codeload.github.com /
|
||||
* gitlab.com / bitbucket.org). Such tarballs need preparation
|
||||
* (preparePackage / packlist) on extraction, and their cached content
|
||||
* depends on whether build scripts ran, so they're addressed by
|
||||
* gitHostedStoreIndexKey rather than the integrity-based key.
|
||||
*
|
||||
* The git resolver sets this when it produces the resolution; the
|
||||
* lockfile loader sets it on entries whose URL matches a known git host
|
||||
* for backward compatibility with lockfiles written before this field
|
||||
* existed.
|
||||
*/
|
||||
gitHosted?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -35,7 +35,6 @@
|
||||
"dependencies": {
|
||||
"@pnpm/dependency-path": "workspace:*",
|
||||
"@pnpm/lockfile.types": "workspace:*",
|
||||
"@pnpm/pick-fetcher": "workspace:*",
|
||||
"@pnpm/resolver-base": "workspace:*",
|
||||
"@pnpm/types": "workspace:*",
|
||||
"get-npm-tarball-url": "catalog:",
|
||||
|
||||
@@ -3,7 +3,6 @@ import { type PackageSnapshot, type TarballResolution } from '@pnpm/lockfile.typ
|
||||
import { type Resolution } from '@pnpm/resolver-base'
|
||||
import { type Registries } from '@pnpm/types'
|
||||
import getNpmTarballUrl from 'get-npm-tarball-url'
|
||||
import { isGitHostedPkgUrl } from '@pnpm/pick-fetcher'
|
||||
import { nameVerFromPkgSnapshot } from './nameVerFromPkgSnapshot.js'
|
||||
|
||||
export function pkgSnapshotToResolution (
|
||||
@@ -14,7 +13,7 @@ export function pkgSnapshotToResolution (
|
||||
if (
|
||||
Boolean((pkgSnapshot.resolution as TarballResolution).type) ||
|
||||
(pkgSnapshot.resolution as TarballResolution).tarball?.startsWith('file:') ||
|
||||
isGitHostedPkgUrl((pkgSnapshot.resolution as TarballResolution).tarball ?? '')
|
||||
(pkgSnapshot.resolution as TarballResolution).gitHosted === true
|
||||
) {
|
||||
return pkgSnapshot.resolution as Resolution
|
||||
}
|
||||
|
||||
@@ -9,9 +9,6 @@
|
||||
"../../__typings__/**/*.d.ts"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../../fetching/pick-fetcher"
|
||||
},
|
||||
{
|
||||
"path": "../../packages/dependency-path"
|
||||
},
|
||||
|
||||
@@ -255,7 +255,11 @@ test('re-adding a git repo with a different tag', async () => {
|
||||
expect(lockfile.packages).toEqual(
|
||||
{
|
||||
'is-negative@https://codeload.github.com/kevva/is-negative/tar.gz/163360a8d3ae6bee9524541043197ff356f8ed99': {
|
||||
resolution: { tarball: 'https://codeload.github.com/kevva/is-negative/tar.gz/163360a8d3ae6bee9524541043197ff356f8ed99' },
|
||||
resolution: {
|
||||
tarball: 'https://codeload.github.com/kevva/is-negative/tar.gz/163360a8d3ae6bee9524541043197ff356f8ed99',
|
||||
integrity: expect.stringMatching(/^sha512-/),
|
||||
gitHosted: true,
|
||||
},
|
||||
version: '1.0.0',
|
||||
engines: { node: '>=0.10.0' },
|
||||
},
|
||||
@@ -272,7 +276,11 @@ test('re-adding a git repo with a different tag', async () => {
|
||||
expect(lockfile.packages).toEqual(
|
||||
{
|
||||
'is-negative@https://codeload.github.com/kevva/is-negative/tar.gz/f7dec4d66a5a56719e49b9f94a24d73f924ddeb3': {
|
||||
resolution: { tarball: 'https://codeload.github.com/kevva/is-negative/tar.gz/f7dec4d66a5a56719e49b9f94a24d73f924ddeb3' },
|
||||
resolution: {
|
||||
tarball: 'https://codeload.github.com/kevva/is-negative/tar.gz/f7dec4d66a5a56719e49b9f94a24d73f924ddeb3',
|
||||
integrity: expect.stringMatching(/^sha512-/),
|
||||
gitHosted: true,
|
||||
},
|
||||
version: '1.0.1',
|
||||
engines: { node: '>=0.10.0' },
|
||||
},
|
||||
|
||||
@@ -401,25 +401,27 @@ function getFilesIndexFilePath (
|
||||
): GetFilesIndexFilePathResult {
|
||||
const targetRelative = depPathToFilename(opts.pkg.id, ctx.virtualStoreDirMaxLength)
|
||||
const target = path.join(ctx.storeDir, targetRelative)
|
||||
if ((opts.pkg.resolution as TarballResolution).integrity) {
|
||||
return {
|
||||
target,
|
||||
filesIndexFile: ctx.getIndexFilePathInCafs((opts.pkg.resolution as TarballResolution).integrity!, opts.pkg.id),
|
||||
resolution: opts.pkg.resolution as AtomicResolution,
|
||||
}
|
||||
}
|
||||
let resolution!: AtomicResolution
|
||||
let resolution: AtomicResolution
|
||||
if (opts.pkg.resolution.type === 'variations') {
|
||||
resolution = findResolution(opts.pkg.resolution.variants, opts.supportedArchitectures)
|
||||
if ((resolution as TarballResolution).integrity) {
|
||||
return {
|
||||
target,
|
||||
filesIndexFile: ctx.getIndexFilePathInCafs((resolution as TarballResolution).integrity!, opts.pkg.id),
|
||||
resolution,
|
||||
}
|
||||
}
|
||||
} else {
|
||||
resolution = opts.pkg.resolution
|
||||
resolution = opts.pkg.resolution as AtomicResolution
|
||||
}
|
||||
// Git-hosted tarballs are post-processed (preparePackage / packlist) on
|
||||
// extraction, and their cached content depends on whether build scripts
|
||||
// ran, so they must stay on the per-package store path keyed by built/
|
||||
// not-built — never on the integrity-based cafs key, even though
|
||||
// integrity is now pinned in the lockfile for tamper detection.
|
||||
if ((resolution as TarballResolution).gitHosted === true) {
|
||||
const filesIndexFile = path.join(target, opts.ignoreScripts ? 'integrity-not-built.json' : 'integrity.json')
|
||||
return { filesIndexFile, target, resolution }
|
||||
}
|
||||
if ((resolution as TarballResolution).integrity) {
|
||||
return {
|
||||
target,
|
||||
filesIndexFile: ctx.getIndexFilePathInCafs((resolution as TarballResolution).integrity!, opts.pkg.id),
|
||||
resolution,
|
||||
}
|
||||
}
|
||||
const filesIndexFile = path.join(target, opts.ignoreScripts ? 'integrity-not-built.json' : 'integrity.json')
|
||||
return { filesIndexFile, target, resolution }
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
type PackageSnapshot,
|
||||
pruneSharedLockfile,
|
||||
} from '@pnpm/lockfile.pruner'
|
||||
import { type Resolution } from '@pnpm/resolver-base'
|
||||
import { type Resolution, type TarballResolution } from '@pnpm/resolver-base'
|
||||
import { type DepPath, type Registries } from '@pnpm/types'
|
||||
import * as dp from '@pnpm/dependency-path'
|
||||
import getNpmTarballUrl from 'get-npm-tarball-url'
|
||||
@@ -189,11 +189,27 @@ function toLockfileResolution (
|
||||
if (resolution.type !== undefined || !resolution['integrity']) {
|
||||
return resolution as LockfileResolution
|
||||
}
|
||||
const tarball = resolution['tarball'] as string | undefined
|
||||
// Honor the resolver-supplied flag, with a URL fallback for resolutions
|
||||
// that didn't go through the git resolver (e.g. legacy lockfiles read by
|
||||
// callers that don't enrich the field).
|
||||
const gitHosted = (resolution as TarballResolution).gitHosted === true ||
|
||||
(tarball != null && isGitHostedTarballUrl(tarball))
|
||||
if (lockfileIncludeTarballUrl) {
|
||||
return {
|
||||
return preservingGitHosted({
|
||||
integrity: resolution['integrity'],
|
||||
tarball: resolution['tarball'],
|
||||
}
|
||||
tarball,
|
||||
}, gitHosted)
|
||||
}
|
||||
// Tarball URLs that cannot be reconstructed from the package name, version,
|
||||
// and registry must always stay in the lockfile, otherwise the package can
|
||||
// no longer be re-fetched. This covers tarballs served by git providers
|
||||
// (GitHub, GitLab, Bitbucket).
|
||||
if (tarball != null && gitHosted) {
|
||||
return preservingGitHosted({
|
||||
integrity: resolution['integrity'],
|
||||
tarball,
|
||||
}, gitHosted)
|
||||
}
|
||||
// Sometimes packages are hosted under non-standard tarball URLs.
|
||||
// For instance, when they are hosted on npm Enterprise. See https://github.com/pnpm/pnpm/issues/867
|
||||
@@ -211,6 +227,24 @@ function toLockfileResolution (
|
||||
}
|
||||
}
|
||||
|
||||
function preservingGitHosted<T extends { tarball?: string, integrity: string }> (
|
||||
resolution: T,
|
||||
gitHosted: boolean
|
||||
): T & { gitHosted?: boolean } {
|
||||
return gitHosted ? { ...resolution, gitHosted: true } : resolution
|
||||
}
|
||||
|
||||
// Inlined to avoid pulling @pnpm/pick-fetcher into this dep graph.
|
||||
// Used as a fallback when callers haven't pre-set the `gitHosted` field
|
||||
// on TarballResolution.
|
||||
function isGitHostedTarballUrl (url: string): boolean {
|
||||
return (
|
||||
url.startsWith('https://codeload.github.com/') ||
|
||||
url.startsWith('https://bitbucket.org/') ||
|
||||
url.startsWith('https://gitlab.com/')
|
||||
) && url.includes('tar.gz')
|
||||
}
|
||||
|
||||
function removeProtocol (url: string): string {
|
||||
return url.split('://')[1]
|
||||
}
|
||||
|
||||
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
@@ -4053,9 +4053,6 @@ importers:
|
||||
'@pnpm/lockfile.types':
|
||||
specifier: workspace:*
|
||||
version: link:../types
|
||||
'@pnpm/pick-fetcher':
|
||||
specifier: workspace:*
|
||||
version: link:../../fetching/pick-fetcher
|
||||
'@pnpm/resolver-base':
|
||||
specifier: workspace:*
|
||||
version: link:../../resolving/resolver-base
|
||||
|
||||
@@ -42,7 +42,7 @@ export function createGitResolver (
|
||||
const tarball = hosted.tarball?.()
|
||||
|
||||
if (tarball) {
|
||||
resolution = { tarball }
|
||||
resolution = { tarball, gitHosted: true }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ test('resolveFromGit() with commit', async () => {
|
||||
normalizedBareSpecifier: 'github:zkochan/is-negative#163360a8d3ae6bee9524541043197ff356f8ed99',
|
||||
resolution: {
|
||||
tarball: 'https://codeload.github.com/zkochan/is-negative/tar.gz/163360a8d3ae6bee9524541043197ff356f8ed99',
|
||||
gitHosted: true,
|
||||
},
|
||||
resolvedVia: 'git-repository',
|
||||
})
|
||||
@@ -39,6 +40,7 @@ test('resolveFromGit() with no commit', async () => {
|
||||
normalizedBareSpecifier: 'github:zkochan/is-negative',
|
||||
resolution: {
|
||||
tarball: 'https://codeload.github.com/zkochan/is-negative/tar.gz/1d7e288222b53a0cab90a331f1865220ec29560c',
|
||||
gitHosted: true,
|
||||
},
|
||||
resolvedVia: 'git-repository',
|
||||
})
|
||||
@@ -52,6 +54,7 @@ test('resolveFromGit() with no commit, when main branch is not master', async ()
|
||||
normalizedBareSpecifier: 'github:zoli-forks/cmd-shim',
|
||||
resolution: {
|
||||
tarball: 'https://codeload.github.com/zoli-forks/cmd-shim/tar.gz/a00a83a1593edb6e395d3ce41f2ef70edf7e2cf5',
|
||||
gitHosted: true,
|
||||
},
|
||||
resolvedVia: 'git-repository',
|
||||
})
|
||||
@@ -64,6 +67,7 @@ test('resolveFromGit() with partial commit', async () => {
|
||||
normalizedBareSpecifier: 'github:zoli-forks/cmd-shim#a00a83a',
|
||||
resolution: {
|
||||
tarball: 'https://codeload.github.com/zoli-forks/cmd-shim/tar.gz/a00a83a1593edb6e395d3ce41f2ef70edf7e2cf5',
|
||||
gitHosted: true,
|
||||
},
|
||||
resolvedVia: 'git-repository',
|
||||
})
|
||||
@@ -82,6 +86,7 @@ test('resolveFromGit() with branch', async () => {
|
||||
normalizedBareSpecifier: 'github:zkochan/is-negative#canary',
|
||||
resolution: {
|
||||
tarball: 'https://codeload.github.com/zkochan/is-negative/tar.gz/4c39fbc124cd4944ee51cb082ad49320fab58121',
|
||||
gitHosted: true,
|
||||
},
|
||||
resolvedVia: 'git-repository',
|
||||
})
|
||||
@@ -94,6 +99,7 @@ test('resolveFromGit() with branch relative to refs', async () => {
|
||||
normalizedBareSpecifier: 'github:zkochan/is-negative#heads/canary',
|
||||
resolution: {
|
||||
tarball: 'https://codeload.github.com/zkochan/is-negative/tar.gz/4c39fbc124cd4944ee51cb082ad49320fab58121',
|
||||
gitHosted: true,
|
||||
},
|
||||
resolvedVia: 'git-repository',
|
||||
})
|
||||
@@ -106,6 +112,7 @@ test('resolveFromGit() with tag', async () => {
|
||||
normalizedBareSpecifier: 'github:zkochan/is-negative#2.0.1',
|
||||
resolution: {
|
||||
tarball: 'https://codeload.github.com/zkochan/is-negative/tar.gz/6dcce91c268805d456b8a575b67d7febc7ae2933',
|
||||
gitHosted: true,
|
||||
},
|
||||
resolvedVia: 'git-repository',
|
||||
})
|
||||
@@ -118,6 +125,7 @@ test.skip('resolveFromGit() with tag (v-prefixed tag)', async () => {
|
||||
normalizedBareSpecifier: 'github:andreineculau/npm-publish-git#v0.0.7',
|
||||
resolution: {
|
||||
tarball: 'https://codeload.github.com/andreineculau/npm-publish-git/tar.gz/a2f8d94562884e9529cb12c0818312ac87ab7f0b',
|
||||
gitHosted: true,
|
||||
},
|
||||
resolvedVia: 'git-repository',
|
||||
})
|
||||
@@ -130,6 +138,7 @@ test('resolveFromGit() with strict semver', async () => {
|
||||
normalizedBareSpecifier: 'github:zkochan/is-negative#semver:1.0.0',
|
||||
resolution: {
|
||||
tarball: 'https://codeload.github.com/zkochan/is-negative/tar.gz/163360a8d3ae6bee9524541043197ff356f8ed99',
|
||||
gitHosted: true,
|
||||
},
|
||||
resolvedVia: 'git-repository',
|
||||
})
|
||||
@@ -142,6 +151,7 @@ test.skip('resolveFromGit() with strict semver (v-prefixed tag)', async () => {
|
||||
normalizedBareSpecifier: 'github:andreineculau/npm-publish-git#semver:v0.0.7',
|
||||
resolution: {
|
||||
tarball: 'https://codeload.github.com/andreineculau/npm-publish-git/tar.gz/a2f8d94562884e9529cb12c0818312ac87ab7f0b',
|
||||
gitHosted: true,
|
||||
},
|
||||
resolvedVia: 'git-repository',
|
||||
})
|
||||
@@ -154,6 +164,7 @@ test('resolveFromGit() with range semver', async () => {
|
||||
normalizedBareSpecifier: 'github:zkochan/is-negative#semver:^1.0.0',
|
||||
resolution: {
|
||||
tarball: 'https://codeload.github.com/zkochan/is-negative/tar.gz/f7dec4d66a5a56719e49b9f94a24d73f924ddeb3',
|
||||
gitHosted: true,
|
||||
},
|
||||
resolvedVia: 'git-repository',
|
||||
})
|
||||
@@ -166,6 +177,7 @@ test.skip('resolveFromGit() with range semver (v-prefixed tag)', async () => {
|
||||
normalizedBareSpecifier: 'github:andreineculau/npm-publish-git#semver:<=v0.0.7',
|
||||
resolution: {
|
||||
tarball: 'https://codeload.github.com/andreineculau/npm-publish-git/tar.gz/a2f8d94562884e9529cb12c0818312ac87ab7f0b',
|
||||
gitHosted: true,
|
||||
},
|
||||
resolvedVia: 'git-repository',
|
||||
})
|
||||
@@ -179,6 +191,7 @@ test('resolveFromGit() with sub folder', async () => {
|
||||
resolution: {
|
||||
tarball: 'https://codeload.github.com/RexSkz/test-git-subfolder-fetch/tar.gz/2b42a57a945f19f8ffab8ecbd2021fdc2c58ee22',
|
||||
path: '/packages/simple-react-app',
|
||||
gitHosted: true,
|
||||
},
|
||||
resolvedVia: 'git-repository',
|
||||
})
|
||||
@@ -192,6 +205,7 @@ test('resolveFromGit() with both sub folder and branch', async () => {
|
||||
resolution: {
|
||||
tarball: 'https://codeload.github.com/RexSkz/test-git-subfolder-fetch/tar.gz/777e8a3e78cc89bbf41fb3fd9f6cf922d5463313',
|
||||
path: '/packages/simple-react-app',
|
||||
gitHosted: true,
|
||||
},
|
||||
resolvedVia: 'git-repository',
|
||||
})
|
||||
@@ -258,6 +272,7 @@ test.skip('resolveFromGit() bitbucket with commit', async () => {
|
||||
normalizedBareSpecifier: 'bitbucket:pnpmjs/git-resolver#988c61e11dc8d9ca0b5580cb15291951812549dc',
|
||||
resolution: {
|
||||
tarball: 'https://bitbucket.org/pnpmjs/git-resolver/get/988c61e11dc8d9ca0b5580cb15291951812549dc.tar.gz',
|
||||
gitHosted: true,
|
||||
},
|
||||
resolvedVia: 'git-repository',
|
||||
})
|
||||
@@ -301,6 +316,7 @@ test.skip('resolveFromGit() bitbucket with tag', async () => {
|
||||
normalizedBareSpecifier: 'bitbucket:pnpmjs/git-resolver#0.3.4',
|
||||
resolution: {
|
||||
tarball: 'https://bitbucket.org/pnpmjs/git-resolver/get/87cf6a67064d2ce56e8cd20624769a5512b83ff9.tar.gz',
|
||||
gitHosted: true,
|
||||
},
|
||||
resolvedVia: 'git-repository',
|
||||
})
|
||||
@@ -328,6 +344,7 @@ test.skip('resolveFromGit() gitlab with commit', async () => {
|
||||
normalizedBareSpecifier: 'gitlab:pnpm/git-resolver#988c61e11dc8d9ca0b5580cb15291951812549dc',
|
||||
resolution: {
|
||||
tarball: 'https://gitlab.com/api/v4/projects/pnpm%2Fgit-resolver/repository/archive.tar.gz?ref=988c61e11dc8d9ca0b5580cb15291951812549dc',
|
||||
gitHosted: true,
|
||||
},
|
||||
resolvedVia: 'git-repository',
|
||||
})
|
||||
@@ -371,6 +388,7 @@ test.skip('resolveFromGit() gitlab with tag', async () => {
|
||||
normalizedBareSpecifier: 'gitlab:pnpm/git-resolver#0.3.4',
|
||||
resolution: {
|
||||
tarball: 'https://gitlab.com/api/v4/projects/pnpm%2Fgit-resolver/repository/archive.tar.gz?ref=87cf6a67064d2ce56e8cd20624769a5512b83ff9',
|
||||
gitHosted: true,
|
||||
},
|
||||
resolvedVia: 'git-repository',
|
||||
})
|
||||
@@ -383,6 +401,7 @@ test('resolveFromGit() normalizes full url', async () => {
|
||||
normalizedBareSpecifier: 'github:zkochan/is-negative#2.0.1',
|
||||
resolution: {
|
||||
tarball: 'https://codeload.github.com/zkochan/is-negative/tar.gz/6dcce91c268805d456b8a575b67d7febc7ae2933',
|
||||
gitHosted: true,
|
||||
},
|
||||
resolvedVia: 'git-repository',
|
||||
})
|
||||
@@ -395,6 +414,7 @@ test('resolveFromGit() normalizes full url with port', async () => {
|
||||
normalizedBareSpecifier: 'github:zkochan/is-negative#2.0.1',
|
||||
resolution: {
|
||||
tarball: 'https://codeload.github.com/zkochan/is-negative/tar.gz/6dcce91c268805d456b8a575b67d7febc7ae2933',
|
||||
gitHosted: true,
|
||||
},
|
||||
resolvedVia: 'git-repository',
|
||||
})
|
||||
@@ -407,6 +427,7 @@ test('resolveFromGit() normalizes full url (alternative form)', async () => {
|
||||
normalizedBareSpecifier: 'github:zkochan/is-negative#2.0.1',
|
||||
resolution: {
|
||||
tarball: 'https://codeload.github.com/zkochan/is-negative/tar.gz/6dcce91c268805d456b8a575b67d7febc7ae2933',
|
||||
gitHosted: true,
|
||||
},
|
||||
resolvedVia: 'git-repository',
|
||||
})
|
||||
@@ -419,6 +440,7 @@ test('resolveFromGit() normalizes full url (alternative form 2)', async () => {
|
||||
normalizedBareSpecifier: 'github:zkochan/is-negative#2.0.1',
|
||||
resolution: {
|
||||
tarball: 'https://codeload.github.com/zkochan/is-negative/tar.gz/6dcce91c268805d456b8a575b67d7febc7ae2933',
|
||||
gitHosted: true,
|
||||
},
|
||||
resolvedVia: 'git-repository',
|
||||
})
|
||||
|
||||
@@ -17,6 +17,14 @@ export interface TarballResolution {
|
||||
tarball: string
|
||||
integrity?: string
|
||||
path?: string
|
||||
/**
|
||||
* True for tarballs sourced from a git host (codeload.github.com /
|
||||
* gitlab.com / bitbucket.org). Such tarballs need preparation
|
||||
* (preparePackage / packlist) on extraction, and their cached content
|
||||
* depends on whether build scripts ran, so they're addressed by
|
||||
* gitHostedStoreIndexKey rather than the integrity-based key.
|
||||
*/
|
||||
gitHosted?: boolean
|
||||
}
|
||||
|
||||
export interface BinaryResolution {
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
import loadJsonFile from 'load-json-file'
|
||||
import { PnpmError } from '@pnpm/error'
|
||||
import { type LicensePackage } from './licenses.js'
|
||||
import { type DirectoryResolution, type PackageSnapshot, pkgSnapshotToResolution, type Resolution } from '@pnpm/lockfile.utils'
|
||||
import { type DirectoryResolution, type PackageSnapshot, pkgSnapshotToResolution, type Resolution, type TarballResolution } from '@pnpm/lockfile.utils'
|
||||
import { fetchFromDir } from '@pnpm/directory-fetcher'
|
||||
|
||||
const limitPkgReads = pLimit(4)
|
||||
@@ -250,7 +250,11 @@ export async function readPackageIndexFile (
|
||||
}
|
||||
}
|
||||
|
||||
const isPackageWithIntegrity = 'integrity' in packageResolution
|
||||
// Git-hosted resolutions are post-processed (preparePackage / packlist)
|
||||
// on extraction, so their index file lives in the per-package store path
|
||||
// even though integrity is now pinned in the lockfile.
|
||||
const isGitHosted = !packageResolution.type && (packageResolution as TarballResolution).gitHosted === true
|
||||
const isPackageWithIntegrity = !isGitHosted && 'integrity' in packageResolution
|
||||
|
||||
let pkgIndexFilePath
|
||||
if (isPackageWithIntegrity) {
|
||||
|
||||
@@ -39,17 +39,19 @@ export async function storeStatus (maybeOpts: StoreStatusOptions): Promise<strin
|
||||
.filter(([depPath]) => !skipped.has(depPath))
|
||||
.map(([depPath, pkgSnapshot]) => {
|
||||
const id = packageIdFromSnapshot(depPath, pkgSnapshot)
|
||||
const resolution = pkgSnapshot.resolution as TarballResolution
|
||||
return {
|
||||
depPath,
|
||||
id,
|
||||
integrity: (pkgSnapshot.resolution as TarballResolution).integrity,
|
||||
integrity: resolution.integrity,
|
||||
gitHosted: resolution.gitHosted === true,
|
||||
pkgPath: depPath,
|
||||
...nameVerFromPkgSnapshot(depPath, pkgSnapshot),
|
||||
}
|
||||
})
|
||||
|
||||
const modified = await pFilter(pkgs, async ({ id, integrity, depPath, name }) => {
|
||||
const pkgIndexFilePath = integrity
|
||||
const modified = await pFilter(pkgs, async ({ id, integrity, gitHosted, depPath, name }) => {
|
||||
const pkgIndexFilePath = !gitHosted && integrity
|
||||
? getIndexFilePathInCafs(storeDir, integrity, id)
|
||||
: path.join(storeDir, dp.depPathToFilename(id, maybeOpts.virtualStoreDirMaxLength), 'integrity.json')
|
||||
const { files } = await loadJsonFile<PackageFilesIndex>(pkgIndexFilePath)
|
||||
|
||||
Reference in New Issue
Block a user