mirror of
https://github.com/pnpm/action-setup.git
synced 2026-04-04 03:10:11 +08:00
fix: point pn to pnpm binary directly, not to @pnpm/exe/pn
pnpm self-update only replaces the pnpm binary — it does not update other files in the @pnpm/exe package (setup.js, pn, pnpx, pnx all remain from the v10 bootstrap). So @pnpm/exe/pn may not exist at all. Instead of relying on @pnpm/exe/pn, create the aliases directly: - pn → symlink to the pnpm binary - pnpx/pnx → shell scripts that exec "pnpm dlx" Also remove the setup.js call after self-update since it's no longer needed and would run the v10 version which doesn't know about pn.
This commit is contained in:
286
dist/index.js
vendored
286
dist/index.js
vendored
File diff suppressed because one or more lines are too long
@@ -12,16 +12,14 @@ async function createTempDir (): Promise<string> {
|
|||||||
async function setupStandaloneFixture (binDir: string): Promise<void> {
|
async function setupStandaloneFixture (binDir: string): Promise<void> {
|
||||||
const exeDir = path.join(binDir, '..', '@pnpm', 'exe')
|
const exeDir = path.join(binDir, '..', '@pnpm', 'exe')
|
||||||
await mkdir(exeDir, { recursive: true })
|
await mkdir(exeDir, { recursive: true })
|
||||||
await writeFile(path.join(exeDir, 'pn'), '#!/bin/sh\necho pn\n', { mode: 0o755 })
|
// Only the pnpm binary exists — pn/pnpx/pnx may not exist after self-update
|
||||||
await writeFile(path.join(exeDir, 'pnpx'), '#!/bin/sh\necho pnpx\n', { mode: 0o755 })
|
await writeFile(path.join(exeDir, 'pnpm'), '#!/bin/sh\necho pnpm\n', { mode: 0o755 })
|
||||||
await writeFile(path.join(exeDir, 'pnx'), '#!/bin/sh\necho pnx\n', { mode: 0o755 })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setupNonStandaloneFixture (binDir: string): Promise<void> {
|
async function setupNonStandaloneFixture (binDir: string): Promise<void> {
|
||||||
const pnpmBinDir = path.join(binDir, '..', 'pnpm', 'bin')
|
const pnpmBinDir = path.join(binDir, '..', 'pnpm', 'bin')
|
||||||
await mkdir(pnpmBinDir, { recursive: true })
|
await mkdir(pnpmBinDir, { recursive: true })
|
||||||
await writeFile(path.join(pnpmBinDir, 'pnpm.cjs'), 'console.log("pnpm")\n')
|
await writeFile(path.join(pnpmBinDir, 'pnpm.cjs'), 'console.log("pnpm")\n')
|
||||||
await writeFile(path.join(pnpmBinDir, 'pnpx.cjs'), 'console.log("pnpx")\n')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('ensureAliasLinks', () => {
|
describe('ensureAliasLinks', () => {
|
||||||
@@ -34,78 +32,85 @@ describe('ensureAliasLinks', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('standalone mode', () => {
|
describe('standalone mode', () => {
|
||||||
it('creates symlinks on unix when targets exist', async () => {
|
it('creates pn as symlink to pnpm binary on unix', async () => {
|
||||||
await setupStandaloneFixture(binDir)
|
await setupStandaloneFixture(binDir)
|
||||||
|
|
||||||
await ensureAliasLinks(binDir, true, 'linux')
|
await ensureAliasLinks(binDir, true, 'linux')
|
||||||
|
|
||||||
expect(existsSync(path.join(binDir, 'pn'))).toBe(true)
|
|
||||||
expect(existsSync(path.join(binDir, 'pnpx'))).toBe(true)
|
|
||||||
expect(existsSync(path.join(binDir, 'pnx'))).toBe(true)
|
|
||||||
|
|
||||||
const pnTarget = await readlink(path.join(binDir, 'pn'))
|
const pnTarget = await readlink(path.join(binDir, 'pn'))
|
||||||
expect(pnTarget).toBe(path.join('..', '@pnpm', 'exe', 'pn'))
|
expect(pnTarget).toBe(path.join('..', '@pnpm', 'exe', 'pnpm'))
|
||||||
})
|
})
|
||||||
|
|
||||||
it('creates .cmd and .ps1 shims on windows when targets exist', async () => {
|
it('creates pnpx and pnx as shell scripts calling pnpm dlx on unix', async () => {
|
||||||
|
await setupStandaloneFixture(binDir)
|
||||||
|
|
||||||
|
await ensureAliasLinks(binDir, true, 'linux')
|
||||||
|
|
||||||
|
for (const name of ['pnpx', 'pnx']) {
|
||||||
|
const content = await readFile(path.join(binDir, name), 'utf8')
|
||||||
|
expect(content).toContain('pnpm')
|
||||||
|
expect(content).toContain('dlx')
|
||||||
|
expect(content).toContain('exec')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('creates .cmd and .ps1 shims on windows', async () => {
|
||||||
await setupStandaloneFixture(binDir)
|
await setupStandaloneFixture(binDir)
|
||||||
|
|
||||||
await ensureAliasLinks(binDir, true, 'win32')
|
await ensureAliasLinks(binDir, true, 'win32')
|
||||||
|
|
||||||
// Should create .cmd shims, not extensionless symlinks
|
// pn shims
|
||||||
expect(existsSync(path.join(binDir, 'pn.cmd'))).toBe(true)
|
const pnCmd = await readFile(path.join(binDir, 'pn.cmd'), 'utf8')
|
||||||
expect(existsSync(path.join(binDir, 'pnx.cmd'))).toBe(true)
|
expect(pnCmd).toContain('pnpm')
|
||||||
expect(existsSync(path.join(binDir, 'pn.ps1'))).toBe(true)
|
expect(pnCmd).toContain('%*')
|
||||||
expect(existsSync(path.join(binDir, 'pnx.ps1'))).toBe(true)
|
expect(pnCmd).not.toContain('dlx')
|
||||||
|
|
||||||
// Should not create extensionless symlinks on windows
|
const pnPs1 = await readFile(path.join(binDir, 'pn.ps1'), 'utf8')
|
||||||
|
expect(pnPs1).toContain('pnpm')
|
||||||
|
expect(pnPs1).toContain('@args')
|
||||||
|
|
||||||
|
// pnpx/pnx shims call pnpm dlx
|
||||||
|
const pnpxCmd = await readFile(path.join(binDir, 'pnpx.cmd'), 'utf8')
|
||||||
|
expect(pnpxCmd).toContain('pnpm')
|
||||||
|
expect(pnpxCmd).toContain('dlx')
|
||||||
|
|
||||||
|
// Should not create extensionless files on windows
|
||||||
expect(existsSync(path.join(binDir, 'pn'))).toBe(false)
|
expect(existsSync(path.join(binDir, 'pn'))).toBe(false)
|
||||||
expect(existsSync(path.join(binDir, 'pnx'))).toBe(false)
|
|
||||||
|
|
||||||
const cmdContent = await readFile(path.join(binDir, 'pn.cmd'), 'utf8')
|
|
||||||
expect(cmdContent).toContain(path.join('..', '@pnpm', 'exe', 'pn'))
|
|
||||||
expect(cmdContent).toContain('%*')
|
|
||||||
|
|
||||||
const ps1Content = await readFile(path.join(binDir, 'pn.ps1'), 'utf8')
|
|
||||||
expect(ps1Content).toContain(path.join('..', '@pnpm', 'exe', 'pn'))
|
|
||||||
expect(ps1Content).toContain('@args')
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('non-standalone mode', () => {
|
describe('non-standalone mode', () => {
|
||||||
it('creates symlinks on unix when targets exist', async () => {
|
it('creates pn as symlink to pnpm.cjs on unix', async () => {
|
||||||
await setupNonStandaloneFixture(binDir)
|
await setupNonStandaloneFixture(binDir)
|
||||||
|
|
||||||
await ensureAliasLinks(binDir, false, 'linux')
|
await ensureAliasLinks(binDir, false, 'linux')
|
||||||
|
|
||||||
expect(existsSync(path.join(binDir, 'pn'))).toBe(true)
|
|
||||||
expect(existsSync(path.join(binDir, 'pnpx'))).toBe(true)
|
|
||||||
expect(existsSync(path.join(binDir, 'pnx'))).toBe(true)
|
|
||||||
|
|
||||||
const pnTarget = await readlink(path.join(binDir, 'pn'))
|
const pnTarget = await readlink(path.join(binDir, 'pn'))
|
||||||
expect(pnTarget).toBe(path.join('..', 'pnpm', 'bin', 'pnpm.cjs'))
|
expect(pnTarget).toBe(path.join('..', 'pnpm', 'bin', 'pnpm.cjs'))
|
||||||
|
|
||||||
// pnx should point to pnpx.cjs (same as pnpx)
|
|
||||||
const pnxTarget = await readlink(path.join(binDir, 'pnx'))
|
|
||||||
expect(pnxTarget).toBe(path.join('..', 'pnpm', 'bin', 'pnpx.cjs'))
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('creates .cmd shims on windows when targets exist', async () => {
|
it('creates pnpx/pnx scripts on unix', async () => {
|
||||||
|
await setupNonStandaloneFixture(binDir)
|
||||||
|
|
||||||
|
await ensureAliasLinks(binDir, false, 'linux')
|
||||||
|
|
||||||
|
const content = await readFile(path.join(binDir, 'pnpx'), 'utf8')
|
||||||
|
expect(content).toContain('pnpm.cjs')
|
||||||
|
expect(content).toContain('dlx')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('creates .cmd shims on windows', async () => {
|
||||||
await setupNonStandaloneFixture(binDir)
|
await setupNonStandaloneFixture(binDir)
|
||||||
|
|
||||||
await ensureAliasLinks(binDir, false, 'win32')
|
await ensureAliasLinks(binDir, false, 'win32')
|
||||||
|
|
||||||
expect(existsSync(path.join(binDir, 'pn.cmd'))).toBe(true)
|
|
||||||
|
|
||||||
const cmdContent = await readFile(path.join(binDir, 'pn.cmd'), 'utf8')
|
const cmdContent = await readFile(path.join(binDir, 'pn.cmd'), 'utf8')
|
||||||
expect(cmdContent).toContain(path.join('..', 'pnpm', 'bin', 'pnpm.cjs'))
|
expect(cmdContent).toContain(path.join('pnpm', 'bin', 'pnpm.cjs'))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('skips when targets do not exist', () => {
|
describe('skips when pnpm binary does not exist', () => {
|
||||||
it('creates no links when target directory is empty (v10)', async () => {
|
it('creates no links on unix', async () => {
|
||||||
// Don't create any fixture files — simulates pnpm v10 without aliases
|
|
||||||
|
|
||||||
await ensureAliasLinks(binDir, true, 'linux')
|
await ensureAliasLinks(binDir, true, 'linux')
|
||||||
|
|
||||||
expect(existsSync(path.join(binDir, 'pn'))).toBe(false)
|
expect(existsSync(path.join(binDir, 'pn'))).toBe(false)
|
||||||
@@ -113,25 +118,23 @@ describe('ensureAliasLinks', () => {
|
|||||||
expect(existsSync(path.join(binDir, 'pnx'))).toBe(false)
|
expect(existsSync(path.join(binDir, 'pnx'))).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('creates no shims on windows when targets do not exist', async () => {
|
it('creates no shims on windows', async () => {
|
||||||
await ensureAliasLinks(binDir, true, 'win32')
|
await ensureAliasLinks(binDir, true, 'win32')
|
||||||
|
|
||||||
expect(existsSync(path.join(binDir, 'pn.cmd'))).toBe(false)
|
expect(existsSync(path.join(binDir, 'pn.cmd'))).toBe(false)
|
||||||
expect(existsSync(path.join(binDir, 'pnx.cmd'))).toBe(false)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('overwrites existing broken links', () => {
|
describe('overwrites existing broken shims', () => {
|
||||||
it('replaces existing file with symlink on unix', async () => {
|
it('replaces npm broken shim with symlink on unix', async () => {
|
||||||
await setupStandaloneFixture(binDir)
|
await setupStandaloneFixture(binDir)
|
||||||
// Simulate npm's broken shim (points to .tools/ placeholder)
|
// Simulate npm's broken shim pointing to .tools/ placeholder
|
||||||
await writeFile(path.join(binDir, 'pn'), '#!/bin/sh\nexec broken\n')
|
await writeFile(path.join(binDir, 'pn'), '#!/bin/sh\nexec .tools/broken "$@"\n')
|
||||||
|
|
||||||
await ensureAliasLinks(binDir, true, 'linux')
|
await ensureAliasLinks(binDir, true, 'linux')
|
||||||
|
|
||||||
// Should be replaced with a symlink to the real target
|
|
||||||
const target = await readlink(path.join(binDir, 'pn'))
|
const target = await readlink(path.join(binDir, 'pn'))
|
||||||
expect(target).toBe(path.join('..', '@pnpm', 'exe', 'pn'))
|
expect(target).toBe(path.join('..', '@pnpm', 'exe', 'pnpm'))
|
||||||
})
|
})
|
||||||
|
|
||||||
it('replaces existing .cmd shims on windows', async () => {
|
it('replaces existing .cmd shims on windows', async () => {
|
||||||
@@ -141,7 +144,7 @@ describe('ensureAliasLinks', () => {
|
|||||||
await ensureAliasLinks(binDir, true, 'win32')
|
await ensureAliasLinks(binDir, true, 'win32')
|
||||||
|
|
||||||
const content = await readFile(path.join(binDir, 'pn.cmd'), 'utf8')
|
const content = await readFile(path.join(binDir, 'pn.cmd'), 'utf8')
|
||||||
expect(content).toContain(path.join('..', '@pnpm', 'exe', 'pn'))
|
expect(content).toContain('pnpm')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2,32 +2,16 @@ import { unlink, writeFile, symlink } from 'fs/promises'
|
|||||||
import { existsSync } from 'fs'
|
import { existsSync } from 'fs'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
|
|
||||||
interface AliasDefinition {
|
function shScript (command: string): string {
|
||||||
name: string
|
return `#!/bin/sh\nexec ${command} "$@"\n`
|
||||||
target: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAliases (standalone: boolean): AliasDefinition[] {
|
function cmdShim (command: string): string {
|
||||||
if (standalone) {
|
return `@ECHO off\r\n${command} %*\r\n`
|
||||||
return [
|
|
||||||
{ name: 'pn', target: path.join('..', '@pnpm', 'exe', 'pn') },
|
|
||||||
{ name: 'pnpx', target: path.join('..', '@pnpm', 'exe', 'pnpx') },
|
|
||||||
{ name: 'pnx', target: path.join('..', '@pnpm', 'exe', 'pnx') },
|
|
||||||
]
|
|
||||||
}
|
|
||||||
return [
|
|
||||||
{ name: 'pn', target: path.join('..', 'pnpm', 'bin', 'pnpm.cjs') },
|
|
||||||
{ name: 'pnpx', target: path.join('..', 'pnpm', 'bin', 'pnpx.cjs') },
|
|
||||||
{ name: 'pnx', target: path.join('..', 'pnpm', 'bin', 'pnpx.cjs') },
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function cmdShim (target: string): string {
|
function pwshShim (command: string): string {
|
||||||
return `@ECHO off\r\n"%~dp0\\${target}" %*\r\n`
|
return `#!/usr/bin/env pwsh\n${command} @args\n`
|
||||||
}
|
|
||||||
|
|
||||||
function pwshShim (target: string): string {
|
|
||||||
return `#!/usr/bin/env pwsh\n& "$PSScriptRoot\\${target}" @args\n`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function forceSymlink (target: string, linkPath: string): Promise<void> {
|
async function forceSymlink (target: string, linkPath: string): Promise<void> {
|
||||||
@@ -35,27 +19,52 @@ async function forceSymlink (target: string, linkPath: string): Promise<void> {
|
|||||||
await symlink(target, linkPath)
|
await symlink(target, linkPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function forceWriteFile (filePath: string, content: string, mode?: number): Promise<void> {
|
||||||
|
try { await unlink(filePath) } catch {}
|
||||||
|
await writeFile(filePath, content, { mode })
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create pn/pnpx/pnx alias links in the bin directory.
|
* Create pn/pnpx/pnx alias links in the bin directory.
|
||||||
* On Unix, creates symlinks. On Windows, creates .cmd and .ps1 shims.
|
|
||||||
* Only creates links when the target file actually exists (pnpm v11+).
|
|
||||||
*
|
*
|
||||||
* Existing links are always replaced because npm may have created shims
|
* pn is an alias for pnpm, so it symlinks (or shims) to the pnpm binary.
|
||||||
* pointing to an isolated .tools/ copy that has stale placeholder files.
|
* pnpx/pnx are aliases for "pnpm dlx", created as shell scripts.
|
||||||
|
*
|
||||||
|
* This does NOT rely on the @pnpm/exe package having pn/pnx files, because
|
||||||
|
* pnpm self-update only replaces the pnpm binary — it doesn't update other
|
||||||
|
* files in the package. The aliases are created by pointing pn directly to
|
||||||
|
* the pnpm binary, and pnpx/pnx as scripts that exec "pnpm dlx".
|
||||||
|
*
|
||||||
|
* Only creates links when the pnpm binary exists in the expected location
|
||||||
|
* (i.e. the package has been installed). This is always true after bootstrap.
|
||||||
*/
|
*/
|
||||||
export async function ensureAliasLinks (binDir: string, standalone: boolean, platform: NodeJS.Platform = process.platform): Promise<void> {
|
export async function ensureAliasLinks (binDir: string, standalone: boolean, platform: NodeJS.Platform = process.platform): Promise<void> {
|
||||||
const aliases = getAliases(standalone)
|
|
||||||
const isWindows = platform === 'win32'
|
const isWindows = platform === 'win32'
|
||||||
|
|
||||||
for (const { name, target } of aliases) {
|
// Determine the pnpm binary path relative to binDir
|
||||||
const resolvedTarget = path.resolve(binDir, target)
|
const pnpmTarget = standalone
|
||||||
if (!existsSync(resolvedTarget)) continue
|
? path.join('..', '@pnpm', 'exe', 'pnpm')
|
||||||
|
: path.join('..', 'pnpm', 'bin', 'pnpm.cjs')
|
||||||
|
|
||||||
if (isWindows) {
|
const resolvedPnpm = path.resolve(binDir, pnpmTarget)
|
||||||
await writeFile(path.join(binDir, `${name}.cmd`), cmdShim(target))
|
if (!existsSync(resolvedPnpm)) return
|
||||||
await writeFile(path.join(binDir, `${name}.ps1`), pwshShim(target))
|
|
||||||
} else {
|
if (isWindows) {
|
||||||
await forceSymlink(target, path.join(binDir, name))
|
// pn → calls pnpm directly
|
||||||
|
await writeFile(path.join(binDir, 'pn.cmd'), cmdShim(`"%~dp0\\${pnpmTarget}"`))
|
||||||
|
await writeFile(path.join(binDir, 'pn.ps1'), pwshShim(`& "$PSScriptRoot\\${pnpmTarget}"`))
|
||||||
|
// pnpx/pnx → calls pnpm dlx
|
||||||
|
for (const name of ['pnpx', 'pnx']) {
|
||||||
|
await writeFile(path.join(binDir, `${name}.cmd`), cmdShim(`"%~dp0\\${pnpmTarget}" dlx`))
|
||||||
|
await writeFile(path.join(binDir, `${name}.ps1`), pwshShim(`& "$PSScriptRoot\\${pnpmTarget}" dlx`))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// pn → symlink to pnpm binary
|
||||||
|
await forceSymlink(pnpmTarget, path.join(binDir, 'pn'))
|
||||||
|
// pnpx/pnx → shell scripts that exec pnpm dlx
|
||||||
|
for (const name of ['pnpx', 'pnx']) {
|
||||||
|
const pnpmPath = `"$(dirname "$0")/${pnpmTarget}"`
|
||||||
|
await forceWriteFile(path.join(binDir, name), shScript(`${pnpmPath} dlx`), 0o755)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,17 +59,6 @@ export async function runSelfInstaller(inputs: Inputs): Promise<number> {
|
|||||||
if (exitCode !== 0) {
|
if (exitCode !== 0) {
|
||||||
return exitCode
|
return exitCode
|
||||||
}
|
}
|
||||||
|
|
||||||
// self-update replaces package files but does not re-run preinstall
|
|
||||||
// scripts, so setup.js (which hardlinks pn/pnpx/pnx to the binary)
|
|
||||||
// needs to be run explicitly.
|
|
||||||
if (standalone) {
|
|
||||||
const exeDir = path.join(dest, 'node_modules', '@pnpm', 'exe')
|
|
||||||
const setupScript = path.join(exeDir, 'setup.js')
|
|
||||||
if (existsSync(setupScript)) {
|
|
||||||
await runCommand(process.execPath, [setupScript], { cwd: exeDir })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create pn/pnx alias bin links if the installed version supports them
|
// Create pn/pnx alias bin links if the installed version supports them
|
||||||
|
|||||||
Reference in New Issue
Block a user