Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 25 additions & 3 deletions crates/nub-cli/src/pm_engine/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -730,6 +730,29 @@ pub(crate) fn session_role_root(
Some((role, detected.dir.clone()))
}

/// Per-process, mtime-validated cache of parsed `aube_manifest::PackageJson`
/// keyed by file path. The PM-engine config phase parses the root manifest
/// through aube's parser several times per command — `apply_config_scope`, the
/// scan's `manifest_has_pnpm_overrides`, and `injected_deps_present`'s
/// `manifest_has_injected` — and `first_catalog_specifier` parses every member
/// manifest. This collapses repeat parses of one path to a single read. mtime
/// validation keeps it stale-proof (a mid-command engine rewrite re-reads).
///
/// A parse ERROR (or missing file) yields `None` and is NOT cached, matching
/// every call site's existing `let Ok(..) = .. else { skip }` handling exactly.
static AUBE_MANIFEST_CACHE: nub_core::config_cache::MtimeCache<aube_manifest::PackageJson> =
nub_core::config_cache::MtimeCache::new();

/// Read + parse `path` as an `aube_manifest::PackageJson` through
/// [`AUBE_MANIFEST_CACHE`]. `None` on a missing/unparseable manifest (never
/// cached) — behavior-identical to a direct `PackageJson::from_path(path).ok()`,
/// just deduplicated across the repeat parses one command makes of the same path.
pub(crate) fn cached_aube_manifest(
path: &Path,
) -> Option<std::sync::Arc<aube_manifest::PackageJson>> {
AUBE_MANIFEST_CACHE.get_or_read(path, || aube_manifest::PackageJson::from_path(path).ok())
}

