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 package_extensions_checksum: None,
152 pnpmfile_checksum: None,
153 };
154
155 let hoisted = if self.auto_install_peers {
162 hoist_auto_installed_peers(canonical)
163 } else {
164 canonical
165 };
166
167 let peer_options = PeerContextOptions {
170 dedupe_peer_dependents: self.dedupe_peer_dependents,
171 dedupe_peers: self.dedupe_peers,
172 resolve_from_workspace_root: self.resolve_peers_from_workspace_root,
173 peers_suffix_max_length: self.peers_suffix_max_length,
174 };
175 let _diag_peer =
176 aube_util::diag::Span::new(aube_util::diag::Category::Resolver, "peer_context_apply");
177 let contextualized = apply_peer_contexts(hoisted, &peer_options)?;
178 drop(_diag_peer);
179 tracing::debug!(
180 "peer-context pass produced {} contextualized packages",
181 contextualized.packages.len()
182 );
183 Ok(contextualized)
184 }
185}
186
187async fn apply_read_package_to_importers(
197 hook: &mut dyn ReadPackageHook,
198 manifests: &mut [(String, PackageJson)],
199) -> Result<(), Error> {
200 for (importer_path, manifest) in manifests.iter_mut() {
201 let input = importer_to_version_metadata(manifest, importer_path)?;
202 let before_name = input.name.clone();
205 let before_version = input.version.clone();
206 let after = hook.read_package(input).await.map_err(|e| {
207 Error::Registry(
208 importer_label(importer_path, manifest),
209 format!("readPackage hook: {e}"),
210 )
211 })?;
212 if after.name != before_name || after.version != before_version {
213 tracing::warn!(
214 code = aube_codes::warnings::WARN_AUBE_HOOK_IDENTITY_REWRITTEN,
215 "[pnpmfile] readPackage rewrote importer {}@{} identity to {}@{}; \
216 aube ignores identity edits",
217 before_name,
218 before_version,
219 after.name,
220 after.version,
221 );
222 }
223 apply_version_metadata_to_importer(manifest, after);
224 }
225 Ok(())
226}
227
228fn importer_to_version_metadata(
235 manifest: &PackageJson,
236 importer_path: &str,
237) -> Result<VersionMetadata, Error> {
238 let mut value = serde_json::to_value(manifest).map_err(|e| {
239 Error::Registry(
240 importer_path.to_string(),
241 format!("readPackage hook: failed to serialize importer manifest: {e}"),
242 )
243 })?;
244 if !value.get("name").is_some_and(serde_json::Value::is_string) {
245 value["name"] = serde_json::Value::String(String::new());
246 }
247 if !value
248 .get("version")
249 .is_some_and(serde_json::Value::is_string)
250 {
251 value["version"] = serde_json::Value::String("0.0.0".to_string());
252 }
253 serde_json::from_value(value).map_err(|e| {
254 Error::Registry(
255 importer_path.to_string(),
256 format!("readPackage hook: failed to build hook input from importer manifest: {e}"),
257 )
258 })
259}
260
261fn apply_version_metadata_to_importer(manifest: &mut PackageJson, after: VersionMetadata) {
264 manifest.dependencies = after.dependencies;
265 manifest.dev_dependencies = after.dev_dependencies;
266 manifest.optional_dependencies = after.optional_dependencies;
267 manifest.peer_dependencies = after.peer_dependencies;
268 if after.peer_dependencies_meta.is_empty() {
273 manifest.extra.remove("peerDependenciesMeta");
274 } else if let Ok(v) = serde_json::to_value(&after.peer_dependencies_meta) {
275 manifest.extra.insert("peerDependenciesMeta".to_string(), v);
276 }
277}
278
279fn importer_label(importer_path: &str, manifest: &PackageJson) -> String {
282 match manifest.name.as_deref() {
283 Some(name) if !name.is_empty() => name.to_string(),
284 _ => importer_path.to_string(),
285 }
286}
287
288#[cfg(test)]
289mod tests {
290 use super::*;
291 use std::future::Future;
292 use std::pin::Pin;
293
294 struct MockHook<F>(F);
298
299 impl<F> ReadPackageHook for MockHook<F>
300 where
301 F: FnMut(VersionMetadata) -> Result<VersionMetadata, String> + Send,
302 {
303 fn read_package<'a>(
304 &'a mut self,
305 pkg: VersionMetadata,
306 ) -> Pin<Box<dyn Future<Output = Result<VersionMetadata, String>> + Send + 'a>> {
307 let out = (self.0)(pkg);
308 Box::pin(async move { out })
309 }
310 }
311
312 fn manifest(name: Option<&str>) -> PackageJson {
313 PackageJson {
314 name: name.map(str::to_string),
315 ..PackageJson::default()
316 }
317 }
318
319 #[tokio::test]
320 async fn applies_hook_edits_to_importer_self_manifest() {
321 let mut manifests = vec![(".".to_string(), manifest(Some("root-pkg")))];
322 let mut hook = MockHook(|mut pkg: VersionMetadata| {
323 if pkg.name == "root-pkg" {
324 pkg.dependencies
325 .insert("is-odd".to_string(), "3.0.1".to_string());
326 }
327 Ok(pkg)
328 });
329 apply_read_package_to_importers(&mut hook, &mut manifests)
330 .await
331 .unwrap();
332 assert_eq!(
333 manifests[0]
334 .1
335 .dependencies
336 .get("is-odd")
337 .map(String::as_str),
338 Some("3.0.1")
339 );
340 }
341
342 #[tokio::test]
343 async fn applies_hook_per_importer_in_a_workspace() {
344 let mut manifests = vec![
347 (".".to_string(), manifest(Some("root"))),
348 ("packages/app".to_string(), manifest(Some("app"))),
349 ("packages/lib".to_string(), manifest(Some("lib"))),
350 ];
351 let mut hook = MockHook(|mut pkg: VersionMetadata| {
352 if pkg.name == "app" {
354 pkg.dependencies
355 .insert("@scope/lib".to_string(), "link:../lib".to_string());
356 }
357 Ok(pkg)
358 });
359 apply_read_package_to_importers(&mut hook, &mut manifests)
360 .await
361 .unwrap();
362 assert_eq!(
363 manifests[1]
364 .1
365 .dependencies
366 .get("@scope/lib")
367 .map(String::as_str),
368 Some("link:../lib")
369 );
370 assert!(manifests[0].1.dependencies.is_empty());
371 assert!(manifests[2].1.dependencies.is_empty());
372 }
373
374 #[tokio::test]
375 async fn nameless_root_is_still_passed_to_hook() {
376 let mut manifests = vec![(".".to_string(), manifest(None))];
379 let mut hook = MockHook(|mut pkg: VersionMetadata| {
380 pkg.dependencies
381 .insert("marker".to_string(), "1.0.0".to_string());
382 Ok(pkg)
383 });
384 apply_read_package_to_importers(&mut hook, &mut manifests)
385 .await
386 .unwrap();
387 assert!(manifests[0].1.dependencies.contains_key("marker"));
388 }
389
390 #[tokio::test]
391 async fn hook_error_surfaces_as_registry_error() {
392 let mut manifests = vec![(".".to_string(), manifest(Some("x")))];
393 let mut hook = MockHook(|_pkg: VersionMetadata| Err("boom".to_string()));
394 let err = apply_read_package_to_importers(&mut hook, &mut manifests)
395 .await
396 .unwrap_err();
397 match err {
398 Error::Registry(name, msg) => {
399 assert_eq!(name, "x");
400 assert!(msg.contains("readPackage hook"), "got: {msg}");
401 assert!(msg.contains("boom"), "got: {msg}");
402 }
403 other => panic!("expected Registry error, got {other:?}"),
404 }
405 }
406
407 #[tokio::test]
408 async fn importer_identity_rewrite_is_ignored_but_deps_apply() {
409 let mut manifests = vec![(".".to_string(), manifest(Some("orig")))];
413 let mut hook = MockHook(|mut pkg: VersionMetadata| {
414 pkg.name = format!("{}-local", pkg.name);
415 pkg.version = "9.9.9".to_string();
416 pkg.dependencies
417 .insert("is-odd".to_string(), "3.0.1".to_string());
418 Ok(pkg)
419 });
420 apply_read_package_to_importers(&mut hook, &mut manifests)
421 .await
422 .unwrap();
423 assert_eq!(manifests[0].1.name.as_deref(), Some("orig"));
425 assert_eq!(
427 manifests[0]
428 .1
429 .dependencies
430 .get("is-odd")
431 .map(String::as_str),
432 Some("3.0.1")
433 );
434 }
435
436 #[test]
437 fn importer_to_version_metadata_injects_defaults_for_nameless_root() {
438 let vm = importer_to_version_metadata(&manifest(None), ".").unwrap();
439 assert_eq!(vm.name, "");
440 assert_eq!(vm.version, "0.0.0");
441 }
442
443 #[test]
444 fn importer_to_version_metadata_carries_all_dep_maps() {
445 let mut m = manifest(Some("p"));
446 m.dependencies.insert("a".into(), "1.0.0".into());
447 m.dev_dependencies.insert("b".into(), "^2".into());
448 m.optional_dependencies.insert("c".into(), "*".into());
449 m.peer_dependencies.insert("d".into(), ">=3".into());
450 let vm = importer_to_version_metadata(&m, ".").unwrap();
451 assert_eq!(vm.dependencies.get("a").map(String::as_str), Some("1.0.0"));
452 assert_eq!(vm.dev_dependencies.get("b").map(String::as_str), Some("^2"));
453 assert_eq!(
454 vm.optional_dependencies.get("c").map(String::as_str),
455 Some("*")
456 );
457 assert_eq!(
458 vm.peer_dependencies.get("d").map(String::as_str),
459 Some(">=3")
460 );
461 }
462
463 #[test]
464 fn apply_version_metadata_keeps_dep_edits_and_ignores_identity() {
465 let mut m = manifest(Some("orig"));
466 let mut after = importer_to_version_metadata(&m, ".").unwrap();
467 after.name = "changed".into();
468 after.version = "9.9.9".into();
469 after.dependencies.insert("x".into(), "1".into());
470 apply_version_metadata_to_importer(&mut m, after);
471 assert_eq!(m.name.as_deref(), Some("orig"));
473 assert_eq!(m.dependencies.get("x").map(String::as_str), Some("1"));
474 }
475}