mirror of
https://github.com/pnpm/pnpm.git
synced 2026-03-28 03:51:40 -04:00
* feat: switch from pkg to Node.js SEA for creating standalone executables Replace @yao-pkg/pkg with Node.js native Single Executable Applications (--build-sea, Node.js 25.5+). The SEA binary embeds only pnpm.cjs (CJS bootstrap), while pnpm.mjs and all assets live in a dist/ directory shipped alongside the binary in platform-specific tarballs. * refactor: move dist/ from platform packages to @pnpm/exe The dist/ directory (pnpm.mjs, worker.js, templates, etc.) is identical across all platforms, so ship it once in @pnpm/exe instead of duplicating it in each platform package. Platform packages now only contain the binary. The self-updater installs @pnpm/exe (not the platform package) so it gets both dist/ and the binary via optionalDependencies. * refactor: externalize @reflink/reflink in esbuild bundle Make @reflink/reflink external in both the main and worker esbuild bundles so the require() calls resolve at runtime from dist/node_modules instead of being inlined. Add @reflink/reflink as a production dependency of both pnpm (bundled into dist/node_modules by bundle-deps.ts) and @pnpm/exe (installed by npm alongside the binary). For GitHub release tarballs, only the target platform's reflink package is kept. For @pnpm/exe npm publishing, all reflink platform packages are stripped from dist/ since npm installs the right one automatically. * chore: update cspell list * test: update system-node-version tests for SEA detection Mock @pnpm/cli-meta's detectIfCurrentPkgIsExecutable instead of setting process.pkg, which is no longer used for SEA detection. * test: improve cli-meta test coverage for SEA migration Add tests for detectIfCurrentPkgIsExecutable() (non-SEA path) and isExecutedByCorepack() which were previously untested. The SEA=true path of detectIfCurrentPkgIsExecutable() cannot be unit tested since node:sea is unavailable in an ESM test environment. * refactor: move GitHub tarball assembly to copy-artifacts.ts build-artifacts.ts (prepublishOnly of @pnpm/exe) now only builds the SEA executables and prepares the exe npm dist/. The per-target dist/ assembly for GitHub release tarballs moves to copy-artifacts.ts, which is the natural owner of that concern. Other changes: - Extract getReflinkKeepPackages/stripReflinkPackages to reflink-utils.ts with tests using node:test - Move --force from top-level pnpm install in release.yml to the pnpm deploy in bundle-deps.ts, where it is actually needed to install all @reflink/reflink-* platform packages into dist/node_modules - Change @pnpm/exe prepublishOnly to run pnpm's full prepublishOnly (compile + bundle-deps) so dist/node_modules is populated before build-artifacts.ts and copy-artifacts.ts read from pnpm/dist * fix: copy dist/ alongside binary when running pnpm setup for SEA When the pnpm CLI is a Node.js SEA binary, it requires a dist/ directory adjacent to the executable at runtime (containing pnpm.mjs and bundled node_modules). The copyCli function in plugin-commands-setup now copies dist/ from alongside the current binary into the tools directory so that the installed pnpm works correctly after `pnpm setup`. * fix: avoid argument list too long when creating Windows zip archives * fix: propagate errors in copy-artifacts script Previously errors in createArtifactTarball were swallowed, causing the script to exit 0 even when artifact creation failed. Now errors are re-thrown with a descriptive message, and the top-level IIFE has a .catch() handler that sets a non-zero exit code. * refactor: remove reflink-utils.ts from @pnpm/exe The stripReflinkPackages call in build-artifacts.ts stripped all platform packages while keeping @reflink/reflink. Instead, just remove the entire @reflink directory from dist/ — @pnpm/exe already declares @reflink/reflink as a runtime dependency, so npm installs it (along with the right platform package via optionalDependencies) automatically. This eliminates reflink-utils.ts, its tests, and the code duplication with copy-artifacts.ts.
122 lines
4.3 KiB
TypeScript
122 lines
4.3 KiB
TypeScript
import fs from 'fs'
|
|
import * as execa from 'execa'
|
|
import path from 'path'
|
|
import makeEmptyDir from 'make-empty-dir'
|
|
import stream from 'stream'
|
|
import * as tar from 'tar'
|
|
import { glob } from 'tinyglobby'
|
|
|
|
const repoRoot = path.join(import.meta.dirname, '../../..')
|
|
const dest = path.join(repoRoot, 'dist')
|
|
const artifactsDir = path.join(repoRoot, 'pnpm/artifacts')
|
|
const pnpmDistDir = path.join(repoRoot, 'pnpm/dist')
|
|
|
|
;(async () => {
|
|
await makeEmptyDir(dest)
|
|
if (!fs.existsSync(path.join(artifactsDir, 'linux-x64/pnpm'))) {
|
|
execa.sync('pnpm', ['--filter=@pnpm/exe', 'run', 'prepublishOnly'], {
|
|
cwd: repoRoot,
|
|
stdio: 'inherit',
|
|
})
|
|
}
|
|
await createArtifactTarball('linux-x64', 'pnpm')
|
|
await createArtifactTarball('linuxstatic-x64', 'pnpm')
|
|
await createArtifactTarball('linuxstatic-arm64', 'pnpm')
|
|
await createArtifactTarball('linux-arm64', 'pnpm')
|
|
await createArtifactTarball('macos-x64', 'pnpm')
|
|
await createArtifactTarball('macos-arm64', 'pnpm')
|
|
await createArtifactTarball('win-x64', 'pnpm.exe')
|
|
await createArtifactTarball('win-arm64', 'pnpm.exe')
|
|
await createSourceMapsArchive()
|
|
})().catch((err) => {
|
|
console.error(err)
|
|
process.exitCode = 1
|
|
})
|
|
|
|
async function createArtifactTarball (target: string, binaryName: string): Promise<void> {
|
|
try {
|
|
const artifactDir = path.join(artifactsDir, target)
|
|
const binaryPath = path.join(artifactDir, binaryName)
|
|
if (!fs.existsSync(binaryPath)) {
|
|
console.log(`Warning: ${binaryPath} not found, skipping ${target}`)
|
|
return
|
|
}
|
|
|
|
// Copy dist/ from the pnpm build output and strip non-target reflink packages.
|
|
// Source maps are removed from this copy — they are archived separately via
|
|
// createSourceMapsArchive(), which reads from the original pnpmDistDir.
|
|
const distDest = path.join(artifactDir, 'dist')
|
|
fs.rmSync(distDest, { recursive: true, force: true })
|
|
fs.cpSync(pnpmDistDir, distDest, { recursive: true })
|
|
stripReflinkPackages(distDest, getReflinkKeepPackages(target))
|
|
for (const mapFile of await glob('**/*.map', { cwd: distDest })) {
|
|
fs.rmSync(path.join(distDest, mapFile))
|
|
}
|
|
|
|
const isWindows = target.startsWith('win-')
|
|
const archiveName = isWindows ? `pnpm-${target}.zip` : `pnpm-${target}.tar.gz`
|
|
|
|
if (isWindows) {
|
|
// Create zip for Windows
|
|
const zipPath = path.join(dest, archiveName)
|
|
execa.sync('zip', ['-r', zipPath, binaryName, 'dist'], {
|
|
cwd: artifactDir,
|
|
stdio: 'inherit',
|
|
})
|
|
} else {
|
|
// Create tar.gz for Unix
|
|
await stream.promises.pipeline(
|
|
tar.create({ gzip: true, cwd: artifactDir }, [binaryName, 'dist']),
|
|
fs.createWriteStream(path.join(dest, archiveName))
|
|
)
|
|
}
|
|
console.log(`Created ${archiveName}`)
|
|
} catch (err) {
|
|
console.error(`Failed to create artifact for target "${target}":`, err)
|
|
throw err
|
|
}
|
|
}
|
|
|
|
async function createSourceMapsArchive () {
|
|
// The tar.create function can accept a filter callback function, but this
|
|
// approach ends up adding empty directories to the archive. Using tinyglobby
|
|
// instead.
|
|
const mapFiles = await glob('**/*.map', { cwd: pnpmDistDir })
|
|
|
|
await stream.promises.pipeline(
|
|
tar.create({ gzip: true, cwd: pnpmDistDir }, mapFiles),
|
|
fs.createWriteStream(path.join(dest, 'source-maps.tgz'))
|
|
)
|
|
}
|
|
|
|
// Reflink platform package names needed for a build target.
|
|
// Target format: 'linux-x64', 'linuxstatic-arm64', 'macos-arm64', 'win-x64'.
|
|
function getReflinkKeepPackages (target: string): string[] {
|
|
if (target.startsWith('macos-')) {
|
|
return [`@reflink/reflink-darwin-${target.slice('macos-'.length)}`]
|
|
}
|
|
if (target.startsWith('win-')) {
|
|
return [`@reflink/reflink-win32-${target.slice('win-'.length)}-msvc`]
|
|
}
|
|
if (target.startsWith('linux')) {
|
|
const arch = target.includes('arm64') ? 'arm64' : 'x64'
|
|
return [
|
|
`@reflink/reflink-linux-${arch}-gnu`,
|
|
`@reflink/reflink-linux-${arch}-musl`,
|
|
]
|
|
}
|
|
return []
|
|
}
|
|
|
|
function stripReflinkPackages (distDir: string, keepPackages: string[]): void {
|
|
const reflinkDir = path.join(distDir, 'node_modules', '@reflink')
|
|
if (!fs.existsSync(reflinkDir)) return
|
|
|
|
for (const entry of fs.readdirSync(reflinkDir)) {
|
|
if (entry === 'reflink') continue // keep the main package
|
|
if (!keepPackages.includes(`@reflink/${entry}`)) {
|
|
fs.rmSync(path.join(reflinkDir, entry), { recursive: true })
|
|
}
|
|
}
|
|
}
|