From a33eeec9cd96b617db6ecdbecf9a1b5deee87d78 Mon Sep 17 00:00:00 2001 From: Zoltan Kochan Date: Sat, 27 Jun 2026 01:24:33 +0200 Subject: [PATCH] feat(pnpm): publish pnpm v12 (the Rust port) as pnpm and @pnpm/exe (#12670) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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/` form; the reporter footer reads `Done in ... using pnpm v`; 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.-[-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/` binaries) and is left untouched. --- .changeset/link-pnpm-v12-exe-binaries.md | 6 + .github/workflows/pacquet-release-to-npm.yml | 67 +++-- pacquet/crates/cli/src/cli_args.rs | 4 +- pacquet/crates/cli/src/cli_args/link.rs | 4 +- pacquet/crates/cli/src/cli_args/patch.rs | 4 +- .../crates/cli/src/cli_args/patch/tests.rs | 6 +- pacquet/crates/cli/src/lib.rs | 27 +- pacquet/crates/cli/src/tests.rs | 28 ++ pacquet/crates/cli/tests/pnpx_alias.rs | 27 ++ pacquet/crates/config/src/defaults.rs | 20 +- pacquet/crates/config/src/defaults/tests.rs | 6 +- pacquet/crates/config/src/lib.rs | 2 +- pacquet/crates/default-reporter/src/lib.rs | 2 +- pacquet/crates/default-reporter/src/state.rs | 2 +- .../crates/default-reporter/tests/render.rs | 4 +- pacquet/crates/network/src/lib.rs | 8 +- pacquet/npm/pacquet/README.md | 17 -- pacquet/npm/pacquet/bin/pacquet | 4 - .../npm/pacquet/scripts/generate-packages.mjs | 148 ---------- pacquet/npm/pnpm/README.md | 272 ++++++++++++++++++ pacquet/npm/{pacquet => pnpm}/install.js | 87 +++--- pacquet/npm/{pacquet => pnpm}/package.json | 26 +- pacquet/npm/pnpm/pn | 2 + pacquet/npm/pnpm/pnpm | 4 + pacquet/npm/pnpm/pnpx | 2 + pacquet/npm/pnpm/pnx | 2 + .../npm/pnpm/scripts/generate-packages.mjs | 138 +++++++++ pnpm11/engine/pm/commands/src/index.ts | 2 +- .../commands/src/self-updater/installPnpm.ts | 113 +++++--- .../test/self-updater/selfUpdate.test.ts | 43 ++- pnpr/npm/pnpr/scripts/generate-packages.mjs | 2 +- 31 files changed, 748 insertions(+), 331 deletions(-) create mode 100644 .changeset/link-pnpm-v12-exe-binaries.md create mode 100644 pacquet/crates/cli/src/tests.rs create mode 100644 pacquet/crates/cli/tests/pnpx_alias.rs delete mode 100644 pacquet/npm/pacquet/README.md delete mode 100644 pacquet/npm/pacquet/bin/pacquet delete mode 100644 pacquet/npm/pacquet/scripts/generate-packages.mjs create mode 100644 pacquet/npm/pnpm/README.md rename pacquet/npm/{pacquet => pnpm}/install.js (59%) rename pacquet/npm/{pacquet => pnpm}/package.json (53%) create mode 100755 pacquet/npm/pnpm/pn create mode 100644 pacquet/npm/pnpm/pnpm create mode 100755 pacquet/npm/pnpm/pnpx create mode 100755 pacquet/npm/pnpm/pnx create mode 100644 pacquet/npm/pnpm/scripts/generate-packages.mjs diff --git a/.changeset/link-pnpm-v12-exe-binaries.md b/.changeset/link-pnpm-v12-exe-binaries.md new file mode 100644 index 0000000000..d2f145c0b9 --- /dev/null +++ b/.changeset/link-pnpm-v12-exe-binaries.md @@ -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.-` 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. diff --git a/.github/workflows/pacquet-release-to-npm.yml b/.github/workflows/pacquet-release-to-npm.yml index a1be116eb2..26ccd5b28b 100644 --- a/.github/workflows/pacquet-release-to-npm.yml +++ b/.github/workflows/pacquet-release-to-npm.yml @@ -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.` 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/` 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.`) 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 diff --git a/pacquet/crates/cli/src/cli_args.rs b/pacquet/crates/cli/src/cli_args.rs index cc85bd6ccf..3a3e9cbd24 100644 --- a/pacquet/crates/cli/src/cli_args.rs +++ b/pacquet/crates/cli/src/cli_args.rs @@ -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 { diff --git a/pacquet/crates/cli/src/cli_args/link.rs b/pacquet/crates/cli/src/cli_args/link.rs index 7864b87596..f30c134d4f 100644 --- a/pacquet/crates/cli/src/cli_args/link.rs +++ b/pacquet/crates/cli/src/cli_args/link.rs @@ -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 ")] + #[display("You must provide a parameter. Usage: pnpm link ")] #[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))] diff --git a/pacquet/crates/cli/src/cli_args/patch.rs b/pacquet/crates/cli/src/cli_args/patch.rs index ead5f6decd..6cf42b526c 100644 --- a/pacquet/crates/cli/src/cli_args/patch.rs +++ b/pacquet/crates/cli/src/cli_args/patch.rs @@ -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) diff --git a/pacquet/crates/cli/src/cli_args/patch/tests.rs b/pacquet/crates/cli/src/cli_args/patch/tests.rs index 89c388bb98..50aa350de9 100644 --- a/pacquet/crates/cli/src/cli_args/patch/tests.rs +++ b/pacquet/crates/cli/src/cli_args/patch/tests.rs @@ -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)] diff --git a/pacquet/crates/cli/src/lib.rs b/pacquet/crates/cli/src/lib.rs index 669467993f..45377a398a 100644 --- a/pacquet/crates/cli/src/lib.rs +++ b/pacquet/crates/cli/src/lib.rs @@ -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` 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 { + 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) -> Vec { + 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; diff --git a/pacquet/crates/cli/src/tests.rs b/pacquet/crates/cli/src/tests.rs new file mode 100644 index 0000000000..1d2fd0adb1 --- /dev/null +++ b/pacquet/crates/cli/src/tests.rs @@ -0,0 +1,28 @@ +use super::inject_alias_subcommand; +use std::ffi::OsString; + +fn argv(parts: &[&str]) -> Vec { + 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); +} diff --git a/pacquet/crates/cli/tests/pnpx_alias.rs b/pacquet/crates/cli/tests/pnpx_alias.rs new file mode 100644 index 0000000000..ce1970f4dc --- /dev/null +++ b/pacquet/crates/cli/tests/pnpx_alias.rs @@ -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)"); +} diff --git a/pacquet/crates/config/src/defaults.rs b/pacquet/crates/config/src/defaults.rs index bcd382557f..4cdf698537 100644 --- a/pacquet/crates/config/src/defaults.rs +++ b/pacquet/crates/config/src/defaults.rs @@ -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-` 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/`, 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(), ) diff --git a/pacquet/crates/config/src/defaults/tests.rs b/pacquet/crates/config/src/defaults/tests.rs index 6be91348ad..18b97df5a8 100644 --- a/pacquet/crates/config/src/defaults/tests.rs +++ b/pacquet/crates/config/src/defaults/tests.rs @@ -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- npm/? node/? ` prefix and the two -/// trailing space-separated tokens rather than the full string. +/// the `pnpm/ 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 ` ` tail, got {ua:?}"); diff --git a/pacquet/crates/config/src/lib.rs b/pacquet/crates/config/src/lib.rs index 697a65bc1c..021b0befd1 100644 --- a/pacquet/crates/config/src/lib.rs +++ b/pacquet/crates/config/src/lib.rs @@ -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- npm/? node/? ` format (built by + /// `pnpm/ npm/? node/? ` format (built by /// `default_user_agent`). #[default(_code = "default_user_agent()")] pub user_agent: String, diff --git a/pacquet/crates/default-reporter/src/lib.rs b/pacquet/crates/default-reporter/src/lib.rs index 14aa2c863e..fef82f88f9 100644 --- a/pacquet/crates/default-reporter/src/lib.rs +++ b/pacquet/crates/default-reporter/src/lib.rs @@ -42,7 +42,7 @@ pub fn set_cwd(cwd: impl Into) { let _ = CWD.set(cwd.into()); } -/// Set the version rendered in the `Done in ... using pacquet v` +/// Set the version rendered in the `Done in ... using pnpm v` /// footer. Call once before the first event; ignored if already set. pub fn set_package_version(version: impl Into) { let _ = PACKAGE_VERSION.set(version.into()); diff --git a/pacquet/crates/default-reporter/src/state.rs b/pacquet/crates/default-reporter/src/state.rs index 3d6db3cffd..f1d8064e53 100644 --- a/pacquet/crates/default-reporter/src/state.rs +++ b/pacquet/crates/default-reporter/src/state.rs @@ -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; diff --git a/pacquet/crates/default-reporter/tests/render.rs b/pacquet/crates/default-reporter/tests/render.rs index 60fdee889d..c1916b34d5 100644 --- a/pacquet/crates/default-reporter/tests/render.rs +++ b/pacquet/crates/default-reporter/tests/render.rs @@ -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", ); } diff --git a/pacquet/crates/network/src/lib.rs b/pacquet/crates/network/src/lib.rs index 3908ad57cb..cfed5b26ed 100644 --- a/pacquet/crates/network/src/lib.rs +++ b/pacquet/crates/network/src/lib.rs @@ -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- npm/? node/? ` 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/ npm/? node/? ` 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 diff --git a/pacquet/npm/pacquet/README.md b/pacquet/npm/pacquet/README.md deleted file mode 100644 index 9fb46e3776..0000000000 --- a/pacquet/npm/pacquet/README.md +++ /dev/null @@ -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`. diff --git a/pacquet/npm/pacquet/bin/pacquet b/pacquet/npm/pacquet/bin/pacquet deleted file mode 100644 index 7902f63c12..0000000000 --- a/pacquet/npm/pacquet/bin/pacquet +++ /dev/null @@ -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). diff --git a/pacquet/npm/pacquet/scripts/generate-packages.mjs b/pacquet/npm/pacquet/scripts/generate-packages.mjs deleted file mode 100644 index 89841141b3..0000000000 --- a/pacquet/npm/pacquet/scripts/generate-packages.mjs +++ /dev/null @@ -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/` 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(); diff --git a/pacquet/npm/pnpm/README.md b/pacquet/npm/pnpm/README.md new file mode 100644 index 0000000000..79dea36c6a --- /dev/null +++ b/pacquet/npm/pnpm/README.md @@ -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/) + + + + + pnpm + + +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. + +[![npm version](https://img.shields.io/npm/v/pnpm.svg?label=latest)](https://github.com/pnpm/pnpm/releases/latest) +[![OpenCollective](https://opencollective.com/pnpm/backers/badge.svg)](https://opencollective.com/pnpm) +[![OpenCollective](https://opencollective.com/pnpm/sponsors/badge.svg)](https://opencollective.com/pnpm) +[![X Follow](https://img.shields.io/twitter/follow/pnpmjs.svg?style=social&label=Follow)](https://x.com/intent/follow?screen_name=pnpmjs®ion=follow_link) +[![Stand With Ukraine](https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/badges/StandWithUkraine.svg)](https://stand-with-ukraine.pp.ua) + + + +## Platinum Sponsors + + + + + + + + +
+ Bit + + + + + + OpenAI + + +
+ +## Gold Sponsors + + + + + + + + + + + + + + + + + + +
+ + + + + Sanity + + + + + + + + Discord + + + + Vite +
+ + + + + SerpApi + + + + + + + + CodeRabbit + + + + + + + + Stackblitz + + +
+ + + + + Workleap + + + + + + + + Nx + + +
+ +## Silver Sponsors + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + Replit + + + + Cybozu + + + + + + BairesDev + + +
+ + + + + Thesys + + + + devowl.io + + + + + + u|screen + + +
+ Leniolabs_ + + + + + + Depot + + + + + + + + Cerbos + + +
+ ⏱️ Time.now +
+ + + +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: + +![](https://pnpm.io/img/benchmarks/alotta-files.svg) + +## 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) + diff --git a/pacquet/npm/pacquet/install.js b/pacquet/npm/pnpm/install.js similarity index 59% rename from pacquet/npm/pacquet/install.js rename to pacquet/npm/pnpm/install.js index a146d8a296..69310f6a01 100644 --- a/pacquet/npm/pacquet/install.js +++ b/pacquet/npm/pnpm/install.js @@ -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 {} } } diff --git a/pacquet/npm/pacquet/package.json b/pacquet/npm/pnpm/package.json similarity index 53% rename from pacquet/npm/pacquet/package.json rename to pacquet/npm/pnpm/package.json index ffb8344a4f..7fb5d6341e 100644 --- a/pacquet/npm/pacquet/package.json +++ b/pacquet/npm/pnpm/package.json @@ -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" ] } diff --git a/pacquet/npm/pnpm/pn b/pacquet/npm/pnpm/pn new file mode 100755 index 0000000000..401b25d633 --- /dev/null +++ b/pacquet/npm/pnpm/pn @@ -0,0 +1,2 @@ +#!/bin/sh +exec pnpm "$@" diff --git a/pacquet/npm/pnpm/pnpm b/pacquet/npm/pnpm/pnpm new file mode 100644 index 0000000000..11637685c1 --- /dev/null +++ b/pacquet/npm/pnpm/pnpm @@ -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). diff --git a/pacquet/npm/pnpm/pnpx b/pacquet/npm/pnpm/pnpx new file mode 100755 index 0000000000..a07a6f3553 --- /dev/null +++ b/pacquet/npm/pnpm/pnpx @@ -0,0 +1,2 @@ +#!/bin/sh +exec pnpm dlx "$@" diff --git a/pacquet/npm/pnpm/pnx b/pacquet/npm/pnpm/pnx new file mode 100755 index 0000000000..a07a6f3553 --- /dev/null +++ b/pacquet/npm/pnpm/pnx @@ -0,0 +1,2 @@ +#!/bin/sh +exec pnpm dlx "$@" diff --git a/pacquet/npm/pnpm/scripts/generate-packages.mjs b/pacquet/npm/pnpm/scripts/generate-packages.mjs new file mode 100644 index 0000000000..951ca926c9 --- /dev/null +++ b/pacquet/npm/pnpm/scripts/generate-packages.mjs @@ -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.` 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-`. +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.` 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.` 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(); diff --git a/pnpm11/engine/pm/commands/src/index.ts b/pnpm11/engine/pm/commands/src/index.ts index 4d4119c559..ac8f032312 100644 --- a/pnpm11/engine/pm/commands/src/index.ts +++ b/pnpm11/engine/pm/commands/src/index.ts @@ -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' diff --git a/pnpm11/engine/pm/commands/src/self-updater/installPnpm.ts b/pnpm11/engine/pm/commands/src/self-updater/installPnpm.ts index e7d62207a6..9e05776991 100644 --- a/pnpm11/engine/pm/commands/src/self-updater/installPnpm.ts +++ b/pnpm11/engine/pm/commands/src/self-updater/installPnpm.ts @@ -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 = { '@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 = { '@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 { - 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.-[-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.-[-musl]` scheme, i.e. the published package + * `@pnpm/exe.-[-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/-` 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.` 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/` 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 + } } } diff --git a/pnpm11/engine/pm/commands/test/self-updater/selfUpdate.test.ts b/pnpm11/engine/pm/commands/test/self-updater/selfUpdate.test.ts index 87ea73e04f..87c0cd07c2 100644 --- a/pnpm11/engine/pm/commands/test/self-updater/selfUpdate.test.ts +++ b/pnpm11/engine/pm/commands/test/self-updater/selfUpdate.test.ts @@ -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. dependency', () => { + const dir = tempDir(false) + + // pnpm v12 (the Rust port) is published as the unscoped `pnpm` wrapper that + // depends on `@pnpm/exe.-[-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') diff --git a/pnpr/npm/pnpr/scripts/generate-packages.mjs b/pnpr/npm/pnpr/scripts/generate-packages.mjs index ab19ee0695..d5350e1c43 100644 --- a/pnpr/npm/pnpr/scripts/generate-packages.mjs +++ b/pnpr/npm/pnpr/scripts/generate-packages.mjs @@ -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";