1mod driver;
2mod fetch;
3mod seed;
4pub(crate) mod 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 let mut lockfile_overrides = self.overrides.clone();
123 for rule in &self.override_rules {
124 let Some(catalog_name) = rule
125 .replacement
126 .strip_prefix("catalog:")
127 .map(|name| if name.is_empty() { "default" } else { name })
128 else {
129 continue;
130 };
131 let Some(real_range) = self
132 .catalogs
133 .get(catalog_name)
134 .and_then(|catalog| catalog.get(&rule.target.name))
135 .filter(|range| !aube_util::pkg::is_catalog_spec(range))
136 else {
137 continue;
138 };
139 lockfile_overrides.insert(rule.raw_key.clone(), real_range.clone());
140 }
141
142 let canonical = LockfileGraph {
143 importers,
144 packages: resolved,
145 settings: aube_lockfile::LockfileSettings {
146 auto_install_peers: self.auto_install_peers,
147 exclude_links_from_lockfile: self.exclude_links_from_lockfile,
148 lockfile_include_tarball_url: false,
152 },
153 overrides: lockfile_overrides,
156 ignored_optional_dependencies: self.ignored_optional_dependencies.clone(),
157 times: resolved_times,
158 skipped_optional_dependencies,
159 catalogs: resolved_catalogs,
160 bun_config_version: None,
164 patched_dependencies: BTreeMap::new(),
168 trusted_dependencies: Vec::new(),
169 runtimes: BTreeMap::new(),
170 extra_fields: BTreeMap::new(),
171 workspace_extra_fields: BTreeMap::new(),
172 package_extensions_checksum: None,
176 pnpmfile_checksum: None,
177 };
178
179 let hoisted = if self.auto_install_peers {
186 hoist_auto_installed_peers(canonical)
187 } else {
188 canonical
189 };
190
191 let peer_options = PeerContextOptions {
194 dedupe_peer_dependents: self.dedupe_peer_dependents,
195 dedupe_peers: self.dedupe_peers,
196 resolve_from_workspace_root: self.resolve_peers_from_workspace_root,
197 peers_suffix_max_length: self.peers_suffix_max_length,
198 };
199 let _diag_peer =
200 aube_util::diag::Span::new(aube_util::diag::Category::Resolver, "peer_context_apply");
201 let contextualized = apply_peer_contexts(hoisted, &peer_options)?;
202 drop(_diag_peer);
203 tracing::debug!(
204 "peer-context pass produced {} contextualized packages",
205 contextualized.packages.len()
206 );
207 Ok(contextualized)
208 }
209}
210
211async fn apply_read_package_to_importers(
221 hook: &mut dyn ReadPackageHook,
222 manifests: &mut [(String, PackageJson)],
223) -> Result<(), Error> {
224 for (importer_path, manifest) in manifests.iter_mut() {
225 let input = importer_to_version_metadata(manifest, importer_path)?;
226 let before_name = input.name.clone();
229 let before_version = input.version.clone();
230 let after = hook.read_package(input).await.map_err(|e| {
231 Error::Registry(
232 importer_label(importer_path, manifest),
233 format!("readPackage hook: {e}"),
234 )
235 })?;
236 if after.name != before_name || after.version != before_version {
237 tracing::warn!(
238 code = aube_codes::warnings::WARN_AUBE_HOOK_IDENTITY_REWRITTEN,
239 "[pnpmfile] readPackage rewrote importer {}@{} identity to {}@{}; \
240 aube ignores identity edits",
241 before_name,
242 before_version,
243 after.name,
244 after.version,
245 );
246 }
247 apply_version_metadata_to_importer(manifest, after);
248 }
249 Ok(())
250}
251
252fn importer_to_version_metadata(
259 manifest: &PackageJson,
260 importer_path: &str,
261) -> Result<VersionMetadata, Error> {
262 let mut value = serde_json::to_value(manifest).map_err(|e| {
263 Error::Registry(
264 importer_path.to_string(),
265 format!("readPackage hook: failed to serialize importer manifest: {e}"),
266 )
267 })?;
268 if !value.get("name").is_some_and(serde_json::Value::is_string) {
269 value["name"] = serde_json::Value::String(String::new());
270 }
271 if !value
272 .get("version")
273 .is_some_and(serde_json::Value::is_string)
274 {
275 value["version"] = serde_json::Value::String("0.0.0".to_string());
276 }
277 serde_json::from_value(value).map_err(|e| {
278 Error::Registry(
279 importer_path.to_string(),
280 format!("readPackage hook: failed to build hook input from importer manifest: {e}"),
281 )
282 })
283}
284
285fn apply_version_metadata_to_importer(manifest: &mut PackageJson, after: VersionMetadata) {
288 manifest.dependencies = after.dependencies;
289 manifest.dev_dependencies = after.dev_dependencies;
290 manifest.optional_dependencies = after.optional_dependencies;
291 manifest.peer_dependencies = after.peer_dependencies;
292 if after.peer_dependencies_meta.is_empty() {
297 manifest.extra.remove("peerDependenciesMeta");
298 } else if let Ok(v) = serde_json::to_value(&after.peer_dependencies_meta) {
299 manifest.extra.insert("peerDependenciesMeta".to_string(), v);
300 }
301}
302
303fn importer_label(importer_path: &str, manifest: &PackageJson) -> String {
306 match manifest.name.as_deref() {
307 Some(name) if !name.is_empty() => name.to_string(),
308 _ => importer_path.to_string(),
309 }
310}
311
312#[cfg(test)]
313mod tests {
314 use super::*;
315 use std::future::Future;
316 use std::pin::Pin;
317
318 struct MockHook<F>(F);
322
323 impl<F> ReadPackageHook for MockHook<F>
324 where
325 F: FnMut(VersionMetadata) -> Result<VersionMetadata, String> + Send,
326 {
327 fn read_package<'a>(
328 &'a mut self,
329 pkg: VersionMetadata,
330 ) -> Pin<Box<dyn Future<Output = Result<VersionMetadata, String>> + Send + 'a>> {
331 let out = (self.0)(pkg);
332 Box::pin(async move { out })
333 }
334 }
335
336 fn manifest(name: Option<&str>) -> PackageJson {
337 PackageJson {
338 name: name.map(str::to_string),
339 ..PackageJson::default()
340 }
341 }
342
343 #[tokio::test]
344 async fn applies_hook_edits_to_importer_self_manifest() {
345 let mut manifests = vec![(".".to_string(), manifest(Some("root-pkg")))];
346 let mut hook = MockHook(|mut pkg: VersionMetadata| {
347 if pkg.name == "root-pkg" {
348 pkg.dependencies
349 .insert("is-odd".to_string(), "3.0.1".to_string());
350 }
351 Ok(pkg)
352 });
353 apply_read_package_to_importers(&mut hook, &mut manifests)
354 .await
355 .unwrap();
356 assert_eq!(
357 manifests[0]
358 .1
359 .dependencies
360 .get("is-odd")
361 .map(String::as_str),
362 Some("3.0.1")
363 );
364 }
365
366 #[tokio::test]
367 async fn applies_hook_per_importer_in_a_workspace() {
368 let mut manifests = vec![
371 (".".to_string(), manifest(Some("root"))),
372 ("packages/app".to_string(), manifest(Some("app"))),
373 ("packages/lib".to_string(), manifest(Some("lib"))),
374 ];
375 let mut hook = MockHook(|mut pkg: VersionMetadata| {
376 if pkg.name == "app" {
378 pkg.dependencies
379 .insert("@scope/lib".to_string(), "link:../lib".to_string());
380 }
381 Ok(pkg)
382 });
383 apply_read_package_to_importers(&mut hook, &mut manifests)
384 .await
385 .unwrap();
386 assert_eq!(
387 manifests[1]
388 .1
389 .dependencies
390 .get("@scope/lib")
391 .map(String::as_str),
392 Some("link:../lib")
393 );
394 assert!(manifests[0].1.dependencies.is_empty());
395 assert!(manifests[2].1.dependencies.is_empty());
396 }
397
398 #[tokio::test]
399 async fn nameless_root_is_still_passed_to_hook() {
400 let mut manifests = vec![(".".to_string(), manifest(None))];
403 let mut hook = MockHook(|mut pkg: VersionMetadata| {
404 pkg.dependencies
405 .insert("marker".to_string(), "1.0.0".to_string());
406 Ok(pkg)
407 });
408 apply_read_package_to_importers(&mut hook, &mut manifests)
409 .await
410 .unwrap();
411 assert!(manifests[0].1.dependencies.contains_key("marker"));
412 }
413
414 #[tokio::test]
415 async fn hook_error_surfaces_as_registry_error() {
416 let mut manifests = vec![(".".to_string(), manifest(Some("x")))];
417 let mut hook = MockHook(|_pkg: VersionMetadata| Err("boom".to_string()));
418 let err = apply_read_package_to_importers(&mut hook, &mut manifests)
419 .await
420 .unwrap_err();
421 match err {
422 Error::Registry(name, msg) => {
423 assert_eq!(name, "x");
424 assert!(msg.contains("readPackage hook"), "got: {msg}");
425 assert!(msg.contains("boom"), "got: {msg}");
426 }
427 other => panic!("expected Registry error, got {other:?}"),
428 }
429 }
430
431 #[tokio::test]
432 async fn importer_identity_rewrite_is_ignored_but_deps_apply() {
433 let mut manifests = vec![(".".to_string(), manifest(Some("orig")))];
437 let mut hook = MockHook(|mut pkg: VersionMetadata| {
438 pkg.name = format!("{}-local", pkg.name);
439 pkg.version = "9.9.9".to_string();
440 pkg.dependencies
441 .insert("is-odd".to_string(), "3.0.1".to_string());
442 Ok(pkg)
443 });
444 apply_read_package_to_importers(&mut hook, &mut manifests)
445 .await
446 .unwrap();
447 assert_eq!(manifests[0].1.name.as_deref(), Some("orig"));
449 assert_eq!(
451 manifests[0]
452 .1
453 .dependencies
454 .get("is-odd")
455 .map(String::as_str),
456 Some("3.0.1")
457 );
458 }
459
460 #[test]
461 fn importer_to_version_metadata_injects_defaults_for_nameless_root() {
462 let vm = importer_to_version_metadata(&manifest(None), ".").unwrap();
463 assert_eq!(vm.name, "");
464 assert_eq!(vm.version, "0.0.0");
465 }
466
467 #[test]
468 fn importer_to_version_metadata_carries_all_dep_maps() {
469 let mut m = manifest(Some("p"));
470 m.dependencies.insert("a".into(), "1.0.0".into());
471 m.dev_dependencies.insert("b".into(), "^2".into());
472 m.optional_dependencies.insert("c".into(), "*".into());
473 m.peer_dependencies.insert("d".into(), ">=3".into());
474 let vm = importer_to_version_metadata(&m, ".").unwrap();
475 assert_eq!(vm.dependencies.get("a").map(String::as_str), Some("1.0.0"));
476 assert_eq!(vm.dev_dependencies.get("b").map(String::as_str), Some("^2"));
477 assert_eq!(
478 vm.optional_dependencies.get("c").map(String::as_str),
479 Some("*")
480 );
481 assert_eq!(
482 vm.peer_dependencies.get("d").map(String::as_str),
483 Some(">=3")
484 );
485 }
486
487 #[test]
488 fn apply_version_metadata_keeps_dep_edits_and_ignores_identity() {
489 let mut m = manifest(Some("orig"));
490 let mut after = importer_to_version_metadata(&m, ".").unwrap();
491 after.name = "changed".into();
492 after.version = "9.9.9".into();
493 after.dependencies.insert("x".into(), "1".into());
494 apply_version_metadata_to_importer(&mut m, after);
495 assert_eq!(m.name.as_deref(), Some("orig"));
497 assert_eq!(m.dependencies.get("x").map(String::as_str), Some("1"));
498 }
499}