Files
pnpm/pkg-manager/core/test/lockfile.ts
Varun Chawla e73da5e27b fix(lockfile): respect lockfile-include-tarball-url=false for non-standard URLs (#10621)
When lockfile-include-tarball-url is explicitly set to false, tarball URLs
are now always excluded from the lockfile. Previously, packages hosted under
non-standard tarball URLs would still have their tarball field written to the
lockfile even when the setting was false, causing flaky and inconsistent
behavior across environments.

The fix makes the option tri-state internally:
- true: always include tarball URLs
- false: never include tarball URLs
- undefined (not set): use the existing heuristic that includes tarball URLs
  only for packages with non-standard registry URLs

close #6667
2026-02-25 11:03:32 +01:00

1632 lines
54 KiB
TypeScript

import fs from 'fs'
import path from 'path'
import { LOCKFILE_VERSION, WANTED_LOCKFILE } from '@pnpm/constants'
import { type RootLog } from '@pnpm/core-loggers'
import { type PnpmError } from '@pnpm/error'
import { fixtures } from '@pnpm/test-fixtures'
import { type LockfileObject, type TarballResolution } from '@pnpm/lockfile.fs'
import { type LockfileFile } from '@pnpm/lockfile.types'
import { tempDir, prepareEmpty, preparePackages } from '@pnpm/prepare'
import { readPackageJsonFromDir } from '@pnpm/read-package-json'
import { addDistTag, getIntegrity, REGISTRY_MOCK_PORT } from '@pnpm/registry-mock'
import { type DepPath, type ProjectManifest, type ProjectRootDir } from '@pnpm/types'
import { jest } from '@jest/globals'
import { sync as readYamlFile } from 'read-yaml-file'
import {
addDependenciesToPackage,
install,
mutateModules,
mutateModulesInSingleProject,
type MutatedProject,
type ProjectOptions,
} from '@pnpm/core'
import { sync as rimraf } from '@zkochan/rimraf'
import { loadJsonFileSync } from 'load-json-file'
import nock from 'nock'
import sinon from 'sinon'
import { sync as writeYamlFile } from 'write-yaml-file'
import { testDefaults } from './utils/index.js'
afterEach(() => {
nock.abortPendingRequests()
nock.cleanAll()
})
const f = fixtures(import.meta.dirname)
const LOCKFILE_WARN_LOG = {
level: 'warn',
message: `A ${WANTED_LOCKFILE} file exists. The current configuration prohibits to read or write a lockfile`,
name: 'pnpm',
}
test('lockfile has correct format', async () => {
await addDistTag({ package: '@pnpm.e2e/pkg-with-1-dep', version: '100.0.0', distTag: 'latest' })
// Mock the HEAD request that isRepoPublic() in @pnpm/git-resolver makes to check if the repo is public.
// Without this, transient network failures cause the resolver to fall back to git+https:// instead of
// resolving via the codeload tarball URL.
const githubNock = nock('https://github.com', { allowUnmocked: true })
.head('/kevva/is-negative')
.reply(200)
const project = prepareEmpty()
await addDependenciesToPackage({},
[
'@pnpm.e2e/pkg-with-1-dep',
'@rstacruz/tap-spec@4.1.1',
'kevva/is-negative#1d7e288222b53a0cab90a331f1865220ec29560c',
], testDefaults({ fastUnpack: false, save: true }))
const modules = project.readModulesManifest()
expect(modules).toBeTruthy()
expect(modules!.pendingBuilds).toHaveLength(0)
const lockfile = project.readLockfile()
const id = '@pnpm.e2e/pkg-with-1-dep@100.0.0'
expect(lockfile.lockfileVersion).toBe(LOCKFILE_VERSION)
expect(lockfile.importers?.['.'].dependencies).toBeTruthy()
expect(lockfile.importers?.['.'].dependencies?.['@pnpm.e2e/pkg-with-1-dep'].version).toBe('100.0.0')
expect(lockfile.importers?.['.'].dependencies).toHaveProperty(['@rstacruz/tap-spec'])
expect(lockfile.importers?.['.'].dependencies?.['is-negative'].version).toContain('/') // has not shortened tarball from the non-standard registry
expect(lockfile.packages).toBeTruthy() // has packages field
expect(lockfile.packages).toHaveProperty([id])
expect(lockfile.snapshots[id].dependencies).toBeTruthy()
expect(lockfile.snapshots[id].dependencies).toHaveProperty(['@pnpm.e2e/dep-of-pkg-with-1-dep'])
expect(lockfile.packages[id].resolution).toBeTruthy()
expect((lockfile.packages[id].resolution as { integrity: string }).integrity).toBeTruthy()
expect((lockfile.packages[id].resolution as TarballResolution).tarball).toBeFalsy()
expect(lockfile.packages).toHaveProperty(['is-negative@https://codeload.github.com/kevva/is-negative/tar.gz/1d7e288222b53a0cab90a331f1865220ec29560c'])
githubNock.done()
})
test('lockfile has dev deps even when installing for prod only', async () => {
const project = prepareEmpty()
await install({
devDependencies: {
'is-negative': '2.1.0',
},
}, testDefaults({ production: true }))
const lockfile = project.readLockfile()
const id = 'is-negative@2.1.0'
expect(lockfile.importers['.'].devDependencies).toBeTruthy()
expect(lockfile.importers['.'].devDependencies?.['is-negative'].version).toBe('2.1.0')
expect(lockfile.packages[id]).toBeTruthy()
})
test('lockfile with scoped package', async () => {
prepareEmpty()
writeYamlFile(WANTED_LOCKFILE, {
importers: {
'.': {
dependencies: {
'@types/semver': {
specifier: '^5.3.31',
version: '5.3.31',
},
},
},
},
lockfileVersion: LOCKFILE_VERSION,
packages: {
'@types/semver@5.3.31': {
resolution: {
integrity: 'sha512-WBv5F9HrWTyG800cB9M3veCVkFahqXN7KA7c3VUCYZm/xhNzzIFiXiq+rZmj75j7GvWelN3YNrLX7FjtqBvhMw==',
},
},
},
snapshots: {
'@types/semver@5.3.31': {},
},
}, { lineWidth: 1000 })
await install({
dependencies: {
'@types/semver': '^5.3.31',
},
}, testDefaults({ frozenLockfile: true }))
})
test("lockfile doesn't lock subdependencies that don't satisfy the new specs", async () => {
const project = prepareEmpty()
// depends on react-onclickoutside@5.9.0
const { updatedManifest: manifest } = await addDependenciesToPackage({}, ['react-datetime@2.8.8'], testDefaults({
autoInstallPeers: false,
fastUnpack: false,
save: true,
strictPeerDependencies: false,
}))
// depends on react-onclickoutside@0.3.4
await addDependenciesToPackage(manifest, ['react-datetime@1.3.0'], testDefaults({
autoInstallPeers: false,
save: true,
strictPeerDependencies: false,
}))
expect(
project.requireModule('.pnpm/react-datetime@1.3.0/node_modules/react-onclickoutside/package.json').version
).toBe('0.3.4') // react-datetime@1.3.0 has react-onclickoutside@0.3.4 in its node_modules
const lockfile = project.readLockfile()
expect(Object.keys(lockfile.importers!['.'].dependencies!)).toHaveLength(1) // resolutions not duplicated
})
test('a lockfile created even when there are no deps in package.json', async () => {
const project = prepareEmpty()
await install({}, testDefaults())
expect(project.readLockfile()).toBeTruthy()
expect(fs.existsSync('node_modules')).toBeFalsy()
})
test('current lockfile removed when no deps in package.json', async () => {
const project = prepareEmpty()
writeYamlFile(WANTED_LOCKFILE, {
dependencies: {
'is-negative': {
specifier: '2.1.0',
version: '2.1.0',
},
},
lockfileVersion: LOCKFILE_VERSION,
packages: {
'is-negative@2.1.0': {
resolution: {
tarball: `http://localhost:${REGISTRY_MOCK_PORT}/is-negative/-/is-negative-2.1.0.tgz`,
},
},
},
}, { lineWidth: 1000 })
await install({}, testDefaults())
expect(project.readLockfile()).toBeTruthy()
expect(fs.existsSync('node_modules')).toBeFalsy()
})
test('lockfile is fixed when it does not match package.json', async () => {
const project = prepareEmpty()
writeYamlFile(WANTED_LOCKFILE, {
dependencies: {
'@types/semver': {
specifier: '5.3.31',
version: '5.3.31',
},
'is-negative': {
specifier: '^2.1.0',
version: '2.1.0',
},
'is-positive': {
specifier: '^3.1.0',
version: '3.1.0',
},
},
lockfileVersion: LOCKFILE_VERSION,
packages: {
'@types/semver@5.3.31': {
resolution: {
integrity: 'sha512-WBv5F9HrWTyG800cB9M3veCVkFahqXN7KA7c3VUCYZm/xhNzzIFiXiq+rZmj75j7GvWelN3YNrLX7FjtqBvhMw==',
},
},
'is-negative@2.1.0': {
resolution: {
tarball: `http://localhost:${REGISTRY_MOCK_PORT}/is-negative/-/is-negative-2.1.0.tgz`,
},
},
'is-positive@3.1.0': {
resolution: {
integrity: 'sha512-8ND1j3y9/HP94TOvGzr69/FgbkX2ruOldhLEsTWwcJVfo4oRjwemJmJxt7RJkKYH8tz7vYBP9JcKQY8CLuJ90Q==',
},
},
},
}, { lineWidth: 1000 })
const reporter = sinon.spy()
await install({
devDependencies: {
'is-negative': '^2.1.0',
},
optionalDependencies: {
'is-positive': '^3.1.0',
},
}, testDefaults({ reporter }))
const progress = sinon.match({
name: 'pnpm:progress',
status: 'resolving',
})
expect(reporter.withArgs(progress).callCount).toBe(0)
const lockfile = project.readLockfile()
expect(lockfile.importers?.['.'].devDependencies?.['is-negative'].version).toBe('2.1.0')
expect(lockfile.importers?.['.'].optionalDependencies?.['is-positive'].version).toBe('3.1.0')
expect(lockfile.importers?.['.'].dependencies).toBeFalsy()
expect(lockfile.packages).not.toHaveProperty(['@types/semver@5.3.31'])
})
test(`doing named installation when ${WANTED_LOCKFILE} exists already`, async () => {
const project = prepareEmpty()
writeYamlFile(WANTED_LOCKFILE, {
dependencies: {
'@types/semver': {
specifier: '5.3.31',
version: '5.3.31',
},
'is-negative': {
specifier: '^2.1.0',
version: '2.1.0',
},
'is-positive': {
specifier: '^3.1.0',
version: '3.1.0',
},
},
lockfileVersion: LOCKFILE_VERSION,
packages: {
'@types/semver@5.3.31': {
resolution: {
integrity: 'sha512-WBv5F9HrWTyG800cB9M3veCVkFahqXN7KA7c3VUCYZm/xhNzzIFiXiq+rZmj75j7GvWelN3YNrLX7FjtqBvhMw==',
},
},
'is-negative@2.1.0': {
resolution: {
tarball: `http://localhost:${REGISTRY_MOCK_PORT}/is-negative/-/is-negative-2.1.0.tgz`,
},
},
'is-positive@3.1.0': {
resolution: {
integrity: 'sha512-8ND1j3y9/HP94TOvGzr69/FgbkX2ruOldhLEsTWwcJVfo4oRjwemJmJxt7RJkKYH8tz7vYBP9JcKQY8CLuJ90Q==',
},
},
},
}, { lineWidth: 1000 })
const reporter = sinon.spy()
const { updatedManifest: manifest } = await addDependenciesToPackage({
dependencies: {
'@types/semver': '5.3.31',
'is-negative': '^2.1.0',
'is-positive': '^3.1.0',
},
}, ['is-positive'], testDefaults({ reporter }))
await install(manifest, testDefaults({ reporter }))
expect(reporter.calledWithMatch(LOCKFILE_WARN_LOG)).toBeFalsy()
project.has('is-negative')
})
test(`respects ${WANTED_LOCKFILE} for top dependencies`, async () => {
const project = prepareEmpty()
const reporter = sinon.spy()
// const fooProgress = sinon.match({
// name: 'pnpm:progress',
// status: 'resolving',
// manifest: {
// name: 'foo',
// },
// })
const pkgs = ['@pnpm.e2e/foo', '@pnpm.e2e/bar', '@pnpm.e2e/qar']
await Promise.all(pkgs.map(async (pkgName) => addDistTag({ package: pkgName, version: '100.0.0', distTag: 'latest' })))
let { updatedManifest: manifest } = await addDependenciesToPackage({}, ['@pnpm.e2e/foo'], testDefaults({ save: true, reporter }))
// t.equal(reporter.withArgs(fooProgress).callCount, 1, 'reported foo once')
manifest = (await addDependenciesToPackage(manifest, ['@pnpm.e2e/bar'], testDefaults({ targetDependenciesField: 'optionalDependencies' }))).updatedManifest
manifest = (await addDependenciesToPackage(manifest, ['@pnpm.e2e/qar'], testDefaults({ addDependenciesToPackage: 'devDependencies' }))).updatedManifest
manifest = (await addDependenciesToPackage(manifest, ['@pnpm.e2e/foobar'], testDefaults({ save: true }))).updatedManifest
expect((await readPackageJsonFromDir(path.resolve('node_modules', '@pnpm.e2e/foo'))).version).toBe('100.0.0')
expect((await readPackageJsonFromDir(path.resolve('node_modules', '@pnpm.e2e/bar'))).version).toBe('100.0.0')
expect((await readPackageJsonFromDir(path.resolve('node_modules', '@pnpm.e2e/qar'))).version).toBe('100.0.0')
expect((await readPackageJsonFromDir(path.resolve('node_modules/.pnpm/@pnpm.e2e+foobar@100.0.0/node_modules/@pnpm.e2e/foo'))).version).toBe('100.0.0')
expect((await readPackageJsonFromDir(path.resolve('node_modules/.pnpm/@pnpm.e2e+foobar@100.0.0/node_modules/@pnpm.e2e/bar'))).version).toBe('100.0.0')
await Promise.all(pkgs.map(async (pkgName) => addDistTag({ package: pkgName, version: '100.1.0', distTag: 'latest' })))
rimraf('node_modules')
rimraf(path.join('..', '.store'))
reporter.resetHistory()
// shouldn't care about what the registry in npmrc is
// the one in lockfile should be used
await install(manifest, testDefaults({
rawConfig: {
registry: 'https://registry.npmjs.org',
},
registry: 'https://registry.npmjs.org',
reporter,
}))
// t.equal(reporter.withArgs(fooProgress).callCount, 0, 'not reported foo')
project.storeHasNot('@pnpm.e2e/foo', '100.1.0')
expect((await readPackageJsonFromDir(path.resolve('node_modules', '@pnpm.e2e/foo'))).version).toBe('100.0.0')
expect((await readPackageJsonFromDir(path.resolve('node_modules', '@pnpm.e2e/bar'))).version).toBe('100.0.0')
expect((await readPackageJsonFromDir(path.resolve('node_modules', '@pnpm.e2e/qar'))).version).toBe('100.0.0')
expect((await readPackageJsonFromDir(path.resolve('node_modules/.pnpm/@pnpm.e2e+foobar@100.0.0/node_modules/@pnpm.e2e/foo'))).version).toBe('100.0.0')
expect((await readPackageJsonFromDir(path.resolve('node_modules/.pnpm/@pnpm.e2e+foobar@100.0.0/node_modules/@pnpm.e2e/bar'))).version).toBe('100.0.0')
})
test(`subdeps are updated on repeat install if outer ${WANTED_LOCKFILE} does not match the inner one`, async () => {
const project = prepareEmpty()
await addDistTag({ package: '@pnpm.e2e/pkg-with-1-dep', version: '100.0.0', distTag: 'latest' })
await addDistTag({ package: '@pnpm.e2e/dep-of-pkg-with-1-dep', version: '100.0.0', distTag: 'latest' })
const { updatedManifest: manifest } = await addDependenciesToPackage({}, ['@pnpm.e2e/pkg-with-1-dep'], testDefaults())
project.storeHas('@pnpm.e2e/dep-of-pkg-with-1-dep', '100.0.0')
const lockfile = project.readLockfile()
expect(lockfile.packages).toHaveProperty(['@pnpm.e2e/dep-of-pkg-with-1-dep@100.0.0'])
delete lockfile.packages['@pnpm.e2e/dep-of-pkg-with-1-dep@100.0.0']
lockfile.packages['@pnpm.e2e/dep-of-pkg-with-1-dep@100.1.0'] = {
resolution: {
integrity: getIntegrity('@pnpm.e2e/dep-of-pkg-with-1-dep', '100.1.0'),
},
}
lockfile.snapshots['@pnpm.e2e/pkg-with-1-dep@100.0.0'].dependencies!['@pnpm.e2e/dep-of-pkg-with-1-dep'] = '100.1.0'
writeYamlFile(WANTED_LOCKFILE, lockfile, { lineWidth: 1000 })
await install(manifest, testDefaults())
project.storeHas('@pnpm.e2e/dep-of-pkg-with-1-dep', '100.1.0')
})
test("recreates lockfile if it doesn't match the dependencies in package.json", async () => {
const project = prepareEmpty()
let { updatedManifest: manifest } = await addDependenciesToPackage({}, ['is-negative@1.0.0'], testDefaults({ pinnedVersion: 'patch', targetDependenciesField: 'dependencies' }))
manifest = (await addDependenciesToPackage(manifest, ['is-positive@1.0.0'], testDefaults({ pinnedVersion: 'patch', targetDependenciesField: 'devDependencies' }))).updatedManifest
manifest = (await addDependenciesToPackage(manifest, ['map-obj@1.0.0'], testDefaults({ pinnedVersion: 'patch', targetDependenciesField: 'optionalDependencies' }))).updatedManifest
const lockfile1 = project.readLockfile()
expect(lockfile1.importers['.'].dependencies?.['is-negative'].version).toBe('1.0.0')
expect(lockfile1.importers['.'].dependencies?.['is-negative'].specifier).toBe('1.0.0')
manifest.dependencies!['is-negative'] = '^2.1.0'
manifest.devDependencies!['is-positive'] = '^2.0.0'
manifest.optionalDependencies!['map-obj'] = '1.0.1'
await install(manifest, testDefaults())
const lockfile = project.readLockfile()
const importer = lockfile.importers!['.']!
expect(importer.dependencies!['is-negative'].version).toBe('2.1.0')
expect(importer.dependencies!['is-negative'].specifier).toBe('^2.1.0')
expect(importer.devDependencies!['is-positive'].version).toBe('2.0.0')
expect(importer.devDependencies!['is-positive'].specifier).toBe('^2.0.0')
expect(importer.optionalDependencies!['map-obj'].version).toBe('1.0.1')
expect(importer.optionalDependencies!['map-obj'].specifier).toBe('1.0.1')
})
test('repeat install with lockfile should not mutate lockfile when dependency has version specified with v prefix', async () => {
const project = prepareEmpty()
const { updatedManifest: manifest } = await addDependenciesToPackage({}, ['highmaps-release@5.0.11'], testDefaults())
const lockfile1 = project.readLockfile()
expect(lockfile1.importers['.'].dependencies?.['highmaps-release'].version).toBe('5.0.11')
rimraf('node_modules')
await install(manifest, testDefaults())
const lockfile2 = project.readLockfile()
expect(lockfile1).toStrictEqual(lockfile2) // lockfile hasn't been changed
})
test('package is not marked optional if it is also a subdep of a regular dependency', async () => {
const project = prepareEmpty()
await addDistTag({ package: '@pnpm.e2e/pkg-with-1-dep', version: '100.0.0', distTag: 'latest' })
await addDistTag({ package: '@pnpm.e2e/dep-of-pkg-with-1-dep', version: '100.0.0', distTag: 'latest' })
const { updatedManifest: manifest } = await addDependenciesToPackage({}, ['@pnpm.e2e/pkg-with-1-dep'], testDefaults())
await addDependenciesToPackage(manifest, ['@pnpm.e2e/dep-of-pkg-with-1-dep'], testDefaults({ targetDependenciesField: 'optionalDependencies' }))
const lockfile = project.readLockfile()
expect(lockfile.snapshots['@pnpm.e2e/dep-of-pkg-with-1-dep@100.0.0'].optional).toBeFalsy()
})
test('scoped module from different registry', async () => {
const project = prepareEmpty()
const registries = {
default: 'https://registry.npmjs.org/',
'@zkochan': `http://localhost:${REGISTRY_MOCK_PORT}`,
'@foo': `http://localhost:${REGISTRY_MOCK_PORT}`,
}
await addDependenciesToPackage({}, ['@zkochan/foo', '@foo/has-dep-from-same-scope', 'is-positive'], testDefaults({ registries }, { registries }))
project.has('@zkochan/foo')
const lockfile = project.readLockfile()
expect(lockfile).toStrictEqual({
settings: {
autoInstallPeers: true,
excludeLinksFromLockfile: false,
},
importers: {
'.': {
dependencies: {
'@foo/has-dep-from-same-scope': {
specifier: '^1.0.0',
version: '1.0.0',
},
'@zkochan/foo': {
specifier: '^1.0.0',
version: '1.0.0',
},
'is-positive': {
specifier: '^3.1.0',
version: '3.1.0',
},
},
},
},
lockfileVersion: LOCKFILE_VERSION,
packages: {
'@foo/has-dep-from-same-scope@1.0.0': {
resolution: {
integrity: getIntegrity('@foo/has-dep-from-same-scope', '1.0.0'),
},
},
'@foo/no-deps@1.0.0': {
resolution: {
integrity: getIntegrity('@foo/no-deps', '1.0.0'),
},
},
'@zkochan/foo@1.0.0': {
resolution: {
integrity: 'sha512-IFvrYpq7E6BqKex7A7czIFnFncPiUVdhSzGhAOWpp8RlkXns4y/9ZdynxaA/e0VkihRxQkihE2pTyvxjfe/wBg==',
},
},
'is-negative@1.0.0': {
engines: {
node: '>=0.10.0',
},
resolution: {
integrity: 'sha512-1aKMsFUc7vYQGzt//8zhkjRWPoYkajY/I5MJEvrc0pDoHXrW7n5ri8DYxhy3rR+Dk0QFl7GjHHsZU1sppQrWtw==',
},
},
'is-positive@3.1.0': {
engines: {
node: '>=0.10.0',
},
resolution: {
integrity: 'sha512-8ND1j3y9/HP94TOvGzr69/FgbkX2ruOldhLEsTWwcJVfo4oRjwemJmJxt7RJkKYH8tz7vYBP9JcKQY8CLuJ90Q==',
},
},
},
snapshots: {
'@foo/has-dep-from-same-scope@1.0.0': {
dependencies: {
'@foo/no-deps': '1.0.0',
'is-negative': '1.0.0',
},
},
'@foo/no-deps@1.0.0': {},
'@zkochan/foo@1.0.0': {},
'is-negative@1.0.0': {},
'is-positive@3.1.0': {},
},
})
})
test('repeat install with no inner lockfile should not rewrite packages in node_modules', async () => {
const project = prepareEmpty()
const { updatedManifest: manifest } = await addDependenciesToPackage({}, ['is-negative@1.0.0'], testDefaults())
rimraf('node_modules/.pnpm/lock.yaml')
await install(manifest, testDefaults())
project.has('is-negative')
})
test('packages are placed in devDependencies even if they are present as non-dev as well', async () => {
const project = prepareEmpty()
await addDistTag({ package: '@pnpm.e2e/pkg-with-1-dep', version: '100.0.0', distTag: 'latest' })
await addDistTag({ package: '@pnpm.e2e/dep-of-pkg-with-1-dep', version: '100.1.0', distTag: 'latest' })
const reporter = sinon.spy()
await install({
devDependencies: {
'@pnpm.e2e/dep-of-pkg-with-1-dep': '^100.1.0',
'@pnpm.e2e/pkg-with-1-dep': '^100.0.0',
},
}, testDefaults({ reporter }))
const importer = project.readLockfile().importers!['.']!
expect(importer.devDependencies).toHaveProperty(['@pnpm.e2e/dep-of-pkg-with-1-dep'])
expect(importer.devDependencies).toHaveProperty(['@pnpm.e2e/pkg-with-1-dep'])
expect(reporter.calledWithMatch({
added: {
dependencyType: 'dev',
name: '@pnpm.e2e/dep-of-pkg-with-1-dep',
version: '100.1.0',
},
level: 'debug',
name: 'pnpm:root',
} as RootLog)).toBeTruthy()
expect(reporter.calledWithMatch({
added: {
dependencyType: 'dev',
name: '@pnpm.e2e/pkg-with-1-dep',
version: '100.0.0',
},
level: 'debug',
name: 'pnpm:root',
} as RootLog)).toBeTruthy()
})
// This testcase verifies that pnpm is not failing when trying to preserve dependencies.
// Only when a dependency is a range dependency, should pnpm try to compare versions of deps with semver.satisfies().
test('updating package that has a github-hosted dependency', async () => {
prepareEmpty()
const { updatedManifest: manifest } = await addDependenciesToPackage({}, ['@pnpm.e2e/has-github-dep@1'], testDefaults())
await addDependenciesToPackage(manifest, ['@pnpm.e2e/has-github-dep@latest'], testDefaults())
})
test('updating package that has deps with peers', async () => {
prepareEmpty()
const { updatedManifest: manifest } = await addDependenciesToPackage({}, ['@pnpm.e2e/abc-grand-parent-with-c@0'], testDefaults())
await addDependenciesToPackage(manifest, ['@pnpm.e2e/abc-grand-parent-with-c@1'], testDefaults())
})
test('pendingBuilds gets updated if install removes packages', async () => {
const project = prepareEmpty()
await install({
dependencies: {
'@pnpm.e2e/pre-and-postinstall-scripts-example': '*',
'@pnpm.e2e/with-postinstall-b': '*',
},
}, testDefaults({ fastUnpack: false, ignoreScripts: true }))
const modules1 = project.readModulesManifest()
await install({
dependencies: {
'@pnpm.e2e/pre-and-postinstall-scripts-example': '*',
},
}, testDefaults({ fastUnpack: false, ignoreScripts: true }))
const modules2 = project.readModulesManifest()
expect(modules1).toBeTruthy()
expect(modules2).toBeTruthy()
expect(modules1!.pendingBuilds.length > modules2!.pendingBuilds.length).toBeTruthy()
})
test('optional properties are correctly updated on named install', async () => {
const project = prepareEmpty()
const { updatedManifest: manifest } = await addDependenciesToPackage({}, ['inflight@1.0.6'], testDefaults({ targetDependenciesField: 'optionalDependencies' }))
await addDependenciesToPackage(manifest, ['foo@npm:inflight@1.0.6'], testDefaults({}))
const lockfile = project.readLockfile()
expect(Object.values(lockfile.snapshots).filter((dep) => typeof dep.optional !== 'undefined')).toStrictEqual([])
})
test('no lockfile', async () => {
const project = prepareEmpty()
const reporter = sinon.spy()
await addDependenciesToPackage({}, ['is-positive'], testDefaults({ useLockfile: false, reporter }))
expect(reporter.calledWithMatch(LOCKFILE_WARN_LOG)).toBeFalsy()
project.has('is-positive')
expect(project.readLockfile()).toBeFalsy()
})
test('lockfile is ignored when lockfile = false', async () => {
const project = prepareEmpty()
writeYamlFile(WANTED_LOCKFILE, {
dependencies: {
'is-negative': {
specifier: '2.1.0',
version: '2.1.0',
},
},
lockfileVersion: LOCKFILE_VERSION,
packages: {
'is-negative@2.1.0': {
resolution: {
integrity: 'sha1-uZnX2TX0P1IHsBsA094ghS9Mp10=', // Invalid integrity
tarball: `http://localhost:${REGISTRY_MOCK_PORT}/is-negative/-/is-negative-2.1.0.tgz`,
},
},
},
}, { lineWidth: 1000 })
const reporter = sinon.spy()
await install({
dependencies: {
'is-negative': '2.1.0',
},
}, testDefaults({ useLockfile: false, reporter }))
expect(reporter.calledWithMatch(LOCKFILE_WARN_LOG)).toBeTruthy()
project.has('is-negative')
expect(project.readLockfile()).toBeTruthy()
})
test(`don't update ${WANTED_LOCKFILE} during uninstall when useLockfile: false`, async () => {
const project = prepareEmpty()
let manifest!: ProjectManifest
{
const reporter = sinon.spy()
manifest = (await addDependenciesToPackage({}, ['is-positive'], testDefaults({ reporter }))).updatedManifest
expect(reporter.calledWithMatch(LOCKFILE_WARN_LOG)).toBeFalsy()
}
{
const reporter = sinon.spy()
await mutateModulesInSingleProject({
dependencyNames: ['is-positive'],
manifest,
mutation: 'uninstallSome',
rootDir: process.cwd() as ProjectRootDir,
}, testDefaults({ useLockfile: false, reporter }))
expect(reporter.calledWithMatch(LOCKFILE_WARN_LOG)).toBeTruthy()
}
project.hasNot('is-positive')
expect(project.readLockfile()).toBeTruthy()
})
test('fail when installing with useLockfile: false and lockfileOnly: true', async () => {
prepareEmpty()
try {
await install({}, testDefaults({ useLockfile: false, lockfileOnly: true }))
throw new Error('installation should have failed')
} catch (err: any) { // eslint-disable-line
expect(err.message).toBe(`Cannot generate a ${WANTED_LOCKFILE} because lockfile is set to false`)
}
})
test("don't remove packages during named install when useLockfile: false", async () => {
const project = prepareEmpty()
const { updatedManifest: manifest } = await addDependenciesToPackage({}, ['is-positive'], testDefaults({ useLockfile: false }))
await addDependenciesToPackage(manifest, ['is-negative'], testDefaults({ useLockfile: false }))
project.has('is-positive')
project.has('is-negative')
})
test('save tarball URL when it is non-standard', async () => {
const project = prepareEmpty()
await addDependenciesToPackage({}, ['esprima-fb@3001.1.0-dev-harmony-fb'], testDefaults({ fastUnpack: false }))
const lockfile = project.readLockfile()
expect((lockfile.packages['esprima-fb@3001.1.0-dev-harmony-fb'].resolution as TarballResolution).tarball).toBe(`http://localhost:${REGISTRY_MOCK_PORT}/esprima-fb/-/esprima-fb-3001.0001.0000-dev-harmony-fb.tgz`)
})
test('packages installed via tarball URL from the default registry are normalized', async () => {
const project = prepareEmpty()
await addDependenciesToPackage({}, [
`http://localhost:${REGISTRY_MOCK_PORT}/@pnpm.e2e/pkg-with-tarball-dep-from-registry/-/pkg-with-tarball-dep-from-registry-1.0.0.tgz`,
'https://registry.npmjs.org/is-positive/-/is-positive-1.0.0.tgz',
], testDefaults())
const lockfile = project.readLockfile()
expect(lockfile).toStrictEqual({
settings: {
autoInstallPeers: true,
excludeLinksFromLockfile: false,
},
importers: {
'.': {
dependencies: {
'is-positive': {
specifier: 'https://registry.npmjs.org/is-positive/-/is-positive-1.0.0.tgz',
version: 'https://registry.npmjs.org/is-positive/-/is-positive-1.0.0.tgz',
},
'@pnpm.e2e/pkg-with-tarball-dep-from-registry': {
specifier: `http://localhost:${REGISTRY_MOCK_PORT}/@pnpm.e2e/pkg-with-tarball-dep-from-registry/-/pkg-with-tarball-dep-from-registry-1.0.0.tgz`,
version: '1.0.0',
},
},
},
},
lockfileVersion: LOCKFILE_VERSION,
packages: {
'@pnpm.e2e/dep-of-pkg-with-1-dep@100.0.0': {
resolution: {
integrity: getIntegrity('@pnpm.e2e/dep-of-pkg-with-1-dep', '100.0.0'),
},
},
'@pnpm.e2e/pkg-with-tarball-dep-from-registry@1.0.0': {
resolution: {
integrity: getIntegrity('@pnpm.e2e/pkg-with-tarball-dep-from-registry', '1.0.0'),
},
},
'is-positive@https://registry.npmjs.org/is-positive/-/is-positive-1.0.0.tgz': {
engines: { node: '>=0.10.0' },
resolution: {
integrity: 'sha512-xxzPGZ4P2uN6rROUa5N9Z7zTX6ERuE0hs6GUOc/cKBLF2NqKc16UwqHMt3tFg4CO6EBTE5UecUasg+3jZx3Ckg==',
tarball: 'https://registry.npmjs.org/is-positive/-/is-positive-1.0.0.tgz',
},
version: '1.0.0',
},
},
snapshots: {
'@pnpm.e2e/dep-of-pkg-with-1-dep@100.0.0': {},
'@pnpm.e2e/pkg-with-tarball-dep-from-registry@1.0.0': {
dependencies: {
'@pnpm.e2e/dep-of-pkg-with-1-dep': '100.0.0',
},
},
'is-positive@https://registry.npmjs.org/is-positive/-/is-positive-1.0.0.tgz': {},
},
})
})
test('lockfile file has correct format when lockfile directory does not equal the prefix directory', async () => {
await addDistTag({ package: '@pnpm.e2e/pkg-with-1-dep', version: '100.0.0', distTag: 'latest' })
// Mock the HEAD request that isRepoPublic() in @pnpm/git-resolver makes to check if the repo is public.
// Without this, transient network failures cause the resolver to fall back to git+https:// instead of
// resolving via the codeload tarball URL.
const githubNock = nock('https://github.com', { allowUnmocked: true })
.head('/kevva/is-negative')
.reply(200)
prepareEmpty()
const storeDir = path.resolve('..', '.store')
const { updatedManifest: manifest } = await addDependenciesToPackage(
{},
[
'@pnpm.e2e/pkg-with-1-dep',
'@zkochan/foo@1.0.0',
'kevva/is-negative#1d7e288222b53a0cab90a331f1865220ec29560c',
],
testDefaults({ save: true, lockfileDir: path.resolve('..'), storeDir })
)
expect(!fs.existsSync('node_modules/.modules.yaml')).toBeTruthy()
process.chdir('..')
const modules = readYamlFile<any>(path.resolve('node_modules', '.modules.yaml')) // eslint-disable-line @typescript-eslint/no-explicit-any
expect(modules).toBeTruthy()
expect(modules.pendingBuilds).toHaveLength(0)
{
const lockfile: LockfileFile = readYamlFile(WANTED_LOCKFILE)
const id = '@pnpm.e2e/pkg-with-1-dep@100.0.0'
expect(lockfile.lockfileVersion).toBe(LOCKFILE_VERSION)
expect(lockfile.importers).toBeTruthy()
expect(lockfile.importers?.project).toBeTruthy()
expect(lockfile.importers?.project).toBeTruthy()
expect(lockfile.importers?.project.dependencies).toBeTruthy()
expect(lockfile.importers?.project.dependencies!['@pnpm.e2e/pkg-with-1-dep'].version).toBe('100.0.0')
expect(lockfile.importers?.project.dependencies!['@zkochan/foo']).toBeTruthy()
expect(lockfile.importers?.project.dependencies!['is-negative'].version).toContain('/')
expect(lockfile.snapshots![id].dependencies).toHaveProperty(['@pnpm.e2e/dep-of-pkg-with-1-dep'])
expect(lockfile.packages![id].resolution).toHaveProperty(['integrity'])
expect(lockfile.packages![id].resolution).not.toHaveProperty(['tarball'])
expect(lockfile.packages).toHaveProperty(['is-negative@https://codeload.github.com/kevva/is-negative/tar.gz/1d7e288222b53a0cab90a331f1865220ec29560c'])
}
fs.mkdirSync('project-2')
process.chdir('project-2')
await addDependenciesToPackage(manifest, ['is-positive'], testDefaults({
save: true,
lockfileDir: path.resolve('..'),
storeDir,
pruneLockfileImporters: false,
}))
{
const lockfile = readYamlFile<LockfileFile>(path.join('..', WANTED_LOCKFILE))
expect(lockfile.importers).toHaveProperty(['project-2'])
// previous entries are not removed
const id = '@pnpm.e2e/pkg-with-1-dep@100.0.0'
expect(lockfile.importers?.project.dependencies!['@pnpm.e2e/pkg-with-1-dep'].version).toBe('100.0.0')
expect(lockfile.importers?.project.dependencies).toHaveProperty(['@zkochan/foo'])
expect(lockfile.importers?.project.dependencies!['is-negative'].version).toContain('/')
expect(lockfile.snapshots).toHaveProperty([id])
expect(lockfile.snapshots![id].dependencies).toBeTruthy()
expect(lockfile.snapshots![id].dependencies).toHaveProperty(['@pnpm.e2e/dep-of-pkg-with-1-dep'])
expect(lockfile.packages![id].resolution).toHaveProperty(['integrity'])
expect(lockfile.packages![id].resolution).not.toHaveProperty(['tarball'])
expect(lockfile.packages).toHaveProperty(['is-negative@https://codeload.github.com/kevva/is-negative/tar.gz/1d7e288222b53a0cab90a331f1865220ec29560c'])
}
githubNock.done()
})
test(`doing named installation when shared ${WANTED_LOCKFILE} exists already`, async () => {
const pkg1 = {
name: 'pkg1',
version: '1.0.0',
dependencies: {
'is-negative': '^2.1.0',
},
}
let pkg2: ProjectManifest = {
name: 'pkg2',
version: '1.0.0',
dependencies: {
'is-positive': '^3.1.0',
},
}
const projects = preparePackages([
pkg1,
pkg2,
])
writeYamlFile(WANTED_LOCKFILE, {
importers: {
pkg1: {
dependencies: {
'is-negative': {
specifier: '^2.1.0',
version: '2.1.0',
},
},
},
pkg2: {
dependencies: {
'is-positive': {
specifier: '^3.1.0',
version: '3.1.0',
},
},
},
},
lockfileVersion: LOCKFILE_VERSION,
packages: {
'is-negative@2.1.0': {
resolution: {
tarball: `http://localhost:${REGISTRY_MOCK_PORT}/is-negative/-/is-negative-2.1.0.tgz`,
},
},
'is-positive@3.1.0': {
resolution: {
integrity: 'sha512-8ND1j3y9/HP94TOvGzr69/FgbkX2ruOldhLEsTWwcJVfo4oRjwemJmJxt7RJkKYH8tz7vYBP9JcKQY8CLuJ90Q==',
},
},
},
}, { lineWidth: 1000 })
pkg2 = (await addDependenciesToPackage(
pkg2,
['is-positive'],
testDefaults({
dir: path.resolve('pkg2'),
lockfileDir: process.cwd(),
})
)).updatedManifest
const currentLockfile = readYamlFile<LockfileFile>(path.resolve('node_modules/.pnpm/lock.yaml'))
expect(Object.keys(currentLockfile.importers ?? {})).toStrictEqual(['pkg2'])
await mutateModules(
[
{
mutation: 'install',
rootDir: path.resolve('pkg1') as ProjectRootDir,
},
{
mutation: 'install',
rootDir: path.resolve('pkg2') as ProjectRootDir,
},
],
testDefaults({
allProjects: [
{
buildIndex: 0,
manifest: pkg1,
rootDir: path.resolve('pkg1') as ProjectRootDir,
},
{
buildIndex: 0,
manifest: pkg2,
rootDir: path.resolve('pkg2') as ProjectRootDir,
},
],
})
)
projects['pkg1'].has('is-negative')
projects['pkg2'].has('is-positive')
})
// Covers https://github.com/pnpm/pnpm/issues/1200
test(`use current ${WANTED_LOCKFILE} as initial wanted one, when wanted was removed`, async () => {
const project = prepareEmpty()
const { updatedManifest: manifest } = await addDependenciesToPackage({}, ['lodash@4.17.11', 'underscore@1.9.0'], testDefaults())
rimraf(WANTED_LOCKFILE)
await addDependenciesToPackage(manifest, ['underscore@1.9.1'], testDefaults())
project.has('lodash')
project.has('underscore')
})
// Covers https://github.com/pnpm/pnpm/issues/1876
test('existing dependencies are preserved when updating a lockfile to a newer format', async () => {
await addDistTag({ package: '@pnpm.e2e/pkg-with-1-dep', version: '100.0.0', distTag: 'latest' })
const project = prepareEmpty()
await addDistTag({ package: '@pnpm.e2e/dep-of-pkg-with-1-dep', version: '100.0.0', distTag: 'latest' })
const { updatedManifest: manifest } = await addDependenciesToPackage({}, ['@pnpm.e2e/pkg-with-1-dep'], testDefaults())
const initialLockfile = project.readLockfile()
writeYamlFile(WANTED_LOCKFILE, { ...initialLockfile, lockfileVersion: '6.0' }, { lineWidth: 1000 })
await addDistTag({ package: '@pnpm.e2e/dep-of-pkg-with-1-dep', version: '100.1.0', distTag: 'latest' })
await mutateModulesInSingleProject({
manifest,
mutation: 'install',
rootDir: process.cwd() as ProjectRootDir,
}, testDefaults())
const updatedLockfile = project.readLockfile()
expect(initialLockfile.packages).toStrictEqual(updatedLockfile.packages)
})
test('broken lockfile is fixed even if it seems like up to date at first. Unless frozenLockfile option is set to true', async () => {
const project = prepareEmpty()
await addDistTag({ package: '@pnpm.e2e/pkg-with-1-dep', version: '100.0.0', distTag: 'latest' })
await addDistTag({ package: '@pnpm.e2e/dep-of-pkg-with-1-dep', version: '100.0.0', distTag: 'latest' })
const { updatedManifest: manifest } = await addDependenciesToPackage({}, ['@pnpm.e2e/pkg-with-1-dep'], testDefaults({ lockfileOnly: true }))
{
const lockfile = project.readLockfile()
expect(lockfile.packages).toHaveProperty(['@pnpm.e2e/dep-of-pkg-with-1-dep@100.0.0'])
delete lockfile.packages['@pnpm.e2e/dep-of-pkg-with-1-dep@100.0.0']
delete lockfile.snapshots['@pnpm.e2e/dep-of-pkg-with-1-dep@100.0.0']
writeYamlFile(WANTED_LOCKFILE, lockfile, { lineWidth: 1000 })
}
let err!: PnpmError
try {
await mutateModulesInSingleProject({
manifest,
mutation: 'install',
rootDir: process.cwd() as ProjectRootDir,
}, testDefaults({ frozenLockfile: true }))
} catch (_err: any) { // eslint-disable-line
err = _err
}
expect(err.code).toBe('ERR_PNPM_LOCKFILE_MISSING_DEPENDENCY')
await mutateModulesInSingleProject({
manifest,
mutation: 'install',
rootDir: process.cwd() as ProjectRootDir,
}, testDefaults({ preferFrozenLockfile: true }))
project.has('@pnpm.e2e/pkg-with-1-dep')
const lockfile = project.readLockfile()
expect(lockfile.packages).toHaveProperty(['@pnpm.e2e/dep-of-pkg-with-1-dep@100.0.0'])
})
const REGISTRY_MIRROR_DIR = path.join(import.meta.dirname, './registry-mirror')
/* eslint-disable @typescript-eslint/no-explicit-any */
const isPositiveMeta = loadJsonFileSync<any>(path.join(REGISTRY_MIRROR_DIR, 'is-positive.json'))
/* eslint-enable @typescript-eslint/no-explicit-any */
const tarballPath = f.find('is-positive-3.1.0.tgz')
test('tarball domain differs from registry domain', async () => {
nock('https://registry.example.com', { allowUnmocked: true })
.get('/is-positive')
.reply(200, isPositiveMeta)
nock('https://registry.npmjs.org', { allowUnmocked: true })
.get('/is-positive/-/is-positive-3.1.0.tgz')
.replyWithFile(200, tarballPath)
const project = prepareEmpty()
await addDependenciesToPackage({},
[
'is-positive',
], testDefaults({
fastUnpack: false,
lockfileOnly: true,
save: true,
}, {
registries: {
default: 'https://registry.example.com',
},
})
)
const lockfile = project.readLockfile()
expect(lockfile).toStrictEqual({
settings: {
autoInstallPeers: true,
excludeLinksFromLockfile: false,
},
importers: {
'.': {
dependencies: {
'is-positive': {
specifier: '^3.1.0',
version: '3.1.0',
},
},
},
},
lockfileVersion: LOCKFILE_VERSION,
packages: {
'is-positive@3.1.0': {
engines: { node: '>=0.10.0' },
resolution: {
integrity: 'sha1-hX21hKG6XRyymAUn/DtsQ103sP0=',
tarball: 'https://registry.npmjs.org/is-positive/-/is-positive-3.1.0.tgz',
},
},
},
snapshots: {
'is-positive@3.1.0': {},
},
})
})
test('tarball installed through non-standard URL endpoint from the registry domain', async () => {
nock('https://registry.npmjs.org', { allowUnmocked: true })
.head('/is-positive/download/is-positive-3.1.0.tgz')
.reply(200, '')
nock('https://registry.npmjs.org', { allowUnmocked: true })
.get('/is-positive/download/is-positive-3.1.0.tgz')
.replyWithFile(200, tarballPath)
const project = prepareEmpty()
await addDependenciesToPackage({},
[
'https://registry.npmjs.org/is-positive/download/is-positive-3.1.0.tgz',
], testDefaults({
fastUnpack: false,
lockfileOnly: true,
registries: {
default: 'https://registry.npmjs.org/',
},
save: true,
})
)
const lockfile = project.readLockfile()
expect(lockfile).toStrictEqual({
settings: {
autoInstallPeers: true,
excludeLinksFromLockfile: false,
},
importers: {
'.': {
dependencies: {
'is-positive': {
specifier: 'https://registry.npmjs.org/is-positive/download/is-positive-3.1.0.tgz',
version: 'https://registry.npmjs.org/is-positive/download/is-positive-3.1.0.tgz',
},
},
},
},
lockfileVersion: LOCKFILE_VERSION,
packages: {
'is-positive@https://registry.npmjs.org/is-positive/download/is-positive-3.1.0.tgz': {
engines: { node: '>=0.10.0' },
resolution: {
integrity: 'sha512-8ND1j3y9/HP94TOvGzr69/FgbkX2ruOldhLEsTWwcJVfo4oRjwemJmJxt7RJkKYH8tz7vYBP9JcKQY8CLuJ90Q==',
tarball: 'https://registry.npmjs.org/is-positive/download/is-positive-3.1.0.tgz',
},
version: '3.1.0',
},
},
snapshots: {
'is-positive@https://registry.npmjs.org/is-positive/download/is-positive-3.1.0.tgz': {},
},
})
})
// TODO: fix merge conflicts with the new lockfile format (TODOv8)
test.skip('a lockfile with merge conflicts is autofixed', async () => {
const project = prepareEmpty()
fs.writeFileSync(WANTED_LOCKFILE, `\
importers:
.:
dependencies:
'@pnpm.e2e/dep-of-pkg-with-1-dep':
specifier: '>100.0.0'
<<<<<<< HEAD
version: 100.0.0
=======
version: 100.1.0
>>>>>>> next
lockfileVersion: ${LOCKFILE_VERSION}
packages:
<<<<<<< HEAD
'@pnpm.e2e/dep-of-pkg-with-1-dep@100.0.0':
dev: false
resolution:
integrity: ${getIntegrity('@pnpm.e2e/dep-of-pkg-with-1-dep', '100.0.0')}
=======
'@pnpm.e2e/dep-of-pkg-with-1-dep@100.1.0':
dev: false
resolution:
integrity: ${getIntegrity('@pnpm.e2e/dep-of-pkg-with-1-dep', '100.1.0')}
>>>>>>> next`, 'utf8')
await install({
dependencies: {
'@pnpm.e2e/dep-of-pkg-with-1-dep': '>100.0.0',
},
}, testDefaults())
const lockfile = project.readLockfile()
expect(lockfile.importers?.['.'].dependencies?.['@pnpm.e2e/dep-of-pkg-with-1-dep'].version).toBe('100.1.0')
})
test('a lockfile v6 with merge conflicts is autofixed', async () => {
const project = prepareEmpty()
fs.writeFileSync(WANTED_LOCKFILE, `\
lockfileVersion: '${LOCKFILE_VERSION}'
importers:
.:
dependencies:
'@pnpm.e2e/dep-of-pkg-with-1-dep':
specifier: '>100.0.0'
<<<<<<< HEAD
version: 100.0.0
=======
version: 100.1.0
>>>>>>> next
packages:
<<<<<<< HEAD
'@pnpm.e2e/dep-of-pkg-with-1-dep@100.0.0':
dev: false
resolution:
integrity: ${getIntegrity('@pnpm.e2e/dep-of-pkg-with-1-dep', '100.0.0')}
=======
'@pnpm.e2e/dep-of-pkg-with-1-dep@100.1.0':
dev: false
resolution:
integrity: ${getIntegrity('@pnpm.e2e/dep-of-pkg-with-1-dep', '100.1.0')}
>>>>>>> next`, 'utf8')
await install({
dependencies: {
'@pnpm.e2e/dep-of-pkg-with-1-dep': '>100.0.0',
},
}, testDefaults())
const lockfile = project.readLockfile()
expect(lockfile.importers?.['.'].dependencies?.['@pnpm.e2e/dep-of-pkg-with-1-dep']).toHaveProperty('version', '100.1.0')
})
test('a lockfile with duplicate keys is fixed', async () => {
const project = prepareEmpty()
fs.writeFileSync(WANTED_LOCKFILE, `\
importers:
.:
dependencies:
'@pnpm.e2e/dep-of-pkg-with-1-dep':
specifier: '100.0.0'
version: 100.0.0
lockfileVersion: ${LOCKFILE_VERSION}
packages:
'@pnpm.e2e/dep-of-pkg-with-1-dep@100.0.0':
resolution: {integrity: ${getIntegrity('@pnpm.e2e/dep-of-pkg-with-1-dep', '100.0.0')}}
dev: false
resolution: {integrity: ${getIntegrity('@pnpm.e2e/dep-of-pkg-with-1-dep', '100.0.0')}}
`, 'utf8')
const reporter = jest.fn()
await install({
dependencies: {
'@pnpm.e2e/dep-of-pkg-with-1-dep': '100.0.0',
},
}, testDefaults({ reporter }))
const lockfile = project.readLockfile()
expect(lockfile.importers?.['.'].dependencies?.['@pnpm.e2e/dep-of-pkg-with-1-dep'].version).toBe('100.0.0')
expect(reporter).toHaveBeenCalledWith(expect.objectContaining({
level: 'warn',
name: 'pnpm',
prefix: process.cwd(),
message: expect.stringMatching(/^Ignoring broken lockfile at .* duplicated mapping key/),
}))
})
test('a lockfile with duplicate keys is causes an exception, when frozenLockfile is true', async () => {
prepareEmpty()
fs.writeFileSync(WANTED_LOCKFILE, `\
importers:
.:
dependencies:
'@pnpm.e2e/dep-of-pkg-with-1-dep':
specifier: '100.0.0'
version: 100.0.0
lockfileVersion: ${LOCKFILE_VERSION}
packages:
'@pnpm.e2e/dep-of-pkg-with-1-dep@100.0.0':
resolution: {integrity: ${getIntegrity('@pnpm.e2e/dep-of-pkg-with-1-dep', '100.0.0')}}
dev: false
resolution: {integrity: ${getIntegrity('@pnpm.e2e/dep-of-pkg-with-1-dep', '100.0.0')}}
`, 'utf8')
await expect(
install({
dependencies: {
'@pnpm.e2e/dep-of-pkg-with-1-dep': '100.0.0',
},
}, testDefaults({ frozenLockfile: true }))
).rejects.toThrow(/^The lockfile at .* is broken: duplicated mapping key/)
})
test('a broken private lockfile is ignored', async () => {
prepareEmpty()
const { updatedManifest: manifest } = await install({
dependencies: {
'@pnpm.e2e/dep-of-pkg-with-1-dep': '100.0.0',
},
}, testDefaults())
fs.writeFileSync('node_modules/.pnpm/lock.yaml', `\
importers:
.:
dependencies:
'@pnpm.e2e/dep-of-pkg-with-1-dep': 100.0.0
specifiers:
'@pnpm.e2e/dep-of-pkg-with-1-dep': '100.0.0'
lockfileVersion: ${LOCKFILE_VERSION}
packages:
'@pnpm.e2e/dep-of-pkg-with-1-dep@100.0.0':
resolution: {integrity: ${getIntegrity('@pnpm.e2e/dep-of-pkg-with-1-dep', '100.0.0')}}
dev: false
resolution: {integrity: ${getIntegrity('@pnpm.e2e/dep-of-pkg-with-1-dep', '100.0.0')}}
`, 'utf8')
const reporter = jest.fn()
await mutateModulesInSingleProject({
mutation: 'install',
manifest,
rootDir: process.cwd() as ProjectRootDir,
}, testDefaults({ reporter }))
expect(reporter).toHaveBeenCalledWith(expect.objectContaining({
level: 'warn',
name: 'pnpm',
prefix: process.cwd(),
message: expect.stringMatching(/^Ignoring broken lockfile at .* duplicated mapping key/),
}))
})
// Covers https://github.com/pnpm/pnpm/issues/2928
test('build metadata is always ignored in versions and the lockfile is not flickering because of them', async () => {
await addDistTag({ package: '@monorepolint/core', version: '0.5.0-alpha.51', distTag: 'latest' })
const project = prepareEmpty()
const { updatedManifest: manifest } = await addDependenciesToPackage({},
[
'@monorepolint/cli@0.5.0-alpha.51',
], testDefaults({ lockfileOnly: true }))
const depPath = '@monorepolint/core@0.5.0-alpha.51'
const initialLockfile = project.readLockfile()
const initialPkgEntry = initialLockfile.packages[depPath]
expect(initialPkgEntry?.resolution).toStrictEqual({
integrity: 'sha512-ihFonHDppOZyG717OW6Bamd37mI2gQHjd09buTjbKhRX8NAHsTbRUKwp39ZYVI5AYgLF1eDlLpgOY4dHy2xGQw==',
})
await addDependenciesToPackage(manifest, ['is-positive'], testDefaults({ lockfileOnly: true }))
const updatedLockfile = project.readLockfile()
expect(initialPkgEntry).toStrictEqual(updatedLockfile.packages[depPath])
})
test('a broken lockfile should not break the store', async () => {
prepareEmpty()
const opts = testDefaults()
const { updatedManifest: manifest } = await addDependenciesToPackage({}, ['is-positive@1.0.0'], { ...opts, lockfileOnly: true })
const lockfile: LockfileObject = readYamlFile(WANTED_LOCKFILE)
lockfile.packages!['is-positive@1.0.0' as DepPath].name = 'bad-name'
lockfile.packages!['is-positive@1.0.0' as DepPath].version = '1.0.0'
writeYamlFile(WANTED_LOCKFILE, lockfile)
await mutateModulesInSingleProject({
manifest,
mutation: 'install',
rootDir: process.cwd() as ProjectRootDir,
}, testDefaults({ lockfileOnly: true, storeDir: path.resolve('store2') }))
delete lockfile.packages!['is-positive@1.0.0' as DepPath].name
delete lockfile.packages!['is-positive@1.0.0' as DepPath].version
writeYamlFile(WANTED_LOCKFILE, lockfile)
rimraf(path.resolve('node_modules'))
await mutateModulesInSingleProject({
manifest,
mutation: 'install',
rootDir: process.cwd() as ProjectRootDir,
}, testDefaults({ lockfileOnly: true, storeDir: path.resolve('store2') }))
})
test('include tarball URL', async () => {
const project = prepareEmpty()
const opts = testDefaults({ fastUnpack: false, lockfileIncludeTarballUrl: true })
await addDependenciesToPackage({}, ['@pnpm.e2e/pkg-with-1-dep@100.0.0'], opts)
const lockfile = project.readLockfile()
expect((lockfile.packages['@pnpm.e2e/pkg-with-1-dep@100.0.0'].resolution as TarballResolution).tarball)
.toBe(`http://localhost:${REGISTRY_MOCK_PORT}/@pnpm.e2e/pkg-with-1-dep/-/pkg-with-1-dep-100.0.0.tgz`)
})
test('exclude tarball URL when lockfileIncludeTarballUrl is false', async () => {
const project = prepareEmpty()
const opts = testDefaults({ fastUnpack: false, lockfileIncludeTarballUrl: false })
await addDependenciesToPackage({}, ['@pnpm.e2e/pkg-with-1-dep@100.0.0'], opts)
const lockfile = project.readLockfile()
expect((lockfile.packages['@pnpm.e2e/pkg-with-1-dep@100.0.0'].resolution as TarballResolution).tarball)
.toBeUndefined()
})
test('exclude non-standard tarball URL when lockfileIncludeTarballUrl is false', async () => {
const project = prepareEmpty()
await addDependenciesToPackage({}, ['esprima-fb@3001.1.0-dev-harmony-fb'], testDefaults({ fastUnpack: false, lockfileIncludeTarballUrl: false }))
const lockfile = project.readLockfile()
expect((lockfile.packages['esprima-fb@3001.1.0-dev-harmony-fb'].resolution as TarballResolution).tarball)
.toBeUndefined()
})
test('lockfile v6', async () => {
prepareEmpty()
const { updatedManifest: manifest } = await addDependenciesToPackage({}, ['@pnpm.e2e/pkg-with-1-dep@100.0.0'], testDefaults({ useLockfileV6: true }))
{
const lockfile = readYamlFile<any>(WANTED_LOCKFILE) // eslint-disable-line @typescript-eslint/no-explicit-any
expect(lockfile.lockfileVersion).toBe(LOCKFILE_VERSION)
expect(lockfile.packages).toHaveProperty(['@pnpm.e2e/pkg-with-1-dep@100.0.0'])
}
await addDependenciesToPackage(manifest, ['@pnpm.e2e/foo@100.0.0'], testDefaults())
{
const lockfile = readYamlFile<any>(WANTED_LOCKFILE) // eslint-disable-line @typescript-eslint/no-explicit-any
expect(lockfile.lockfileVersion).toBe(LOCKFILE_VERSION)
expect(lockfile.packages).toHaveProperty(['@pnpm.e2e/pkg-with-1-dep@100.0.0'])
expect(lockfile.packages).toHaveProperty(['@pnpm.e2e/foo@100.0.0'])
}
})
test('lockfile v5 is converted to lockfile v6', async () => {
const tmp = tempDir()
f.copy('lockfile-v5', tmp)
prepareEmpty()
await install({ dependencies: { '@pnpm.e2e/pkg-with-1-dep': '100.0.0' } }, testDefaults())
const lockfile = readYamlFile<any>(WANTED_LOCKFILE) // eslint-disable-line @typescript-eslint/no-explicit-any
expect(lockfile.lockfileVersion).toBe(LOCKFILE_VERSION)
expect(lockfile.packages).toHaveProperty(['@pnpm.e2e/pkg-with-1-dep@100.0.0'])
})
test('update the lockfile when a new project is added to the workspace', async () => {
preparePackages([
{
location: 'project-1',
package: { name: 'project-1' },
},
])
const importers: MutatedProject[] = [
{
mutation: 'install',
rootDir: path.resolve('project-1') as ProjectRootDir,
},
]
const allProjects: ProjectOptions[] = [
{
buildIndex: 0,
manifest: {
name: 'project-1',
version: '1.0.0',
dependencies: {
'is-positive': '1.0.0',
},
},
rootDir: path.resolve('project-1') as ProjectRootDir,
},
]
await mutateModules(importers, testDefaults({ allProjects }))
importers.push({
mutation: 'install',
rootDir: path.resolve('project-2') as ProjectRootDir,
})
allProjects.push({
buildIndex: 0,
manifest: {
name: 'project-2',
version: '1.0.0',
},
rootDir: path.resolve('project-2') as ProjectRootDir,
})
await mutateModules(importers, testDefaults({ allProjects }))
const lockfile: LockfileObject = readYamlFile(WANTED_LOCKFILE)
expect(Object.keys(lockfile.importers)).toStrictEqual(['project-1', 'project-2'])
})
test('update the lockfile when a new project is added to the workspace and lockfile-only installation is used', async () => {
preparePackages([
{
location: 'project-1',
package: { name: 'project-1' },
},
])
const importers: MutatedProject[] = [
{
mutation: 'install',
rootDir: path.resolve('project-1') as ProjectRootDir,
},
]
const allProjects: ProjectOptions[] = [
{
buildIndex: 0,
manifest: {
name: 'project-1',
version: '1.0.0',
dependencies: {
'is-positive': '1.0.0',
},
},
rootDir: path.resolve('project-1') as ProjectRootDir,
},
]
await mutateModules(importers, testDefaults({ allProjects, lockfileOnly: true }))
importers.push({
mutation: 'install',
rootDir: path.resolve('project-2') as ProjectRootDir,
})
allProjects.push({
buildIndex: 0,
manifest: {
name: 'project-2',
version: '1.0.0',
},
rootDir: path.resolve('project-2') as ProjectRootDir,
})
await mutateModules(importers, testDefaults({ allProjects, lockfileOnly: true }))
const lockfile: LockfileObject = readYamlFile(WANTED_LOCKFILE)
expect(Object.keys(lockfile.importers)).toStrictEqual(['project-1', 'project-2'])
})
test('lockfile is not written when it has no changes', async () => {
prepareEmpty()
const { updatedManifest: manifest } = await install({
dependencies: {
'@types/semver': '^5.3.31',
},
}, testDefaults())
const stat = fs.statSync(WANTED_LOCKFILE)
const initialMtime = stat.mtimeMs
await install(manifest, testDefaults())
expect(fs.statSync(WANTED_LOCKFILE)).toHaveProperty('mtimeMs', initialMtime)
})
test('installation should work with packages that have () in the scope name', async () => {
prepareEmpty()
const opts = testDefaults()
const { updatedManifest: manifest } = await addDependenciesToPackage({}, ['@(-.-)/env@0.3.1'], opts)
await install(manifest, opts)
})
test('setting a custom peersSuffixMaxLength', async () => {
const project = prepareEmpty()
await addDependenciesToPackage({}, ['@pnpm.e2e/abc@1.0.0'], testDefaults({ peersSuffixMaxLength: 10 }))
const lockfile = project.readLockfile()
expect(lockfile.settings.peersSuffixMaxLength).toBe(10)
expect(lockfile.importers['.']?.dependencies?.['@pnpm.e2e/abc']?.version?.length).toBe(39)
})