Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
10 changes: 10 additions & 0 deletions packages/zpm-config/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,16 @@
"description": "The linker to use for node_modules",
"default": "pnp"
},
"nodeExperimentalPackageMap": {
"type": "boolean",
"description": "Whether to inject the generated Node.js package map into NODE_OPTIONS for node-modules and pnpm installs",
"default": false
},
"nodePackageMapType": {
"type": "crate::NodePackageMapType",
"description": "Whether generated package maps should reflect declared dependencies (standard) or the installed node_modules layout (loose)",
"default": "standard"
},
"nmHoistingLimits": {
"type": "crate::NmHoistingLimits",
"description": "How far the node-modules linker is allowed to hoist a workspace's dependencies (none / workspaces / dependencies)",
Expand Down
2 changes: 2 additions & 0 deletions packages/zpm-config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1398,6 +1398,7 @@ merge_optional_settings!(zpm_utils::Os);
merge_optional_settings!(zpm_utils::Secret<String>);

merge_settings!(crate::types::NodeLinker, |s: &str| FromFileString::from_file_string(s).unwrap());
merge_settings!(crate::types::NodePackageMapType, |s: &str| FromFileString::from_file_string(s).unwrap());
merge_settings!(crate::types::IslandLinker, |s: &str| FromFileString::from_file_string(s).unwrap());
merge_settings!(crate::types::PnpFallbackMode, |s: &str| FromFileString::from_file_string(s).unwrap());
merge_settings!(crate::types::NmHoistingLimits, |s: &str| FromFileString::from_file_string(s).unwrap());
Expand All @@ -1407,6 +1408,7 @@ merge_settings!(crate::types::LogLevel, |s: &str| FromFileString::from_file_stri
merge_settings!(crate::types::NpmPublishAccess, |s: &str| FromFileString::from_file_string(s).unwrap());
merge_settings!(crate::types::EcosystemFilter, |s: &str| FromFileString::from_file_string(s).unwrap());
merge_optional_settings!(crate::types::NodeLinker);
merge_optional_settings!(crate::types::NodePackageMapType);
merge_optional_settings!(crate::types::IslandLinker);
merge_optional_settings!(crate::types::PnpFallbackMode);
merge_optional_settings!(crate::types::NmHoistingLimits);
Expand Down
10 changes: 10 additions & 0 deletions packages/zpm-config/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,16 @@ pub enum NodeLinker {
NodeModules,
}

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

#[literal("loose")]
Loose,
}

#[zpm_enum(error = ConfigurationError, or_else = |s| Err(ConfigurationError::EnumError(s.to_string())))]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IslandLinker {
Expand Down
38 changes: 38 additions & 0 deletions packages/zpm-utils/src/path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,22 @@ impl Path {
}
}

pub fn without_trailing_separators(&self) -> Path {
if self.is_root() {
return self.clone();
}

let trimmed = self.path.trim_end_matches('/');

if trimmed.len() == self.path.len() {
self.clone()
} else {
Path {
path: trimmed.to_string(),
}
}
}

