feat(pacquet): expose linkWorkspacePackages in the napi binding (#12844)

Add `linkWorkspacePackages` (`true` / `false` / `"deep"`) to the `@pnpm/napi`
install options, mapping it onto `Config::link_workspace_packages` (reusing the
config crate's `Deserialize`). Programmatic consumers need this to opt
bare-semver dependencies — including an auto-installed peer that names a sibling
workspace package — into workspace-package resolution, matching pnpm's
`link-workspace-packages` setting. The default stays `false`, so existing
behavior is unchanged.
This commit is contained in:
Zoltan Kochan
2026-07-07 19:30:20 +02:00
committed by GitHub
parent 25bd5c3fe5
commit be8e8ea747
4 changed files with 70 additions and 1 deletions

View File

@@ -34,7 +34,8 @@ use std::{
use dashmap::DashMap;
use indexmap::IndexMap;
use pacquet_config::{
Config, GetHomeDir, Host, LoadWorkspaceYamlError, NodeLinker, PackageImportMethod,
Config, GetHomeDir, Host, LinkWorkspacePackages, LoadWorkspaceYamlError, NodeLinker,
PackageImportMethod,
};
use pacquet_network::{AuthHeaders, ProxyConfig, TlsConfig};
use pacquet_store_dir::StoreDir;
@@ -52,6 +53,10 @@ pub struct ConfigOverlay {
pub proxy: Option<ProxyConfig>,
pub tls: Option<TlsConfig>,
pub node_linker: Option<NodeLinker>,
/// `linkWorkspacePackages` — whether a bare-semver dependency may resolve
/// to a workspace package by name. `Off` (the default) matches only
/// `workspace:`-prefixed ranges.
pub link_workspace_packages: Option<LinkWorkspacePackages>,
pub package_import_method: Option<PackageImportMethod>,
pub virtual_store_dir_max_length: Option<u64>,
pub hoist_pattern: Option<Vec<String>>,
@@ -262,6 +267,9 @@ fn build_config(dir: &Path, overlay: &ConfigOverlay) -> Result<Config, LoadWorks
if let Some(node_linker) = overlay.node_linker {
config.node_linker = node_linker;
}
if let Some(link_workspace_packages) = overlay.link_workspace_packages {
config.link_workspace_packages = link_workspace_packages;
}
if let Some(method) = overlay.package_import_method {
config.package_import_method = method;
}

View File

@@ -56,6 +56,10 @@ pub struct InstallOptions {
pub proxy_config: Option<ProxyConfigInput>,
pub network_config: Option<NetworkConfigInput>,
pub node_linker: Option<String>,
/// `linkWorkspacePackages` — `true` / `false` / `"deep"`. When enabled, a
/// bare-semver dependency may resolve to a workspace package by name (not
/// only `workspace:`-prefixed ranges).
pub link_workspace_packages: Option<serde_json::Value>,
pub hoist_pattern: Option<Vec<String>>,
pub public_hoist_pattern: Option<Vec<String>>,
pub external_dependencies: Option<Vec<String>>,
@@ -416,6 +420,9 @@ fn build_overlay(options: &InstallOptions) -> napi::Result<ConfigOverlay> {
proxy: options.proxy_config.as_ref().map(build_proxy_config).transpose()?,
tls: network_config.map(build_tls_config).transpose()?,
node_linker: options.node_linker.as_deref().and_then(parse_node_linker),
link_workspace_packages: parse_link_workspace_packages(
options.link_workspace_packages.as_ref(),
)?,
package_import_method: options
.package_import_method
.as_deref()
@@ -600,6 +607,25 @@ fn parse_node_linker(value: &str) -> Option<pacquet_config::NodeLinker> {
}
}
/// Parse the JS `linkWorkspacePackages` value (`true` / `false` / `"deep"`)
/// into a [`pacquet_config::LinkWorkspacePackages`], reusing the config
/// crate's `Deserialize`. Rejects any other value.
fn parse_link_workspace_packages(
value: Option<&serde_json::Value>,
) -> napi::Result<Option<pacquet_config::LinkWorkspacePackages>> {
value
.map(|value| {
serde_json::from_value::<pacquet_config::LinkWorkspacePackages>(value.clone()).map_err(
|error| {
napi::Error::from_reason(format!(
r#"invalid linkWorkspacePackages (expected true, false, or "deep"): {error}"#,
))
},
)
})
.transpose()
}
fn parse_import_method(value: &str) -> Option<pacquet_config::PackageImportMethod> {
match value {
"auto" => Some(pacquet_config::PackageImportMethod::Auto),

View File

@@ -86,6 +86,34 @@ fn build_overlay_maps_supported_install_options() {
assert_eq!(tls.local_address.map(|ip| ip.to_string()), Some("127.0.0.1".to_string()));
}
#[test]
fn build_overlay_parses_link_workspace_packages() {
use pacquet_config::LinkWorkspacePackages;
let mut options = install_options();
options.link_workspace_packages = Some(serde_json::json!("deep"));
assert_eq!(
build_overlay(&options).expect("overlay").link_workspace_packages,
Some(LinkWorkspacePackages::Deep),
);
options.link_workspace_packages = Some(serde_json::json!(true));
assert_eq!(
build_overlay(&options).expect("overlay").link_workspace_packages,
Some(LinkWorkspacePackages::DirectOnly),
);
options.link_workspace_packages = Some(serde_json::json!(false));
assert_eq!(
build_overlay(&options).expect("overlay").link_workspace_packages,
Some(LinkWorkspacePackages::Off),
);
// Anything other than a boolean or "deep" is rejected.
options.link_workspace_packages = Some(serde_json::json!("shallow"));
assert!(build_overlay(&options).is_err());
}
#[test]
fn unsupported_install_options_fail_closed() {
let mut options = install_options();
@@ -143,6 +171,7 @@ fn install_options() -> InstallOptions {
proxy_config: None,
network_config: None,
node_linker: None,
link_workspace_packages: None,
hoist_pattern: None,
public_hoist_pattern: None,
external_dependencies: None,

View File

@@ -90,6 +90,12 @@ export interface InstallOptions extends SharedEngineOptions {
projects: NodeApiProject[]
storeDir?: string
nodeLinker?: 'hoisted' | 'isolated'
/**
* pnpm's `linkWorkspacePackages`. When `true`/`'deep'`, a bare-semver
* dependency (including an auto-installed peer) may resolve to a workspace
* package by name; `false` (the default) matches only `workspace:` ranges.
*/
linkWorkspacePackages?: boolean | 'deep'
hoistPattern?: string[]
publicHoistPattern?: string[]
/** Packages linked from outside the workspace; excluded from hoisting/pruning. */