mirror of
https://github.com/pnpm/pnpm.git
synced 2026-06-29 18:35:18 -04:00
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 <z@kochan.io>
This commit is contained in:
6
.changeset/preserve-user-npm-config-vars.md
Normal file
6
.changeset/preserve-user-npm-config-vars.md
Normal file
@@ -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).
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"
|
||||
5
exec/lifecycle/test/fixtures/inspect-npm-config-env/postinstall.js
vendored
Normal file
5
exec/lifecycle/test/fixtures/inspect-npm-config-env/postinstall.js
vendored
Normal file
@@ -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`)
|
||||
}
|
||||
}
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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
|
||||
/// <https://github.com/pnpm/pnpm/blob/b4f8f47ac2/exec/lifecycle/test/index.ts#L65-L77>
|
||||
/// `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
|
||||
/// <https://github.com/pnpm/pnpm/blob/b4f8f47ac2/exec/lifecycle/test/index.ts#L65-L82>
|
||||
/// 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<std::ffi::OsString>,
|
||||
}
|
||||
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");
|
||||
|
||||
@@ -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
|
||||
/// <https://github.com/pnpm/npm-lifecycle/blob/d2d8e790/index.js#L52-L113>
|
||||
/// `lifecycle()` from `@pnpm/npm-lifecycle@9e2ac78148` at
|
||||
/// <https://github.com/pnpm/npm-lifecycle/blob/9e2ac78148/index.js#L52-L113>
|
||||
/// plus the wrapper's `extraEnv` additions at
|
||||
/// <https://github.com/pnpm/pnpm/blob/b4f8f47ac2/exec/lifecycle/src/runLifecycleHook.ts#L119-L124>.
|
||||
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<String, String>,
|
||||
) -> 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<String, String>) -> HashMap<String, String> {
|
||||
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
|
||||
|
||||
@@ -30,21 +30,33 @@ fn base_opts<'a>(
|
||||
}
|
||||
|
||||
/// Ports `test('makeEnv')` from
|
||||
/// <https://github.com/pnpm/npm-lifecycle/blob/d2d8e790/test/index.js#L97-L124>.
|
||||
/// <https://github.com/pnpm/npm-lifecycle/blob/9e2ac78148/test/index.js#L97-L153>.
|
||||
///
|
||||
/// 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.
|
||||
|
||||
12
pnpm-lock.yaml
generated
12
pnpm-lock.yaml
generated
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user