mirror of
https://github.com/pnpm/pnpm.git
synced 2025-12-23 23:29:17 -05:00
feat!: global bins should be created only in predefined locations (#4280)
This commit is contained in:
6
.changeset/tricky-moles-float.md
Normal file
6
.changeset/tricky-moles-float.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@pnpm/config": major
|
||||
"pnpm": major
|
||||
---
|
||||
|
||||
`pnpm install -g pkg` will add the global command only to a predefined location. pnpm will not try to add a bin to the global Node.js or npm folder. To set the global bin directory, either set the `PNPM_HOME` env variable or the [`global-bin-dir`](https://pnpm.io/npmrc#global-bin-dir) setting.
|
||||
@@ -34,14 +34,15 @@
|
||||
"dependencies": {
|
||||
"@pnpm/constants": "workspace:5.0.0",
|
||||
"@pnpm/error": "workspace:2.0.0",
|
||||
"@pnpm/global-bin-dir": "workspace:3.0.0",
|
||||
"@pnpm/pnpmfile": "workspace:1.2.4",
|
||||
"@pnpm/read-project-manifest": "workspace:2.0.11",
|
||||
"@pnpm/types": "workspace:7.9.0",
|
||||
"@zkochan/npm-conf": "2.0.2",
|
||||
"camelcase": "^6.2.0",
|
||||
"can-write-to-dir": "^1.1.1",
|
||||
"is-subdir": "^1.1.1",
|
||||
"normalize-registry-url": "2.0.0",
|
||||
"path-name": "^1.0.0",
|
||||
"ramda": "^0.27.1",
|
||||
"realpath-missing": "^1.1.0",
|
||||
"which": "^2.0.2"
|
||||
|
||||
33
packages/config/src/checkGlobalBinDir.ts
Normal file
33
packages/config/src/checkGlobalBinDir.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import path from 'path'
|
||||
import PnpmError from '@pnpm/error'
|
||||
import { sync as canWriteToDir } from 'can-write-to-dir'
|
||||
import PATH from 'path-name'
|
||||
|
||||
export function checkGlobalBinDir (
|
||||
globalBinDir: string,
|
||||
{ env, shouldAllowWrite }: { env: Record<string, string | undefined>, shouldAllowWrite?: boolean }
|
||||
): void {
|
||||
if (!env[PATH]) {
|
||||
throw new PnpmError('NO_PATH_ENV',
|
||||
`Couldn't find a global directory for executables because the "${PATH}" environment variable is not set.`)
|
||||
}
|
||||
const dirs = env[PATH]?.split(path.delimiter) ?? []
|
||||
if (!dirs.some((dir) => areDirsEqual(globalBinDir, dir))) {
|
||||
throw new PnpmError('GLOBAL_BIN_DIR_NOT_IN_PATH', `The configured global bin directory "${globalBinDir}" is not in PATH`)
|
||||
}
|
||||
if (shouldAllowWrite && !canWriteToDirAndExists(globalBinDir)) {
|
||||
throw new PnpmError('PNPM_DIR_NOT_WRITABLE', `The CLI has no write access to the pnpm home directory at ${globalBinDir}`)
|
||||
}
|
||||
}
|
||||
|
||||
const areDirsEqual = (dir1: string, dir2: string) =>
|
||||
path.relative(dir1, dir2) === ''
|
||||
|
||||
function canWriteToDirAndExists (dir: string) {
|
||||
try {
|
||||
return canWriteToDir(dir)
|
||||
} catch (err: any) { // eslint-disable-line
|
||||
if (err.code !== 'ENOENT') throw err
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@ import path from 'path'
|
||||
import fs from 'fs'
|
||||
import { LAYOUT_VERSION } from '@pnpm/constants'
|
||||
import PnpmError from '@pnpm/error'
|
||||
import globalBinDir from '@pnpm/global-bin-dir'
|
||||
import { requireHooks } from '@pnpm/pnpmfile'
|
||||
import { safeReadProjectManifestOnly } from '@pnpm/read-project-manifest'
|
||||
import camelcase from 'camelcase'
|
||||
@@ -12,6 +11,7 @@ import normalizeRegistryUrl from 'normalize-registry-url'
|
||||
import fromPairs from 'ramda/src/fromPairs'
|
||||
import realpathMissing from 'realpath-missing'
|
||||
import whichcb from 'which'
|
||||
import { checkGlobalBinDir } from './checkGlobalBinDir'
|
||||
import getScopeRegistries from './getScopeRegistries'
|
||||
import { getCacheDir, getConfigDir, getDataDir, getStateDir } from './dirs'
|
||||
import {
|
||||
@@ -131,8 +131,10 @@ export default async (
|
||||
rcOptionsTypes?: Record<string, unknown>
|
||||
workspaceDir?: string | undefined
|
||||
checkUnknownSetting?: boolean
|
||||
env?: Record<string, string | undefined>
|
||||
}
|
||||
): Promise<{ config: Config, warnings: string[] }> => {
|
||||
const env = opts.env ?? process.env
|
||||
const packageManager = opts.packageManager ?? { name: 'pnpm', version: 'undefined' }
|
||||
const cliOptions = opts.cliOptions ?? {}
|
||||
const warnings = new Array<string>()
|
||||
@@ -275,7 +277,6 @@ export default async (
|
||||
pnpmConfig.pnpmHomeDir = getDataDir(process)
|
||||
|
||||
if (cliOptions['global']) {
|
||||
const knownGlobalBinDirCandidates: string[] = []
|
||||
let globalDirRoot
|
||||
if (pnpmConfig['globalDir']) {
|
||||
globalDirRoot = pnpmConfig['globalDir']
|
||||
@@ -284,18 +285,14 @@ export default async (
|
||||
}
|
||||
pnpmConfig.dir = path.join(globalDirRoot, LAYOUT_VERSION.toString())
|
||||
|
||||
const npmConfigGlobalBinDir = npmConfig.get('global-bin-dir')
|
||||
if (typeof npmConfigGlobalBinDir === 'string') {
|
||||
fs.mkdirSync(npmConfigGlobalBinDir, { recursive: true })
|
||||
pnpmConfig.bin = npmConfigGlobalBinDir
|
||||
pnpmConfig.bin = npmConfig.get('global-bin-dir') ?? env.PNPM_HOME
|
||||
if (pnpmConfig.bin) {
|
||||
fs.mkdirSync(pnpmConfig.bin, { recursive: true })
|
||||
checkGlobalBinDir(pnpmConfig.bin, { env, shouldAllowWrite: opts.globalDirShouldAllowWrite })
|
||||
} else {
|
||||
pnpmConfig.bin = cliOptions.dir
|
||||
? (
|
||||
process.platform === 'win32'
|
||||
? cliOptions.dir
|
||||
: path.resolve(cliOptions.dir, 'bin')
|
||||
)
|
||||
: globalBinDir(knownGlobalBinDirCandidates, { shouldAllowWrite: opts.globalDirShouldAllowWrite === true })
|
||||
throw new PnpmError('NO_GLOBAL_BIN_DIR', 'Unable to find the global bin directory', {
|
||||
hint: 'Run "pnpm setup" to create it automatically, or set the global-bin-dir setting, or the PNPM_HOME env variable. The global bin directory should be in the PATH.',
|
||||
})
|
||||
}
|
||||
pnpmConfig.save = true
|
||||
pnpmConfig.allowNew = true
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
/// <reference path="../../../typings/index.d.ts"/>
|
||||
import path from 'path'
|
||||
import pathName from 'path-name'
|
||||
import { homedir } from 'os'
|
||||
import getConfig from '@pnpm/config'
|
||||
|
||||
const globalBinDir = path.join(homedir(), '.local', 'pnpm')
|
||||
const isWindows = process.platform === 'win32'
|
||||
|
||||
jest.mock('@zkochan/npm-conf/lib/conf', () => {
|
||||
@@ -34,12 +36,15 @@ test('respects global-bin-dir in npmrc', async () => {
|
||||
cliOptions: {
|
||||
global: true,
|
||||
},
|
||||
env: {
|
||||
[pathName]: `${globalBinDir}${path.delimiter}${process.env[pathName]!}`,
|
||||
},
|
||||
packageManager: {
|
||||
name: 'pnpm',
|
||||
version: '1.0.0',
|
||||
},
|
||||
})
|
||||
expect(config.bin).toBe(path.join(homedir(), '.local', 'pnpm'))
|
||||
expect(config.bin).toBe(globalBinDir)
|
||||
})
|
||||
|
||||
test('respects global-bin-dir rather than dir', async () => {
|
||||
@@ -48,10 +53,13 @@ test('respects global-bin-dir rather than dir', async () => {
|
||||
global: true,
|
||||
dir: __dirname,
|
||||
},
|
||||
env: {
|
||||
[pathName]: `${globalBinDir}${path.delimiter}${process.env[pathName]!}`,
|
||||
},
|
||||
packageManager: {
|
||||
name: 'pnpm',
|
||||
version: '1.0.0',
|
||||
},
|
||||
})
|
||||
expect(config.bin).toBe(path.join(homedir(), '.local', 'pnpm'))
|
||||
expect(config.bin).toBe(globalBinDir)
|
||||
})
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/// <reference path="../../../typings/index.d.ts"/>
|
||||
import { promises as fs } from 'fs'
|
||||
import path from 'path'
|
||||
import PATH from 'path-name'
|
||||
import getConfig from '@pnpm/config'
|
||||
import PnpmError from '@pnpm/error'
|
||||
import prepare, { prepareEmpty } from '@pnpm/prepare'
|
||||
@@ -17,6 +18,11 @@ delete process.env.npm_config_virtual_store_dir
|
||||
delete process.env.npm_config_shared_workspace_lockfile
|
||||
delete process.env.npm_config_side_effects_cache
|
||||
|
||||
const env = {
|
||||
PNPM_HOME: __dirname,
|
||||
[PATH]: __dirname,
|
||||
}
|
||||
|
||||
test('getConfig()', async () => {
|
||||
const { config } = await getConfig({
|
||||
cliOptions: {},
|
||||
@@ -39,6 +45,7 @@ test('throw error if --link-workspace-packages is used with --global', async ()
|
||||
global: true,
|
||||
'link-workspace-packages': true,
|
||||
},
|
||||
env,
|
||||
packageManager: {
|
||||
name: 'pnpm',
|
||||
version: '1.0.0',
|
||||
@@ -56,6 +63,7 @@ test('correct settings on global install', async () => {
|
||||
global: true,
|
||||
save: false,
|
||||
},
|
||||
env,
|
||||
packageManager: {
|
||||
name: 'pnpm',
|
||||
version: '1.0.0',
|
||||
@@ -71,6 +79,7 @@ test('throw error if --shared-workspace-lockfile is used with --global', async (
|
||||
global: true,
|
||||
'shared-workspace-lockfile': true,
|
||||
},
|
||||
env,
|
||||
packageManager: {
|
||||
name: 'pnpm',
|
||||
version: '1.0.0',
|
||||
@@ -89,6 +98,7 @@ test('throw error if --lockfile-dir is used with --global', async () => {
|
||||
global: true,
|
||||
'lockfile-dir': '/home/src',
|
||||
},
|
||||
env,
|
||||
packageManager: {
|
||||
name: 'pnpm',
|
||||
version: '1.0.0',
|
||||
@@ -107,6 +117,7 @@ test('throw error if --hoist-pattern is used with --global', async () => {
|
||||
global: true,
|
||||
'hoist-pattern': 'eslint',
|
||||
},
|
||||
env,
|
||||
packageManager: {
|
||||
name: 'pnpm',
|
||||
version: '1.0.0',
|
||||
@@ -125,6 +136,7 @@ test('throw error if --virtual-store-dir is used with --global', async () => {
|
||||
global: true,
|
||||
'virtual-store-dir': 'pkgs',
|
||||
},
|
||||
env,
|
||||
packageManager: {
|
||||
name: 'pnpm',
|
||||
version: '1.0.0',
|
||||
@@ -167,6 +179,7 @@ test('when using --global, link-workspace-packages, shared-workspace-shrinwrap a
|
||||
cliOptions: {
|
||||
global: true,
|
||||
},
|
||||
env,
|
||||
packageManager: {
|
||||
name: 'pnpm',
|
||||
version: '1.0.0',
|
||||
|
||||
@@ -18,9 +18,6 @@
|
||||
{
|
||||
"path": "../error"
|
||||
},
|
||||
{
|
||||
"path": "../global-bin-dir"
|
||||
},
|
||||
{
|
||||
"path": "../pnpmfile"
|
||||
},
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
# @pnpm/global-bin-dir
|
||||
|
||||
## 3.0.0
|
||||
|
||||
### Major Changes
|
||||
|
||||
- 47a1e9696: Always prefer the pnpm home directory, when searching for global bin location.
|
||||
|
||||
## 2.0.0
|
||||
|
||||
### Major Changes
|
||||
|
||||
- 97b986fbc: Node.js 10 support is dropped. At least Node.js 12.17 is required for the package to work.
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [97b986fbc]
|
||||
- @pnpm/error@2.0.0
|
||||
|
||||
## 1.2.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 941c5e8de: `npx pnpm install --global pnpm` should not install pnpm to the temporary directory of npx.
|
||||
|
||||
## 1.2.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [0c5f1bcc9]
|
||||
- @pnpm/error@1.4.0
|
||||
|
||||
## 1.2.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 846887de3: When searching for a suitable global bin directory, search for symlinked node, npm, pnpm commands, not only command files.
|
||||
|
||||
## 1.2.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [75a36deba]
|
||||
- @pnpm/error@1.3.1
|
||||
|
||||
## 1.2.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 4d4d22b63: A directory is considered a valid global executable directory for pnpm, if it contains a node, or npm, or pnpm executable, not directory.
|
||||
|
||||
## 1.2.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [6d480dd7a]
|
||||
- @pnpm/error@1.3.0
|
||||
|
||||
## 1.2.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- ad69677a7: Add a new optional argument. When the argument is `false`, a global bin directory is returned even if the process has no write access to it.
|
||||
|
||||
## 1.1.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 245221baa: When searching a suitable global executables directory, take any directory from the PATH that has a node, pnpm, or npm command in it.
|
||||
|
||||
## 1.1.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 915828b46: `globalBinDir()` may accept an array of suitable executable directories.
|
||||
If one of these directories is in PATH and has bigger priority than the
|
||||
npm/pnpm/nodejs directories, then that directory will be used.
|
||||
|
||||
## 1.0.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 2c190d49d: When looking for suitable directories for global executables, ignore case.
|
||||
|
||||
When comparing to the currently running Node.js executable directory,
|
||||
ignore any trailing slash. `/foo/bar` is the same as `/foo/bar/`.
|
||||
|
||||
## 1.0.0
|
||||
|
||||
### Major Changes
|
||||
|
||||
- 1146b76d2: Finds a directory that is in PATH and we have permission to write to it.
|
||||
@@ -1,15 +0,0 @@
|
||||
# @pnpm/global-bin-dir
|
||||
|
||||
> Finds a directory that is in PATH and we have permission to write to it
|
||||
|
||||
[](https://www.npmjs.com/package/@pnpm/global-bin-dir)
|
||||
|
||||
## Installation
|
||||
|
||||
```sh
|
||||
pnpm add @pnpm/global-bin-dir
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
@@ -1 +0,0 @@
|
||||
module.exports = require('../../jest.config.js')
|
||||
@@ -1,41 +0,0 @@
|
||||
{
|
||||
"name": "@pnpm/global-bin-dir",
|
||||
"version": "3.0.0",
|
||||
"description": "Finds a directory that is in PATH and we have permission to write to i",
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/index.d.ts",
|
||||
"files": [
|
||||
"lib",
|
||||
"!*.map"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12.17"
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "eslint src/**/*.ts test/**/*.ts",
|
||||
"test": "pnpm run compile && pnpm run _test",
|
||||
"prepublishOnly": "pnpm run compile",
|
||||
"compile": "tsc --build && pnpm run lint -- --fix",
|
||||
"_test": "jest"
|
||||
},
|
||||
"repository": "https://github.com/pnpm/pnpm/blob/main/packages/global-bin-dir",
|
||||
"keywords": [
|
||||
"pnpm6",
|
||||
"pnpm"
|
||||
],
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/pnpm/pnpm/issues"
|
||||
},
|
||||
"homepage": "https://github.com/pnpm/pnpm/blob/main/packages/global-bin-dir#readme",
|
||||
"funding": "https://opencollective.com/pnpm",
|
||||
"dependencies": {
|
||||
"@pnpm/error": "workspace:2.0.0",
|
||||
"can-write-to-dir": "^1.1.1",
|
||||
"path-name": "^1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@pnpm/global-bin-dir": "workspace:3.0.0",
|
||||
"is-windows": "^1.0.2"
|
||||
}
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import PnpmError from '@pnpm/error'
|
||||
import { sync as canWriteToDir } from 'can-write-to-dir'
|
||||
import PATH from 'path-name'
|
||||
|
||||
export default function (
|
||||
knownCandidates: string[] = [],
|
||||
{ shouldAllowWrite = true }: { shouldAllowWrite?: boolean } = {}
|
||||
) {
|
||||
if (!process.env[PATH]) {
|
||||
throw new PnpmError('NO_PATH_ENV',
|
||||
`Couldn't find a global directory for executables because the "${PATH}" environment variable is not set.`)
|
||||
}
|
||||
const dirs = process.env[PATH]?.split(path.delimiter) ?? []
|
||||
const nodeBinDir = path.dirname(process.execPath)
|
||||
return pickBestGlobalBinDir(dirs, [
|
||||
...knownCandidates,
|
||||
nodeBinDir,
|
||||
], shouldAllowWrite)
|
||||
}
|
||||
|
||||
const areDirsEqual = (dir1: string, dir2: string) =>
|
||||
path.relative(dir1, dir2) === ''
|
||||
|
||||
function pickBestGlobalBinDir (
|
||||
dirs: string[],
|
||||
knownCandidates: string[],
|
||||
shouldAllowWrite: boolean
|
||||
) {
|
||||
const noWriteAccessDirs = [] as string[]
|
||||
const pnpmDir = dirs.find((dir) => isUnderDir('pnpm', dir.toLowerCase()))
|
||||
if (pnpmDir != null) {
|
||||
if (canWriteToDirAndExists(pnpmDir)) return pnpmDir
|
||||
throw new PnpmError('PNPM_DIR_NOT_WRITABLE', `The CLI has no write access to the pnpm home directory at ${pnpmDir}`)
|
||||
}
|
||||
for (const dir of dirs) {
|
||||
const lowCaseDir = dir.toLowerCase()
|
||||
if ((
|
||||
isUnderDir('node', lowCaseDir) ||
|
||||
isUnderDir('nodejs', lowCaseDir) ||
|
||||
isUnderDir('npm', lowCaseDir) ||
|
||||
knownCandidates.some((candidate) => areDirsEqual(candidate, dir)) ||
|
||||
dirHasNodeRelatedCommand(dir)
|
||||
) && !isUnderDir('_npx', lowCaseDir)) {
|
||||
if (canWriteToDirAndExists(dir)) return dir
|
||||
noWriteAccessDirs.push(dir)
|
||||
}
|
||||
}
|
||||
if (noWriteAccessDirs.length === 0) {
|
||||
throw new PnpmError('NO_GLOBAL_BIN_DIR', "Couldn't find a suitable global executables directory.", {
|
||||
hint: `There should be a node, nodejs, npm, or pnpm directory in the "${PATH}" environment variable`,
|
||||
})
|
||||
}
|
||||
if (shouldAllowWrite) {
|
||||
throw new PnpmError('GLOBAL_BIN_DIR_PERMISSION', 'No write access to the found global executable directories', {
|
||||
hint: `The found directories:
|
||||
${noWriteAccessDirs.join('\n')}`,
|
||||
})
|
||||
}
|
||||
return noWriteAccessDirs[0]
|
||||
}
|
||||
|
||||
const NODE_RELATED_COMMANDS = new Set(['pnpm', 'npm', 'node'])
|
||||
|
||||
function dirHasNodeRelatedCommand (dir: string) {
|
||||
try {
|
||||
return fs.readdirSync(dir, { withFileTypes: true })
|
||||
// We are searching for files or symlinks, not directories
|
||||
.filter((entry) => !entry.isDirectory())
|
||||
.map(({ name }) => name.toLowerCase())
|
||||
.some((file) => NODE_RELATED_COMMANDS.has(file.split('.')[0]))
|
||||
} catch (err: any) { // eslint-disable-line
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function isUnderDir (dir: string, target: string) {
|
||||
target = target.endsWith(path.sep) ? target : `${target}${path.sep}`
|
||||
return target.includes(`${path.sep}${dir}${path.sep}`) ||
|
||||
target.includes(`${path.sep}.${dir}${path.sep}`)
|
||||
}
|
||||
|
||||
function canWriteToDirAndExists (dir: string) {
|
||||
try {
|
||||
return canWriteToDir(dir)
|
||||
} catch (err: any) { // eslint-disable-line
|
||||
if (err.code !== 'ENOENT') throw err
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -1,214 +0,0 @@
|
||||
|
||||
import path from 'path'
|
||||
import PnpmError from '@pnpm/error'
|
||||
import { sync as _canWriteToDir } from 'can-write-to-dir'
|
||||
|
||||
import isWindows from 'is-windows'
|
||||
import globalBinDir from '../src/index'
|
||||
|
||||
const makePath =
|
||||
isWindows()
|
||||
? (...paths: string[]) => `C:\\${path.join(...paths)}`
|
||||
: (...paths: string[]) => `/${path.join(...paths)}`
|
||||
|
||||
let canWriteToDir!: typeof _canWriteToDir
|
||||
let readdirSync = (dir: string) => [] as Array<{ name: string, isDirectory: () => boolean }>
|
||||
const FAKE_PATH = 'FAKE_PATH'
|
||||
|
||||
function makeFileEntry (name: string) {
|
||||
return { name, isDirectory: () => false }
|
||||
}
|
||||
|
||||
function makeDirEntry (name: string) {
|
||||
return { name, isDirectory: () => true }
|
||||
}
|
||||
|
||||
jest.mock('can-write-to-dir', () => ({
|
||||
sync: (dir: string) => canWriteToDir(dir),
|
||||
}))
|
||||
|
||||
jest.mock('fs', () => {
|
||||
const originalModule = jest.requireActual('fs')
|
||||
return {
|
||||
...originalModule,
|
||||
readdirSync: (dir: string) => readdirSync(dir),
|
||||
}
|
||||
})
|
||||
|
||||
let originalPath: string | undefined
|
||||
|
||||
beforeEach(() => {
|
||||
originalPath = process.env[FAKE_PATH]
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
process.env[FAKE_PATH] = originalPath
|
||||
})
|
||||
|
||||
jest.mock('path-name', () => 'FAKE_PATH')
|
||||
|
||||
const userGlobalBin = makePath('usr', 'local', 'bin')
|
||||
const nodeGlobalBin = makePath('home', 'z', '.nvs', 'node', '12.0.0', 'x64', 'bin')
|
||||
const npmGlobalBin = makePath('home', 'z', '.npm')
|
||||
const pnpmGlobalBin = makePath('home', 'z', '.pnpm')
|
||||
const npxGlobalBin = makePath('home', 'z', '.npm', '_npx', '123')
|
||||
const otherDir = makePath('some', 'dir')
|
||||
const currentExecDir = makePath('current', 'exec')
|
||||
const dirWithTrailingSlash = `${makePath('current', 'slash')}${path.sep}`
|
||||
const BIG_PATH = [
|
||||
npxGlobalBin,
|
||||
userGlobalBin,
|
||||
nodeGlobalBin,
|
||||
npmGlobalBin,
|
||||
otherDir,
|
||||
currentExecDir,
|
||||
dirWithTrailingSlash,
|
||||
].join(path.delimiter)
|
||||
|
||||
test('prefer the pnpm home directory', () => {
|
||||
process.env[FAKE_PATH] = [
|
||||
npmGlobalBin,
|
||||
currentExecDir,
|
||||
nodeGlobalBin,
|
||||
pnpmGlobalBin,
|
||||
].join(path.delimiter)
|
||||
canWriteToDir = () => true
|
||||
expect(globalBinDir()).toStrictEqual(pnpmGlobalBin)
|
||||
})
|
||||
|
||||
test('fail if there is no write access to the pnpm home directory', () => {
|
||||
process.env[FAKE_PATH] = [
|
||||
npmGlobalBin,
|
||||
currentExecDir,
|
||||
nodeGlobalBin,
|
||||
pnpmGlobalBin,
|
||||
].join(path.delimiter)
|
||||
canWriteToDir = (dir) => dir !== pnpmGlobalBin
|
||||
expect(() => globalBinDir()).toThrow(`The CLI has no write access to the pnpm home directory at ${pnpmGlobalBin}`)
|
||||
})
|
||||
|
||||
test('prefer a directory that has "nodejs" or "npm" in the path', () => {
|
||||
process.env[FAKE_PATH] = BIG_PATH
|
||||
canWriteToDir = () => true
|
||||
expect(globalBinDir()).toStrictEqual(nodeGlobalBin)
|
||||
|
||||
canWriteToDir = (dir) => dir !== nodeGlobalBin
|
||||
expect(globalBinDir()).toStrictEqual(npmGlobalBin)
|
||||
})
|
||||
|
||||
test('prefer directory that is passed in as a known suitable location', () => {
|
||||
process.env[FAKE_PATH] = BIG_PATH
|
||||
canWriteToDir = () => true
|
||||
expect(globalBinDir([userGlobalBin])).toStrictEqual(userGlobalBin)
|
||||
})
|
||||
|
||||
test("ignore directories that don't exist", () => {
|
||||
process.env[FAKE_PATH] = BIG_PATH
|
||||
canWriteToDir = (dir) => {
|
||||
if (dir === nodeGlobalBin) {
|
||||
const err = new Error('Not exists')
|
||||
err['code'] = 'ENOENT'
|
||||
throw err
|
||||
}
|
||||
return true
|
||||
}
|
||||
expect(globalBinDir()).toEqual(npmGlobalBin)
|
||||
})
|
||||
|
||||
test('prefer the directory of the currently executed nodejs command', () => {
|
||||
process.env[FAKE_PATH] = BIG_PATH
|
||||
const originalExecPath = process.execPath
|
||||
process.execPath = path.join(currentExecDir, 'n')
|
||||
canWriteToDir = (dir) => dir !== nodeGlobalBin && dir !== npmGlobalBin && dir !== pnpmGlobalBin
|
||||
expect(globalBinDir()).toEqual(currentExecDir)
|
||||
|
||||
process.execPath = path.join(dirWithTrailingSlash, 'n')
|
||||
expect(globalBinDir()).toEqual(dirWithTrailingSlash)
|
||||
|
||||
process.execPath = originalExecPath
|
||||
})
|
||||
|
||||
test('when the process has no write access to any of the suitable directories, throw an error', () => {
|
||||
process.env[FAKE_PATH] = BIG_PATH
|
||||
canWriteToDir = (dir) => dir === otherDir
|
||||
let err!: PnpmError
|
||||
try {
|
||||
globalBinDir()
|
||||
} catch (_err: any) { // eslint-disable-line
|
||||
err = _err
|
||||
}
|
||||
expect(err).toBeDefined()
|
||||
expect(err.code).toEqual('ERR_PNPM_GLOBAL_BIN_DIR_PERMISSION')
|
||||
})
|
||||
|
||||
test('when the process has no write access to any of the suitable directories, but opts.shouldAllowWrite is false, return the first match', () => {
|
||||
process.env[FAKE_PATH] = BIG_PATH
|
||||
canWriteToDir = (dir) => dir === otherDir
|
||||
expect(globalBinDir([], { shouldAllowWrite: false })).toEqual(nodeGlobalBin)
|
||||
})
|
||||
|
||||
test('throw an exception if non of the directories in the PATH are suitable', () => {
|
||||
process.env[FAKE_PATH] = [otherDir].join(path.delimiter)
|
||||
canWriteToDir = () => true
|
||||
let err!: PnpmError
|
||||
try {
|
||||
globalBinDir()
|
||||
} catch (_err: any) { // eslint-disable-line
|
||||
err = _err
|
||||
}
|
||||
expect(err).toBeDefined()
|
||||
expect(err.code).toEqual('ERR_PNPM_NO_GLOBAL_BIN_DIR')
|
||||
})
|
||||
|
||||
test('throw exception if PATH is not set', () => {
|
||||
delete process.env[FAKE_PATH]
|
||||
expect(() => globalBinDir()).toThrow(/Couldn't find a global directory/)
|
||||
})
|
||||
|
||||
test('prefer a directory that has "Node" in the path', () => {
|
||||
const capitalizedNodeGlobalBin = makePath('home', 'z', '.nvs', 'Node', '12.0.0', 'x64', 'bin')
|
||||
process.env[FAKE_PATH] = capitalizedNodeGlobalBin
|
||||
|
||||
canWriteToDir = () => true
|
||||
expect(globalBinDir()).toEqual(capitalizedNodeGlobalBin)
|
||||
})
|
||||
|
||||
test('select a directory that has a node command in it', () => {
|
||||
const dir1 = makePath('foo')
|
||||
const dir2 = makePath('bar')
|
||||
process.env[FAKE_PATH] = [
|
||||
dir1,
|
||||
dir2,
|
||||
].join(path.delimiter)
|
||||
|
||||
canWriteToDir = () => true
|
||||
readdirSync = (dir) => dir === dir2 ? [makeFileEntry('node')] : []
|
||||
expect(globalBinDir()).toEqual(dir2)
|
||||
})
|
||||
|
||||
test('do not select a directory that has a node directory in it', () => {
|
||||
const dir1 = makePath('foo')
|
||||
const dir2 = makePath('bar')
|
||||
process.env[FAKE_PATH] = [
|
||||
dir1,
|
||||
dir2,
|
||||
].join(path.delimiter)
|
||||
|
||||
canWriteToDir = () => true
|
||||
readdirSync = (dir) => dir === dir2 ? [makeDirEntry('node')] : []
|
||||
|
||||
expect(() => globalBinDir()).toThrow(/Couldn't find a suitable/)
|
||||
})
|
||||
|
||||
test('select a directory that has a node.bat command in it', () => {
|
||||
const dir1 = makePath('foo')
|
||||
const dir2 = makePath('bar')
|
||||
process.env[FAKE_PATH] = [
|
||||
dir1,
|
||||
dir2,
|
||||
].join(path.delimiter)
|
||||
|
||||
canWriteToDir = () => true
|
||||
readdirSync = (dir) => dir === dir2 ? [makeFileEntry('node.bat')] : []
|
||||
expect(globalBinDir()).toEqual(dir2)
|
||||
})
|
||||
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"extends": "@pnpm/tsconfig",
|
||||
"compilerOptions": {
|
||||
"outDir": "lib",
|
||||
"rootDir": "src"
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"../../typings/**/*.d.ts"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../error"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"test/**/*.ts",
|
||||
"../../typings/**/*.d.ts"
|
||||
]
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { promises as fs } from 'fs'
|
||||
import path from 'path'
|
||||
import PATH_NAME from 'path-name'
|
||||
import { tempDir } from '@pnpm/prepare'
|
||||
import PATH from 'path-name'
|
||||
import { execPnpmSync } from './utils'
|
||||
|
||||
test('pnpm bin', async () => {
|
||||
@@ -17,8 +17,13 @@ test('pnpm bin', async () => {
|
||||
test('pnpm bin -g', async () => {
|
||||
tempDir()
|
||||
|
||||
const result = execPnpmSync(['bin', '-g'])
|
||||
const env = {
|
||||
PNPM_HOME: process.cwd(),
|
||||
[PATH_NAME]: process.cwd(),
|
||||
}
|
||||
|
||||
const result = execPnpmSync(['bin', '-g'], { env })
|
||||
|
||||
expect(result.status).toStrictEqual(0)
|
||||
expect(process.env[PATH]!).toContain(result.stdout.toString().trim())
|
||||
expect(result.stdout.toString().trim()).toEqual(env.PNPM_HOME)
|
||||
})
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import path from 'path'
|
||||
import { promises as fs } from 'fs'
|
||||
import PATH_NAME from 'path-name'
|
||||
import fs from 'fs'
|
||||
import { LAYOUT_VERSION } from '@pnpm/constants'
|
||||
import prepare from '@pnpm/prepare'
|
||||
import isWindows from 'is-windows'
|
||||
@@ -12,9 +13,10 @@ import {
|
||||
test('global installation', async () => {
|
||||
prepare()
|
||||
const global = path.resolve('..', 'global')
|
||||
await fs.mkdir(global)
|
||||
const pnpmHome = path.join(global, 'pnpm')
|
||||
fs.mkdirSync(global)
|
||||
|
||||
const env = { XDG_DATA_HOME: global }
|
||||
const env = { [PATH_NAME]: pnpmHome, PNPM_HOME: pnpmHome, XDG_DATA_HOME: global }
|
||||
|
||||
await execPnpm(['install', '--global', 'is-positive'], { env })
|
||||
|
||||
@@ -33,8 +35,11 @@ test('global installation', async () => {
|
||||
|
||||
test('global installation to custom directory with --global-dir', async () => {
|
||||
prepare()
|
||||
const global = path.resolve('..', 'global')
|
||||
const pnpmHome = path.join(global, 'pnpm')
|
||||
const env = { [PATH_NAME]: pnpmHome, PNPM_HOME: pnpmHome }
|
||||
|
||||
await execPnpm(['add', '--global', '--global-dir=../global', 'is-positive'])
|
||||
await execPnpm(['add', '--global', '--global-dir=../global', 'is-positive'], { env })
|
||||
|
||||
const { default: isPositive } = await import(path.resolve(`../global/${LAYOUT_VERSION}/node_modules/is-positive`))
|
||||
expect(typeof isPositive).toBe('function')
|
||||
@@ -45,9 +50,10 @@ test('always install latest when doing global installation without spec', async
|
||||
await addDistTag('peer-c', '2.0.0', 'latest')
|
||||
|
||||
const global = path.resolve('..', 'global')
|
||||
await fs.mkdir(global)
|
||||
const pnpmHome = path.join(global, 'pnpm')
|
||||
fs.mkdirSync(global)
|
||||
|
||||
const env = { XDG_DATA_HOME: global }
|
||||
const env = { [PATH_NAME]: pnpmHome, PNPM_HOME: pnpmHome, XDG_DATA_HOME: global }
|
||||
|
||||
await execPnpm(['install', '-g', 'peer-c@1'], { env })
|
||||
await execPnpm(['install', '-g', 'peer-c'], { env })
|
||||
@@ -68,9 +74,14 @@ test('run lifecycle events of global packages in correct working directory', asy
|
||||
|
||||
prepare()
|
||||
const global = path.resolve('..', 'global')
|
||||
await fs.mkdir(global)
|
||||
const pnpmHome = path.join(global, 'pnpm')
|
||||
fs.mkdirSync(global)
|
||||
|
||||
const env = { XDG_DATA_HOME: global }
|
||||
const env = {
|
||||
[PATH_NAME]: `${pnpmHome}${path.delimiter}${process.env[PATH_NAME]!}`,
|
||||
PNPM_HOME: pnpmHome,
|
||||
XDG_DATA_HOME: global,
|
||||
}
|
||||
|
||||
await execPnpm(['install', '-g', 'postinstall-calls-pnpm@1.0.0'], { env })
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import path from 'path'
|
||||
import PATH_NAME from 'path-name'
|
||||
import { promises as fs } from 'fs'
|
||||
import prepare from '@pnpm/prepare'
|
||||
import isWindows from 'is-windows'
|
||||
@@ -22,9 +23,14 @@ skipOnWindows('self-update stops the store server', async () => {
|
||||
expect(serverJson.connectionOptions).toBeTruthy()
|
||||
|
||||
const global = path.resolve('global')
|
||||
const pnpmHome = path.join(global, 'pnpm')
|
||||
await fs.mkdir(global)
|
||||
|
||||
const env = { XDG_DATA_HOME: global }
|
||||
const env = {
|
||||
[PATH_NAME]: `${pnpmHome}${path.delimiter}${process.env[PATH_NAME]!}`,
|
||||
PNPM_HOME: pnpmHome,
|
||||
XDG_DATA_HOME: global,
|
||||
}
|
||||
|
||||
await execPnpm(['install', '-g', 'pnpm', '--store-dir', path.resolve('..', 'store')], { env })
|
||||
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { promises as fs } from 'fs'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import PATH_NAME from 'path-name'
|
||||
import { LAYOUT_VERSION } from '@pnpm/constants'
|
||||
import { tempDir } from '@pnpm/prepare'
|
||||
import { execPnpmSync } from './utils'
|
||||
|
||||
test('pnpm root', async () => {
|
||||
tempDir()
|
||||
await fs.writeFile('package.json', '{}', 'utf8')
|
||||
fs.writeFileSync('package.json', '{}', 'utf8')
|
||||
|
||||
const result = execPnpmSync(['root'])
|
||||
|
||||
@@ -19,9 +20,10 @@ test('pnpm root -g', async () => {
|
||||
tempDir()
|
||||
|
||||
const global = path.resolve('global')
|
||||
await fs.mkdir(global)
|
||||
const pnpmHome = path.join(global, 'pnpm')
|
||||
fs.mkdirSync(global)
|
||||
|
||||
const env = { XDG_DATA_HOME: global }
|
||||
const env = { [PATH_NAME]: pnpmHome, PNPM_HOME: pnpmHome, XDG_DATA_HOME: global }
|
||||
|
||||
const result = execPnpmSync(['root', '-g'], { env })
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ test('uninstall global package with its bin files', async () => {
|
||||
|
||||
const env = {
|
||||
NPM_CONFIG_PREFIX: global,
|
||||
PNPM_HOME: globalBin,
|
||||
[PATH]: `${globalBin}${path.delimiter}${process.env[PATH] ?? ''}`,
|
||||
}
|
||||
if (process.env.APPDATA) env['APPDATA'] = global
|
||||
|
||||
23
pnpm-lock.yaml
generated
23
pnpm-lock.yaml
generated
@@ -341,7 +341,6 @@ importers:
|
||||
'@pnpm/config': workspace:13.13.0
|
||||
'@pnpm/constants': workspace:5.0.0
|
||||
'@pnpm/error': workspace:2.0.0
|
||||
'@pnpm/global-bin-dir': workspace:3.0.0
|
||||
'@pnpm/pnpmfile': workspace:1.2.4
|
||||
'@pnpm/prepare': workspace:*
|
||||
'@pnpm/read-project-manifest': workspace:2.0.11
|
||||
@@ -350,8 +349,10 @@ importers:
|
||||
'@types/which': ^2.0.0
|
||||
'@zkochan/npm-conf': 2.0.2
|
||||
camelcase: ^6.2.0
|
||||
can-write-to-dir: ^1.1.1
|
||||
is-subdir: ^1.1.1
|
||||
normalize-registry-url: 2.0.0
|
||||
path-name: ^1.0.0
|
||||
ramda: ^0.27.1
|
||||
realpath-missing: ^1.1.0
|
||||
symlink-dir: ^5.0.0
|
||||
@@ -359,14 +360,15 @@ importers:
|
||||
dependencies:
|
||||
'@pnpm/constants': link:../constants
|
||||
'@pnpm/error': link:../error
|
||||
'@pnpm/global-bin-dir': link:../global-bin-dir
|
||||
'@pnpm/pnpmfile': link:../pnpmfile
|
||||
'@pnpm/read-project-manifest': link:../read-project-manifest
|
||||
'@pnpm/types': link:../types
|
||||
'@zkochan/npm-conf': 2.0.2
|
||||
camelcase: 6.3.0
|
||||
can-write-to-dir: 1.1.1
|
||||
is-subdir: 1.2.0
|
||||
normalize-registry-url: 2.0.0
|
||||
path-name: 1.0.0
|
||||
ramda: 0.27.2
|
||||
realpath-missing: 1.1.0
|
||||
which: 2.0.2
|
||||
@@ -1024,21 +1026,6 @@ importers:
|
||||
'@types/semver': 7.3.9
|
||||
is-windows: 1.0.2
|
||||
|
||||
packages/global-bin-dir:
|
||||
specifiers:
|
||||
'@pnpm/error': workspace:2.0.0
|
||||
'@pnpm/global-bin-dir': workspace:3.0.0
|
||||
can-write-to-dir: ^1.1.1
|
||||
is-windows: ^1.0.2
|
||||
path-name: ^1.0.0
|
||||
dependencies:
|
||||
'@pnpm/error': link:../error
|
||||
can-write-to-dir: 1.1.1
|
||||
path-name: 1.0.0
|
||||
devDependencies:
|
||||
'@pnpm/global-bin-dir': 'link:'
|
||||
is-windows: 1.0.2
|
||||
|
||||
packages/graceful-fs:
|
||||
specifiers:
|
||||
'@pnpm/graceful-fs': workspace:1.0.0
|
||||
@@ -8231,7 +8218,7 @@ packages:
|
||||
resolution: {integrity: sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==}
|
||||
engines: {node: ^10.0.0 || ^12.0.0 || >= 14.0.0}
|
||||
peerDependencies:
|
||||
eslint: '>=5 || * || *'
|
||||
eslint: '>=5 || *'
|
||||
dependencies:
|
||||
eslint: 8.8.0
|
||||
eslint-visitor-keys: 2.1.0
|
||||
|
||||
Reference in New Issue
Block a user