diff --git a/config/config/src/index.ts b/config/config/src/index.ts index 76808e04c5..ce309343eb 100644 --- a/config/config/src/index.ts +++ b/config/config/src/index.ts @@ -334,9 +334,9 @@ export async function getConfig (opts: { globalDirRoot = path.join(pnpmConfig.pnpmHomeDir, 'global') } pnpmConfig.globalPkgDir = path.join(globalDirRoot, GLOBAL_LAYOUT_VERSION) + pnpmConfig.dir = cwd if (cliOptions['global']) { delete pnpmConfig.workspaceDir - pnpmConfig.dir = pnpmConfig.globalPkgDir pnpmConfig.bin = npmConfig.get('global-bin-dir') ?? env.PNPM_HOME if (pnpmConfig.bin) { fs.mkdirSync(pnpmConfig.bin, { recursive: true }) @@ -382,11 +382,8 @@ export async function getConfig (opts: { if (pnpmConfig.enableGlobalVirtualStore == null) { pnpmConfig.enableGlobalVirtualStore = true } - } else { - pnpmConfig.dir = cwd - if (!pnpmConfig.bin) { - pnpmConfig.bin = path.join(pnpmConfig.dir, 'node_modules', '.bin') - } + } else if (!pnpmConfig.bin) { + pnpmConfig.bin = path.join(pnpmConfig.dir, 'node_modules', '.bin') } pnpmConfig.packageManager = packageManager diff --git a/global/commands/src/globalAdd.ts b/global/commands/src/globalAdd.ts index 2e9dd2d577..095a88ddd0 100644 --- a/global/commands/src/globalAdd.ts +++ b/global/commands/src/globalAdd.ts @@ -37,6 +37,10 @@ export async function handleGlobalAdd ( opts: GlobalAddOptions, params: string[] ): Promise { + // Resolve relative path selectors to absolute paths before the working + // directory is changed to the global install dir, otherwise "." or + // "./foo" would resolve against the temp install directory. + params = params.map((param) => resolveLocalParam(param, opts.dir)) const globalDir = opts.globalPkgDir! const globalBinDir = opts.bin! cleanOrphanedInstallDirs(globalDir) @@ -137,6 +141,22 @@ export async function handleGlobalAdd ( await linkBinsOfPackages(pkgs, globalBinDir, { excludeBins: binsToSkip }) } +function resolveLocalParam (param: string, baseDir: string): string { + for (const prefix of ['file:', 'link:']) { + if (param.startsWith(prefix)) { + const rest = param.slice(prefix.length) + if (rest.startsWith('.')) { + return prefix + path.resolve(baseDir, rest) + } + return param + } + } + if (param.startsWith('.')) { + return path.resolve(baseDir, param) + } + return param +} + async function removeExistingGlobalInstalls ( globalDir: string, globalBinDir: string, diff --git a/pnpm/src/cmd/root.ts b/pnpm/src/cmd/root.ts index ee037cd40f..7cdac30448 100644 --- a/pnpm/src/cmd/root.ts +++ b/pnpm/src/cmd/root.ts @@ -39,10 +39,11 @@ export async function handler ( opts: { dir: string global?: boolean + globalPkgDir?: string } ): Promise { if (opts.global) { - return `${opts.dir}\n` + return `${opts.globalPkgDir}\n` } return `${path.join(opts.dir, 'node_modules')}\n` } diff --git a/pnpm/test/install/global.ts b/pnpm/test/install/global.ts index 1aeb2ba43f..0cc5b1ac74 100644 --- a/pnpm/test/install/global.ts +++ b/pnpm/test/install/global.ts @@ -370,6 +370,51 @@ test('global add refuses to install when bin name conflicts with another global expect(findGlobalPkg(globalPkgDir(pnpmHome), 'pkg-a')).toBeTruthy() }) +test('global add from a local directory using "."', () => { + prepare() + const global = path.resolve('..', 'global') + const pnpmHome = path.join(global, 'pnpm') + fs.mkdirSync(pnpmHome, { recursive: true }) + + // Create a local package with a bin + const localPkg = path.resolve('..', 'my-local-tool') + fs.mkdirSync(localPkg, { recursive: true }) + fs.writeFileSync(path.join(localPkg, 'package.json'), JSON.stringify({ + name: 'my-local-tool', + version: '1.0.0', + bin: { 'my-local-tool': './index.js' }, + })) + fs.writeFileSync(path.join(localPkg, 'index.js'), '#!/usr/bin/env node\nconsole.log("hello")\n') + + const env = { + [PATH_NAME]: pnpmHome, + PNPM_HOME: pnpmHome, + XDG_DATA_HOME: global, + pnpm_config_store_dir: path.resolve('..', 'store'), + } + + // Install globally from within the package directory using "." + // This used to fail because "." was resolved relative to the temp install + // directory instead of the user's CWD. + execPnpmSync(['add', '-g', '.'], { cwd: localPkg, env, expectSuccess: true }) + + // Verify the package was installed globally + expect(findGlobalPkg(globalPkgDir(pnpmHome), 'my-local-tool')).toBeTruthy() + + // Verify the bin was linked + expect(fs.existsSync(path.join(pnpmHome, 'my-local-tool'))).toBeTruthy() + + // Install globally using a file: relative selector + execPnpmSync(['add', '-g', 'file:./'], { cwd: localPkg, env, expectSuccess: true }) + expect(findGlobalPkg(globalPkgDir(pnpmHome), 'my-local-tool')).toBeTruthy() + expect(fs.existsSync(path.join(pnpmHome, 'my-local-tool'))).toBeTruthy() + + // Install globally using a link: relative selector + execPnpmSync(['add', '-g', 'link:../my-local-tool'], { cwd: process.cwd(), env, expectSuccess: true }) + expect(findGlobalPkg(globalPkgDir(pnpmHome), 'my-local-tool')).toBeTruthy() + expect(fs.existsSync(path.join(pnpmHome, 'my-local-tool'))).toBeTruthy() +}) + test('global remove deletes install group and bin shims', async () => { prepare() const global = path.resolve('..', 'global')