Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
06e0d60
Split yarn run script parsing
arcanis May 27, 2026
60828cb
Update cache checksum expectations
arcanis May 27, 2026
f2b653b
Emit install reports as JSON
arcanis May 27, 2026
337bfb7
Align install cache request tests
arcanis May 27, 2026
bdc0105
Fix build dependency ordering
arcanis May 27, 2026
d5a38e7
Align build log cleanup expectations
arcanis May 27, 2026
717b23a
Fix node-modules binary permissions
arcanis May 27, 2026
27c03f7
Resolve binaries from visible locations
arcanis May 27, 2026
8b38ba9
Preserve symlinked node_modules folders
arcanis May 27, 2026
e7b94cc
Align PnP copy and preload tests
arcanis May 27, 2026
37bd705
Align external cache add test
arcanis May 27, 2026
c3a91af
Use cacheFolder for local cache path
arcanis May 28, 2026
b91529d
Mark package bins executable in archives
arcanis May 29, 2026
1d76b33
Fix aliased virtual peer instances
arcanis May 30, 2026
1203ab5
Keep peer sets distinct when deduping virtuals
arcanis May 30, 2026
ce3b649
Preserve explicit @types dependencies
arcanis May 30, 2026
4b0170a
Accept target triples in switch fixture
arcanis May 30, 2026
55261d6
Refetch mutable file references
arcanis May 30, 2026
74ea702
Apply network TLS settings by host
arcanis May 30, 2026
1716016
Reuse cached npm metadata offline
arcanis May 30, 2026
ce8cc87
Honor enableMirror for local caches
arcanis May 30, 2026
d4d58ca
Add time metadata to npm test registry
arcanis May 30, 2026
dfce367
Populate the PnP fallback pool
arcanis May 30, 2026
6907a95
Apply deferred bumps before declining
arcanis May 30, 2026
49a1a20
Report resolved dlx packages
arcanis May 30, 2026
0f65d68
Implement jsr protocol
arcanis May 30, 2026
01ec6e7
Implement exec protocol
arcanis May 30, 2026
07f0af6
Hash local package locators
arcanis May 31, 2026
9a381c5
Pin jsr loose resolutions
arcanis May 31, 2026
d89915e
Update lockfile
arcanis May 31, 2026
6556d7a
Fixes checksums
arcanis May 31, 2026
5216203
Avoid change that would be better served by updating tests
arcanis May 31, 2026
a15a6e5
Feedback
arcanis May 31, 2026
4236bb8
Fixes lint
arcanis May 31, 2026
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
5 changes: 5 additions & 0 deletions .github/actions/prepare-node/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ runs:
chmod +x ${{inputs.link}}
yarn switch link ${{inputs.link}}

- name: Install pnpm
shell: bash
run: |
npm install --global pnpm@10

