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