Files
pnpm/pkg-manager/core/test/lockfile.ts

1586 lines
52 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 { 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 loadJsonFile from 'load-json-file'
import nock from 'nock'
import sinon from 'sinon'
import { sync as writeYamlFile } from 'write-yaml-file'
import { testDefaults } from './utils'
const f = fixtures(__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' })
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.length).toBe(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'])
})
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 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!).length).toBe(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 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 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' }))
manifest = await addDependenciesToPackage(manifest, ['@pnpm.e2e/qar'], testDefaults({ addDependenciesToPackage: 'devDependencies' }))
manifest = await addDependenciesToPackage(manifest, ['@pnpm.e2e/foobar'], testDefaults({ save: true }))
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 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 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' }))
manifest = await addDependenciesToPackage(manifest, ['map-obj@1.0.0'], testDefaults({ pinnedVersion: 'patch', targetDependenciesField: 'optionalDependencies' }))
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 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 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 opts = testDefaults()
opts.registries!.default = 'https://registry.npmjs.org/'
opts.registries!['@zkochan'] = `http://localhost:${REGISTRY_MOCK_PORT}`
opts.registries!['@foo'] = `http://localhost:${REGISTRY_MOCK_PORT}`
await addDependenciesToPackage({}, ['@zkochan/foo', '@foo/has-dep-from-same-scope', 'is-positive'], opts)
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 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 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 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 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 }))
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 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: {
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' })
prepareEmpty()
const storeDir = path.resolve('..', '.store')
const 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.length).toBe(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'])
}
})
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(),
})
)
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 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 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 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(__dirname, './registry-mirror')
/* eslint-disable @typescript-eslint/no-explicit-any */
const isPositiveMeta = loadJsonFile.sync<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,
registries: {
default: 'https://registry.example.com',
},
save: true,
})
)
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: {
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 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 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 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('lockfile v6', async () => {
prepareEmpty()
const 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 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 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)
})