- name: Install dependencies
shell: bash
run: |
Expand Down
49 changes: 44 additions & 5 deletions packages/zpm-config/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,11 @@
"description": "Whether to keep the packages in a global cache rather than inside the project",
"default": true
},
"enableMirror": {
"type": "boolean",
"description": "Whether to mirror local cache entries into the global cache",
"default": true
},
"enableLocalCacheCleanup": {
"type": "boolean",
"description": "Whether to remove unused packages from the local cache after each install",
Expand Down Expand Up @@ -196,16 +201,33 @@
"type": ["string", "null"],
"description": "Define a proxy to use when making an HTTPS request"
},
"localCacheFolderName": {
"type": "string",
"description": "The name of the folder where the local cache will be stored inside the .yarn folder",
"default": "cache"
},
"networkConcurrency": {
"type": "usize",
"description": "The number of concurrent network requests Yarn can perform",
"default": 100
},
"logFilters": {
"type": "array",
"description": "Filters that can discard log entries or change their reported level",
"items": {
"type": "object",
"title": "LogFilter",
"properties": {
"text": {
"type": ["string", "null"],
"description": "Exact log text to match"
},
"pattern": {
"type": ["zpm_utils::Glob", "null"],
"description": "Glob pattern to match against log text"
},
"level": {
"type": "crate::types::LogLevel",
"description": "Level to assign to matching log entries"
}
}
}
},
"networkSettings": {
"type": "object",
"description": "Configuration specific to each network setting",
Expand All @@ -219,6 +241,18 @@
"enableNetwork": {
"type": ["boolean", "null"],
"description": "Whether to allow network access for this specific glob"
},
"httpsCaFilePath": {
"type": ["zpm_utils::Path", "null"],
"description": "Path to a file containing one or multiple Certificate Authority signing certificates"
},
"httpsCertFilePath": {
"type": ["zpm_utils::Path", "null"],
"description": "Path to a file containing a certificate chain in PEM format"
},
"httpsKeyFilePath": {
"type": ["zpm_utils::Path", "null"],
"description": "Path to a file containing a private key in PEM format"
}
}
}
Expand Down Expand Up @@ -464,6 +498,11 @@
"description": "Whether to use interactive prompts by default when running yarn commands",
"default": false
},
"preferTruncatedLines": {
"type": "boolean",
"description": "Whether to prefer truncating lines in the terminal output",
"default": false
},
"preferReuse": {
"type": "boolean",
"description": "Whether to prefer reusing dependencies from the lockfile when running `yarn add`",
Expand Down
160 changes: 144 additions & 16 deletions packages/zpm-config/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::{collections::BTreeMap, fmt::Display, ops::Deref, sync::Arc, time::UNIX_EPOCH};
use std::{collections::{BTreeMap, BTreeSet}, fmt::Display, ops::Deref, sync::Arc, time::UNIX_EPOCH};

use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
use zpm_utils::{AbstractValue, Container, Cpu, DataType, FromFileString, IoResultExt, LastModifiedAt, Libc, Os, Path, RawString, Serialized, System, ToFileString, ToHumanString, tree};
Expand Down Expand Up @@ -900,6 +900,145 @@ struct RcFile {
text: Option<String>,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum ConflictMode {
Extend,
Reset,
}

fn parse_conflict_mode(value: &serde_yaml::Value) -> Option<ConflictMode> {
let serde_yaml::Value::String(value) = value else {
return None;
};

match value.as_str() {
"extend" => Some(ConflictMode::Extend),
"reset" => Some(ConflictMode::Reset),
_ => None,
}
}

fn normalize_conflict_metadata(value: &mut serde_yaml::Value) -> (Option<ConflictMode>, BTreeMap<String, ConflictMode>) {
let serde_yaml::Value::Mapping(mapping) = value else {
return (None, BTreeMap::new());
};

let on_conflict_key
= serde_yaml::Value::String("onConflict".to_string());
let value_key
= serde_yaml::Value::String("value".to_string());

let root_mode
= mapping.remove(&on_conflict_key)
.as_ref()
.and_then(parse_conflict_mode);

let mut field_modes
= BTreeMap::new();

let mut fields_to_remove
= Vec::new();

for (key, field_value) in mapping.iter_mut() {
let serde_yaml::Value::String(key) = key else {
continue;
};

let serde_yaml::Value::Mapping(field_mapping) = field_value else {
continue;
};

let Some(mode) = field_mapping
.remove(&on_conflict_key)
.as_ref()
.and_then(parse_conflict_mode) else {
continue;
};

field_modes.insert(key.clone(), mode);

if let Some(value) = field_mapping.remove(&value_key) {
*field_value = value;
} else if field_mapping.is_empty() {
fields_to_remove.push(key.clone());
}
Comment thread
cursor[bot] marked this conversation as resolved.
}

for key in fields_to_remove {
mapping.remove(serde_yaml::Value::String(key));
}

(root_mode, field_modes)
}

fn retain_user_conflict_fields(value: &mut serde_yaml::Value, root_mode: Option<ConflictMode>, field_modes: &BTreeMap<String, ConflictMode>) {
let serde_yaml::Value::Mapping(mapping) = value else {
return;
};

let retained_fields = field_modes
.iter()
.filter_map(|(key, mode)| (*mode == ConflictMode::Extend).then_some(key.as_str()))
.collect::<BTreeSet<_>>();

if root_mode == Some(ConflictMode::Reset) {
mapping.retain(|key, _| {
let serde_yaml::Value::String(key) = key else {
return false;
};

retained_fields.contains(key.as_str())
});
} else {
for (key, mode) in field_modes {
if *mode == ConflictMode::Reset {
mapping.remove(serde_yaml::Value::String(key.clone()));
}
}
}
}

fn deserialize_rc_pair(user_rc: Option<&RcFile>, project_rc: Option<&RcFile>) -> Result<(Partial<intermediate::Settings>, Partial<intermediate::Settings>), ConfigurationError> {
let mut user_value = user_rc
.and_then(|rc| rc.text.as_ref())
.map(|text| serde_yaml::from_str::<serde_yaml::Value>(text))
.transpose()?;

let mut project_value = project_rc
.and_then(|rc| rc.text.as_ref())
.map(|text| serde_yaml::from_str::<serde_yaml::Value>(text))
.transpose()?;

let user_field_modes = user_value
.as_mut()
.map(normalize_conflict_metadata);

if let Some((root_mode, field_modes)) = user_field_modes {
retain_user_conflict_fields(user_value.as_mut().unwrap(), root_mode, &field_modes);
}

let (project_root_mode, project_field_modes) = project_value
.as_mut()
.map(normalize_conflict_metadata)
.unwrap_or((None, BTreeMap::new()));

if let Some(user_value) = user_value.as_mut() {
retain_user_conflict_fields(user_value, project_root_mode, &project_field_modes);
}

let user = match user_value {
Some(value) => Partial::Value(serde_yaml::from_value(value)?),
None => Partial::Missing,
};

let project = match project_value {
Some(value) => Partial::Value(serde_yaml::from_value(value)?),
None => Partial::Missing,
};

Ok((user, project))
}

impl RcFile {
fn try_read(dir: Option<&Path>, rc_filename: &str, last_modified_at: &mut LastModifiedAt) -> Result<Option<Self>, ConfigurationError> {
let Some(dir) = dir else {
Expand Down Expand Up @@ -930,12 +1069,6 @@ impl RcFile {
Ok(Some(RcFile {path, text: Some(text)}))
}

fn deserialize(&self) -> Option<Result<intermediate::Settings, ConfigurationError>> {
self.text.as_ref().map(|text| {
Ok(serde_yaml::from_str(text)?)
})
}

/// Extract the `injectEnvironmentFiles` value from the raw YAML text.
/// Uses a minimal struct to avoid full deserialization, which would fail
/// if config values reference env vars not yet loaded from .env files.
Expand Down Expand Up @@ -1079,15 +1212,8 @@ impl Configuration {
= project_rc.as_ref()
.map(|rc| rc.path.clone());

let intermediate_user_config = match user_rc.and_then(|rc| rc.deserialize()) {
Some(result) => Partial::Value(result?),
None => Partial::Missing,
};

let intermediate_project_config = match project_rc.and_then(|rc| rc.deserialize()) {
Some(result) => Partial::Value(result?),
None => Partial::Missing,
};
let (intermediate_user_config, intermediate_project_config)
= deserialize_rc_pair(user_rc.as_ref(), project_rc.as_ref())?;

let mut settings = Settings::merge(
&enriched_context,
Expand Down Expand Up @@ -1189,10 +1315,12 @@ merge_settings!(crate::types::PnpFallbackMode, |s: &str| FromFileString::from_fi
merge_settings!(crate::types::NmHoistingLimits, |s: &str| FromFileString::from_file_string(s).unwrap());
merge_settings!(crate::types::NmMode, |s: &str| FromFileString::from_file_string(s).unwrap());
merge_settings!(crate::types::WinLinkType, |s: &str| FromFileString::from_file_string(s).unwrap());
merge_settings!(crate::types::LogLevel, |s: &str| FromFileString::from_file_string(s).unwrap());
merge_optional_settings!(crate::types::NodeLinker);
merge_optional_settings!(crate::types::IslandLinker);
merge_optional_settings!(crate::types::PnpFallbackMode);
merge_optional_settings!(crate::types::NmHoistingLimits);
merge_optional_settings!(crate::types::NmMode);
merge_optional_settings!(crate::types::WinLinkType);
merge_optional_settings!(crate::types::LogLevel);
merge_optional_settings!(Path);
16 changes: 16 additions & 0 deletions packages/zpm-config/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,19 @@ pub enum WinLinkType {
#[literal("junctions")]
Junctions,
}

#[zpm_enum(error = ConfigurationError, or_else = |s| Err(ConfigurationError::EnumError(s.to_string())))]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LogLevel {
#[literal("discard")]
Discard,

#[literal("info")]
Info,

#[literal("warning")]
Warning,

#[literal("error")]
ErrorLevel,
}
10 changes: 9 additions & 1 deletion packages/zpm-parsers/src/yaml_doc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -302,8 +302,15 @@ impl YamlDocument {
}

fn insert_top_level_key(&mut self, new_key: &str, value: Value) -> Result<(), Error> {
let (self_indent, style)
= self.paths.iter()
.filter(|(path, _)| path.is_direct_child_of(&Path::new()))
.filter_map(|(_, offset)| self.find_indent_at(*offset))
.min_by_key(|(indent, _)| *indent)
.unwrap_or((0, IndentStyle::Spaces));

let top_level_indent
= Indent::with_style(Some(0), Some(2), IndentStyle::Spaces);
= Indent::with_style(Some(self_indent), Some(self_indent + 2), style);

self.insert_at(0, &Path::new(), new_key, top_level_indent, value)
}
Expand Down Expand Up @@ -1123,6 +1130,7 @@ mod tests {

// Insert new top-level keys
#[case(b"existing: value\n", vec!["new_key"], Value::String("another".to_string()), b"existing: value\nnew_key: another\n")]
#[case(b" existing: value\n", vec!["new_key"], Value::String("another".to_string()), b" existing: value\n new_key: another\n")]

// Insert nested keys
#[case(b"parent:\n existing: value\n", vec!["parent", "new_child"], Value::String("new".to_string()), b"parent:\n existing: value\n new_child: new\n")]
Expand Down
Loading
Loading