import { promisify } from 'util' import { promises as fs } from 'fs' import path from 'path' import { addDependenciesToPackage, install, link, linkFromGlobal, linkToGlobal, } from '@pnpm/core' import { pathToLocalPkg } from '@pnpm/test-fixtures' import { prepareEmpty } from '@pnpm/prepare' import { RootLog } from '@pnpm/core-loggers' import { isExecutable } from '@pnpm/assert-project' import exists from 'path-exists' import sinon from 'sinon' import writeJsonFile from 'write-json-file' import ncpCB from 'ncp' import symlink from 'symlink-dir' import { testDefaults } from './utils' const ncp = promisify(ncpCB.ncp) test('relative link', async () => { const project = prepareEmpty() const linkedPkgName = 'hello-world-js-bin' const linkedPkgPath = path.resolve('..', linkedPkgName) await ncp(pathToLocalPkg(linkedPkgName), linkedPkgPath) await link([`../${linkedPkgName}`], path.join(process.cwd(), 'node_modules'), await testDefaults({ dir: process.cwd(), manifest: { dependencies: { 'hello-world-js-bin': '*', }, }, })) await project.isExecutable('.bin/hello-world-js-bin') const wantedLockfile = await project.readLockfile() expect(wantedLockfile.dependencies['hello-world-js-bin']).toBe('link:../hello-world-js-bin') expect(wantedLockfile.specifiers['hello-world-js-bin']).toBe('*') const currentLockfile = await project.readCurrentLockfile() expect(currentLockfile.dependencies['hello-world-js-bin']).toBe('link:../hello-world-js-bin') }) test('relative link is linked by the name of the alias', async () => { const linkedPkgName = 'hello-world-js-bin' const project = prepareEmpty() const linkedPkgPath = path.resolve('..', linkedPkgName) await ncp(pathToLocalPkg(linkedPkgName), linkedPkgPath) await install({ dependencies: { hello: `link:../${linkedPkgName}`, }, }, await testDefaults()) await project.isExecutable('.bin/hello-world-js-bin') await project.has('hello') const wantedLockfile = await project.readLockfile() expect(wantedLockfile.dependencies).toStrictEqual({ hello: 'link:../hello-world-js-bin', }) }) test('relative link is not rewritten by argumentless install', async () => { const project = prepareEmpty() const linkedPkgName = 'hello-world-js-bin' const linkedPkgPath = path.resolve('..', linkedPkgName) const reporter = sinon.spy() const opts = await testDefaults() await ncp(pathToLocalPkg(linkedPkgName), linkedPkgPath) const manifest = await link( [linkedPkgPath], path.join(process.cwd(), 'node_modules'), { ...opts, dir: process.cwd(), manifest: {}, reporter, }) // eslint-disable-line @typescript-eslint/no-explicit-any expect(reporter.calledWithMatch({ added: { dependencyType: undefined, linkedFrom: linkedPkgPath, name: 'hello-world-js-bin', realName: 'hello-world-js-bin', version: '1.0.0', }, level: 'debug', name: 'pnpm:root', prefix: process.cwd(), } as RootLog)).toBeTruthy() await install(manifest, opts) expect(project.requireModule('hello-world-js-bin/package.json').isLocal).toBeTruthy() }) test('relative link is rewritten by named installation to regular dependency', async () => { const project = prepareEmpty() const linkedPkgName = 'hello-world-js-bin' const linkedPkgPath = path.resolve('..', linkedPkgName) const reporter = sinon.spy() const opts = await testDefaults({ fastUnpack: false }) await ncp(pathToLocalPkg(linkedPkgName), linkedPkgPath) let manifest = await link( [linkedPkgPath], path.join(process.cwd(), 'node_modules'), { ...opts, dir: process.cwd(), manifest: {}, reporter, } ) expect(reporter.calledWithMatch({ added: { dependencyType: undefined, linkedFrom: linkedPkgPath, name: 'hello-world-js-bin', realName: 'hello-world-js-bin', version: '1.0.0', }, level: 'debug', name: 'pnpm:root', prefix: process.cwd(), } as RootLog)).toBeTruthy() manifest = await addDependenciesToPackage(manifest, ['hello-world-js-bin'], opts) expect(manifest.dependencies).toStrictEqual({ 'hello-world-js-bin': '^1.0.0' }) expect(project.requireModule('hello-world-js-bin/package.json').isLocal).toBeFalsy() const wantedLockfile = await project.readLockfile() expect(wantedLockfile.dependencies['hello-world-js-bin']).toBe('1.0.0') const currentLockfile = await project.readCurrentLockfile() expect(currentLockfile.dependencies['hello-world-js-bin']).toBe('1.0.0') }) test('global link', async () => { const project = prepareEmpty() const projectPath = process.cwd() const linkedPkgName = 'hello-world-js-bin' const linkedPkgPath = path.resolve('..', linkedPkgName) await ncp(pathToLocalPkg(linkedPkgName), linkedPkgPath) const opts = await testDefaults() process.chdir(linkedPkgPath) const globalDir = path.resolve('..', 'global') const globalBin = path.resolve('..', 'global', 'bin') await linkToGlobal(process.cwd(), { ...opts, globalDir, globalBin, manifest: {} }) // eslint-disable-line @typescript-eslint/no-explicit-any await isExecutable((value, comment) => expect(value).toBeTruthy(), path.join(globalBin, 'hello-world-js-bin')) // bins of dependencies should not be linked, see issue https://github.com/pnpm/pnpm/issues/905 expect(await exists(path.join(globalBin, 'cowsay'))).toBeFalsy() // cowsay not linked expect(await exists(path.join(globalBin, 'cowthink'))).toBeFalsy() // cowthink not linked process.chdir(projectPath) await linkFromGlobal([linkedPkgName], process.cwd(), { ...opts, globalDir, manifest: {} }) // eslint-disable-line @typescript-eslint/no-explicit-any await project.isExecutable('.bin/hello-world-js-bin') }) test('failed linking should not create empty folder', async () => { prepareEmpty() const globalDir = path.resolve('..', 'global') try { await linkFromGlobal(['does-not-exist'], process.cwd(), await testDefaults({ globalDir, manifest: {} })) throw new Error('should have failed') } catch (err: any) { // eslint-disable-line expect(await exists(path.join(globalDir, 'node_modules', 'does-not-exist'))).toBeFalsy() } }) test('node_modules is pruned after linking', async () => { prepareEmpty() await writeJsonFile('../is-positive/package.json', { name: 'is-positive', version: '1.0.0' }) const manifest = await addDependenciesToPackage({}, ['is-positive@1.0.0'], await testDefaults()) expect(await exists('node_modules/.pnpm/is-positive@1.0.0/node_modules/is-positive/package.json')).toBeTruthy() await link(['../is-positive'], path.resolve('node_modules'), await testDefaults({ manifest, dir: process.cwd() })) expect(await exists('node_modules/.pnpm/is-positive@1.0.0/node_modules/is-positive/package.json')).toBeFalsy() }) test('relative link uses realpath when contained in a symlinked dir', async () => { prepareEmpty() // `process.cwd()` is now `.tmp/X/project`. await ncp(pathToLocalPkg('symlink-workspace'), path.resolve('../symlink-workspace')) const app1RelPath = '../symlink-workspace/app1' const app2RelPath = '../symlink-workspace/app2' const app1 = path.resolve(app1RelPath) const app2 = path.resolve(app2RelPath) const dest = path.join(app2, 'packages/public') const src = path.resolve(app1, 'packages/public') console.log(`${dest}->${src}`) // We must manually create the symlink so it works in Windows too. await symlink(src, dest) process.chdir(path.join(app2, '/packages/public/foo')) // `process.cwd()` is now `.tmp/X/symlink-workspace/app2/packages/public/foo`. const linkFrom = path.join(app1, '/packages/public/bar') const linkTo = path.join(app2, '/packages/public/foo', 'node_modules') await link([linkFrom], linkTo, await testDefaults({ manifest: {}, dir: process.cwd() })) const linkToRelLink = await fs.readlink(path.join(linkTo, 'bar')) if (process.platform === 'win32') { expect(path.relative(linkToRelLink, path.join(src, 'bar'))).toBe('') // link points to real location } else { expect(linkToRelLink).toBe('../../bar') // If we don't use real paths we get a link like this. expect(linkToRelLink).not.toBe('../../../../../app1/packages/public/bar') } }) test('throws error is package name is not defined', async () => { prepareEmpty() await writeJsonFile('../is-positive/package.json', { version: '1.0.0' }) const manifest = await addDependenciesToPackage({}, ['is-positive@1.0.0'], await testDefaults()) try { await link(['../is-positive'], path.resolve('node_modules'), await testDefaults({ manifest, dir: process.cwd() })) throw new Error('link package should fail') } catch (err: any) { // eslint-disable-line expect(err.message).toBe('Package in ../is-positive must have a name field to be linked') expect(err.code).toBe('ERR_PNPM_INVALID_PACKAGE_NAME') } }) // test.skip('relative link when an external lockfile is used', async () => { // const projects = prepare(t, [ // { // name: 'project', // version: '1.0.0', // dependencies: {}, // }, // ]) // const opts = await testDefaults({ lockfileDir: path.join('..') }) // await link([process.cwd()], path.resolve(process.cwd(), 'node_modules'), opts) // const lockfile = await readYamlFile(path.resolve('..', WANTED_LOCKFILE)) // t.deepEqual(lockfile && lockfile['importers'], { // project: { // dependencies: { // project: 'link:', // }, // specifiers: {}, // }, // }) // })