import fs from 'fs' import path from 'path' import { LOCKFILE_VERSION } from '@pnpm/constants' import { type LockfileFile } from '@pnpm/lockfile.fs' import { prepareEmpty, preparePackages } from '@pnpm/prepare' import { addDistTag } from '@pnpm/registry-mock' import { fixtures } from '@pnpm/test-fixtures' import { type ProjectRootDir } from '@pnpm/types' import { addDependenciesToPackage, install, mutateModules, type MutatedProject, mutateModulesInSingleProject, type ProjectOptions, } from '@pnpm/core' import { sync as rimraf } from '@zkochan/rimraf' import normalizePath from 'normalize-path' import { sync as readYamlFile } from 'read-yaml-file' import symlinkDir from 'symlink-dir' import { testDefaults } from '../utils/index.js' const f = fixtures(import.meta.dirname) test('scoped modules from a directory', async () => { const project = prepareEmpty() await addDependenciesToPackage({}, [`file:${f.find('local-scoped-pkg')}`], testDefaults()) const m = project.requireModule('@scope/local-scoped-pkg') expect(m()).toBe('@scope/local-scoped-pkg') }) test('local file', async () => { const project = prepareEmpty() f.copy('local-pkg', path.resolve('..', 'local-pkg')) const { updatedManifest: manifest } = await addDependenciesToPackage({}, ['link:../local-pkg'], testDefaults()) const expectedSpecs = { 'local-pkg': `link:..${path.sep}local-pkg` } expect(manifest.dependencies).toStrictEqual(expectedSpecs) const m = project.requireModule('local-pkg') expect(m).toBeTruthy() const lockfile = project.readLockfile() expect(lockfile).toStrictEqual({ settings: { autoInstallPeers: true, excludeLinksFromLockfile: false, }, importers: { '.': { dependencies: { 'local-pkg': { specifier: expectedSpecs['local-pkg'], version: 'link:../local-pkg', }, }, }, }, lockfileVersion: LOCKFILE_VERSION, }) }) test('a symlink to a symlink to a local dependency is preserved', async () => { prepareEmpty() const localPkgDir = path.resolve('..', 'local-pkg') f.copy('local-pkg', localPkgDir) await symlinkDir(localPkgDir, path.resolve('../symlink')) await addDependenciesToPackage({}, ['link:../symlink'], testDefaults()) expect(fs.readlinkSync(path.resolve('node_modules/local-pkg'))).toContain('symlink') }) test('local directory with no package.json', async () => { const project = prepareEmpty() fs.mkdirSync('pkg') fs.writeFileSync('pkg/index.js', 'hello', 'utf8') const { updatedManifest: manifest } = await addDependenciesToPackage({}, ['file:./pkg'], testDefaults()) const expectedSpecs = { pkg: 'file:pkg' } expect(manifest.dependencies).toStrictEqual(expectedSpecs) project.has('pkg') rimraf('node_modules') await install(manifest, testDefaults({ frozenLockfile: true })) project.has('pkg') }) test('local file via link:', async () => { const project = prepareEmpty() f.copy('local-pkg', path.resolve('..', 'local-pkg')) const { updatedManifest: manifest } = await addDependenciesToPackage({}, ['link:../local-pkg'], testDefaults()) const expectedSpecs = { 'local-pkg': `link:..${path.sep}local-pkg` } expect(manifest.dependencies).toStrictEqual(expectedSpecs) const m = project.requireModule('local-pkg') expect(m).toBeTruthy() const lockfile = project.readLockfile() expect(lockfile).toStrictEqual({ settings: { autoInstallPeers: true, excludeLinksFromLockfile: false, }, importers: { '.': { dependencies: { 'local-pkg': { specifier: expectedSpecs['local-pkg'], version: 'link:../local-pkg', }, }, }, }, lockfileVersion: LOCKFILE_VERSION, }) }) test('local file with symlinked node_modules', async () => { const project = prepareEmpty() f.copy('local-pkg', path.resolve('..', 'local-pkg')) fs.mkdirSync(path.join('..', 'node_modules')) await symlinkDir(path.join('..', 'node_modules'), 'node_modules') const { updatedManifest: manifest } = await addDependenciesToPackage({}, ['link:../local-pkg'], testDefaults()) const expectedSpecs = { 'local-pkg': `link:..${path.sep}local-pkg` } expect(manifest.dependencies).toStrictEqual(expectedSpecs) const m = project.requireModule('local-pkg') expect(m).toBeTruthy() const lockfile = project.readLockfile() expect(lockfile).toStrictEqual({ settings: { autoInstallPeers: true, excludeLinksFromLockfile: false, }, importers: { '.': { dependencies: { 'local-pkg': { specifier: expectedSpecs['local-pkg'], version: 'link:../local-pkg', }, }, }, }, lockfileVersion: LOCKFILE_VERSION, }) }) test('package with a broken symlink', async () => { const project = prepareEmpty() await addDependenciesToPackage({}, [f.find('has-broken-symlink.tar.gz')], testDefaults({ fastUnpack: false })) const m = project.requireModule('has-broken-symlink') expect(m).toBeTruthy() }) test('tarball local package', async () => { const project = prepareEmpty() const { updatedManifest: manifest } = await addDependenciesToPackage({}, [f.find('tar-pkg-1.0.0.tgz')], testDefaults({ fastUnpack: false })) const m = project.requireModule('tar-pkg') expect(m()).toBe('tar-pkg') const pkgSpec = `file:${normalizePath(f.find('tar-pkg-1.0.0.tgz'))}` expect(manifest.dependencies).toStrictEqual({ 'tar-pkg': pkgSpec }) const lockfile = project.readLockfile() expect(lockfile.packages[`tar-pkg@${lockfile.importers['.'].dependencies!['tar-pkg'].version}`]).toStrictEqual({ resolution: { integrity: 'sha512-HP/5Rgt3pVFLzjmN9qJJ6vZMgCwoCIl/m2bPndYT283CUqnmFiMx0GeeIJ7SyK6TYoJM78SEvFEOQie++caHqw==', tarball: `file:${normalizePath(path.relative(process.cwd(), f.find('tar-pkg-1.0.0.tgz')))}`, }, version: '1.0.0', }) }) test('tarball local package from project directory', async () => { const project = prepareEmpty() f.copy('tar-pkg-1.0.0.tgz', path.resolve('tar-pkg-1.0.0.tgz')) const { updatedManifest: manifest } = await install({ dependencies: { 'tar-pkg': 'file:tar-pkg-1.0.0.tgz', }, }, testDefaults({ fastUnpack: false })) const m = project.requireModule('tar-pkg') expect(m()).toBe('tar-pkg') const pkgSpec = 'file:tar-pkg-1.0.0.tgz' expect(manifest.dependencies).toStrictEqual({ 'tar-pkg': pkgSpec }) const lockfile = project.readLockfile() expect(lockfile.importers['.'].dependencies?.['tar-pkg'].version).toBe(pkgSpec) expect(lockfile.packages[`tar-pkg@${lockfile.importers['.'].dependencies!['tar-pkg'].version}`]).toStrictEqual({ resolution: { integrity: 'sha512-HP/5Rgt3pVFLzjmN9qJJ6vZMgCwoCIl/m2bPndYT283CUqnmFiMx0GeeIJ7SyK6TYoJM78SEvFEOQie++caHqw==', tarball: pkgSpec, }, version: '1.0.0', }) }) test('update tarball local package when its integrity changes', async () => { const project = prepareEmpty() f.copy('tar-pkg-with-dep-1/tar-pkg-with-dep-1.0.0.tgz', path.resolve('..', 'tar.tgz')) const { updatedManifest: manifest } = await addDependenciesToPackage({}, ['../tar.tgz'], testDefaults()) const lockfile1 = project.readLockfile() expect(lockfile1.snapshots['tar-pkg-with-dep@file:../tar.tgz'].dependencies!['is-positive']).toBe('1.0.0') f.copy('tar-pkg-with-dep-2/tar-pkg-with-dep-1.0.0.tgz', path.resolve('..', 'tar.tgz')) await install(manifest, testDefaults()) const lockfile2 = project.readLockfile() expect(lockfile2.snapshots['tar-pkg-with-dep@file:../tar.tgz'].dependencies!['is-positive']).toBe('2.0.0') const { default: manifestOfTarballDep } = await import(path.resolve('node_modules/tar-pkg-with-dep/package.json')) expect(manifestOfTarballDep.dependencies['is-positive']).toBe('^2.0.0') }) // Similar to the test above, but for a filtered install. // Regression test for https://github.com/pnpm/pnpm/pull/9805. test('update tarball local package when its integrity changes (filtered install)', async () => { const rootProject = prepareEmpty() const lockfileDir = rootProject.dir() const manifests = { project1: { name: 'project1', }, project2: { name: 'project2', }, } preparePackages(Object.values(manifests), { tempDir: lockfileDir }) const allProjects: ProjectOptions[] = Object.entries(manifests) .map(([id, manifest]) => ({ buildIndex: 0, manifest, rootDir: path.join(lockfileDir, id) as ProjectRootDir, })) const options = { ...testDefaults({ allProjects, }), lockfileDir, } f.copy('tar-pkg-with-dep-1/tar-pkg-with-dep-1.0.0.tgz', path.resolve('.', 'tar.tgz')) await addDependenciesToPackage( manifests['project1'], ['../tar.tgz'], { ...options, dir: path.join(options.lockfileDir, 'project1'), }) const manifestOfTarballDep1 = JSON.parse(fs.readFileSync('project1/node_modules/tar-pkg-with-dep/package.json').toString()) expect(manifestOfTarballDep1.dependencies['is-positive']).toBe('^1.0.0') f.copy('tar-pkg-with-dep-2/tar-pkg-with-dep-1.0.0.tgz', path.resolve('.', 'tar.tgz')) // Re-initialize the store controller that's created within the testDefaults() // function. Otherwise the fetchingLocker will contain results from a prior // installation and skip store fetches for the same package ID. const nextOptions = { ...options, ...testDefaults(allProjects), } const project1InstallOptions: MutatedProject = { mutation: 'install', rootDir: path.join(lockfileDir, 'project1') as ProjectRootDir, } await mutateModules([project1InstallOptions], nextOptions) const manifestOfTarballDep2 = JSON.parse(fs.readFileSync('project1/node_modules/tar-pkg-with-dep/package.json').toString()) expect(manifestOfTarballDep2.dependencies['is-positive']).toBe('^2.0.0') }) // Covers https://github.com/pnpm/pnpm/issues/1878 test('do not update deps when installing in a project that has local tarball dep', async () => { await addDistTag({ package: '@pnpm.e2e/peer-a', version: '1.0.0', distTag: 'latest' }) const project = prepareEmpty() f.copy('tar-pkg-with-dep-1/tar-pkg-with-dep-1.0.0.tgz', path.resolve('..', 'tar.tgz')) const { updatedManifest: manifest } = await addDependenciesToPackage({}, ['../tar.tgz', '@pnpm.e2e/peer-a'], testDefaults({ lockfileOnly: true })) const initialLockfile = project.readLockfile() await addDistTag({ package: '@pnpm.e2e/peer-a', version: '1.0.1', distTag: 'latest' }) await mutateModulesInSingleProject({ manifest, mutation: 'install', rootDir: process.cwd() as ProjectRootDir, }, testDefaults()) const latestLockfile = project.readLockfile() expect(initialLockfile).toStrictEqual(latestLockfile) }) // Covers https://github.com/pnpm/pnpm/issues/1882 test('frozen-lockfile: installation fails if the integrity of a tarball dependency changed', async () => { prepareEmpty() f.copy('tar-pkg-with-dep-1/tar-pkg-with-dep-1.0.0.tgz', path.resolve('..', 'tar.tgz')) const { updatedManifest: manifest } = await addDependenciesToPackage({}, ['../tar.tgz'], testDefaults()) rimraf('node_modules') f.copy('tar-pkg-with-dep-2/tar-pkg-with-dep-1.0.0.tgz', path.resolve('..', 'tar.tgz')) await expect( install(manifest, testDefaults({ frozenLockfile: true })) ).rejects.toThrow(/Got unexpected checksum/) }) test('deep local', async () => { const manifest1 = { name: 'project-1', version: '1.0.0', dependencies: { 'project-2': 'file:../project-2', }, } preparePackages([ { location: 'project-1', package: manifest1, }, { location: 'project-2', package: { name: 'project-2', version: '1.0.0', dependencies: { 'project-3': 'file:./project-3', }, }, }, { location: 'project-2/project-3', package: { name: 'project-3', version: '1.0.0', }, }, ]) process.chdir('project-1') await install(manifest1, testDefaults()) const lockfile = readYamlFile('pnpm-lock.yaml') expect(Object.keys(lockfile.packages ?? {})).toStrictEqual(['project-2@file:../project-2', 'project-3@file:../project-2/project-3']) }) // Covers https://github.com/pnpm/pnpm/issues/5327 test('resolution should not fail when a peer is resolved from a local package and there are many circular dependencies', async () => { const manifest1 = { name: 'chained-iterator', version: '0.0.4', dependencies: { '@bryntum/siesta': '6.0.0-beta-1', }, } const manifest2 = { name: '@bryntum/chronograph', version: '2.0.3', dependencies: { '@bryntum/siesta': '6.0.0-beta-1', 'typescript-serializable-mixin': '0.0.3', 'typescript-mixin-class': 'link:../typescript-mixin-class', }, } const manifest3 = { name: 'typescript-mixin-class', version: '0.0.3', dependencies: { '@bryntum/siesta': '6.0.0-beta-1', }, } preparePackages([ { location: manifest1.name, package: manifest1, }, { location: manifest2.name, package: manifest2, }, { location: manifest3.name, package: manifest3, }, ]) const importers: MutatedProject[] = [ { mutation: 'install', rootDir: path.resolve(manifest1.name) as ProjectRootDir, }, { mutation: 'install', rootDir: path.resolve(manifest2.name) as ProjectRootDir, }, { mutation: 'install', rootDir: path.resolve(manifest3.name) as ProjectRootDir, }, ] const allProjects = [ { buildIndex: 0, manifest: manifest1, rootDir: path.resolve(manifest1.name) as ProjectRootDir, }, { buildIndex: 0, manifest: manifest2, rootDir: path.resolve(manifest2.name) as ProjectRootDir, }, { buildIndex: 0, manifest: manifest3, rootDir: path.resolve(manifest3.name) as ProjectRootDir, }, ] await mutateModules(importers, testDefaults({ allProjects, lockfileOnly: true, strictPeerDependencies: false })) // All we need to know in this test is that installation doesn't fail }) test('re-install should update local file dependency', async () => { const project = prepareEmpty() f.copy('local-pkg', path.resolve('..', 'local-pkg')) const { updatedManifest: manifest } = await addDependenciesToPackage({}, ['file:../local-pkg'], testDefaults()) const expectedSpecs = { 'local-pkg': `file:..${path.sep}local-pkg` } expect(manifest.dependencies).toStrictEqual(expectedSpecs) const m = project.requireModule('local-pkg') expect(m).toBeTruthy() expect(fs.existsSync('./node_modules/local-pkg/add.js')).toBeFalsy() let lockfile = project.readLockfile() expect(lockfile).toStrictEqual({ settings: { autoInstallPeers: true, excludeLinksFromLockfile: false, }, importers: { '.': { dependencies: { 'local-pkg': { specifier: expectedSpecs['local-pkg'], version: 'file:../local-pkg', }, }, }, }, packages: { 'local-pkg@file:../local-pkg': { resolution: { directory: '../local-pkg', type: 'directory' }, }, }, snapshots: { 'local-pkg@file:../local-pkg': {}, }, lockfileVersion: LOCKFILE_VERSION, }) // add file fs.writeFileSync('../local-pkg/add.js', 'added', 'utf8') await install(manifest, testDefaults()) expect(fs.existsSync('./node_modules/local-pkg/add.js')).toBeTruthy() // remove file fs.rmSync('../local-pkg/add.js') await install(manifest, testDefaults()) expect(fs.existsSync('./node_modules/local-pkg/add.js')).toBeFalsy() // add dependency expect(fs.existsSync('./node_modules/.pnpm/is-positive@1.0.0')).toBeFalsy() fs.writeFileSync('../local-pkg/package.json', JSON.stringify({ name: 'local-pkg', version: '1.0.0', dependencies: { 'is-positive': '1.0.0', }, }), 'utf8') await install(manifest, testDefaults()) expect(fs.existsSync('./node_modules/.pnpm/is-positive@1.0.0')).toBeTruthy() lockfile = project.readLockfile() expect(lockfile).toMatchObject({ snapshots: { 'local-pkg@file:../local-pkg': { dependencies: { 'is-positive': '1.0.0', }, }, }, packages: { 'local-pkg@file:../local-pkg': { resolution: { directory: '../local-pkg', type: 'directory' }, }, }, }) // update dependency fs.writeFileSync('../local-pkg/package.json', JSON.stringify({ name: 'local-pkg', version: '1.0.0', dependencies: { 'is-positive': '2.0.0', }, }), 'utf8') await install(manifest, testDefaults()) expect(fs.existsSync('./node_modules/.pnpm/is-positive@2.0.0')).toBeTruthy() lockfile = project.readLockfile() expect(lockfile).toMatchObject({ packages: { 'local-pkg@file:../local-pkg': { resolution: { directory: '../local-pkg', type: 'directory' }, }, }, snapshots: { 'local-pkg@file:../local-pkg': { dependencies: { 'is-positive': '2.0.0', }, }, }, lockfileVersion: LOCKFILE_VERSION, }) }) test('local directory is not relinked if disableRelinkLocalDirDeps is set to true', async () => { prepareEmpty() fs.mkdirSync('pkg') fs.writeFileSync('pkg/index.js', 'hello', 'utf8') fs.writeFileSync('pkg/package.json', '{"name": "pkg"}', 'utf8') const { updatedManifest: manifest } = await addDependenciesToPackage({}, ['file:./pkg'], testDefaults()) fs.writeFileSync('pkg/new.js', 'hello', 'utf8') await addDependenciesToPackage(manifest, ['is-odd@1.0.0'], testDefaults({ disableRelinkLocalDirDeps: true })) expect(fs.readdirSync('node_modules/pkg').sort()).toStrictEqual(['index.js', 'package.json']) await install(manifest, testDefaults({ frozenLockfile: true, disableRelinkLocalDirDeps: true })) expect(fs.readdirSync('node_modules/pkg').sort()).toStrictEqual(['index.js', 'package.json']) })