From 23716ed9b07fdd9f45efdc5fb0acab87980e79c3 Mon Sep 17 00:00:00 2001 From: Felipe Santos Date: Sun, 14 Jun 2026 09:35:44 -0300 Subject: [PATCH] fix: preserve user-defined npm_config_* env vars in lifecycle scripts (#12400) * fix: preserve user-defined npm_config_* env vars in lifecycle scripts * fix: use released `@pnpm/npm-lifecycle` and port npm_config_* filter to pacquet Pin the catalog to the released `@pnpm/npm-lifecycle` ^1100.0.0 instead of a mutable PR-head ref, regenerating the lockfile to the immutable registry tarball. Port the upstream env filter to pacquet's make_env so user-defined npm_config_* vars (e.g. npm_config_platform_arch) survive lifecycle scripts while (npm|pnpm)_config_* auth keys are still stripped, matching `@pnpm/npm-lifecycle` 9e2ac78148. Harden the new TS test to save/restore npm_config_platform_arch. * test(executor): restore env vars to pre-test value in lifecycle EnvGuard The guard removed the seeded var unconditionally on drop, which would discard any value the process env already had. Capture the original via var_os and restore it (or remove only when originally absent). --------- Co-authored-by: Zoltan Kochan --- .changeset/preserve-user-npm-config-vars.md | 6 ++ .../inspect-frozen-lockfile/postinstall.js | 13 --- .../package.json | 2 +- .../inspect-npm-config-env/postinstall.js | 5 + exec/lifecycle/test/index.ts | 28 +++-- .../crates/executor/src/lifecycle/tests.rs | 46 +++++--- pacquet/crates/executor/src/make_env.rs | 92 ++++++++++------ pacquet/crates/executor/src/make_env/tests.rs | 101 ++++++++++++++---- pnpm-lock.yaml | 12 +-- pnpm-workspace.yaml | 2 +- 10 files changed, 211 insertions(+), 96 deletions(-) create mode 100644 .changeset/preserve-user-npm-config-vars.md delete mode 100644 exec/lifecycle/test/fixtures/inspect-frozen-lockfile/postinstall.js rename exec/lifecycle/test/fixtures/{inspect-frozen-lockfile => inspect-npm-config-env}/package.json (76%) create mode 100644 exec/lifecycle/test/fixtures/inspect-npm-config-env/postinstall.js diff --git a/.changeset/preserve-user-npm-config-vars.md b/.changeset/preserve-user-npm-config-vars.md new file mode 100644 index 0000000000..cd4f1a24a7 --- /dev/null +++ b/.changeset/preserve-user-npm-config-vars.md @@ -0,0 +1,6 @@ +--- +"@pnpm/exec.lifecycle": patch +"pnpm": patch +--- + +User-defined `npm_config_*` environment variables are now preserved during lifecycle script execution. Previously, all `npm_`-prefixed env vars were stripped, which caused user-set variables like `npm_config_platform_arch` to be lost [pnpm/pnpm#12399](https://github.com/pnpm/pnpm/issues/12399). diff --git a/exec/lifecycle/test/fixtures/inspect-frozen-lockfile/postinstall.js b/exec/lifecycle/test/fixtures/inspect-frozen-lockfile/postinstall.js deleted file mode 100644 index 07955a1acb..0000000000 --- a/exec/lifecycle/test/fixtures/inspect-frozen-lockfile/postinstall.js +++ /dev/null @@ -1,13 +0,0 @@ -const value = process.env['npm_config_frozen_lockfile'] - -switch(value) { - case undefined: - process.stdout.write('unset') - break - case '': - process.stdout.write('empty string') - break - default: - process.stdout.write('string: ' + value) - break -} diff --git a/exec/lifecycle/test/fixtures/inspect-frozen-lockfile/package.json b/exec/lifecycle/test/fixtures/inspect-npm-config-env/package.json similarity index 76% rename from exec/lifecycle/test/fixtures/inspect-frozen-lockfile/package.json rename to exec/lifecycle/test/fixtures/inspect-npm-config-env/package.json index 102b261284..94aed30cb3 100644 --- a/exec/lifecycle/test/fixtures/inspect-frozen-lockfile/package.json +++ b/exec/lifecycle/test/fixtures/inspect-npm-config-env/package.json @@ -1,5 +1,5 @@ { - "name": "inspect-frozen-lockfile", + "name": "inspect-npm-config-env", "version": "1.0.0", "scripts": { "postinstall": "node postinstall.js | test-ipc-server-client ./test.sock" diff --git a/exec/lifecycle/test/fixtures/inspect-npm-config-env/postinstall.js b/exec/lifecycle/test/fixtures/inspect-npm-config-env/postinstall.js new file mode 100644 index 0000000000..c60b78134e --- /dev/null +++ b/exec/lifecycle/test/fixtures/inspect-npm-config-env/postinstall.js @@ -0,0 +1,5 @@ +for (const [key, value] of Object.entries(process.env)) { + if (key.startsWith('npm_config_') && key !== 'npm_config_node_gyp') { + process.stdout.write(`${key}=${value}\n`) + } +} diff --git a/exec/lifecycle/test/index.ts b/exec/lifecycle/test/index.ts index c0c45f1d22..7419c99e99 100644 --- a/exec/lifecycle/test/index.ts +++ b/exec/lifecycle/test/index.ts @@ -62,18 +62,28 @@ test('runLifecycleHook() passes newline correctly', async () => { ]) }) -test('runLifecycleHook() does not set npm_config env vars', async () => { - const pkgRoot = f.find('inspect-frozen-lockfile') +test('runLifecycleHook() does not set npm_config env vars but preserves user-defined ones', async () => { + const pkgRoot = f.find('inspect-npm-config-env') await using server = await createTestIpcServer(path.join(pkgRoot, 'test.sock')) const { default: pkg } = await import(path.join(pkgRoot, 'package.json')) - await runLifecycleHook('postinstall', pkg, { - depPath: '/inspect-frozen-lockfile/1.0.0', - pkgRoot, - rootModulesDir, - unsafePerm: true, - }) + const prevPlatformArch = process.env.npm_config_platform_arch + process.env.npm_config_platform_arch = 'x64' + try { + await runLifecycleHook('postinstall', pkg, { + depPath: '/inspect-npm-config-env/1.0.0', + pkgRoot, + rootModulesDir, + unsafePerm: true, + }) + } finally { + if (prevPlatformArch === undefined) { + delete process.env.npm_config_platform_arch + } else { + process.env.npm_config_platform_arch = prevPlatformArch + } + } - expect(server.getLines()).toStrictEqual(['unset']) + expect(server.getLines()).toStrictEqual(['npm_config_platform_arch=x64']) }) test('runPostinstallHooks()', async () => { diff --git a/pacquet/crates/executor/src/lifecycle/tests.rs b/pacquet/crates/executor/src/lifecycle/tests.rs index 83246f0140..8e26dfdc1b 100644 --- a/pacquet/crates/executor/src/lifecycle/tests.rs +++ b/pacquet/crates/executor/src/lifecycle/tests.rs @@ -332,9 +332,10 @@ fn missing_manifest_returns_false() { /// End-to-end check that the spawned child sees `npm_lifecycle_event`, /// `npm_lifecycle_script`, `INIT_CWD`, `npm_package_name`, and -/// `npm_package_version`, and does NOT see leaked `npm_config_*` keys -/// from this process's env. Adapts the upstream test at -/// +/// `npm_package_version`; that a user-defined `npm_config_*` var from +/// this process's env is PRESERVED; and that a `npm_config_*` auth var +/// is stripped. Adapts the upstream test at +/// /// to a file-dump model so we don't need an IPC fixture. /// /// Unix-only: relies on `printf` and `$VAR` expansion, which `cmd` @@ -343,23 +344,41 @@ fn missing_manifest_returns_false() { /// [`crate::make_env`]. #[cfg(unix)] #[test] -fn child_sees_stamped_npm_package_and_no_leaked_npm_config() { - /// RAII guard that removes a process env var on drop, so an - /// assertion failure can't leak the seed into sibling tests. - struct EnvGuard(&'static str); +fn child_sees_stamped_npm_package_and_preserves_user_config() { + /// RAII guard that restores a process env var to its pre-test + /// value on drop, so an assertion failure can't leak the seed + /// into sibling tests — nor clobber a value the env already had. + struct EnvGuard { + key: &'static str, + prev: Option, + } + impl EnvGuard { + fn new(key: &'static str) -> Self { + EnvGuard { key, prev: std::env::var_os(key) } + } + } impl Drop for EnvGuard { fn drop(&mut self) { // SAFETY: nextest runs each test in its own thread, so the // only risk is sibling tests calling `env::vars()` // concurrently — this `Drop` still runs on panic. - unsafe { std::env::remove_var(self.0) } + unsafe { + match self.prev.take() { + Some(value) => std::env::set_var(self.key, value), + None => std::env::remove_var(self.key), + } + } } } - let _guard = EnvGuard("npm_config_should_be_stripped"); + let _user_guard = EnvGuard::new("npm_config_platform_arch"); + let _auth_guard = EnvGuard::new("npm_config__authtoken"); // SAFETY: nextest runs each test in its own thread, so the only // risk is sibling tests calling `env::vars()` concurrently — the - // guard's `Drop` removes the var even on panic. - unsafe { std::env::set_var("npm_config_should_be_stripped", "leak") }; + // guards' `Drop` removes the vars even on panic. + unsafe { + std::env::set_var("npm_config_platform_arch", "x64"); + std::env::set_var("npm_config__authtoken", "should-not-leak"); + } let dir = tempdir().expect("create temp dir"); let pkg_root = dir.path(); @@ -374,7 +393,7 @@ fn child_sees_stamped_npm_package_and_no_leaked_npm_config() { // printf so the line endings are deterministic across // shells. "postinstall": format!( - "printf 'stage=%s\\nscript=%s\\nname=%s\\nver=%s\\nconfig=%s\\ninit_cwd=%s\\nleak=%s\\n' \"$npm_lifecycle_event\" \"$npm_lifecycle_script\" \"$npm_package_name\" \"$npm_package_version\" \"$npm_package_config_myKey\" \"$INIT_CWD\" \"$npm_config_should_be_stripped\" > {}", + "printf 'stage=%s\\nscript=%s\\nname=%s\\nver=%s\\nconfig=%s\\ninit_cwd=%s\\nuser=%s\\nauth=%s\\n' \"$npm_lifecycle_event\" \"$npm_lifecycle_script\" \"$npm_package_name\" \"$npm_package_version\" \"$npm_package_config_myKey\" \"$INIT_CWD\" \"$npm_config_platform_arch\" \"$npm_config__authtoken\" > {}", dump_path.display(), ), }, @@ -413,7 +432,8 @@ fn child_sees_stamped_npm_package_and_no_leaked_npm_config() { ("ver", "9.9.9"), ("config", "myValue"), ("init_cwd", expected_init_cwd.as_ref()), - ("leak", ""), // stripped — child sees empty string + ("user", "x64"), // user-defined npm_config_* is preserved + ("auth", ""), // auth npm_config_* is stripped — child sees empty string ]; for (k, v) in expected_pairs { let line = format!("{k}={v}\n"); diff --git a/pacquet/crates/executor/src/make_env.rs b/pacquet/crates/executor/src/make_env.rs index 55c8eed177..ff22d78a30 100644 --- a/pacquet/crates/executor/src/make_env.rs +++ b/pacquet/crates/executor/src/make_env.rs @@ -8,8 +8,8 @@ use std::{ /// Inputs needed to build the env for a single lifecycle hook spawn. /// /// Mirrors the union of `makeEnv` inputs and the per-call additions in -/// `lifecycle()` from `@pnpm/npm-lifecycle@d2d8e790` at -/// +/// `lifecycle()` from `@pnpm/npm-lifecycle@9e2ac78148` at +/// /// plus the wrapper's `extraEnv` additions at /// . pub struct EnvOptions<'a> { @@ -38,9 +38,9 @@ pub struct EnvBuild { /// process env to inherit from. /// /// Ports `makeEnv` + the surrounding env block in `lifecycle()` from -/// `@pnpm/npm-lifecycle@d2d8e790`: -/// - `index.js:73-104` for the post-`makeEnv` stamping. -/// - `index.js:354-414` for `makeEnv` itself (parent-env filter, +/// `@pnpm/npm-lifecycle@9e2ac78148`: +/// - `index.js:74-104` for the post-`makeEnv` stamping. +/// - `index.js:354-423` for `makeEnv` itself (parent-env filter, /// `npm_package_*` recursion, multi-line escaping). /// /// Plus the wrapper's `extraEnv` additions at @@ -54,12 +54,14 @@ pub fn build_env( manifest: &Value, parent_env: HashMap, ) -> EnvBuild { - // 1. Start from the parent env, stripping every `npm_*` key - // plus the per-call stamps we re-derive (`NODE`, `TMPDIR`, - // `INIT_CWD`, `PNPM_SCRIPT_SRC_DIR`). Mirrors the - // `!i.match(/^npm_/)` filter at index.js:359 — `pnpm_*` keys - // such as `PNPM_HOME` are intentionally NOT in the filter - // (upstream doesn't strip them either). + // 1. Start from the parent env, stripping `npm_package_*` (we + // regenerate them below) and the `(npm|pnpm)_config_*` auth + // keys, plus the per-call stamps we re-derive (`NODE`, + // `TMPDIR`, `INIT_CWD`, `PNPM_SCRIPT_SRC_DIR`). User-defined + // `npm_config_*` such as `npm_config_platform_arch` are + // preserved. Mirrors the parent-env filter at index.js:359-367 + // — `pnpm_*` keys such as `PNPM_HOME` are intentionally NOT in + // the filter (upstream doesn't strip them either). let mut env = filter_parent_env(parent_env); // 2. `npm_package_*` recursive stamp. Top-level keeps only @@ -126,14 +128,15 @@ pub fn build_env( EnvBuild { env, tmpdir } } -/// Keep PATH (handled by the caller) and everything that does not -/// start with `npm_`; drop NODE / TMPDIR / `INIT_CWD` / -/// `PNPM_SCRIPT_SRC_DIR` because we re-derive them. +/// Keep PATH (handled by the caller) and every key that is not an +/// `npm_package_*` stamp, a `(npm|pnpm)_config_*` auth key, or one of +/// the per-call stamps we re-derive (NODE / TMPDIR / `INIT_CWD` / +/// `PNPM_SCRIPT_SRC_DIR`). /// /// On Windows the comparison is case-insensitive because Rust's /// `Command::env` treats env keys case-insensitively on that -/// platform (see [`Command::env`] docs). Leaving e.g. `NPM_CONFIG_FOO` -/// alongside our `npm_*` inserts would collapse at spawn time with +/// platform (see [`Command::env`] docs). Leaving e.g. `NPM_PACKAGE_FOO` +/// alongside our stamped inserts would collapse at spawn time with /// an unpredictable winner. /// /// [`Command::env`]: https://doc.rust-lang.org/std/process/struct.Command.html#method.env @@ -141,35 +144,62 @@ fn filter_parent_env(env: HashMap) -> HashMap { env.into_iter().filter(|(k, _)| !is_stamping_key(k, cfg!(windows))).collect() } -/// Mirrors `!i.match(/^npm_/)` at index.js:359 plus the per-call +/// Mirrors the parent-env filter at index.js:359-367 plus the per-call /// stamps we always re-derive (`NODE`, `TMPDIR`, `INIT_CWD`, -/// `PNPM_SCRIPT_SRC_DIR`). Only the `npm_*` prefix is stripped — -/// `pnpm_*` keys (e.g. `PNPM_HOME`, feature flags) are upstream- -/// preserved and pacquet does the same. +/// `PNPM_SCRIPT_SRC_DIR`). A key is stripped when it is: +/// - an `npm_package_*` key (regenerated by [`stamp_package`]), or +/// - a `(npm|pnpm)_config_*` auth key — one whose remainder starts +/// with `_`, `/`, or `@`, or contains `:_` (e.g. `_authToken`, +/// `//registry.npmjs.org/:_password`) — so credentials never leak +/// into dependency lifecycle scripts, or +/// - one of the per-call stamps above. +/// +/// User-defined config such as `npm_config_platform_arch` and `pnpm_*` +/// keys (e.g. `PNPM_HOME`) are preserved, matching upstream. /// /// `is_windows` toggles case-insensitive matching so test code can /// drive both branches without `#[cfg(windows)]` gating the test /// bodies. Production callers pass `cfg!(windows)`. fn is_stamping_key(key: &str, is_windows: bool) -> bool { + if strip_env_prefix(key, "npm_package_", is_windows).is_some() { + return true; + } + if let Some(rest) = strip_env_prefix(key, "npm_config_", is_windows) + .or_else(|| strip_env_prefix(key, "pnpm_config_", is_windows)) + && (rest.starts_with(['_', '/', '@']) || rest.contains(":_")) + { + return true; + } if is_windows { - // Byte-level prefix check: `key[..4]` would panic if byte 4 - // lands inside a multi-byte UTF-8 codepoint (e.g. a key - // containing `𐀀` whose first byte sits at index 0). Since - // the prefix we want is pure ASCII, comparing bytes - // sidesteps the UTF-8 boundary question entirely. - if key.as_bytes().get(..4).is_some_and(|b| b.eq_ignore_ascii_case(b"npm_")) { - return true; - } return ["NODE", "TMPDIR", "INIT_CWD", "PNPM_SCRIPT_SRC_DIR"] .iter() .any(|name| key.eq_ignore_ascii_case(name)); } - if key.starts_with("npm_") { - return true; - } matches!(key, "NODE" | "TMPDIR" | "INIT_CWD" | "PNPM_SCRIPT_SRC_DIR") } +/// Return the slice of `key` after `prefix` when `key` starts with it +/// — case-sensitively on POSIX, case-insensitively on Windows (where +/// `Command::env` collapses key case). Returns `None` otherwise. +/// +/// The Windows branch compares bytes rather than chars so it never +/// panics on a non-ASCII key whose UTF-8 representation crosses the +/// prefix boundary; `prefix` is ASCII, so `prefix.len()` is a valid +/// char boundary whenever the bytes match. +fn strip_env_prefix<'key>(key: &'key str, prefix: &str, is_windows: bool) -> Option<&'key str> { + if is_windows { + if key + .as_bytes() + .get(..prefix.len()) + .is_some_and(|b| b.eq_ignore_ascii_case(prefix.as_bytes())) + { + return key.get(prefix.len()..); + } + return None; + } + key.strip_prefix(prefix) +} + /// Look up the `PATH` value from `env` case-insensitively. On /// Windows the system variable is typically `Path`, not `PATH`; /// returning the value here lets the rest of `build_env` stay diff --git a/pacquet/crates/executor/src/make_env/tests.rs b/pacquet/crates/executor/src/make_env/tests.rs index fd84997c52..9a06534fe2 100644 --- a/pacquet/crates/executor/src/make_env/tests.rs +++ b/pacquet/crates/executor/src/make_env/tests.rs @@ -30,21 +30,33 @@ fn base_opts<'a>( } /// Ports `test('makeEnv')` from -/// . +/// . /// -/// Four invariants we mirror: +/// Invariants we mirror: /// - top-level `npm_package_name` is set from the manifest's `name`, /// - package-local config like `_myPackage` keys are NOT promoted to /// `npm_package_config_*`, -/// - `npm_*` keys leaked from the parent env are stripped (upstream's -/// `!i.match(/^npm_/)` filter at `index.js:359`), +/// - user-defined `npm_config_*` keys (e.g. `npm_config_platform_arch`) +/// leaked from the parent env are PRESERVED, +/// - `(npm|pnpm)_config_*` auth keys (`_auth*`, scope/registry-scoped) +/// are stripped so credentials never leak, +/// - `npm_package_*` keys leaked from the parent env are stripped +/// (they're regenerated from the manifest), /// - everything else passes through — including `pnpm_*` keys like /// `PNPM_HOME`, which upstream does not filter. #[test] -fn make_env_stamps_top_level_keys_and_strips_npm_config_leakage() { +fn make_env_preserves_user_config_and_strips_auth_and_package_leakage() { let mut parent = HashMap::new(); parent.insert("PATH".into(), "/usr/bin".into()); - parent.insert("npm_config_enteente".into(), "should-be-stripped".into()); + parent.insert("npm_config_platform_arch".into(), "x64".into()); + parent.insert("npm_config__auth".into(), "should-not-leak".into()); + parent.insert("npm_config__authToken".into(), "should-not-leak".into()); + parent.insert("npm_config__password".into(), "should-not-leak".into()); + parent.insert("npm_config_//registry.npmjs.org/:_authToken".into(), "should-not-leak".into()); + parent.insert("npm_config_@scope:registry".into(), "https://example.com".into()); + parent.insert("pnpm_config__authToken".into(), "should-not-leak".into()); + parent.insert("pnpm_config_//registry.npmjs.org/:_authToken".into(), "should-not-leak".into()); + parent.insert("npm_package_name".into(), "should-be-regenerated".into()); parent.insert("PNPM_HOME".into(), "/opt/pnpm".into()); parent.insert("HOME".into(), "/home/me".into()); @@ -67,11 +79,27 @@ fn make_env_stamps_top_level_keys_and_strips_npm_config_leakage() { !built.env.contains_key("npm_package__myPackage_secret"), "underscore-prefixed manifest keys must be ignored", ); - assert!( - !built.env.contains_key("npm_config_enteente"), - "npm_config_* must be stripped from parent env: {:?}", + assert_eq!( + built.env.get("npm_config_platform_arch").map(String::as_str), + Some("x64"), + "user-defined npm_config_* vars from the parent env are preserved: {:?}", built.env, ); + for stripped in [ + "npm_config__auth", + "npm_config__authToken", + "npm_config__password", + "npm_config_//registry.npmjs.org/:_authToken", + "npm_config_@scope:registry", + "pnpm_config__authToken", + "pnpm_config_//registry.npmjs.org/:_authToken", + ] { + assert!( + !built.env.contains_key(stripped), + "auth config key {stripped} must be stripped: {:?}", + built.env, + ); + } assert_eq!( built.env.get("PNPM_HOME").map(String::as_str), Some("/opt/pnpm"), @@ -251,15 +279,33 @@ fn sanitize_env_key_matches_upstream_regex() { assert_eq!(sanitize_env_key("npm_package_já"), "npm_package_j_"); } -/// On POSIX, env keys are case-sensitive: `NPM_CONFIG_FOO` is a -/// different variable than `npm_config_foo`, so only the lowercase -/// `npm_` prefix matches — matching upstream's `/^npm_/` regex +/// On POSIX, env keys are case-sensitive: `NPM_PACKAGE_FOO` is a +/// different variable than `npm_package_foo`, so only the lowercase +/// prefixes match — matching upstream's case-sensitive regexes /// exactly. #[test] fn is_stamping_key_is_case_sensitive_on_posix() { - assert!(is_stamping_key("npm_config_user_agent", false)); - assert!(!is_stamping_key("NPM_CONFIG_USER_AGENT", false)); + // npm_package_* are regenerated from the manifest. + assert!(is_stamping_key("npm_package_name", false)); + assert!(!is_stamping_key("NPM_PACKAGE_NAME", false)); + // User-defined config is preserved. + assert!(!is_stamping_key("npm_config_user_agent", false)); + assert!(!is_stamping_key("npm_config_platform_arch", false)); + assert!(!is_stamping_key("pnpm_config_registry", false)); + // Auth config is stripped: remainder starts with `_`/`/`/`@` or + // contains `:_`. + assert!(is_stamping_key("npm_config__auth", false)); + assert!(is_stamping_key("npm_config__authToken", false)); + assert!(is_stamping_key("npm_config_@scope:registry", false)); + assert!(is_stamping_key("npm_config_//registry.npmjs.org/:_authToken", false)); + assert!(is_stamping_key("npm_config_foo:_bar", false)); + assert!(is_stamping_key("pnpm_config__authToken", false)); + assert!(!is_stamping_key("NPM_CONFIG__AUTH", false)); + // Non-package, non-config npm_* keys are preserved (they get + // overwritten by the per-call stamps when relevant). + assert!(!is_stamping_key("npm_lifecycle_event", false)); assert!(!is_stamping_key("Npm_Lifecycle_Event", false)); + // Per-call stamps we always re-derive. assert!(is_stamping_key("NODE", false)); assert!(!is_stamping_key("Node", false)); assert!(!is_stamping_key("node", false)); @@ -291,22 +337,33 @@ fn is_stamping_key_handles_non_ascii_keys_without_panicking() { } /// On Windows, Rust's `Command::env` treats env keys -/// case-insensitively, so `NPM_CONFIG_FOO` and `npm_config_foo` -/// refer to the same variable. We must strip the entire family -/// case-insensitively or our `npm_*` inserts collide at spawn time -/// with an unpredictable winner. +/// case-insensitively, so `NPM_PACKAGE_FOO` and `npm_package_foo` +/// refer to the same variable. We must strip each stamped family +/// case-insensitively or our inserts collide at spawn time with an +/// unpredictable winner. #[test] fn is_stamping_key_is_case_insensitive_on_windows() { - assert!(is_stamping_key("npm_config_user_agent", true)); - assert!(is_stamping_key("NPM_CONFIG_USER_AGENT", true)); - assert!(is_stamping_key("Npm_Lifecycle_Event", true)); + // npm_package_* stripped case-insensitively. + assert!(is_stamping_key("npm_package_name", true)); + assert!(is_stamping_key("NPM_PACKAGE_NAME", true)); + // User-defined config preserved, in any case. + assert!(!is_stamping_key("npm_config_platform_arch", true)); + assert!(!is_stamping_key("NPM_CONFIG_PLATFORM_ARCH", true)); + // Auth config stripped case-insensitively (the prefix is matched + // CI; the `_`/`/`/`@`/`:_` markers are ASCII and case-agnostic). + assert!(is_stamping_key("npm_config__auth", true)); + assert!(is_stamping_key("NPM_CONFIG__AUTH", true)); + assert!(is_stamping_key("npm_config_@scope:registry", true)); + // Non-package, non-config npm_* keys are preserved. + assert!(!is_stamping_key("Npm_Lifecycle_Event", true)); + // Per-call stamps we always re-derive. assert!(is_stamping_key("NODE", true)); assert!(is_stamping_key("Node", true)); assert!(is_stamping_key("node", true)); assert!(is_stamping_key("tmpdir", true)); assert!(is_stamping_key("init_cwd", true)); assert!(is_stamping_key("pnpm_script_src_dir", true)); - // Edge: short keys shouldn't accidentally match `npm_`. + // Edge: short keys shouldn't accidentally match a prefix. assert!(!is_stamping_key("NPM", true)); assert!(!is_stamping_key("npm", true)); // pnpm_* keys other than the well-known stamps still survive. diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a96d0e8bc6..1f528c1c72 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -354,8 +354,8 @@ catalogs: specifier: ^0.3.1 version: 0.3.1 '@pnpm/npm-lifecycle': - specifier: 1100.0.0-1 - version: 1100.0.0-1 + specifier: ^1100.0.0 + version: 1100.0.0 '@pnpm/npm-package-arg': specifier: ^2.0.0 version: 2.0.0 @@ -4383,7 +4383,7 @@ importers: version: link:../../fetching/directory-fetcher '@pnpm/npm-lifecycle': specifier: 'catalog:' - version: 1100.0.0-1(typanion@3.14.0) + version: 1100.0.0(typanion@3.14.0) '@pnpm/pkg-manifest.reader': specifier: workspace:* version: link:../../pkg-manifest/reader @@ -11646,8 +11646,8 @@ packages: resolution: {integrity: sha512-5jW/GNLdZMiw+PJ8FYSvOghoApSjsORNIro2fj8j6NHAqJxJjcHekC5/NsKaawoI5LAkU/XDDVjNC71Yz+uS1w==} engines: {node: '>=18.12'} - '@pnpm/npm-lifecycle@1100.0.0-1': - resolution: {integrity: sha512-sqXZFikFaJfwY8K+Gg9oPMxxYnJ9O7OkMxXAWNPBUEiZh8XK/DaNFlbNOJ3X8P0WKS5nDE9A6TiOBWrhTrpS+A==} + '@pnpm/npm-lifecycle@1100.0.0': + resolution: {integrity: sha512-igdq1MDnShKMFLdA9uBkMZ8lI/65FUMeh7mjgd1x710qjk9JtF8bV2iYOs/hbBz6IFo98adyQvg1Trx1n5dwIw==} engines: {node: '>=22.13'} '@pnpm/npm-package-arg@2.0.0': @@ -19234,7 +19234,7 @@ snapshots: - supports-color - typanion - '@pnpm/npm-lifecycle@1100.0.0-1(typanion@3.14.0)': + '@pnpm/npm-lifecycle@1100.0.0(typanion@3.14.0)': dependencies: '@pnpm/byline': 1.0.0 '@pnpm/error': 1000.1.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index d6bca743d8..3c62d6b603 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -91,7 +91,7 @@ catalog: '@pnpm/logger': '^1100.0.0' '@pnpm/meta-updater': 2.0.6 '@pnpm/nopt': ^0.3.1 - '@pnpm/npm-lifecycle': 1100.0.0-1 + '@pnpm/npm-lifecycle': ^1100.0.0 '@pnpm/npm-package-arg': ^2.0.0 '@pnpm/os.env.path-extender': ^3.0.1 '@pnpm/patch-package': 0.0.1