From e1cb00e286edd2e76a1cc10c6c725bbef1f2dcda Mon Sep 17 00:00:00 2001 From: Zoltan Kochan Date: Mon, 6 Jun 2022 23:22:33 +0300 Subject: [PATCH] fix(setup): use @pnpm/os.env.path-extender (#4862) --- packages/plugin-commands-setup/package.json | 4 +- packages/plugin-commands-setup/src/errors.ts | 21 -- packages/plugin-commands-setup/src/setup.ts | 146 +++------- .../src/setupOnWindows.ts | 105 ------- .../plugin-commands-setup/test/setup.test.ts | 250 +++++----------- .../test/setupOnWindows.test.ts | 271 ------------------ pnpm-lock.yaml | 55 +++- 7 files changed, 161 insertions(+), 691 deletions(-) delete mode 100644 packages/plugin-commands-setup/src/errors.ts delete mode 100644 packages/plugin-commands-setup/src/setupOnWindows.ts delete mode 100644 packages/plugin-commands-setup/test/setupOnWindows.test.ts diff --git a/packages/plugin-commands-setup/package.json b/packages/plugin-commands-setup/package.json index 33c5b17f07..16597b09c0 100644 --- a/packages/plugin-commands-setup/package.json +++ b/packages/plugin-commands-setup/package.json @@ -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:*" diff --git a/packages/plugin-commands-setup/src/errors.ts b/packages/plugin-commands-setup/src/errors.ts deleted file mode 100644 index 0cf58debf9..0000000000 --- a/packages/plugin-commands-setup/src/errors.ts +++ /dev/null @@ -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 - } -} diff --git a/packages/plugin-commands-setup/src/setup.ts b/packages/plugin-commands-setup/src/setup.ts index c7c3ae665f..ab11758ab3 100644 --- a/packages/plugin-commands-setup/src/setup.ts +++ b/packages/plugin-commands-setup/src/setup.ts @@ -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 { - 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 { - 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 { - 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 { - 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}` + } } diff --git a/packages/plugin-commands-setup/src/setupOnWindows.ts b/packages/plugin-commands-setup/src/setupOnWindows.ts deleted file mode 100644 index a6eaa7068e..0000000000 --- a/packages/plugin-commands-setup/src/setupOnWindows.ts +++ /dev/null @@ -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 { - // 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 { - 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 { - 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 { - const regexp = new RegExp(`^ {4}(?${envVarName}) {4}(?\\w+) {4}(?.*)$`, '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) -} diff --git a/packages/plugin-commands-setup/test/setup.test.ts b/packages/plugin-commands-setup/test/setup.test.ts index 6b87abbc80..5cbc67b973 100644 --- a/packages/plugin-commands-setup/test/setup.test.ts +++ b/packages/plugin-commands-setup/test/setup.test.ts @@ -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({ + 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({ + 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({ + 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') }) diff --git a/packages/plugin-commands-setup/test/setupOnWindows.test.ts b/packages/plugin-commands-setup/test/setupOnWindows.test.ts deleted file mode 100644 index 8e062643d6..0000000000 --- a/packages/plugin-commands-setup/test/setupOnWindows.test.ts +++ /dev/null @@ -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() -}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0e1fe54869..64c87a39f0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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'}