Skip to content

Commit 2e234c9

Browse files
committed
fix(setup): bypass pnpm minimum release age without prompting
1 parent d3edacc commit 2e234c9

4 files changed

Lines changed: 16 additions & 174 deletions

File tree

crates/vp_global_cli/src/commands/upgrade/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@ async fn install_platform_and_main(
199199
install::generate_wrapper_package_json(version_dir, new_version).await?;
200200

201201
// Install production dependencies (pnpm installs vite-plus + all transitive deps)
202-
install::install_production_deps(version_dir, registry, silent, new_version).await?;
202+
install::install_production_deps(version_dir, registry).await?;
203203

204204
// Save previous version for rollback
205205
let previous_version = install::save_previous_version(install_dir).await?;

crates/vp_global_cli/src/self_setup.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -141,8 +141,7 @@ async fn run(source: &Path) -> Result<(), Error> {
141141
output::info(&format!("installing vite-plus@{version}..."));
142142
install::generate_wrapper_package_json(&version_dir, version).await?;
143143
if !skip_deps {
144-
install::install_production_deps(&version_dir, registry, !interactive(), version)
145-
.await?;
144+
install::install_production_deps(&version_dir, registry).await?;
146145
}
147146
}
148147
#[cfg(windows)]

crates/vp_installer/src/legacy.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,8 +137,7 @@ async fn install_new_version(
137137
if !opts.quiet {
138138
print_info("installing dependencies (this may take a moment)...");
139139
}
140-
install::install_production_deps(version_dir, opts.registry.as_deref(), opts.yes, version)
141-
.await?;
140+
install::install_production_deps(version_dir, opts.registry.as_deref()).await?;
142141

143142
let previous_version =
144143
if has_previous { install::save_previous_version(install_dir).await? } else { None };

crates/vp_setup/src/install.rs

Lines changed: 13 additions & 169 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
66
use std::{
77
env,
8-
io::{Cursor, Read as _, Write as _},
8+
io::{Cursor, Read as _},
99
path::Path,
1010
process::{self, Output},
1111
time::{SystemTime, UNIX_EPOCH},
@@ -136,92 +136,11 @@ pub async fn write_release_age_overrides(version_dir: &AbsolutePath) -> Result<(
136136
Ok(())
137137
}
138138

139-
fn is_affirmative_response(input: &str) -> bool {
140-
matches!(input.trim().to_ascii_lowercase().as_str(), "y" | "yes")
141-
}
142-
143-
fn should_prompt_release_age_override(silent: bool) -> bool {
144-
!silent && vp_shared::is_stdin_terminal() && vp_shared::is_stderr_terminal()
145-
}
146-
147-
fn prompt_release_age_override(version: &str) -> bool {
148-
eprintln!();
149-
eprintln!("warn: Your minimumReleaseAge setting prevented installing vite-plus@{version}.");
150-
eprintln!("This setting helps protect against newly published compromised packages.");
151-
eprintln!("Proceeding will disable this protection for this Vite+ install only.");
152-
eprint!("Do you want to proceed? (y/N): ");
153-
if std::io::stderr().flush().is_err() {
154-
return false;
155-
}
156-
157-
let mut input = String::new();
158-
if std::io::stdin().read_line(&mut input).is_err() {
159-
return false;
160-
}
161-
162-
is_affirmative_response(&input)
163-
}
164-
165-
fn is_release_age_error(stdout: &[u8], stderr: &[u8]) -> bool {
166-
let output =
167-
format!("{}\n{}", String::from_utf8_lossy(stdout), String::from_utf8_lossy(stderr));
168-
let lower = output.to_ascii_lowercase();
169-
170-
// This wrapper install path is pinned to pnpm via packageManager, so this
171-
// detection follows pnpm's resolver/reporter output rather than npm/yarn.
172-
//
173-
// pnpm's PnpmError prefixes internal codes with ERR_PNPM_, so
174-
// `NO_MATURE_MATCHING_VERSION` becomes `ERR_PNPM_NO_MATURE_MATCHING_VERSION`
175-
// in CLI output. We still match the unprefixed code as a fallback in case
176-
// future reporter/log output includes the raw internal code.
177-
// https://github.com/pnpm/pnpm/blob/16cfde66ec71125d692ea828eba2a5f9b3cc54fc/core/error/src/index.ts#L18-L20
178-
//
179-
// npm-resolver chooses NO_MATURE_MATCHING_VERSION when
180-
// publishedBy/minimumReleaseAge rejects a matching version, and uses the
181-
// "does not meet the minimumReleaseAge constraint" message.
182-
// https://github.com/pnpm/pnpm/blob/16cfde66ec71125d692ea828eba2a5f9b3cc54fc/resolving/npm-resolver/src/index.ts#L76-L84
183-
//
184-
// default-reporter handles both ERR_PNPM_NO_MATURE_MATCHING_VERSION and
185-
// ERR_PNPM_NO_MATCHING_VERSION, and may append guidance mentioning
186-
// minimumReleaseAgeExclude when the error has an immatureVersion.
187-
// https://github.com/pnpm/pnpm/blob/16cfde66ec71125d692ea828eba2a5f9b3cc54fc/cli/default-reporter/src/reportError.ts#L163-L164
188-
//
189-
// pnpm itself notes that NO_MATCHING_VERSION can also happen under
190-
// minimumReleaseAge when all candidate versions are newer than the threshold.
191-
// Because it is also used for real missing versions, we only treat it as
192-
// release-age related when accompanied by the age-gate text below.
193-
// https://github.com/pnpm/pnpm/blob/16cfde66ec71125d692ea828eba2a5f9b3cc54fc/deps/inspection/outdated/src/createManifestGetter.ts#L66-L76
194-
//
195-
// minimum-release-age is the pnpm .npmrc key; npm's min-release-age is
196-
// intentionally not treated as a pnpm signal here.
197-
// https://github.com/pnpm/pnpm/blob/16cfde66ec71125d692ea828eba2a5f9b3cc54fc/config/reader/src/types.ts#L73-L74
198-
let has_release_age_text = output.contains("does not meet the minimumReleaseAge constraint")
199-
|| output.contains("minimumReleaseAge")
200-
|| output.contains("minimumReleaseAgeExclude")
201-
|| lower.contains("minimum release age")
202-
|| lower.contains("minimum-release-age");
203-
204-
output.contains("ERR_PNPM_NO_MATURE_MATCHING_VERSION")
205-
|| output.contains("NO_MATURE_MATCHING_VERSION")
206-
|| (output.contains("ERR_PNPM_NO_MATCHING_VERSION") && has_release_age_text)
207-
|| has_release_age_text
208-
}
209-
210-
fn format_install_failure_message(
211-
exit_code: i32,
212-
log_path: Option<&AbsolutePathBuf>,
213-
release_age_blocked: bool,
214-
) -> String {
139+
fn format_install_failure_message(exit_code: i32, log_path: Option<&AbsolutePathBuf>) -> String {
215140
let log_msg = log_path
216141
.map_or_else(String::new, |p| format!(". See log for details: {}", p.as_path().display()));
217142

218-
if release_age_blocked {
219-
format!(
220-
"Upgrade blocked by your minimumReleaseAge setting. Wait until the package is old enough or adjust your package manager configuration explicitly{log_msg}"
221-
)
222-
} else {
223-
format!("Failed to install production dependencies (exit code: {exit_code}){log_msg}")
224-
}
143+
format!("Failed to install production dependencies (exit code: {exit_code}){log_msg}")
225144
}
226145

227146
/// Write stdout and stderr from a failed install to `upgrade.log`.
@@ -257,15 +176,12 @@ pub async fn write_upgrade_log(
257176
pub async fn install_production_deps(
258177
version_dir: &AbsolutePath,
259178
registry: Option<&str>,
260-
silent: bool,
261-
new_version: &str,
262179
) -> Result<(), Error> {
263180
tracing::debug!("Running pnpm install in {}", version_dir.as_path().display());
264181

265-
// Do not pass `--silent` to the inner install: pnpm suppresses the
266-
// release-age error body in silent mode, which would leave upgrade.log
267-
// empty and make the release-age gate impossible to detect. This outer
268-
// process captures the output and only surfaces it through the log.
182+
// Keep the bypass local to this Vite+ installation.
183+
write_release_age_overrides(version_dir).await?;
184+
269185
let mut args = vec!["install"];
270186
if let Some(registry_url) = registry {
271187
args.push("--registry");
@@ -297,48 +213,13 @@ pub async fn install_production_deps(
297213

298214
if !output.status.success() {
299215
let log_path = write_upgrade_log(version_dir, &output.stdout, &output.stderr).await;
300-
let release_age_blocked = is_release_age_error(&output.stdout, &output.stderr);
301-
302-
if !release_age_blocked {
303-
return Err(Error::Setup(
304-
format_install_failure_message(
305-
vp_shared::exit_code_from_status(output.status),
306-
log_path.as_ref(),
307-
false,
308-
)
309-
.into(),
310-
));
311-
}
312-
313-
if !should_prompt_release_age_override(silent) || !prompt_release_age_override(new_version)
314-
{
315-
return Err(Error::Setup(
316-
format_install_failure_message(
317-
vp_shared::exit_code_from_status(output.status),
318-
log_path.as_ref(),
319-
true,
320-
)
321-
.into(),
322-
));
323-
}
324-
325-
// Only create the local override after explicit consent. This preserves
326-
// minimumReleaseAge protection for the default and non-interactive paths.
327-
write_release_age_overrides(version_dir).await?;
328-
let retry_output =
329-
run_pnpm_install(version_dir, &node_runtime, &pnpm_entry, &args, registry).await?;
330-
if !retry_output.status.success() {
331-
let retry_log_path =
332-
write_upgrade_log(version_dir, &retry_output.stdout, &retry_output.stderr).await;
333-
return Err(Error::Setup(
334-
format_install_failure_message(
335-
vp_shared::exit_code_from_status(retry_output.status),
336-
retry_log_path.as_ref(),
337-
false,
338-
)
339-
.into(),
340-
));
341-
}
216+
return Err(Error::Setup(
217+
format_install_failure_message(
218+
vp_shared::exit_code_from_status(output.status),
219+
log_path.as_ref(),
220+
)
221+
.into(),
222+
));
342223
}
343224

344225
Ok(())
@@ -1021,41 +902,4 @@ mod tests {
1021902
let registry = tokio::fs::read_to_string(version_dir.join("registry.txt")).await.unwrap();
1022903
assert_eq!(registry, registry_url);
1023904
}
1024-
1025-
#[test]
1026-
fn test_is_release_age_error_detects_pnpm_no_mature_code() {
1027-
assert!(is_release_age_error(
1028-
b"",
1029-
b"ERR_PNPM_NO_MATURE_MATCHING_VERSION Version 0.1.16 of vite-plus does not meet the minimumReleaseAge constraint",
1030-
));
1031-
}
1032-
1033-
#[test]
1034-
fn test_is_release_age_error_detects_minimum_release_age_message() {
1035-
assert!(is_release_age_error(
1036-
b"",
1037-
b"Version 0.1.16 (released just now) of vite-plus does not meet the minimumReleaseAge constraint",
1038-
));
1039-
}
1040-
1041-
#[test]
1042-
fn test_is_release_age_error_detects_no_matching_with_release_age_context() {
1043-
assert!(is_release_age_error(
1044-
b"",
1045-
b"ERR_PNPM_NO_MATCHING_VERSION No matching version found. Add the package name to minimumReleaseAgeExclude if you want to ignore the time it was published.",
1046-
));
1047-
}
1048-
1049-
#[test]
1050-
fn test_is_release_age_error_ignores_plain_no_matching_version() {
1051-
assert!(!is_release_age_error(
1052-
b"",
1053-
b"ERR_PNPM_NO_MATCHING_VERSION No matching version found for vite-plus@999.999.999",
1054-
));
1055-
}
1056-
1057-
#[test]
1058-
fn test_is_release_age_error_ignores_npm_min_release_age() {
1059-
assert!(!is_release_age_error(b"", b"min-release-age prevented installing vite-plus",));
1060-
}
1061905
}

0 commit comments

Comments
 (0)