1use eyre::{OptionExt, Result};
2use itertools::{Either, Itertools};
3use lux_lib::{
4 config::Config,
5 progress::{MultiProgress, Progress, ProgressBar},
6 project::Project,
7 remote_package_db::RemotePackageDB,
8 rockspec::lua_dependency::{self},
9};
10
11use crate::utils::project::{
12 sync_build_dependencies_if_locked, sync_dependencies_if_locked,
13 sync_test_dependencies_if_locked, PackageReqOrGitShorthand,
14};
15
16#[derive(clap::Args)]
17pub struct Add {
18 package_req: Vec<PackageReqOrGitShorthand>,
28
29 #[arg(long, visible_short_alias = 'f')]
31 force: bool,
32
33 #[arg(short, long, alias = "dev", visible_short_aliases = ['d', 'b'])]
36 build: Option<Vec<PackageReqOrGitShorthand>>,
37
38 #[arg(short, long, visible_short_alias = 't')]
40 test: Option<Vec<PackageReqOrGitShorthand>>,
41}
42
43pub async fn add(data: Add, config: Config) -> Result<()> {
44 let mut project = Project::current()?.ok_or_eyre("No project found")?;
45
46 let db = RemotePackageDB::from_config(&config, &Progress::Progress(ProgressBar::new())).await?;
47
48 let progress = MultiProgress::new_arc();
49
50 let (dependencies, git_dependencies): (Vec<_>, Vec<_>) =
51 data.package_req.iter().partition_map(|req| match req {
52 PackageReqOrGitShorthand::PackageReq(req) => Either::Left(req.clone()),
53 PackageReqOrGitShorthand::GitShorthand(url) => Either::Right(url.clone()),
54 });
55
56 if !data.package_req.is_empty() {
57 project
58 .add(lua_dependency::DependencyType::Regular(dependencies), &db)
59 .await?;
60 project
61 .add_git(lua_dependency::LuaDependencyType::Regular(git_dependencies))
62 .await?;
63 sync_dependencies_if_locked(&project, progress.clone(), &config).await?;
64 }
65
66 let build_packages = data.build.unwrap_or_default();
67 if !build_packages.is_empty() {
68 let (dependencies, git_dependencies): (Vec<_>, Vec<_>) =
69 build_packages.iter().partition_map(|req| match req {
70 PackageReqOrGitShorthand::PackageReq(req) => Either::Left(req.clone()),
71 PackageReqOrGitShorthand::GitShorthand(url) => Either::Right(url.clone()),
72 });
73 project
74 .add(lua_dependency::DependencyType::Build(dependencies), &db)
75 .await?;
76 project
77 .add_git(lua_dependency::LuaDependencyType::Build(git_dependencies))
78 .await?;
79 sync_build_dependencies_if_locked(&project, progress.clone(), &config).await?;
80 }
81
82 let test_packages = data.test.unwrap_or_default();
83 if !test_packages.is_empty() {
84 let (dependencies, git_dependencies): (Vec<_>, Vec<_>) =
85 test_packages.iter().partition_map(|req| match req {
86 PackageReqOrGitShorthand::PackageReq(req) => Either::Left(req.clone()),
87 PackageReqOrGitShorthand::GitShorthand(url) => Either::Right(url.clone()),
88 });
89 project
90 .add(lua_dependency::DependencyType::Test(dependencies), &db)
91 .await?;
92 project
93 .add_git(lua_dependency::LuaDependencyType::Test(git_dependencies))
94 .await?;
95 sync_test_dependencies_if_locked(&project, progress.clone(), &config).await?;
96 }
97
98 Ok(())
99}
100
101#[cfg(test)]
102mod test {
103 use assert_fs::{prelude::PathCopy, TempDir};
104 use lux_lib::config::ConfigBuilder;
105 use serial_test::serial;
106
107 use super::*;
108 use std::path::PathBuf;
109
110 #[serial]
111 #[tokio::test]
112 async fn test_add_regular_dependencies() {
113 if std::env::var("LUX_SKIP_IMPURE_TESTS").unwrap_or("0".into()) == "1" {
114 println!("Skipping impure test");
115 return;
116 }
117 let sample_project: PathBuf = "resources/test/sample-project-init/".into();
118 let project_root = TempDir::new().unwrap();
119 project_root.copy_from(&sample_project, &["**"]).unwrap();
120 let cwd = std::env::current_dir().unwrap();
121 std::env::set_current_dir(&project_root).unwrap();
122 let config = ConfigBuilder::new().unwrap().build().unwrap();
123 let args = Add {
124 package_req: vec!["penlight@1.5".parse().unwrap()],
125 force: false,
126 build: Option::None,
127 test: Option::None,
128 };
129 add(args, config.clone()).await.unwrap();
130 let lockfile_path = project_root.join("lux.lock");
131 let lockfile_content =
132 String::from_utf8(tokio::fs::read(&lockfile_path).await.unwrap()).unwrap();
133 assert!(lockfile_content.contains("penlight"));
134 assert!(lockfile_content.contains("luafilesystem")); let args = Add {
137 package_req: vec!["md5".parse().unwrap()],
138 force: false,
139 build: Option::None,
140 test: Option::None,
141 };
142 add(args, config.clone()).await.unwrap();
143 let lockfile_path = project_root.join("lux.lock");
144 let lockfile_content =
145 String::from_utf8(tokio::fs::read(&lockfile_path).await.unwrap()).unwrap();
146 assert!(lockfile_content.contains("penlight"));
147 assert!(lockfile_content.contains("luafilesystem"));
148 assert!(lockfile_content.contains("md5"));
149
150 std::env::set_current_dir(&cwd).unwrap();
151 }
152
153 #[serial]
154 #[tokio::test]
155 async fn test_add_build_dependencies() {
156 if std::env::var("LUX_SKIP_IMPURE_TESTS").unwrap_or("0".into()) == "1" {
157 println!("Skipping impure test");
158 return;
159 }
160 let sample_project: PathBuf = "resources/test/sample-project-init/".into();
161 let project_root = TempDir::new().unwrap();
162 project_root.copy_from(&sample_project, &["**"]).unwrap();
163 let cwd = std::env::current_dir().unwrap();
164 std::env::set_current_dir(&project_root).unwrap();
165 let config = ConfigBuilder::new().unwrap().build().unwrap();
166 let args = Add {
167 package_req: Vec::new(),
168 force: false,
169 build: Option::Some(vec!["penlight@1.5".parse().unwrap()]),
170 test: Option::None,
171 };
172 add(args, config.clone()).await.unwrap();
173 let lockfile_path = project_root.join("lux.lock");
174 let lockfile_content =
175 String::from_utf8(tokio::fs::read(&lockfile_path).await.unwrap()).unwrap();
176 assert!(lockfile_content.contains("penlight"));
177 assert!(lockfile_content.contains("luafilesystem")); let args = Add {
180 package_req: Vec::new(),
181 force: false,
182 build: Option::Some(vec!["md5".parse().unwrap()]),
183 test: Option::None,
184 };
185 add(args, config.clone()).await.unwrap();
186 let lockfile_path = project_root.join("lux.lock");
187 let lockfile_content =
188 String::from_utf8(tokio::fs::read(&lockfile_path).await.unwrap()).unwrap();
189 assert!(lockfile_content.contains("penlight"));
190 assert!(lockfile_content.contains("luafilesystem"));
191 assert!(lockfile_content.contains("md5"));
192
193 std::env::set_current_dir(&cwd).unwrap();
194 }
195
196 #[serial]
197 #[tokio::test]
198 async fn test_add_test_dependencies() {
199 if std::env::var("LUX_SKIP_IMPURE_TESTS").unwrap_or("0".into()) == "1" {
200 println!("Skipping impure test");
201 return;
202 }
203 let sample_project: PathBuf = "resources/test/sample-project-init/".into();
204 let project_root = TempDir::new().unwrap();
205 project_root.copy_from(&sample_project, &["**"]).unwrap();
206 let cwd = std::env::current_dir().unwrap();
207 std::env::set_current_dir(&project_root).unwrap();
208 let config = ConfigBuilder::new().unwrap().build().unwrap();
209 let args = Add {
210 package_req: Vec::new(),
211 force: false,
212 build: Option::None,
213 test: Option::Some(vec!["penlight@1.5".parse().unwrap()]),
214 };
215 add(args, config.clone()).await.unwrap();
216 let lockfile_path = project_root.join("lux.lock");
217 let lockfile_content =
218 String::from_utf8(tokio::fs::read(&lockfile_path).await.unwrap()).unwrap();
219 assert!(lockfile_content.contains("penlight"));
220 assert!(lockfile_content.contains("luafilesystem")); let args = Add {
223 package_req: Vec::new(),
224 force: false,
225 build: Option::None,
226 test: Option::Some(vec!["md5".parse().unwrap()]),
227 };
228 add(args, config.clone()).await.unwrap();
229 let lockfile_path = project_root.join("lux.lock");
230 let lockfile_content =
231 String::from_utf8(tokio::fs::read(&lockfile_path).await.unwrap()).unwrap();
232 assert!(lockfile_content.contains("penlight"));
233 assert!(lockfile_content.contains("luafilesystem"));
234 assert!(lockfile_content.contains("md5"));
235
236 std::env::set_current_dir(&cwd).unwrap();
237 }
238}