/// Apply the config-scoping policy for one verb invocation: resolve the
/// active-PM role, scope the manifest's graph-shaping override fields to
/// that role's dialect, register the scoped source + trusted-deps toggle on
Expand All @@ -751,7 +774,7 @@ fn apply_config_scope(

let root = detected.map(|d| d.dir.as_path()).unwrap_or(cwd);
let manifest_path = root.join("package.json");
let Ok(manifest) = aube_manifest::PackageJson::from_path(&manifest_path) else {
let Some(manifest) = cached_aube_manifest(&manifest_path) else {
return Ok(());
};

Expand Down Expand Up @@ -873,8 +896,7 @@ fn first_catalog_specifier(manifest: &aube_manifest::PackageJson, root: &Path) -
// independently, so a member-only `catalog:` ref must refuse too.
if let Ok(members) = aube_workspace::find_workspace_packages(root) {
for dir in members {
let Ok(member) = aube_manifest::PackageJson::from_path(&dir.join("package.json"))
else {
let Some(member) = cached_aube_manifest(&dir.join("package.json")) else {
continue;
};
if let Some(hit) = first_catalog_in_dep_maps(&member) {
Expand Down
43 changes: 30 additions & 13 deletions crates/nub-cli/src/pm_engine/unsupported_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,28 @@
//! symmetric brand-boundary discipline the rest of `pm_engine` enforces.

use std::path::{Path, PathBuf};
use std::sync::Arc;

use nub_core::config_cache::MtimeCache;

use super::config_scope::{IgnoredField, Role};

/// Per-process, mtime-validated cache of raw config-file CONTENTS keyed by path.
/// The unsupported-config readers each opened the same `.yarnrc.yml` / `.npmrc`
/// file once PER KEY (immutable, scripts, network, hardened; omit, include,
/// legacy-peer-deps, install-strategy) — several reads of one file per command.
/// This collapses them to a single read per `(path, mtime)`; the per-key parse
/// then runs against the cached string. mtime validation keeps it stale-proof:
/// any rewrite of the file bumps the mtime, the next lookup misses and re-reads.
static CONFIG_TEXT_CACHE: MtimeCache<String> = MtimeCache::new();

/// Read a config file's full contents through [`CONFIG_TEXT_CACHE`]. `None`
/// (missing / unreadable) is never cached — identical to `read_to_string().ok()`
/// on those paths, just deduplicated across repeated readers in one command.
fn read_config_text(path: &Path) -> Option<Arc<String>> {
CONFIG_TEXT_CACHE.get_or_read(path, || std::fs::read_to_string(path).ok())
}

/// A config-derived override of the dependency selection axis
/// (`--prod`/`--dev`/`--no-optional`). `None` per field means "not pinned by
/// config — leave the CLI/default behavior". Composed onto the install args
Expand Down Expand Up @@ -81,7 +100,7 @@ fn npm_omit_include(root: &Path) -> DepSelectionConfig {
let mut omit: Vec<String> = Vec::new();
let mut include: Vec<String> = Vec::new();
for path in npmrc_paths(root) {
let Ok(content) = std::fs::read_to_string(&path) else {
let Some(content) = read_config_text(&path) else {
continue;
};
if let Some(v) = npmrc_scalar(&content, "omit") {
Expand Down Expand Up @@ -132,7 +151,7 @@ pub(crate) fn frozen_from_config(role: Role, root: &Path) -> bool {
/// yarn `.yarnrc.yml` `enableImmutableInstalls: true` or a non-empty
/// `immutablePatterns:` block.
fn yarn_immutable(root: &Path) -> bool {
let Ok(content) = std::fs::read_to_string(root.join(".yarnrc.yml")) else {
let Some(content) = read_config_text(&root.join(".yarnrc.yml")) else {
return false;
};
if yarnrc_top_level_bool(&content, "enableImmutableInstalls") == Some(true) {
Expand All @@ -149,8 +168,7 @@ pub(crate) fn yarn_scripts_disabled(role: Role, root: &Path) -> bool {
if role != Role::Yarn {
return false;
}
std::fs::read_to_string(root.join(".yarnrc.yml"))
.ok()
read_config_text(&root.join(".yarnrc.yml"))
.and_then(|c| yarnrc_top_level_bool(&c, "enableScripts"))
== Some(false)
}
Expand All @@ -165,8 +183,7 @@ pub(crate) fn yarn_network_disabled(role: Role, root: &Path) -> bool {
if role != Role::Yarn {
return false;
}
std::fs::read_to_string(root.join(".yarnrc.yml"))
.ok()
read_config_text(&root.join(".yarnrc.yml"))
.and_then(|c| yarnrc_top_level_bool(&c, "enableNetwork"))
== Some(false)
}
Expand All @@ -182,8 +199,8 @@ pub(crate) fn yarn_network_disabled(role: Role, root: &Path) -> bool {
/// The classic `.yarnrc` is a space-separated `key "value"` format (parsed by
/// Yarn 1's own lockfile parser), distinct from Berry's `.yarnrc.yml`.
fn yarn_offline_mirror_configured(root: &Path) -> bool {
std::fs::read_to_string(root.join(".yarnrc"))
.is_ok_and(|c| classic_yarnrc_has_key(&c, "yarn-offline-mirror"))
read_config_text(&root.join(".yarnrc"))
.is_some_and(|c| classic_yarnrc_has_key(&c, "yarn-offline-mirror"))
}

/// Whether the root (or any workspace member) manifest declares
Expand All @@ -201,7 +218,7 @@ pub(crate) fn injected_deps_present(root: &Path) -> bool {
}

fn manifest_has_injected(manifest_path: &Path) -> bool {
let Ok(manifest) = aube_manifest::PackageJson::from_path(manifest_path) else {
let Some(manifest) = super::cached_aube_manifest(manifest_path) else {
return false;
};
let Some(meta) = manifest
Expand Down Expand Up @@ -358,7 +375,7 @@ fn scan_warn(role: Role, root: &Path) -> Vec<IgnoredField> {
}

fn manifest_has_pnpm_overrides(root: &Path) -> bool {
let Ok(manifest) = aube_manifest::PackageJson::from_path(&root.join("package.json")) else {
let Some(manifest) = super::cached_aube_manifest(&root.join("package.json")) else {
return false;
};
manifest
Expand All @@ -371,7 +388,7 @@ fn manifest_has_pnpm_overrides(root: &Path) -> bool {
}

fn yarnrc_top_level_bool_str(root: &Path, key: &str) -> Option<bool> {
let content = std::fs::read_to_string(root.join(".yarnrc.yml")).ok()?;
let content = read_config_text(&root.join(".yarnrc.yml"))?;
yarnrc_top_level_bool(&content, key)
}

Expand Down Expand Up @@ -426,7 +443,7 @@ fn npmrc_value_in(paths: &[PathBuf], key: &str) -> Option<String> {
paths
.iter()
.filter_map(|path| {
let content = std::fs::read_to_string(path).ok()?;
let content = read_config_text(path)?;
npmrc_scalar(&content, key)
})
.next_back()
Expand Down Expand Up @@ -505,7 +522,7 @@ fn split_list(v: &str) -> Vec<String> {
fn bunfig_install_bool(root: &Path, key: &str) -> Option<bool> {
let mut value = None;
for path in bunfig_paths(root) {
if let Ok(raw) = std::fs::read_to_string(&path)
if let Some(raw) = read_config_text(&path)
&& let Ok(parsed) = raw.parse::<toml::Value>()
&& let Some(b) = parsed
.get("install")
Expand Down
Loading
Loading