fix: verify the name and version of the package in the store (#3224)

* fix: verify the name and version of the package in the store

ref #3202

* fix: autofix broken lockfile

* docs: add changesets
This commit is contained in:
Zoltan Kochan
2021-03-08 21:22:33 +02:00
committed by Zoltan Kochan
parent 96e10ff26f
commit 8d1dfa89c6
16 changed files with 353 additions and 124 deletions

View File

@@ -0,0 +1,14 @@
---
"@pnpm/headless": major
"@pnpm/package-requester": major
"@pnpm/package-store": major
"@pnpm/store-connection-manager": major
"@pnpm/server": major
"@pnpm/store-controller-types": major
"supi": minor
"@pnpm/resolve-dependencies": major
---
Breaking changes to the store controller API.
The options to `requestPackage()` and `fetchPackage()` changed.

View File

@@ -0,0 +1,5 @@
---
"@pnpm/cafs": minor
---
New fields added to `PackageFilesIndex`: `name` and `version`.

View File

@@ -11,6 +11,13 @@ const limit = pLimit(20)
const MAX_BULK_SIZE = 1 * 1024 * 1024 // 1MB
export interface PackageFilesIndex {
// name and version are nullable for backward compatibility
// the intitial specs of pnpm store v3 did not require these fields.
// However, it might be possible that some types of dependencies don't
// have the name/version fields, like the local tarball dependencies.
name?: string
version?: string
files: Record<string, PackageFileInfo>
sideEffects?: Record<string, Record<string, PackageFileInfo>>
}

View File

@@ -598,8 +598,12 @@ async function lockfileToDepGraph (
fetchResponse = opts.storeController.fetchPackage({
force: false,
lockfileDir: opts.lockfileDir,
pkgId: packageId,
resolution,
pkg: {
name: pkgName,
version: pkgVersion,
id: packageId,
resolution,
},
})
if (fetchResponse instanceof Promise) fetchResponse = await fetchResponse
} catch (err) {

View File

@@ -37,6 +37,7 @@
"dependencies": {
"@pnpm/cafs": "workspace:2.0.5",
"@pnpm/core-loggers": "workspace:5.0.3",
"@pnpm/error": "workspace:1.4.0",
"@pnpm/fetcher-base": "workspace:9.0.4",
"@pnpm/read-package-json": "workspace:3.1.9",
"@pnpm/resolver-base": "workspace:7.1.1",

View File

@@ -7,6 +7,7 @@ import createCafs, {
PackageFilesIndex,
} from '@pnpm/cafs'
import { fetchingProgressLogger } from '@pnpm/core-loggers'
import PnpmError from '@pnpm/error'
import {
Cafs,
DeferredManifestPromise,
@@ -126,8 +127,8 @@ async function resolveAndFetch (
let latest: string | undefined
let manifest: DependencyManifest | undefined
let normalizedPref: string | undefined
let resolution = options.currentResolution as Resolution
let pkgId = options.currentPackageId
let resolution = options.currentPkg?.resolution as Resolution
let pkgId = options.currentPkg?.id
const skipResolution = resolution && !options.update
let forceFetch = false
let updated = false
@@ -138,8 +139,8 @@ async function resolveAndFetch (
// When we don't fetch, the only way to get the package's manifest is via resolving it.
//
// The resolution step is never skipped for local dependencies.
if (!skipResolution || options.skipFetch === true || pkgId?.startsWith('file:')) {
const resolveResult = await ctx.requestsQueue.add<ResolveResult>(() => ctx.resolve(wantedDependency, {
if (!skipResolution || options.skipFetch === true || Boolean(pkgId?.startsWith('file:'))) {
const resolveResult = await ctx.requestsQueue.add<ResolveResult>(async () => ctx.resolve(wantedDependency, {
alwaysTryWorkspacePackages: options.alwaysTryWorkspacePackages,
defaultTag: options.defaultTag,
lockfileDir: options.lockfileDir,
@@ -157,17 +158,13 @@ async function resolveAndFetch (
// If the integrity of a local tarball dependency has changed,
// the local tarball should be unpacked, so a fetch to the store should be forced
forceFetch = Boolean(
options.currentResolution &&
options.currentPkg?.resolution &&
pkgId?.startsWith('file:') &&
options.currentResolution['integrity'] !== resolveResult.resolution['integrity'] // eslint-disable-line @typescript-eslint/dot-notation
options.currentPkg?.resolution['integrity'] !== resolveResult.resolution['integrity'] // eslint-disable-line @typescript-eslint/dot-notation
)
updated = pkgId !== resolveResult.id || !resolution || forceFetch
// Keep the lockfile resolution when possible
// to keep the original shasum.
if (updated) {
resolution = resolveResult.resolution
}
resolution = resolveResult.resolution
pkgId = resolveResult.id
normalizedPref = resolveResult.normalizedPref
}
@@ -212,8 +209,11 @@ async function resolveAndFetch (
fetchRawManifest: updated || !manifest,
force: forceFetch,
lockfileDir: options.lockfileDir,
pkgId: id,
resolution: resolution,
pkg: {
...R.pick(['name', 'version'], manifest ?? options.currentPkg ?? {}),
id,
resolution,
},
})
return {
@@ -260,11 +260,15 @@ function fetchToStore (
verifyStoreIntegrity: boolean
},
opts: {
pkg: {
name?: string
version?: string
id: string
resolution: Resolution
}
fetchRawManifest?: boolean
force: boolean
pkgId: string
lockfileDir: string
resolution: Resolution
}
): {
bundledManifest?: () => Promise<BundledManifest>
@@ -272,28 +276,28 @@ function fetchToStore (
files: () => Promise<PackageFilesResponse>
finishing: () => Promise<void>
} {
const targetRelative = depPathToFilename(opts.pkgId, opts.lockfileDir)
const targetRelative = depPathToFilename(opts.pkg.id, opts.lockfileDir)
const target = path.join(ctx.storeDir, targetRelative)
if (!ctx.fetchingLocker.has(opts.pkgId)) {
if (!ctx.fetchingLocker.has(opts.pkg.id)) {
const bundledManifest = pDefer<BundledManifest>()
const files = pDefer<PackageFilesResponse>()
const finishing = pDefer<undefined>()
const filesIndexFile = opts.resolution['integrity']
? ctx.getFilePathInCafs(opts.resolution['integrity'], 'index')
const filesIndexFile = opts.pkg.resolution['integrity']
? ctx.getFilePathInCafs(opts.pkg.resolution['integrity'], 'index')
: path.join(target, 'integrity.json')
doFetchToStore(filesIndexFile, bundledManifest, files, finishing) // eslint-disable-line
if (opts.fetchRawManifest) {
ctx.fetchingLocker.set(opts.pkgId, {
ctx.fetchingLocker.set(opts.pkg.id, {
bundledManifest: removeKeyOnFail(bundledManifest.promise),
files: removeKeyOnFail(files.promise),
filesIndexFile,
finishing: removeKeyOnFail(finishing.promise),
})
} else {
ctx.fetchingLocker.set(opts.pkgId, {
ctx.fetchingLocker.set(opts.pkg.id, {
files: removeKeyOnFail(files.promise),
filesIndexFile,
finishing: removeKeyOnFail(finishing.promise),
@@ -312,13 +316,13 @@ function fetchToStore (
return
}
const tmp = ctx.fetchingLocker.get(opts.pkgId)
const tmp = ctx.fetchingLocker.get(opts.pkg.id)
// If fetching failed then it was removed from the cache.
// It is OK. In that case there is no need to update it.
if (!tmp) return
ctx.fetchingLocker.set(opts.pkgId, {
ctx.fetchingLocker.set(opts.pkg.id, {
...tmp,
files: Promise.resolve({
...cache,
@@ -327,11 +331,11 @@ function fetchToStore (
})
})
.catch(() => {
ctx.fetchingLocker.delete(opts.pkgId)
ctx.fetchingLocker.delete(opts.pkg.id)
})
}
const result = ctx.fetchingLocker.get(opts.pkgId)!
const result = ctx.fetchingLocker.get(opts.pkg.id)!
if (opts.fetchRawManifest && !result.bundledManifest) {
result.bundledManifest = removeKeyOnFail(
@@ -354,7 +358,7 @@ function fetchToStore (
try {
return await p
} catch (err) {
ctx.fetchingLocker.delete(opts.pkgId)
ctx.fetchingLocker.delete(opts.pkg.id)
throw err
}
}
@@ -366,13 +370,13 @@ function fetchToStore (
finishing: pDefer.DeferredPromise<void>
) {
try {
const isLocalTarballDep = opts.pkgId.startsWith('file:')
const isLocalTarballDep = opts.pkg.id.startsWith('file:')
if (
!opts.force &&
(
!isLocalTarballDep ||
await tarballIsUpToDate(opts.resolution as any, target, opts.lockfileDir) // eslint-disable-line
await tarballIsUpToDate(opts.pkg.resolution as any, target, opts.lockfileDir) // eslint-disable-line
)
) {
let pkgFilesIndex
@@ -387,6 +391,17 @@ function fetchToStore (
const manifest = opts.fetchRawManifest
? safeDeferredPromise<DependencyManifest>()
: undefined
if (
pkgFilesIndex.name != null && opts.pkg.name != null && pkgFilesIndex.name !== opts.pkg.name ||
pkgFilesIndex.version != null && opts.pkg.version != null && pkgFilesIndex.version !== opts.pkg.version
) {
/* eslint-disable @typescript-eslint/restrict-template-expressions */
throw new PnpmError('UNEXPECTED_PKG_CONTENT_IN_STORE', `\
Package name mismatch found while reading ${JSON.stringify(opts.pkg.resolution)} from the store. \
This means that the lockfile is broken. Expected package: ${opts.pkg.name}@${opts.pkg.version}. \
Actual package in the store by the given integrity: ${pkgFilesIndex.name}@${pkgFilesIndex.version}.`)
/* eslint-enable @typescript-eslint/restrict-template-expressions */
}
const verified = await ctx.checkFilesIntegrity(pkgFilesIndex.files, manifest)
if (verified) {
files.resolve({
@@ -426,23 +441,23 @@ function fetchToStore (
.then((manifest) => bundledManifest.resolve(pickBundledManifest(manifest)))
.catch(bundledManifest.reject)
}
const fetchedPackage = await ctx.requestsQueue.add(() => ctx.fetch(
opts.pkgId,
opts.resolution,
const fetchedPackage = await ctx.requestsQueue.add(async () => ctx.fetch(
opts.pkg.id,
opts.pkg.resolution,
{
lockfileDir: opts.lockfileDir,
manifest: fetchManifest,
onProgress: (downloaded) => {
fetchingProgressLogger.debug({
downloaded,
packageId: opts.pkgId,
packageId: opts.pkg.id,
status: 'in_progress',
})
},
onStart: (size, attempt) => {
fetchingProgressLogger.debug({
attempt,
packageId: opts.pkgId,
packageId: opts.pkg.id,
size,
status: 'started',
})
@@ -471,11 +486,15 @@ function fetchToStore (
}
})
)
await writeJsonFile(filesIndexFile, { files: integrity })
await writeJsonFile(filesIndexFile, {
name: opts.pkg.name,
version: opts.pkg.version,
files: integrity,
})
if (isLocalTarballDep && opts.resolution['integrity']) { // eslint-disable-line @typescript-eslint/dot-notation
if (isLocalTarballDep && opts.pkg.resolution['integrity']) { // eslint-disable-line @typescript-eslint/dot-notation
await fs.mkdir(target, { recursive: true })
await fs.writeFile(path.join(target, TARBALL_INTEGRITY_FILENAME), opts.resolution['integrity'], 'utf8') // eslint-disable-line @typescript-eslint/dot-notation
await fs.writeFile(path.join(target, TARBALL_INTEGRITY_FILENAME), opts.pkg.resolution['integrity'], 'utf8') // eslint-disable-line @typescript-eslint/dot-notation
}
files.resolve({

View File

@@ -112,11 +112,15 @@ test('request package but skip fetching, when resolution is already available',
const projectDir = tempy.directory()
const pkgResponse = await requestPackage({ alias: 'is-positive', pref: '1.0.0' }, {
currentPackageId: 'registry.npmjs.org/is-positive/1.0.0',
currentResolution: {
integrity: 'sha1-iACYVrZKLx632LsBeUGEJK4EUss=',
registry: 'https://registry.npmjs.org/',
tarball: 'https://registry.npmjs.org/is-positive/-/is-positive-1.0.0.tgz',
currentPkg: {
name: 'is-positive',
version: '1.0.0',
id: 'registry.npmjs.org/is-positive/1.0.0',
resolution: {
integrity: 'sha1-iACYVrZKLx632LsBeUGEJK4EUss=',
registry: 'https://registry.npmjs.org/',
tarball: 'https://registry.npmjs.org/is-positive/-/is-positive-1.0.0.tgz',
},
},
downloadPriority: 0,
lockfileDir: projectDir,
@@ -162,7 +166,6 @@ test('refetch local tarball if its integrity has changed', async () => {
const storeDir = tempy.directory()
const pkgId = `file:${normalize(tarballRelativePath)}`
const requestPackageOpts = {
currentPackageId: pkgId,
downloadPriority: 0,
lockfileDir: projectDir,
preferredVersions: {},
@@ -180,9 +183,14 @@ test('refetch local tarball if its integrity has changed', async () => {
const response = await requestPackage(wantedPackage, {
...requestPackageOpts,
currentResolution: {
integrity: 'sha512-lqODmYcc/FKOGROEUByd5Sbugqhzgkv+Hij9PXH0sZVQsU2npTQ0x3L81GCtHilFKme8lhBtD31Vxg/AKYrAvg==',
tarball,
currentPkg: {
name: '@pnpm/package-requester',
version: '0.8.1',
id: pkgId,
resolution: {
integrity: 'sha512-lqODmYcc/FKOGROEUByd5Sbugqhzgkv+Hij9PXH0sZVQsU2npTQ0x3L81GCtHilFKme8lhBtD31Vxg/AKYrAvg==',
tarball,
},
},
}) as PackageResponse & {
files: () => Promise<PackageFilesResponse>
@@ -207,9 +215,14 @@ test('refetch local tarball if its integrity has changed', async () => {
const response = await requestPackage(wantedPackage, {
...requestPackageOpts,
currentResolution: {
integrity: 'sha512-lqODmYcc/FKOGROEUByd5Sbugqhzgkv+Hij9PXH0sZVQsU2npTQ0x3L81GCtHilFKme8lhBtD31Vxg/AKYrAvg==',
tarball,
currentPkg: {
name: '@pnpm/package-requester',
version: '0.8.1',
id: pkgId,
resolution: {
integrity: 'sha512-lqODmYcc/FKOGROEUByd5Sbugqhzgkv+Hij9PXH0sZVQsU2npTQ0x3L81GCtHilFKme8lhBtD31Vxg/AKYrAvg==',
tarball,
},
},
})
await response.files!()
@@ -228,9 +241,14 @@ test('refetch local tarball if its integrity has changed', async () => {
const response = await requestPackage(wantedPackage, {
...requestPackageOpts,
currentResolution: {
integrity: 'sha512-v3uhYkN+Eh3Nus4EZmegjQhrfpdPIH+2FjrkeBc6ueqZJWWRaLnSYIkD0An6m16D3v+6HCE18ox6t95eGxj5Pw==',
tarball,
currentPkg: {
name: '@pnpm/package-requester',
version: '0.8.1',
id: pkgId,
resolution: {
integrity: 'sha512-v3uhYkN+Eh3Nus4EZmegjQhrfpdPIH+2FjrkeBc6ueqZJWWRaLnSYIkD0An6m16D3v+6HCE18ox6t95eGxj5Pw==',
tarball,
},
},
}) as PackageResponse & {
files: () => Promise<PackageFilesResponse>
@@ -329,11 +347,15 @@ test('fetchPackageToStore()', async () => {
const fetchResult = packageRequester.fetchPackageToStore({
force: false,
lockfileDir: tempy.directory(),
pkgId,
resolution: {
integrity: 'sha1-iACYVrZKLx632LsBeUGEJK4EUss=',
registry: 'https://registry.npmjs.org/',
tarball: 'https://registry.npmjs.org/is-positive/-/is-positive-1.0.0.tgz',
pkg: {
name: 'is-positive',
version: '1.0.0',
id: pkgId,
resolution: {
integrity: 'sha1-iACYVrZKLx632LsBeUGEJK4EUss=',
registry: 'https://registry.npmjs.org/',
tarball: 'https://registry.npmjs.org/is-positive/-/is-positive-1.0.0.tgz',
},
},
})
@@ -353,11 +375,15 @@ test('fetchPackageToStore()', async () => {
fetchRawManifest: true,
force: false,
lockfileDir: tempy.directory(),
pkgId,
resolution: {
integrity: 'sha1-iACYVrZKLx632LsBeUGEJK4EUss=',
registry: 'https://registry.npmjs.org/',
tarball: 'https://registry.npmjs.org/is-positive/-/is-positive-1.0.0.tgz',
pkg: {
name: 'is-positive',
version: '1.0.0',
id: pkgId,
resolution: {
integrity: 'sha1-iACYVrZKLx632LsBeUGEJK4EUss=',
registry: 'https://registry.npmjs.org/',
tarball: 'https://registry.npmjs.org/is-positive/-/is-positive-1.0.0.tgz',
},
},
})
@@ -391,21 +417,29 @@ test('fetchPackageToStore() concurrency check', async () => {
packageRequester.fetchPackageToStore({
force: false,
lockfileDir: projectDir1,
pkgId,
resolution: {
integrity: 'sha1-iACYVrZKLx632LsBeUGEJK4EUss=',
registry: 'https://registry.npmjs.org/',
tarball: 'https://registry.npmjs.org/is-positive/-/is-positive-1.0.0.tgz',
pkg: {
name: 'is-positive',
version: '1.0.0',
id: pkgId,
resolution: {
integrity: 'sha1-iACYVrZKLx632LsBeUGEJK4EUss=',
registry: 'https://registry.npmjs.org/',
tarball: 'https://registry.npmjs.org/is-positive/-/is-positive-1.0.0.tgz',
},
},
}),
packageRequester.fetchPackageToStore({
force: false,
lockfileDir: projectDir2,
pkgId,
resolution: {
integrity: 'sha1-iACYVrZKLx632LsBeUGEJK4EUss=',
registry: 'https://registry.npmjs.org/',
tarball: 'https://registry.npmjs.org/is-positive/-/is-positive-1.0.0.tgz',
pkg: {
name: 'is-positive',
version: '1.0.0',
id: pkgId,
resolution: {
integrity: 'sha1-iACYVrZKLx632LsBeUGEJK4EUss=',
registry: 'https://registry.npmjs.org/',
tarball: 'https://registry.npmjs.org/is-positive/-/is-positive-1.0.0.tgz',
},
},
}),
])
@@ -466,11 +500,15 @@ test('fetchPackageToStore() does not cache errors', async () => {
const badRequest = packageRequester.fetchPackageToStore({
force: false,
lockfileDir: tempy.directory(),
pkgId,
resolution: {
integrity: 'sha1-iACYVrZKLx632LsBeUGEJK4EUss=',
registry: 'https://registry.npmjs.org/',
tarball: 'https://registry.npmjs.org/is-positive/-/is-positive-1.0.0.tgz',
pkg: {
name: 'is-positive',
version: '1.0.0',
id: pkgId,
resolution: {
integrity: 'sha1-iACYVrZKLx632LsBeUGEJK4EUss=',
registry: 'https://registry.npmjs.org/',
tarball: 'https://registry.npmjs.org/is-positive/-/is-positive-1.0.0.tgz',
},
},
})
await expect(badRequest.files()).rejects.toThrow()
@@ -478,11 +516,15 @@ test('fetchPackageToStore() does not cache errors', async () => {
const fetchResult = packageRequester.fetchPackageToStore({
force: false,
lockfileDir: tempy.directory(),
pkgId,
resolution: {
integrity: 'sha1-iACYVrZKLx632LsBeUGEJK4EUss=',
registry: 'https://registry.npmjs.org/',
tarball: 'https://registry.npmjs.org/is-positive/-/is-positive-1.0.0.tgz',
pkg: {
name: 'is-positive',
version: '1.0.0',
id: pkgId,
resolution: {
integrity: 'sha1-iACYVrZKLx632LsBeUGEJK4EUss=',
registry: 'https://registry.npmjs.org/',
tarball: 'https://registry.npmjs.org/is-positive/-/is-positive-1.0.0.tgz',
},
},
})
const files = await fetchResult.files()
@@ -519,11 +561,15 @@ test('always return a package manifest in the response', async () => {
{
const pkgResponse = await requestPackage({ alias: 'is-positive', pref: '1.0.0' }, {
currentPackageId: 'registry.npmjs.org/is-positive/1.0.0',
currentResolution: {
integrity: 'sha1-iACYVrZKLx632LsBeUGEJK4EUss=',
registry: 'https://registry.npmjs.org/',
tarball: 'https://registry.npmjs.org/is-positive/-/is-positive-1.0.0.tgz',
currentPkg: {
name: 'is-positive',
version: '1.0.0',
id: 'registry.npmjs.org/is-positive/1.0.0',
resolution: {
integrity: 'sha1-iACYVrZKLx632LsBeUGEJK4EUss=',
registry: 'https://registry.npmjs.org/',
tarball: 'https://registry.npmjs.org/is-positive/-/is-positive-1.0.0.tgz',
},
},
downloadPriority: 0,
lockfileDir: projectDir,
@@ -568,15 +614,23 @@ test('fetchPackageToStore() fetch raw manifest of cached package', async () => {
fetchRawManifest: false,
force: false,
lockfileDir: tempy.directory(),
pkgId,
resolution,
pkg: {
name: 'is-positive',
version: '1.0.0',
id: pkgId,
resolution,
},
}),
packageRequester.fetchPackageToStore({
fetchRawManifest: true,
force: false,
lockfileDir: tempy.directory(),
pkgId,
resolution,
pkg: {
name: 'is-positive',
version: '1.0.0',
id: pkgId,
resolution,
},
}),
])
@@ -607,8 +661,12 @@ test('refetch package to store if it has been modified', async () => {
fetchRawManifest: false,
force: false,
lockfileDir,
pkgId,
resolution,
pkg: {
name: 'magic-hook',
version: '2.0.0',
id: pkgId,
resolution,
},
})
const { filesIndex } = await fetchResult.files()
@@ -633,8 +691,12 @@ test('refetch package to store if it has been modified', async () => {
fetchRawManifest: false,
force: false,
lockfileDir,
pkgId,
resolution,
pkg: {
name: 'magic-hook',
version: '2.0.0',
id: pkgId,
resolution,
},
})
await fetchResult.files()

View File

@@ -21,6 +21,9 @@
{
"path": "../dependency-path"
},
{
"path": "../error"
},
{
"path": "../fetcher-base"
},

View File

@@ -20,11 +20,13 @@ describe('store.importPackage()', () => {
const fetchResponse = storeController.fetchPackage({
force: false,
lockfileDir: tempy.directory(),
pkgId,
resolution: {
integrity: 'sha1-iACYVrZKLx632LsBeUGEJK4EUss=',
registry: 'https://registry.npmjs.org/',
tarball: 'https://registry.npmjs.org/is-positive/-/is-positive-1.0.0.tgz',
pkg: {
id: pkgId,
resolution: {
integrity: 'sha1-iACYVrZKLx632LsBeUGEJK4EUss=',
registry: 'https://registry.npmjs.org/',
tarball: 'https://registry.npmjs.org/is-positive/-/is-positive-1.0.0.tgz',
},
},
})
const importTo = tempy.directory()
@@ -53,11 +55,13 @@ describe('store.importPackage()', () => {
const fetchResponse = storeController.fetchPackage({
force: false,
lockfileDir: tempy.directory(),
pkgId,
resolution: {
integrity: 'sha1-iACYVrZKLx632LsBeUGEJK4EUss=',
registry: 'https://registry.npmjs.org/',
tarball: 'https://registry.npmjs.org/is-positive/-/is-positive-1.0.0.tgz',
pkg: {
id: pkgId,
resolution: {
integrity: 'sha1-iACYVrZKLx632LsBeUGEJK4EUss=',
registry: 'https://registry.npmjs.org/',
tarball: 'https://registry.npmjs.org/is-positive/-/is-positive-1.0.0.tgz',
},
},
})
const importTo = tempy.directory()

View File

@@ -486,6 +486,8 @@ function referenceSatisfiesWantedSpec (
interface InfoFromLockfile {
dependencyLockfile?: PackageSnapshot
depPath: string
name?: string
version?: string
pkgId: string
resolution?: Resolution
}
@@ -525,6 +527,7 @@ function getInfoFromLockfile (
}
return {
...nameVerFromPkgSnapshot(depPath, dependencyLockfile),
dependencyLockfile,
depPath,
pkgId: packageIdFromSnapshot(depPath, dependencyLockfile, registries),
@@ -542,6 +545,8 @@ interface ResolveDependencyOptions {
currentDepth: number
currentPkg?: {
depPath?: string
name?: string
version?: string
pkgId?: string
resolution?: Resolution
dependencyLockfile?: PackageSnapshot
@@ -574,7 +579,7 @@ async function resolveDependency (
ctx.virtualStoreDir,
dp.depPathToFilename(currentPkg.depPath, ctx.prefix),
'node_modules',
nameVerFromPkgSnapshot(currentPkg.depPath, currentPkg.dependencyLockfile).name,
currentPkg.name!,
'package.json'
)
)
@@ -588,8 +593,12 @@ async function resolveDependency (
try {
pkgResponse = await ctx.storeController.requestPackage(wantedDependency, {
alwaysTryWorkspacePackages: ctx.linkWorkspacePackagesDepth >= options.currentDepth,
currentPackageId: currentPkg.pkgId,
currentResolution: currentPkg.resolution,
currentPkg: currentPkg ? {
name: currentPkg.name,
version: currentPkg.version,
id: currentPkg.pkgId,
resolution: currentPkg.resolution,
} : undefined,
defaultTag: ctx.defaultTag,
downloadPriority: -options.currentDepth,
lockfileDir: ctx.lockfileDir,

View File

@@ -2,7 +2,6 @@ import { IncomingMessage, Server, ServerResponse } from 'http'
import { globalInfo } from '@pnpm/logger'
import {
RequestPackageOptions,
Resolution,
StoreController,
WantedDependency,
} from '@pnpm/store-controller-types'
@@ -96,7 +95,7 @@ export default function (
case '/fetchPackage': {
try {
body = await bodyPromise
const pkgResponse = store.fetchPackage(body.options as RequestPackageOptions & {force: boolean, pkgId: string, resolution: Resolution})
const pkgResponse = store.fetchPackage(body.options as any) // eslint-disable-line
if (pkgResponse['bundledManifest']) { // eslint-disable-line
rawManifestPromises[body.msgId] = pkgResponse['bundledManifest'] // eslint-disable-line
}

View File

@@ -84,11 +84,13 @@ test('fetchPackage', async () => {
fetchRawManifest: true,
force: false,
lockfileDir: process.cwd(),
pkgId,
resolution: {
integrity: 'sha1-iACYVrZKLx632LsBeUGEJK4EUss=',
registry: 'https://registry.npmjs.org/',
tarball: 'https://registry.npmjs.org/is-positive/-/is-positive-1.0.0.tgz',
pkg: {
id: pkgId,
resolution: {
integrity: 'sha1-iACYVrZKLx632LsBeUGEJK4EUss=',
registry: 'https://registry.npmjs.org/',
tarball: 'https://registry.npmjs.org/is-positive/-/is-positive-1.0.0.tgz',
},
},
})

View File

@@ -50,8 +50,12 @@ export interface FetchPackageToStoreOptions {
fetchRawManifest?: boolean
force: boolean
lockfileDir: string
pkgId: string
resolution: Resolution
pkg: {
id: string
name?: string
version?: string
resolution: Resolution
}
}
export type ImportPackageFunction = (
@@ -83,8 +87,12 @@ export type RequestPackageFunction = (
export interface RequestPackageOptions {
alwaysTryWorkspacePackages?: boolean
currentPackageId?: string
currentResolution?: Resolution
currentPkg?: {
id?: string
name?: string
version?: string
resolution?: Resolution
}
defaultTag?: string
downloadPriority: number
projectDir: string

View File

@@ -175,7 +175,7 @@ export async function mutateModules (
return result
async function _install (): Promise<Array<{ rootDir: string, manifest: ProjectManifest }>> {
const needsFullResolution = !R.equals(ctx.wantedLockfile.overrides ?? {}, overrides ?? {}) ||
let needsFullResolution = !R.equals(ctx.wantedLockfile.overrides ?? {}, overrides ?? {}) ||
!R.equals((ctx.wantedLockfile.neverBuiltDependencies ?? []).sort(), (neverBuiltDependencies ?? []).sort())
ctx.wantedLockfile.overrides = overrides
ctx.wantedLockfile.neverBuiltDependencies = neverBuiltDependencies
@@ -255,7 +255,15 @@ export async function mutateModules (
})
return projects
} catch (error) {
if (frozenLockfile || error.code !== 'ERR_PNPM_LOCKFILE_MISSING_DEPENDENCY') throw error
if (
frozenLockfile ||
error.code !== 'ERR_PNPM_LOCKFILE_MISSING_DEPENDENCY' && error.code !== 'ERR_PNPM_UNEXPECTED_PKG_CONTENT_IN_STORE'
) throw error
if (error.code === 'ERR_PNPM_UNEXPECTED_PKG_CONTENT_IN_STORE') {
needsFullResolution = true
// Ideally, we would not update but currently there is no other way to redownload the integrity of the package
opts.update = true
}
// A broken lockfile may be caused by a badly resolved Git conflict
logger.warn({
error,
@@ -577,7 +585,7 @@ export type ImporterToUpdate = {
wantedDependencies: Array<WantedDependency & { isNew?: Boolean, updateSpec?: Boolean }>
} & DependenciesMutation
async function installInContext (
type InstallFunction = (
projects: ImporterToUpdate[],
ctx: PnpmContext<DependenciesMutation>,
opts: StrictInstallOptions & {
@@ -589,7 +597,9 @@ async function installInContext (
preferredVersions?: PreferredVersions
currentLockfileIsUpToDate: boolean
}
) {
) => Promise<Array<{ rootDir: string, manifest: ProjectManifest }>>
const _installInContext: InstallFunction = async (projects, ctx, opts) => {
if (opts.lockfileOnly && ctx.existsCurrentLockfile) {
logger.warn({
message: '`node_modules` is present. Lockfile only installation will make it out-of-date',
@@ -883,6 +893,25 @@ async function installInContext (
return projectsToResolve.map(({ manifest, rootDir }) => ({ rootDir, manifest }))
}
const installInContext: InstallFunction = async (projects, ctx, opts) => {
try {
return await _installInContext(projects, ctx, opts)
} catch (error) {
if (
error.code !== 'ERR_PNPM_UNEXPECTED_PKG_CONTENT_IN_STORE'
) throw error
opts.needsFullResolution = true
// Ideally, we would not update but currently there is no other way to redownload the integrity of the package
opts.update = true
logger.warn({
error,
message: 'The lockfile is broken! pnpm will attempt to fix it.',
prefix: ctx.lockfileDir,
})
return _installInContext(projects, ctx, opts)
}
}
async function toResolveImporter (
opts: {
defaultUpdateDepth: number

View File

@@ -0,0 +1,61 @@
import { WANTED_LOCKFILE } from '@pnpm/constants'
import { prepareEmpty } from '@pnpm/prepare'
import rimraf from '@zkochan/rimraf'
import {
addDependenciesToPackage,
mutateModules,
} from 'supi'
import writeYamlFile from 'write-yaml-file'
import {
addDistTag,
testDefaults,
} from './utils'
test('installation breaks if the lockfile contains the wrong checksum', async () => {
await addDistTag('dep-of-pkg-with-1-dep', '100.0.0', 'latest')
const project = prepareEmpty()
const manifest = await addDependenciesToPackage({},
[
'pkg-with-1-dep@100.0.0',
],
await testDefaults({ lockfileOnly: true })
)
const lockfile = await project.readLockfile()
// breaking the lockfile
lockfile.packages['/pkg-with-1-dep/100.0.0'].resolution['integrity'] = lockfile.packages['/dep-of-pkg-with-1-dep/100.0.0'].resolution['integrity']
await writeYamlFile(WANTED_LOCKFILE, lockfile, { lineWidth: 1000 })
await expect(mutateModules([
{
buildIndex: 0,
manifest,
mutation: 'install',
rootDir: process.cwd(),
},
], await testDefaults({ frozenLockfile: true }))).rejects.toThrowError(/Package name mismatch found while reading/)
await mutateModules([
{
buildIndex: 0,
manifest,
mutation: 'install',
rootDir: process.cwd(),
},
], await testDefaults())
// Breaking the lockfile again
await writeYamlFile(WANTED_LOCKFILE, lockfile, { lineWidth: 1000 })
await rimraf('node_modules')
await mutateModules([
{
buildIndex: 0,
manifest,
mutation: 'install',
rootDir: process.cwd(),
},
], await testDefaults({ preferFrozenLockfile: false }))
})

2
pnpm-lock.yaml generated
View File

@@ -1489,6 +1489,7 @@ importers:
dependencies:
'@pnpm/cafs': link:../cafs
'@pnpm/core-loggers': link:../core-loggers
'@pnpm/error': link:../error
'@pnpm/fetcher-base': link:../fetcher-base
'@pnpm/read-package-json': link:../read-package-json
'@pnpm/resolver-base': link:../resolver-base
@@ -1523,6 +1524,7 @@ importers:
'@pnpm/cafs': workspace:2.0.5
'@pnpm/client': workspace:2.0.22
'@pnpm/core-loggers': workspace:5.0.3
'@pnpm/error': workspace:1.4.0
'@pnpm/fetcher-base': workspace:9.0.4
'@pnpm/logger': ^3.2.3
'@pnpm/package-requester': 'link:'