mirror of
https://github.com/pnpm/pnpm.git
synced 2026-05-12 10:11:42 -04:00
fix: pin integrity of git-hosted tarballs in lockfile (#11481)
For git-hosted tarballs (`codeload.github.com` / `gitlab.com` / `bitbucket.org`) the fetcher dropped the integrity it computed while downloading, so the lockfile only ever 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 — the lockfile had no hash to compare against.
This pins the SHA-512 SRI of the raw tarball in the lockfile, in the same `sha512-<base64>` form npm-registry tarballs use. The only difference is the source: for npm we pass through `dist.integrity`, for git we compute it locally from the downloaded buffer. Subsequent installs validate the download against that integrity in the worker (`addTarballToStore` → `parseIntegrity` → hash compare), so a tampered tarball fails with `TarballIntegrityError`.
## Why git-hosted stays on `gitHostedStoreIndexKey`
The lockfile pins integrity for security, but the *store key* for git-hosted resolutions stays on `gitHostedStoreIndexKey(pkgId, { built })` rather than collapsing under the integrity-based key. Reason: git-hosted tarballs are post-processed (`preparePackage` / `packlist`), so the cached file set depends on whether build scripts ran during fetch. The integrity-only key would fold the built and not-built variants into a single slot, letting one overwrite the other and serving the wrong content if `ignoreScripts` was toggled between runs. Keeping git-hosted on the existing key shape preserves that dimension; the integrity is still validated on every fresh download.
## How the routing stays clean
The naive way to express "use gitHostedStoreIndexKey for git-hosted, integrity key for npm" is to call `isGitHostedPkgUrl(resolution.tarball)` everywhere a store key is computed — fragile, scattered, and easy to forget when adding new readers (Copilot caught two of those during review). Instead, a typed annotation: `TarballResolution` gets an optional `gitHosted: boolean` field. The git resolver sets it; the lockfile loader (`convertToLockfileObject`) backfills it for entries written by older pnpm versions; `toLockfileResolution` carries it through on serialize. Every consumer reads `resolution.gitHosted` directly. URL detection lives in exactly two places — the resolver and the loader — instead of seven.
## Changes
### Security fix
- `fetching/tarball-fetcher/src/gitHostedTarballFetcher.ts` — return the `integrity` that the inner remote-tarball fetch already computed (was being silently dropped by the destructure).
### Lockfile schema (additive)
- `@pnpm/lockfile.types` and `@pnpm/resolving.resolver-base` — `TarballResolution` gains optional `gitHosted: boolean`.
- `@pnpm/resolving.git-resolver` — sets `gitHosted: true` on every git-hosted tarball it produces.
- `@pnpm/lockfile.fs` (`convertToLockfileObject`) — backfills the field on load for older lockfiles via inlined URL detection.
- `@pnpm/lockfile.utils` (`toLockfileResolution`, `pkgSnapshotToResolution`) — preserve / read the field.
### Store-key consumers (now one-line typed reads, dropped the URL-sniffing dep)
- `installing/package-requester` (`getFilesIndexFilePath`)
- `store/pkg-finder` (`readPackageFileMap`)
- `modules-mounter/daemon` (`createFuseHandlers`)
- `building/after-install` (side-effects-cache lookup + write)
- `store/commands/storeStatus`
- `installing/deps-installer` (agent-mode store-controller wrapper)
### Fetcher routing
- `fetching/pick-fetcher` — `pickFetcher` prefers `resolution.gitHosted`; URL fallback retained for ad-hoc resolutions.
### Tests
- New integrity-validation test in `tarball-fetcher` (mismatched `integrity` on the resolution must throw `TarballIntegrityError`).
- New git-hosted lookup test in `pkg-finder` asserting routing through `gitHostedStoreIndexKey` even when integrity is present.
- New `toLockfileResolution` test asserting `gitHosted: true` flows through serialization.
- `fromRepo.ts` lockfile snapshot updated for the now-pinned integrity + `gitHosted: true`.
- `git-resolver` tests updated to assert `gitHosted: true` in produced resolutions.
This commit is contained in:
20
.changeset/git-tarball-integrity.md
Normal file
20
.changeset/git-tarball-integrity.md
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
"@pnpm/building.after-install": patch
|
||||
"@pnpm/fetching.pick-fetcher": patch
|
||||
"@pnpm/fetching.tarball-fetcher": patch
|
||||
"@pnpm/installing.deps-installer": patch
|
||||
"@pnpm/installing.package-requester": patch
|
||||
"@pnpm/lockfile.fs": patch
|
||||
"@pnpm/lockfile.types": patch
|
||||
"@pnpm/lockfile.utils": patch
|
||||
"@pnpm/modules-mounter.daemon": patch
|
||||
"@pnpm/resolving.git-resolver": patch
|
||||
"@pnpm/resolving.resolver-base": patch
|
||||
"@pnpm/store.commands": patch
|
||||
"@pnpm/store.pkg-finder": 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.
|
||||
@@ -33,7 +33,7 @@ import npa from '@pnpm/npm-package-arg'
|
||||
import { safeReadPackageJsonFromDir } from '@pnpm/pkg-manifest.reader'
|
||||
import type { PackageFilesIndex } from '@pnpm/store.cafs'
|
||||
import { createStoreController } from '@pnpm/store.connection-manager'
|
||||
import { StoreIndex, storeIndexKey } from '@pnpm/store.index'
|
||||
import { pickStoreIndexKey, StoreIndex } from '@pnpm/store.index'
|
||||
import type {
|
||||
DepPath,
|
||||
IgnoredBuilds,
|
||||
@@ -358,9 +358,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 = storeIndexKey(resolution.integrity!.toString(), pkgId)
|
||||
// Match the resolver-supplied pkg.id used by the writer in
|
||||
// @pnpm/installing.package-requester: that's the tarball URL for
|
||||
// git-hosted packages (nonSemverVersion) and `name@version` otherwise.
|
||||
const pkgId = pkgInfo.nonSemverVersion ?? `${pkgInfo.name}@${pkgInfo.version}`
|
||||
if (opts.skipIfHasSideEffectsCache && (resolution.gitHosted || resolution.integrity)) {
|
||||
const filesIndexFile = pickStoreIndexKey(resolution, pkgId, { built: true })
|
||||
const pkgFilesIndex = storeIndex!.get(filesIndexFile) as PackageFilesIndex | undefined
|
||||
if (pkgFilesIndex) {
|
||||
sideEffectsCacheKey = calcDepState(depGraph, depsStateCache, depPath, {
|
||||
@@ -393,9 +396,9 @@ async function _rebuild (
|
||||
unsafePerm: opts.unsafePerm || false,
|
||||
userAgent: opts.userAgent,
|
||||
})
|
||||
if (hasSideEffects && (opts.sideEffectsCacheWrite ?? true) && resolution.integrity) {
|
||||
if (hasSideEffects && (opts.sideEffectsCacheWrite ?? true) && (resolution.gitHosted || resolution.integrity)) {
|
||||
builtDepPaths.add(depPath)
|
||||
const filesIndexFile = storeIndexKey(resolution.integrity!.toString(), pkgId)
|
||||
const filesIndexFile = pickStoreIndexKey(resolution, pkgId, { built: true })
|
||||
try {
|
||||
if (!sideEffectsCacheKey) {
|
||||
sideEffectsCacheKey = calcDepState(depGraph, depsStateCache, depPath, {
|
||||
|
||||
@@ -40,7 +40,12 @@ export async function pickFetcher (
|
||||
if ('tarball' in resolution && resolution.tarball) {
|
||||
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'
|
||||
|
||||
@@ -26,7 +26,7 @@ export interface CreateGitHostedTarballFetcher {
|
||||
export function createGitHostedTarballFetcher (fetchRemoteTarball: FetchFunction, fetcherOpts: CreateGitHostedTarballFetcher): FetchFunction {
|
||||
const fetch = async (cafs: Cafs, resolution: Resolution, opts: FetchOptions) => {
|
||||
const rawFilesIndexFile = `${opts.filesIndexFile}\traw`
|
||||
const { filesMap, manifest, requiresBuild } = await fetchRemoteTarball(cafs, resolution, {
|
||||
const { filesMap, manifest, requiresBuild, integrity } = await fetchRemoteTarball(cafs, resolution, {
|
||||
...opts,
|
||||
filesIndexFile: rawFilesIndexFile,
|
||||
})
|
||||
@@ -42,6 +42,9 @@ export function createGitHostedTarballFetcher (fetchRemoteTarball: FetchFunction
|
||||
filesMap: prepareResult.filesMap,
|
||||
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))
|
||||
|
||||
@@ -506,6 +506,29 @@ 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 () => {
|
||||
// Enable network for this test
|
||||
mockAgent.enableNetConnect(/codeload\.github\.com/)
|
||||
|
||||
process.chdir(temporaryDirectory())
|
||||
|
||||
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 () => {
|
||||
|
||||
@@ -2181,7 +2181,11 @@ async function installFromPnpmRegistry (
|
||||
await fileDownloads
|
||||
const resolution = fetchOpts.pkg.resolution
|
||||
const integrity = resolution?.integrity
|
||||
if (integrity) {
|
||||
// Fall through to the regular store controller for git-hosted tarballs.
|
||||
// Their cached entry lives under gitHostedStoreIndexKey (preserves the
|
||||
// built/not-built dimension), not the integrity-keyed path the agent
|
||||
// uses for npm tarballs. See @pnpm/store.pkg-finder for the rationale.
|
||||
if (integrity && !resolution?.gitHosted) {
|
||||
const filesIndexFile = _storeIndexKey(integrity, fetchOpts.pkg.id)
|
||||
const result = await readPkgFromCafs(
|
||||
{ storeDir: opts.storeDir, verifyStoreIntegrity: false },
|
||||
|
||||
@@ -295,7 +295,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' },
|
||||
},
|
||||
@@ -312,7 +316,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' },
|
||||
},
|
||||
|
||||
@@ -43,7 +43,7 @@ import type {
|
||||
RequestPackageOptions,
|
||||
WantedDependency,
|
||||
} from '@pnpm/store.controller-types'
|
||||
import { gitHostedStoreIndexKey, storeIndexKey } from '@pnpm/store.index'
|
||||
import { pickStoreIndexKey } from '@pnpm/store.index'
|
||||
import type { DependencyManifest, SupportedArchitectures } from '@pnpm/types'
|
||||
import {
|
||||
calcMaxWorkers,
|
||||
@@ -345,28 +345,18 @@ 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: storeIndexKey((opts.pkg.resolution as TarballResolution).integrity!, opts.pkg.id),
|
||||
resolution: opts.pkg.resolution as AtomicResolution,
|
||||
}
|
||||
}
|
||||
let resolution!: AtomicResolution
|
||||
const built = !opts.ignoreScripts
|
||||
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: storeIndexKey((resolution as TarballResolution).integrity!, opts.pkg.id),
|
||||
resolution,
|
||||
}
|
||||
}
|
||||
} else {
|
||||
resolution = opts.pkg.resolution
|
||||
}
|
||||
const filesIndexFile = gitHostedStoreIndexKey(opts.pkg.id, { built: !opts.ignoreScripts })
|
||||
return { filesIndexFile, target, resolution }
|
||||
return {
|
||||
target,
|
||||
filesIndexFile: pickStoreIndexKey(resolution as TarballResolution, opts.pkg.id, { built }),
|
||||
resolution,
|
||||
}
|
||||
}
|
||||
|
||||
function findResolution (resolutionVariants: PlatformAssetResolution[], supportedArchitectures?: SupportedArchitectures): AtomicResolution {
|
||||
|
||||
@@ -10,10 +10,24 @@ import type {
|
||||
PackageSnapshots,
|
||||
ProjectSnapshot,
|
||||
ResolvedDependencies,
|
||||
TarballResolution,
|
||||
} from '@pnpm/lockfile.types'
|
||||
import { DEPENDENCIES_FIELDS, type DepPath } from '@pnpm/types'
|
||||
import { isEmpty, map as _mapValues, omit, pick, pickBy } from 'ramda'
|
||||
|
||||
// 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> = {}
|
||||
@@ -131,6 +145,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),
|
||||
@@ -140,6 +155,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 migratePatchedDependencies (patchedDependencies: Record<string, string | { hash: string }> | undefined): Record<string, string> | undefined {
|
||||
if (!patchedDependencies) return undefined
|
||||
const result: Record<string, string> = {}
|
||||
|
||||
@@ -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/deps.path": "workspace:*",
|
||||
"@pnpm/error": "workspace:*",
|
||||
"@pnpm/fetching.pick-fetcher": "workspace:*",
|
||||
"@pnpm/hooks.types": "workspace:*",
|
||||
"@pnpm/lockfile.types": "workspace:*",
|
||||
"@pnpm/resolving.resolver-base": "workspace:*",
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import url from 'node:url'
|
||||
|
||||
import * as dp from '@pnpm/deps.path'
|
||||
import { isGitHostedPkgUrl } from '@pnpm/fetching.pick-fetcher'
|
||||
import type { PackageSnapshot, TarballResolution } from '@pnpm/lockfile.types'
|
||||
import type { Resolution } from '@pnpm/resolving.resolver-base'
|
||||
import type { Registries } from '@pnpm/types'
|
||||
@@ -17,7 +16,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
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { isGitHostedPkgUrl } from '@pnpm/fetching.pick-fetcher'
|
||||
import type { LockfileResolution } from '@pnpm/lockfile.types'
|
||||
import type { Resolution } from '@pnpm/resolving.resolver-base'
|
||||
import type { Resolution, TarballResolution } from '@pnpm/resolving.resolver-base'
|
||||
import getNpmTarballUrl from 'get-npm-tarball-url'
|
||||
|
||||
export function toLockfileResolution (
|
||||
@@ -16,21 +15,26 @@ export function toLockfileResolution (
|
||||
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. config-dep migrations or
|
||||
// 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,
|
||||
}
|
||||
}, 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 local `file:` tarballs and tarballs
|
||||
// served by git providers (GitHub, GitLab, Bitbucket).
|
||||
if (tarball != null && (tarball.startsWith('file:') || isGitHostedPkgUrl(tarball))) {
|
||||
return {
|
||||
if (tarball != null && (tarball.startsWith('file:') || gitHosted)) {
|
||||
return preservingGitHosted({
|
||||
integrity: resolution['integrity'],
|
||||
tarball,
|
||||
}
|
||||
}, gitHosted)
|
||||
}
|
||||
if (lockfileIncludeTarballUrl === false) {
|
||||
return {
|
||||
@@ -43,16 +47,34 @@ export function toLockfileResolution (
|
||||
const expectedTarball = getNpmTarballUrl(pkg.name, pkg.version, { registry })
|
||||
const actualTarball = tarball!.replaceAll('%2f', '/')
|
||||
if (removeProtocol(expectedTarball) !== removeProtocol(actualTarball)) {
|
||||
return {
|
||||
return preservingGitHosted({
|
||||
integrity: resolution['integrity'],
|
||||
tarball,
|
||||
}
|
||||
}, gitHosted)
|
||||
}
|
||||
return {
|
||||
integrity: resolution['integrity'],
|
||||
}
|
||||
}
|
||||
|
||||
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/fetching.pick-fetcher into the lockfile-utils
|
||||
// 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]
|
||||
}
|
||||
|
||||
@@ -81,5 +81,19 @@ test('keeps git-hosted tarballs when lockfileIncludeTarballUrl is false', () =>
|
||||
)).toEqual({
|
||||
integrity: 'sha512-AAAA',
|
||||
tarball: 'https://codeload.github.com/foo/bar/tar.gz/abcdef',
|
||||
gitHosted: true,
|
||||
})
|
||||
})
|
||||
|
||||
test('records gitHosted on the lockfile entry when set on the resolution', () => {
|
||||
expect(toLockfileResolution(
|
||||
{ name: 'foo', version: '1.0.0' },
|
||||
{ integrity: 'sha512-AAAA', tarball: 'https://codeload.github.com/foo/bar/tar.gz/abcdef', gitHosted: true },
|
||||
REGISTRY,
|
||||
true
|
||||
)).toEqual({
|
||||
integrity: 'sha512-AAAA',
|
||||
tarball: 'https://codeload.github.com/foo/bar/tar.gz/abcdef',
|
||||
gitHosted: true,
|
||||
})
|
||||
})
|
||||
|
||||
@@ -18,9 +18,6 @@
|
||||
{
|
||||
"path": "../../deps/path"
|
||||
},
|
||||
{
|
||||
"path": "../../fetching/pick-fetcher"
|
||||
},
|
||||
{
|
||||
"path": "../../hooks/types"
|
||||
},
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
nameVerFromPkgSnapshot,
|
||||
} from '@pnpm/lockfile.utils'
|
||||
import { getFilePathByModeInCafs, type PackageFilesIndex } from '@pnpm/store.cafs'
|
||||
import { StoreIndex, storeIndexKey } from '@pnpm/store.index'
|
||||
import { pickStoreIndexKey, StoreIndex } from '@pnpm/store.index'
|
||||
import type { DepPath } from '@pnpm/types'
|
||||
import Fuse from 'fuse-native'
|
||||
import schemas from 'hyperdrive-schemas'
|
||||
@@ -176,7 +176,9 @@ export function createFuseHandlersFromLockfile (lockfile: LockfileObject, storeD
|
||||
const pkgSnapshot = lockfile.packages?.[depPath as DepPath]
|
||||
if (pkgSnapshot == null) return undefined
|
||||
const nameVer = nameVerFromPkgSnapshot(depPath, pkgSnapshot)
|
||||
const pkgIndexFilePath = storeIndexKey((pkgSnapshot.resolution as TarballResolution).integrity!, `${nameVer.name}@${nameVer.version}`)
|
||||
const resolution = pkgSnapshot.resolution as TarballResolution
|
||||
const pkgId = nameVer.nonSemverVersion ?? `${nameVer.name}@${nameVer.version}`
|
||||
const pkgIndexFilePath = pickStoreIndexKey(resolution, pkgId, { built: true })
|
||||
const pkgIndex = storeIndex.get(pkgIndexFilePath) as PackageFilesIndex
|
||||
pkgSnapshotCache.set(depPath, {
|
||||
...nameVer,
|
||||
|
||||
6
pnpm-lock.yaml
generated
6
pnpm-lock.yaml
generated
@@ -6954,9 +6954,6 @@ importers:
|
||||
'@pnpm/error':
|
||||
specifier: workspace:*
|
||||
version: link:../../core/error
|
||||
'@pnpm/fetching.pick-fetcher':
|
||||
specifier: workspace:*
|
||||
version: link:../../fetching/pick-fetcher
|
||||
'@pnpm/hooks.types':
|
||||
specifier: workspace:*
|
||||
version: link:../../hooks/types
|
||||
@@ -9159,9 +9156,6 @@ importers:
|
||||
|
||||
store/pkg-finder:
|
||||
dependencies:
|
||||
'@pnpm/deps.path':
|
||||
specifier: workspace:*
|
||||
version: link:../../deps/path
|
||||
'@pnpm/fetching.directory-fetcher':
|
||||
specifier: workspace:*
|
||||
version: link:../../fetching/directory-fetcher
|
||||
|
||||
@@ -66,7 +66,7 @@ export function createGitResolver (
|
||||
const tarball = hosted.tarball?.()
|
||||
|
||||
if (tarball) {
|
||||
resolution = { tarball }
|
||||
resolution = { tarball, gitHosted: true }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -36,6 +36,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',
|
||||
})
|
||||
@@ -53,6 +54,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',
|
||||
})
|
||||
@@ -66,6 +68,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',
|
||||
})
|
||||
@@ -78,6 +81,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',
|
||||
})
|
||||
@@ -96,6 +100,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',
|
||||
})
|
||||
@@ -108,6 +113,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',
|
||||
})
|
||||
@@ -120,6 +126,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',
|
||||
})
|
||||
@@ -132,6 +139,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',
|
||||
})
|
||||
@@ -144,6 +152,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',
|
||||
})
|
||||
@@ -156,6 +165,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',
|
||||
})
|
||||
@@ -168,6 +178,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',
|
||||
})
|
||||
@@ -180,6 +191,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',
|
||||
})
|
||||
@@ -203,6 +215,7 @@ test('resolveFromGit() with sub folder', async () => {
|
||||
resolution: {
|
||||
tarball: `https://codeload.github.com/RexSkz/test-git-subfolder-fetch/tar.gz/${headCommit}`,
|
||||
path: '/packages/simple-react-app',
|
||||
gitHosted: true,
|
||||
},
|
||||
resolvedVia: 'git-repository',
|
||||
})
|
||||
@@ -226,6 +239,7 @@ test('resolveFromGit() with both sub folder and branch', async () => {
|
||||
resolution: {
|
||||
tarball: `https://codeload.github.com/RexSkz/test-git-subfolder-fetch/tar.gz/${betaCommit}`,
|
||||
path: '/packages/simple-react-app',
|
||||
gitHosted: true,
|
||||
},
|
||||
resolvedVia: 'git-repository',
|
||||
})
|
||||
@@ -292,6 +306,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',
|
||||
})
|
||||
@@ -335,6 +350,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',
|
||||
})
|
||||
@@ -362,6 +378,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',
|
||||
})
|
||||
@@ -405,6 +422,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',
|
||||
})
|
||||
@@ -417,6 +435,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',
|
||||
})
|
||||
@@ -429,6 +448,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',
|
||||
})
|
||||
@@ -441,6 +461,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',
|
||||
})
|
||||
@@ -453,6 +474,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',
|
||||
})
|
||||
|
||||
@@ -18,6 +18,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 {
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
import { streamParser } from '@pnpm/logger'
|
||||
import type { PackageFilesIndex } from '@pnpm/store.cafs'
|
||||
import type { TarballResolution } from '@pnpm/store.controller-types'
|
||||
import { gitHostedStoreIndexKey, storeIndexKey } from '@pnpm/store.index'
|
||||
import { pickStoreIndexKey } from '@pnpm/store.index'
|
||||
import { StoreIndex } from '@pnpm/store.index'
|
||||
import type { DepPath } from '@pnpm/types'
|
||||
import dint from 'dint'
|
||||
@@ -43,10 +43,11 @@ 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,
|
||||
resolution,
|
||||
pkgPath: depPath,
|
||||
...nameVerFromPkgSnapshot(depPath, pkgSnapshot),
|
||||
}
|
||||
@@ -54,10 +55,8 @@ export async function storeStatus (maybeOpts: StoreStatusOptions): Promise<strin
|
||||
|
||||
const storeIndex = new StoreIndex(storeDir)
|
||||
try {
|
||||
const modified = await pFilter(pkgs, async ({ id, integrity, depPath, name }) => {
|
||||
const pkgIndexFilePath = integrity
|
||||
? storeIndexKey(integrity, id)
|
||||
: gitHostedStoreIndexKey(id, { built: true })
|
||||
const modified = await pFilter(pkgs, async ({ id, resolution, depPath, name }) => {
|
||||
const pkgIndexFilePath = pickStoreIndexKey(resolution, id, { built: true })
|
||||
const pkgFilesIndex = storeIndex.get(pkgIndexFilePath) as PackageFilesIndex | undefined
|
||||
if (!pkgFilesIndex) {
|
||||
return false
|
||||
|
||||
@@ -66,6 +66,33 @@ export function gitHostedStoreIndexKey (pkgId: string, opts: { built: boolean })
|
||||
return storeIndexKey(pkgId, opts.built ? 'built' : 'not-built')
|
||||
}
|
||||
|
||||
/**
|
||||
* Pick the store index key for a tarball-shaped resolution.
|
||||
*
|
||||
* Git-hosted tarballs (`resolution.gitHosted === true`) are addressed by
|
||||
* `gitHostedStoreIndexKey(pkgId, { built })` — their cached content depends
|
||||
* on whether build scripts ran during fetch (`preparePackage`), so the
|
||||
* `built` dimension is part of the key. The integrity-only key would
|
||||
* collapse the built/not-built variants into one slot.
|
||||
*
|
||||
* Tarballs with integrity that aren't git-hosted are addressed by
|
||||
* `storeIndexKey(integrity, pkgId)`.
|
||||
*
|
||||
* Resolutions that have neither flag fall through to
|
||||
* `gitHostedStoreIndexKey` — these are typically lockfile entries written
|
||||
* by older pnpm versions that lacked integrity.
|
||||
*/
|
||||
export function pickStoreIndexKey (
|
||||
resolution: { gitHosted?: boolean, integrity?: string },
|
||||
pkgId: string,
|
||||
opts: { built: boolean }
|
||||
): string {
|
||||
if (resolution.gitHosted || !resolution.integrity) {
|
||||
return gitHostedStoreIndexKey(pkgId, opts)
|
||||
}
|
||||
return storeIndexKey(resolution.integrity, pkgId)
|
||||
}
|
||||
|
||||
const openInstances = new Set<StoreIndex>()
|
||||
|
||||
/**
|
||||
|
||||
@@ -31,7 +31,6 @@
|
||||
".test": "cross-env NODE_OPTIONS=\"$NODE_OPTIONS --experimental-vm-modules --disable-warning=ExperimentalWarning --disable-warning=DEP0169\" jest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@pnpm/deps.path": "workspace:*",
|
||||
"@pnpm/fetching.directory-fetcher": "workspace:*",
|
||||
"@pnpm/resolving.resolver-base": "workspace:*",
|
||||
"@pnpm/store.cafs": "workspace:*",
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import path from 'node:path'
|
||||
|
||||
import { parse } from '@pnpm/deps.path'
|
||||
import { fetchFromDir } from '@pnpm/fetching.directory-fetcher'
|
||||
import type { Resolution } from '@pnpm/resolving.resolver-base'
|
||||
import type { Resolution, TarballResolution } from '@pnpm/resolving.resolver-base'
|
||||
import { getFilePathByModeInCafs, type PackageFilesIndex } from '@pnpm/store.cafs'
|
||||
import { gitHostedStoreIndexKey, type StoreIndex, storeIndexKey } from '@pnpm/store.index'
|
||||
import { pickStoreIndexKey, type StoreIndex } from '@pnpm/store.index'
|
||||
|
||||
export interface ReadPackageFileMapOptions {
|
||||
storeDir: string
|
||||
@@ -17,10 +16,18 @@ export interface ReadPackageFileMapOptions {
|
||||
* Reads the file index for a package and returns a `Map<string, string>`
|
||||
* mapping filenames to absolute paths on disk.
|
||||
*
|
||||
* Handles three types of package resolutions:
|
||||
* - Directory packages: fetches the file list from the local directory
|
||||
* - Packages with integrity: looks up the index file in the CAFS by integrity hash
|
||||
* - Tarball packages: looks up the index file by package directory name
|
||||
* Picks the store key by resolution shape:
|
||||
* - Directory packages: fetches the file list from the local directory.
|
||||
* - Git-hosted tarballs (codeload.github.com / gitlab.com / bitbucket.org):
|
||||
* keyed by `gitHostedStoreIndexKey(packageId, { built: true })`. The
|
||||
* lockfile pins their integrity for security, but the cached payload
|
||||
* depends on whether build scripts ran during fetch (preparePackage), so
|
||||
* the `built` dimension is part of the key. Folding them under the
|
||||
* integrity-only key would collapse that distinction.
|
||||
* - npm-registry tarballs with integrity: keyed by
|
||||
* `storeIndexKey(integrity, packageId)`.
|
||||
* - Other tarball / git resolutions without integrity: keyed by
|
||||
* `gitHostedStoreIndexKey(packageId, { built: true })`.
|
||||
*
|
||||
* For CAFS packages, the content-addressed digests are resolved to file
|
||||
* paths upfront, so callers get a uniform map regardless of resolution type.
|
||||
@@ -44,19 +51,16 @@ export async function readPackageFileMap (
|
||||
return localInfo.filesMap
|
||||
}
|
||||
|
||||
const isPackageWithIntegrity = 'integrity' in packageResolution
|
||||
|
||||
let pkgIndexFilePath: string
|
||||
if (isPackageWithIntegrity) {
|
||||
const parsedId = parse(packageId)
|
||||
pkgIndexFilePath = storeIndexKey(
|
||||
packageResolution.integrity as string,
|
||||
parsedId.nonSemverVersion ?? `${parsedId.name}@${parsedId.version}`
|
||||
if (
|
||||
(!packageResolution.type && 'tarball' in packageResolution && packageResolution.tarball) ||
|
||||
packageResolution.type === 'git'
|
||||
) {
|
||||
pkgIndexFilePath = pickStoreIndexKey(
|
||||
packageResolution as TarballResolution,
|
||||
packageId,
|
||||
{ built: true }
|
||||
)
|
||||
} else if (!packageResolution.type && 'tarball' in packageResolution && packageResolution.tarball) {
|
||||
pkgIndexFilePath = gitHostedStoreIndexKey(packageId, { built: true })
|
||||
} else if (packageResolution.type === 'git') {
|
||||
pkgIndexFilePath = gitHostedStoreIndexKey(packageId, { built: true })
|
||||
} else {
|
||||
return undefined
|
||||
}
|
||||
|
||||
@@ -64,6 +64,29 @@ describe('readPackageFileMap', () => {
|
||||
expect(result!.has('index.js')).toBe(true)
|
||||
})
|
||||
|
||||
it('should resolve git-hosted tarball packages with integrity by gitHostedStoreIndexKey', async () => {
|
||||
// Git-hosted tarballs are keyed by gitHostedStoreIndexKey to preserve the
|
||||
// built/not-built dimension that the integrity-only key would collapse.
|
||||
// The lockfile still pins integrity for security; the routing is driven
|
||||
// by the `gitHosted` field set by the resolver / lockfile loader.
|
||||
const pkgId = 'https://codeload.github.com/stevemao/left-pad/tar.gz/cafe1234'
|
||||
const key = gitHostedStoreIndexKey(pkgId, { built: true })
|
||||
|
||||
storeIndex.set(key, createFilesIndex())
|
||||
|
||||
const resolution: TarballResolution = {
|
||||
integrity: 'sha512-abc123gitHosted',
|
||||
tarball: pkgId,
|
||||
gitHosted: true,
|
||||
}
|
||||
|
||||
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 })
|
||||
|
||||
@@ -9,9 +9,6 @@
|
||||
"../../__typings__/**/*.d.ts"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../../deps/path"
|
||||
},
|
||||
{
|
||||
"path": "../../fetching/directory-fetcher"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user