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};
11use aube_lockfile::{DirectDep, LockedPackage, LockfileGraph};
12use aube_manifest::PackageJson;
13use aube_registry::VersionMetadata;
14use std::collections::{BTreeMap, HashMap};
15
16impl Resolver {
17 pub async fn resolve(
23 &mut self,
24 manifest: &PackageJson,
25 existing: Option<&LockfileGraph>,
26 ) -> Result<LockfileGraph, Error> {
27 self.resolve_workspace(
28 &[(".".to_string(), manifest.clone())],
29 existing,
30 &HashMap::new(),
31 )
32 .await
33 }
34
35 pub async fn resolve_workspace(
44 &mut self,
45 manifests: &[(String, PackageJson)],
46 existing: Option<&LockfileGraph>,
47 workspace_packages: &HashMap<String, String>,
48 ) -> Result<LockfileGraph, Error> {
49 let hooked_manifests = if let Some(hook) = self.read_package_hook.as_deref_mut() {
59 let mut owned = manifests.to_vec();
60 apply_read_package_to_importers(hook, &mut owned).await?;
61 Some(owned)
62 } else {
63 None
64 };
65 let manifests = hooked_manifests.as_deref().unwrap_or(manifests);
66 driver::ResolveDriver::new(self, manifests, existing, workspace_packages)
67 .run()
68 .await
69 }
70
71 fn is_prefetchable(
81 &self,
82 name: &str,
83 range: &str,
84 workspace_packages: &HashMap<String, String>,
85 ) -> bool {
86 let workspace_hit = workspace_packages
87 .get(name)
88 .is_some_and(|ws_v| version_satisfies(ws_v, range));
89 !aube_util::pkg::is_workspace_spec(range)
90 && !aube_util::pkg::is_catalog_spec(range)
91 && !aube_util::pkg::is_npm_spec(range)
92 && !aube_util::pkg::is_jsr_spec(range)
93 && !is_non_registry_specifier(range)
94 && !self.overrides.contains_key(name)
95 && !workspace_hit
96 }
97
98 fn finalize_resolved_graph(
105 &self,
106 importers: BTreeMap<String, Vec<DirectDep>>,
107 resolved: BTreeMap<String, LockedPackage>,
108 resolved_versions: &FxHashMap<String, Vec<String>>,
109 resolved_times: BTreeMap<String, String>,
110 skipped_optional_dependencies: BTreeMap<String, BTreeMap<String, String>>,
111 catalog_picks: BTreeMap<String, BTreeMap<String, String>>,
112 ) -> Result<LockfileGraph, Error> {
113 let resolved_catalogs =
114 catalog::materialize_catalog_picks(catalog_picks, resolved_versions);
115 let mut lockfile_overrides = self.overrides.clone();
122 for rule in &self.override_rules {
123 let Some(catalog_name) = rule
124 .replacement
125 .strip_prefix("catalog:")
126 .map(|name| if name.is_empty() { "default" } else { name })
127 else {
128 continue;
129 };
130 let Some(real_range) = self
131 .catalogs
132 .get(catalog_name)
133 .and_then(|catalog| catalog.get(&rule.target.name))
134 .filter(|range| !aube_util::pkg::is_catalog_spec(range))
135 else {
136 continue;
137 };
138 lockfile_overrides.insert(rule.raw_key.clone(), real_range.clone());
139 }
140
141 let canonical = LockfileGraph {
142 importers,
143 packages: resolved,
144 settings: aube_lockfile::LockfileSettings {
145 auto_install_peers: self.auto_install_peers,
146 exclude_links_from_lockfile: self.exclude_links_from_lockfile,
147 lockfile_include_tarball_url: false,
151 },
152 overrides: lockfile_overrides,
155 ignored_optional_dependencies: self.ignored_optional_dependencies.clone(),
156 times: resolved_times,
157 skipped_optional_dependencies,
158 catalogs: resolved_catalogs,
159 bun_config_version: None,
163 patched_dependencies: BTreeMap::new(),
167 trusted_dependencies: Vec::new(),
168 runtimes: BTreeMap::new(),
169 extra_fields: BTreeMap::new(),
170 workspace_extra_fields: BTreeMap::new(),
171 package_extensions_checksum: None,
175 pnpmfile_checksum: None,
176 };
177
178 let peer_options = PeerContextOptions {
181 dedupe_peer_dependents: self.dedupe_peer_dependents,
182 dedupe_peers: self.dedupe_peers,
183 resolve_from_workspace_root: self.resolve_peers_from_workspace_root,
184 peers_suffix_max_length: self.peers_suffix_max_length,
185 };
186 let _diag_peer =
187 aube_util::diag::Span::new(aube_util::diag::Category::Resolver, "peer_context_apply");
188 let contextualized = apply_peer_contexts(canonical, &peer_options)?;
189 drop(_diag_peer);
190 tracing::debug!(
191 "peer-context pass produced {} contextualized packages",
192 contextualized.packages.len()
193 );
194 Ok(contextualized)
195 }
196}
197
198async fn apply_read_package_to_importers(
208 hook: &mut dyn ReadPackageHook,
209 manifests: &mut [(String, PackageJson)],
210) -> Result<(), Error> {
211 for (importer_path, manifest) in manifests.iter_mut() {
212 let input = importer_to_version_metadata(manifest, importer_path)?;
213 let before_name = input.name.clone();
216 let before_version = input.version.clone();
217 let after = hook.read_package(input).await.map_err(|e| {
218 Error::Registry(
219 importer_label(importer_path, manifest),
220 format!("readPackage hook: {e}"),
221 )
222 })?;
223 if after.name != before_name || after.version != before_version {
224 tracing::warn!(
225 code = aube_codes::warnings::WARN_AUBE_HOOK_IDENTITY_REWRITTEN,
226 "[pnpmfile] readPackage rewrote importer {}@{} identity to {}@{}; \
227 aube ignores identity edits",
228 before_name,
229 before_version,
230 after.name,
231 after.version,
232 );
233 }
234 apply_version_metadata_to_importer(manifest, after);
235 }
236 Ok(())
237}
238
239fn importer_to_version_metadata(
246 manifest: &PackageJson,
247 importer_path: &str,
248) -> Result<VersionMetadata, Error> {
249 let mut value = serde_json::to_value(manifest).map_err(|e| {
250 Error::Registry(
251 importer_path.to_string(),
252 format!("readPackage hook: failed to serialize importer manifest: {e}"),
253 )
254 })?;
255 if !value.get("name").is_some_and(serde_json::Value::is_string) {
256 value["name"] = serde_json::Value::String(String::new());
257 }
258 if !value
259 .get("version")
260 .is_some_and(serde_json::Value::is_string)
261 {
262 value["version"] = serde_json::Value::String("0.0.0".to_string());
263 }
264 serde_json::from_value(value).map_err(|e| {
265 Error::Registry(
266 importer_path.to_string(),
267 format!("readPackage hook: failed to build hook input from importer manifest: {e}"),
268 )
269 })
270}
271
272fn apply_version_metadata_to_importer(manifest: &mut PackageJson, after: VersionMetadata) {
275 manifest.dependencies = after.dependencies;
276 manifest.dev_dependencies = after.dev_dependencies;
277 manifest.optional_dependencies = after.optional_dependencies;
278 manifest.peer_dependencies = after.peer_dependencies;
279 if after.peer_dependencies_meta.is_empty() {
284 manifest.extra.remove("peerDependenciesMeta");
285 } else if let Ok(v) = serde_json::to_value(&after.peer_dependencies_meta) {
286 manifest.extra.insert("peerDependenciesMeta".to_string(), v);
287 }
288}
289
290fn importer_label(importer_path: &str, manifest: &PackageJson) -> String {
293 match manifest.name.as_deref() {
294 Some(name) if !name.is_empty() => name.to_string(),
295 _ => importer_path.to_string(),
296 }
297}
298
299#[cfg(test)]
300mod tests {
301 use super::*;
302 use std::future::Future;
303 use std::pin::Pin;
304
305 struct MockHook<F>(F);
309
310 impl<F> ReadPackageHook for MockHook<F>
311 where
312 F: FnMut(VersionMetadata) -> Result<VersionMetadata, String> + Send,
313 {
314 fn read_package<'a>(
315 &'a mut self,
316 pkg: VersionMetadata,
317 ) -> Pin<Box<dyn Future<Output = Result<VersionMetadata, String>> + Send + 'a>> {
318 let out = (self.0)(pkg);
319 Box::pin(async move { out })
320 }
321 }
322
323 fn manifest(name: Option<&str>) -> PackageJson {
324 PackageJson {
325 name: name.map(str::to_string),
326 ..PackageJson::default()
327 }
328 }
329
330 #[tokio::test]
331 async fn applies_hook_edits_to_importer_self_manifest() {
332 let mut manifests = vec![(".".to_string(), manifest(Some("root-pkg")))];
333 let mut hook = MockHook(|mut pkg: VersionMetadata| {
334 if pkg.name == "root-pkg" {
335 pkg.dependencies
336 .insert("is-odd".to_string(), "3.0.1".to_string());
337 }
338 Ok(pkg)
339 });
340 apply_read_package_to_importers(&mut hook, &mut manifests)
341 .await
342 .unwrap();
343 assert_eq!(
344 manifests[0]
345 .1
346 .dependencies
347 .get("is-odd")
348 .map(String::as_str),
349 Some("3.0.1")
350 );
351 }
352
353 #[tokio::test]
354 async fn applies_hook_per_importer_in_a_workspace() {
355 let mut manifests = vec![
358 (".".to_string(), manifest(Some("root"))),
359 ("packages/app".to_string(), manifest(Some("app"))),
360 ("packages/lib".to_string(), manifest(Some("lib"))),
361 ];
362 let mut hook = MockHook(|mut pkg: VersionMetadata| {
363 if pkg.name == "app" {
365 pkg.dependencies
366 .insert("@scope/lib".to_string(), "link:../lib".to_string());
367 }
368 Ok(pkg)
369 });
370 apply_read_package_to_importers(&mut hook, &mut manifests)
371 .await
372 .unwrap();
373 assert_eq!(
374 manifests[1]
375 .1
376 .dependencies
377 .get("@scope/lib")
378 .map(String::as_str),
379 Some("link:../lib")
380 );
381 assert!(manifests[0].1.dependencies.is_empty());
382 assert!(manifests[2].1.dependencies.is_empty());
383 }
384
385 #[tokio::test]
386 async fn nameless_root_is_still_passed_to_hook() {
387 let mut manifests = vec![(".".to_string(), manifest(None))];
390 let mut hook = MockHook(|mut pkg: VersionMetadata| {
391 pkg.dependencies
392 .insert("marker".to_string(), "1.0.0".to_string());
393 Ok(pkg)
394 });
395 apply_read_package_to_importers(&mut hook, &mut manifests)
396 .await
397 .unwrap();
398 assert!(manifests[0].1.dependencies.contains_key("marker"));
399 }
400
401 #[tokio::test]
402 async fn hook_error_surfaces_as_registry_error() {
403 let mut manifests = vec![(".".to_string(), manifest(Some("x")))];
404 let mut hook = MockHook(|_pkg: VersionMetadata| Err("boom".to_string()));
405 let err = apply_read_package_to_importers(&mut hook, &mut manifests)
406 .await
407 .unwrap_err();
408 match err {
409 Error::Registry(name, msg) => {
410 assert_eq!(name, "x");
411 assert!(msg.contains("readPackage hook"), "got: {msg}");
412 assert!(msg.contains("boom"), "got: {msg}");
413 }
414 other => panic!("expected Registry error, got {other:?}"),
415 }
416 }
417
418 #[tokio::test]
419 async fn importer_identity_rewrite_is_ignored_but_deps_apply() {
420 let mut manifests = vec![(".".to_string(), manifest(Some("orig")))];
424 let mut hook = MockHook(|mut pkg: VersionMetadata| {
425 pkg.name = format!("{}-local", pkg.name);
426 pkg.version = "9.9.9".to_string();
427 pkg.dependencies
428 .insert("is-odd".to_string(), "3.0.1".to_string());
429 Ok(pkg)
430 });
431 apply_read_package_to_importers(&mut hook, &mut manifests)
432 .await
433 .unwrap();
434 assert_eq!(manifests[0].1.name.as_deref(), Some("orig"));
436 assert_eq!(
438 manifests[0]
439 .1
440 .dependencies
441 .get("is-odd")
442 .map(String::as_str),
443 Some("3.0.1")
444 );
445 }
446
447 #[test]
448 fn importer_to_version_metadata_injects_defaults_for_nameless_root() {
449 let vm = importer_to_version_metadata(&manifest(None), ".").unwrap();
450 assert_eq!(vm.name, "");
451 assert_eq!(vm.version, "0.0.0");
452 }
453
454 #[test]
455 fn importer_to_version_metadata_carries_all_dep_maps() {
456 let mut m = manifest(Some("p"));
457 m.dependencies.insert("a".into(), "1.0.0".into());
458 m.dev_dependencies.insert("b".into(), "^2".into());
459 m.optional_dependencies.insert("c".into(), "*".into());
460 m.peer_dependencies.insert("d".into(), ">=3".into());
461 let vm = importer_to_version_metadata(&m, ".").unwrap();
462 assert_eq!(vm.dependencies.get("a").map(String::as_str), Some("1.0.0"));
463 assert_eq!(vm.dev_dependencies.get("b").map(String::as_str), Some("^2"));
464 assert_eq!(
465 vm.optional_dependencies.get("c").map(String::as_str),
466 Some("*")
467 );
468 assert_eq!(
469 vm.peer_dependencies.get("d").map(String::as_str),
470 Some(">=3")
471 );
472 }
473
474 #[test]
475 fn apply_version_metadata_keeps_dep_edits_and_ignores_identity() {
476 let mut m = manifest(Some("orig"));
477 let mut after = importer_to_version_metadata(&m, ".").unwrap();
478 after.name = "changed".into();
479 after.version = "9.9.9".into();
480 after.dependencies.insert("x".into(), "1".into());
481 apply_version_metadata_to_importer(&mut m, after);
482 assert_eq!(m.name.as_deref(), Some("orig"));
484 assert_eq!(m.dependencies.get("x").map(String::as_str), Some("1"));
485 }
486}