mirror of
https://github.com/pnpm/pnpm.git
synced 2026-03-30 13:02:03 -04:00
fix: resolve relative path params in global add against CWD (#10848)
* fix: resolve relative path params in global add against CWD When running `pnpm -g add .`, the "." was resolved relative to the temporary install directory instead of the user's working directory. This happened because handleGlobalAdd switches opts.dir to a fresh temp directory before the dependency selectors are resolved. Now relative path params (., ./foo, ../bar, file:./foo, link:../bar) are resolved to absolute paths before the directory is switched. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: resolve relative local selectors against opts.dir instead of process.cwd() This fixes `pnpm -C <dir> -g add .` where the relative selector would incorrectly resolve against process.cwd() instead of the user's intended directory. Also adds test coverage for file: and link: prefixed selectors. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(config): stop overwriting dir to globalPkgDir for global commands Previously, `pnpmConfig.dir` was set to `globalPkgDir` when `--global` was used. This caused `opts.dir` to point to the global packages directory instead of the user's CWD, breaking `pnpm -g add .` because relative paths resolved against the wrong directory. Now `pnpmConfig.dir` is always set to the user's CWD. Global command handlers already use `opts.globalPkgDir` where they need the global packages directory. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: use globalPkgDir in pnpm root -g handler The root command handler was using opts.dir which no longer points to the global packages directory. Use opts.globalPkgDir instead. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -37,6 +37,10 @@ export async function handleGlobalAdd (
|
||||
opts: GlobalAddOptions,
|
||||
params: string[]
|
||||
): Promise<void> {
|
||||
// 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,
|
||||
|
||||
@@ -39,10 +39,11 @@ export async function handler (
|
||||
opts: {
|
||||
dir: string
|
||||
global?: boolean
|
||||
globalPkgDir?: string
|
||||
}
|
||||
): Promise<string> {
|
||||
if (opts.global) {
|
||||
return `${opts.dir}\n`
|
||||
return `${opts.globalPkgDir}\n`
|
||||
}
|
||||
return `${path.join(opts.dir, 'node_modules')}\n`
|
||||
}
|
||||
|
||||
@@ -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')
|
||||
|
||||
Reference in New Issue
Block a user