1mod driver;
2mod fetch;
3mod seed;
4mod vulnerable;
5
6use crate::local_source::is_non_registry_specifier;
7use crate::semver_util::version_satisfies;
8use crate::{
9 Error, FxHashMap, PeerContextOptions, ReadPackageHook, Resolver, apply_peer_contexts, catalog,
10 hoist_auto_installed_peers,
11};
12use aube_lockfile::{DirectDep, LockedPackage, LockfileGraph};
13use aube_manifest::PackageJson;
14use aube_registry::VersionMetadata;
15use std::collections::{BTreeMap, HashMap};
16
17impl Resolver {
18 pub async fn resolve(
24 &mut self,
25 manifest: &PackageJson,
26 existing: Option<&LockfileGraph>,
27 ) -> Result<LockfileGraph, Error> {
28 self.resolve_workspace(
29 &[(".".to_string(), manifest.clone())],
30 existing,
31 &HashMap::new(),
32 )
33 .await
34 }
35
36 pub async fn resolve_workspace(
45 &mut self,
46 manifests: &[(String, PackageJson)],
47 existing: Option<&LockfileGraph>,
48 workspace_packages: &HashMap<String, String>,
49 ) -> Result<LockfileGraph, Error> {
50 let hooked_manifests = if let Some(hook) = self.read_package_hook.as_deref_mut() {
60 let mut owned = manifests.to_vec();
61 apply_read_package_to_importers(hook, &mut owned).await?;
62 Some(owned)
63 } else {
64 None
65 };
66 let manifests = hooked_manifests.as_deref().unwrap_or(manifests);
67 driver::ResolveDriver::new(self, manifests, existing, workspace_packages)
68 .run()
69 .await
70 }
71
72 fn is_prefetchable(
82 &self,
83 name: &str,
84 range: &str,
85 workspace_packages: &HashMap<String, String>,
86 ) -> bool {
87 let workspace_hit = workspace_packages
88 .get(name)
89 .is_some_and(|ws_v| version_satisfies(ws_v, range));
90 !aube_util::pkg::is_workspace_spec(range)
91 && !aube_util::pkg::is_catalog_spec(range)
92 && !aube_util::pkg::is_npm_spec(range)
93 && !aube_util::pkg::is_jsr_spec(range)
94 && !is_non_registry_specifier(range)
95 && !self.overrides.contains_key(name)
96 && !workspace_hit
97 }
98
99 fn finalize_resolved_graph(
106 &self,
107 importers: BTreeMap<String, Vec<DirectDep>>,
108 resolved: BTreeMap<String, LockedPackage>,
109 resolved_versions: &FxHashMap<String, Vec<String>>,
110 resolved_times: BTreeMap<String, String>,
111 skipped_optional_dependencies: BTreeMap<String, BTreeMap<String, String>>,
112 catalog_picks: BTreeMap<String, BTreeMap<String, String>>,
113 ) -> Result<LockfileGraph, Error> {
114 let resolved_catalogs =
115 catalog::materialize_catalog_picks(catalog_picks, resolved_versions);
116
117 let canonical = LockfileGraph {
118 importers,
119 packages: resolved,
120 settings: aube_lockfile::LockfileSettings {
121 auto_install_peers: self.auto_install_peers,
122 exclude_links_from_lockfile: self.exclude_links_from_lockfile,
123 lockfile_include_tarball_url: false,
127 },
128 overrides: self.overrides.clone(),
132 ignored_optional_dependencies: self.ignored_optional_dependencies.clone(),
133 times: resolved_times,
134 skipped_optional_dependencies,
135 catalogs: resolved_catalogs,
136 bun_config_version: None,
140 patched_dependencies: BTreeMap::new(),
144 trusted_dependencies: Vec::new(),
145 runtimes: BTreeMap::new(),
146 extra_fields: BTreeMap::new(),
147 workspace_extra_fields: BTreeMap::new(),
148 };
149
150 let hoisted = if self.auto_install_peers {
157 hoist_auto_installed_peers(canonical)
158 } else {
159 canonical
160 };
161
162 let peer_options = PeerContextOptions {
165 dedupe_peer_dependents: self.dedupe_peer_dependents,
166 dedupe_peers: self.dedupe_peers,
167 resolve_from_workspace_root: self.resolve_peers_from_workspace_root,
168 peers_suffix_max_length: self.peers_suffix_max_length,
169 };
170 let _diag_peer =
171 aube_util::diag::Span::new(aube_util::diag::Category::Resolver, "peer_context_apply");
172 let contextualized = apply_peer_contexts(hoisted, &peer_options)?;
173 drop(_diag_peer);
174 tracing::debug!(
175 "peer-context pass produced {} contextualized packages",
176 contextualized.packages.len()
177 );
178 Ok(contextualized)
179 }
180}
181
182async fn apply_read_package_to_importers(
192 hook: &mut dyn ReadPackageHook,
193 manifests: &mut [(String, PackageJson)],
194) -> Result<(), Error> {
195 for (importer_path, manifest) in manifests.iter_mut() {
196 let input = importer_to_version_metadata(manifest, importer_path)?;
197 let before_name = input.name.clone();
200 let before_version = input.version.clone();
201 let after = hook.read_package(input).await.map_err(|e| {
202 Error::Registry(
203 importer_label(importer_path, manifest),
204 format!("readPackage hook: {e}"),
205 )
206 })?;
207 if after.name != before_name || after.version != before_version {
208 tracing::warn!(
209 code = aube_codes::warnings::WARN_AUBE_HOOK_IDENTITY_REWRITTEN,
210 "[pnpmfile] readPackage rewrote importer {}@{} identity to {}@{}; \
211 aube ignores identity edits",
212 before_name,
213 before_version,
214 after.name,
215 after.version,
216 );
217 }
218 apply_version_metadata_to_importer(manifest, after);
219 }
220 Ok(())
221}
222
223fn importer_to_version_metadata(
230 manifest: &PackageJson,
231 importer_path: &str,
232) -> Result<VersionMetadata, Error> {
233 let mut value = serde_json::to_value(manifest).map_err(|e| {
234 Error::Registry(
235 importer_path.to_string(),
236 format!("readPackage hook: failed to serialize importer manifest: {e}"),
237 )
238 })?;
239 if !value.get("name").is_some_and(serde_json::Value::is_string) {
240 value["name"] = serde_json::Value::String(String::new());
241 }
242 if !value
243 .get("version")
244 .is_some_and(serde_json::Value::is_string)
245 {
246 value["version"] = serde_json::Value::String("0.0.0".to_string());
247 }
248 serde_json::from_value(value).map_err(|e| {
249 Error::Registry(
250 importer_path.to_string(),
251 format!("readPackage hook: failed to build hook input from importer manifest: {e}"),
252 )
253 })
254}
255
256fn apply_version_metadata_to_importer(manifest: &mut PackageJson, after: VersionMetadata) {
259 manifest.dependencies = after.dependencies;
260 manifest.dev_dependencies = after.dev_dependencies;
261 manifest.optional_dependencies = after.optional_dependencies;
262 manifest.peer_dependencies = after.peer_dependencies;
263 if after.peer_dependencies_meta.is_empty() {
268 manifest.extra.remove("peerDependenciesMeta");
269 } else if let Ok(v) = serde_json::to_value(&after.peer_dependencies_meta) {
270 manifest.extra.insert("peerDependenciesMeta".to_string(), v);
271 }
272}
273
274fn importer_label(importer_path: &str, manifest: &PackageJson) -> String {
277 match manifest.name.as_deref() {
278 Some(name) if !name.is_empty() => name.to_string(),
279 _ => importer_path.to_string(),
280 }
281}
282
283#[cfg(test)]
284mod tests {
285 use super::*;
286 use std::future::Future;
287 use std::pin::Pin;
288
289 struct MockHook<F>(F);
293
294 impl<F> ReadPackageHook for MockHook<F>
295 where
296 F: FnMut(VersionMetadata) -> Result<VersionMetadata, String> + Send,
297 {
298 fn read_package<'a>(
299 &'a mut self,
300 pkg: VersionMetadata,
301 ) -> Pin<Box<dyn Future<Output = Result<VersionMetadata, String>> + Send + 'a>> {
302 let out = (self.0)(pkg);
303 Box::pin(async move { out })
304 }
305 }
306
307 fn manifest(name: Option<&str>) -> PackageJson {
308 PackageJson {
309 name: name.map(str::to_string),
310 ..PackageJson::default()
311 }
312 }
313
314 #[tokio::test]
315 async fn applies_hook_edits_to_importer_self_manifest() {
316 let mut manifests = vec![(".".to_string(), manifest(Some("root-pkg")))];
317 let mut hook = MockHook(|mut pkg: VersionMetadata| {
318 if pkg.name == "root-pkg" {
319 pkg.dependencies
320 .insert("is-odd".to_string(), "3.0.1".to_string());
321 }
322 Ok(pkg)
323 });
324 apply_read_package_to_importers(&mut hook, &mut manifests)
325 .await
326 .unwrap();
327 assert_eq!(
328 manifests[0]
329 .1
330 .dependencies
331 .get("is-odd")
332 .map(String::as_str),
333 Some("3.0.1")
334 );
335 }
336
337 #[tokio::test]
338 async fn applies_hook_per_importer_in_a_workspace() {
339 let mut manifests = vec![
342 (".".to_string(), manifest(Some("root"))),
343 ("packages/app".to_string(), manifest(Some("app"))),
344 ("packages/lib".to_string(), manifest(Some("lib"))),
345 ];
346 let mut hook = MockHook(|mut pkg: VersionMetadata| {
347 if pkg.name == "app" {
349 pkg.dependencies
350 .insert("@scope/lib".to_string(), "link:../lib".to_string());
351 }
352 Ok(pkg)
353 });
354 apply_read_package_to_importers(&mut hook, &mut manifests)
355 .await
356 .unwrap();
357 assert_eq!(
358 manifests[1]
359 .1
360 .dependencies
361 .get("@scope/lib")
362 .map(String::as_str),
363 Some("link:../lib")
364 );
365 assert!(manifests[0].1.dependencies.is_empty());
366 assert!(manifests[2].1.dependencies.is_empty());
367 }
368
369 #[tokio::test]
370 async fn nameless_root_is_still_passed_to_hook() {
371 let mut manifests = vec![(".".to_string(), manifest(None))];
374 let mut hook = MockHook(|mut pkg: VersionMetadata| {
375 pkg.dependencies
376 .insert("marker".to_string(), "1.0.0".to_string());
377 Ok(pkg)
378 });
379 apply_read_package_to_importers(&mut hook, &mut manifests)
380 .await
381 .unwrap();
382 assert!(manifests[0].1.dependencies.contains_key("marker"));
383 }
384
385 #[tokio::test]
386 async fn hook_error_surfaces_as_registry_error() {
387 let mut manifests = vec![(".".to_string(), manifest(Some("x")))];
388 let mut hook = MockHook(|_pkg: VersionMetadata| Err("boom".to_string()));
389 let err = apply_read_package_to_importers(&mut hook, &mut manifests)
390 .await
391 .unwrap_err();
392 match err {
393 Error::Registry(name, msg) => {
394 assert_eq!(name, "x");
395 assert!(msg.contains("readPackage hook"), "got: {msg}");
396 assert!(msg.contains("boom"), "got: {msg}");
397 }
398 other => panic!("expected Registry error, got {other:?}"),
399 }
400 }
401
402 #[tokio::test]
403 async fn importer_identity_rewrite_is_ignored_but_deps_apply() {
404 let mut manifests = vec![(".".to_string(), manifest(Some("orig")))];
408 let mut hook = MockHook(|mut pkg: VersionMetadata| {
409 pkg.name = format!("{}-local", pkg.name);
410 pkg.version = "9.9.9".to_string();
411 pkg.dependencies
412 .insert("is-odd".to_string(), "3.0.1".to_string());
413 Ok(pkg)
414 });
415 apply_read_package_to_importers(&mut hook, &mut manifests)
416 .await
417 .unwrap();
418 assert_eq!(manifests[0].1.name.as_deref(), Some("orig"));
420 assert_eq!(
422 manifests[0]
423 .1
424 .dependencies
425 .get("is-odd")
426 .map(String::as_str),
427 Some("3.0.1")
428 );
429 }
430
431 #[test]
432 fn importer_to_version_metadata_injects_defaults_for_nameless_root() {
433 let vm = importer_to_version_metadata(&manifest(None), ".").unwrap();
434 assert_eq!(vm.name, "");
435 assert_eq!(vm.version, "0.0.0");
436 }
437
438 #[test]
439 fn importer_to_version_metadata_carries_all_dep_maps() {
440 let mut m = manifest(Some("p"));
441 m.dependencies.insert("a".into(), "1.0.0".into());
442 m.dev_dependencies.insert("b".into(), "^2".into());
443 m.optional_dependencies.insert("c".into(), "*".into());
444 m.peer_dependencies.insert("d".into(), ">=3".into());
445 let vm = importer_to_version_metadata(&m, ".").unwrap();
446 assert_eq!(vm.dependencies.get("a").map(String::as_str), Some("1.0.0"));
447 assert_eq!(vm.dev_dependencies.get("b").map(String::as_str), Some("^2"));
448 assert_eq!(
449 vm.optional_dependencies.get("c").map(String::as_str),
450 Some("*")
451 );
452 assert_eq!(
453 vm.peer_dependencies.get("d").map(String::as_str),
454 Some(">=3")
455 );
456 }
457
458 #[test]
459 fn apply_version_metadata_keeps_dep_edits_and_ignores_identity() {
460 let mut m = manifest(Some("orig"));
461 let mut after = importer_to_version_metadata(&m, ".").unwrap();
462 after.name = "changed".into();
463 after.version = "9.9.9".into();
464 after.dependencies.insert("x".into(), "1".into());
465 apply_version_metadata_to_importer(&mut m, after);
466 assert_eq!(m.name.as_deref(), Some("orig"));
468 assert_eq!(m.dependencies.get("x").map(String::as_str), Some("1"));
469 }
470}