mirror of
https://github.com/pnpm/pnpm.git
synced 2026-06-30 19:05:23 -04:00
fix(setup): use @pnpm/os.env.path-extender (#4862)
This commit is contained in:
@@ -31,8 +31,7 @@
|
||||
"homepage": "https://github.com/pnpm/pnpm/blob/main/packages/plugin-commands-setup#readme",
|
||||
"dependencies": {
|
||||
"@pnpm/cli-utils": "workspace:0.7.10",
|
||||
"@pnpm/error": "workspace:3.0.1",
|
||||
"execa": "npm:safe-execa@^0.1.1",
|
||||
"@pnpm/os.env.path-extender": "^0.2.3",
|
||||
"render-help": "^1.0.1"
|
||||
},
|
||||
"funding": "https://opencollective.com/pnpm",
|
||||
@@ -40,6 +39,7 @@
|
||||
"@pnpm/logger": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@pnpm/error": "workspace:3.0.1",
|
||||
"@pnpm/logger": "^4.0.0",
|
||||
"@pnpm/plugin-commands-setup": "workspace:2.0.11",
|
||||
"@pnpm/prepare": "workspace:*"
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
import PnpmError from '@pnpm/error'
|
||||
|
||||
export class BadEnvVariableError extends PnpmError {
|
||||
constructor ({ envName, wantedValue, currentValue }: { envName: string, wantedValue: string, currentValue: string }) {
|
||||
super('BAD_ENV_FOUND', `Currently '${envName}' is set to '${wantedValue}'`, {
|
||||
hint: `If you want to override the existing ${envName} env variable, use the --force option`,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export class BadShellSectionError extends PnpmError {
|
||||
public current: string
|
||||
public wanted: string
|
||||
constructor ({ wanted, current, configFile }: { wanted: string, current: string, configFile: string }) {
|
||||
super('BAD_SHELL_SECTION', `The config file at "${configFile} already contains a pnpm section but with other configuration`, {
|
||||
hint: 'If you want to override the existing configuration section, use the --force option',
|
||||
})
|
||||
this.current = current
|
||||
this.wanted = wanted
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,13 @@
|
||||
import fs from 'fs'
|
||||
import os from 'os'
|
||||
import path from 'path'
|
||||
import { docsUrl } from '@pnpm/cli-utils'
|
||||
import logger from '@pnpm/logger'
|
||||
import {
|
||||
addDirToEnvPath,
|
||||
ConfigReport,
|
||||
PathExtenderReport,
|
||||
} from '@pnpm/os.env.path-extender'
|
||||
import renderHelp from 'render-help'
|
||||
import { setupWindowsEnvironmentPath } from './setupOnWindows'
|
||||
import { BadShellSectionError } from './errors'
|
||||
|
||||
export const rcOptionsTypes = () => ({})
|
||||
|
||||
@@ -69,108 +71,46 @@ export async function handler (
|
||||
if (execPath.match(/\.[cm]?js$/) == null) {
|
||||
copyCli(execPath, opts.pnpmHomeDir)
|
||||
}
|
||||
const currentShell = detectCurrentShell()
|
||||
const updateOutput = await updateShell(currentShell, opts.pnpmHomeDir, { force: opts.force ?? false })
|
||||
return `${updateOutput}
|
||||
|
||||
Setup complete. Open a new terminal to start using pnpm.`
|
||||
}
|
||||
|
||||
function detectCurrentShell () {
|
||||
if (process.env.ZSH_VERSION) return 'zsh'
|
||||
if (process.env.BASH_VERSION) return 'bash'
|
||||
if (process.env.FISH_VERSION) return 'fish'
|
||||
return typeof process.env.SHELL === 'string' ? path.basename(process.env.SHELL) : null
|
||||
}
|
||||
|
||||
async function updateShell (
|
||||
currentShell: string | null,
|
||||
pnpmHomeDir: string,
|
||||
opts: { force: boolean }
|
||||
): Promise<string> {
|
||||
switch (currentShell) {
|
||||
case 'bash':
|
||||
case 'zsh': {
|
||||
return reportShellChange(await setupShell(currentShell, pnpmHomeDir, opts))
|
||||
}
|
||||
case 'fish': {
|
||||
return reportShellChange(await setupFishShell(pnpmHomeDir, opts))
|
||||
}
|
||||
}
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
return setupWindowsEnvironmentPath(pnpmHomeDir, opts)
|
||||
}
|
||||
|
||||
return 'Could not infer shell type.'
|
||||
}
|
||||
|
||||
function reportShellChange ({ action, configFile }: ShellSetupResult): string {
|
||||
switch (action) {
|
||||
case 'created': return `Created ${configFile}`
|
||||
case 'added': return `Appended new lines to ${configFile}`
|
||||
case 'updated': return `Replaced configuration in ${configFile}`
|
||||
case 'skipped': return `Configuration already up-to-date in ${configFile}`
|
||||
}
|
||||
}
|
||||
|
||||
type ShellSetupAction = 'created' | 'added' | 'updated' | 'skipped'
|
||||
|
||||
interface ShellSetupResult {
|
||||
configFile: string
|
||||
action: ShellSetupAction
|
||||
}
|
||||
|
||||
async function setupShell (shell: 'bash' | 'zsh', pnpmHomeDir: string, opts: { force: boolean }): Promise<ShellSetupResult> {
|
||||
const configFile = path.join(os.homedir(), `.${shell}rc`)
|
||||
const content = `# pnpm
|
||||
export PNPM_HOME="${pnpmHomeDir}"
|
||||
export PATH="$PNPM_HOME:$PATH"
|
||||
# pnpm end`
|
||||
return {
|
||||
action: await updateShellConfig(configFile, content, opts),
|
||||
configFile,
|
||||
}
|
||||
}
|
||||
|
||||
async function setupFishShell (pnpmHomeDir: string, opts: { force: boolean }): Promise<ShellSetupResult> {
|
||||
const configFile = path.join(os.homedir(), '.config/fish/config.fish')
|
||||
const content = `# pnpm
|
||||
set -gx PNPM_HOME "${pnpmHomeDir}"
|
||||
set -gx PATH "$PNPM_HOME" $PATH
|
||||
# pnpm end`
|
||||
return {
|
||||
action: await updateShellConfig(configFile, content, opts),
|
||||
configFile,
|
||||
}
|
||||
}
|
||||
|
||||
async function updateShellConfig (
|
||||
configFile: string,
|
||||
newContent: string,
|
||||
opts: { force: boolean }
|
||||
): Promise<ShellSetupAction> {
|
||||
if (!fs.existsSync(configFile)) {
|
||||
await fs.promises.writeFile(configFile, newContent, 'utf8')
|
||||
return 'created'
|
||||
}
|
||||
const configContent = await fs.promises.readFile(configFile, 'utf8')
|
||||
const match = configContent.match(/# pnpm[\s\S]*# pnpm end/)
|
||||
if (!match) {
|
||||
await fs.promises.appendFile(configFile, `\n${newContent}`, 'utf8')
|
||||
return 'added'
|
||||
}
|
||||
if (match[0] !== newContent) {
|
||||
if (!opts.force) {
|
||||
throw new BadShellSectionError({ current: match[1], wanted: newContent, configFile })
|
||||
try {
|
||||
const report = await addDirToEnvPath(opts.pnpmHomeDir, {
|
||||
configSectionName: 'pnpm',
|
||||
proxyVarName: 'PNPM_HOME',
|
||||
overwrite: opts.force,
|
||||
position: 'start',
|
||||
})
|
||||
return renderSetupOutput(report)
|
||||
} catch (err: any) { // eslint-disable-line
|
||||
switch (err.code) {
|
||||
case 'ERR_PNPM_BAD_ENV_FOUND':
|
||||
err.hint = 'If you want to override the existing env variable, use the --force option'
|
||||
break
|
||||
case 'ERR_PNPM_BAD_SHELL_SECTION':
|
||||
err.hint = 'If you want to override the existing configuration section, use the --force option'
|
||||
break
|
||||
}
|
||||
const newConfigContent = replaceSection(configContent, newContent)
|
||||
await fs.promises.writeFile(configFile, newConfigContent, 'utf8')
|
||||
return 'updated'
|
||||
throw err
|
||||
}
|
||||
return 'skipped'
|
||||
}
|
||||
|
||||
function replaceSection (originalContent: string, newSection: string): string {
|
||||
return originalContent.replace(/# pnpm[\s\S]*# pnpm end/g, newSection)
|
||||
function renderSetupOutput (report: PathExtenderReport) {
|
||||
if (report.oldSettings === report.newSettings) {
|
||||
return 'No changes to the environment were made. Everything is already up-to-date.'
|
||||
}
|
||||
const output = []
|
||||
if (report.configFile) {
|
||||
output.push(reportConfigChange(report.configFile))
|
||||
}
|
||||
output.push(`Next configuration changes were made:
|
||||
${report.newSettings}`)
|
||||
output.push('Setup complete. Open a new terminal to start using pnpm.')
|
||||
return output.join('\n\n')
|
||||
}
|
||||
|
||||
function reportConfigChange (configReport: ConfigReport): string {
|
||||
switch (configReport.changeType) {
|
||||
case 'created': return `Created ${configReport.path}`
|
||||
case 'appended': return `Appended new lines to ${configReport.path}`
|
||||
case 'modified': return `Replaced configuration in ${configReport.path}`
|
||||
case 'skipped': return `Configuration already up-to-date in ${configReport.path}`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
import PnpmError from '@pnpm/error'
|
||||
import { win32 as path } from 'path'
|
||||
import execa from 'execa'
|
||||
import { BadEnvVariableError } from './errors'
|
||||
|
||||
type IEnvironmentValueMatch = { groups: { name: string, type: string, data: string } } & RegExpMatchArray
|
||||
|
||||
const REG_KEY = 'HKEY_CURRENT_USER\\Environment'
|
||||
|
||||
export async function setupWindowsEnvironmentPath (pnpmHomeDir: string, opts: { force: boolean }): Promise<string> {
|
||||
// Use `chcp` to make `reg` use utf8 encoding for output.
|
||||
// Otherwise, the non-ascii characters in the environment variables will become garbled characters.
|
||||
const chcpResult = await execa('chcp')
|
||||
const cpMatch = /\d+/.exec(chcpResult.stdout) ?? []
|
||||
const cpBak = parseInt(cpMatch[0])
|
||||
if (chcpResult.failed || !(cpBak > 0)) {
|
||||
return `exec chcp failed: ${cpBak}, ${chcpResult.stderr}`
|
||||
}
|
||||
await execa('chcp', ['65001'])
|
||||
try {
|
||||
return await _setupWindowsEnvironmentPath(path.normalize(pnpmHomeDir), opts)
|
||||
} finally {
|
||||
await execa('chcp', [cpBak.toString()])
|
||||
}
|
||||
}
|
||||
|
||||
async function _setupWindowsEnvironmentPath (pnpmHomeDir: string, opts: { force: boolean }): Promise<string> {
|
||||
const registryOutput = await getRegistryOutput()
|
||||
const logger: string[] = []
|
||||
logger.push(logEnvUpdate(await updateEnvVariable(registryOutput, 'PNPM_HOME', pnpmHomeDir, opts), 'PNPM_HOME'))
|
||||
logger.push(logEnvUpdate(await prependToPath(registryOutput, '%PNPM_HOME%'), 'Path'))
|
||||
|
||||
return logger.join('\n')
|
||||
}
|
||||
|
||||
function logEnvUpdate (envUpdateResult: 'skipped' | 'updated', envName: string): string {
|
||||
switch (envUpdateResult) {
|
||||
case 'skipped': return `${envName} was already up-to-date`
|
||||
case 'updated': return `${envName} was updated`
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
async function updateEnvVariable (registryOutput: string, name: string, value: string, opts: { force: boolean }) {
|
||||
const currentValue = await getEnvValueFromRegistry(registryOutput, name)
|
||||
if (currentValue && !opts.force) {
|
||||
if (currentValue !== value) {
|
||||
throw new BadEnvVariableError({ envName: name, currentValue, wantedValue: value })
|
||||
}
|
||||
return 'skipped'
|
||||
} else {
|
||||
await setEnvVarInRegistry(name, value)
|
||||
return 'updated'
|
||||
}
|
||||
}
|
||||
|
||||
async function prependToPath (registryOutput: string, prependDir: string) {
|
||||
const pathData = await getEnvValueFromRegistry(registryOutput, 'Path')
|
||||
if (pathData === undefined || pathData == null || pathData.trim() === '') {
|
||||
throw new PnpmError('NO_PATH', '"Path" environment variable is not found in the registry')
|
||||
} else if (pathData.split(path.delimiter).includes(prependDir)) {
|
||||
return 'skipped'
|
||||
} else {
|
||||
const newPathValue = `${prependDir}${path.delimiter}${pathData}`
|
||||
await setEnvVarInRegistry('Path', newPathValue)
|
||||
return 'updated'
|
||||
}
|
||||
}
|
||||
|
||||
// `windowsHide` in `execa` is true by default, which will cause `chcp` to have no effect.
|
||||
const EXEC_OPTS = { windowsHide: false }
|
||||
|
||||
/**
|
||||
* We read all the registry values and then pick the keys that we need.
|
||||
* This is done because if we would try to pick a key that is not in the registry, the command would fail.
|
||||
* And it is hard to identify the real cause of the command failure.
|
||||
*/
|
||||
async function getRegistryOutput (): Promise<string> {
|
||||
try {
|
||||
const queryResult = await execa('reg', ['query', REG_KEY], EXEC_OPTS)
|
||||
return queryResult.stdout
|
||||
} catch (err: any) { // eslint-disable-line
|
||||
throw new PnpmError('REG_READ', 'win32 registry environment values could not be retrieved')
|
||||
}
|
||||
}
|
||||
|
||||
async function getEnvValueFromRegistry (registryOutput: string, envVarName: string): Promise<string | undefined> {
|
||||
const regexp = new RegExp(`^ {4}(?<name>${envVarName}) {4}(?<type>\\w+) {4}(?<data>.*)$`, 'gim')
|
||||
const match = Array.from(registryOutput.matchAll(regexp))[0] as IEnvironmentValueMatch
|
||||
return match?.groups.data
|
||||
}
|
||||
|
||||
async function setEnvVarInRegistry (envVarName: string, envVarValue: string) {
|
||||
try {
|
||||
await execa('reg', ['add', REG_KEY, '/v', envVarName, '/t', 'REG_EXPAND_SZ', '/d', envVarValue, '/f'], EXEC_OPTS)
|
||||
} catch (err: any) { // eslint-disable-line
|
||||
throw new PnpmError('FAILED_SET_ENV', `Failed to set "${envVarName}" to "${envVarValue}": ${err.stderr as string}`)
|
||||
}
|
||||
// When setting environment variables through the registry, they will not be recognized immediately.
|
||||
// There is a workaround though, to set at least one environment variable with `setx`.
|
||||
// We have some redundancy here because we run it for each env var.
|
||||
// It would be enough also to run it only for the last changed env var.
|
||||
// Read more at: https://bit.ly/39OlQnF
|
||||
await execa('setx', [envVarName, envVarValue], EXEC_OPTS)
|
||||
}
|
||||
@@ -1,192 +1,70 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
import { homedir } from 'os'
|
||||
import { tempDir } from '@pnpm/prepare'
|
||||
import PnpmError from '@pnpm/error'
|
||||
import { setup } from '@pnpm/plugin-commands-setup'
|
||||
import { addDirToEnvPath, PathExtenderReport } from '@pnpm/os.env.path-extender'
|
||||
|
||||
jest.mock('os', () => {
|
||||
const os = jest.requireActual('os')
|
||||
return {
|
||||
...os,
|
||||
homedir: jest.fn(),
|
||||
jest.mock('@pnpm/os.env.path-extender', () => ({
|
||||
addDirToEnvPath: jest.fn(),
|
||||
}))
|
||||
|
||||
jest.mock('fs')
|
||||
|
||||
test('setup makes no changes', async () => {
|
||||
addDirToEnvPath['mockReturnValue'](Promise.resolve<PathExtenderReport>({
|
||||
oldSettings: 'PNPM_HOME=dir',
|
||||
newSettings: 'PNPM_HOME=dir',
|
||||
}))
|
||||
const output = await setup.handler({ pnpmHomeDir: '' })
|
||||
expect(output).toBe('No changes to the environment were made. Everything is already up-to-date.')
|
||||
})
|
||||
|
||||
test('setup makes changes on POSIX', async () => {
|
||||
addDirToEnvPath['mockReturnValue'](Promise.resolve<PathExtenderReport>({
|
||||
configFile: {
|
||||
changeType: 'created',
|
||||
path: '~/.bashrc',
|
||||
},
|
||||
oldSettings: 'export PNPM_HOME=dir1',
|
||||
newSettings: 'export PNPM_HOME=dir2',
|
||||
}))
|
||||
const output = await setup.handler({ pnpmHomeDir: '' })
|
||||
expect(output).toBe(`Created ~/.bashrc
|
||||
|
||||
Next configuration changes were made:
|
||||
export PNPM_HOME=dir2
|
||||
|
||||
Setup complete. Open a new terminal to start using pnpm.`)
|
||||
})
|
||||
|
||||
test('setup makes changes on Windows', async () => {
|
||||
addDirToEnvPath['mockReturnValue'](Promise.resolve<PathExtenderReport>({
|
||||
oldSettings: 'export PNPM_HOME=dir1',
|
||||
newSettings: 'export PNPM_HOME=dir2',
|
||||
}))
|
||||
const output = await setup.handler({ pnpmHomeDir: '' })
|
||||
expect(output).toBe(`Next configuration changes were made:
|
||||
export PNPM_HOME=dir2
|
||||
|
||||
Setup complete. Open a new terminal to start using pnpm.`)
|
||||
})
|
||||
|
||||
test('hint is added to ERR_PNPM_BAD_ENV_FOUND error object', async () => {
|
||||
addDirToEnvPath['mockReturnValue'](Promise.reject(new PnpmError('BAD_ENV_FOUND', '')))
|
||||
let err!: PnpmError
|
||||
try {
|
||||
await setup.handler({ pnpmHomeDir: '' })
|
||||
} catch (_err: any) { // eslint-disable-line
|
||||
err = _err
|
||||
}
|
||||
expect(err?.hint).toBe('If you want to override the existing env variable, use the --force option')
|
||||
})
|
||||
|
||||
let homeDir!: string
|
||||
let pnpmHomeDir!: string
|
||||
|
||||
beforeEach(() => {
|
||||
homeDir = tempDir()
|
||||
pnpmHomeDir = path.join(homeDir, '.pnpm')
|
||||
homedir['mockReturnValue'](homeDir)
|
||||
})
|
||||
|
||||
describe('Bash', () => {
|
||||
beforeAll(() => {
|
||||
process.env.SHELL = '/bin/bash'
|
||||
})
|
||||
it('should append to empty shell script', async () => {
|
||||
fs.writeFileSync('.bashrc', '', 'utf8')
|
||||
const output = await setup.handler({ pnpmHomeDir })
|
||||
expect(output).toMatch(/^Appended new /)
|
||||
const bashRCContent = fs.readFileSync('.bashrc', 'utf8')
|
||||
expect(bashRCContent).toEqual(`
|
||||
# pnpm
|
||||
export PNPM_HOME="${pnpmHomeDir}"
|
||||
export PATH="$PNPM_HOME:$PATH"
|
||||
# pnpm end`)
|
||||
})
|
||||
it('should create a shell script', async () => {
|
||||
const output = await setup.handler({ pnpmHomeDir })
|
||||
expect(output).toMatch(/^Created /)
|
||||
const bashRCContent = fs.readFileSync('.bashrc', 'utf8')
|
||||
expect(bashRCContent).toEqual(`# pnpm
|
||||
export PNPM_HOME="${pnpmHomeDir}"
|
||||
export PATH="$PNPM_HOME:$PATH"
|
||||
# pnpm end`)
|
||||
})
|
||||
it('should make no changes to a shell script that already has the necessary configurations', async () => {
|
||||
fs.writeFileSync('.bashrc', `
|
||||
# pnpm
|
||||
export PNPM_HOME="${pnpmHomeDir}"
|
||||
export PATH="$PNPM_HOME:$PATH"
|
||||
# pnpm end`, 'utf8')
|
||||
const output = await setup.handler({ pnpmHomeDir })
|
||||
expect(output).toMatch(/^Configuration already up-to-date /)
|
||||
const bashRCContent = fs.readFileSync('.bashrc', 'utf8')
|
||||
expect(bashRCContent).toEqual(`
|
||||
# pnpm
|
||||
export PNPM_HOME="${pnpmHomeDir}"
|
||||
export PATH="$PNPM_HOME:$PATH"
|
||||
# pnpm end`)
|
||||
})
|
||||
it('should fail if the shell already has PNPM_HOME set to a different directory', async () => {
|
||||
fs.writeFileSync('.bashrc', `
|
||||
# pnpm
|
||||
export PNPM_HOME="pnpm_home"
|
||||
export PATH="$PNPM_HOME:$PATH"
|
||||
# pnpm end`, 'utf8')
|
||||
await expect(
|
||||
setup.handler({ pnpmHomeDir })
|
||||
).rejects.toThrowError(/The config file at/)
|
||||
})
|
||||
it('should not fail if setup is forced', async () => {
|
||||
fs.writeFileSync('.bashrc', `
|
||||
# pnpm
|
||||
export PNPM_HOME="pnpm_home"
|
||||
export PATH="$PNPM_HOME:$PATH"
|
||||
# pnpm end`, 'utf8')
|
||||
const output = await setup.handler({ force: true, pnpmHomeDir })
|
||||
expect(output).toMatch(/^Replaced /)
|
||||
const bashRCContent = fs.readFileSync('.bashrc', 'utf8')
|
||||
expect(bashRCContent).toEqual(`
|
||||
# pnpm
|
||||
export PNPM_HOME="${pnpmHomeDir}"
|
||||
export PATH="$PNPM_HOME:$PATH"
|
||||
# pnpm end`)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Zsh', () => {
|
||||
beforeAll(() => {
|
||||
process.env.SHELL = '/bin/zsh'
|
||||
})
|
||||
it('should append to empty shell script', async () => {
|
||||
fs.writeFileSync('.zshrc', '', 'utf8')
|
||||
const output = await setup.handler({ pnpmHomeDir })
|
||||
expect(output).toMatch(/^Appended new /)
|
||||
const bashRCContent = fs.readFileSync('.zshrc', 'utf8')
|
||||
expect(bashRCContent).toEqual(`
|
||||
# pnpm
|
||||
export PNPM_HOME="${pnpmHomeDir}"
|
||||
export PATH="$PNPM_HOME:$PATH"
|
||||
# pnpm end`)
|
||||
})
|
||||
it('should make no changes to a shell script that already has the necessary configurations', async () => {
|
||||
fs.writeFileSync('.zshrc', `
|
||||
# pnpm
|
||||
export PNPM_HOME="${pnpmHomeDir}"
|
||||
export PATH="$PNPM_HOME:$PATH"
|
||||
# pnpm end`, 'utf8')
|
||||
const output = await setup.handler({ pnpmHomeDir })
|
||||
expect(output).toMatch(/^Configuration already up-to-date /)
|
||||
const bashRCContent = fs.readFileSync('.zshrc', 'utf8')
|
||||
expect(bashRCContent).toEqual(`
|
||||
# pnpm
|
||||
export PNPM_HOME="${pnpmHomeDir}"
|
||||
export PATH="$PNPM_HOME:$PATH"
|
||||
# pnpm end`)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Fish', () => {
|
||||
beforeAll(() => {
|
||||
process.env.SHELL = '/bin/fish'
|
||||
})
|
||||
it('should append to empty shell script', async () => {
|
||||
fs.mkdirSync('.config/fish', { recursive: true })
|
||||
fs.writeFileSync('.config/fish/config.fish', '', 'utf8')
|
||||
const output = await setup.handler({ pnpmHomeDir })
|
||||
expect(output).toMatch(/^Appended new /)
|
||||
const bashRCContent = fs.readFileSync('.config/fish/config.fish', 'utf8')
|
||||
expect(bashRCContent).toEqual(`
|
||||
# pnpm
|
||||
set -gx PNPM_HOME "${pnpmHomeDir}"
|
||||
set -gx PATH "$PNPM_HOME" $PATH
|
||||
# pnpm end`)
|
||||
})
|
||||
it('should create a shell script', async () => {
|
||||
fs.mkdirSync('.config/fish', { recursive: true })
|
||||
const output = await setup.handler({ pnpmHomeDir })
|
||||
expect(output).toMatch(/^Created /)
|
||||
const bashRCContent = fs.readFileSync('.config/fish/config.fish', 'utf8')
|
||||
expect(bashRCContent).toEqual(`# pnpm
|
||||
set -gx PNPM_HOME "${pnpmHomeDir}"
|
||||
set -gx PATH "$PNPM_HOME" $PATH
|
||||
# pnpm end`)
|
||||
})
|
||||
it('should make no changes to a shell script that already has the necessary configurations', async () => {
|
||||
fs.mkdirSync('.config/fish', { recursive: true })
|
||||
fs.writeFileSync('.config/fish/config.fish', `
|
||||
# pnpm
|
||||
set -gx PNPM_HOME "${pnpmHomeDir}"
|
||||
set -gx PATH "$PNPM_HOME" $PATH
|
||||
# pnpm end`, 'utf8')
|
||||
const output = await setup.handler({ pnpmHomeDir })
|
||||
expect(output).toMatch(/^Configuration already up-to-date /)
|
||||
const bashRCContent = fs.readFileSync('.config/fish/config.fish', 'utf8')
|
||||
expect(bashRCContent).toEqual(`
|
||||
# pnpm
|
||||
set -gx PNPM_HOME "${pnpmHomeDir}"
|
||||
set -gx PATH "$PNPM_HOME" $PATH
|
||||
# pnpm end`)
|
||||
})
|
||||
it('should fail if the shell already has PNPM_HOME set to a different directory', async () => {
|
||||
fs.mkdirSync('.config/fish', { recursive: true })
|
||||
fs.writeFileSync('.config/fish/config.fish', `
|
||||
# pnpm
|
||||
set -gx PNPM_HOME "pnpm_home"
|
||||
set -gx PATH "$PNPM_HOME" $PATH
|
||||
# pnpm end`, 'utf8')
|
||||
await expect(
|
||||
setup.handler({ pnpmHomeDir })
|
||||
).rejects.toThrowError(/The config file at/)
|
||||
})
|
||||
it('should not fail if setup is forced', async () => {
|
||||
fs.mkdirSync('.config/fish', { recursive: true })
|
||||
fs.writeFileSync('.config/fish/config.fish', `
|
||||
# pnpm
|
||||
set -gx PNPM_HOME "pnpm_home"
|
||||
set -gx PATH "$PNPM_HOME" $PATH
|
||||
# pnpm end`, 'utf8')
|
||||
const output = await setup.handler({ force: true, pnpmHomeDir })
|
||||
expect(output).toMatch(/^Replaced /)
|
||||
const bashRCContent = fs.readFileSync('.config/fish/config.fish', 'utf8')
|
||||
expect(bashRCContent).toEqual(`
|
||||
# pnpm
|
||||
set -gx PNPM_HOME "${pnpmHomeDir}"
|
||||
set -gx PATH "$PNPM_HOME" $PATH
|
||||
# pnpm end`)
|
||||
})
|
||||
test('hint is added to ERR_PNPM_BAD_SHELL_SECTION error object', async () => {
|
||||
addDirToEnvPath['mockReturnValue'](Promise.reject(new PnpmError('BAD_SHELL_SECTION', '')))
|
||||
let err!: PnpmError
|
||||
try {
|
||||
await setup.handler({ pnpmHomeDir: '' })
|
||||
} catch (_err: any) { // eslint-disable-line
|
||||
err = _err
|
||||
}
|
||||
expect(err?.hint).toBe('If you want to override the existing configuration section, use the --force option')
|
||||
})
|
||||
|
||||
@@ -1,271 +0,0 @@
|
||||
import { win32 as path } from 'path'
|
||||
import execa from 'execa'
|
||||
import { setup } from '@pnpm/plugin-commands-setup'
|
||||
import { tempDir } from '@pnpm/prepare'
|
||||
|
||||
jest.mock('execa')
|
||||
|
||||
let originalShell: string | undefined
|
||||
let originalPlatform = ''
|
||||
|
||||
beforeEach(() => {
|
||||
execa['mockReset']()
|
||||
})
|
||||
|
||||
beforeAll(() => {
|
||||
originalShell = process.env.SHELL
|
||||
originalPlatform = process.platform
|
||||
|
||||
process.env.SHELL = ''
|
||||
Object.defineProperty(process, 'platform', {
|
||||
value: 'win32',
|
||||
})
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
process.env.SHELL = originalShell
|
||||
Object.defineProperty(process, 'platform', {
|
||||
value: originalPlatform,
|
||||
})
|
||||
})
|
||||
|
||||
const regKey = 'HKEY_CURRENT_USER\\Environment'
|
||||
|
||||
function createExecaError ({ stderr }: { stderr: string}) {
|
||||
const err = new Error('Command failed with exit code 1')
|
||||
err['stderr'] = stderr
|
||||
return err
|
||||
}
|
||||
|
||||
test('win32 registry environment values could not be retrieved', async () => {
|
||||
execa['mockResolvedValueOnce']({
|
||||
failed: false,
|
||||
stdout: '活动代码页: 936',
|
||||
}).mockResolvedValueOnce({
|
||||
failed: false,
|
||||
stdout: '',
|
||||
}).mockRejectedValue(createExecaError({ stderr: 'ERROR: Some error' }))
|
||||
|
||||
await expect(setup.handler({ pnpmHomeDir: tempDir(false) }))
|
||||
.rejects.toThrow()
|
||||
})
|
||||
|
||||
test('environment Path is not configured correctly', async () => {
|
||||
execa['mockResolvedValueOnce']({
|
||||
failed: false,
|
||||
stdout: '活动代码页: 936',
|
||||
}).mockResolvedValueOnce({
|
||||
failed: false,
|
||||
stdout: 'SOME KIND OF ERROR OR UNSUPPORTED RESPONSE FORMAT',
|
||||
}).mockResolvedValue({
|
||||
failed: false,
|
||||
stdout: '',
|
||||
})
|
||||
|
||||
await expect(
|
||||
setup.handler({
|
||||
pnpmHomeDir: tempDir(false),
|
||||
})
|
||||
).rejects.toThrow(/"Path" environment variable is not found/)
|
||||
|
||||
expect(execa).toHaveBeenNthCalledWith(3, 'reg', ['query', regKey], { windowsHide: false })
|
||||
})
|
||||
|
||||
test('environment Path is empty', async () => {
|
||||
execa['mockResolvedValueOnce']({
|
||||
failed: false,
|
||||
stdout: '活动代码页: 936',
|
||||
}).mockResolvedValueOnce({
|
||||
failed: false,
|
||||
stdout: '',
|
||||
}).mockResolvedValueOnce({
|
||||
failed: false,
|
||||
stdout: `
|
||||
HKEY_CURRENT_USER\\Environment
|
||||
Path REG_EXPAND_SZ
|
||||
`,
|
||||
}).mockResolvedValue({
|
||||
failed: false,
|
||||
stdout: '',
|
||||
})
|
||||
|
||||
await expect(
|
||||
setup.handler({
|
||||
pnpmHomeDir: tempDir(false),
|
||||
})
|
||||
).rejects.toThrow(/"Path" environment variable is not found/)
|
||||
|
||||
expect(execa).toHaveBeenNthCalledWith(3, 'reg', ['query', regKey], { windowsHide: false })
|
||||
})
|
||||
|
||||
test('successful first time installation', async () => {
|
||||
const currentPathInRegistry = '%USERPROFILE%\\AppData\\Local\\Microsoft\\WindowsApps;%USERPROFILE%\\.config\\etc;'
|
||||
|
||||
execa['mockResolvedValueOnce']({
|
||||
failed: false,
|
||||
stdout: '活动代码页: 936',
|
||||
}).mockResolvedValueOnce({
|
||||
failed: false,
|
||||
stdout: '',
|
||||
}).mockResolvedValueOnce({
|
||||
failed: false,
|
||||
stdout: `
|
||||
HKEY_CURRENT_USER\\Environment
|
||||
Path REG_EXPAND_SZ ${currentPathInRegistry}
|
||||
`,
|
||||
}).mockResolvedValueOnce({
|
||||
failed: false,
|
||||
stdout: 'PNPM_HOME ENV VAR SET',
|
||||
}).mockResolvedValueOnce({
|
||||
failed: false,
|
||||
stdout: 'setx PNPM_HOME',
|
||||
}).mockResolvedValueOnce({
|
||||
failed: false,
|
||||
stdout: 'setx PNPM_HOME',
|
||||
}).mockResolvedValue({
|
||||
failed: true,
|
||||
stderr: 'UNEXPECTED',
|
||||
})
|
||||
|
||||
const pnpmHomeDir = tempDir(false)
|
||||
const pnpmHomeDirNormalized = path.normalize(pnpmHomeDir)
|
||||
const output = await setup.handler({ pnpmHomeDir })
|
||||
|
||||
expect(execa).toHaveBeenNthCalledWith(3, 'reg', ['query', regKey], { windowsHide: false })
|
||||
expect(execa).toHaveBeenNthCalledWith(4, 'reg', ['add', regKey, '/v', 'PNPM_HOME', '/t', 'REG_EXPAND_SZ', '/d', pnpmHomeDirNormalized, '/f'], { windowsHide: false })
|
||||
expect(execa).toHaveBeenNthCalledWith(5, 'setx', ['PNPM_HOME', pnpmHomeDirNormalized], { windowsHide: false })
|
||||
expect(execa).toHaveBeenNthCalledWith(6, 'reg', ['add', regKey, '/v', 'Path', '/t', 'REG_EXPAND_SZ', '/d', `%PNPM_HOME%;${currentPathInRegistry}`, '/f'], { windowsHide: false })
|
||||
expect(execa).toHaveBeenNthCalledWith(7, 'setx', ['Path', `%PNPM_HOME%;${currentPathInRegistry}`], { windowsHide: false })
|
||||
expect(output).toContain('Path was updated')
|
||||
expect(output).toContain('PNPM_HOME was updated')
|
||||
})
|
||||
|
||||
test('PNPM_HOME is already set, but Path is updated', async () => {
|
||||
const currentPathInRegistry = '%USERPROFILE%\\AppData\\Local\\Microsoft\\WindowsApps;%USERPROFILE%\\.config\\etc;'
|
||||
const pnpmHomeDir = tempDir(false)
|
||||
const pnpmHomeDirNormalized = path.normalize(pnpmHomeDir)
|
||||
execa['mockResolvedValueOnce']({
|
||||
failed: false,
|
||||
stdout: '活动代码页: 936',
|
||||
}).mockResolvedValueOnce({
|
||||
failed: false,
|
||||
stdout: '',
|
||||
}).mockResolvedValueOnce({
|
||||
failed: false,
|
||||
stdout: `
|
||||
HKEY_CURRENT_USER\\Environment
|
||||
PNPM_HOME REG_EXPAND_SZ ${pnpmHomeDirNormalized}
|
||||
Path REG_EXPAND_SZ ${currentPathInRegistry}
|
||||
`,
|
||||
}).mockResolvedValueOnce({
|
||||
failed: false,
|
||||
stdout: 'Path UPDATED',
|
||||
}).mockResolvedValueOnce({
|
||||
failed: false,
|
||||
stdout: 'setx PATH',
|
||||
}).mockResolvedValue({
|
||||
failed: true,
|
||||
stderr: 'UNEXPECTED',
|
||||
})
|
||||
|
||||
const output = await setup.handler({ pnpmHomeDir })
|
||||
|
||||
expect(execa).toHaveBeenNthCalledWith(3, 'reg', ['query', regKey], { windowsHide: false })
|
||||
expect(execa).toHaveBeenNthCalledWith(4, 'reg', ['add', regKey, '/v', 'Path', '/t', 'REG_EXPAND_SZ', '/d', `%PNPM_HOME%;${currentPathInRegistry}`, '/f'], { windowsHide: false })
|
||||
expect(execa).toHaveBeenNthCalledWith(5, 'setx', ['Path', `%PNPM_HOME%;${currentPathInRegistry}`], { windowsHide: false })
|
||||
expect(output).toContain('PNPM_HOME was already up-to-date')
|
||||
expect(output).toContain('Path was updated')
|
||||
})
|
||||
|
||||
test('setup throws an error if PNPM_HOME is already set to a different directory', async () => {
|
||||
execa['mockResolvedValueOnce']({
|
||||
failed: false,
|
||||
stdout: '活动代码页: 936',
|
||||
}).mockResolvedValueOnce({
|
||||
failed: false,
|
||||
stdout: '',
|
||||
}).mockResolvedValueOnce({
|
||||
failed: false,
|
||||
stdout: `
|
||||
HKEY_CURRENT_USER\\Environment
|
||||
PNPM_HOME REG_EXPAND_SZ .pnpm\\home
|
||||
Path REG_EXPAND_SZ %USERPROFILE%\\AppData\\Local\\Microsoft\\WindowsApps;%USERPROFILE%\\.config\\etc;.pnpm\\home;C:\\Windows;
|
||||
`,
|
||||
}).mockResolvedValue({
|
||||
failed: true,
|
||||
stderr: 'UNEXPECTED',
|
||||
})
|
||||
|
||||
const pnpmHomeDir = tempDir(false)
|
||||
await expect(
|
||||
setup.handler({ pnpmHomeDir })
|
||||
).rejects.toThrowError(/Currently 'PNPM_HOME' is set to/)
|
||||
})
|
||||
|
||||
test('setup overrides PNPM_HOME, when setup is forced', async () => {
|
||||
execa['mockReset']()
|
||||
execa['mockResolvedValueOnce']({
|
||||
failed: false,
|
||||
stdout: '活动代码页: 936',
|
||||
}).mockResolvedValueOnce({
|
||||
failed: false,
|
||||
stdout: '',
|
||||
}).mockResolvedValueOnce({
|
||||
failed: false,
|
||||
stdout: `
|
||||
HKEY_CURRENT_USER\\Environment
|
||||
PNPM_HOME REG_EXPAND_SZ .pnpm\\home
|
||||
Path REG_EXPAND_SZ %USERPROFILE%\\AppData\\Local\\Microsoft\\WindowsApps;%USERPROFILE%\\.config\\etc;.pnpm\\home;C:\\Windows;
|
||||
`,
|
||||
}).mockResolvedValueOnce({
|
||||
failed: false,
|
||||
stdout: '',
|
||||
}).mockResolvedValueOnce({
|
||||
failed: false,
|
||||
stdout: '',
|
||||
}).mockResolvedValueOnce({
|
||||
}).mockResolvedValueOnce({
|
||||
failed: false,
|
||||
stdout: '',
|
||||
}).mockResolvedValueOnce({
|
||||
failed: false,
|
||||
stdout: '',
|
||||
}).mockResolvedValue({
|
||||
failed: true,
|
||||
stderr: 'UNEXPECTED',
|
||||
})
|
||||
|
||||
const pnpmHomeDir = tempDir(false)
|
||||
const pnpmHomeDirNormalized = path.normalize(pnpmHomeDir)
|
||||
const output = await setup.handler({
|
||||
force: true,
|
||||
pnpmHomeDir,
|
||||
})
|
||||
|
||||
expect(execa).toHaveBeenNthCalledWith(3, 'reg', ['query', regKey], { windowsHide: false })
|
||||
expect(execa).toHaveBeenNthCalledWith(4, 'reg', ['add', regKey, '/v', 'PNPM_HOME', '/t', 'REG_EXPAND_SZ', '/d', pnpmHomeDirNormalized, '/f'], { windowsHide: false })
|
||||
expect(output).toContain('PNPM_HOME was updated')
|
||||
})
|
||||
|
||||
test('failure to install', async () => {
|
||||
const currentPathInRegistry = '%USERPROFILE%\\AppData\\Local\\Microsoft\\WindowsApps;%USERPROFILE%\\.config\\etc;'
|
||||
|
||||
execa['mockResolvedValueOnce']({
|
||||
failed: false,
|
||||
stdout: '活动代码页: 936',
|
||||
}).mockResolvedValueOnce({
|
||||
failed: false,
|
||||
stdout: '',
|
||||
}).mockResolvedValueOnce({
|
||||
failed: false,
|
||||
stdout: `
|
||||
HKEY_CURRENT_USER\\Environment
|
||||
Path REG_EXPAND_SZ ${currentPathInRegistry}
|
||||
`,
|
||||
}).mockRejectedValue(new Error())
|
||||
|
||||
const pnpmHomeDir = tempDir(false)
|
||||
await expect(
|
||||
setup.handler({ pnpmHomeDir })
|
||||
).rejects.toThrow()
|
||||
})
|
||||
55
pnpm-lock.yaml
generated
55
pnpm-lock.yaml
generated
@@ -2722,16 +2722,16 @@ importers:
|
||||
'@pnpm/cli-utils': workspace:0.7.10
|
||||
'@pnpm/error': workspace:3.0.1
|
||||
'@pnpm/logger': ^4.0.0
|
||||
'@pnpm/os.env.path-extender': ^0.2.3
|
||||
'@pnpm/plugin-commands-setup': workspace:2.0.11
|
||||
'@pnpm/prepare': workspace:*
|
||||
execa: npm:safe-execa@^0.1.1
|
||||
render-help: ^1.0.1
|
||||
dependencies:
|
||||
'@pnpm/cli-utils': link:../cli-utils
|
||||
'@pnpm/error': link:../error
|
||||
execa: /safe-execa/0.1.1
|
||||
'@pnpm/os.env.path-extender': 0.2.3
|
||||
render-help: 1.0.2
|
||||
devDependencies:
|
||||
'@pnpm/error': link:../error
|
||||
'@pnpm/logger': 4.0.0
|
||||
'@pnpm/plugin-commands-setup': 'link:'
|
||||
'@pnpm/prepare': link:../../privatePackages/prepare
|
||||
@@ -4963,6 +4963,11 @@ packages:
|
||||
resolution: {integrity: sha512-VhUGKR5jvAtoBHgHAB3Kfc9g42ocVUws9iOafGAQ+xjR8uLokUCReXDpLXRRtrqw8N8yyh3gLNpCJs/AYadA1g==}
|
||||
engines: {node: '>=12.17'}
|
||||
|
||||
/@pnpm/constants/6.1.0:
|
||||
resolution: {integrity: sha512-L6AiU3OXv9kjKGTJN9j8n1TeJGDcLX9atQlZvAkthlvbXjvKc5SKNWESc/eXhr5nEfuMWhQhiKHDJCpYejmeCQ==}
|
||||
engines: {node: '>=14.19'}
|
||||
dev: false
|
||||
|
||||
/@pnpm/core-loggers/6.1.4_@pnpm+logger@4.0.0:
|
||||
resolution: {integrity: sha512-vF4Qc8E8e4uCGbVc7USCgqxrRe4eZsz8XEuCTUy6asZFVnNnRY/N1vJaVG3hti/UYKI/1si1dJYibppXhgTimA==}
|
||||
engines: {node: '>=12.17'}
|
||||
@@ -5006,6 +5011,13 @@ packages:
|
||||
dependencies:
|
||||
'@pnpm/constants': 5.0.0
|
||||
|
||||
/@pnpm/error/3.0.1:
|
||||
resolution: {integrity: sha512-hMlbWbFcfcfolNfSjKjpeaZFow71kNg438LZ8rAd01swiVIYRUf/sRv8gGySru6AijYfz5UqslpIJRDbYBkgQA==}
|
||||
engines: {node: '>=14.19'}
|
||||
dependencies:
|
||||
'@pnpm/constants': 6.1.0
|
||||
dev: false
|
||||
|
||||
/@pnpm/exec/2.0.0:
|
||||
resolution: {integrity: sha512-b5ALfWEOFQprWKntN7MF8XWCyslBk2c8u20GEDcDDQOs6c0HyHlWxX5lig8riQKdS000U6YyS4L4b32NOleXAQ==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -5135,6 +5147,30 @@ packages:
|
||||
- supports-color
|
||||
dev: false
|
||||
|
||||
/@pnpm/os.env.path-extender-posix/0.2.2:
|
||||
resolution: {integrity: sha512-S742bEimO44pwQCoIONSSBs27GxraEEWDhSuQMfR4eX4AbLk4WiEa+fURmrE8Lz8lh+ZobO8Yi5DsAPJXGG9wA==}
|
||||
engines: {node: '>=12.22.0'}
|
||||
dependencies:
|
||||
'@pnpm/error': 3.0.1
|
||||
dev: false
|
||||
|
||||
/@pnpm/os.env.path-extender-windows/0.2.0:
|
||||
resolution: {integrity: sha512-m5JfJe3I64jKe+9hgKaN0tE2QmY63GuXPg5VY4oN4Z0G4B9exu4I1VEEMU/kyL7D7XfutPAPsXqiLHDNlv6RYg==}
|
||||
engines: {node: '>=12.22.0'}
|
||||
dependencies:
|
||||
'@pnpm/error': 3.0.1
|
||||
safe-execa: 0.1.1
|
||||
string.prototype.matchall: 4.0.7
|
||||
dev: false
|
||||
|
||||
/@pnpm/os.env.path-extender/0.2.3:
|
||||
resolution: {integrity: sha512-1/hVPGxyR76GdfjsSKDbgz3yTfFWR++2GMl5P2svan9k3PQfnuay41w7Bw2iS+FjQdDxlDDMymRdYY5cD+O/fA==}
|
||||
engines: {node: '>=12.22.0'}
|
||||
dependencies:
|
||||
'@pnpm/os.env.path-extender-posix': 0.2.2
|
||||
'@pnpm/os.env.path-extender-windows': 0.2.0
|
||||
dev: false
|
||||
|
||||
/@pnpm/package-is-installable/5.0.13_@pnpm+logger@4.0.0:
|
||||
resolution: {integrity: sha512-a3Jm7sc7Cd05s8HX/mWLdX+2e9L6d9t2xL0bPnTzQN76Qx8JadBid81etAWqXP7Ecu2cs4kXz67w0CJR/SIdiA==}
|
||||
engines: {node: '>=12.17'}
|
||||
@@ -13791,6 +13827,19 @@ packages:
|
||||
is-fullwidth-code-point: 3.0.0
|
||||
strip-ansi: 6.0.1
|
||||
|
||||
/string.prototype.matchall/4.0.7:
|
||||
resolution: {integrity: sha512-f48okCX7JiwVi1NXCVWcFnZgADDC/n2vePlQ/KUCNqCikLLilQvwjMO8+BHVKvgzH0JB0J9LEPgxOGT02RoETg==}
|
||||
dependencies:
|
||||
call-bind: 1.0.2
|
||||
define-properties: 1.1.4
|
||||
es-abstract: 1.20.1
|
||||
get-intrinsic: 1.1.1
|
||||
has-symbols: 1.0.3
|
||||
internal-slot: 1.0.3
|
||||
regexp.prototype.flags: 1.4.3
|
||||
side-channel: 1.0.4
|
||||
dev: false
|
||||
|
||||
/string.prototype.padend/3.1.3:
|
||||
resolution: {integrity: sha512-jNIIeokznm8SD/TZISQsZKYu7RJyheFNt84DUPrh482GC8RVp2MKqm2O5oBRdGxbDQoXrhhWtPIWQOiy20svUg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
Reference in New Issue
Block a user