mirror of
https://github.com/pnpm/pnpm.git
synced 2026-04-05 07:46:10 -04:00
feat(setup): setup PATH and PNPM_HOME on win32 platform (#3734)
This commit is contained in:
5
.changeset/weak-fans-dream.md
Normal file
5
.changeset/weak-fans-dream.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@pnpm/plugin-commands-setup": patch
|
||||
---
|
||||
|
||||
Set `PATH` environment variable to `PNPM_HOME` on Win32 platform
|
||||
@@ -30,6 +30,7 @@
|
||||
"homepage": "https://github.com/pnpm/pnpm/blob/master/packages/plugin-commands-setup#readme",
|
||||
"dependencies": {
|
||||
"@pnpm/cli-utils": "workspace:0.6.19",
|
||||
"execa": "^5.0.0",
|
||||
"render-help": "^1.0.1"
|
||||
},
|
||||
"funding": "https://opencollective.com/pnpm",
|
||||
|
||||
@@ -4,6 +4,7 @@ import path from 'path'
|
||||
import { docsUrl } from '@pnpm/cli-utils'
|
||||
import logger from '@pnpm/logger'
|
||||
import renderHelp from 'render-help'
|
||||
import { setupWindowsEnvironmentPath } from './setupOnWindows'
|
||||
|
||||
export const rcOptionsTypes = () => ({})
|
||||
|
||||
@@ -49,21 +50,18 @@ export async function handler (
|
||||
pnpmHomeDir: string
|
||||
}
|
||||
) {
|
||||
const currentShell = process.env.SHELL ? path.basename(process.env.SHELL) : null
|
||||
const currentShell = typeof process.env.SHELL === 'string' ? path.basename(process.env.SHELL) : null
|
||||
const execPath = getExecPath()
|
||||
if (execPath.match(/\.[cm]?js$/) == null) {
|
||||
copyCli(execPath, opts.pnpmHomeDir)
|
||||
}
|
||||
const updateOutput = await updateShell(currentShell, opts.pnpmHomeDir)
|
||||
if (updateOutput === false) {
|
||||
return 'Could not infer shell type.'
|
||||
}
|
||||
return `${updateOutput}
|
||||
|
||||
Setup complete. Open a new terminal to start using pnpm.`
|
||||
}
|
||||
|
||||
async function updateShell (currentShell: string | null, pnpmHomeDir: string): Promise<string | false> {
|
||||
async function updateShell (currentShell: string | null, pnpmHomeDir: string): Promise<string> {
|
||||
switch (currentShell) {
|
||||
case 'bash': {
|
||||
const configFile = path.join(os.homedir(), '.bashrc')
|
||||
@@ -77,10 +75,15 @@ async function updateShell (currentShell: string | null, pnpmHomeDir: string): P
|
||||
return setupFishShell(pnpmHomeDir)
|
||||
}
|
||||
}
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
return setupWindowsEnvironmentPath(pnpmHomeDir)
|
||||
}
|
||||
|
||||
return 'Could not infer shell type.'
|
||||
}
|
||||
|
||||
async function setupShell (configFile: string, pnpmHomeDir: string) {
|
||||
async function setupShell (configFile: string, pnpmHomeDir: string): Promise<string> {
|
||||
if (!fs.existsSync(configFile)) return `Could not setup pnpm. No ${configFile} found`
|
||||
const configContent = await fs.promises.readFile(configFile, 'utf8')
|
||||
if (configContent.includes('PNPM_HOME')) {
|
||||
@@ -93,7 +96,7 @@ export PATH="$PNPM_HOME:$PATH"
|
||||
return `Updated ${configFile}`
|
||||
}
|
||||
|
||||
async function setupFishShell (pnpmHomeDir: string) {
|
||||
async function setupFishShell (pnpmHomeDir: string): Promise<string> {
|
||||
const configFile = path.join(os.homedir(), '.config/fish/config.fish')
|
||||
if (!fs.existsSync(configFile)) return `Could not setup pnpm. No ${configFile} found`
|
||||
const configContent = await fs.promises.readFile(configFile, 'utf8')
|
||||
|
||||
74
packages/plugin-commands-setup/src/setupOnWindows.ts
Normal file
74
packages/plugin-commands-setup/src/setupOnWindows.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { win32 as path } from 'path'
|
||||
import execa from 'execa'
|
||||
|
||||
type IEnvironmentValueMatch = { groups: { name: string, type: string, data: string } } & RegExpMatchArray
|
||||
|
||||
export async function setupWindowsEnvironmentPath (pnpmHomeDir: string): Promise<string> {
|
||||
const pathRegex = /^ {4}(?<name>PATH) {4}(?<type>\w+) {4}(?<data>.*)$/gim
|
||||
const pnpmHomeRegex = /^ {4}(?<name>PNPM_HOME) {4}(?<type>\w+) {4}(?<data>.*)$/gim
|
||||
const regKey = 'HKEY_CURRENT_USER\\Environment'
|
||||
|
||||
const queryResult = await execa('reg', ['query', regKey])
|
||||
|
||||
if (queryResult.failed) {
|
||||
return 'Win32 registry environment values could not be retrieved'
|
||||
}
|
||||
|
||||
const queryOutput = queryResult.stdout
|
||||
const pathValueMatch = [...queryOutput.matchAll(pathRegex)] as IEnvironmentValueMatch[]
|
||||
const homeValueMatch = [...queryOutput.matchAll(pnpmHomeRegex)] as IEnvironmentValueMatch[]
|
||||
|
||||
let commitNeeded = false
|
||||
let homeDir = pnpmHomeDir
|
||||
const logger = []
|
||||
|
||||
if (homeValueMatch.length === 1) {
|
||||
homeDir = homeValueMatch[0].groups.data
|
||||
logger.push(`Currently 'PNPM_HOME' is set to '${homeDir}'`)
|
||||
} else {
|
||||
logger.push(`Setting 'PNPM_HOME' to value '${homeDir}'`)
|
||||
const addResult = await execa('reg', ['add', regKey, '/v', 'PNPM_HOME', '/t', 'REG_EXPAND_SZ', '/d', homeDir, '/f'])
|
||||
if (addResult.failed) {
|
||||
logger.push(`\t${addResult.stderr}`)
|
||||
} else {
|
||||
commitNeeded = true
|
||||
logger.push(`\t${addResult.stdout}`)
|
||||
}
|
||||
}
|
||||
|
||||
if (pathValueMatch.length === 1) {
|
||||
const pathData = pathValueMatch[0].groups.data
|
||||
if (pathData == null || pathData.trim() === '') {
|
||||
logger.push('Current PATH is empty. No changes to this environment variable are applied')
|
||||
} else {
|
||||
const homeDirPath = path.parse(path.normalize(homeDir))
|
||||
|
||||
if (pathData
|
||||
.split(path.delimiter)
|
||||
.map(p => path.normalize(p))
|
||||
.map(p => path.parse(p))
|
||||
.map(p => `${p.dir}${path.sep}${p.base}`.toUpperCase())
|
||||
.filter(p => p !== '')
|
||||
.includes(`${homeDirPath.dir}${path.sep}${homeDirPath.base}`.toUpperCase())) {
|
||||
logger.push('PATH already contains PNPM_HOME')
|
||||
} else {
|
||||
logger.push('Updating PATH')
|
||||
const addResult = await execa('reg', ['add', regKey, '/v', pathValueMatch[0].groups.name, '/t', 'REG_EXPAND_SZ', '/d', `${homeDir}${path.delimiter}${pathData}`, '/f'])
|
||||
if (addResult.failed) {
|
||||
logger.push(`\t${addResult.stderr}`)
|
||||
} else {
|
||||
commitNeeded = true
|
||||
logger.push(`\t${addResult.stdout}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logger.push('Current PATH is not set. No changes to this environment variable are applied')
|
||||
}
|
||||
|
||||
if (commitNeeded) {
|
||||
await execa('setx', ['PNPM_HOME', homeDir])
|
||||
}
|
||||
|
||||
return logger.join('\n')
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import execa from 'execa'
|
||||
import { setup } from '@pnpm/plugin-commands-setup'
|
||||
|
||||
jest.mock('execa')
|
||||
|
||||
let originalShell: string | undefined
|
||||
let originalPlatform = ''
|
||||
|
||||
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'
|
||||
|
||||
test('Win32 registry environment values could not be retrieved', async () => {
|
||||
execa['mockResolvedValue']({
|
||||
failed: true,
|
||||
})
|
||||
|
||||
const output = await setup.handler({
|
||||
pnpmHomeDir: __dirname,
|
||||
})
|
||||
|
||||
expect(execa).toHaveBeenNthCalledWith(1, 'reg', ['query', regKey])
|
||||
expect(output).toContain('Win32 registry environment values could not be retrieved')
|
||||
})
|
||||
@@ -0,0 +1,42 @@
|
||||
import execa from 'execa'
|
||||
import { setup } from '@pnpm/plugin-commands-setup'
|
||||
|
||||
jest.mock('execa')
|
||||
|
||||
let originalShell: string | undefined
|
||||
let originalPlatform = ''
|
||||
|
||||
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'
|
||||
|
||||
test('Environment PATH is not configured correctly', async () => {
|
||||
execa['mockResolvedValueOnce']({
|
||||
failed: false,
|
||||
stdout: 'SOME KIND OF ERROR OR UNSUPPORTED RESPONSE FORMAT',
|
||||
}).mockResolvedValue({
|
||||
failed: true,
|
||||
})
|
||||
|
||||
const output = await setup.handler({
|
||||
pnpmHomeDir: __dirname,
|
||||
})
|
||||
|
||||
expect(execa).toHaveBeenNthCalledWith(1, 'reg', ['query', regKey])
|
||||
expect(output).toContain('Current PATH is not set. No changes to this environment variable are applied')
|
||||
})
|
||||
@@ -0,0 +1,45 @@
|
||||
import execa from 'execa'
|
||||
import { setup } from '@pnpm/plugin-commands-setup'
|
||||
|
||||
jest.mock('execa')
|
||||
|
||||
let originalShell: string | undefined
|
||||
let originalPlatform = ''
|
||||
|
||||
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'
|
||||
|
||||
test('Environment PATH is empty', async () => {
|
||||
execa['mockResolvedValueOnce']({
|
||||
failed: false,
|
||||
stdout: `
|
||||
HKEY_CURRENT_USER\\Environment
|
||||
Path REG_EXPAND_SZ
|
||||
`,
|
||||
}).mockResolvedValue({
|
||||
failed: false,
|
||||
})
|
||||
|
||||
const output = await setup.handler({
|
||||
pnpmHomeDir: __dirname,
|
||||
})
|
||||
|
||||
expect(execa).toHaveBeenNthCalledWith(1, 'reg', ['query', regKey])
|
||||
expect(output).toContain('Current PATH is empty. No changes to this environment variable are applied')
|
||||
})
|
||||
@@ -0,0 +1,60 @@
|
||||
import execa from 'execa'
|
||||
import { setup } from '@pnpm/plugin-commands-setup'
|
||||
|
||||
jest.mock('execa')
|
||||
|
||||
let originalShell: string | undefined
|
||||
let originalPlatform = ''
|
||||
|
||||
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'
|
||||
|
||||
test('Successful first time installation', async () => {
|
||||
const currentPathInRegistry = '%USERPROFILE%\\AppData\\Local\\Microsoft\\WindowsApps;%USERPROFILE%\\.config\\etc;'
|
||||
|
||||
execa['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: 'PATH UPDATED',
|
||||
}).mockResolvedValue({
|
||||
failed: true,
|
||||
stderr: 'UNEXPECTED',
|
||||
})
|
||||
|
||||
const output = await setup.handler({
|
||||
pnpmHomeDir: __dirname,
|
||||
})
|
||||
|
||||
expect(execa).toHaveBeenNthCalledWith(1, 'reg', ['query', regKey])
|
||||
expect(execa).toHaveBeenNthCalledWith(2, 'reg', ['add', regKey, '/v', 'PNPM_HOME', '/t', 'REG_EXPAND_SZ', '/d', __dirname, '/f'])
|
||||
expect(execa).toHaveBeenNthCalledWith(3, 'reg', ['add', regKey, '/v', 'Path', '/t', 'REG_EXPAND_SZ', '/d', `${__dirname};${currentPathInRegistry}`, '/f'])
|
||||
expect(execa).toHaveBeenNthCalledWith(4, 'setx', ['PNPM_HOME', __dirname])
|
||||
expect(output).toContain(`Setting 'PNPM_HOME' to value '${__dirname}`)
|
||||
expect(output).toContain('Updating PATH')
|
||||
expect(output).toContain('PNPM_HOME ENV VAR SET')
|
||||
expect(output).toContain('PATH UPDATED')
|
||||
})
|
||||
@@ -0,0 +1,55 @@
|
||||
import execa from 'execa'
|
||||
import { setup } from '@pnpm/plugin-commands-setup'
|
||||
|
||||
jest.mock('execa')
|
||||
|
||||
let originalShell: string | undefined
|
||||
let originalPlatform = ''
|
||||
|
||||
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'
|
||||
|
||||
test('PNPM_HOME is already set, but path is updated', async () => {
|
||||
const currentPathInRegistry = '%USERPROFILE%\\AppData\\Local\\Microsoft\\WindowsApps;%USERPROFILE%\\.config\\etc;'
|
||||
execa['mockResolvedValueOnce']({
|
||||
failed: false,
|
||||
stdout: `
|
||||
HKEY_CURRENT_USER\\Environment
|
||||
PNPM_HOME REG_EXPAND_SZ .pnpm\\home
|
||||
Path REG_EXPAND_SZ ${currentPathInRegistry}
|
||||
`,
|
||||
}).mockResolvedValueOnce({
|
||||
failed: false,
|
||||
stdout: 'PATH UPDATED',
|
||||
}).mockResolvedValue({
|
||||
failed: true,
|
||||
stderr: 'UNEXPECTED',
|
||||
})
|
||||
|
||||
const output = await setup.handler({
|
||||
pnpmHomeDir: __dirname,
|
||||
})
|
||||
|
||||
expect(execa).toHaveBeenNthCalledWith(1, 'reg', ['query', regKey])
|
||||
expect(execa).toHaveBeenNthCalledWith(2, 'reg', ['add', regKey, '/v', 'Path', '/t', 'REG_EXPAND_SZ', '/d', `${'.pnpm\\home'};${currentPathInRegistry}`, '/f'])
|
||||
expect(execa).toHaveBeenNthCalledWith(3, 'setx', ['PNPM_HOME', '.pnpm\\home'])
|
||||
expect(output).toContain(`Currently 'PNPM_HOME' is set to '${'.pnpm\\home'}'`)
|
||||
expect(output).toContain('Updating PATH')
|
||||
expect(output).toContain('PATH UPDATED')
|
||||
})
|
||||
@@ -0,0 +1,48 @@
|
||||
import execa from 'execa'
|
||||
import { setup } from '@pnpm/plugin-commands-setup'
|
||||
|
||||
jest.mock('execa')
|
||||
|
||||
let originalShell: string | undefined
|
||||
let originalPlatform = ''
|
||||
|
||||
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'
|
||||
|
||||
test('Existing installation', async () => {
|
||||
execa['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 output = await setup.handler({
|
||||
pnpmHomeDir: __dirname,
|
||||
})
|
||||
|
||||
expect(execa).toHaveBeenNthCalledWith(1, 'reg', ['query', regKey])
|
||||
expect(output).toContain(`Currently 'PNPM_HOME' is set to '${'.pnpm\\home'}'`)
|
||||
expect(output).toContain('PATH already contains PNPM_HOME')
|
||||
})
|
||||
@@ -0,0 +1,59 @@
|
||||
import execa from 'execa'
|
||||
import { setup } from '@pnpm/plugin-commands-setup'
|
||||
|
||||
jest.mock('execa')
|
||||
|
||||
let originalShell: string | undefined
|
||||
let originalPlatform = ''
|
||||
|
||||
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'
|
||||
|
||||
test('Failure to install', async () => {
|
||||
const currentPathInRegistry = '%USERPROFILE%\\AppData\\Local\\Microsoft\\WindowsApps;%USERPROFILE%\\.config\\etc;'
|
||||
|
||||
execa['mockResolvedValueOnce']({
|
||||
failed: false,
|
||||
stdout: `
|
||||
HKEY_CURRENT_USER\\Environment
|
||||
Path REG_EXPAND_SZ ${currentPathInRegistry}
|
||||
`,
|
||||
}).mockResolvedValueOnce({
|
||||
failed: true,
|
||||
stderr: 'FAILED TO SET PNPM_HOME',
|
||||
}).mockResolvedValueOnce({
|
||||
failed: true,
|
||||
stderr: 'FAILED TO UPDATE PATH',
|
||||
}).mockResolvedValue({
|
||||
failed: true,
|
||||
stderr: 'UNEXPECTED',
|
||||
})
|
||||
|
||||
const output = await setup.handler({
|
||||
pnpmHomeDir: __dirname,
|
||||
})
|
||||
|
||||
expect(execa).toHaveBeenNthCalledWith(1, 'reg', ['query', regKey])
|
||||
expect(execa).toHaveBeenNthCalledWith(2, 'reg', ['add', regKey, '/v', 'PNPM_HOME', '/t', 'REG_EXPAND_SZ', '/d', __dirname, '/f'])
|
||||
expect(execa).toHaveBeenNthCalledWith(3, 'reg', ['add', regKey, '/v', 'Path', '/t', 'REG_EXPAND_SZ', '/d', `${__dirname};${currentPathInRegistry}`, '/f'])
|
||||
expect(output).toContain(`Setting 'PNPM_HOME' to value '${__dirname}`)
|
||||
expect(output).toContain('FAILED TO SET PNPM_HOME')
|
||||
expect(output).toContain('Updating PATH')
|
||||
expect(output).toContain('FAILED TO UPDATE PATH')
|
||||
})
|
||||
@@ -2,7 +2,8 @@
|
||||
"extends": "@pnpm/tsconfig",
|
||||
"compilerOptions": {
|
||||
"outDir": "lib",
|
||||
"rootDir": "src"
|
||||
"rootDir": "src",
|
||||
"target": "es2020"
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
|
||||
2
pnpm-lock.yaml
generated
2
pnpm-lock.yaml
generated
@@ -2378,9 +2378,11 @@ importers:
|
||||
'@pnpm/logger': ^4.0.0
|
||||
'@pnpm/plugin-commands-setup': 'link:'
|
||||
'@pnpm/prepare': workspace:0.0.26
|
||||
execa: ^5.0.0
|
||||
render-help: ^1.0.1
|
||||
dependencies:
|
||||
'@pnpm/cli-utils': link:../cli-utils
|
||||
execa: 5.1.1
|
||||
render-help: 1.0.2
|
||||
devDependencies:
|
||||
'@pnpm/logger': 4.0.0
|
||||
|
||||
Reference in New Issue
Block a user