mirror of
https://github.com/pnpm/pnpm.git
synced 2026-06-29 18:35:18 -04:00
perf: close repeat-install and warm-resolve gaps (lazy lockfile, pre-runtime fast path, 304 freshness renewal) (#12364)
* perf(cli): keep the repeat-install fast path off the lockfile parse and thread spawns The "Already up to date" short-circuit decides from manifest mtimes alone (mirroring upstream checkDepsStatus, which never reads the wanted lockfile), yet pacquet parsed pnpm-lock.yaml eagerly in State::init before the check ran — a multi-millisecond YAML parse on every no-op install, scaling with lockfile size (babylon's 720 KB lockfile dominated its repeat-install wall time). pnpm-lock.yaml now loads through a LazyLockfile (OnceLock-backed) that install forces only after the fast path has passed on the run; add / update / remove / outdated / the pnpr path force it up front, keeping their behavior unchanged. The repeat-install regenerate branch probes for the file's existence instead of its parsed contents, so the fast path stays mtime-cheap. The rayon global pool is likewise no longer built eagerly at startup: the worker count is published via RAYON_NUM_THREADS (set in fn main while the process is still single-threaded) so the pool spawns lazily on first parallel use — commands that never reach a parallel phase no longer pay 2×CPUs thread spawns. A corrupt lockfile now surfaces its parse error when the install actually reads the file; an up-to-date project with an unreadable lockfile reports "Already up to date" exactly as pnpm does. * perf(cli): finish up-to-date installs before building the async runtime A plain `pacquet install` that is already up to date now completes on the main thread, before the tokio runtime, the rayon pool, the HTTP client, or any install state exists. The new `install_already_up_to_date` twin of the repeat-install short-circuit reuses the exact same workspace discovery and `check_optimistic_repeat_install` inputs as `Install::run`, and the CLI invokes it from the (now synchronous) `main` after clap parsing. Gates mirror everything that would make the full path behave differently: `--frozen-lockfile` / `--lockfile-only`, a configured pnpr server (that path never runs the optimistic check), `--recursive` / `--filter`, config dependencies, and pnpmfile updateConfig hooks (both can mutate the config the check compares against). Any gate or error falls through to the full install path, which re-runs the check and reproduces failures with their established error shape; the "Already up to date" + summary emissions are byte-identical. Repeat-install instruction count on the vue fixture drops from ~203M to ~41M retired instructions — within a rounding error of the `pacquet --version` floor (~39M). * perf(resolving): renew metadata-mirror freshness on 304 Not Modified The minimumReleaseAge freshness shortcut treats a metadata mirror younger than the cutoff as authoritative and resolves without touching the network. But a 304 revalidation never rewrote the mirror file, so its mtime froze at the last 200 response: once a cached packument grew older than minimumReleaseAge (24h by default), every subsequent install re-validated every package against the registry, forever. A 304 proves the cached packument equals the registry's current document, so the validation clock legitimately restarts at the response: bump the mirror's mtime to now (fire-and-forget — a read-only cache dir only costs the next install another conditional request). Applied to both stacks: pnpm's pickPackage notModified branch and pacquet's fetch_full_metadata_cached 304 path. On a vue-fixture install with a stale cache, the second warm resolve drops from ~2s (520 conditional requests) to ~250ms (zero requests). * style(resolving): use clippy's preferred Duration units in the 304 mtime test CI clippy denies duration_suboptimal_units; from_hours / from_mins replace the hand-multiplied from_secs values. * style(package-manager): use clippy's preferred Duration unit in the sync fast-path test Same duration_suboptimal_units deny as the previous commit, one site CI's clippy surfaced after it stopped at the first failing crate. * fix(lockfile): address review — dir-addressed LazyLockfile, read-open 304 touch LazyLockfile resolved pnpm-lock.yaml against the process cwd while the CLI honours a canonicalized --dir without chdir, so the deferred load and the existence probe could consult a different lockfile than the rest of the install (which derives lockfile_path from the manifest's directory). The lazy handle now carries the manifest's directory and loads via load_wanted_from_dir; lockfile-disabled config gets an explicit disabled() constructor. The pre-runtime fast path builds its handle from the same directory, keeping verdict parity with Install::run. renew_mirror_freshness opened the mirror with append just to bump the mtime; set_modified only needs a file handle plus ownership (futimens semantics), so a plain read-open also covers mirrors whose mode dropped write permission. * test(integrated-benchmark): compare best-of-N samples in the slow-start proxy test The test raced two single wall-clock samples, and a loaded CI runner can inflate the ramped-vs-flat comparison in either direction (macOS runner measured flat 318ms vs ramped 304ms against a ~66ms model). Scheduler stalls only ever inflate a sample, while slow start's ramp overhead is structural and survives in every sample — so the minimum of several runs per side is the noise-resistant estimator. * chore(deps): bump esbuild to 0.28.1 to clear GHSA-gv7w-rqvm-qjhr The new advisory (install-module RCE via NPM_CONFIG_REGISTRY, patched in 0.28.1) fails the audit gate. 0.28.1 was published within the minimumReleaseAge window, so the patched version is excluded from the age gate — the same mechanism pnpm audit --fix uses — including its '@esbuild/*' platform packages, whose versions move in lockstep with the root package. * fix(lockfile): make the unloaded presence probe match the loader's absence rules The loader treats an empty file and an env-only combined document as an absent wanted lockfile (Ok(None)), but is_loaded_or_on_disk probed bare file existence, so the repeat-install path could skip restoring a semantically-missing pnpm-lock.yaml. The probe now reads the file and checks the main document is non-empty (Lockfile::wanted_exists_in_dir) — the loader's exact absence rules, still without the YAML parse. * fix(cli): build the rayon pool after the fast-path gate instead of injecting env Publishing the worker count through RAYON_NUM_THREADS leaked the variable into every child process the install spawns — lifecycle scripts, node probes, git — and pnpm exposes no such variable to scripts. Build the global pool with ThreadPoolBuilder again, but only once the repeat-install fast path has declined: real installs pay exactly the cost they always did, the no-op path still spawns no workers, and the process environment stays untouched (which also drops the unsafe set_var and its single-threaded contract). * fix(lockfile): treat only NotFound as absence in the presence probe A permission or I/O failure reading pnpm-lock.yaml reported the file as absent, which would send the repeat-install path into the regenerate-on-missing branch — overwriting an existing lockfile it merely could not read. Only NotFound counts as absent now; any other read failure reports presence, and the real load surfaces the underlying error when the contents are actually needed. * fix(resolving): open the mirror write-capable for the 304 touch, read-only as fallback Windows' set_modified requires write-attributes access on the handle, so the read-only open silently failed there (caught by the Windows CI run of a_304_renews_the_mirror_mtime). Append-mode open carries that access; the read-only fallback still covers Unix mirrors whose mode dropped write permission, where timestamp syscalls need ownership rather than write access. * fix(package-manager): never short-circuit partial installs as already up to date add and remove mutate the manifest in memory and persist it only after Install::run returns, so the on-disk mtimes the optimistic repeat-install check reads still describe the pre-mutation project. With a fresh workspace state, `pacquet add X` right after a clean install reported "Already up to date", skipped the entire install, and then saved a package.json declaring a dependency that was never resolved, lockfiled, or materialized (self-healing on the next run, which sees the newer manifest mtime). Gate the short-circuit on is_full_install, mirroring upstream installDeps calling checkDepsStatus only for the plain-install mutation, never for installSome / uninstallSome. The new partial_install_disables_optimistic_short_circuit test fails without the gate. The bug predates this PR (the KeepAll gate has carried add since the optimistic path landed) — surfaced by CodeRabbit review on pnpm/pnpm#12364.
This commit is contained in:
6
.changeset/spotty-citrus-fix.md
Normal file
6
.changeset/spotty-citrus-fix.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@pnpm/resolving.npm-resolver": patch
|
||||
"pnpm": patch
|
||||
---
|
||||
|
||||
A `304 Not Modified` answer from the registry now renews the cached metadata file's mtime, so the `minimumReleaseAge` freshness shortcut keeps serving resolutions from the cache. Previously, once a cached packument grew older than `minimumReleaseAge`, every subsequent install re-validated it against the registry forever, because a 304 never rewrites the file.
|
||||
@@ -1,4 +1,3 @@
|
||||
#[tokio::main(flavor = "multi_thread")]
|
||||
pub async fn main() -> miette::Result<()> {
|
||||
pacquet_cli::main().await
|
||||
pub fn main() -> miette::Result<()> {
|
||||
pacquet_cli::main()
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ use outdated::{OutdatedArgs, OutdatedOutcome};
|
||||
use pacquet_config::{Config, Host};
|
||||
use pacquet_executor::execute_shell;
|
||||
use pacquet_package_manifest::PackageManifest;
|
||||
use pacquet_reporter::{NdjsonReporter, SilentReporter};
|
||||
use pacquet_reporter::{NdjsonReporter, Reporter, SilentReporter};
|
||||
use remove::RemoveArgs;
|
||||
use run::RunArgs;
|
||||
use std::path::PathBuf;
|
||||
@@ -137,6 +137,40 @@ pub enum CliCommand {
|
||||
}
|
||||
|
||||
impl CliArgs {
|
||||
/// Try to finish `pacquet install` synchronously through the
|
||||
/// repeat-install fast path, before the caller builds the async
|
||||
/// runtime. `true` means the install completed (the "Already up to
|
||||
/// date" events were emitted); `false` means undecided — proceed
|
||||
/// with [`Self::run`], which loads its own config and re-runs the
|
||||
/// same check.
|
||||
///
|
||||
/// Mirrors the install arm of [`Self::run`]'s dispatch: the same
|
||||
/// canonicalized `--dir`, the same config layering (`.npmrc` auth
|
||||
/// file seed + `--config.<key>` overrides). Workspace-filtered and
|
||||
/// recursive installs always take the full path.
|
||||
pub fn finished_via_install_fast_path(&self, config_overrides: &ConfigOverrides) -> bool {
|
||||
let CliCommand::Install(install_args) = &self.command else {
|
||||
return false;
|
||||
};
|
||||
if self.recursive || !self.filter.is_empty() || !self.filter_prod.is_empty() {
|
||||
return false;
|
||||
}
|
||||
let Ok(dir) = dunce::canonicalize(&self.dir) else {
|
||||
return false;
|
||||
};
|
||||
let loaded = Config { npmrc_auth_file: self.npmrc_auth_file.clone(), ..Config::default() }
|
||||
.current::<Host>(&dir);
|
||||
let Ok(mut config) = loaded else {
|
||||
return false;
|
||||
};
|
||||
config_overrides.apply(&mut config);
|
||||
let emit = match self.reporter {
|
||||
ReporterType::Ndjson => NdjsonReporter::emit as fn(&pacquet_reporter::LogEvent),
|
||||
ReporterType::Silent => SilentReporter::emit as fn(&pacquet_reporter::LogEvent),
|
||||
};
|
||||
install_args.finished_via_up_to_date_fast_path(&dir, &config, emit)
|
||||
}
|
||||
|
||||
/// Execute the command. `config_overrides` carries `--config.<key>=<value>`
|
||||
/// tokens already stripped from argv by [`ConfigOverrides::extract`];
|
||||
/// they're layered on top of `.npmrc` / `pnpm-workspace.yaml` whenever
|
||||
|
||||
@@ -194,6 +194,8 @@ where
|
||||
// TODO: if a package already exists in another dependency group, don't remove the existing entry.
|
||||
let State { tarball_mem_cache, http_client, config, manifest, lockfile, resolved_packages } =
|
||||
&mut state;
|
||||
let lockfile =
|
||||
lockfile.get().map_err(|err| miette::Report::new(err).wrap_err("load the lockfile"))?;
|
||||
|
||||
let lockfile_path =
|
||||
manifest.path().parent().map(|parent| parent.join(pacquet_lockfile::Lockfile::FILE_NAME));
|
||||
@@ -203,7 +205,7 @@ where
|
||||
http_client_arc: std::sync::Arc::clone(http_client),
|
||||
config,
|
||||
manifest,
|
||||
lockfile: lockfile.as_ref(),
|
||||
lockfile,
|
||||
lockfile_path: lockfile_path.as_deref(),
|
||||
list_dependency_groups,
|
||||
package_name,
|
||||
|
||||
@@ -3,10 +3,10 @@ use clap::{Args, ValueEnum};
|
||||
use derive_more::{Display, Error};
|
||||
use miette::{Context, Diagnostic};
|
||||
use pacquet_config::NodeLinker;
|
||||
use pacquet_lockfile::{Lockfile, LockfileResolution};
|
||||
use pacquet_lockfile::{Lockfile, LockfileResolution, MaybeLazyLockfile};
|
||||
use pacquet_package_manager::{
|
||||
Install, InstallFrozenLockfileError, LockfileVerificationOverride, TarballPrefetcher,
|
||||
UpdateSeedPolicy,
|
||||
UpToDateFastPathCheck, UpdateSeedPolicy, install_already_up_to_date,
|
||||
};
|
||||
use pacquet_package_manifest::DependencyGroup;
|
||||
use pacquet_pnpr_client::{PnprClient, PnprClientError, ResolveOptions, VerifyLockfileOptions};
|
||||
@@ -255,6 +255,73 @@ pub struct InstallArgs {
|
||||
}
|
||||
|
||||
impl InstallArgs {
|
||||
/// Run the repeat-install fast path before any of the async install
|
||||
/// machinery exists: when every gate below holds and
|
||||
/// [`install_already_up_to_date`] confirms nothing changed since the
|
||||
/// previous install, emit the same "Already up to date" + summary
|
||||
/// events [`Install::run`] would and report the install as finished.
|
||||
///
|
||||
/// The gates mirror the dispatch in [`crate::cli_args::CliArgs::run`]
|
||||
/// (which checks `recursive` / `filter` before reaching here) plus
|
||||
/// every input that would make [`Install::run`] skip its own
|
||||
/// short-circuit or do extra pre-install work: an explicit
|
||||
/// `--frozen-lockfile` / `--lockfile-only`, a configured pnpr
|
||||
/// server (that path never runs the optimistic check), config
|
||||
/// dependencies, and pnpmfile `updateConfig` hooks (both can
|
||||
/// mutate the config the check compares against).
|
||||
///
|
||||
/// `false` means "not decided" — the caller proceeds with the full
|
||||
/// install path, which re-runs the same check cheaply and
|
||||
/// reproduces any error with its established shape.
|
||||
pub fn finished_via_up_to_date_fast_path(
|
||||
&self,
|
||||
dir: &std::path::Path,
|
||||
config: &pacquet_config::Config,
|
||||
emit: fn(&pacquet_reporter::LogEvent),
|
||||
) -> bool {
|
||||
if self.frozen_lockfile || self.lockfile_only || self.pnpr_server.is_some() {
|
||||
return false;
|
||||
}
|
||||
if config.pnpr_server.is_some() {
|
||||
return false;
|
||||
}
|
||||
if config.config_dependencies.as_ref().is_some_and(|deps| !deps.is_empty()) {
|
||||
return false;
|
||||
}
|
||||
let config_root = config.workspace_dir.clone().unwrap_or_else(|| dir.to_path_buf());
|
||||
if pacquet_hooks::finder::find_pnpmfile(&config_root).is_some() {
|
||||
return false;
|
||||
}
|
||||
let manifest_path = dir.join("package.json");
|
||||
if !manifest_path.is_file() {
|
||||
return false;
|
||||
}
|
||||
let Ok(manifest) = pacquet_package_manifest::PackageManifest::from_path(manifest_path)
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
let node_linker = self.node_linker.map_or(config.node_linker, NodeLinkerArg::into_config);
|
||||
let Some(workspace_root) = install_already_up_to_date(&UpToDateFastPathCheck {
|
||||
config,
|
||||
manifest: &manifest,
|
||||
dependency_groups: self.dependency_options.dependency_groups().collect(),
|
||||
node_linker,
|
||||
}) else {
|
||||
return false;
|
||||
};
|
||||
let prefix = workspace_root.to_string_lossy().into_owned();
|
||||
emit(&pacquet_reporter::LogEvent::Pnpm(pacquet_reporter::PnpmLog {
|
||||
level: pacquet_reporter::LogLevel::Info,
|
||||
message: "Already up to date".to_string(),
|
||||
prefix: prefix.clone(),
|
||||
}));
|
||||
emit(&pacquet_reporter::LogEvent::Summary(pacquet_reporter::SummaryLog {
|
||||
level: pacquet_reporter::LogLevel::Debug,
|
||||
prefix,
|
||||
}));
|
||||
true
|
||||
}
|
||||
|
||||
pub async fn run<Reporter: self::Reporter + 'static>(self, state: State) -> miette::Result<()> {
|
||||
let State { tarball_mem_cache, http_client, config, manifest, lockfile, resolved_packages } =
|
||||
&state;
|
||||
@@ -362,7 +429,7 @@ impl InstallArgs {
|
||||
http_client_arc: std::sync::Arc::clone(http_client),
|
||||
config,
|
||||
manifest,
|
||||
lockfile: lockfile.as_ref(),
|
||||
lockfile: MaybeLazyLockfile::Lazy(lockfile),
|
||||
lockfile_path: lockfile_path.as_deref(),
|
||||
dependency_groups: dependency_options.dependency_groups(),
|
||||
frozen_lockfile,
|
||||
@@ -541,7 +608,11 @@ async fn install_via_pnpr<Reporter: self::Reporter + 'static>(
|
||||
.collect(),
|
||||
authorization: state.config.auth_headers.for_url(pnpr_server),
|
||||
overrides,
|
||||
lockfile: state.lockfile.clone(),
|
||||
lockfile: state
|
||||
.lockfile
|
||||
.get()
|
||||
.map_err(|err| miette::Report::new(err).wrap_err("load the lockfile"))?
|
||||
.cloned(),
|
||||
frozen_lockfile: link.frozen_lockfile,
|
||||
prefer_frozen_lockfile: Some(link.prefer_frozen_lockfile),
|
||||
ignore_manifest_check: link.ignore_manifest_check,
|
||||
@@ -562,7 +633,10 @@ async fn install_via_pnpr<Reporter: self::Reporter + 'static>(
|
||||
|
||||
if link.frozen_lockfile
|
||||
&& !link.lockfile_only
|
||||
&& let Some(lockfile) = state.lockfile.as_ref()
|
||||
&& let Some(lockfile) = state
|
||||
.lockfile
|
||||
.get()
|
||||
.map_err(|err| miette::Report::new(err).wrap_err("load the lockfile"))?
|
||||
{
|
||||
let prefetcher = TarballPrefetcher::new(
|
||||
state.config,
|
||||
@@ -601,7 +675,7 @@ async fn install_via_pnpr<Reporter: self::Reporter + 'static>(
|
||||
http_client_arc: std::sync::Arc::clone(&state.http_client),
|
||||
config: state.config,
|
||||
manifest: &state.manifest,
|
||||
lockfile: Some(lockfile),
|
||||
lockfile: MaybeLazyLockfile::Loaded(Some(lockfile)),
|
||||
lockfile_path: link.lockfile_path,
|
||||
dependency_groups: link.dependency_groups,
|
||||
frozen_lockfile: true,
|
||||
@@ -721,7 +795,7 @@ async fn install_via_pnpr<Reporter: self::Reporter + 'static>(
|
||||
http_client_arc: std::sync::Arc::clone(&state.http_client),
|
||||
config: state.config,
|
||||
manifest: &state.manifest,
|
||||
lockfile: Some(&outcome.lockfile),
|
||||
lockfile: MaybeLazyLockfile::Loaded(Some(&outcome.lockfile)),
|
||||
lockfile_path: link.lockfile_path,
|
||||
dependency_groups: link.dependency_groups,
|
||||
frozen_lockfile: true,
|
||||
|
||||
@@ -326,7 +326,10 @@ impl OutdatedArgs {
|
||||
|
||||
let config = state.config;
|
||||
let manifest = &state.manifest;
|
||||
let lockfile = &state.lockfile;
|
||||
let lockfile = state
|
||||
.lockfile
|
||||
.get()
|
||||
.map_err(|err| miette::Report::new(err).wrap_err("load the lockfile"))?;
|
||||
let http_client = &state.http_client;
|
||||
|
||||
// A manifest with no dependencies at all is reported as up to date
|
||||
@@ -361,7 +364,7 @@ impl OutdatedArgs {
|
||||
include_deprecated: true,
|
||||
};
|
||||
let mut outdated =
|
||||
collect_outdated(manifest, lockfile.as_ref(), config, http_client, &query).await?;
|
||||
collect_outdated(manifest, lockfile, config, http_client, &query).await?;
|
||||
|
||||
sort_outdated(&mut outdated, self.sort_by);
|
||||
|
||||
|
||||
@@ -60,6 +60,8 @@ impl RemoveArgs {
|
||||
) -> miette::Result<()> {
|
||||
let State { tarball_mem_cache, http_client, config, manifest, lockfile, resolved_packages } =
|
||||
&mut state;
|
||||
let lockfile =
|
||||
lockfile.get().map_err(|err| miette::Report::new(err).wrap_err("load the lockfile"))?;
|
||||
|
||||
let lockfile_path = manifest
|
||||
.path()
|
||||
@@ -71,7 +73,7 @@ impl RemoveArgs {
|
||||
http_client_arc: std::sync::Arc::clone(http_client),
|
||||
config,
|
||||
manifest,
|
||||
lockfile: lockfile.as_ref(),
|
||||
lockfile,
|
||||
lockfile_path: lockfile_path.as_deref(),
|
||||
package_names: &self.package_names,
|
||||
save_type: self.dependency_options.save_type(),
|
||||
|
||||
@@ -131,6 +131,8 @@ impl UpdateArgs {
|
||||
|
||||
let State { tarball_mem_cache, http_client, config, manifest, lockfile, resolved_packages } =
|
||||
&mut state;
|
||||
let lockfile =
|
||||
lockfile.get().map_err(|err| miette::Report::new(err).wrap_err("load the lockfile"))?;
|
||||
|
||||
let supported_architectures =
|
||||
self.supported_architectures.apply_to(config.supported_architectures.clone());
|
||||
@@ -143,7 +145,7 @@ impl UpdateArgs {
|
||||
let packages = if self.interactive {
|
||||
match crate::cli_args::update_interactive::select_packages(
|
||||
manifest,
|
||||
lockfile.as_ref(),
|
||||
lockfile,
|
||||
config,
|
||||
http_client,
|
||||
self.latest,
|
||||
@@ -168,7 +170,7 @@ impl UpdateArgs {
|
||||
http_client_arc: std::sync::Arc::clone(http_client),
|
||||
config,
|
||||
manifest,
|
||||
lockfile: lockfile.as_ref(),
|
||||
lockfile,
|
||||
lockfile_path: lockfile_path.as_deref(),
|
||||
packages: &packages,
|
||||
latest: self.latest,
|
||||
|
||||
@@ -10,7 +10,7 @@ use miette::set_panic_hook;
|
||||
use pacquet_diagnostics::enable_tracing_by_env;
|
||||
use state::State;
|
||||
|
||||
pub async fn main() -> miette::Result<()> {
|
||||
pub fn main() -> miette::Result<()> {
|
||||
enable_tracing_by_env();
|
||||
set_panic_hook();
|
||||
// Extract pnpm's `--config.<key>=<value>` tokens before clap sees
|
||||
@@ -19,16 +19,21 @@ pub async fn main() -> miette::Result<()> {
|
||||
// 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());
|
||||
// Run argument parsing *before* sizing the rayon pool so
|
||||
// `pacquet --help` / `--version` (and any clap parse error) exit
|
||||
// without spinning up worker threads. `clap::Parser::parse` calls
|
||||
// `std::process::exit` on those paths, so we never reach
|
||||
// `configure_rayon_pool` for them (Copilot review on <https://github.com/pnpm/pacquet/pull/292>).
|
||||
let args = CliArgs::parse_from(argv);
|
||||
// An up-to-date `pacquet install` finishes here, without paying for
|
||||
// the runtime, the HTTP client, or any worker threads.
|
||||
if args.finished_via_install_fast_path(&config_overrides) {
|
||||
return Ok(());
|
||||
}
|
||||
configure_rayon_pool();
|
||||
// Boxed for `clippy::large_futures`: the command future exceeds the
|
||||
// lint's stack-size threshold (the limit trips on Windows first).
|
||||
Box::pin(args.run(&config_overrides)).await
|
||||
tokio::runtime::Builder::new_multi_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.expect("build the tokio runtime")
|
||||
// Boxed for `clippy::large_futures`: the command future exceeds
|
||||
// the lint's stack-size threshold (the limit trips on Windows
|
||||
// first).
|
||||
.block_on(Box::pin(args.run(&config_overrides)))
|
||||
}
|
||||
|
||||
/// Size rayon's global pool at `2 × available_parallelism`. The link
|
||||
@@ -41,6 +46,16 @@ pub async fn main() -> miette::Result<()> {
|
||||
/// switching and per-thread fixed costs (`user` time scales linearly
|
||||
/// past 50 without any wall-time payoff).
|
||||
///
|
||||
/// Runs after the repeat-install fast path has declined, so commands
|
||||
/// that never reach a parallel phase (`--help`, the "Already up to
|
||||
/// date" short-circuit) skip the worker-thread spawn cost entirely.
|
||||
/// Deliberately NOT communicated via the `RAYON_NUM_THREADS`
|
||||
/// environment variable: a process-env write would leak into every
|
||||
/// child the install spawns (lifecycle scripts, `node --version`,
|
||||
/// git), and pnpm exposes no such variable to scripts. An explicit
|
||||
/// `RAYON_NUM_THREADS` from the caller is honoured by skipping the
|
||||
/// override.
|
||||
///
|
||||
/// Use [`std::thread::available_parallelism`] rather than the
|
||||
/// workspace's existing `num_cpus::get()` so cgroup / CPU-quota
|
||||
/// limits in containers and CI runners are respected — `num_cpus`
|
||||
@@ -59,12 +74,8 @@ pub async fn main() -> miette::Result<()> {
|
||||
/// quota literally — Copilot's follow-up flagged the tension; we're
|
||||
/// keeping the floor and documenting it explicitly.
|
||||
///
|
||||
/// Honours an explicit `RAYON_NUM_THREADS` env var by skipping our
|
||||
/// override (rayon's `build_global` errors if a pool is already set,
|
||||
/// but env vars don't pre-init it — so we just apply a smaller
|
||||
/// override only when nothing else has been configured). Best-effort:
|
||||
/// if another part of the binary already initialised the pool, leave
|
||||
/// it alone.
|
||||
/// Best-effort: if another part of the binary already initialised the
|
||||
/// pool, leave it alone.
|
||||
///
|
||||
/// [#292]: https://github.com/pnpm/pacquet/pull/292
|
||||
fn configure_rayon_pool() {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use derive_more::{Display, Error};
|
||||
use miette::Diagnostic;
|
||||
use pacquet_config::Config;
|
||||
use pacquet_lockfile::{LoadLockfileError, Lockfile};
|
||||
use pacquet_lockfile::LazyLockfile;
|
||||
use pacquet_network::{ForInstallsError, NetworkSettings, ThrottledClient};
|
||||
use pacquet_package_manager::ResolvedPackages;
|
||||
use pacquet_package_manifest::{PackageManifest, PackageManifestError};
|
||||
@@ -28,8 +28,10 @@ pub struct State {
|
||||
pub config: &'static Config,
|
||||
/// Data from the `package.json` file.
|
||||
pub manifest: PackageManifest,
|
||||
/// Data from the `pnpm-lock.yaml` file.
|
||||
pub lockfile: Option<Lockfile>,
|
||||
/// The `pnpm-lock.yaml` file, read + parsed on first access so the
|
||||
/// repeat-install fast path (which never needs its contents) skips
|
||||
/// the YAML parse.
|
||||
pub lockfile: LazyLockfile,
|
||||
/// In-memory cache for packages that have started resolving dependencies.
|
||||
pub resolved_packages: ResolvedPackages,
|
||||
}
|
||||
@@ -41,9 +43,6 @@ pub enum InitStateError {
|
||||
#[diagnostic(transparent)]
|
||||
Manifest(#[error(source)] PackageManifestError),
|
||||
|
||||
#[diagnostic(transparent)]
|
||||
Lockfile(#[error(source)] LoadLockfileError),
|
||||
|
||||
#[diagnostic(transparent)]
|
||||
Network(#[error(source)] ForInstallsError),
|
||||
}
|
||||
@@ -63,13 +62,21 @@ impl State {
|
||||
require_lockfile: bool,
|
||||
) -> Result<Self, InitStateError> {
|
||||
let should_load = config.lockfile || require_lockfile;
|
||||
let lockfile = if should_load {
|
||||
manifest_path
|
||||
.parent()
|
||||
.expect("manifest path always has a parent dir")
|
||||
.to_path_buf()
|
||||
.pipe(LazyLockfile::deferred)
|
||||
} else {
|
||||
LazyLockfile::disabled()
|
||||
};
|
||||
Ok(State {
|
||||
config,
|
||||
manifest: manifest_path
|
||||
.pipe(PackageManifest::create_if_needed)
|
||||
.map_err(InitStateError::Manifest)?,
|
||||
lockfile: call_load_lockfile(should_load, Lockfile::load_from_current_dir)
|
||||
.map_err(InitStateError::Lockfile)?,
|
||||
lockfile,
|
||||
http_client: std::sync::Arc::new(
|
||||
ThrottledClient::for_installs(
|
||||
&config.proxy,
|
||||
@@ -88,21 +95,3 @@ impl State {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Load the lockfile from the current directory when `should_load` is
|
||||
/// `true`. Callers compose `should_load` from `config.lockfile ||
|
||||
/// --frozen-lockfile` so the CLI flag is always honoured.
|
||||
///
|
||||
/// This function was extracted to be tested independently.
|
||||
fn call_load_lockfile<LoadLockfile, Lockfile, Error>(
|
||||
should_load: bool,
|
||||
load_lockfile: LoadLockfile,
|
||||
) -> Result<Option<Lockfile>, Error>
|
||||
where
|
||||
LoadLockfile: FnOnce() -> Result<Option<Lockfile>, Error>,
|
||||
{
|
||||
should_load.then(load_lockfile).transpose().map(Option::flatten)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
use super::call_load_lockfile;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_call_load_lockfile() {
|
||||
macro_rules! case {
|
||||
($should_load:expr, $load_lockfile:expr => $output:expr) => {{
|
||||
let should_load = $should_load;
|
||||
let load_lockfile = $load_lockfile;
|
||||
let output: Result<Option<&str>, &str> = $output;
|
||||
eprintln!(
|
||||
"CASE: {should_load:?}, {load_lockfile} => {output:?}",
|
||||
load_lockfile = stringify!($load_lockfile),
|
||||
);
|
||||
assert_eq!(call_load_lockfile(should_load, load_lockfile), output);
|
||||
}};
|
||||
}
|
||||
|
||||
case!(false, || unreachable!() => Ok(None));
|
||||
case!(true, || Err("error") => Err("error"));
|
||||
case!(true, || Ok(None) => Ok(None));
|
||||
case!(true, || Ok(Some("value")) => Ok(Some("value")));
|
||||
}
|
||||
111
pacquet/crates/lockfile/src/lazy_lockfile.rs
Normal file
111
pacquet/crates/lockfile/src/lazy_lockfile.rs
Normal file
@@ -0,0 +1,111 @@
|
||||
use crate::{LoadLockfileError, Lockfile};
|
||||
use std::{path::PathBuf, sync::OnceLock};
|
||||
|
||||
/// Wanted lockfile (`pnpm-lock.yaml`) whose read + parse are deferred
|
||||
/// until a consumer actually needs the contents.
|
||||
///
|
||||
/// The optimistic repeat-install fast path decides "Already up to
|
||||
/// date" from manifest mtimes alone — upstream's `checkDepsStatus`
|
||||
/// never reads the wanted lockfile on that path — so parsing a
|
||||
/// multi-megabyte YAML document up front is pure overhead for the
|
||||
/// repeat-install case. Commands that always need the lockfile call
|
||||
/// [`LazyLockfile::get`] immediately and behave as if it were loaded
|
||||
/// eagerly.
|
||||
pub struct LazyLockfile {
|
||||
/// Directory containing `pnpm-lock.yaml`. `None` mirrors
|
||||
/// `lockfile: false` config: [`Self::get`] yields `None` without
|
||||
/// touching the filesystem, matching the eager loader's "don't
|
||||
/// even read the file" behavior.
|
||||
dir: Option<PathBuf>,
|
||||
cell: OnceLock<Option<Lockfile>>,
|
||||
}
|
||||
|
||||
impl LazyLockfile {
|
||||
/// A lockfile that will be loaded from `<dir>/pnpm-lock.yaml` (the
|
||||
/// same source as [`Lockfile::load_wanted_from_dir`]) on first
|
||||
/// [`Self::get`].
|
||||
#[must_use]
|
||||
pub fn deferred(dir: PathBuf) -> Self {
|
||||
LazyLockfile { dir: Some(dir), cell: OnceLock::new() }
|
||||
}
|
||||
|
||||
/// A lockfile that is never loaded — [`Self::get`] yields `None`
|
||||
/// without touching the filesystem. Mirrors `lockfile: false`
|
||||
/// config.
|
||||
#[must_use]
|
||||
pub fn disabled() -> Self {
|
||||
LazyLockfile { dir: None, cell: OnceLock::new() }
|
||||
}
|
||||
|
||||
/// A lockfile that is already in memory; [`Self::get`] returns it
|
||||
/// without touching the filesystem.
|
||||
#[must_use]
|
||||
pub fn preloaded(lockfile: Option<Lockfile>) -> Self {
|
||||
let cell = OnceLock::new();
|
||||
cell.set(lockfile).expect("a fresh OnceLock accepts the first set");
|
||||
LazyLockfile { dir: None, cell }
|
||||
}
|
||||
|
||||
/// The parsed wanted lockfile, loading it on first call. `None`
|
||||
/// when the file is absent, empty, or loading is disabled. A load
|
||||
/// error is returned without being cached, so a subsequent call
|
||||
/// retries — callers abort on the first error in practice.
|
||||
pub fn get(&self) -> Result<Option<&Lockfile>, LoadLockfileError> {
|
||||
if let Some(lockfile) = self.cell.get() {
|
||||
return Ok(lockfile.as_ref());
|
||||
}
|
||||
let loaded = match self.dir.as_deref() {
|
||||
Some(dir) => Lockfile::load_wanted_from_dir(dir)?,
|
||||
None => None,
|
||||
};
|
||||
Ok(self.cell.get_or_init(|| loaded).as_ref())
|
||||
}
|
||||
|
||||
/// Whether a wanted lockfile is known to be available: the parsed
|
||||
/// document when already loaded, otherwise
|
||||
/// [`Lockfile::wanted_exists_in_dir`]'s semantic-presence probe —
|
||||
/// the same absence rules as the loader (an empty or env-only
|
||||
/// file counts as absent), without paying for the YAML parse on
|
||||
/// the repeat-install fast path.
|
||||
#[must_use]
|
||||
pub fn is_loaded_or_on_disk(&self) -> bool {
|
||||
if let Some(lockfile) = self.cell.get() {
|
||||
return lockfile.is_some();
|
||||
}
|
||||
self.dir.as_deref().is_some_and(Lockfile::wanted_exists_in_dir)
|
||||
}
|
||||
}
|
||||
|
||||
/// A wanted lockfile that is either already parsed (callers that
|
||||
/// re-resolve after a manifest mutation hold one) or lazily loadable.
|
||||
/// `Copy` so it threads through the install pipeline like the
|
||||
/// `Option<&Lockfile>` it replaces.
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum MaybeLazyLockfile<'a> {
|
||||
Loaded(Option<&'a Lockfile>),
|
||||
Lazy(&'a LazyLockfile),
|
||||
}
|
||||
|
||||
impl<'a> MaybeLazyLockfile<'a> {
|
||||
/// The parsed wanted lockfile, loading it now when lazy. See
|
||||
/// [`LazyLockfile::get`] for the error contract.
|
||||
pub fn get(self) -> Result<Option<&'a Lockfile>, LoadLockfileError> {
|
||||
match self {
|
||||
MaybeLazyLockfile::Loaded(lockfile) => Ok(lockfile),
|
||||
MaybeLazyLockfile::Lazy(lazy) => lazy.get(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether a wanted lockfile is available, without forcing a parse
|
||||
/// in the lazy case. See [`LazyLockfile::is_loaded_or_on_disk`].
|
||||
#[must_use]
|
||||
pub fn is_loaded_or_on_disk(self) -> bool {
|
||||
match self {
|
||||
MaybeLazyLockfile::Loaded(lockfile) => lockfile.is_some(),
|
||||
MaybeLazyLockfile::Lazy(lazy) => lazy.is_loaded_or_on_disk(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
87
pacquet/crates/lockfile/src/lazy_lockfile/tests.rs
Normal file
87
pacquet/crates/lockfile/src/lazy_lockfile/tests.rs
Normal file
@@ -0,0 +1,87 @@
|
||||
use super::{LazyLockfile, MaybeLazyLockfile};
|
||||
use crate::Lockfile;
|
||||
use std::fs;
|
||||
|
||||
fn minimal_lockfile() -> Lockfile {
|
||||
serde_saphyr::from_str("lockfileVersion: '9.0'\n").expect("parse a minimal lockfile")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preloaded_returns_the_stored_lockfile_without_io() {
|
||||
let lazy = LazyLockfile::preloaded(Some(minimal_lockfile()));
|
||||
let loaded = lazy.get().expect("preloaded lockfile loads infallibly");
|
||||
assert!(loaded.is_some());
|
||||
assert!(lazy.is_loaded_or_on_disk());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preloaded_none_reports_absent() {
|
||||
let lazy = LazyLockfile::preloaded(None);
|
||||
assert!(lazy.get().expect("preloaded lockfile loads infallibly").is_none());
|
||||
assert!(!lazy.is_loaded_or_on_disk());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn disabled_never_touches_the_filesystem() {
|
||||
let lazy = LazyLockfile::disabled();
|
||||
assert!(lazy.get().expect("disabled load is infallible").is_none());
|
||||
assert!(!lazy.is_loaded_or_on_disk());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deferred_loads_from_the_given_dir_not_the_process_cwd() {
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
fs::write(dir.path().join(Lockfile::FILE_NAME), "lockfileVersion: '9.0'\n")
|
||||
.expect("write pnpm-lock.yaml");
|
||||
|
||||
let lazy = LazyLockfile::deferred(dir.path().to_path_buf());
|
||||
assert!(lazy.is_loaded_or_on_disk(), "probe must find the dir-addressed lockfile");
|
||||
assert!(lazy.get().expect("deferred load succeeds").is_some());
|
||||
|
||||
let empty = tempfile::tempdir().expect("tempdir");
|
||||
let lazy = LazyLockfile::deferred(empty.path().to_path_buf());
|
||||
assert!(!lazy.is_loaded_or_on_disk());
|
||||
assert!(lazy.get().expect("absent lockfile loads as None").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_and_env_only_files_count_as_absent() {
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
let path = dir.path().join(Lockfile::FILE_NAME);
|
||||
|
||||
fs::write(&path, "").expect("write empty lockfile");
|
||||
let lazy = LazyLockfile::deferred(dir.path().to_path_buf());
|
||||
assert!(!lazy.is_loaded_or_on_disk(), "an empty file must count as absent");
|
||||
|
||||
fs::write(&path, "---\nenvDependencies:\n node: '22.0.0'\n").expect("write env-only lockfile");
|
||||
let lazy = LazyLockfile::deferred(dir.path().to_path_buf());
|
||||
assert!(!lazy.is_loaded_or_on_disk(), "an env-only document must count as absent");
|
||||
assert!(lazy.get().expect("env-only lockfile loads as None").is_none());
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn unreadable_lockfile_counts_as_present() {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
let path = dir.path().join(Lockfile::FILE_NAME);
|
||||
fs::write(&path, "lockfileVersion: '9.0'\n").expect("write pnpm-lock.yaml");
|
||||
fs::set_permissions(&path, fs::Permissions::from_mode(0o000)).expect("drop permissions");
|
||||
|
||||
let lazy = LazyLockfile::deferred(dir.path().to_path_buf());
|
||||
let present = lazy.is_loaded_or_on_disk();
|
||||
fs::set_permissions(&path, fs::Permissions::from_mode(0o644)).expect("restore permissions");
|
||||
assert!(present, "an unreadable lockfile must not be mistaken for a missing one");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn loaded_variant_passes_through() {
|
||||
let lockfile = minimal_lockfile();
|
||||
let maybe = MaybeLazyLockfile::Loaded(Some(&lockfile));
|
||||
assert!(maybe.get().expect("loaded variant is infallible").is_some());
|
||||
assert!(maybe.is_loaded_or_on_disk());
|
||||
let maybe = MaybeLazyLockfile::Loaded(None);
|
||||
assert!(maybe.get().expect("loaded variant is infallible").is_none());
|
||||
assert!(!maybe.is_loaded_or_on_disk());
|
||||
}
|
||||
@@ -2,6 +2,7 @@ mod catalog_snapshots;
|
||||
mod comver;
|
||||
mod env_lockfile;
|
||||
mod freshness;
|
||||
mod lazy_lockfile;
|
||||
mod load_lockfile;
|
||||
mod lockfile_version;
|
||||
mod package_metadata;
|
||||
@@ -25,6 +26,7 @@ pub use catalog_snapshots::*;
|
||||
pub use comver::*;
|
||||
pub use env_lockfile::*;
|
||||
pub use freshness::*;
|
||||
pub use lazy_lockfile::*;
|
||||
pub use load_lockfile::*;
|
||||
pub use lockfile_version::*;
|
||||
pub use package_metadata::*;
|
||||
|
||||
@@ -62,6 +62,27 @@ impl Lockfile {
|
||||
Self::load_from_path(&dir.join(Lockfile::FILE_NAME))
|
||||
}
|
||||
|
||||
/// Whether `<dir>/pnpm-lock.yaml` would load as `Some`: the file
|
||||
/// exists and its main document is non-empty. The same absence
|
||||
/// rules as [`Self::load_wanted_from_dir`] (a missing file, an
|
||||
/// empty file, and an env-only combined document all count as
|
||||
/// absent) without paying for the YAML parse — only the read and
|
||||
/// the document split.
|
||||
///
|
||||
/// Any read failure other than `NotFound` (permissions, invalid
|
||||
/// UTF-8, I/O) reports the file as present: an existing-but-
|
||||
/// unreadable lockfile must not be mistaken for a missing one —
|
||||
/// the regenerate-on-missing path would overwrite it — and the
|
||||
/// real load surfaces the underlying error when the contents are
|
||||
/// actually needed.
|
||||
#[must_use]
|
||||
pub fn wanted_exists_in_dir(dir: &Path) -> bool {
|
||||
match fs::read_to_string(dir.join(Lockfile::FILE_NAME)) {
|
||||
Ok(content) => !extract_main_document(&content).trim().is_empty(),
|
||||
Err(error) => error.kind() != ErrorKind::NotFound,
|
||||
}
|
||||
}
|
||||
|
||||
fn load_from_path(file_path: &Path) -> Result<Option<Self>, LoadLockfileError> {
|
||||
let content = match fs::read_to_string(file_path) {
|
||||
Ok(content) => content,
|
||||
|
||||
@@ -10,7 +10,7 @@ use pacquet_catalogs_config::{
|
||||
use pacquet_catalogs_protocol_parser::parse_catalog_protocol;
|
||||
use pacquet_catalogs_types::Catalogs;
|
||||
use pacquet_config::Config;
|
||||
use pacquet_lockfile::Lockfile;
|
||||
use pacquet_lockfile::{Lockfile, MaybeLazyLockfile};
|
||||
use pacquet_network::ThrottledClient;
|
||||
use pacquet_package_manifest::{DependencyGroup, PackageManifest, PackageManifestError};
|
||||
use pacquet_registry::{PackageTag, PackageVersion};
|
||||
@@ -216,7 +216,7 @@ where
|
||||
http_client_arc,
|
||||
config,
|
||||
manifest,
|
||||
lockfile,
|
||||
lockfile: MaybeLazyLockfile::Loaded(lockfile),
|
||||
lockfile_path,
|
||||
dependency_groups: list_dependency_groups(),
|
||||
frozen_lockfile: false,
|
||||
|
||||
@@ -17,7 +17,8 @@ use pacquet_executor::{
|
||||
ScriptsPrependNodePath as ExecScriptsPrependNodePath, run_project_lifecycle_scripts,
|
||||
};
|
||||
use pacquet_lockfile::{
|
||||
LoadLockfileError, Lockfile, SaveLockfileError, StalenessReason, satisfies_package_manifest,
|
||||
LazyLockfile, LoadLockfileError, Lockfile, MaybeLazyLockfile, SaveLockfileError,
|
||||
StalenessReason, satisfies_package_manifest,
|
||||
};
|
||||
use pacquet_lockfile_verification::{
|
||||
VerifyError, VerifyLockfileResolutionsOptions, record_lockfile_verified,
|
||||
@@ -41,7 +42,7 @@ use pacquet_workspace_state::{
|
||||
};
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
path::Path,
|
||||
path::{Path, PathBuf},
|
||||
sync::{Arc, atomic::AtomicU8},
|
||||
time::SystemTime,
|
||||
};
|
||||
@@ -106,7 +107,7 @@ where
|
||||
pub http_client_arc: Arc<ThrottledClient>,
|
||||
pub config: &'static Config,
|
||||
pub manifest: &'a PackageManifest,
|
||||
pub lockfile: Option<&'a Lockfile>,
|
||||
pub lockfile: MaybeLazyLockfile<'a>,
|
||||
/// Absolute path of the loaded `pnpm-lock.yaml`. Threaded into
|
||||
/// the lockfile-verification gate so the per-path stat shortcut
|
||||
/// in `<cache_dir>/lockfile-verified.jsonl` can fire on repeat
|
||||
@@ -285,6 +286,12 @@ pub enum InstallError {
|
||||
#[diagnostic(transparent)]
|
||||
LoadCurrentLockfile(#[error(source)] LoadLockfileError),
|
||||
|
||||
/// Surfaces a `pnpm-lock.yaml` read or parse failure from the
|
||||
/// deferred load that runs once the repeat-install fast path has
|
||||
/// passed on the install (see [`MaybeLazyLockfile`]).
|
||||
#[diagnostic(transparent)]
|
||||
LoadWantedLockfile(#[error(source)] LoadLockfileError),
|
||||
|
||||
/// Surfaces a failure to persist the current lockfile so the next
|
||||
/// install can diff against it. A best-effort warn would let
|
||||
/// silent disk-full or permission issues compound across installs;
|
||||
@@ -546,13 +553,20 @@ where
|
||||
// forcing the frozen path.
|
||||
let project_manifests =
|
||||
build_project_manifests_list(&workspace_root, manifest, workspace_projects.as_deref());
|
||||
// `pacquet update` must always re-resolve, so it bypasses the
|
||||
// optimistic short-circuit: a compatible bump leaves the
|
||||
// manifest byte-identical, which the repeat-install check would
|
||||
// otherwise read as "nothing changed → already up to date" and
|
||||
// skip the registry re-resolution entirely. Gating on
|
||||
// `KeepAll` keeps `install` / `add` on the fast path.
|
||||
if matches!(update_seed_policy, UpdateSeedPolicy::KeepAll)
|
||||
// Only a full `pacquet install` may short-circuit. `add` and
|
||||
// `remove` mutate the manifest in memory and persist it after
|
||||
// this run returns, so the on-disk mtimes the check reads still
|
||||
// describe the pre-mutation project — without this gate a fresh
|
||||
// workspace state would read as "nothing changed → already up
|
||||
// to date" and the mutation would never be resolved or
|
||||
// materialized. Mirrors upstream `installDeps` calling
|
||||
// `checkDepsStatus` only for the plain-install mutation, never
|
||||
// for `installSome` / `uninstallSome`. `pacquet update` is
|
||||
// excluded through its seed policy: a compatible bump leaves
|
||||
// the manifest byte-identical, which the check would likewise
|
||||
// read as up to date and skip the registry re-resolution.
|
||||
let optimistic_decision = is_full_install
|
||||
&& matches!(update_seed_policy, UpdateSeedPolicy::KeepAll)
|
||||
&& !frozen_lockfile
|
||||
&& check_optimistic_repeat_install(&OptimisticRepeatInstallCheck {
|
||||
workspace_root: &workspace_root,
|
||||
@@ -563,8 +577,8 @@ where
|
||||
is_workspace_install: workspace_manifest.is_some(),
|
||||
lockfile,
|
||||
catalogs: &catalogs,
|
||||
}) == OptimisticRepeatInstallDecision::UpToDate
|
||||
{
|
||||
}) == OptimisticRepeatInstallDecision::UpToDate;
|
||||
if optimistic_decision {
|
||||
Reporter::emit(&LogEvent::Pnpm(PnpmLog {
|
||||
level: LogLevel::Info,
|
||||
message: "Already up to date".to_string(),
|
||||
@@ -574,6 +588,10 @@ where
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Past the repeat-install fast path every install flavor needs
|
||||
// the wanted lockfile's contents; force the deferred load here.
|
||||
let lockfile = lockfile.get().map_err(InstallError::LoadWantedLockfile)?;
|
||||
|
||||
// Register the project against the shared store for prune
|
||||
// tracking, once per install at the workspace root. Mirrors
|
||||
// upstream's call into `@pnpm/store.controller`'s
|
||||
@@ -1757,6 +1775,70 @@ fn run_projects_lifecycle_scripts<Reporter: self::Reporter>(
|
||||
///
|
||||
/// `workspace_projects.is_none()` covers single-project installs (no
|
||||
/// `pnpm-workspace.yaml`) — the only manifest is the root one.
|
||||
/// Inputs for [`install_already_up_to_date`].
|
||||
pub struct UpToDateFastPathCheck<'a> {
|
||||
pub config: &'a Config,
|
||||
pub manifest: &'a PackageManifest,
|
||||
pub dependency_groups: Vec<DependencyGroup>,
|
||||
pub node_linker: NodeLinker,
|
||||
}
|
||||
|
||||
/// Pre-runtime twin of the repeat-install short-circuit inside
|
||||
/// [`Install::run`]: same workspace discovery, same
|
||||
/// [`check_optimistic_repeat_install`] inputs, callable from a
|
||||
/// synchronous context so the CLI can finish an up-to-date install
|
||||
/// before paying for the async runtime, the HTTP client, and the
|
||||
/// state setup. Returns the workspace root — the reporter `prefix`
|
||||
/// for the "Already up to date" emission — when the install can
|
||||
/// short-circuit.
|
||||
///
|
||||
/// Failures deliberately collapse to `None`: the caller falls through
|
||||
/// to the full install path, which reproduces the failure with its
|
||||
/// established error shape.
|
||||
#[must_use]
|
||||
pub fn install_already_up_to_date(check: &UpToDateFastPathCheck<'_>) -> Option<PathBuf> {
|
||||
let UpToDateFastPathCheck { config, manifest, dependency_groups, node_linker } = check;
|
||||
let included = IncludedDependencies {
|
||||
dependencies: dependency_groups.contains(&DependencyGroup::Prod),
|
||||
dev_dependencies: dependency_groups.contains(&DependencyGroup::Dev),
|
||||
optional_dependencies: dependency_groups.contains(&DependencyGroup::Optional),
|
||||
};
|
||||
let manifest_dir = manifest.path().parent()?;
|
||||
let workspace_dir_opt = pacquet_workspace::find_workspace_dir(manifest_dir).ok()?;
|
||||
let workspace_root = workspace_dir_opt.clone().unwrap_or_else(|| manifest_dir.to_path_buf());
|
||||
let workspace_manifest = match workspace_dir_opt.as_deref() {
|
||||
Some(dir) => pacquet_workspace::read_workspace_manifest(dir).ok()?,
|
||||
None => None,
|
||||
};
|
||||
let catalogs = match config.catalogs.clone() {
|
||||
Some(catalogs) => catalogs,
|
||||
None => get_catalogs_from_workspace_manifest(workspace_manifest.as_ref()).ok()?,
|
||||
};
|
||||
let workspace_projects =
|
||||
load_workspace_projects(&workspace_root, workspace_manifest.as_ref()).ok()?;
|
||||
let project_manifests =
|
||||
build_project_manifests_list(&workspace_root, manifest, workspace_projects.as_deref());
|
||||
// Same lockfile source as `State::init`'s (the manifest's
|
||||
// directory), so the pre-runtime check and the in-pipeline check
|
||||
// reach their verdicts from the same file.
|
||||
let lockfile = if config.lockfile {
|
||||
LazyLockfile::deferred(manifest_dir.to_path_buf())
|
||||
} else {
|
||||
LazyLockfile::disabled()
|
||||
};
|
||||
(check_optimistic_repeat_install(&OptimisticRepeatInstallCheck {
|
||||
workspace_root: &workspace_root,
|
||||
config,
|
||||
node_linker: *node_linker,
|
||||
included,
|
||||
project_manifests: &project_manifests,
|
||||
is_workspace_install: workspace_manifest.is_some(),
|
||||
lockfile: MaybeLazyLockfile::Lazy(&lockfile),
|
||||
catalogs: &catalogs,
|
||||
}) == OptimisticRepeatInstallDecision::UpToDate)
|
||||
.then_some(workspace_root)
|
||||
}
|
||||
|
||||
fn build_project_manifests_list<'a>(
|
||||
workspace_root: &std::path::Path,
|
||||
root_manifest: &'a PackageManifest,
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
reason = "struct-literal test fixtures; field types are evident from the literal and naming each would force ~20 imports"
|
||||
)]
|
||||
|
||||
use super::{Install, InstallError};
|
||||
use super::{Install, InstallError, UpToDateFastPathCheck, install_already_up_to_date};
|
||||
use pacquet_config::Config;
|
||||
use pacquet_lockfile::Lockfile;
|
||||
use pacquet_lockfile::{Lockfile, MaybeLazyLockfile};
|
||||
use pacquet_modules_yaml::{
|
||||
DEFAULT_VIRTUAL_STORE_DIR_MAX_LENGTH, Host, LayoutVersion, Modules, NodeLinker,
|
||||
read_modules_manifest, write_modules_manifest,
|
||||
@@ -83,7 +83,7 @@ async fn should_install_dependencies() {
|
||||
http_client_arc: std::sync::Arc::new(Default::default()),
|
||||
config,
|
||||
manifest: &manifest,
|
||||
lockfile: None,
|
||||
lockfile: MaybeLazyLockfile::Loaded(None),
|
||||
lockfile_path: None,
|
||||
dependency_groups: [DependencyGroup::Prod, DependencyGroup::Dev, DependencyGroup::Optional],
|
||||
frozen_lockfile: false,
|
||||
@@ -179,7 +179,7 @@ async fn lockfile_only_routes_scoped_packages_to_configured_scoped_registry() {
|
||||
http_client_arc: std::sync::Arc::new(Default::default()),
|
||||
config,
|
||||
manifest: &manifest,
|
||||
lockfile: None,
|
||||
lockfile: MaybeLazyLockfile::Loaded(None),
|
||||
lockfile_path: None,
|
||||
dependency_groups: [DependencyGroup::Prod],
|
||||
frozen_lockfile: false,
|
||||
@@ -231,7 +231,7 @@ async fn should_error_when_frozen_lockfile_is_requested_but_none_exists() {
|
||||
http_client_arc: std::sync::Arc::new(Default::default()),
|
||||
config,
|
||||
manifest: &manifest,
|
||||
lockfile: None,
|
||||
lockfile: MaybeLazyLockfile::Loaded(None),
|
||||
lockfile_path: None,
|
||||
dependency_groups: [DependencyGroup::Prod],
|
||||
frozen_lockfile: true,
|
||||
@@ -280,7 +280,7 @@ async fn should_error_when_frozen_lockfile_and_update_checksums_are_both_set() {
|
||||
http_client_arc: std::sync::Arc::new(Default::default()),
|
||||
config,
|
||||
manifest: &manifest,
|
||||
lockfile: None,
|
||||
lockfile: MaybeLazyLockfile::Loaded(None),
|
||||
lockfile_path: None,
|
||||
dependency_groups: [DependencyGroup::Prod],
|
||||
frozen_lockfile: true,
|
||||
@@ -358,7 +358,7 @@ async fn frozen_lockfile_flag_overrides_config_lockfile_false() {
|
||||
http_client_arc: std::sync::Arc::new(Default::default()),
|
||||
config,
|
||||
manifest: &manifest,
|
||||
lockfile: Some(&lockfile),
|
||||
lockfile: MaybeLazyLockfile::Loaded(Some(&lockfile)),
|
||||
lockfile_path: None,
|
||||
dependency_groups: [DependencyGroup::Prod],
|
||||
frozen_lockfile: true,
|
||||
@@ -429,7 +429,7 @@ async fn npm_alias_dependency_installs_under_alias_key() {
|
||||
http_client_arc: std::sync::Arc::new(Default::default()),
|
||||
config,
|
||||
manifest: &manifest,
|
||||
lockfile: None,
|
||||
lockfile: MaybeLazyLockfile::Loaded(None),
|
||||
lockfile_path: None,
|
||||
dependency_groups: [DependencyGroup::Prod],
|
||||
frozen_lockfile: false,
|
||||
@@ -519,7 +519,7 @@ async fn unversioned_npm_alias_defaults_to_latest() {
|
||||
http_client_arc: std::sync::Arc::new(Default::default()),
|
||||
config,
|
||||
manifest: &manifest,
|
||||
lockfile: None,
|
||||
lockfile: MaybeLazyLockfile::Loaded(None),
|
||||
lockfile_path: None,
|
||||
dependency_groups: [DependencyGroup::Prod],
|
||||
frozen_lockfile: false,
|
||||
@@ -592,7 +592,7 @@ async fn frozen_lockfile_flag_with_no_lockfile_errors() {
|
||||
http_client_arc: std::sync::Arc::new(Default::default()),
|
||||
config,
|
||||
manifest: &manifest,
|
||||
lockfile: None,
|
||||
lockfile: MaybeLazyLockfile::Loaded(None),
|
||||
lockfile_path: None,
|
||||
dependency_groups: [DependencyGroup::Prod],
|
||||
frozen_lockfile: true,
|
||||
@@ -688,7 +688,7 @@ async fn install_emits_pnpm_event_sequence() {
|
||||
http_client_arc: std::sync::Arc::new(Default::default()),
|
||||
config,
|
||||
manifest: &manifest,
|
||||
lockfile: Some(&lockfile),
|
||||
lockfile: MaybeLazyLockfile::Loaded(Some(&lockfile)),
|
||||
lockfile_path: None,
|
||||
dependency_groups: [DependencyGroup::Prod],
|
||||
frozen_lockfile: true,
|
||||
@@ -838,7 +838,7 @@ async fn install_writes_modules_yaml() {
|
||||
http_client_arc: std::sync::Arc::new(Default::default()),
|
||||
config,
|
||||
manifest: &manifest,
|
||||
lockfile: Some(&lockfile),
|
||||
lockfile: MaybeLazyLockfile::Loaded(Some(&lockfile)),
|
||||
lockfile_path: None,
|
||||
// Drive a non-default `included`: prod + optional, no dev,
|
||||
// so the assertion below pins the mapping of dispatched
|
||||
@@ -948,7 +948,7 @@ async fn install_writes_workspace_state() {
|
||||
http_client_arc: std::sync::Arc::new(Default::default()),
|
||||
config,
|
||||
manifest: &manifest,
|
||||
lockfile: Some(&lockfile),
|
||||
lockfile: MaybeLazyLockfile::Loaded(Some(&lockfile)),
|
||||
lockfile_path: None,
|
||||
// Same `included` shape as `install_writes_modules_yaml` so the
|
||||
// dev/optional/production assertions below line up with the
|
||||
@@ -1185,7 +1185,7 @@ async fn install_optional_failing_postinstall_dep_via_registry_mock_succeeds() {
|
||||
http_client_arc: std::sync::Arc::new(Default::default()),
|
||||
config,
|
||||
manifest: &manifest,
|
||||
lockfile: None,
|
||||
lockfile: MaybeLazyLockfile::Loaded(None),
|
||||
lockfile_path: None,
|
||||
dependency_groups: [DependencyGroup::Prod, DependencyGroup::Optional],
|
||||
frozen_lockfile: false,
|
||||
@@ -1263,7 +1263,7 @@ async fn auto_install_peers_does_not_cascade_optional_peers() {
|
||||
http_client_arc: std::sync::Arc::new(Default::default()),
|
||||
config,
|
||||
manifest: &manifest,
|
||||
lockfile: None,
|
||||
lockfile: MaybeLazyLockfile::Loaded(None),
|
||||
lockfile_path: None,
|
||||
dependency_groups: [DependencyGroup::Prod, DependencyGroup::Optional],
|
||||
frozen_lockfile: false,
|
||||
@@ -1364,7 +1364,7 @@ async fn auto_install_peers_skips_meta_only_optional_peers() {
|
||||
http_client_arc: std::sync::Arc::new(Default::default()),
|
||||
config,
|
||||
manifest: &manifest,
|
||||
lockfile: None,
|
||||
lockfile: MaybeLazyLockfile::Loaded(None),
|
||||
lockfile_path: None,
|
||||
dependency_groups: [DependencyGroup::Prod, DependencyGroup::Optional],
|
||||
frozen_lockfile: false,
|
||||
@@ -1501,7 +1501,7 @@ async fn warm_reinstall_skips_snapshot_when_current_lockfile_matches() {
|
||||
http_client_arc: std::sync::Arc::new(Default::default()),
|
||||
config,
|
||||
manifest: &manifest,
|
||||
lockfile: Some(&lockfile),
|
||||
lockfile: MaybeLazyLockfile::Loaded(Some(&lockfile)),
|
||||
lockfile_path: None,
|
||||
dependency_groups: [DependencyGroup::Prod],
|
||||
frozen_lockfile: true,
|
||||
@@ -1604,7 +1604,7 @@ async fn warm_reinstall_emits_broken_modules_when_dir_is_missing() {
|
||||
http_client_arc: std::sync::Arc::new(Default::default()),
|
||||
config,
|
||||
manifest: &manifest,
|
||||
lockfile: Some(&lockfile),
|
||||
lockfile: MaybeLazyLockfile::Loaded(Some(&lockfile)),
|
||||
lockfile_path: None,
|
||||
dependency_groups: [DependencyGroup::Prod],
|
||||
frozen_lockfile: true,
|
||||
@@ -1715,7 +1715,7 @@ async fn context_log_reflects_current_lockfile_after_first_install() {
|
||||
http_client_arc: std::sync::Arc::new(Default::default()),
|
||||
config,
|
||||
manifest: &manifest,
|
||||
lockfile: Some(&lockfile),
|
||||
lockfile: MaybeLazyLockfile::Loaded(Some(&lockfile)),
|
||||
lockfile_path: None,
|
||||
dependency_groups: [DependencyGroup::Prod],
|
||||
frozen_lockfile: true,
|
||||
@@ -1770,7 +1770,7 @@ async fn context_log_reflects_current_lockfile_after_first_install() {
|
||||
http_client_arc: std::sync::Arc::new(Default::default()),
|
||||
config,
|
||||
manifest: &manifest,
|
||||
lockfile: Some(&lockfile),
|
||||
lockfile: MaybeLazyLockfile::Loaded(Some(&lockfile)),
|
||||
lockfile_path: None,
|
||||
dependency_groups: [DependencyGroup::Prod],
|
||||
frozen_lockfile: true,
|
||||
@@ -1867,7 +1867,7 @@ async fn warm_reinstall_reports_added_zero_and_emits_no_imported_events() {
|
||||
http_client_arc: std::sync::Arc::new(Default::default()),
|
||||
config,
|
||||
manifest: &manifest,
|
||||
lockfile: Some(&lockfile),
|
||||
lockfile: MaybeLazyLockfile::Loaded(Some(&lockfile)),
|
||||
lockfile_path: None,
|
||||
dependency_groups: [DependencyGroup::Prod],
|
||||
frozen_lockfile: true,
|
||||
@@ -1974,7 +1974,7 @@ async fn frozen_lockfile_errors_when_manifest_drifts_from_lockfile() {
|
||||
http_client_arc: std::sync::Arc::new(Default::default()),
|
||||
config,
|
||||
manifest: &manifest,
|
||||
lockfile: Some(&lockfile),
|
||||
lockfile: MaybeLazyLockfile::Loaded(Some(&lockfile)),
|
||||
lockfile_path: None,
|
||||
dependency_groups: [DependencyGroup::Prod],
|
||||
frozen_lockfile: true,
|
||||
@@ -2043,7 +2043,7 @@ async fn ignore_manifest_check_bypasses_manifest_freshness_gate() {
|
||||
http_client_arc: std::sync::Arc::new(Default::default()),
|
||||
config,
|
||||
manifest: &manifest,
|
||||
lockfile: Some(&lockfile),
|
||||
lockfile: MaybeLazyLockfile::Loaded(Some(&lockfile)),
|
||||
lockfile_path: None,
|
||||
dependency_groups: [DependencyGroup::Prod],
|
||||
frozen_lockfile: true,
|
||||
@@ -2113,7 +2113,7 @@ async fn frozen_lockfile_errors_when_overrides_drift_from_lockfile() {
|
||||
http_client_arc: std::sync::Arc::new(Default::default()),
|
||||
config,
|
||||
manifest: &manifest,
|
||||
lockfile: Some(&lockfile),
|
||||
lockfile: MaybeLazyLockfile::Loaded(Some(&lockfile)),
|
||||
lockfile_path: None,
|
||||
dependency_groups: [DependencyGroup::Prod],
|
||||
frozen_lockfile: true,
|
||||
@@ -2209,7 +2209,7 @@ async fn frozen_lockfile_applies_overrides_to_manifest_before_freshness_check()
|
||||
http_client_arc: std::sync::Arc::new(Default::default()),
|
||||
config,
|
||||
manifest: &manifest,
|
||||
lockfile: Some(&lockfile),
|
||||
lockfile: MaybeLazyLockfile::Loaded(Some(&lockfile)),
|
||||
lockfile_path: None,
|
||||
dependency_groups: [DependencyGroup::Prod],
|
||||
frozen_lockfile: true,
|
||||
@@ -2321,7 +2321,7 @@ async fn frozen_lockfile_resolves_catalog_protocol_in_overrides_before_freshness
|
||||
http_client_arc: std::sync::Arc::new(Default::default()),
|
||||
config,
|
||||
manifest: &manifest,
|
||||
lockfile: Some(&lockfile),
|
||||
lockfile: MaybeLazyLockfile::Loaded(Some(&lockfile)),
|
||||
lockfile_path: None,
|
||||
dependency_groups: [DependencyGroup::Prod],
|
||||
frozen_lockfile: true,
|
||||
@@ -2387,7 +2387,7 @@ async fn frozen_lockfile_errors_when_lockfile_has_no_root_importer() {
|
||||
http_client_arc: std::sync::Arc::new(Default::default()),
|
||||
config,
|
||||
manifest: &manifest,
|
||||
lockfile: Some(&lockfile),
|
||||
lockfile: MaybeLazyLockfile::Loaded(Some(&lockfile)),
|
||||
lockfile_path: None,
|
||||
dependency_groups: [DependencyGroup::Prod],
|
||||
frozen_lockfile: true,
|
||||
@@ -2480,7 +2480,7 @@ async fn frozen_lockfile_under_gvs_registers_project_and_runs_clean() {
|
||||
http_client_arc: std::sync::Arc::new(Default::default()),
|
||||
config,
|
||||
manifest: &manifest,
|
||||
lockfile: Some(&lockfile),
|
||||
lockfile: MaybeLazyLockfile::Loaded(Some(&lockfile)),
|
||||
lockfile_path: None,
|
||||
dependency_groups: [DependencyGroup::Prod],
|
||||
frozen_lockfile: true,
|
||||
@@ -2592,7 +2592,7 @@ async fn gvs_persists_global_virtual_store_dir_in_modules_yaml_and_context_log()
|
||||
http_client_arc: std::sync::Arc::new(Default::default()),
|
||||
config,
|
||||
manifest: &manifest,
|
||||
lockfile: Some(&lockfile),
|
||||
lockfile: MaybeLazyLockfile::Loaded(Some(&lockfile)),
|
||||
lockfile_path: None,
|
||||
dependency_groups: [DependencyGroup::Prod],
|
||||
frozen_lockfile: true,
|
||||
@@ -2711,7 +2711,7 @@ async fn frozen_lockfile_with_gvs_off_skips_project_registry() {
|
||||
http_client_arc: std::sync::Arc::new(Default::default()),
|
||||
config,
|
||||
manifest: &manifest,
|
||||
lockfile: Some(&lockfile),
|
||||
lockfile: MaybeLazyLockfile::Loaded(Some(&lockfile)),
|
||||
lockfile_path: None,
|
||||
dependency_groups: [DependencyGroup::Prod],
|
||||
frozen_lockfile: true,
|
||||
@@ -2796,7 +2796,7 @@ async fn frozen_lockfile_under_gvs_registers_workspace_root_only() {
|
||||
http_client_arc: std::sync::Arc::new(Default::default()),
|
||||
config,
|
||||
manifest: &manifest,
|
||||
lockfile: Some(&lockfile),
|
||||
lockfile: MaybeLazyLockfile::Loaded(Some(&lockfile)),
|
||||
lockfile_path: None,
|
||||
dependency_groups: [DependencyGroup::Prod],
|
||||
frozen_lockfile: true,
|
||||
@@ -3001,7 +3001,7 @@ async fn frozen_install_preserves_seeded_skipped_across_reinstall() {
|
||||
http_client_arc: std::sync::Arc::new(Default::default()),
|
||||
config,
|
||||
manifest: &manifest,
|
||||
lockfile: Some(&lockfile),
|
||||
lockfile: MaybeLazyLockfile::Loaded(Some(&lockfile)),
|
||||
lockfile_path: None,
|
||||
dependency_groups: [DependencyGroup::Prod],
|
||||
frozen_lockfile: true,
|
||||
@@ -3124,7 +3124,7 @@ async fn frozen_install_silently_swallows_unreachable_optional_tarball() {
|
||||
http_client_arc: std::sync::Arc::new(Default::default()),
|
||||
config,
|
||||
manifest: &manifest,
|
||||
lockfile: Some(&lockfile),
|
||||
lockfile: MaybeLazyLockfile::Loaded(Some(&lockfile)),
|
||||
lockfile_path: None,
|
||||
dependency_groups: [DependencyGroup::Prod, DependencyGroup::Optional],
|
||||
frozen_lockfile: true,
|
||||
@@ -3235,7 +3235,7 @@ async fn frozen_install_propagates_non_optional_fetch_failure() {
|
||||
http_client_arc: std::sync::Arc::new(Default::default()),
|
||||
config,
|
||||
manifest: &manifest,
|
||||
lockfile: Some(&lockfile),
|
||||
lockfile: MaybeLazyLockfile::Loaded(Some(&lockfile)),
|
||||
lockfile_path: None,
|
||||
dependency_groups: [DependencyGroup::Prod],
|
||||
frozen_lockfile: true,
|
||||
@@ -3346,7 +3346,7 @@ async fn frozen_install_no_optional_drops_optional_only_snapshots() {
|
||||
http_client_arc: std::sync::Arc::new(Default::default()),
|
||||
config,
|
||||
manifest: &manifest,
|
||||
lockfile: Some(&lockfile),
|
||||
lockfile: MaybeLazyLockfile::Loaded(Some(&lockfile)),
|
||||
lockfile_path: None,
|
||||
dependency_groups: [DependencyGroup::Prod],
|
||||
frozen_lockfile: true,
|
||||
@@ -3442,7 +3442,7 @@ async fn frozen_install_optional_included_surfaces_missing_metadata() {
|
||||
http_client_arc: std::sync::Arc::new(Default::default()),
|
||||
config,
|
||||
manifest: &manifest,
|
||||
lockfile: Some(&lockfile),
|
||||
lockfile: MaybeLazyLockfile::Loaded(Some(&lockfile)),
|
||||
lockfile_path: None,
|
||||
dependency_groups: [DependencyGroup::Prod, DependencyGroup::Optional],
|
||||
frozen_lockfile: true,
|
||||
@@ -3540,7 +3540,7 @@ async fn frozen_install_no_optional_keeps_shared_non_optional_snapshot() {
|
||||
http_client_arc: std::sync::Arc::new(Default::default()),
|
||||
config,
|
||||
manifest: &manifest,
|
||||
lockfile: Some(&lockfile),
|
||||
lockfile: MaybeLazyLockfile::Loaded(Some(&lockfile)),
|
||||
lockfile_path: None,
|
||||
// `--no-optional` shape: Optional NOT in the dispatch list.
|
||||
dependency_groups: [DependencyGroup::Prod],
|
||||
@@ -3639,7 +3639,7 @@ async fn hoisted_node_linker_empty_lockfile_writes_modules_yaml() {
|
||||
http_client_arc: std::sync::Arc::new(Default::default()),
|
||||
config,
|
||||
manifest: &manifest,
|
||||
lockfile: Some(&lockfile),
|
||||
lockfile: MaybeLazyLockfile::Loaded(Some(&lockfile)),
|
||||
lockfile_path: None,
|
||||
dependency_groups: [DependencyGroup::Prod],
|
||||
frozen_lockfile: true,
|
||||
@@ -3732,7 +3732,7 @@ async fn hoisted_node_linker_does_not_create_virtual_store_root() {
|
||||
http_client_arc: std::sync::Arc::new(Default::default()),
|
||||
config,
|
||||
manifest: &manifest,
|
||||
lockfile: Some(&lockfile),
|
||||
lockfile: MaybeLazyLockfile::Loaded(Some(&lockfile)),
|
||||
lockfile_path: None,
|
||||
dependency_groups: [DependencyGroup::Prod],
|
||||
frozen_lockfile: true,
|
||||
@@ -3833,7 +3833,7 @@ async fn frozen_lockfile_install_errors_when_no_variant_matches_host() {
|
||||
http_client_arc: std::sync::Arc::new(Default::default()),
|
||||
config,
|
||||
manifest: &manifest,
|
||||
lockfile: Some(&lockfile),
|
||||
lockfile: MaybeLazyLockfile::Loaded(Some(&lockfile)),
|
||||
lockfile_path: None,
|
||||
dependency_groups: [DependencyGroup::Prod, DependencyGroup::Optional],
|
||||
frozen_lockfile: true,
|
||||
@@ -3932,7 +3932,7 @@ async fn frozen_lockfile_install_skips_runtime_when_skip_runtimes_set() {
|
||||
http_client_arc: std::sync::Arc::new(Default::default()),
|
||||
config,
|
||||
manifest: &manifest,
|
||||
lockfile: Some(&lockfile),
|
||||
lockfile: MaybeLazyLockfile::Loaded(Some(&lockfile)),
|
||||
lockfile_path: None,
|
||||
dependency_groups: [DependencyGroup::Prod, DependencyGroup::Optional],
|
||||
frozen_lockfile: true,
|
||||
@@ -4035,7 +4035,7 @@ async fn install_rejects_invalid_minimum_release_age_exclude_pattern() {
|
||||
http_client_arc: std::sync::Arc::new(Default::default()),
|
||||
config,
|
||||
manifest: &manifest,
|
||||
lockfile: Some(&lockfile),
|
||||
lockfile: MaybeLazyLockfile::Loaded(Some(&lockfile)),
|
||||
lockfile_path: None,
|
||||
dependency_groups: [DependencyGroup::Prod],
|
||||
frozen_lockfile: true,
|
||||
@@ -4140,7 +4140,7 @@ async fn frozen_lockfile_gate_rejects_under_huge_minimum_release_age() {
|
||||
http_client_arc: std::sync::Arc::new(Default::default()),
|
||||
config,
|
||||
manifest: &manifest,
|
||||
lockfile: Some(&lockfile),
|
||||
lockfile: MaybeLazyLockfile::Loaded(Some(&lockfile)),
|
||||
lockfile_path: None,
|
||||
dependency_groups: [DependencyGroup::Prod],
|
||||
frozen_lockfile: true,
|
||||
@@ -4231,7 +4231,7 @@ async fn fresh_install_writes_pnpm_lock_yaml_with_expected_shape() {
|
||||
http_client_arc: std::sync::Arc::new(Default::default()),
|
||||
config,
|
||||
manifest: &manifest,
|
||||
lockfile: None,
|
||||
lockfile: MaybeLazyLockfile::Loaded(None),
|
||||
lockfile_path: None,
|
||||
dependency_groups: [DependencyGroup::Prod, DependencyGroup::Dev, DependencyGroup::Optional],
|
||||
frozen_lockfile: false,
|
||||
@@ -4320,7 +4320,7 @@ async fn fresh_install_splits_dev_and_prod_dependency_sections() {
|
||||
http_client_arc: std::sync::Arc::new(Default::default()),
|
||||
config,
|
||||
manifest: &manifest,
|
||||
lockfile: None,
|
||||
lockfile: MaybeLazyLockfile::Loaded(None),
|
||||
lockfile_path: None,
|
||||
dependency_groups: [DependencyGroup::Prod, DependencyGroup::Dev, DependencyGroup::Optional],
|
||||
frozen_lockfile: false,
|
||||
@@ -4395,7 +4395,7 @@ async fn fresh_install_records_user_written_specifier() {
|
||||
http_client_arc: std::sync::Arc::new(Default::default()),
|
||||
config,
|
||||
manifest: &manifest,
|
||||
lockfile: None,
|
||||
lockfile: MaybeLazyLockfile::Loaded(None),
|
||||
lockfile_path: None,
|
||||
dependency_groups: [DependencyGroup::Prod, DependencyGroup::Dev, DependencyGroup::Optional],
|
||||
frozen_lockfile: false,
|
||||
@@ -4466,7 +4466,7 @@ async fn fresh_install_lockfile_round_trips_through_load_save_load() {
|
||||
http_client_arc: std::sync::Arc::new(Default::default()),
|
||||
config,
|
||||
manifest: &manifest,
|
||||
lockfile: None,
|
||||
lockfile: MaybeLazyLockfile::Loaded(None),
|
||||
lockfile_path: None,
|
||||
dependency_groups: [DependencyGroup::Prod, DependencyGroup::Dev, DependencyGroup::Optional],
|
||||
frozen_lockfile: false,
|
||||
@@ -4536,7 +4536,7 @@ async fn fresh_install_with_lockfile_disabled_does_not_write_a_lockfile() {
|
||||
http_client_arc: std::sync::Arc::new(Default::default()),
|
||||
config,
|
||||
manifest: &manifest,
|
||||
lockfile: None,
|
||||
lockfile: MaybeLazyLockfile::Loaded(None),
|
||||
lockfile_path: None,
|
||||
dependency_groups: [DependencyGroup::Prod, DependencyGroup::Dev, DependencyGroup::Optional],
|
||||
frozen_lockfile: false,
|
||||
@@ -4609,7 +4609,7 @@ async fn fresh_install_also_writes_current_lockfile_under_virtual_store() {
|
||||
http_client_arc: std::sync::Arc::new(Default::default()),
|
||||
config,
|
||||
manifest: &manifest,
|
||||
lockfile: None,
|
||||
lockfile: MaybeLazyLockfile::Loaded(None),
|
||||
lockfile_path: None,
|
||||
dependency_groups: [DependencyGroup::Prod, DependencyGroup::Dev, DependencyGroup::Optional],
|
||||
frozen_lockfile: false,
|
||||
@@ -4698,7 +4698,7 @@ async fn fresh_install_with_lockfile_disabled_skips_current_lockfile_too() {
|
||||
http_client_arc: std::sync::Arc::new(Default::default()),
|
||||
config,
|
||||
manifest: &manifest,
|
||||
lockfile: None,
|
||||
lockfile: MaybeLazyLockfile::Loaded(None),
|
||||
lockfile_path: None,
|
||||
dependency_groups: [DependencyGroup::Prod, DependencyGroup::Dev, DependencyGroup::Optional],
|
||||
frozen_lockfile: false,
|
||||
@@ -4765,7 +4765,7 @@ async fn fresh_install_marks_optional_snapshots_in_pnpm_lock_yaml() {
|
||||
http_client_arc: std::sync::Arc::new(Default::default()),
|
||||
config,
|
||||
manifest: &manifest,
|
||||
lockfile: None,
|
||||
lockfile: MaybeLazyLockfile::Loaded(None),
|
||||
lockfile_path: None,
|
||||
dependency_groups: [DependencyGroup::Prod, DependencyGroup::Dev, DependencyGroup::Optional],
|
||||
frozen_lockfile: false,
|
||||
@@ -4857,7 +4857,7 @@ async fn fresh_install_hoisted_node_linker_records_modules_yaml() {
|
||||
http_client_arc: std::sync::Arc::new(Default::default()),
|
||||
config,
|
||||
manifest: &manifest,
|
||||
lockfile: None,
|
||||
lockfile: MaybeLazyLockfile::Loaded(None),
|
||||
lockfile_path: None,
|
||||
dependency_groups: [DependencyGroup::Prod],
|
||||
frozen_lockfile: false,
|
||||
@@ -4929,7 +4929,7 @@ async fn fresh_install_refuses_skip_runtimes_before_writing_state() {
|
||||
http_client_arc: std::sync::Arc::new(Default::default()),
|
||||
config,
|
||||
manifest: &manifest,
|
||||
lockfile: None,
|
||||
lockfile: MaybeLazyLockfile::Loaded(None),
|
||||
lockfile_path: None,
|
||||
dependency_groups: [DependencyGroup::Prod],
|
||||
frozen_lockfile: false,
|
||||
@@ -5003,7 +5003,7 @@ async fn prefer_frozen_lockfile_takes_frozen_path_when_lockfile_is_fresh() {
|
||||
http_client_arc: std::sync::Arc::new(Default::default()),
|
||||
config,
|
||||
manifest: &manifest,
|
||||
lockfile: Some(&lockfile),
|
||||
lockfile: MaybeLazyLockfile::Loaded(Some(&lockfile)),
|
||||
lockfile_path: None,
|
||||
dependency_groups: [DependencyGroup::Prod],
|
||||
// No `--frozen-lockfile`; the dispatch must auto-go-frozen
|
||||
@@ -5080,7 +5080,7 @@ async fn no_prefer_frozen_lockfile_flag_forces_fresh_resolve() {
|
||||
http_client_arc: std::sync::Arc::new(Default::default()),
|
||||
config,
|
||||
manifest: &manifest,
|
||||
lockfile: Some(&lockfile),
|
||||
lockfile: MaybeLazyLockfile::Loaded(Some(&lockfile)),
|
||||
lockfile_path: None,
|
||||
dependency_groups: [DependencyGroup::Prod],
|
||||
frozen_lockfile: false,
|
||||
@@ -5153,7 +5153,7 @@ async fn stale_lockfile_under_no_flag_falls_through_to_fresh_resolve() {
|
||||
http_client_arc: std::sync::Arc::new(Default::default()),
|
||||
config,
|
||||
manifest: &manifest,
|
||||
lockfile: Some(&lockfile),
|
||||
lockfile: MaybeLazyLockfile::Loaded(Some(&lockfile)),
|
||||
lockfile_path: None,
|
||||
dependency_groups: [DependencyGroup::Prod],
|
||||
frozen_lockfile: false,
|
||||
@@ -5410,7 +5410,7 @@ async fn frozen_install_short_circuits_when_modules_and_lockfile_are_consistent(
|
||||
http_client_arc: std::sync::Arc::new(Default::default()),
|
||||
config,
|
||||
manifest: &manifest,
|
||||
lockfile: Some(&lockfile),
|
||||
lockfile: MaybeLazyLockfile::Loaded(Some(&lockfile)),
|
||||
lockfile_path: None,
|
||||
dependency_groups: [DependencyGroup::Prod],
|
||||
frozen_lockfile: true,
|
||||
@@ -5594,7 +5594,7 @@ async fn optimistic_repeat_install_skips_entire_pipeline_when_state_is_fresh() {
|
||||
http_client_arc: std::sync::Arc::new(Default::default()),
|
||||
config,
|
||||
manifest: &manifest,
|
||||
lockfile: Some(&lockfile),
|
||||
lockfile: MaybeLazyLockfile::Loaded(Some(&lockfile)),
|
||||
lockfile_path: None,
|
||||
dependency_groups: [DependencyGroup::Prod],
|
||||
frozen_lockfile: false,
|
||||
@@ -5644,6 +5644,89 @@ async fn optimistic_repeat_install_skips_entire_pipeline_when_state_is_fresh() {
|
||||
);
|
||||
}
|
||||
|
||||
/// The synchronous pre-runtime twin of the short-circuit
|
||||
/// ([`install_already_up_to_date`]) must reach the same verdict from
|
||||
/// the same on-disk state — and flip to `None` (fall through to the
|
||||
/// full install) as soon as a manifest outdates the recorded
|
||||
/// validation timestamp.
|
||||
#[test]
|
||||
fn sync_fast_path_matches_optimistic_short_circuit() {
|
||||
let dir = tempdir().unwrap();
|
||||
let project_root = dir.path().join("project");
|
||||
let modules_dir = project_root.join("node_modules");
|
||||
|
||||
std::fs::create_dir_all(&modules_dir).expect("create modules dir so the deps gate passes");
|
||||
let manifest_path = project_root.join("package.json");
|
||||
let mut manifest = PackageManifest::create_if_needed(manifest_path.clone()).unwrap();
|
||||
manifest.add_dependency("sibling", "link:../sibling", DependencyGroup::Prod).unwrap();
|
||||
manifest.save().unwrap();
|
||||
std::fs::write(project_root.join("pnpm-lock.yaml"), "lockfileVersion: '9.0'\n")
|
||||
.expect("seed pnpm-lock.yaml");
|
||||
|
||||
let mut config = Config::new();
|
||||
config.lockfile = false;
|
||||
config.store_dir = dir.path().join("pacquet-store").into();
|
||||
config.modules_dir = modules_dir.clone();
|
||||
config.virtual_store_dir = modules_dir.join(".pacquet");
|
||||
let config = config.leak();
|
||||
|
||||
let included = pacquet_modules_yaml::IncludedDependencies {
|
||||
dependencies: true,
|
||||
dev_dependencies: false,
|
||||
optional_dependencies: false,
|
||||
};
|
||||
let mut projects = std::collections::BTreeMap::new();
|
||||
projects.insert(
|
||||
project_root.to_string_lossy().into_owned(),
|
||||
workspace_state::ProjectEntry {
|
||||
name: Some("project".to_string()),
|
||||
version: Some("1.0.0".to_string()),
|
||||
},
|
||||
);
|
||||
let settings = crate::optimistic_repeat_install::current_settings(
|
||||
config,
|
||||
pacquet_config::NodeLinker::Isolated,
|
||||
included,
|
||||
);
|
||||
workspace_state::update_workspace_state(
|
||||
&project_root,
|
||||
&pacquet_workspace_state::WorkspaceState {
|
||||
last_validated_timestamp: pacquet_workspace_state::now_millis() + 60_000,
|
||||
projects,
|
||||
pnpmfiles: Vec::new(),
|
||||
filtered_install: false,
|
||||
config_dependencies: None,
|
||||
settings,
|
||||
},
|
||||
)
|
||||
.expect("seed workspace state");
|
||||
|
||||
let check = UpToDateFastPathCheck {
|
||||
config,
|
||||
manifest: &manifest,
|
||||
dependency_groups: vec![DependencyGroup::Prod],
|
||||
node_linker: pacquet_config::NodeLinker::Isolated,
|
||||
};
|
||||
let root = install_already_up_to_date(&check);
|
||||
assert_eq!(root.as_deref(), Some(&*project_root), "fresh state must short-circuit");
|
||||
|
||||
// Outdate the manifest relative to the recorded timestamp: the
|
||||
// fast path must decline and leave the decision to the full
|
||||
// install. The far-future mtime defeats filesystem mtime
|
||||
// resolution without sleeping.
|
||||
let future = std::time::SystemTime::now() + std::time::Duration::from_mins(2);
|
||||
let file = std::fs::OpenOptions::new()
|
||||
.append(true)
|
||||
.open(&manifest_path)
|
||||
.expect("open manifest for mtime bump");
|
||||
file.set_modified(future).expect("bump manifest mtime");
|
||||
drop(file);
|
||||
// The manifest content still matches no lockfile (config.lockfile
|
||||
// is off and no current lockfile exists), so the content re-check
|
||||
// cannot vouch for it either.
|
||||
assert_eq!(install_already_up_to_date(&check), None, "modified manifest must fall through");
|
||||
}
|
||||
|
||||
/// `--frozen-lockfile` disables the optimistic short-circuit because
|
||||
/// a headless install must always fail loudly on a missing or stale
|
||||
/// lockfile (matching pnpm's `installDeps` not calling
|
||||
@@ -5748,7 +5831,7 @@ async fn frozen_lockfile_disables_optimistic_short_circuit() {
|
||||
http_client_arc: std::sync::Arc::new(Default::default()),
|
||||
config,
|
||||
manifest: &manifest,
|
||||
lockfile: Some(&lockfile),
|
||||
lockfile: MaybeLazyLockfile::Loaded(Some(&lockfile)),
|
||||
lockfile_path: None,
|
||||
dependency_groups: [DependencyGroup::Prod],
|
||||
// The only difference vs the optimistic test above.
|
||||
@@ -5786,6 +5869,145 @@ async fn frozen_lockfile_disables_optimistic_short_circuit() {
|
||||
// absent so the polarity of the gate is clear.
|
||||
}
|
||||
|
||||
/// `add` / `remove` mutate the manifest in memory and persist it only
|
||||
/// after `Install::run` returns, so the on-disk mtimes the optimistic
|
||||
/// check reads still describe the pre-mutation project. A partial
|
||||
/// install (`is_full_install: false`) must therefore never take the
|
||||
/// optimistic short-circuit — otherwise a fresh workspace state would
|
||||
/// read as "already up to date" and the mutation would never be
|
||||
/// resolved or materialized. Mirrors upstream `installDeps` calling
|
||||
/// `checkDepsStatus` only for the plain-install mutation.
|
||||
#[tokio::test]
|
||||
async fn partial_install_disables_optimistic_short_circuit() {
|
||||
static EVENTS: Mutex<Vec<LogEvent>> = Mutex::new(Vec::new());
|
||||
EVENTS.lock().unwrap().clear();
|
||||
|
||||
struct RecordingReporter;
|
||||
impl Reporter for RecordingReporter {
|
||||
fn emit(event: &LogEvent) {
|
||||
EVENTS.lock().unwrap().push(event.clone());
|
||||
}
|
||||
}
|
||||
|
||||
let dir = tempdir().unwrap();
|
||||
let store_dir = dir.path().join("pacquet-store");
|
||||
let project_root = dir.path().join("project");
|
||||
let modules_dir = project_root.join("node_modules");
|
||||
let virtual_store_dir = modules_dir.join(".pacquet");
|
||||
|
||||
std::fs::create_dir_all(&project_root).expect("create project root");
|
||||
let manifest_path = project_root.join("package.json");
|
||||
let mut manifest = PackageManifest::create_if_needed(manifest_path).unwrap();
|
||||
manifest.add_dependency("sibling", "link:../sibling", DependencyGroup::Prod).unwrap();
|
||||
manifest.save().unwrap();
|
||||
|
||||
let mut config = Config::new();
|
||||
config.lockfile = false;
|
||||
config.store_dir = store_dir.into();
|
||||
config.modules_dir = modules_dir.clone();
|
||||
config.virtual_store_dir = virtual_store_dir.clone();
|
||||
let config = config.leak();
|
||||
|
||||
let lockfile: Lockfile = serde_saphyr::from_str(text_block! {
|
||||
"lockfileVersion: '9.0'"
|
||||
"importers:"
|
||||
" .:"
|
||||
" dependencies:"
|
||||
" sibling:"
|
||||
" specifier: link:../sibling"
|
||||
" version: link:../sibling"
|
||||
"packages: {}"
|
||||
"snapshots: {}"
|
||||
})
|
||||
.expect("parse lockfile");
|
||||
|
||||
let included = pacquet_modules_yaml::IncludedDependencies {
|
||||
dependencies: true,
|
||||
dev_dependencies: false,
|
||||
optional_dependencies: false,
|
||||
};
|
||||
|
||||
// Seed the same state the optimistic test uses, so the only
|
||||
// difference between the two is `is_full_install`.
|
||||
let seed_modules = Modules {
|
||||
layout_version: Some(LayoutVersion),
|
||||
node_linker: Some(NodeLinker::Isolated),
|
||||
included,
|
||||
hoist_pattern: config.hoist_pattern.clone(),
|
||||
public_hoist_pattern: config.public_hoist_pattern.clone(),
|
||||
store_dir: config.store_dir.display().to_string(),
|
||||
virtual_store_dir: config.effective_virtual_store_dir().to_string_lossy().into_owned(),
|
||||
virtual_store_dir_max_length: config.virtual_store_dir_max_length,
|
||||
..Default::default()
|
||||
};
|
||||
write_modules_manifest::<Host>(&modules_dir, seed_modules).expect("seed .modules.yaml");
|
||||
lockfile.save_current_to_virtual_store_dir(&virtual_store_dir).expect("seed current lockfile");
|
||||
|
||||
let mut projects = std::collections::BTreeMap::new();
|
||||
projects.insert(
|
||||
project_root.to_string_lossy().into_owned(),
|
||||
workspace_state::ProjectEntry {
|
||||
name: Some("project".to_string()),
|
||||
version: Some("1.0.0".to_string()),
|
||||
},
|
||||
);
|
||||
let settings = crate::optimistic_repeat_install::current_settings(
|
||||
config,
|
||||
pacquet_config::NodeLinker::Isolated,
|
||||
included,
|
||||
);
|
||||
workspace_state::update_workspace_state(
|
||||
&project_root,
|
||||
&pacquet_workspace_state::WorkspaceState {
|
||||
last_validated_timestamp: pacquet_workspace_state::now_millis() + 60_000,
|
||||
projects,
|
||||
pnpmfiles: Vec::new(),
|
||||
filtered_install: false,
|
||||
config_dependencies: None,
|
||||
settings,
|
||||
},
|
||||
)
|
||||
.expect("seed workspace state");
|
||||
|
||||
Install {
|
||||
tarball_mem_cache: Default::default(),
|
||||
http_client: &Default::default(),
|
||||
http_client_arc: std::sync::Arc::new(Default::default()),
|
||||
config,
|
||||
manifest: &manifest,
|
||||
lockfile: MaybeLazyLockfile::Loaded(Some(&lockfile)),
|
||||
lockfile_path: None,
|
||||
dependency_groups: [DependencyGroup::Prod],
|
||||
frozen_lockfile: false,
|
||||
prefer_frozen_lockfile: None,
|
||||
ignore_manifest_check: false,
|
||||
skip_runtimes: false,
|
||||
trust_lockfile: true,
|
||||
update_checksums: false,
|
||||
// The only difference vs the optimistic test above.
|
||||
is_full_install: false,
|
||||
supported_architectures: None,
|
||||
node_linker: pacquet_config::NodeLinker::Isolated,
|
||||
lockfile_only: false,
|
||||
resolved_packages: &Default::default(),
|
||||
update_seed_policy: crate::UpdateSeedPolicy::KeepAll,
|
||||
auth_override: None,
|
||||
resolution_observer: None,
|
||||
}
|
||||
.run::<RecordingReporter>()
|
||||
.await
|
||||
.expect("partial install must still succeed via the regular dispatch");
|
||||
|
||||
let captured = EVENTS.lock().unwrap();
|
||||
assert!(
|
||||
!captured.iter().any(|event| matches!(
|
||||
event,
|
||||
LogEvent::Pnpm(log) if log.message == "Already up to date"
|
||||
)),
|
||||
"the optimistic 'Already up to date' log MUST NOT fire for a partial install; got events: {captured:#?}",
|
||||
);
|
||||
}
|
||||
|
||||
/// Regression: a single-project install with NO lockfile anywhere —
|
||||
/// `pnpm-lock.yaml` is gone and the virtual store has no current
|
||||
/// `lock.yaml` to stand in for it — must NOT short-circuit, even when
|
||||
@@ -5894,7 +6116,7 @@ async fn optimistic_repeat_install_does_not_short_circuit_when_lockfile_missing(
|
||||
http_client_arc: std::sync::Arc::new(Default::default()),
|
||||
config,
|
||||
manifest: &manifest,
|
||||
lockfile: None,
|
||||
lockfile: MaybeLazyLockfile::Loaded(None),
|
||||
lockfile_path: None,
|
||||
dependency_groups: [DependencyGroup::Prod],
|
||||
frozen_lockfile: false,
|
||||
@@ -5977,7 +6199,7 @@ async fn optimistic_repeat_install_round_trips_on_single_project_install() {
|
||||
http_client_arc: std::sync::Arc::new(Default::default()),
|
||||
config,
|
||||
manifest: &manifest,
|
||||
lockfile: None,
|
||||
lockfile: MaybeLazyLockfile::Loaded(None),
|
||||
lockfile_path: None,
|
||||
dependency_groups: [DependencyGroup::Prod],
|
||||
frozen_lockfile: false,
|
||||
@@ -6033,7 +6255,7 @@ async fn optimistic_repeat_install_round_trips_on_single_project_install() {
|
||||
// *before* the lockfile is even loaded. (Matching pnpm's
|
||||
// dispatch ordering: `checkDepsStatus` runs before any
|
||||
// lockfile parse.)
|
||||
lockfile: None,
|
||||
lockfile: MaybeLazyLockfile::Loaded(None),
|
||||
lockfile_path: None,
|
||||
dependency_groups: [DependencyGroup::Prod],
|
||||
frozen_lockfile: false,
|
||||
@@ -6121,7 +6343,7 @@ async fn fresh_install_records_lockfile_verification_for_mtime_bypassed_noop() {
|
||||
http_client_arc: std::sync::Arc::new(Default::default()),
|
||||
config,
|
||||
manifest: &manifest,
|
||||
lockfile: None,
|
||||
lockfile: MaybeLazyLockfile::Loaded(None),
|
||||
lockfile_path: None,
|
||||
dependency_groups: [DependencyGroup::Prod],
|
||||
frozen_lockfile: false,
|
||||
@@ -6185,7 +6407,7 @@ async fn fresh_install_records_lockfile_verification_for_mtime_bypassed_noop() {
|
||||
http_client_arc: std::sync::Arc::new(Default::default()),
|
||||
config: second_config,
|
||||
manifest: &touched_manifest,
|
||||
lockfile: Some(&wanted_lockfile),
|
||||
lockfile: MaybeLazyLockfile::Loaded(Some(&wanted_lockfile)),
|
||||
lockfile_path: Some(&lockfile_path),
|
||||
dependency_groups: [DependencyGroup::Prod],
|
||||
frozen_lockfile: false,
|
||||
@@ -6272,7 +6494,7 @@ async fn install_then_go_offline() -> (tempfile::TempDir, &'static Config, Packa
|
||||
http_client_arc: std::sync::Arc::new(Default::default()),
|
||||
config,
|
||||
manifest: &manifest,
|
||||
lockfile: None,
|
||||
lockfile: MaybeLazyLockfile::Loaded(None),
|
||||
lockfile_path: None,
|
||||
dependency_groups: [DependencyGroup::Prod],
|
||||
frozen_lockfile: false,
|
||||
@@ -6360,7 +6582,7 @@ async fn optimistic_repeat_install_short_circuits_offline_when_touched_manifest_
|
||||
http_client_arc: std::sync::Arc::new(Default::default()),
|
||||
config: offline_config,
|
||||
manifest: &touched_manifest,
|
||||
lockfile: Some(&wanted_lockfile),
|
||||
lockfile: MaybeLazyLockfile::Loaded(Some(&wanted_lockfile)),
|
||||
lockfile_path: Some(&lockfile_path),
|
||||
dependency_groups: [DependencyGroup::Prod],
|
||||
frozen_lockfile: false,
|
||||
@@ -6440,7 +6662,7 @@ async fn optimistic_repeat_install_restores_missing_lockfile_offline() {
|
||||
http_client_arc: std::sync::Arc::new(Default::default()),
|
||||
config: offline_config,
|
||||
manifest: &touched_manifest,
|
||||
lockfile: None,
|
||||
lockfile: MaybeLazyLockfile::Loaded(None),
|
||||
lockfile_path: None,
|
||||
dependency_groups: [DependencyGroup::Prod],
|
||||
frozen_lockfile: false,
|
||||
@@ -6584,7 +6806,7 @@ async fn fresh_lockfile_only_with_overrides(
|
||||
http_client_arc: std::sync::Arc::new(Default::default()),
|
||||
config,
|
||||
manifest: &manifest,
|
||||
lockfile: None,
|
||||
lockfile: MaybeLazyLockfile::Loaded(None),
|
||||
lockfile_path: None,
|
||||
dependency_groups: [DependencyGroup::Prod],
|
||||
frozen_lockfile: false,
|
||||
@@ -6690,7 +6912,7 @@ async fn fresh_install_applies_package_extensions_to_dependency_manifest() {
|
||||
http_client_arc: std::sync::Arc::new(Default::default()),
|
||||
config,
|
||||
manifest: &manifest,
|
||||
lockfile: None,
|
||||
lockfile: MaybeLazyLockfile::Loaded(None),
|
||||
lockfile_path: None,
|
||||
dependency_groups: [DependencyGroup::Prod],
|
||||
frozen_lockfile: false,
|
||||
@@ -6790,7 +7012,7 @@ async fn frozen_lockfile_errors_when_package_extensions_drift_from_lockfile() {
|
||||
http_client_arc: std::sync::Arc::new(Default::default()),
|
||||
config,
|
||||
manifest: &manifest,
|
||||
lockfile: Some(&lockfile),
|
||||
lockfile: MaybeLazyLockfile::Loaded(Some(&lockfile)),
|
||||
lockfile_path: None,
|
||||
dependency_groups: [DependencyGroup::Prod],
|
||||
frozen_lockfile: true,
|
||||
@@ -6872,7 +7094,7 @@ async fn install_with_pnpmfile_reporter<Reporter: self::Reporter + 'static>(
|
||||
http_client_arc: std::sync::Arc::new(Default::default()),
|
||||
config,
|
||||
manifest: &manifest,
|
||||
lockfile: None,
|
||||
lockfile: MaybeLazyLockfile::Loaded(None),
|
||||
lockfile_path: None,
|
||||
dependency_groups: [DependencyGroup::Prod, DependencyGroup::Dev, DependencyGroup::Optional],
|
||||
frozen_lockfile: false,
|
||||
|
||||
@@ -56,7 +56,7 @@ use std::{
|
||||
|
||||
use pacquet_catalogs_types::Catalogs;
|
||||
use pacquet_config::{Config, LinkWorkspacePackages, NodeLinker};
|
||||
use pacquet_lockfile::{ImporterDepVersion, Lockfile, ProjectSnapshot};
|
||||
use pacquet_lockfile::{ImporterDepVersion, Lockfile, MaybeLazyLockfile, ProjectSnapshot};
|
||||
use pacquet_modules_yaml::IncludedDependencies;
|
||||
use pacquet_package_manifest::{DependencyGroup, PackageManifest};
|
||||
use pacquet_workspace_state::{
|
||||
@@ -107,15 +107,16 @@ pub struct OptimisticRepeatInstallCheck<'a> {
|
||||
/// the outer `try` converts into `upToDate: false` — and keys its
|
||||
/// comparisons off the lockfile mtimes instead.
|
||||
pub is_workspace_install: bool,
|
||||
/// The wanted lockfile as loaded by the CLI (`None` when
|
||||
/// `pnpm-lock.yaml` is absent or empty). Consulted only by the
|
||||
/// modified-manifests content re-check; the pure-mtime fast path
|
||||
/// never reads it. When `None` and `<virtual_store_dir>/lock.yaml`
|
||||
/// exists, the current lockfile stands in as the wanted one — it
|
||||
/// records exactly what the previous install materialized — and
|
||||
/// `pnpm-lock.yaml` is regenerated from it before the check
|
||||
/// reports up-to-date.
|
||||
pub lockfile: Option<&'a Lockfile>,
|
||||
/// The wanted lockfile (`None` once loaded when `pnpm-lock.yaml`
|
||||
/// is absent or empty). Consulted only by the modified-manifests
|
||||
/// content re-check; the pure-mtime fast path never reads it —
|
||||
/// which is why it arrives lazily, so the common repeat-install
|
||||
/// run skips the YAML parse entirely. When absent and
|
||||
/// `<virtual_store_dir>/lock.yaml` exists, the current lockfile
|
||||
/// stands in as the wanted one — it records exactly what the
|
||||
/// previous install materialized — and `pnpm-lock.yaml` is
|
||||
/// regenerated from it before the check reports up-to-date.
|
||||
pub lockfile: MaybeLazyLockfile<'a>,
|
||||
/// Workspace catalogs, for resolving `catalog:` values inside
|
||||
/// `pnpm.overrides` before the lockfile settings comparison.
|
||||
pub catalogs: &'a Catalogs,
|
||||
@@ -287,7 +288,7 @@ fn regenerate_wanted_lockfile_if_missing(
|
||||
check: &OptimisticRepeatInstallCheck<'_>,
|
||||
loaded_current: Option<Lockfile>,
|
||||
) -> Result<(), &'static str> {
|
||||
if check.lockfile.is_some() || !check.config.lockfile {
|
||||
if check.lockfile.is_loaded_or_on_disk() || !check.config.lockfile {
|
||||
return Ok(());
|
||||
}
|
||||
let current = match loaded_current {
|
||||
@@ -340,6 +341,7 @@ fn modified_manifests_match_lockfile(
|
||||
} = check;
|
||||
let mut loaded_current: Option<Lockfile> = None;
|
||||
let mut wanted_is_current = false;
|
||||
let lockfile = lockfile.get().map_err(|_| "the wanted lockfile cannot be read or parsed")?;
|
||||
let (wanted, wanted_mtime_ms): (&Lockfile, i64) = if let Some(wanted) = lockfile {
|
||||
let Some(mtime) = mtime_ms(&workspace_root.join(Lockfile::FILE_NAME)) else {
|
||||
return Err(
|
||||
|
||||
@@ -2,7 +2,7 @@ use super::{
|
||||
Decision, OptimisticRepeatInstallCheck, check_optimistic_repeat_install, current_settings,
|
||||
};
|
||||
use pacquet_config::Config;
|
||||
use pacquet_lockfile::Lockfile;
|
||||
use pacquet_lockfile::{Lockfile, MaybeLazyLockfile};
|
||||
use pacquet_modules_yaml::IncludedDependencies;
|
||||
use pacquet_package_manifest::PackageManifest;
|
||||
use pacquet_workspace_state::{
|
||||
@@ -31,7 +31,7 @@ fn check(
|
||||
included: isolated_included(),
|
||||
project_manifests,
|
||||
is_workspace_install: false,
|
||||
lockfile: None,
|
||||
lockfile: MaybeLazyLockfile::Loaded(None),
|
||||
catalogs: &BTreeMap::default(),
|
||||
})
|
||||
}
|
||||
@@ -1112,7 +1112,7 @@ fn returns_skipped_when_sibling_node_modules_missing_for_project_with_deps() {
|
||||
(sibling_dir, &sibling_manifest),
|
||||
],
|
||||
is_workspace_install: true,
|
||||
lockfile: None,
|
||||
lockfile: MaybeLazyLockfile::Loaded(None),
|
||||
catalogs: &BTreeMap::default(),
|
||||
});
|
||||
assert!(matches!(decision, Decision::Skipped { reason } if reason.contains("node_modules")));
|
||||
@@ -1174,7 +1174,7 @@ fn returns_up_to_date_in_workspace_mode_without_lockfile() {
|
||||
included: isolated_included(),
|
||||
project_manifests: &[(dir.path().to_path_buf(), &manifest)],
|
||||
is_workspace_install: true,
|
||||
lockfile: None,
|
||||
lockfile: MaybeLazyLockfile::Loaded(None),
|
||||
catalogs: &BTreeMap::default(),
|
||||
});
|
||||
assert_eq!(decision, Decision::UpToDate);
|
||||
@@ -1249,7 +1249,7 @@ fn content_check_decision(
|
||||
included: isolated_included(),
|
||||
project_manifests,
|
||||
is_workspace_install,
|
||||
lockfile: lockfile.as_ref(),
|
||||
lockfile: MaybeLazyLockfile::Loaded(lockfile.as_ref()),
|
||||
catalogs: &BTreeMap::default(),
|
||||
})
|
||||
}
|
||||
@@ -1423,7 +1423,7 @@ importers:
|
||||
(sibling_dir, &sibling_manifest),
|
||||
],
|
||||
is_workspace_install: true,
|
||||
lockfile: lockfile.as_ref(),
|
||||
lockfile: MaybeLazyLockfile::Loaded(lockfile.as_ref()),
|
||||
catalogs: &BTreeMap::default(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ use crate::{Install, InstallError, ResolvedPackages, UpdateSeedPolicy};
|
||||
use derive_more::{Display, Error};
|
||||
use miette::Diagnostic;
|
||||
use pacquet_config::Config;
|
||||
use pacquet_lockfile::Lockfile;
|
||||
use pacquet_lockfile::{Lockfile, MaybeLazyLockfile};
|
||||
use pacquet_network::ThrottledClient;
|
||||
use pacquet_package_manifest::{DependencyGroup, PackageManifest, PackageManifestError};
|
||||
use pacquet_reporter::{LogEvent, LogLevel, PackageManifestLog, PackageManifestMessage, Reporter};
|
||||
@@ -105,7 +105,7 @@ impl Remove<'_> {
|
||||
http_client_arc,
|
||||
config,
|
||||
manifest,
|
||||
lockfile,
|
||||
lockfile: MaybeLazyLockfile::Loaded(lockfile),
|
||||
lockfile_path,
|
||||
// `pnpm remove`'s `include` defaults to every dependency
|
||||
// group (`production`/`dev`/`optional` !== false), so the
|
||||
|
||||
@@ -10,7 +10,7 @@ use pacquet_catalogs_config::{
|
||||
use pacquet_catalogs_protocol_parser::parse_catalog_protocol;
|
||||
use pacquet_catalogs_types::Catalogs;
|
||||
use pacquet_config::{CatalogMode, Config, matcher::create_matcher};
|
||||
use pacquet_lockfile::Lockfile;
|
||||
use pacquet_lockfile::{Lockfile, MaybeLazyLockfile};
|
||||
use pacquet_network::ThrottledClient;
|
||||
use pacquet_package_manifest::{DependencyGroup, PackageManifest, PackageManifestError};
|
||||
use pacquet_registry::{PackageTag, PackageVersion};
|
||||
@@ -492,7 +492,7 @@ impl Update<'_> {
|
||||
http_client_arc,
|
||||
config,
|
||||
manifest,
|
||||
lockfile,
|
||||
lockfile: MaybeLazyLockfile::Loaded(lockfile),
|
||||
lockfile_path,
|
||||
// `include` is always all-true for updates: the materialized
|
||||
// `node_modules` layout must not change just because the
|
||||
|
||||
@@ -151,9 +151,11 @@ pub async fn fetch_full_metadata_cached(
|
||||
pkg_name: pkg_name.to_string(),
|
||||
});
|
||||
};
|
||||
return load_meta_async(Some(&path)).await.ok_or_else(|| {
|
||||
let meta = load_meta_async(Some(&path)).await.ok_or_else(|| {
|
||||
FetchMetadataError::CacheMissingAfter304 { pkg_name: pkg_name.to_string() }
|
||||
});
|
||||
})?;
|
||||
renew_mirror_freshness(&path);
|
||||
return Ok(meta);
|
||||
}
|
||||
|
||||
let response = response
|
||||
@@ -214,5 +216,37 @@ pub async fn fetch_full_metadata_cached(
|
||||
meta.pipe(Ok)
|
||||
}
|
||||
|
||||
/// Bump the mirror file's mtime to "now" after a `304 Not Modified`.
|
||||
///
|
||||
/// The publishedBy mtime shortcut in [`crate::pick_package()`] treats
|
||||
/// a mirror younger than the maturity cutoff as authoritative. A 304
|
||||
/// proves the cached packument equals the registry's current one, so
|
||||
/// the validation clock legitimately restarts here — without the
|
||||
/// touch, a mirror older than `minimumReleaseAge` re-validates every
|
||||
/// package on every subsequent install, because a 304 never rewrites
|
||||
/// the file.
|
||||
///
|
||||
/// The append-mode open carries the write-attributes access Windows'
|
||||
/// `set_modified` needs (a read-only handle cannot set file times
|
||||
/// there). The read-only fallback covers Unix mirrors whose mode
|
||||
/// dropped write permission: `futimens`-style timestamp syscalls
|
||||
/// require ownership, not write access. Best-effort: a failure only
|
||||
/// costs the next install another conditional request.
|
||||
fn renew_mirror_freshness(path: &Path) {
|
||||
let touched = std::fs::OpenOptions::new()
|
||||
.append(true)
|
||||
.open(path)
|
||||
.or_else(|_| std::fs::File::open(path))
|
||||
.and_then(|file| file.set_modified(std::time::SystemTime::now()));
|
||||
if let Err(error) = touched {
|
||||
tracing::debug!(
|
||||
target: "pacquet_resolving_npm_resolver::cache",
|
||||
?error,
|
||||
path = %path.display(),
|
||||
"could not renew mirror freshness after 304",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
@@ -112,6 +112,65 @@ async fn warm_cache_serves_from_mirror_on_304() {
|
||||
assert_eq!(second_pkg.published_at("1.0.0"), Some("2025-01-10T08:30:00.000Z"));
|
||||
}
|
||||
|
||||
/// A 304 renews the mirror's mtime so the publishedBy freshness
|
||||
/// shortcut in `pick_package` can fire again on the next install —
|
||||
/// without the touch, a mirror older than `minimumReleaseAge`
|
||||
/// re-validates on every subsequent install forever.
|
||||
#[tokio::test]
|
||||
async fn a_304_renews_the_mirror_mtime() {
|
||||
let mut server = mockito::Server::new_async().await;
|
||||
server
|
||||
.mock("GET", "/acme")
|
||||
.with_status(200)
|
||||
.with_header("etag", r#"W/"v1""#)
|
||||
.with_body(PACKAGE_BODY)
|
||||
.expect(1)
|
||||
.create_async()
|
||||
.await;
|
||||
server
|
||||
.mock("GET", "/acme")
|
||||
.match_header("if-none-match", r#"W/"v1""#)
|
||||
.with_status(304)
|
||||
.expect(1)
|
||||
.create_async()
|
||||
.await;
|
||||
|
||||
let cache = TempDir::new().expect("tempdir");
|
||||
let registry = format!("{}/", server.url());
|
||||
let http_client = ThrottledClient::default();
|
||||
let auth_headers = AuthHeaders::default();
|
||||
let opts = FetchFullMetadataCachedOptions {
|
||||
registry: ®istry,
|
||||
http_client: &http_client,
|
||||
auth_headers: &auth_headers,
|
||||
cache_dir: Some(cache.path()),
|
||||
full_metadata: true,
|
||||
retry_opts: no_retry_opts(),
|
||||
};
|
||||
|
||||
fetch_full_metadata_cached("acme", &opts).await.expect("200 populates cache");
|
||||
let mirror_path =
|
||||
get_pkg_mirror_path(cache.path(), FULL_META_DIR, ®istry, "acme").expect("path");
|
||||
|
||||
// Age the mirror far past any maturity cutoff.
|
||||
let aged = std::time::SystemTime::now() - std::time::Duration::from_hours(365 * 24);
|
||||
std::fs::OpenOptions::new()
|
||||
.append(true)
|
||||
.open(&mirror_path)
|
||||
.expect("open mirror")
|
||||
.set_modified(aged)
|
||||
.expect("age mirror");
|
||||
|
||||
fetch_full_metadata_cached("acme", &opts).await.expect("304 reads from mirror");
|
||||
|
||||
let renewed = std::fs::metadata(&mirror_path).expect("stat mirror").modified().expect("mtime");
|
||||
let age = std::time::SystemTime::now().duration_since(renewed).expect("mtime in the past");
|
||||
assert!(
|
||||
age < std::time::Duration::from_mins(1),
|
||||
"mirror mtime must be renewed by the 304; still {age:?} old",
|
||||
);
|
||||
}
|
||||
|
||||
/// Warm cache + stale `If-None-Match` → 200 → mirror is overwritten
|
||||
/// with the new body + new etag.
|
||||
#[tokio::test]
|
||||
|
||||
@@ -163,13 +163,18 @@ fn slow_start_ramps_per_connection_throughput() {
|
||||
start.elapsed()
|
||||
};
|
||||
|
||||
let flat = timed_transfer(false).min(timed_transfer(false));
|
||||
let ramped = timed_transfer(true);
|
||||
let best_of = |samples: u32, slow_start: bool| {
|
||||
(0..samples).map(|_| timed_transfer(slow_start)).min().expect("at least one sample")
|
||||
};
|
||||
// Flat: ~2×20ms latency + 256KiB/10MB/s ≈ 66 ms. Ramped: the first
|
||||
// windows (14.6 KB and doubling) each serialize at cwnd/RTT, adding ~3-4
|
||||
// window-times before the rate approaches the cap. Compare against the
|
||||
// faster flat sample so scheduler noise on one baseline run does not hide
|
||||
// the slow-start overhead.
|
||||
// window-times before the rate approaches the cap. Compare the best of
|
||||
// several samples on each side: the minimum is the noise-resistant
|
||||
// estimator on a loaded CI runner — scheduler stalls only ever inflate a
|
||||
// sample, while slow start's ramp overhead is structural and survives in
|
||||
// every sample, including the minimum.
|
||||
let flat = best_of(3, false);
|
||||
let ramped = best_of(3, true);
|
||||
assert!(
|
||||
ramped > flat + Duration::from_millis(25),
|
||||
"slow start should add ramp-up time: flat {flat:?} vs ramped {ramped:?}",
|
||||
|
||||
222
pnpm-lock.yaml
generated
222
pnpm-lock.yaml
generated
@@ -621,8 +621,8 @@ catalogs:
|
||||
specifier: ^3.0.1
|
||||
version: 3.0.1
|
||||
esbuild:
|
||||
specifier: ^0.28.0
|
||||
version: 0.28.0
|
||||
specifier: ^0.28.1
|
||||
version: 0.28.1
|
||||
escape-string-regexp:
|
||||
specifier: ^5.0.0
|
||||
version: 5.0.0
|
||||
@@ -7928,7 +7928,7 @@ importers:
|
||||
version: 3.0.0
|
||||
esbuild:
|
||||
specifier: 'catalog:'
|
||||
version: 0.28.0
|
||||
version: 0.28.1
|
||||
execa:
|
||||
specifier: 'catalog:'
|
||||
version: safe-execa@0.3.0
|
||||
@@ -8113,7 +8113,7 @@ importers:
|
||||
version: link:../../workspace/workspace-manifest-reader
|
||||
esbuild:
|
||||
specifier: 'catalog:'
|
||||
version: 0.28.0
|
||||
version: 0.28.1
|
||||
devDependencies:
|
||||
pd:
|
||||
specifier: workspace:*
|
||||
@@ -10691,158 +10691,158 @@ packages:
|
||||
'@epic-web/invariant@1.0.0':
|
||||
resolution: {integrity: sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==}
|
||||
|
||||
'@esbuild/aix-ppc64@0.28.0':
|
||||
resolution: {integrity: sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==}
|
||||
'@esbuild/aix-ppc64@0.28.1':
|
||||
resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ppc64]
|
||||
os: [aix]
|
||||
|
||||
'@esbuild/android-arm64@0.28.0':
|
||||
resolution: {integrity: sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==}
|
||||
'@esbuild/android-arm64@0.28.1':
|
||||
resolution: {integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/android-arm@0.28.0':
|
||||
resolution: {integrity: sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==}
|
||||
'@esbuild/android-arm@0.28.1':
|
||||
resolution: {integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/android-x64@0.28.0':
|
||||
resolution: {integrity: sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==}
|
||||
'@esbuild/android-x64@0.28.1':
|
||||
resolution: {integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/darwin-arm64@0.28.0':
|
||||
resolution: {integrity: sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==}
|
||||
'@esbuild/darwin-arm64@0.28.1':
|
||||
resolution: {integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@esbuild/darwin-x64@0.28.0':
|
||||
resolution: {integrity: sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==}
|
||||
'@esbuild/darwin-x64@0.28.1':
|
||||
resolution: {integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@esbuild/freebsd-arm64@0.28.0':
|
||||
resolution: {integrity: sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==}
|
||||
'@esbuild/freebsd-arm64@0.28.1':
|
||||
resolution: {integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [freebsd]
|
||||
|
||||
'@esbuild/freebsd-x64@0.28.0':
|
||||
resolution: {integrity: sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==}
|
||||
'@esbuild/freebsd-x64@0.28.1':
|
||||
resolution: {integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
'@esbuild/linux-arm64@0.28.0':
|
||||
resolution: {integrity: sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==}
|
||||
'@esbuild/linux-arm64@0.28.1':
|
||||
resolution: {integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-arm@0.28.0':
|
||||
resolution: {integrity: sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==}
|
||||
'@esbuild/linux-arm@0.28.1':
|
||||
resolution: {integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-ia32@0.28.0':
|
||||
resolution: {integrity: sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==}
|
||||
'@esbuild/linux-ia32@0.28.1':
|
||||
resolution: {integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ia32]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-loong64@0.28.0':
|
||||
resolution: {integrity: sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==}
|
||||
'@esbuild/linux-loong64@0.28.1':
|
||||
resolution: {integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-mips64el@0.28.0':
|
||||
resolution: {integrity: sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==}
|
||||
'@esbuild/linux-mips64el@0.28.1':
|
||||
resolution: {integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [mips64el]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-ppc64@0.28.0':
|
||||
resolution: {integrity: sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==}
|
||||
'@esbuild/linux-ppc64@0.28.1':
|
||||
resolution: {integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-riscv64@0.28.0':
|
||||
resolution: {integrity: sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==}
|
||||
'@esbuild/linux-riscv64@0.28.1':
|
||||
resolution: {integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-s390x@0.28.0':
|
||||
resolution: {integrity: sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==}
|
||||
'@esbuild/linux-s390x@0.28.1':
|
||||
resolution: {integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-x64@0.28.0':
|
||||
resolution: {integrity: sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==}
|
||||
'@esbuild/linux-x64@0.28.1':
|
||||
resolution: {integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/netbsd-arm64@0.28.0':
|
||||
resolution: {integrity: sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==}
|
||||
'@esbuild/netbsd-arm64@0.28.1':
|
||||
resolution: {integrity: sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [netbsd]
|
||||
|
||||
'@esbuild/netbsd-x64@0.28.0':
|
||||
resolution: {integrity: sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==}
|
||||
'@esbuild/netbsd-x64@0.28.1':
|
||||
resolution: {integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [netbsd]
|
||||
|
||||
'@esbuild/openbsd-arm64@0.28.0':
|
||||
resolution: {integrity: sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==}
|
||||
'@esbuild/openbsd-arm64@0.28.1':
|
||||
resolution: {integrity: sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [openbsd]
|
||||
|
||||
'@esbuild/openbsd-x64@0.28.0':
|
||||
resolution: {integrity: sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==}
|
||||
'@esbuild/openbsd-x64@0.28.1':
|
||||
resolution: {integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [openbsd]
|
||||
|
||||
'@esbuild/openharmony-arm64@0.28.0':
|
||||
resolution: {integrity: sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==}
|
||||
'@esbuild/openharmony-arm64@0.28.1':
|
||||
resolution: {integrity: sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [openharmony]
|
||||
|
||||
'@esbuild/sunos-x64@0.28.0':
|
||||
resolution: {integrity: sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==}
|
||||
'@esbuild/sunos-x64@0.28.1':
|
||||
resolution: {integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [sunos]
|
||||
|
||||
'@esbuild/win32-arm64@0.28.0':
|
||||
resolution: {integrity: sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==}
|
||||
'@esbuild/win32-arm64@0.28.1':
|
||||
resolution: {integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@esbuild/win32-ia32@0.28.0':
|
||||
resolution: {integrity: sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==}
|
||||
'@esbuild/win32-ia32@0.28.1':
|
||||
resolution: {integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
|
||||
'@esbuild/win32-x64@0.28.0':
|
||||
resolution: {integrity: sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==}
|
||||
'@esbuild/win32-x64@0.28.1':
|
||||
resolution: {integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
@@ -13560,8 +13560,8 @@ packages:
|
||||
es-toolkit@1.47.0:
|
||||
resolution: {integrity: sha512-n1GuoD0WEQZMBk5tttoZSqwgyLx01oqa5XsBmCHwPyNe1S9jPBEmtR2pSgp2kJuWE3ciFZ6yRHmY4pM4C3OOkw==}
|
||||
|
||||
esbuild@0.28.0:
|
||||
resolution: {integrity: sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==}
|
||||
esbuild@0.28.1:
|
||||
resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
@@ -17979,82 +17979,82 @@ snapshots:
|
||||
|
||||
'@epic-web/invariant@1.0.0': {}
|
||||
|
||||
'@esbuild/aix-ppc64@0.28.0':
|
||||
'@esbuild/aix-ppc64@0.28.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-arm64@0.28.0':
|
||||
'@esbuild/android-arm64@0.28.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-arm@0.28.0':
|
||||
'@esbuild/android-arm@0.28.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-x64@0.28.0':
|
||||
'@esbuild/android-x64@0.28.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/darwin-arm64@0.28.0':
|
||||
'@esbuild/darwin-arm64@0.28.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/darwin-x64@0.28.0':
|
||||
'@esbuild/darwin-x64@0.28.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/freebsd-arm64@0.28.0':
|
||||
'@esbuild/freebsd-arm64@0.28.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/freebsd-x64@0.28.0':
|
||||
'@esbuild/freebsd-x64@0.28.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-arm64@0.28.0':
|
||||
'@esbuild/linux-arm64@0.28.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-arm@0.28.0':
|
||||
'@esbuild/linux-arm@0.28.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-ia32@0.28.0':
|
||||
'@esbuild/linux-ia32@0.28.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-loong64@0.28.0':
|
||||
'@esbuild/linux-loong64@0.28.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-mips64el@0.28.0':
|
||||
'@esbuild/linux-mips64el@0.28.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-ppc64@0.28.0':
|
||||
'@esbuild/linux-ppc64@0.28.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-riscv64@0.28.0':
|
||||
'@esbuild/linux-riscv64@0.28.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-s390x@0.28.0':
|
||||
'@esbuild/linux-s390x@0.28.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-x64@0.28.0':
|
||||
'@esbuild/linux-x64@0.28.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/netbsd-arm64@0.28.0':
|
||||
'@esbuild/netbsd-arm64@0.28.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/netbsd-x64@0.28.0':
|
||||
'@esbuild/netbsd-x64@0.28.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openbsd-arm64@0.28.0':
|
||||
'@esbuild/openbsd-arm64@0.28.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openbsd-x64@0.28.0':
|
||||
'@esbuild/openbsd-x64@0.28.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openharmony-arm64@0.28.0':
|
||||
'@esbuild/openharmony-arm64@0.28.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/sunos-x64@0.28.0':
|
||||
'@esbuild/sunos-x64@0.28.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-arm64@0.28.0':
|
||||
'@esbuild/win32-arm64@0.28.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-ia32@0.28.0':
|
||||
'@esbuild/win32-ia32@0.28.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-x64@0.28.0':
|
||||
'@esbuild/win32-x64@0.28.1':
|
||||
optional: true
|
||||
|
||||
'@eslint-community/eslint-utils@4.9.1(eslint@10.4.1(jiti@2.6.1))':
|
||||
@@ -21520,34 +21520,34 @@ snapshots:
|
||||
|
||||
es-toolkit@1.47.0: {}
|
||||
|
||||
esbuild@0.28.0:
|
||||
esbuild@0.28.1:
|
||||
optionalDependencies:
|
||||
'@esbuild/aix-ppc64': 0.28.0
|
||||
'@esbuild/android-arm': 0.28.0
|
||||
'@esbuild/android-arm64': 0.28.0
|
||||
'@esbuild/android-x64': 0.28.0
|
||||
'@esbuild/darwin-arm64': 0.28.0
|
||||
'@esbuild/darwin-x64': 0.28.0
|
||||
'@esbuild/freebsd-arm64': 0.28.0
|
||||
'@esbuild/freebsd-x64': 0.28.0
|
||||
'@esbuild/linux-arm': 0.28.0
|
||||
'@esbuild/linux-arm64': 0.28.0
|
||||
'@esbuild/linux-ia32': 0.28.0
|
||||
'@esbuild/linux-loong64': 0.28.0
|
||||
'@esbuild/linux-mips64el': 0.28.0
|
||||
'@esbuild/linux-ppc64': 0.28.0
|
||||
'@esbuild/linux-riscv64': 0.28.0
|
||||
'@esbuild/linux-s390x': 0.28.0
|
||||
'@esbuild/linux-x64': 0.28.0
|
||||
'@esbuild/netbsd-arm64': 0.28.0
|
||||
'@esbuild/netbsd-x64': 0.28.0
|
||||
'@esbuild/openbsd-arm64': 0.28.0
|
||||
'@esbuild/openbsd-x64': 0.28.0
|
||||
'@esbuild/openharmony-arm64': 0.28.0
|
||||
'@esbuild/sunos-x64': 0.28.0
|
||||
'@esbuild/win32-arm64': 0.28.0
|
||||
'@esbuild/win32-ia32': 0.28.0
|
||||
'@esbuild/win32-x64': 0.28.0
|
||||
'@esbuild/aix-ppc64': 0.28.1
|
||||
'@esbuild/android-arm': 0.28.1
|
||||
'@esbuild/android-arm64': 0.28.1
|
||||
'@esbuild/android-x64': 0.28.1
|
||||
'@esbuild/darwin-arm64': 0.28.1
|
||||
'@esbuild/darwin-x64': 0.28.1
|
||||
'@esbuild/freebsd-arm64': 0.28.1
|
||||
'@esbuild/freebsd-x64': 0.28.1
|
||||
'@esbuild/linux-arm': 0.28.1
|
||||
'@esbuild/linux-arm64': 0.28.1
|
||||
'@esbuild/linux-ia32': 0.28.1
|
||||
'@esbuild/linux-loong64': 0.28.1
|
||||
'@esbuild/linux-mips64el': 0.28.1
|
||||
'@esbuild/linux-ppc64': 0.28.1
|
||||
'@esbuild/linux-riscv64': 0.28.1
|
||||
'@esbuild/linux-s390x': 0.28.1
|
||||
'@esbuild/linux-x64': 0.28.1
|
||||
'@esbuild/netbsd-arm64': 0.28.1
|
||||
'@esbuild/netbsd-x64': 0.28.1
|
||||
'@esbuild/openbsd-arm64': 0.28.1
|
||||
'@esbuild/openbsd-x64': 0.28.1
|
||||
'@esbuild/openharmony-arm64': 0.28.1
|
||||
'@esbuild/sunos-x64': 0.28.1
|
||||
'@esbuild/win32-arm64': 0.28.1
|
||||
'@esbuild/win32-ia32': 0.28.1
|
||||
'@esbuild/win32-x64': 0.28.1
|
||||
|
||||
escalade@3.2.0: {}
|
||||
|
||||
|
||||
@@ -189,7 +189,7 @@ catalog:
|
||||
dint: ^5.1.0
|
||||
dir-is-case-sensitive: ^3.0.0
|
||||
encode-registry: ^3.0.1
|
||||
esbuild: ^0.28.0
|
||||
esbuild: ^0.28.1
|
||||
escape-string-regexp: ^5.0.0
|
||||
eslint: ^10.4.0
|
||||
eslint-plugin-import-x: ^4.16.2
|
||||
@@ -375,6 +375,8 @@ minimumReleaseAgeExclude:
|
||||
- cmd-extension
|
||||
- comver-to-semver
|
||||
- dir-is-case-sensitive
|
||||
- esbuild@0.28.1
|
||||
- '@esbuild/*'
|
||||
- express@4.22.1
|
||||
- glob@11.1.0
|
||||
- graceful-git
|
||||
|
||||
@@ -159,7 +159,7 @@ pub async fn resolve(
|
||||
http_client_arc: Arc::clone(client),
|
||||
config,
|
||||
manifest: &manifest,
|
||||
lockfile: input_lockfile,
|
||||
lockfile: pacquet_lockfile::MaybeLazyLockfile::Loaded(input_lockfile),
|
||||
lockfile_path: input_lockfile.map(|_| lockfile_path.as_path()),
|
||||
dependency_groups: vec![
|
||||
DependencyGroup::Prod,
|
||||
|
||||
@@ -334,6 +334,17 @@ export async function pickPackage (
|
||||
if (fetchResult.notModified) {
|
||||
metaCachedInStore = metaCachedInStore ?? await limit(async () => loadMeta(pkgMirror))
|
||||
if (metaCachedInStore != null) {
|
||||
// The registry just vouched that the cached packument equals its
|
||||
// current one, so the validation clock restarts now: bump the
|
||||
// mirror's mtime so the publishedBy freshness shortcut above can
|
||||
// fire again on the next install. Without this, a mirror older
|
||||
// than minimumReleaseAge re-validates on every subsequent
|
||||
// install — a 304 never rewrites the file. Fire-and-forget: a
|
||||
// read-only cache dir only costs another conditional request.
|
||||
if (!opts.dryRun) {
|
||||
const now = new Date()
|
||||
fs.utimes(pkgMirror, now, now).catch(() => {})
|
||||
}
|
||||
// The cached metadata may be abbreviated (no per-version `time`).
|
||||
// When minimumReleaseAge is active we need `time` for the maturity check,
|
||||
// so upgrade to full metadata via a follow-up fetch when warranted.
|
||||
|
||||
@@ -72,6 +72,53 @@ test('use local cache when registry returns 304 Not Modified', async () => {
|
||||
expect(resolveResult!.id).toBe('is-positive@3.1.0')
|
||||
})
|
||||
|
||||
test('a 304 Not Modified renews the metadata file mtime so the publishedBy freshness shortcut can fire again', async () => {
|
||||
const cacheDir = temporaryDirectory()
|
||||
const metaDir = path.join(cacheDir, `${ABBREVIATED_META_DIR}/registry.npmjs.org`)
|
||||
fs.mkdirSync(metaDir, { recursive: true })
|
||||
const metaPath = path.join(metaDir, 'is-positive.jsonl')
|
||||
const headers = JSON.stringify({ etag: '"abc123"', modified: isPositiveMeta.modified })
|
||||
fs.writeFileSync(metaPath, `${headers}\n${JSON.stringify(isPositiveMeta)}`, 'utf8')
|
||||
// Age the mirror far past any maturity cutoff.
|
||||
const aged = new Date(Date.now() - 365 * 24 * 60 * 60 * 1000)
|
||||
fs.utimesSync(metaPath, aged, aged)
|
||||
|
||||
getMockAgent().get(registries.default.replace(/\/$/, ''))
|
||||
.intercept({
|
||||
path: '/is-positive',
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'if-none-match': '"abc123"',
|
||||
},
|
||||
})
|
||||
.reply(304, '')
|
||||
|
||||
const { resolveFromNpm } = createResolveFromNpm({
|
||||
storeDir: temporaryDirectory(),
|
||||
cacheDir,
|
||||
registries,
|
||||
})
|
||||
const resolveResult = await resolveFromNpm(
|
||||
{ alias: 'is-positive', bareSpecifier: '^3.0.0' },
|
||||
{}
|
||||
)
|
||||
expect(resolveResult!.id).toBe('is-positive@3.1.0')
|
||||
|
||||
// The touch is fire-and-forget, so poll briefly instead of asserting
|
||||
// immediately.
|
||||
const renewed = () => fs.statSync(metaPath).mtime.getTime() > aged.getTime() + 1000
|
||||
await new Promise<void>((resolve) => {
|
||||
const start = Date.now()
|
||||
const timer = setInterval(() => {
|
||||
if (renewed() || Date.now() - start > 5000) {
|
||||
clearInterval(timer)
|
||||
resolve()
|
||||
}
|
||||
}, 50)
|
||||
})
|
||||
expect(renewed()).toBe(true)
|
||||
})
|
||||
|
||||
test('store etag from 200 response in cache', async () => {
|
||||
const cacheDir = temporaryDirectory()
|
||||
const responseHeaders = {
|
||||
|
||||
Reference in New Issue
Block a user