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