1use std::path::{Path, PathBuf};
2use std::sync::Arc;
3use std::time::Duration;
4
5#[cfg(not(target_arch = "wasm32"))]
6use async_std::fs;
7use nassun::client::{Nassun, NassunOpts};
8use nassun::package::Package;
9use oro_common::CorgiManifest;
10use unicase::UniCase;
11use url::Url;
12
13#[cfg(not(target_arch = "wasm32"))]
14use crate::error::IoContext;
15use crate::error::NodeMaintainerError;
16use crate::graph::{Graph, Node};
17use crate::linkers::Linker;
18#[cfg(not(target_arch = "wasm32"))]
19use crate::linkers::LinkerOptions;
20use crate::resolver::Resolver;
21use crate::{IntoKdl, Lockfile};
22
23pub const DEFAULT_CONCURRENCY: usize = 50;
24pub const DEFAULT_SCRIPT_CONCURRENCY: usize = 6;
25
26#[cfg(not(target_arch = "wasm32"))]
27pub const META_FILE_NAME: &str = ".orogene-meta.kdl";
28#[cfg(not(target_arch = "wasm32"))]
29pub const STORE_DIR_NAME: &str = ".oro-store";
30
31pub type ProgressAdded = Arc<dyn Fn() + Send + Sync>;
32pub type ProgressHandler = Arc<dyn Fn(&Package, Duration) + Send + Sync>;
33pub type PruneProgress = Arc<dyn Fn(&Path) + Send + Sync>;
34pub type ScriptStartHandler = Arc<dyn Fn(&Package, &str) + Send + Sync>;
35pub type ScriptLineHandler = Arc<dyn Fn(&str) + Send + Sync>;
36
37#[derive(Clone)]
38pub struct NodeMaintainerOptions {
39 nassun_opts: NassunOpts,
40 nassun: Option<Nassun>,
41 concurrency: usize,
42 locked: bool,
43 kdl_lock: Option<Lockfile>,
44 npm_lock: Option<Lockfile>,
45
46 #[allow(dead_code)]
47 hoisted: bool,
48 #[allow(dead_code)]
49 script_concurrency: usize,
50 #[allow(dead_code)]
51 cache: Option<PathBuf>,
52 #[allow(dead_code)]
53 prefer_copy: bool,
54 #[allow(dead_code)]
55 validate: bool,
56 #[allow(dead_code)]
57 root: Option<PathBuf>,
58
59 on_resolution_added: Option<ProgressAdded>,
61 on_resolve_progress: Option<ProgressHandler>,
62 #[allow(dead_code)]
63 on_prune_progress: Option<PruneProgress>,
64 #[allow(dead_code)]
65 on_extract_progress: Option<ProgressHandler>,
66 #[allow(dead_code)]
67 on_script_start: Option<ScriptStartHandler>,
68 #[allow(dead_code)]
69 on_script_line: Option<ScriptLineHandler>,
70}
71
72impl NodeMaintainerOptions {
73 pub fn new() -> Self {
75 Self::default()
76 }
77
78 #[cfg(not(target_arch = "wasm32"))]
80 pub fn cache(mut self, cache: impl AsRef<Path>) -> Self {
81 self.nassun_opts = self.nassun_opts.cache(PathBuf::from(cache.as_ref()));
82 self.cache = Some(PathBuf::from(cache.as_ref()));
83 self
84 }
85
86 pub fn concurrency(mut self, concurrency: usize) -> Self {
90 self.concurrency = concurrency;
91 self
92 }
93
94 pub fn locked(mut self, locked: bool) -> Self {
97 self.locked = locked;
98 self
99 }
100
101 pub fn script_concurrency(mut self, concurrency: usize) -> Self {
105 self.script_concurrency = concurrency;
106 self
107 }
108
109 pub fn kdl_lock(mut self, kdl_lock: impl IntoKdl) -> Result<Self, NodeMaintainerError> {
114 let lock = Lockfile::from_kdl(kdl_lock)?;
115 self.kdl_lock = Some(lock);
116 Ok(self)
117 }
118
119 pub fn npm_lock(mut self, npm_lock: impl AsRef<str>) -> Result<Self, NodeMaintainerError> {
124 let lock = Lockfile::from_npm(npm_lock)?;
125 self.npm_lock = Some(lock);
126 Ok(self)
127 }
128
129 pub fn registry(mut self, registry: Url) -> Self {
133 self.nassun_opts = self.nassun_opts.registry(registry);
134 self
135 }
136
137 pub fn scope_registry(mut self, scope: impl AsRef<str>, registry: Url) -> Self {
141 self.nassun_opts = self.nassun_opts.scope_registry(scope, registry);
142 self
143 }
144
145 pub fn basic_auth(
147 mut self,
148 registry: Url,
149 username: impl AsRef<str>,
150 password: Option<impl AsRef<str>>,
151 ) -> Self {
152 let username = username.as_ref();
153 let password = password.map(|p| p.as_ref().to_string());
154 self.nassun_opts = self.nassun_opts.basic_auth(registry, username, password);
155 self
156 }
157
158 pub fn token_auth(mut self, registry: Url, token: impl AsRef<str>) -> Self {
160 self.nassun_opts = self.nassun_opts.token_auth(registry, token.as_ref());
161 self
162 }
163
164 pub fn legacy_auth(mut self, registry: Url, legacy_auth_token: impl AsRef<str>) -> Self {
166 self.nassun_opts = self
167 .nassun_opts
168 .legacy_auth(registry, legacy_auth_token.as_ref());
169 self
170 }
171
172 #[cfg(not(target_arch = "wasm32"))]
174 pub fn root(mut self, path: impl AsRef<Path>) -> Self {
175 self.nassun_opts = self.nassun_opts.base_dir(path.as_ref());
176 self.root = Some(PathBuf::from(path.as_ref()));
177 self
178 }
179
180 pub fn default_tag(mut self, tag: impl AsRef<str>) -> Self {
182 self.nassun_opts = self.nassun_opts.default_tag(tag);
183 self
184 }
185
186 pub fn nassun(mut self, nassun: Nassun) -> Self {
189 self.nassun = Some(nassun);
190 self
191 }
192
193 #[cfg(not(target_arch = "wasm32"))]
200 pub fn prefer_copy(mut self, prefer_copy: bool) -> Self {
201 self.prefer_copy = prefer_copy;
202 self
203 }
204
205 pub fn hoisted(mut self, hoisted: bool) -> Self {
211 self.hoisted = hoisted;
212 self
213 }
214
215 #[cfg(not(target_arch = "wasm32"))]
216 pub fn proxy(mut self, proxy: bool) -> Self {
217 self.nassun_opts = self.nassun_opts.proxy(proxy);
218 self
219 }
220
221 #[cfg(not(target_arch = "wasm32"))]
222 pub fn proxy_url(mut self, proxy_url: impl AsRef<str>) -> Result<Self, NodeMaintainerError> {
223 self.nassun_opts = self.nassun_opts.proxy_url(proxy_url.as_ref())?;
224 Ok(self)
225 }
226
227 #[cfg(not(target_arch = "wasm32"))]
228 pub fn no_proxy_domain(mut self, no_proxy_domain: impl AsRef<str>) -> Self {
229 self.nassun_opts = self.nassun_opts.no_proxy_domain(no_proxy_domain.as_ref());
230 self
231 }
232
233 pub fn on_resolution_added<F>(mut self, f: F) -> Self
234 where
235 F: Fn() + Send + Sync + 'static,
236 {
237 self.on_resolution_added = Some(Arc::new(f));
238 self
239 }
240
241 pub fn on_resolve_progress<F>(mut self, f: F) -> Self
242 where
243 F: Fn(&Package, Duration) + Send + Sync + 'static,
244 {
245 self.on_resolve_progress = Some(Arc::new(f));
246 self
247 }
248
249 #[cfg(not(target_arch = "wasm32"))]
250 pub fn on_prune_progress<F>(mut self, f: F) -> Self
251 where
252 F: Fn(&Path) + Send + Sync + 'static,
253 {
254 self.on_prune_progress = Some(Arc::new(f));
255 self
256 }
257
258 #[cfg(not(target_arch = "wasm32"))]
259 pub fn on_extract_progress<F>(mut self, f: F) -> Self
260 where
261 F: Fn(&Package, Duration) + Send + Sync + 'static,
262 {
263 self.on_extract_progress = Some(Arc::new(f));
264 self
265 }
266
267 #[cfg(not(target_arch = "wasm32"))]
268 pub fn on_script_start<F>(mut self, f: F) -> Self
269 where
270 F: Fn(&Package, &str) + Send + Sync + 'static,
271 {
272 self.on_script_start = Some(Arc::new(f));
273 self
274 }
275
276 #[cfg(not(target_arch = "wasm32"))]
277 pub fn on_script_line<F>(mut self, f: F) -> Self
278 where
279 F: Fn(&str) + Send + Sync + 'static,
280 {
281 self.on_script_line = Some(Arc::new(f));
282 self
283 }
284
285 async fn get_lockfile(&self) -> Result<Option<Lockfile>, NodeMaintainerError> {
286 if let Some(kdl_lock) = &self.kdl_lock {
287 return Ok(Some(kdl_lock.clone()));
288 }
289 if let Some(npm_lock) = &self.npm_lock {
290 return Ok(Some(npm_lock.clone()));
291 }
292 #[cfg(not(target_arch = "wasm32"))]
293 if let Some(root) = &self.root {
294 let kdl_lock = root.join("package-lock.kdl");
295 if kdl_lock.exists() {
296 match async_std::fs::read_to_string(&kdl_lock)
297 .await
298 .io_context(|| format!("Failed to read {}", kdl_lock.display()))
299 .and_then(Lockfile::from_kdl)
300 {
301 Ok(lock) => return Ok(Some(lock)),
302 Err(e) => tracing::debug!("Failed to parse existing package-lock.kdl: {}", e),
303 }
304 }
305 let npm_lock = root.join("package-lock.json");
306 if npm_lock.exists() {
307 match async_std::fs::read_to_string(&npm_lock)
308 .await
309 .io_context(|| format!("Failed to read {}", npm_lock.display()))
310 .and_then(Lockfile::from_npm)
311 {
312 Ok(lock) => return Ok(Some(lock)),
313 Err(e) => tracing::debug!("Failed to parse existing package-lock.json: {}", e),
314 }
315 }
316 let npm_lock = root.join("npm-shrinkwrap.json");
317 if npm_lock.exists() {
318 match async_std::fs::read_to_string(&npm_lock)
319 .await
320 .io_context(|| format!("Failed to read {}", npm_lock.display()))
321 .and_then(Lockfile::from_npm)
322 {
323 Ok(lock) => return Ok(Some(lock)),
324 Err(e) => {
325 tracing::debug!("Failed to parse existing npm-shrinkwrap.json: {}", e)
326 }
327 }
328 }
329 }
330 Ok(None)
331 }
332
333 pub async fn resolve_manifest(
335 self,
336 root: CorgiManifest,
337 ) -> Result<NodeMaintainer, NodeMaintainerError> {
338 let lockfile = self.get_lockfile().await?;
339 let nassun = self.nassun.unwrap_or_else(|| self.nassun_opts.build());
340 let root_pkg = Nassun::dummy_from_manifest(root.clone());
341 let proj_root = self.root.unwrap_or_else(|| PathBuf::from("."));
342 let mut resolver = Resolver {
343 nassun,
344 graph: Default::default(),
345 concurrency: self.concurrency,
346 locked: self.locked,
347 root: &proj_root,
348 actual_tree: None,
349 on_resolution_added: self.on_resolution_added,
350 on_resolve_progress: self.on_resolve_progress,
351 };
352 let node = resolver.graph.inner.add_node(Node::new(
353 UniCase::new("".to_string()),
354 root_pkg,
355 root,
356 true,
357 )?);
358 resolver.graph[node].root = node;
359 let (graph, _actual_tree) = resolver.run_resolver(lockfile).await?;
360 #[cfg(not(target_arch = "wasm32"))]
361 let linker_opts = LinkerOptions {
362 actual_tree: _actual_tree,
363 concurrency: self.concurrency,
364 script_concurrency: self.script_concurrency,
365 cache: self.cache,
366 prefer_copy: self.prefer_copy,
367 root: proj_root,
368 on_prune_progress: self.on_prune_progress,
369 on_extract_progress: self.on_extract_progress,
370 on_script_start: self.on_script_start,
371 on_script_line: self.on_script_line,
372 };
373 let nm = NodeMaintainer {
374 graph,
375 #[cfg(target_arch = "wasm32")]
376 linker: Linker::null(),
377 #[cfg(not(target_arch = "wasm32"))]
378 linker: if self.hoisted {
379 Linker::hoisted(linker_opts)
380 } else {
381 Linker::isolated(linker_opts)
382 },
383 };
384 #[cfg(debug_assertions)]
385 nm.graph.validate()?;
386 Ok(nm)
387 }
388
389 pub async fn resolve_spec(
392 self,
393 root_spec: impl AsRef<str>,
394 ) -> Result<NodeMaintainer, NodeMaintainerError> {
395 let lockfile = self.get_lockfile().await?;
396 let nassun = self.nassun_opts.build();
397 let root_pkg = nassun.resolve(root_spec).await?;
398 let proj_root = self.root.unwrap_or_else(|| PathBuf::from("."));
399 let mut resolver = Resolver {
400 nassun,
401 graph: Default::default(),
402 concurrency: self.concurrency,
403 locked: self.locked,
404 root: &proj_root,
405 actual_tree: None,
406 on_resolution_added: self.on_resolution_added,
407 on_resolve_progress: self.on_resolve_progress,
408 };
409 let corgi = root_pkg.corgi_metadata().await?.manifest;
410 let node = resolver.graph.inner.add_node(Node::new(
411 UniCase::new("".to_string()),
412 root_pkg,
413 corgi,
414 true,
415 )?);
416 resolver.graph[node].root = node;
417 let (graph, _actual_tree) = resolver.run_resolver(lockfile).await?;
418 #[cfg(not(target_arch = "wasm32"))]
419 let linker_opts = LinkerOptions {
420 actual_tree: _actual_tree,
421 concurrency: self.concurrency,
422 script_concurrency: self.script_concurrency,
423 cache: self.cache,
424 prefer_copy: self.prefer_copy,
425 root: proj_root,
426 on_prune_progress: self.on_prune_progress,
427 on_extract_progress: self.on_extract_progress,
428 on_script_start: self.on_script_start,
429 on_script_line: self.on_script_line,
430 };
431 let nm = NodeMaintainer {
432 graph,
433 #[cfg(target_arch = "wasm32")]
434 linker: Linker::null(),
435 #[cfg(not(target_arch = "wasm32"))]
436 linker: if self.hoisted {
437 Linker::hoisted(linker_opts)
438 } else {
439 Linker::isolated(linker_opts)
440 },
441 };
442 #[cfg(debug_assertions)]
443 nm.graph.validate()?;
444 Ok(nm)
445 }
446}
447
448impl Default for NodeMaintainerOptions {
449 fn default() -> Self {
450 NodeMaintainerOptions {
451 nassun_opts: Default::default(),
452 nassun: None,
453 concurrency: DEFAULT_CONCURRENCY,
454 kdl_lock: None,
455 npm_lock: None,
456 locked: false,
457 script_concurrency: DEFAULT_SCRIPT_CONCURRENCY,
458 cache: None,
459 hoisted: false,
460 prefer_copy: false,
461 validate: false,
462 root: None,
463 on_resolution_added: None,
464 on_resolve_progress: None,
465 on_prune_progress: None,
466 on_extract_progress: None,
467 on_script_start: None,
468 on_script_line: None,
469 }
470 }
471}
472
473pub struct NodeMaintainer {
475 pub(crate) graph: Graph,
476 #[allow(dead_code)]
477 linker: Linker,
478}
479
480impl NodeMaintainer {
481 pub fn builder() -> NodeMaintainerOptions {
484 NodeMaintainerOptions::new()
485 }
486
487 #[cfg(not(target_arch = "wasm32"))]
489 pub async fn resolve_manifest(
490 root: CorgiManifest,
491 ) -> Result<NodeMaintainer, NodeMaintainerError> {
492 Self::builder().resolve_manifest(root).await
493 }
494
495 #[cfg(not(target_arch = "wasm32"))]
498 pub async fn resolve_spec(
499 root_spec: impl AsRef<str>,
500 ) -> Result<NodeMaintainer, NodeMaintainerError> {
501 Self::builder().resolve_spec(root_spec).await
502 }
503
504 #[cfg(not(target_arch = "wasm32"))]
506 pub async fn write_lockfile(&self, path: impl AsRef<Path>) -> Result<(), NodeMaintainerError> {
507 let path = path.as_ref();
508 fs::write(path, self.graph.to_kdl()?.to_string())
509 .await
510 .io_context(|| format!("Failed to write lockfile to {}", path.display()))?;
511 Ok(())
512 }
513
514 pub fn to_lockfile(&self) -> Result<crate::Lockfile, NodeMaintainerError> {
516 self.graph.to_lockfile()
517 }
518
519 pub fn to_kdl(&self) -> Result<kdl::KdlDocument, NodeMaintainerError> {
521 self.graph.to_kdl()
522 }
523
524 pub fn package_at_path(&self, path: &Path) -> Option<Package> {
528 self.graph.package_at_path(path)
529 }
530
531 pub fn package_count(&self) -> usize {
533 self.graph.inner.node_count()
534 }
535
536 #[cfg(not(target_arch = "wasm32"))]
540 pub async fn prune(&self) -> Result<usize, NodeMaintainerError> {
541 self.linker.prune(&self.graph).await
542 }
543
544 #[cfg(not(target_arch = "wasm32"))]
549 pub async fn extract(&self) -> Result<usize, NodeMaintainerError> {
550 self.linker.extract(&self.graph).await
551 }
552
553 #[cfg(not(target_arch = "wasm32"))]
556 pub async fn rebuild(&self, ignore_scripts: bool) -> Result<(), NodeMaintainerError> {
557 self.linker.rebuild(&self.graph, ignore_scripts).await
558 }
559}