diff --git a/.changeset/napi-explicit-settings.md b/.changeset/napi-explicit-settings.md new file mode 100644 index 0000000000..4ac047cc05 --- /dev/null +++ b/.changeset/napi-explicit-settings.md @@ -0,0 +1,5 @@ +--- +"@pnpm/napi": minor +--- + +`readConfig` now returns `explicitSettings` — the camelCase names of settings the config cascade set explicitly — so hosts that layer the resolved config over their own defaults can forward only the values the user actually configured. diff --git a/pnpm/crates/napi/src/read_config.rs b/pnpm/crates/napi/src/read_config.rs index caf6da6353..db40e43086 100644 --- a/pnpm/crates/napi/src/read_config.rs +++ b/pnpm/crates/napi/src/read_config.rs @@ -81,6 +81,12 @@ pub struct ResolvedConfig { /// default) — not a per-project setting. `None` when no home /// directory is resolvable. pub pnpm_home_dir: Option, + /// The camelCase names of settings the cascade set explicitly + /// (`pnpm-workspace.yaml`, the global config, `pnpm_config_*` env + /// vars). Every other projected value is an engine default; an + /// embedder that layers this config over its own must forward only + /// the explicit ones. + pub explicit_settings: Vec, } #[napi(js_name = "readConfig")] @@ -161,6 +167,7 @@ fn project_config(config: &pacquet_config::Config) -> ResolvedConfig { shamefully_hoist: config.shamefully_hoist, pnpm_home_dir: pacquet_config::default_pnpm_home_dir::() .map(|dir| dir.display().to_string()), + explicit_settings: config.explicit_settings.keys().cloned().collect(), } } diff --git a/pnpm/crates/napi/src/read_config/tests.rs b/pnpm/crates/napi/src/read_config/tests.rs index 36b52ed70f..ef527a9529 100644 --- a/pnpm/crates/napi/src/read_config/tests.rs +++ b/pnpm/crates/napi/src/read_config/tests.rs @@ -170,3 +170,21 @@ fn read_config_resolves_the_project_npmrc_cascade() { assert!(!resolved.store_dir.is_empty()); assert!(!resolved.cache_dir.is_empty()); } + +/// A `pnpm-workspace.yaml` setting must surface both as its resolved +/// value and as an entry in `explicit_settings`, while an unset sibling +/// (whose projected value is an engine default) stays off that list. +#[test] +fn read_config_reports_explicitly_set_workspace_settings() { + let dir = tempfile::tempdir().expect("tempdir"); + std::fs::write(dir.path().join("pnpm-workspace.yaml"), "fetchRetries: 7\n") + .expect("write pnpm-workspace.yaml"); + + let resolved = + super::read_config(super::ReadConfigOptions { dir: dir.path().display().to_string() }) + .expect("read config"); + + assert_eq!(resolved.fetch_retries, 7); + assert!(resolved.explicit_settings.contains(&"fetchRetries".to_string())); + assert!(!resolved.explicit_settings.contains(&"fetchTimeout".to_string())); +} diff --git a/pnpm/npm/napi/index.d.ts b/pnpm/npm/napi/index.d.ts index 5ab51f0cd5..ac62ea6972 100644 --- a/pnpm/npm/napi/index.d.ts +++ b/pnpm/npm/napi/index.d.ts @@ -415,6 +415,13 @@ export interface ResolvedConfig { * not a per-project setting. Absent when no home directory is resolvable. */ pnpmHomeDir?: string + /** + * The camelCase names of settings the cascade set explicitly + * (`pnpm-workspace.yaml`, the global config, `pnpm_config_*` env vars). + * Every other projected value is an engine default; an embedder that + * layers this config over its own must forward only the explicit ones. + */ + explicitSettings: string[] } /**