pub fn extname<'a>(&'a self) -> Option<&'a str> {
self.basename().and_then(|basename| {
if let Some(mut last_dot) = basename.rfind('.') {
Expand Down Expand Up @@ -1154,5 +1170,27 @@ impl ToHumanString for Path {
}
}

#[cfg(test)]
mod tests {
use std::str::FromStr;

use super::Path;

#[test]
fn normalizes_repeated_trailing_separators() {
assert_eq!(Path::from_str("foo//").unwrap().as_str(), "foo/");
assert_eq!(Path::from_str("/foo///").unwrap().as_str(), "/foo/");
assert_eq!(Path::from_str("///").unwrap().as_str(), "/");
}

#[test]
fn removes_trailing_separators() {
assert_eq!(Path::from_str("foo/").unwrap().without_trailing_separators().as_str(), "foo");
assert_eq!(Path::from_str("/foo/").unwrap().without_trailing_separators().as_str(), "/foo");
assert_eq!(Path::from_str("/").unwrap().without_trailing_separators().as_str(), "/");
assert_eq!(Path::new().without_trailing_separators().as_str(), "");
}
}

impl_file_string_from_str!(Path);
impl_file_string_serialization!(Path);
3 changes: 3 additions & 0 deletions packages/zpm/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,9 @@ pub enum Error {
#[error("Lockfile generation error: {0}")]
LockfileGenerationError(zpm_parsers::Error),

#[error("Package map generation error: {0}")]
PackageMapGenerationError(String),

#[error("Incompatible options: {}", .0.join(", "))]
IncompatibleOptions(Vec<String>),

Expand Down
9 changes: 8 additions & 1 deletion packages/zpm/src/linker/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use std::collections::BTreeMap;

use zpm_config::{IslandLinker, NodeLinker};
use zpm_primitives::Locator;
use zpm_utils::Path;
use zpm_utils::{IoResultExt, Path};

use crate::{
build::BuildRequests,
Expand All @@ -13,6 +13,7 @@ use crate::{

pub mod helpers;
pub mod nm;
pub mod package_map;
pub mod pnpm;
pub mod pnp;
pub mod venv;
Expand Down Expand Up @@ -73,5 +74,11 @@ fn cleanup_inactive_linker_artifacts(project: &Project) -> Result<(), Error> {
}
}

if active == NodeLinker::Pnp {
project.package_map_path(None)
.fs_rm()
.ok_missing()?;
}

Ok(())
}
73 changes: 72 additions & 1 deletion packages/zpm/src/linker/nm/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use zpm_sync::{SyncItem, SyncTemplate, SyncTree};
use zpm_utils::{FromFileString, IoResultExt, Path, ToHumanString};

use crate::{
build::{self, BuildRequest, BuildRequests}, content_flags, error::Error, fetchers::PackageData, install::Install, linker::{self, LinkResult, helpers::PackageMeta, nm::hoist::{Hoister, WorkTree}}, project::Project
build::{self, BuildRequest, BuildRequests}, content_flags, error::Error, fetchers::PackageData, install::Install, linker::{self, LinkResult, helpers::PackageMeta, nm::hoist::{Hoister, WorkTree}, package_map::{NodeModulesPackageMapBuilder, persist_package_map, persist_package_map_at}}, project::Project
};

pub mod hoist;
Expand Down Expand Up @@ -86,6 +86,7 @@ fn register_workspace_symlinks_at(
project: &Project,
install: &Install,
workspace_nm_tree: &mut SyncTree,
mut package_map_builder: Option<&mut NodeModulesPackageMapBuilder>,
host_node: &hoist::WorkNode,
host_abs_path: &Path,
candidate_workspaces: impl IntoIterator<Item = (Ident, Path)>,
Expand Down Expand Up @@ -147,9 +148,20 @@ fn register_workspace_symlinks_at(
= workspace_dir
.relative_to(&host_abs_path.with_join(&symlink_path).dirname().unwrap_or_default());

let symlink_location
= host_abs_path.with_join(&symlink_path);

workspace_nm_tree.register_entry(symlink_path, SyncItem::Symlink {
target_path,
})?;

if let Some(package_map_builder) = package_map_builder.as_deref_mut() {
package_map_builder.register_package(
symlink_location,
workspace_dir,
&target_locator,
);
}
}

Ok(())
Expand Down Expand Up @@ -187,11 +199,15 @@ fn generate_workspace_node_modules(
install: &Install,
work_tree: &WorkTree,
workspace_node_idx: usize,
package_map_builder: Option<&mut NodeModulesPackageMapBuilder>,
Comment thread
cursor[bot] marked this conversation as resolved.
packages_by_location: &mut BTreeMap<Path, zpm_primitives::Locator>,
canonical_build_locations: &mut BTreeMap<Locator, Path>,
force_rebuild_locators: &mut BTreeSet<Locator>,
cas_extractions: &mut Vec<(Path, Locator)>,
) -> Result<(), Error> {
let mut package_map_builder
= package_map_builder;

let hardlinks_mode = matches!(
project.config.settings.nm_mode.value,
zpm_config::NmMode::HardlinksLocal | zpm_config::NmMode::HardlinksGlobal,
Expand All @@ -213,6 +229,14 @@ fn generate_workspace_node_modules(
= project.project_cwd
.with_join(&workspace.rel_path);

if let Some(package_map_builder) = package_map_builder.as_deref_mut() {
package_map_builder.register_package(
workspace_dir.clone(),
workspace_dir.clone(),
&workspace_node.locator,
);
}

let workspace_abs_path
= workspace_dir
.with_join_str("node_modules");
Expand Down Expand Up @@ -255,6 +279,7 @@ fn generate_workspace_node_modules(
project,
install,
&mut workspace_nm_tree,
package_map_builder.as_deref_mut(),
&work_tree.nodes[workspace_node_idx],
&workspace_abs_path,
candidate_workspaces,
Expand Down Expand Up @@ -315,6 +340,14 @@ fn generate_workspace_node_modules(
let child_abs_path
= workspace_abs_path.with_join(&child_rel_path);

if let Some(package_map_builder) = package_map_builder.as_deref_mut() {
package_map_builder.register_package(
child_abs_path.clone(),
package_directory.clone(),
&child_node.locator,
);
}

let target_path
= package_directory.relative_to(&child_abs_path.dirname().unwrap());

Expand All @@ -328,6 +361,14 @@ fn generate_workspace_node_modules(
},

Some(PackageData::Zip {archive_path, package_directory, ..}) => {
if let Some(package_map_builder) = package_map_builder.as_deref_mut() {
package_map_builder.register_package(
abs_path.clone(),
abs_path.clone(),
&child_node.locator,
);
}

// SyncTree re-extracts user-deleted destinations
// automatically; we just need to flag for rebuild
// so the build cache doesn't short-circuit.
Expand Down Expand Up @@ -373,6 +414,14 @@ fn generate_workspace_node_modules(
let target_path
= Path::from_file_string(&params.path)?;

if let Some(package_map_builder) = package_map_builder.as_deref_mut() {
package_map_builder.register_package(
abs_path.clone(),
target_path.clone(),
&child_node.locator,
);
}

workspace_nm_tree.register_entry(child_rel_path, SyncItem::Symlink {
target_path,
})?;
Expand Down Expand Up @@ -492,10 +541,16 @@ pub async fn link_island_nm(
= BTreeSet::new();
let mut cas_extractions
= Vec::new();
let mut package_maps
= Vec::new();

for workspace_ident in &island.workspace_idents {
let workspace = project.workspace_by_ident(workspace_ident)?;
let workspace_locator = workspace.locator();
let package_map_base_path
= workspace.path.with_join_str("node_modules");
let mut package_map_builder
= NodeModulesPackageMapBuilder::new_at(project, install, package_map_base_path.clone());

let mut work_tree = WorkTree::new_for_island_workspace(
project,
Expand All @@ -513,15 +568,25 @@ pub async fn link_island_nm(
install,
&work_tree,
0,
Some(&mut package_map_builder),
&mut packages_by_location,
&mut canonical_build_locations,
&mut force_rebuild_locators,
&mut cas_extractions,
)?;

package_maps.push((
project.package_map_path(Some(workspace)),
package_map_builder.build()?,
));
}

run_cas_extractions(project, install, &cas_extractions)?;

for (package_map_path, package_map) in package_maps {
persist_package_map_at(&package_map_path, &package_map)?;
}

let dependencies_meta
= linker::helpers::TopLevelConfiguration::from_project(project);

Expand Down Expand Up @@ -772,6 +837,9 @@ pub async fn link_project_nm(project: &Project, install: &Install) -> Result<Lin

check_external_portal_conflicts(project, install, &work_tree)?;

let mut package_map_builder
= NodeModulesPackageMapBuilder::new(project, install);

let mut project_queue
= vec![0usize];

Expand All @@ -781,6 +849,7 @@ pub async fn link_project_nm(project: &Project, install: &Install) -> Result<Lin
install,
&work_tree,
workspace_node_idx,
Some(&mut package_map_builder),
&mut packages_by_location,
&mut canonical_build_locations,
&mut force_rebuild_locators,
Expand All @@ -792,6 +861,8 @@ pub async fn link_project_nm(project: &Project, install: &Install) -> Result<Lin

run_cas_extractions(project, install, &cas_extractions)?;

persist_package_map(project, &package_map_builder.build()?)?;
Comment thread
cursor[bot] marked this conversation as resolved.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale map after build failure

Medium Severity

The node-modules linker updates node_modules while generating workspaces, but writes .package-map.json only after package_map_builder.build() succeeds. Unlike pnpm, it does not remove the old map at link start, so a failed build leaves a previous map on disk that may not match the new layout.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 6eadee7. Configure here.


let dependencies_meta
= linker::helpers::TopLevelConfiguration::from_project(project);

Expand Down
Loading
Loading