prest_npm/
lib.rs

1mod cache;
2mod config;
3mod npm;
4mod package;
5mod plan;
6mod progress;
7mod resolve;
8mod scoped_path;
9mod util;
10
11use color_eyre::eyre::Result;
12use color_eyre::owo_colors::OwoColorize;
13use compact_str::ToCompactString;
14use itertools::Itertools;
15use package::PackageMetadata;
16use plan::tree_size;
17use progress::{log_progress, log_verbose};
18use resolve::Lockfile;
19use std::collections::HashMap;
20use std::time::Instant;
21use tokio::fs::create_dir_all;
22use tokio::fs::read_to_string;
23use util::{read_package, write_json};
24
25use crate::progress::log_warning;
26use crate::util::load_graph_from_lockfile;
27use crate::{
28    plan::{execute_plan, Plan},
29    progress::PROGRESS_BAR,
30};
31
32// Default paths for development mode
33pub const STORE_PATH: &str = "./target/.cotton/store";
34pub const NM_COTTON_PATH: &str = "./node_modules/.cotton";
35pub const NM_COTTON_PLAN_PATH: &str = "./node_modules/.cotton/plan.json";
36
37// Configuration for custom paths
38pub struct NpmConfig {
39    pub base_path: String,
40    pub store_path: String,
41    pub nm_cotton_path: String,
42    pub nm_cotton_plan_path: String,
43}
44
45impl Default for NpmConfig {
46    fn default() -> Self {
47        Self {
48            base_path: ".".to_string(),
49            store_path: STORE_PATH.to_string(),
50            nm_cotton_path: NM_COTTON_PATH.to_string(),
51            nm_cotton_plan_path: NM_COTTON_PLAN_PATH.to_string(),
52        }
53    }
54}
55
56impl NpmConfig {
57    pub fn with_base_path(base_path: &str) -> Self {
58        Self {
59            base_path: base_path.to_string(),
60            store_path: format!("{}/target/.cotton/store", base_path),
61            nm_cotton_path: format!("{}/node_modules/.cotton", base_path),
62            nm_cotton_plan_path: format!("{}/node_modules/.cotton/plan.json", base_path),
63        }
64    }
65}
66
67#[tokio::main]
68pub async fn run() -> Result<HashMap<String, String>> {
69    run_with_config(NpmConfig::default()).await
70}
71
72pub async fn run_with_path(base_path: &str) -> Result<HashMap<String, String>> {
73    run_with_config(NpmConfig::with_base_path(base_path)).await
74}
75
76async fn run_with_config(config: NpmConfig) -> Result<HashMap<String, String>> {
77    let package = read_package().await?;
78
79    init_storage(&config).await?;
80
81    let start = Instant::now();
82
83    let plan = prepare_plan(&package, &config).await?;
84    let size = tree_size(&plan.trees);
85
86    if matches!(verify_installation(&package, &plan, &config).await, Ok(true)) {
87        log_verbose("Packages already installed")
88    } else {
89        log_warning("Executing plan");
90        execute_plan(plan.clone()).await?;
91
92        PROGRESS_BAR.suspend(|| {
93            if size > 0 {
94                println!(
95                    "Installed {} packages in {}ms",
96                    size.yellow(),
97                    start.elapsed().as_millis().yellow()
98                )
99            }
100        });
101        write_json(&config.nm_cotton_plan_path, &plan).await?;
102    }
103
104    PROGRESS_BAR.finish_and_clear();
105
106    Ok(package.exports.to_string_map())
107}
108
109async fn prepare_plan(package: &PackageMetadata, config: &NpmConfig) -> Result<Plan> {
110    log_progress("Preparing");
111
112    let mut graph = load_graph_from_lockfile().await;
113
114    graph.append(package.iter_all(), true).await?;
115    let lockfile_path = format!("{}/cotton.lock", config.base_path);
116    write_json(&lockfile_path, Lockfile::new(graph.clone())).await?;
117
118    log_progress("Retrieved dependency graph");
119
120    let trees = graph.build_trees(&package.iter_all().collect_vec())?;
121    log_progress(&format!("Fetched {} root deps", trees.len().yellow()));
122
123    let plan = Plan::new(
124        trees
125            .iter()
126            .map(|x| (x.root.name.to_compact_string(), x.clone()))
127            .collect(),
128    );
129
130    log_progress(&format!(
131        "Planned {} dependencies",
132        plan.trees.len().yellow()
133    ));
134
135    Ok(plan)
136}
137
138async fn read_plan(path: &str) -> Result<Plan> {
139    let plan = read_to_string(path).await?;
140    Ok(serde_json::from_str(&plan)?)
141}
142
143async fn verify_installation(package: &PackageMetadata, plan: &Plan, config: &NpmConfig) -> Result<bool> {
144    let installed = read_plan(&config.nm_cotton_plan_path).await?;
145
146    if &installed != plan {
147        return Ok(false);
148    }
149
150    Ok(installed.satisfies(package))
151}
152
153async fn init_storage(config: &NpmConfig) -> Result<()> {
154    create_dir_all(&config.store_path).await?;
155    create_dir_all(&config.nm_cotton_path).await?;
156    Ok(())
157}