mirror of
https://github.com/pnpm/pnpm.git
synced 2026-06-27 09:25:24 -04:00
feat(pnpm): publish pnpm v12 (the Rust port) as pnpm and @pnpm/exe (#12670)
Make the pacquet Rust port installable from npm as pnpm v12, published with equal content under two names — `pnpm` and `@pnpm/exe` — on the `next-12` dist-tag (never `latest`, which keeps serving the production TypeScript pnpm v11). First release is 12.0.0-alpha.0. CLI presents as pnpm: clap name/bin_name are `pnpm`; PACQUET_VERSION holds the pnpm 12.x version; the default User-Agent is the plain `pnpm/<version>` form; the reporter footer reads `Done in ... using pnpm v<version>`; the `pnpm link` / `pnpm patch` / `pnpm patch-commit` hints now name pnpm (parity fixes); and launched as `pnpx`/`pnx`, the binary injects the `dlx` subcommand itself by detecting its current_exe name (porting pnpm's buildArgv). npm packaging mirrors how `@pnpm/exe` ships pnpm: both the `pnpm` and `@pnpm/exe` wrappers ship root-level placeholder bins (pnpm/pn/pnpx/pnx) plus an install.js preinstall that hardlinks the host's native binary over the placeholder (no Node launcher). Native binaries publish as `@pnpm/exe.<platform>-<arch>[-musl]` (file `pnpm` inside). `pacquet`/`@pnpm/pacquet` are no longer published. installPnpm initializes v12 exactly like `@pnpm/exe`: linkExePlatformBinary takes the wrapper name and resolves the native binary via require, and PNPM_ALLOW_BUILDS includes both names so the GVS hash is per-platform. resolvePackageManagerIntegrities already resolves both `pnpm` and `@pnpm/exe`, so publishing `@pnpm/exe@12` makes self-update / version-switch work with no v12-specific logic. From v12 the install always uses the `pnpm` package (even from a `@pnpm/exe` build), via pnpmPackageNameToInstall(). The configDependencies install-engine path still installs the published `pacquet`/`@pnpm/pacquet` releases (which ship `@pacquet/<target>` binaries) and is left untouched.
This commit is contained in:
6
.changeset/link-pnpm-v12-exe-binaries.md
Normal file
6
.changeset/link-pnpm-v12-exe-binaries.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@pnpm/engine.pm.commands": minor
|
||||
"pnpm": minor
|
||||
---
|
||||
|
||||
`pnpm self-update` and `packageManager` version-switching can now install and link pnpm v12 (the Rust port), published with equal content under both the `pnpm` and `@pnpm/exe` names on the `next-12` dist-tag. Its native binaries ship as `@pnpm/exe.<platform>-<arch>` packages, which pnpm's built-in installer links directly — no Node.js launcher, so the command pays no Node startup cost. v12 is initialized exactly like `@pnpm/exe`, including per-platform global-virtual-store hashing. From v12 onward the install converges on the unscoped `pnpm` package (the Rust exe) — even when updating from the SEA `@pnpm/exe` build.
|
||||
67
.github/workflows/pacquet-release-to-npm.yml
vendored
67
.github/workflows/pacquet-release-to-npm.yml
vendored
@@ -1,16 +1,27 @@
|
||||
name: Release Pacquet
|
||||
name: Release pnpm (Rust)
|
||||
|
||||
# Manual-trigger only. Type the version to publish — the workflow patches
|
||||
# pacquet/npm/pacquet/package.json with it before generating per-platform
|
||||
# packages and publishing. No git tag is created and no GitHub release
|
||||
# asset is uploaded; npm is the authoritative artifact store.
|
||||
# Manual-trigger only: patches the version into pacquet/npm/pnpm/package.json,
|
||||
# generates the per-platform packages, and publishes (no git tag / release asset
|
||||
# — npm is the artifact store). Ships the Rust port under two names, `pnpm` and
|
||||
# `@pnpm/exe`, both depending on the `@pnpm/exe.<target>` natives. The `tag`
|
||||
# defaults to `next-12` so a release never touches the production `latest`.
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'Version to publish (e.g. 0.2.3 or 0.2.3-rc.1)'
|
||||
description: 'Version to publish (e.g. 12.0.0-alpha.0)'
|
||||
required: true
|
||||
type: string
|
||||
tag:
|
||||
# A `choice` (not free-form) so a mistaken `latest` can't move the
|
||||
# production `pnpm` dist-tag. `latest` is intentionally omitted.
|
||||
description: 'npm dist-tag to publish under'
|
||||
required: true
|
||||
default: 'next-12'
|
||||
type: choice
|
||||
options:
|
||||
- next-12
|
||||
- next
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
@@ -160,18 +171,15 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Inject version into pacquet/npm/pacquet/package.json
|
||||
# The committed package.json has no `version` field; the version comes
|
||||
# from the workflow_dispatch input. Patching it here means
|
||||
# generate-packages.mjs (which copies the version into each
|
||||
# per-platform manifest) and `pnpm publish` (which reads from
|
||||
# package.json) both see the right value.
|
||||
- name: Inject version into pacquet/npm/pnpm/package.json
|
||||
# The committed package.json has no `version`; patch it from the input so
|
||||
# generate-packages.mjs and `pnpm publish` both pick it up.
|
||||
env:
|
||||
VERSION: ${{ inputs.version }}
|
||||
run: |
|
||||
jq --arg v "$VERSION" '.version = $v' pacquet/npm/pacquet/package.json > pacquet/npm/pacquet/package.json.tmp
|
||||
mv pacquet/npm/pacquet/package.json.tmp pacquet/npm/pacquet/package.json
|
||||
cat pacquet/npm/pacquet/package.json
|
||||
jq --arg v "$VERSION" '.version = $v' pacquet/npm/pnpm/package.json > pacquet/npm/pnpm/package.json.tmp
|
||||
mv pacquet/npm/pnpm/package.json.tmp pacquet/npm/pnpm/package.json
|
||||
cat pacquet/npm/pnpm/package.json
|
||||
|
||||
- name: Install pnpm and Node
|
||||
uses: pnpm/setup@77cf06832101b3ac8c65caaf76a21643936d07a4
|
||||
@@ -195,22 +203,19 @@ jobs:
|
||||
|
||||
- name: Generate npm packages
|
||||
run: |
|
||||
node pacquet/npm/pacquet/scripts/generate-packages.mjs
|
||||
cat pacquet/npm/pacquet/package.json
|
||||
for package in pacquet/npm/pacquet* pacquet/npm/pnpm-pacquet; do cat $package/package.json ; echo ; done
|
||||
node pacquet/npm/pnpm/scripts/generate-packages.mjs
|
||||
cat pacquet/npm/pnpm/package.json
|
||||
for package in pacquet/npm/pacquet-* pacquet/npm/pnpm*; do cat $package/package.json ; echo ; done
|
||||
|
||||
- name: Publish npm packages as latest
|
||||
# Auth is via npm's trusted publishing: `id-token: write` above grants
|
||||
# this job an OIDC token that pnpm/npm exchange with the registry,
|
||||
# so no NPM_TOKEN is needed. `--provenance` attaches the same OIDC
|
||||
# token to a provenance attestation on each tarball.
|
||||
# The trailing slash on $package/ changes it to publishing the directory.
|
||||
# `pnpm-pacquet` is the `@pnpm/pacquet` scoped alias mirror —
|
||||
# same shim and same `@pacquet/<target>` optional deps as the
|
||||
# unscoped `pacquet`. Published alongside so users can adopt
|
||||
# either name; the per-target binary sub-packages stay under
|
||||
# the `@pacquet/` scope (no need to mirror those).
|
||||
- name: Publish npm packages
|
||||
# Auth via npm trusted publishing (the `id-token: write` OIDC token), so
|
||||
# no NPM_TOKEN; `--provenance` attests each tarball. The trailing slash
|
||||
# publishes the directory. The glob covers the native sub-packages (build
|
||||
# dirs `pacquet-*`, published as `@pnpm/exe.<target>`) plus the `pnpm` and
|
||||
# `@pnpm/exe` (`pnpm-exe`) wrappers.
|
||||
env:
|
||||
TAG: ${{ inputs.tag }}
|
||||
run: |
|
||||
for package in pacquet/npm/pacquet* pacquet/npm/pnpm-pacquet; do
|
||||
pnpm publish "$package/" --tag latest --access public --provenance --no-git-checks
|
||||
for package in pacquet/npm/pacquet-* pacquet/npm/pnpm*; do
|
||||
pnpm publish "$package/" --tag "$TAG" --access public --provenance --no-git-checks
|
||||
done
|
||||
|
||||
@@ -110,8 +110,8 @@ use why::WhyArgs;
|
||||
|
||||
/// Experimental package manager for node.js written in rust.
|
||||
#[derive(Debug, Parser)]
|
||||
#[clap(name = "pacquet")]
|
||||
#[clap(bin_name = "pacquet")]
|
||||
#[clap(name = "pnpm")]
|
||||
#[clap(bin_name = "pnpm")]
|
||||
#[clap(version = pacquet_config::PACQUET_VERSION)]
|
||||
#[clap(about = "Experimental package manager for node.js")]
|
||||
pub struct CliArgs {
|
||||
|
||||
@@ -22,11 +22,11 @@ pub struct LinkArgs {
|
||||
#[derive(Debug, Display, Error, Diagnostic)]
|
||||
#[non_exhaustive]
|
||||
pub enum LinkError {
|
||||
#[display("You must provide a parameter. Usage: pacquet link <dir>")]
|
||||
#[display("You must provide a parameter. Usage: pnpm link <dir>")]
|
||||
#[diagnostic(code(ERR_PNPM_LINK_BAD_PARAMS))]
|
||||
NoParams,
|
||||
|
||||
#[display(r#"Cannot link by package name. Use a relative or absolute path instead, e.g. "pacquet link ./{name}""#)]
|
||||
#[display(r#"Cannot link by package name. Use a relative or absolute path instead, e.g. "pnpm link ./{name}""#)]
|
||||
#[diagnostic(code(ERR_PNPM_LINK_BAD_PARAMS))]
|
||||
LinkByName {
|
||||
#[error(not(source))]
|
||||
|
||||
@@ -36,7 +36,7 @@ pub struct PatchArgs {
|
||||
#[derive(Debug, Display, Error, Diagnostic)]
|
||||
#[non_exhaustive]
|
||||
pub enum PatchError {
|
||||
#[display("`pacquet patch` requires the package name")]
|
||||
#[display("`pnpm patch` requires the package name")]
|
||||
#[diagnostic(code(ERR_PNPM_MISSING_PACKAGE_NAME))]
|
||||
MissingPackageName,
|
||||
|
||||
@@ -538,7 +538,7 @@ fn print_success(edit_dir: &Path) {
|
||||
|
||||
fn render_success(edit_dir: &Path, colors_enabled: bool) -> String {
|
||||
let edit_dir = edit_dir.display().to_string();
|
||||
let command = format!("pacquet patch-commit {}", shell_quote(&edit_dir));
|
||||
let command = format!("pnpm patch-commit {}", shell_quote(&edit_dir));
|
||||
let edit_dir = if colors_enabled { edit_dir.blue().to_string() } else { edit_dir };
|
||||
let command = if colors_enabled { command.green().to_string() } else { command };
|
||||
render_success_parts(&edit_dir, &command)
|
||||
|
||||
@@ -97,7 +97,7 @@ fn success_message_colors_edit_dir_and_commit_command_when_enabled() {
|
||||
assert!(rendered.contains("\u{1b}[34m/tmp/edit-dir\u{1b}[39m"), "{rendered:?}");
|
||||
assert!(
|
||||
rendered.contains(&format!(
|
||||
"\u{1b}[32mpacquet patch-commit {quote}/tmp/edit-dir{quote}\u{1b}[39m",
|
||||
"\u{1b}[32mpnpm patch-commit {quote}/tmp/edit-dir{quote}\u{1b}[39m",
|
||||
)),
|
||||
"{rendered:?}",
|
||||
);
|
||||
@@ -112,7 +112,7 @@ fn success_message_is_plain_when_colors_are_disabled() {
|
||||
assert_eq!(
|
||||
rendered,
|
||||
format!(
|
||||
"Patch: You can now edit the package at:\n\n /tmp/edit-dir\n\nTo commit your changes, run:\n\n pacquet patch-commit {quote}/tmp/edit-dir{quote}\n\n",
|
||||
"Patch: You can now edit the package at:\n\n /tmp/edit-dir\n\nTo commit your changes, run:\n\n pnpm patch-commit {quote}/tmp/edit-dir{quote}\n\n",
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -123,7 +123,7 @@ fn success_message_shell_quotes_single_quotes_in_edit_dir() {
|
||||
let edit_dir = Path::new("/tmp/patch user's dir");
|
||||
let rendered = render_success(edit_dir, false);
|
||||
|
||||
assert!(rendered.contains(r"pacquet patch-commit '/tmp/patch user'\''s dir'"), "{rendered}");
|
||||
assert!(rendered.contains(r"pnpm patch-commit '/tmp/patch user'\''s dir'"), "{rendered}");
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
|
||||
@@ -10,6 +10,7 @@ use config_overrides::ConfigOverrides;
|
||||
use miette::set_panic_hook;
|
||||
use pacquet_diagnostics::enable_tracing_by_env;
|
||||
use state::State;
|
||||
use std::{ffi::OsString, path::Path};
|
||||
|
||||
pub fn main() -> miette::Result<()> {
|
||||
enable_tracing_by_env();
|
||||
@@ -19,7 +20,7 @@ pub fn main() -> miette::Result<()> {
|
||||
// arbitrary, so a `--config.registry=...` from pnpm's forwarded flags
|
||||
// would otherwise error out as "unexpected argument". Each extracted
|
||||
// token is layered onto `Config` after `.npmrc` / yaml run.
|
||||
let (config_overrides, argv) = ConfigOverrides::extract(std::env::args_os());
|
||||
let (config_overrides, argv) = ConfigOverrides::extract(argv_with_alias_subcommand());
|
||||
// The default reporter's `Done in ... using pacquet v<version>` footer needs
|
||||
// the version before the first event (including the fast path's).
|
||||
pacquet_default_reporter::set_package_version(pacquet_config::PACQUET_VERSION);
|
||||
@@ -67,6 +68,27 @@ pub fn main() -> miette::Result<()> {
|
||||
/// same room on every platform, including Windows (1 MiB default).
|
||||
const MAIN_STACK_SIZE: usize = 32 * 1024 * 1024;
|
||||
|
||||
/// Process argv with a leading `dlx` injected when launched as `pnpx`/`pnx`
|
||||
/// (shorthand for `pnpm dlx`), mirroring pnpm's `buildArgv`. Only the Windows
|
||||
/// hardlink aliases rely on this — the Unix alias scripts inject `dlx`
|
||||
/// themselves — and there `current_exe` is the only signal of the launch name.
|
||||
fn argv_with_alias_subcommand() -> Vec<OsString> {
|
||||
let exe = std::env::current_exe().ok();
|
||||
let exe_name =
|
||||
exe.as_deref().and_then(Path::file_stem).map(|stem| stem.to_string_lossy().to_lowercase());
|
||||
inject_alias_subcommand(exe_name.as_deref(), std::env::args_os().collect())
|
||||
}
|
||||
|
||||
/// Insert a leading `dlx` token after the program name when `exe_name` is a
|
||||
/// `pnpx`/`pnx` alias. Split out from [`argv_with_alias_subcommand`] so the
|
||||
/// argv rewrite is unit-testable without depending on `current_exe`.
|
||||
fn inject_alias_subcommand(exe_name: Option<&str>, mut argv: Vec<OsString>) -> Vec<OsString> {
|
||||
if matches!(exe_name, Some("pnpx" | "pnx")) {
|
||||
argv.insert(argv.len().min(1), OsString::from("dlx"));
|
||||
}
|
||||
argv
|
||||
}
|
||||
|
||||
/// Size rayon's global pool at `2 × available_parallelism`. The link
|
||||
/// phase is dominated by clonefile / hardlink syscalls that block the
|
||||
/// calling thread on the kernel's metadata journal, not by CPU work,
|
||||
@@ -123,3 +145,6 @@ fn configure_rayon_pool() {
|
||||
.max(4);
|
||||
let _ = rayon::ThreadPoolBuilder::new().num_threads(n).build_global();
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
28
pacquet/crates/cli/src/tests.rs
Normal file
28
pacquet/crates/cli/src/tests.rs
Normal file
@@ -0,0 +1,28 @@
|
||||
use super::inject_alias_subcommand;
|
||||
use std::ffi::OsString;
|
||||
|
||||
fn argv(parts: &[&str]) -> Vec<OsString> {
|
||||
parts.iter().map(OsString::from).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pnpx_and_pnx_inject_dlx_after_the_program_name() {
|
||||
for alias in ["pnpx", "pnx"] {
|
||||
let result = inject_alias_subcommand(Some(alias), argv(&[alias, "create-vite", "app"]));
|
||||
assert_eq!(result, argv(&[alias, "dlx", "create-vite", "app"]));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pnpm_pn_and_pacquet_names_are_left_untouched() {
|
||||
for name in ["pnpm", "pn", "pacquet", "PNPX-not-exact"] {
|
||||
let original = argv(&[name, "install"]);
|
||||
assert_eq!(inject_alias_subcommand(Some(name), original.clone()), original);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_unknown_executable_name_is_left_untouched() {
|
||||
let original = argv(&["whatever", "install"]);
|
||||
assert_eq!(inject_alias_subcommand(None, original.clone()), original);
|
||||
}
|
||||
27
pacquet/crates/cli/tests/pnpx_alias.rs
Normal file
27
pacquet/crates/cli/tests/pnpx_alias.rs
Normal file
@@ -0,0 +1,27 @@
|
||||
//! Covers the `current_exe`-based `dlx` injection (`argv_with_alias_subcommand`)
|
||||
//! that pnpm relies on for the Windows `pnpx`/`pnx` hardlinks, exercised on Unix
|
||||
//! by copying the binary under the alias name — the same code runs everywhere.
|
||||
#![cfg(unix)]
|
||||
|
||||
use std::{fs, os::unix::fs::PermissionsExt, process::Command};
|
||||
|
||||
use tempfile::TempDir;
|
||||
|
||||
#[test]
|
||||
fn launched_as_pnpx_injects_the_dlx_subcommand() {
|
||||
let pacquet = env!("CARGO_BIN_EXE_pacquet");
|
||||
|
||||
let dir = TempDir::new().expect("create temp dir");
|
||||
let pnpx = dir.path().join("pnpx");
|
||||
fs::copy(pacquet, &pnpx).expect("copy the binary under the pnpx name");
|
||||
fs::set_permissions(&pnpx, fs::Permissions::from_mode(0o755)).expect("make pnpx executable");
|
||||
|
||||
let via_pnpx = Command::new(&pnpx).arg("--help").output().expect("run `pnpx --help`");
|
||||
let via_dlx = Command::new(pacquet).args(["dlx", "--help"]).output().expect("run `dlx --help`");
|
||||
|
||||
assert!(via_pnpx.status.success(), "`pnpx --help` exited with a failure status");
|
||||
assert!(via_dlx.status.success(), "`dlx --help` (the control) exited with a failure status");
|
||||
let pnpx_help = String::from_utf8(via_pnpx.stdout).expect("pnpx help is UTF-8");
|
||||
let dlx_help = String::from_utf8(via_dlx.stdout).expect("dlx help is UTF-8");
|
||||
assert_eq!(pnpx_help, dlx_help, "`pnpx --help` should equal `dlx --help` (dlx was injected)");
|
||||
}
|
||||
@@ -273,11 +273,13 @@ pub fn default_fetch_retry_maxtimeout() -> u64 {
|
||||
60_000
|
||||
}
|
||||
|
||||
/// pacquet's user-facing release version — the same value
|
||||
/// `pacquet --version` prints. Single source of truth so the CLI
|
||||
/// The CLI's user-facing release version — the same value
|
||||
/// `pnpm --version` prints. Single source of truth so the CLI
|
||||
/// version string and the default `User-Agent` (`default_user_agent`)
|
||||
/// can't drift apart.
|
||||
pub const PACQUET_VERSION: &str = "0.2.2";
|
||||
/// can't drift apart. The release workflow patches this constant to the
|
||||
/// version being published; the committed value tracks the current
|
||||
/// pre-release line.
|
||||
pub const PACQUET_VERSION: &str = "12.0.0-alpha.0";
|
||||
|
||||
pub fn default_fetch_timeout() -> u64 {
|
||||
pacquet_network::DEFAULT_FETCH_TIMEOUT_MS
|
||||
@@ -286,14 +288,14 @@ pub fn default_fetch_timeout() -> u64 {
|
||||
/// Default `User-Agent`, mirroring pnpm v11's
|
||||
/// [`config/reader/src/index.ts:293`](https://github.com/pnpm/pnpm/blob/1819226b51/config/reader/src/index.ts#L293)
|
||||
/// format `${name}/${version} npm/? node/${nodeVersion} ${platform} ${arch}`.
|
||||
/// The `name/version` segment is `pnpm/pacquet-<version>` so registries can
|
||||
/// tell pacquet's traffic apart from the TypeScript pnpm CLI. pacquet has no
|
||||
/// embedded Node runtime, so the `node/` segment is the `?` placeholder pnpm
|
||||
/// already uses for `npm/`. Platform and arch use Node's naming via
|
||||
/// The `name/version` segment is `pnpm/<version>`, matching the TypeScript
|
||||
/// pnpm CLI exactly. There is no embedded Node runtime, so the `node/`
|
||||
/// segment is the `?` placeholder pnpm already uses for `npm/`. Platform and
|
||||
/// arch use Node's naming via
|
||||
/// [`pacquet_detect_libc::host_platform`] / [`pacquet_detect_libc::host_arch`].
|
||||
pub fn default_user_agent() -> String {
|
||||
format!(
|
||||
"pnpm/pacquet-{PACQUET_VERSION} npm/? node/? {} {}",
|
||||
"pnpm/{PACQUET_VERSION} npm/? node/? {} {}",
|
||||
pacquet_detect_libc::host_platform(),
|
||||
pacquet_detect_libc::host_arch(),
|
||||
)
|
||||
|
||||
@@ -360,12 +360,12 @@ fn fetch_timeout_default_matches_pnpm() {
|
||||
}
|
||||
|
||||
/// The exact platform / arch depend on where the test runs, so assert
|
||||
/// the `pnpm/pacquet-<version> npm/? node/? ` prefix and the two
|
||||
/// trailing space-separated tokens rather than the full string.
|
||||
/// the `pnpm/<version> npm/? node/? ` prefix and the two trailing
|
||||
/// space-separated tokens rather than the full string.
|
||||
#[test]
|
||||
fn user_agent_default_matches_pnpm_format() {
|
||||
let ua = default_user_agent();
|
||||
let prefix = format!("pnpm/pacquet-{PACQUET_VERSION} npm/? node/? ");
|
||||
let prefix = format!("pnpm/{PACQUET_VERSION} npm/? node/? ");
|
||||
assert!(ua.starts_with(&prefix), "user-agent {ua:?} must start with {prefix:?}");
|
||||
let tail: Vec<&str> = ua[prefix.len()..].split(' ').collect();
|
||||
assert_eq!(tail.len(), 2, "expected `<platform> <arch>` tail, got {ua:?}");
|
||||
|
||||
@@ -1029,7 +1029,7 @@ pub struct Config {
|
||||
|
||||
/// Value of the `User-Agent` header sent on every registry request.
|
||||
/// Mirrors pnpm's `userAgent`; the default is pnpm's
|
||||
/// `pnpm/pacquet-<version> npm/? node/? <platform> <arch>` format (built by
|
||||
/// `pnpm/<version> npm/? node/? <platform> <arch>` format (built by
|
||||
/// `default_user_agent`).
|
||||
#[default(_code = "default_user_agent()")]
|
||||
pub user_agent: String,
|
||||
|
||||
@@ -42,7 +42,7 @@ pub fn set_cwd(cwd: impl Into<String>) {
|
||||
let _ = CWD.set(cwd.into());
|
||||
}
|
||||
|
||||
/// Set the version rendered in the `Done in ... using pacquet v<version>`
|
||||
/// Set the version rendered in the `Done in ... using pnpm v<version>`
|
||||
/// footer. Call once before the first event; ignored if already set.
|
||||
pub fn set_package_version(version: impl Into<String>) {
|
||||
let _ = PACKAGE_VERSION.set(version.into());
|
||||
|
||||
@@ -960,7 +960,7 @@ impl ReporterState {
|
||||
fn on_execution_time(&mut self, log: &ExecutionTimeLog) {
|
||||
let elapsed = log.ended_at.saturating_sub(log.started_at);
|
||||
let msg =
|
||||
format!("Done in {} using pacquet v{}", pretty_ms(elapsed), crate::package_version());
|
||||
format!("Done in {} using pnpm v{}", pretty_ms(elapsed), crate::package_version());
|
||||
let mut slot = std::mem::take(&mut self.exec_slot);
|
||||
self.frame.emit(&mut slot, msg, true);
|
||||
self.exec_slot = slot;
|
||||
|
||||
@@ -206,7 +206,7 @@ fn execution_time_renders_done_footer() {
|
||||
ended_at: 3500,
|
||||
})],
|
||||
);
|
||||
assert!(frame.starts_with("Done in 2.5s using pacquet v"), "got: {frame}");
|
||||
assert!(frame.starts_with("Done in 2.5s using pnpm v"), "got: {frame}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -250,7 +250,7 @@ fn full_install_frame_orders_blocks_like_pnpm() {
|
||||
frame,
|
||||
"Packages: +1\n+\n\ndependencies:\n+ foo 1.0.0\n\n\
|
||||
Progress: resolved 1, reused 1, downloaded 0, added 1, done\n\
|
||||
Done in 1.2s using pacquet v0.0.1",
|
||||
Done in 1.2s using pnpm v0.0.1",
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -30,10 +30,10 @@ use std::{collections::HashMap, num::NonZeroUsize, ops::Deref, sync::Arc, time::
|
||||
///
|
||||
/// Production installs override this with the value resolved by
|
||||
/// `pacquet-config` (`userAgent`, defaulting to pnpm's
|
||||
/// `pnpm/pacquet-<version> npm/? node/? <platform> <arch>` format — see
|
||||
/// `config/reader/src/index.ts`). The `pnpm` token is preserved in
|
||||
/// that default so any UA-keyed allow / rate-limit rule that lets pnpm
|
||||
/// through also lets pacquet through.
|
||||
/// `pnpm/<version> npm/? node/? <platform> <arch>` format — see
|
||||
/// `config/reader/src/index.ts`). The leading `pnpm` token matches the
|
||||
/// TypeScript CLI exactly, so any UA-keyed allow / rate-limit rule that
|
||||
/// lets pnpm through also lets this build through.
|
||||
///
|
||||
/// A default `reqwest::Client` sends *no* User-Agent at all, which
|
||||
/// some registry CDNs and corporate WAFs treat as a bot signature and
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
# pacquet
|
||||
|
||||
> **pacquet is under active development and not yet ready for production use.** See the [project roadmap](https://github.com/pnpm/pacquet/issues/299).
|
||||
|
||||
The official pnpm rewrite in Rust.
|
||||
|
||||
pacquet is a port of the [pnpm](https://github.com/pnpm/pnpm) CLI from TypeScript to Rust. It is not a new package manager and not a reimagining of pnpm. Its behavior, flags, defaults, error codes, file formats, and directory layout will match pnpm exactly.
|
||||
|
||||
## Installation
|
||||
|
||||
```sh
|
||||
pnpm add -g pacquet
|
||||
```
|
||||
|
||||
This package is a thin Node.js wrapper that dispatches to a platform-specific native binary for your operating system and architecture.
|
||||
|
||||
Prebuilt binaries are available for `linux-x64`, `linux-arm64`, `linux-x64-musl`, `linux-arm64-musl`, `darwin-x64`, `darwin-arm64`, `win32-x64`, and `win32-arm64`.
|
||||
@@ -1,4 +0,0 @@
|
||||
This is a placeholder. pacquet's native binary replaces this file during
|
||||
installation (see ../install.js). If you are reading this, the install/build
|
||||
script did not run — reinstall with build scripts enabled (e.g. allow-list
|
||||
pacquet's build under pnpm or Bun, or drop --ignore-scripts).
|
||||
@@ -1,148 +0,0 @@
|
||||
// Code copied from [Rome](https://github.com/rome/tools/blob/392d188a49/npm/rome/scripts/generate-packages.mjs)
|
||||
|
||||
import { resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import * as fs from "node:fs";
|
||||
|
||||
const BIN_NAME = "pacquet";
|
||||
// The wrapper package is published under two names: the original
|
||||
// `pacquet` (kept for back-compat) and `@pnpm/pacquet` (the official
|
||||
// pnpm-scoped alias). Both ship the same placeholder bin + preinstall
|
||||
// relinker and depend on the same `@pacquet/<target>` binary sub-packages.
|
||||
const SCOPED_ALIAS_NAME = "@pnpm/pacquet";
|
||||
const SCOPED_ALIAS_DIR = "pnpm-pacquet";
|
||||
const PACQUET_ROOT = resolve(fileURLToPath(import.meta.url), "../..");
|
||||
const PACKAGES_ROOT = resolve(PACQUET_ROOT, "..");
|
||||
const REPO_ROOT = resolve(PACKAGES_ROOT, "../..");
|
||||
const MANIFEST_PATH = resolve(PACQUET_ROOT, "package.json");
|
||||
|
||||
const rootManifest = JSON.parse(
|
||||
fs.readFileSync(MANIFEST_PATH, "utf-8")
|
||||
);
|
||||
|
||||
function generateNativePackage(target) {
|
||||
const packageName = nativePackageName(target);
|
||||
const packageRoot = resolve(PACKAGES_ROOT, `${BIN_NAME}-${target.codeTarget}`);
|
||||
|
||||
// Remove the directory just in case it already exists (it's autogenerated
|
||||
// so there shouldn't be anything important there anyway)
|
||||
fs.rmSync(packageRoot, { recursive: true, force: true });
|
||||
|
||||
// Create the package directory
|
||||
console.log(`Create directory ${packageRoot}`);
|
||||
fs.mkdirSync(packageRoot);
|
||||
|
||||
// Generate the package.json manifest
|
||||
const { version } = rootManifest;
|
||||
|
||||
// publishConfig.executableFiles tells pnpm pack to keep mode 0755
|
||||
// on the binary in the published tarball. Without it, pack normalizes
|
||||
// every non-bin file to 0644 and the preinstall hard-link inherits a
|
||||
// non-executable inode, so the relinked `pacquet` fails with EACCES.
|
||||
const ext = target.platform === "win32" ? ".exe" : "";
|
||||
const manifestData = {
|
||||
name: packageName,
|
||||
version,
|
||||
os: [target.platform],
|
||||
cpu: [target.arch],
|
||||
repository: {
|
||||
type: "git",
|
||||
url: "https://github.com/pnpm/pnpm",
|
||||
},
|
||||
publishConfig: {
|
||||
executableFiles: [`./${BIN_NAME}${ext}`],
|
||||
},
|
||||
};
|
||||
if (target.libc) {
|
||||
manifestData.libc = [target.libc];
|
||||
}
|
||||
const manifest = JSON.stringify(manifestData, null, 2);
|
||||
|
||||
const manifestPath = resolve(packageRoot, "package.json");
|
||||
console.log(`Create manifest ${manifestPath}`);
|
||||
fs.writeFileSync(manifestPath, manifest);
|
||||
|
||||
// Copy the binary
|
||||
const binarySource = resolve(REPO_ROOT, `${BIN_NAME}-${target.codeTarget}${ext}`);
|
||||
const binaryTarget = resolve(packageRoot, `${BIN_NAME}${ext}`);
|
||||
|
||||
console.log(`Copy binary ${binaryTarget}`);
|
||||
fs.copyFileSync(binarySource, binaryTarget);
|
||||
fs.chmodSync(binaryTarget, 0o755);
|
||||
}
|
||||
|
||||
function writeManifest() {
|
||||
const manifestPath = resolve(PACKAGES_ROOT, BIN_NAME, "package.json");
|
||||
|
||||
const manifestData = JSON.parse(
|
||||
fs.readFileSync(manifestPath, "utf-8")
|
||||
);
|
||||
|
||||
const nativePackages = TARGETS.map((target) => [
|
||||
nativePackageName(target),
|
||||
rootManifest.version,
|
||||
]);
|
||||
|
||||
manifestData["version"] = rootManifest.version;
|
||||
manifestData["optionalDependencies"] = Object.fromEntries(nativePackages);
|
||||
|
||||
console.log(`Update manifest ${manifestPath}`);
|
||||
const content = JSON.stringify(manifestData);
|
||||
fs.writeFileSync(manifestPath, content);
|
||||
}
|
||||
|
||||
function generateScopedAliasPackage() {
|
||||
const aliasRoot = resolve(PACKAGES_ROOT, SCOPED_ALIAS_DIR);
|
||||
fs.rmSync(aliasRoot, { recursive: true, force: true });
|
||||
fs.mkdirSync(resolve(aliasRoot, "bin"), { recursive: true });
|
||||
|
||||
// Mirror the placeholder bin and the preinstall relinker 1:1. Copying instead
|
||||
// of symlinking keeps the tarball self-contained for `pnpm publish`.
|
||||
fs.copyFileSync(
|
||||
resolve(PACQUET_ROOT, "bin", BIN_NAME),
|
||||
resolve(aliasRoot, "bin", BIN_NAME),
|
||||
);
|
||||
fs.copyFileSync(
|
||||
resolve(PACQUET_ROOT, "install.js"),
|
||||
resolve(aliasRoot, "install.js"),
|
||||
);
|
||||
|
||||
// The pacquet manifest at this point already carries the version and
|
||||
// optionalDependencies that `writeManifest` patched in. Reuse it and
|
||||
// swap only the package name + repo.directory pointer.
|
||||
const baseManifest = JSON.parse(fs.readFileSync(MANIFEST_PATH, "utf-8"));
|
||||
const aliasManifest = {
|
||||
...baseManifest,
|
||||
name: SCOPED_ALIAS_NAME,
|
||||
repository: { ...baseManifest.repository, directory: `pacquet/npm/${SCOPED_ALIAS_DIR}` },
|
||||
};
|
||||
fs.writeFileSync(
|
||||
resolve(aliasRoot, "package.json"),
|
||||
JSON.stringify(aliasManifest),
|
||||
);
|
||||
}
|
||||
|
||||
function nativePackageName(target) {
|
||||
return `@${BIN_NAME}/${target.packageTarget}`;
|
||||
}
|
||||
|
||||
const TARGETS = [
|
||||
{ platform: "win32", arch: "x64", codeTarget: "win32-x64", packageTarget: "win32-x64" },
|
||||
{ platform: "win32", arch: "arm64", codeTarget: "win32-arm64", packageTarget: "win32-arm64" },
|
||||
{ platform: "darwin", arch: "x64", codeTarget: "darwin-x64", packageTarget: "darwin-x64" },
|
||||
{ platform: "darwin", arch: "arm64", codeTarget: "darwin-arm64", packageTarget: "darwin-arm64" },
|
||||
{ platform: "linux", arch: "x64", libc: "glibc", codeTarget: "linux-x64", packageTarget: "linux-x64" },
|
||||
{ platform: "linux", arch: "arm64", libc: "glibc", codeTarget: "linux-arm64", packageTarget: "linux-arm64" },
|
||||
{ platform: "linux", arch: "x64", libc: "musl", codeTarget: "linux-x64-musl", packageTarget: "linux-x64-musl" },
|
||||
{ platform: "linux", arch: "arm64", libc: "musl", codeTarget: "linux-arm64-musl", packageTarget: "linux-arm64-musl" },
|
||||
];
|
||||
|
||||
for (const target of TARGETS) {
|
||||
generateNativePackage(target);
|
||||
}
|
||||
|
||||
writeManifest();
|
||||
// Must run after `writeManifest`: the alias mirrors the patched
|
||||
// pacquet manifest (version + optionalDependencies), so reading it
|
||||
// before the patch would copy stale values.
|
||||
generateScopedAliasPackage();
|
||||
272
pacquet/npm/pnpm/README.md
Normal file
272
pacquet/npm/pnpm/README.md
Normal file
@@ -0,0 +1,272 @@
|
||||
[简体中文](https://pnpm.io/zh/) |
|
||||
[日本語](https://pnpm.io/ja/) |
|
||||
[한국어](https://pnpm.io/ko/) |
|
||||
[Italiano](https://pnpm.io/it/) |
|
||||
[Português Brasileiro](https://pnpm.io/pt/)
|
||||
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://i.imgur.com/qlW1eEG.png">
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://i.imgur.com/qlW1eEG.png">
|
||||
<img src="https://i.imgur.com/qlW1eEG.png" alt="pnpm">
|
||||
</picture>
|
||||
|
||||
Fast, disk space efficient package manager:
|
||||
|
||||
* **Fast.** Up to 2x faster than the alternatives (see [benchmark](#benchmark)).
|
||||
* **Efficient.** Files inside `node_modules` are linked from a single content-addressable storage.
|
||||
* **[Great for monorepos](https://pnpm.io/workspaces).**
|
||||
* **Strict.** A package can access only dependencies that are specified in its `package.json`.
|
||||
* **Deterministic.** Has a lockfile called `pnpm-lock.yaml`.
|
||||
* **Works as a Node.js version manager.** See [pnpm runtime](https://pnpm.io/11.x/cli/runtime).
|
||||
* **Works everywhere.** Supports Windows, Linux, and macOS.
|
||||
* **Battle-tested.** Used in production by teams of [all sizes](https://pnpm.io/workspaces#usage-examples) since 2016.
|
||||
* [See the full feature comparison with npm and Yarn](https://pnpm.io/feature-comparison).
|
||||
|
||||
To quote the [Rush](https://rushjs.io/) team:
|
||||
|
||||
> Microsoft uses pnpm in Rush repos with hundreds of projects and hundreds of PRs per day, and we’ve found it to be very fast and reliable.
|
||||
|
||||
[](https://github.com/pnpm/pnpm/releases/latest)
|
||||
[](https://opencollective.com/pnpm)
|
||||
[](https://opencollective.com/pnpm)
|
||||
[](https://x.com/intent/follow?screen_name=pnpmjs®ion=follow_link)
|
||||
[](https://stand-with-ukraine.pp.ua)
|
||||
|
||||
<!-- sponsors -->
|
||||
|
||||
## Platinum Sponsors
|
||||
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://bit.cloud/?utm_source=pnpm&utm_medium=readme" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/bit.svg" width="80" alt="Bit"></a>
|
||||
</td>
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://openai.com/?utm_source=pnpm&utm_medium=readme" target="_blank" rel="noopener noreferrer">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/openai_dark.svg" />
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/openai_light.svg" />
|
||||
<img src="https://pnpm.io/img/users/openai_dark.svg" width="160" alt="OpenAI" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
## Gold Sponsors
|
||||
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://sanity.io/?utm_source=pnpm&utm_medium=readme" target="_blank" rel="noopener noreferrer">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/sanity.svg" />
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/sanity_light.svg" />
|
||||
<img src="https://pnpm.io/img/users/sanity.svg" width="120" alt="Sanity" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://discord.com/?utm_source=pnpm&utm_medium=readme" target="_blank" rel="noopener noreferrer">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/discord.svg" />
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/discord_light.svg" />
|
||||
<img src="https://pnpm.io/img/users/discord.svg" width="220" alt="Discord" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://vite.dev/?utm_source=pnpm&utm_medium=readme" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/vitejs.svg" width="42" alt="Vite"></a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://serpapi.com/?utm_source=pnpm&utm_medium=readme" target="_blank" rel="noopener noreferrer">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/serpapi_dark.svg" />
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/serpapi_light.svg" />
|
||||
<img src="https://pnpm.io/img/users/serpapi_dark.svg" width="160" alt="SerpApi" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://coderabbit.ai/?utm_source=pnpm&utm_medium=readme" target="_blank" rel="noopener noreferrer">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/coderabbit.svg" />
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/coderabbit_light.svg" />
|
||||
<img src="https://pnpm.io/img/users/coderabbit.svg" width="220" alt="CodeRabbit" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://stackblitz.com/?utm_source=pnpm&utm_medium=readme" target="_blank" rel="noopener noreferrer">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/stackblitz.svg" />
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/stackblitz_light.svg" />
|
||||
<img src="https://pnpm.io/img/users/stackblitz.svg" width="190" alt="Stackblitz" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://workleap.com/?utm_source=pnpm&utm_medium=readme" target="_blank" rel="noopener noreferrer">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/workleap.svg" />
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/workleap_light.svg" />
|
||||
<img src="https://pnpm.io/img/users/workleap.svg" width="190" alt="Workleap" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://nx.dev/?utm_source=pnpm&utm_medium=readme" target="_blank" rel="noopener noreferrer">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/nx.svg" />
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/nx_light.svg" />
|
||||
<img src="https://pnpm.io/img/users/nx.svg" width="50" alt="Nx" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
## Silver Sponsors
|
||||
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://replit.com/?utm_source=pnpm&utm_medium=readme" target="_blank" rel="noopener noreferrer">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/replit.png" />
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/replit_light.png" />
|
||||
<img src="https://pnpm.io/img/users/replit.png" width="140" alt="Replit" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://cybozu.co.jp/?utm_source=pnpm&utm_medium=readme" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/cybozu.svg" width="70" alt="Cybozu"></a>
|
||||
</td>
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://www.bairesdev.com/?utm_source=pnpm&utm_medium=readme" target="_blank" rel="noopener noreferrer">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/bairesdev.svg" />
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/bairesdev_light.svg" />
|
||||
<img src="https://pnpm.io/img/users/bairesdev.svg" width="160" alt="BairesDev" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://www.thesys.dev/?utm_source=pnpm&utm_medium=readme" target="_blank" rel="noopener noreferrer">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/thesys.svg" />
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/thesys_light.svg" />
|
||||
<img src="https://pnpm.io/img/users/thesys.svg" width="120" alt="Thesys" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://devowl.io/?utm_source=pnpm&utm_medium=readme" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/devowlio.svg" width="100" alt="devowl.io"></a>
|
||||
</td>
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://uscreen.de/?utm_source=pnpm&utm_medium=readme" target="_blank" rel="noopener noreferrer">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/uscreen.svg" />
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/uscreen_light.svg" />
|
||||
<img src="https://pnpm.io/img/users/uscreen.svg" width="180" alt="u|screen" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://www.leniolabs.com/?utm_source=pnpm&utm_medium=readme" target="_blank" rel="noopener noreferrer"><img src="https://pnpm.io/img/users/leniolabs.jpg" width="40" alt="Leniolabs_"></a>
|
||||
</td>
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://depot.dev/?utm_source=pnpm&utm_medium=readme" target="_blank" rel="noopener noreferrer">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/depot.svg" />
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/depot_light.svg" />
|
||||
<img src="https://pnpm.io/img/users/depot.svg" width="100" alt="Depot" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://cerbos.dev/?utm_source=pnpm&utm_medium=readme" target="_blank" rel="noopener noreferrer">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://pnpm.io/img/users/cerbos.svg" />
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://pnpm.io/img/users/cerbos_light.svg" />
|
||||
<img src="https://pnpm.io/img/users/cerbos.svg" width="90" alt="Cerbos" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://time.now/?utm_source=pnpm&utm_medium=readme" target="_blank" rel="noopener noreferrer">⏱️ Time.now</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- sponsors end -->
|
||||
|
||||
Support this project by [becoming a sponsor](https://opencollective.com/pnpm#sponsor).
|
||||
|
||||
## Background
|
||||
|
||||
pnpm uses a content-addressable filesystem to store all files from all module directories on a disk.
|
||||
When using npm, if you have 100 projects using lodash, you will have 100 copies of lodash on disk.
|
||||
With pnpm, lodash will be stored in a content-addressable storage, so:
|
||||
|
||||
1. If you depend on different versions of lodash, only the files that differ are added to the store.
|
||||
If lodash has 100 files, and a new version has a change only in one of those files,
|
||||
`pnpm update` will only add 1 new file to the storage.
|
||||
1. All the files are saved in a single place on the disk. When packages are installed, their files are linked
|
||||
from that single place consuming no additional disk space. Linking is performed using either hard-links or reflinks (copy-on-write).
|
||||
|
||||
As a result, you save gigabytes of space on your disk and you have a lot faster installations!
|
||||
If you'd like more details about the unique `node_modules` structure that pnpm creates and
|
||||
why it works fine with the Node.js ecosystem, read this small article: [Flat node_modules is not the only way](https://pnpm.io/blog/2020/05/27/flat-node-modules-is-not-the-only-way).
|
||||
|
||||
💖 Like this project? Let people know with a [tweet](https://r.pnpm.io/tweet)
|
||||
|
||||
## Installation
|
||||
|
||||
For installation options [visit our website](https://pnpm.io/installation).
|
||||
|
||||
## Usage
|
||||
|
||||
Just use pnpm in place of npm/Yarn. E.g., install dependencies via:
|
||||
|
||||
```
|
||||
pnpm install
|
||||
```
|
||||
|
||||
For more advanced usage, read [pnpm CLI](https://pnpm.io/pnpm-cli) on our website, or run `pnpm help`.
|
||||
|
||||
## Benchmark
|
||||
|
||||
pnpm is up to 2x faster than npm and Yarn classic. See all benchmarks [here](https://r.pnpm.io/benchmarks).
|
||||
|
||||
Benchmarks on an app with lots of dependencies:
|
||||
|
||||

|
||||
|
||||
## Support
|
||||
|
||||
- [Frequently Asked Questions](https://pnpm.io/faq)
|
||||
- [X](https://x.com/pnpmjs)
|
||||
- [Bluesky](https://bsky.app/profile/pnpm.io)
|
||||
- [Discord](https://r.pnpm.io/chat)
|
||||
|
||||
## License
|
||||
|
||||
[MIT](https://github.com/pnpm/pnpm/blob/main/LICENSE)
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
#!/usr/bin/env node
|
||||
// Preinstall: replace the placeholder `bin/pacquet` with the platform's native
|
||||
// binary, so the command runs the binary directly instead of paying Node.js
|
||||
// startup on every call. Mirrors how `@pnpm/exe` ships pnpm.
|
||||
// Preinstall for the pnpm v12 wrapper (shared verbatim by `pnpm` and
|
||||
// `@pnpm/exe`): replace the shebang-less placeholder bins with the host's native
|
||||
// binary so `pnpm` runs directly, no Node startup per call. A placeholder (not a
|
||||
// Node launcher) is required because the Windows shim is generated from the bin
|
||||
// file and npm won't re-read package.json after preinstall; the tradeoff is no
|
||||
// fallback when build scripts are blocked (`--ignore-scripts`, pnpm/Bun default).
|
||||
//
|
||||
// The published `bin/pacquet` is a shebang-less placeholder: the Windows `.bin`
|
||||
// shim is generated from the bin file, so a Node launcher there would bake in a
|
||||
// `node bin/pacquet` call this script cannot rewrite (npm does not re-read
|
||||
// package.json after preinstall). The cost is that there is no fallback — when
|
||||
// build scripts are blocked (`--ignore-scripts`, pnpm/Bun defaults) the
|
||||
// placeholder stays until the build is allow-listed.
|
||||
// `pn`/`pnpx`/`pnx` are committed `#!/bin/sh` scripts on Unix (so only `pnpm` is
|
||||
// relinked); on Windows the native binary is hardlinked onto each and
|
||||
// self-detects its launch name to inject `dlx` (see `argv_with_alias_subcommand`
|
||||
// in the cli crate).
|
||||
import console from 'node:console'
|
||||
import fs from 'node:fs'
|
||||
import { createRequire } from 'node:module'
|
||||
@@ -22,31 +23,33 @@ const { platform, arch } = process
|
||||
|
||||
const PLATFORMS = {
|
||||
win32: {
|
||||
x64: '@pacquet/win32-x64/pacquet.exe',
|
||||
arm64: '@pacquet/win32-arm64/pacquet.exe',
|
||||
x64: '@pnpm/exe.win32-x64/pnpm.exe',
|
||||
arm64: '@pnpm/exe.win32-arm64/pnpm.exe',
|
||||
},
|
||||
darwin: {
|
||||
x64: '@pacquet/darwin-x64/pacquet',
|
||||
arm64: '@pacquet/darwin-arm64/pacquet',
|
||||
x64: '@pnpm/exe.darwin-x64/pnpm',
|
||||
arm64: '@pnpm/exe.darwin-arm64/pnpm',
|
||||
},
|
||||
linux: {
|
||||
x64: {
|
||||
glibc: '@pacquet/linux-x64/pacquet',
|
||||
musl: '@pacquet/linux-x64-musl/pacquet',
|
||||
glibc: '@pnpm/exe.linux-x64/pnpm',
|
||||
musl: '@pnpm/exe.linux-x64-musl/pnpm',
|
||||
},
|
||||
arm64: {
|
||||
glibc: '@pacquet/linux-arm64/pacquet',
|
||||
musl: '@pacquet/linux-arm64-musl/pacquet',
|
||||
glibc: '@pnpm/exe.linux-arm64/pnpm',
|
||||
musl: '@pnpm/exe.linux-arm64-musl/pnpm',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const BIN_NAMES = ['pnpm', 'pn', 'pnpx', 'pnx']
|
||||
|
||||
setup()
|
||||
|
||||
function setup () {
|
||||
const candidates = getBinCandidates()
|
||||
if (candidates.length === 0) {
|
||||
fail(`pacquet does not ship a prebuilt binary for ${platform}-${arch}.`)
|
||||
fail(`pnpm does not ship a prebuilt binary for ${platform}-${arch}.`)
|
||||
}
|
||||
|
||||
// Use whichever platform package the package manager installed: it already
|
||||
@@ -56,35 +59,36 @@ function setup () {
|
||||
try {
|
||||
nativeBinary = require.resolve(target)
|
||||
break
|
||||
} catch {
|
||||
// Not installed for this host; try the next candidate.
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
if (nativeBinary == null) {
|
||||
const pkgName = candidates[0].split('/').slice(0, 2).join('/')
|
||||
fail(
|
||||
`The "${pkgName}" package is not installed, so pacquet has no native binary to run.\n` +
|
||||
`The "${pkgName}" package is not installed, so pnpm has no native binary to run.\n` +
|
||||
'If your package manager skipped optional dependencies or blocked build scripts, ' +
|
||||
'enable them and reinstall.'
|
||||
)
|
||||
}
|
||||
|
||||
const binDir = path.join(ownDir, 'bin')
|
||||
if (platform === 'win32') {
|
||||
// The existing shim points at `bin/pacquet`, so that file must become the
|
||||
// binary; the `.exe` twin and `bin` rewrite are for shims generated later.
|
||||
placeBinary(nativeBinary, path.join(binDir, 'pacquet.exe'))
|
||||
placeBinary(nativeBinary, path.join(binDir, 'pacquet'))
|
||||
rewriteBin('bin/pacquet.exe')
|
||||
const newBin = {}
|
||||
for (const name of BIN_NAMES) {
|
||||
// The existing shim points at the no-ext file, so it must become the
|
||||
// binary; the `.exe` twin + bin rewrite are for shims generated later.
|
||||
placeBinary(nativeBinary, path.join(ownDir, `${name}.exe`))
|
||||
placeBinary(nativeBinary, path.join(ownDir, name))
|
||||
newBin[name] = `${name}.exe`
|
||||
}
|
||||
rewriteBin(newBin)
|
||||
} else {
|
||||
placeBinary(nativeBinary, path.join(binDir, 'pacquet'), 0o755)
|
||||
placeBinary(nativeBinary, path.join(ownDir, 'pnpm'), 0o755)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Atomically place `nativeBinary` at `destPath` (hard link, falling back to a
|
||||
* copy across filesystems, via a temp file + rename). Exits the process on
|
||||
* failure — without the binary there is no working `pacquet`.
|
||||
* failure — without the binary there is no working `pnpm`.
|
||||
*
|
||||
* @param {string} nativeBinary Absolute path to the resolved native binary.
|
||||
* @param {string} destPath Absolute path to create.
|
||||
@@ -92,7 +96,7 @@ function setup () {
|
||||
* source inode (the shared store blob under pnpm), so its mode must not change.
|
||||
*/
|
||||
function placeBinary (nativeBinary, destPath, mode) {
|
||||
const tempPath = `${destPath}.pacquet-tmp`
|
||||
const tempPath = `${destPath}.pnpm-tmp`
|
||||
try {
|
||||
fs.rmSync(tempPath, { force: true })
|
||||
let linked = false
|
||||
@@ -109,31 +113,24 @@ function placeBinary (nativeBinary, destPath, mode) {
|
||||
} catch (err) {
|
||||
try {
|
||||
fs.rmSync(tempPath, { force: true })
|
||||
} catch {
|
||||
// Nothing to clean up.
|
||||
}
|
||||
fail(`Could not install the pacquet binary at ${destPath}: ${err.message}`)
|
||||
} catch {}
|
||||
fail(`Could not install the pnpm binary at ${destPath}: ${err.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
function rewriteBin (binValue) {
|
||||
function rewriteBin (binMap) {
|
||||
const pkgJsonPath = path.join(ownDir, 'package.json')
|
||||
// Write a fresh file and rename it over package.json rather than truncating in
|
||||
// place: pnpm hard-links package.json from its content-addressable store, so an
|
||||
// in-place write would mutate the shared store blob. Best-effort — it only
|
||||
// helps shims generated later.
|
||||
const tempPath = `${pkgJsonPath}.pacquet-tmp`
|
||||
// Temp file + rename, not in-place: package.json is hard-linked from the store.
|
||||
const tempPath = `${pkgJsonPath}.pnpm-tmp`
|
||||
try {
|
||||
const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8'))
|
||||
pkg.bin = binValue
|
||||
pkg.bin = binMap
|
||||
fs.writeFileSync(tempPath, JSON.stringify(pkg, null, 2))
|
||||
fs.renameSync(tempPath, pkgJsonPath)
|
||||
} catch {
|
||||
try {
|
||||
fs.rmSync(tempPath, { force: true })
|
||||
} catch {
|
||||
// Nothing to clean up.
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,25 @@
|
||||
{
|
||||
"name": "pacquet",
|
||||
"description": "The official pnpm rewrite in Rust (preview, not production-ready)",
|
||||
"name": "pnpm",
|
||||
"description": "Fast, disk space efficient package manager — the Rust port of pnpm (alpha, not production-ready)",
|
||||
"keywords": [
|
||||
"pnpm"
|
||||
"pnpm",
|
||||
"package manager",
|
||||
"rust"
|
||||
],
|
||||
"author": {
|
||||
"name": "Yagiz Nizipli",
|
||||
"email": "yagiz@nizipli.com"
|
||||
},
|
||||
"license": "MIT",
|
||||
"homepage": "https://github.com/pnpm/pnpm/tree/main/pacquet",
|
||||
"bugs": "https://github.com/pnpm/pnpm/issues",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/pnpm/pnpm",
|
||||
"directory": "pacquet/npm/pacquet"
|
||||
"directory": "pacquet/npm/pnpm"
|
||||
},
|
||||
"bin": {
|
||||
"pnpm": "pnpm",
|
||||
"pn": "pn",
|
||||
"pnpx": "pnpx",
|
||||
"pnx": "pnx"
|
||||
},
|
||||
"bin": "bin/pacquet",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"preinstall": "node install.js"
|
||||
@@ -25,7 +28,10 @@
|
||||
"node": ">=18.*"
|
||||
},
|
||||
"files": [
|
||||
"bin/pacquet",
|
||||
"pnpm",
|
||||
"pn",
|
||||
"pnpx",
|
||||
"pnx",
|
||||
"install.js"
|
||||
]
|
||||
}
|
||||
2
pacquet/npm/pnpm/pn
Executable file
2
pacquet/npm/pnpm/pn
Executable file
@@ -0,0 +1,2 @@
|
||||
#!/bin/sh
|
||||
exec pnpm "$@"
|
||||
4
pacquet/npm/pnpm/pnpm
Normal file
4
pacquet/npm/pnpm/pnpm
Normal file
@@ -0,0 +1,4 @@
|
||||
This is a placeholder. pnpm's native binary replaces this file during
|
||||
installation (see ./install.js). If you are reading this, the install/build
|
||||
script did not run — reinstall with build scripts enabled (e.g. allow-list
|
||||
pnpm's build under pnpm or Bun, or drop --ignore-scripts).
|
||||
2
pacquet/npm/pnpm/pnpx
Executable file
2
pacquet/npm/pnpm/pnpx
Executable file
@@ -0,0 +1,2 @@
|
||||
#!/bin/sh
|
||||
exec pnpm dlx "$@"
|
||||
2
pacquet/npm/pnpm/pnx
Executable file
2
pacquet/npm/pnpm/pnx
Executable file
@@ -0,0 +1,2 @@
|
||||
#!/bin/sh
|
||||
exec pnpm dlx "$@"
|
||||
138
pacquet/npm/pnpm/scripts/generate-packages.mjs
Normal file
138
pacquet/npm/pnpm/scripts/generate-packages.mjs
Normal file
@@ -0,0 +1,138 @@
|
||||
// Adapted from Rome (https://github.com/rome/tools/blob/392d188a49/npm/rome/scripts/generate-packages.mjs).
|
||||
//
|
||||
// Generates the per-platform `@pnpm/exe.<target>` native packages and the
|
||||
// `@pnpm/exe` wrapper (an equal-content copy of the committed `pnpm` wrapper).
|
||||
|
||||
import { resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import * as fs from "node:fs";
|
||||
|
||||
// Cargo bin / archived binary name; the workflow uploads `pacquet-<codeTarget>`.
|
||||
const BIN_NAME = "pacquet";
|
||||
// Binary file name inside each native package; `installPnpm`'s
|
||||
// `linkExePlatformBinary` hardcodes `pnpm`, so both must agree.
|
||||
const NATIVE_BIN_FILE = "pnpm";
|
||||
const EXE_WRAPPER_NAME = "@pnpm/exe";
|
||||
const EXE_WRAPPER_DIR = "pnpm-exe";
|
||||
// Files shared verbatim by both wrappers: the root-level bins + preinstall + README.
|
||||
const WRAPPER_FILES = ["pnpm", "pn", "pnpx", "pnx", "install.js", "README.md"];
|
||||
|
||||
const PNPM_ROOT = resolve(fileURLToPath(import.meta.url), "../..");
|
||||
const PACKAGES_ROOT = resolve(PNPM_ROOT, "..");
|
||||
const REPO_ROOT = resolve(PACKAGES_ROOT, "../..");
|
||||
const MANIFEST_PATH = resolve(PNPM_ROOT, "package.json");
|
||||
|
||||
const rootManifest = JSON.parse(
|
||||
fs.readFileSync(MANIFEST_PATH, "utf-8")
|
||||
);
|
||||
|
||||
function generateNativePackage(target) {
|
||||
const packageName = nativePackageName(target);
|
||||
const packageRoot = resolve(PACKAGES_ROOT, `${BIN_NAME}-${target.codeTarget}`);
|
||||
|
||||
// Remove the directory just in case it already exists (it's autogenerated
|
||||
// so there shouldn't be anything important there anyway)
|
||||
fs.rmSync(packageRoot, { recursive: true, force: true });
|
||||
|
||||
console.log(`Create directory ${packageRoot}`);
|
||||
fs.mkdirSync(packageRoot);
|
||||
|
||||
const { version } = rootManifest;
|
||||
|
||||
// executableFiles keeps the binary at 0755 in the tarball; otherwise pack
|
||||
// normalizes it to 0644 and the preinstall hard-link inherits a non-exec
|
||||
// inode, so the relinked `pnpm` fails with EACCES.
|
||||
const ext = target.platform === "win32" ? ".exe" : "";
|
||||
const manifestData = {
|
||||
name: packageName,
|
||||
version,
|
||||
os: [target.platform],
|
||||
cpu: [target.arch],
|
||||
repository: {
|
||||
type: "git",
|
||||
url: "https://github.com/pnpm/pnpm",
|
||||
},
|
||||
publishConfig: {
|
||||
executableFiles: [`./${NATIVE_BIN_FILE}${ext}`],
|
||||
},
|
||||
};
|
||||
if (target.libc) {
|
||||
manifestData.libc = [target.libc];
|
||||
}
|
||||
const manifest = JSON.stringify(manifestData, null, 2);
|
||||
|
||||
const manifestPath = resolve(packageRoot, "package.json");
|
||||
console.log(`Create manifest ${manifestPath}`);
|
||||
fs.writeFileSync(manifestPath, manifest);
|
||||
|
||||
const binarySource = resolve(REPO_ROOT, `${BIN_NAME}-${target.codeTarget}${ext}`);
|
||||
const binaryTarget = resolve(packageRoot, `${NATIVE_BIN_FILE}${ext}`);
|
||||
|
||||
console.log(`Copy binary ${binaryTarget}`);
|
||||
fs.copyFileSync(binarySource, binaryTarget);
|
||||
fs.chmodSync(binaryTarget, 0o755);
|
||||
}
|
||||
|
||||
// Patch the committed `pnpm` manifest with the release version + the full set of
|
||||
// `@pnpm/exe.<target>` optional dependencies, preserving its other fields.
|
||||
function patchPnpmWrapperManifest() {
|
||||
const nativePackages = TARGETS.map((target) => [
|
||||
nativePackageName(target),
|
||||
rootManifest.version,
|
||||
]);
|
||||
|
||||
rootManifest["version"] = rootManifest.version;
|
||||
rootManifest["optionalDependencies"] = Object.fromEntries(nativePackages);
|
||||
|
||||
console.log(`Update manifest ${MANIFEST_PATH}`);
|
||||
fs.writeFileSync(MANIFEST_PATH, JSON.stringify(rootManifest));
|
||||
}
|
||||
|
||||
// Generate the `@pnpm/exe` wrapper as a copy of the `pnpm` wrapper with only the
|
||||
// name + repo.directory changed. Must run after `patchPnpmWrapperManifest` so the
|
||||
// copied manifest already carries the version and optionalDependencies.
|
||||
function generateExeWrapper() {
|
||||
const exeRoot = resolve(PACKAGES_ROOT, EXE_WRAPPER_DIR);
|
||||
fs.rmSync(exeRoot, { recursive: true, force: true });
|
||||
fs.mkdirSync(exeRoot, { recursive: true });
|
||||
|
||||
// Copy instead of symlinking so the tarball is self-contained for publish.
|
||||
for (const file of WRAPPER_FILES) {
|
||||
fs.copyFileSync(resolve(PNPM_ROOT, file), resolve(exeRoot, file));
|
||||
fs.chmodSync(resolve(exeRoot, file), fs.statSync(resolve(PNPM_ROOT, file)).mode);
|
||||
}
|
||||
|
||||
const baseManifest = JSON.parse(fs.readFileSync(MANIFEST_PATH, "utf-8"));
|
||||
const exeManifest = {
|
||||
...baseManifest,
|
||||
name: EXE_WRAPPER_NAME,
|
||||
repository: { ...baseManifest.repository, directory: `pacquet/npm/${EXE_WRAPPER_DIR}` },
|
||||
};
|
||||
console.log(`Create wrapper ${exeRoot}`);
|
||||
fs.writeFileSync(resolve(exeRoot, "package.json"), JSON.stringify(exeManifest));
|
||||
}
|
||||
|
||||
// `@pnpm/exe.<target>` is the convention `installPnpm`'s relinker already
|
||||
// anticipates (`exePlatformPkgDirNameNext`), so a switch to v12 needs no special
|
||||
// case.
|
||||
function nativePackageName(target) {
|
||||
return `@pnpm/exe.${target.packageTarget}`;
|
||||
}
|
||||
|
||||
const TARGETS = [
|
||||
{ platform: "win32", arch: "x64", codeTarget: "win32-x64", packageTarget: "win32-x64" },
|
||||
{ platform: "win32", arch: "arm64", codeTarget: "win32-arm64", packageTarget: "win32-arm64" },
|
||||
{ platform: "darwin", arch: "x64", codeTarget: "darwin-x64", packageTarget: "darwin-x64" },
|
||||
{ platform: "darwin", arch: "arm64", codeTarget: "darwin-arm64", packageTarget: "darwin-arm64" },
|
||||
{ platform: "linux", arch: "x64", libc: "glibc", codeTarget: "linux-x64", packageTarget: "linux-x64" },
|
||||
{ platform: "linux", arch: "arm64", libc: "glibc", codeTarget: "linux-arm64", packageTarget: "linux-arm64" },
|
||||
{ platform: "linux", arch: "x64", libc: "musl", codeTarget: "linux-x64-musl", packageTarget: "linux-x64-musl" },
|
||||
{ platform: "linux", arch: "arm64", libc: "musl", codeTarget: "linux-arm64-musl", packageTarget: "linux-arm64-musl" },
|
||||
];
|
||||
|
||||
for (const target of TARGETS) {
|
||||
generateNativePackage(target);
|
||||
}
|
||||
|
||||
patchPnpmWrapperManifest();
|
||||
generateExeWrapper();
|
||||
@@ -1,5 +1,5 @@
|
||||
export { selfUpdate } from './self-updater/index.js'
|
||||
export { exePlatformPkgDirName, exePlatformPkgDirNameNext, installPnpm, installPnpmToStore, linkExePlatformBinary } from './self-updater/installPnpm.js'
|
||||
export { exePlatformPkgDirName, exePlatformPkgDirNameNext, installPnpm, installPnpmToStore, linkExePlatformBinary, pnpmPackageNameToInstall } from './self-updater/installPnpm.js'
|
||||
export { verifyPnpmEngineIdentity, type VerifyPnpmEngineIdentityOptions } from './self-updater/verifyPnpmEngineIdentity.js'
|
||||
export { setup } from './setup/index.js'
|
||||
export { withCmd } from './with/index.js'
|
||||
|
||||
@@ -23,13 +23,27 @@ import type { EnvLockfile, LockfileObject, PackageSnapshot } from '@pnpm/lockfil
|
||||
import { registerProject, type StoreController } from '@pnpm/store.controller'
|
||||
import type { DepPath, ProjectId, ProjectRootDir, Registries } from '@pnpm/types'
|
||||
import { familySync } from 'detect-libc'
|
||||
import semver from 'semver'
|
||||
import { symlinkDir } from 'symlink-dir'
|
||||
|
||||
import { verifyPnpmEngineIdentity, type VerifyPnpmEngineIdentityOptions } from './verifyPnpmEngineIdentity.js'
|
||||
|
||||
// @pnpm/exe has platform-specific binaries, so its GVS hash must
|
||||
// include ENGINE_NAME for correct per-platform resolution.
|
||||
const PNPM_ALLOW_BUILDS: Record<string, boolean> = { '@pnpm/exe': true }
|
||||
// Both pnpm wrappers (`@pnpm/exe`, unscoped `pnpm`) carry platform-specific
|
||||
// binaries; marking them buildable puts ENGINE_NAME in the GVS hash so each
|
||||
// platform resolves to its own entry instead of colliding.
|
||||
const PNPM_ALLOW_BUILDS: Record<string, boolean> = { '@pnpm/exe': true, 'pnpm': true }
|
||||
|
||||
/**
|
||||
* Package name to install for a switch to `pnpmVersion`. From v12 the unscoped
|
||||
* `pnpm` is itself the native exe (equal content to `@pnpm/exe`), so v12+ always
|
||||
* converges on `pnpm`, even from a SEA `@pnpm/exe` build. Earlier majors keep
|
||||
* `pnpm` (JS) and `@pnpm/exe` (SEA) distinct, preserving the running identity.
|
||||
*/
|
||||
export function pnpmPackageNameToInstall (pnpmVersion: string): string {
|
||||
const parsed = semver.parse(pnpmVersion, { loose: true })
|
||||
if (parsed != null && parsed.major >= 12) return 'pnpm'
|
||||
return getCurrentPackageName()
|
||||
}
|
||||
|
||||
export interface InstallPnpmResult {
|
||||
binDir: string
|
||||
@@ -51,15 +65,15 @@ export interface InstallPnpmOptions extends GlobalAddOptions {
|
||||
* Creates an entry in globalPkgDir that is visible to `pnpm ls -g`.
|
||||
*/
|
||||
export async function installPnpm (pnpmVersion: string, opts: InstallPnpmOptions): Promise<InstallPnpmResult> {
|
||||
const currentPkgName = getCurrentPackageName()
|
||||
const pkgName = pnpmPackageNameToInstall(pnpmVersion)
|
||||
|
||||
const wantedLockfile = opts.envLockfile
|
||||
? buildLockfileFromEnvLockfile(opts.envLockfile, currentPkgName, pnpmVersion)
|
||||
? buildLockfileFromEnvLockfile(opts.envLockfile, pkgName, pnpmVersion)
|
||||
: undefined
|
||||
|
||||
const result = await installPnpmToGlobalDir(
|
||||
opts,
|
||||
currentPkgName,
|
||||
pkgName,
|
||||
pnpmVersion,
|
||||
wantedLockfile
|
||||
)
|
||||
@@ -87,13 +101,13 @@ export async function installPnpmToStore (
|
||||
packageManager?: { name: string, version: string }
|
||||
} & VerifyPnpmEngineIdentityOptions
|
||||
): Promise<{ binDir: string }> {
|
||||
const currentPkgName = getCurrentPackageName()
|
||||
const wantedLockfile = buildLockfileFromEnvLockfile(opts.envLockfile, currentPkgName, pnpmVersion)
|
||||
const pkgName = pnpmPackageNameToInstall(pnpmVersion)
|
||||
const wantedLockfile = buildLockfileFromEnvLockfile(opts.envLockfile, pkgName, pnpmVersion)
|
||||
const globalVirtualStoreDir = path.join(opts.storeDir, 'links')
|
||||
|
||||
// Compute the GVS hash for the pnpm package to find its path
|
||||
const pnpmGvsPath = findPnpmGvsPath(wantedLockfile, currentPkgName, globalVirtualStoreDir, PNPM_ALLOW_BUILDS)
|
||||
const pnpmPkgDir = path.join(pnpmGvsPath, 'node_modules', currentPkgName)
|
||||
const pnpmGvsPath = findPnpmGvsPath(wantedLockfile, pkgName, globalVirtualStoreDir, PNPM_ALLOW_BUILDS)
|
||||
const pnpmPkgDir = path.join(pnpmGvsPath, 'node_modules', pkgName)
|
||||
const binDir = path.join(pnpmGvsPath, 'bin')
|
||||
|
||||
// Check if already installed in the GVS
|
||||
@@ -125,7 +139,7 @@ export async function installPnpmToStore (
|
||||
})
|
||||
|
||||
// Now the GVS should be populated — create bins alongside the GVS entry
|
||||
linkExePlatformBinary(pnpmGvsPath)
|
||||
linkExePlatformBinary(pnpmGvsPath, pkgName)
|
||||
await linkBins(path.join(pnpmGvsPath, 'node_modules'), binDir, { warn: noop })
|
||||
|
||||
return { binDir }
|
||||
@@ -219,7 +233,7 @@ async function installPnpmToGlobalDir (
|
||||
await installFromResolution(installDir, opts, [`${pkgName}@${version}`])
|
||||
}
|
||||
|
||||
linkExePlatformBinary(installDir)
|
||||
linkExePlatformBinary(installDir, pkgName)
|
||||
await linkBins(path.join(installDir, 'node_modules'), binDir, { warn: noop })
|
||||
|
||||
// Create hash symlink for the global packages system
|
||||
@@ -360,10 +374,14 @@ function legacyOsSegment (platform: NodeJS.Platform, libcFamily: string | null):
|
||||
}
|
||||
|
||||
/**
|
||||
* Future scope-local directory name of the `@pnpm/exe` platform package, under
|
||||
* the `exe.<platform>-<arch>[-musl]` scheme that matches the workspace
|
||||
* directory layout. `linkExePlatformBinary` checks this as a fallback so a
|
||||
* future rename of the published packages works without touching this logic.
|
||||
* Scope-local directory name of the platform package under the
|
||||
* `exe.<platform>-<arch>[-musl]` scheme, i.e. the published package
|
||||
* `@pnpm/exe.<platform>-<arch>[-musl]`. pnpm v12 (the Rust port) ships its
|
||||
* native binaries under exactly this convention, so `linkExePlatformBinary`
|
||||
* relinks a v12 install with no v12-specific logic. `@pnpm/exe` (the
|
||||
* TypeScript SEA build) is expected to adopt the same scheme in a future
|
||||
* release, which is why the legacy `@pnpm/<os>-<arch>` name is still checked
|
||||
* first as a fallback.
|
||||
*/
|
||||
export function exePlatformPkgDirNameNext (
|
||||
platform: NodeJS.Platform,
|
||||
@@ -375,60 +393,71 @@ export function exePlatformPkgDirNameNext (
|
||||
return `exe.${platform}-${normalizedArch}${libcSuffix}`
|
||||
}
|
||||
|
||||
// @pnpm/exe bundles Node.js via optional platform-specific packages
|
||||
// (e.g. @pnpm/macos-arm64, @pnpm/linuxstatic-x64; or, after a future rename,
|
||||
// @pnpm/exe.darwin-arm64, @pnpm/exe.linux-x64-musl). Its postinstall script
|
||||
// links the correct binary into the @pnpm/exe package dir. Since scripts are
|
||||
// disabled during install (to support systems without Node.js), we replicate
|
||||
// that linking here, checking both naming schemes so self-update works across
|
||||
// the rename.
|
||||
export function linkExePlatformBinary (installDir: string): void {
|
||||
const exePkgDir = path.join(installDir, 'node_modules', '@pnpm', 'exe')
|
||||
if (!fs.existsSync(exePkgDir)) return
|
||||
// In pnpm's symlinked node_modules layout, the platform package is not hoisted
|
||||
// to the top-level node_modules. It's a dependency of @pnpm/exe and lives as a
|
||||
// sibling in the virtual store. Resolve through the @pnpm/exe symlink to find it.
|
||||
const exeRealDir = fs.realpathSync(exePkgDir)
|
||||
// The wrapper's preinstall links the platform binary into the wrapper dir, but
|
||||
// scripts are disabled during pnpm's own installs, so replicate it here — trying
|
||||
// the legacy and the newer `exe.<target>` platform-package names.
|
||||
export function linkExePlatformBinary (installDir: string, wrapperPkgName: string = '@pnpm/exe'): void {
|
||||
const wrapperDir = path.join(installDir, 'node_modules', ...wrapperPkgName.split('/'))
|
||||
if (!fs.existsSync(wrapperDir)) return
|
||||
const platform = process.platform
|
||||
const arch = process.arch
|
||||
const libcFamily = familySync()
|
||||
const executable = platform === 'win32' ? 'pnpm.exe' : 'pnpm'
|
||||
// Resolve the platform binary by its explicit adjacent path in the real
|
||||
// virtual store, not via Node resolution: a `node_modules` walk could be
|
||||
// shadowed by a higher-precedence `@pnpm/<dirName>` in a repo-controlled
|
||||
// `store-dir`. `@pnpm/exe`'s parent is already `@pnpm`; `pnpm` descends into it.
|
||||
const wrapperRealDir = fs.realpathSync(wrapperDir)
|
||||
const scopeDir = wrapperPkgName.startsWith('@')
|
||||
? path.dirname(wrapperRealDir)
|
||||
: path.join(path.dirname(wrapperRealDir), '@pnpm')
|
||||
const candidateDirNames = [
|
||||
exePlatformPkgDirName(platform, arch, libcFamily),
|
||||
exePlatformPkgDirNameNext(platform, arch, libcFamily),
|
||||
]
|
||||
let src: string | undefined
|
||||
for (const dirName of candidateDirNames) {
|
||||
const candidate = path.join(path.dirname(exeRealDir), dirName, executable)
|
||||
const candidate = path.join(scopeDir, dirName, executable)
|
||||
if (fs.existsSync(candidate)) {
|
||||
src = candidate
|
||||
break
|
||||
}
|
||||
}
|
||||
if (src == null) return
|
||||
const dest = path.join(exePkgDir, executable)
|
||||
const dest = path.join(wrapperDir, executable)
|
||||
forceLink(src, dest)
|
||||
|
||||
if (platform === 'win32') {
|
||||
// Aliases (pn / pnpx / pnx) need to be .exe hardlinks of the SEA binary,
|
||||
// Aliases (pn / pnpx / pnx) need to be .exe hardlinks of the native binary,
|
||||
// not the .cmd wrappers we ship in the tarball. cmd-shim's Bash shim for
|
||||
// a .cmd target wraps it in `exec cmd /C ...`, and MSYS2 / Git Bash
|
||||
// mangles `/C` into a Windows path — cmd.exe then falls into interactive
|
||||
// mode and prints its banner instead of running the alias. .exe sources
|
||||
// sidestep cmd-shim's wrapper. The SEA binary detects which name it was
|
||||
// sidestep cmd-shim's wrapper. The native binary detects which name it was
|
||||
// launched as via process.execPath and prepends `dlx` for pnpx / pnx.
|
||||
// See https://github.com/pnpm/pnpm/issues/11486.
|
||||
for (const alias of ['pn', 'pnpx', 'pnx']) {
|
||||
forceLink(src, path.join(exePkgDir, `${alias}.exe`))
|
||||
forceLink(src, path.join(wrapperDir, `${alias}.exe`))
|
||||
}
|
||||
|
||||
const exePkgJsonPath = path.join(exePkgDir, 'package.json')
|
||||
const exePkg = JSON.parse(fs.readFileSync(exePkgJsonPath, 'utf8'))
|
||||
exePkg.bin.pnpm = 'pnpm.exe'
|
||||
exePkg.bin.pn = 'pn.exe'
|
||||
exePkg.bin.pnpx = 'pnpx.exe'
|
||||
exePkg.bin.pnx = 'pnx.exe'
|
||||
fs.writeFileSync(exePkgJsonPath, JSON.stringify(exePkg, null, 2))
|
||||
const wrapperPkgJsonPath = path.join(wrapperDir, 'package.json')
|
||||
const wrapperPkg = JSON.parse(fs.readFileSync(wrapperPkgJsonPath, 'utf8'))
|
||||
wrapperPkg.bin.pnpm = 'pnpm.exe'
|
||||
wrapperPkg.bin.pn = 'pn.exe'
|
||||
wrapperPkg.bin.pnpx = 'pnpx.exe'
|
||||
wrapperPkg.bin.pnx = 'pnx.exe'
|
||||
// Temp file + rename, not in-place: package.json is hard-linked from the
|
||||
// content-addressable store, so writing in place would mutate the shared blob.
|
||||
const tempPkgJsonPath = `${wrapperPkgJsonPath}.pnpm-tmp`
|
||||
try {
|
||||
fs.writeFileSync(tempPkgJsonPath, JSON.stringify(wrapperPkg, null, 2))
|
||||
fs.renameSync(tempPkgJsonPath, wrapperPkgJsonPath)
|
||||
} catch (err: unknown) {
|
||||
try {
|
||||
fs.rmSync(tempPkgJsonPath, { force: true })
|
||||
} catch {}
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ jest.unstable_mockModule('@pnpm/cli.meta', () => {
|
||||
},
|
||||
}
|
||||
})
|
||||
const { selfUpdate, installPnpm, linkExePlatformBinary, exePlatformPkgDirName, exePlatformPkgDirNameNext } = await import('@pnpm/engine.pm.commands')
|
||||
const { selfUpdate, installPnpm, linkExePlatformBinary, exePlatformPkgDirName, exePlatformPkgDirNameNext, pnpmPackageNameToInstall } = await import('@pnpm/engine.pm.commands')
|
||||
|
||||
beforeEach(async () => {
|
||||
await setupMockAgent()
|
||||
@@ -1122,6 +1122,32 @@ describe('linkExePlatformBinary', () => {
|
||||
expect(result).toBe(fakeBinaryContent)
|
||||
})
|
||||
|
||||
test('links the pnpm v12 wrapper from its @pnpm/exe.<target> dependency', () => {
|
||||
const dir = tempDir(false)
|
||||
|
||||
// pnpm v12 (the Rust port) is published as the unscoped `pnpm` wrapper that
|
||||
// depends on `@pnpm/exe.<platform>-<arch>[-musl]` — the `exe.<...>` scheme.
|
||||
const nextPkgName = exePlatformPkgDirNameNext(platform, arch, libcFamily)
|
||||
const wrapperDir = path.join(dir, 'node_modules', 'pnpm')
|
||||
const platformDir = path.join(dir, 'node_modules', '@pnpm', nextPkgName)
|
||||
|
||||
fs.mkdirSync(wrapperDir, { recursive: true })
|
||||
fs.mkdirSync(platformDir, { recursive: true })
|
||||
|
||||
fs.writeFileSync(path.join(wrapperDir, executable), 'This is a placeholder.')
|
||||
fs.writeFileSync(path.join(wrapperDir, 'package.json'), JSON.stringify({
|
||||
bin: { pnpm: 'pnpm', pn: 'pn', pnpx: 'pnpx', pnx: 'pnx' },
|
||||
}))
|
||||
|
||||
const fakeBinaryContent = '#!/bin/sh\necho "fake pnpm v12 binary"'
|
||||
fs.writeFileSync(path.join(platformDir, executable), fakeBinaryContent)
|
||||
|
||||
linkExePlatformBinary(dir, 'pnpm')
|
||||
|
||||
const result = fs.readFileSync(path.join(wrapperDir, executable), 'utf8')
|
||||
expect(result).toBe(fakeBinaryContent)
|
||||
})
|
||||
|
||||
// Regression coverage for https://github.com/pnpm/pnpm/issues/11486 — the
|
||||
// `pn` / `pnpx` / `pnx` aliases were broken in MSYS2 / Git Bash on Windows.
|
||||
// Root cause: linkExePlatformBinary pointed those bin entries at .cmd files,
|
||||
@@ -1171,6 +1197,21 @@ describe('linkExePlatformBinary', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('pnpmPackageNameToInstall', () => {
|
||||
test('installs the unscoped `pnpm` package from v12 onward', () => {
|
||||
expect(pnpmPackageNameToInstall('12.0.0-alpha.0')).toBe('pnpm')
|
||||
expect(pnpmPackageNameToInstall('12.3.4')).toBe('pnpm')
|
||||
expect(pnpmPackageNameToInstall('13.0.0')).toBe('pnpm')
|
||||
})
|
||||
|
||||
test('keeps the running package identity before v12', () => {
|
||||
// getCurrentPackageName() is `pnpm` in the (non-SEA) test runtime, so this
|
||||
// asserts v11 and earlier are not forced onto a different package.
|
||||
expect(pnpmPackageNameToInstall('11.9.0')).toBe('pnpm')
|
||||
expect(pnpmPackageNameToInstall('9.1.0')).toBe('pnpm')
|
||||
})
|
||||
})
|
||||
|
||||
describe('exePlatformPkgDirName', () => {
|
||||
test('uses linuxstatic- prefix for linux + musl libc family', () => {
|
||||
expect(exePlatformPkgDirName('linux', 'x64', 'musl')).toBe('linuxstatic-x64')
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Adapted from pacquet/npm/pacquet/scripts/generate-packages.mjs, which was
|
||||
// Adapted from pacquet/npm/pnpm/scripts/generate-packages.mjs, which was
|
||||
// itself copied from Rome (https://github.com/rome/tools/blob/392d188a49/npm/rome/scripts/generate-packages.mjs).
|
||||
|
||||
import { resolve } from "node:path";
|
||||
|
||||
Reference in New Issue
Block a user