create.rs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415
  1. use super::*;
  2. use crate::TraceSrc;
  3. use cargo_generate::{GenerateArgs, TemplatePath};
  4. use std::path::Path;
  5. pub(crate) static DEFAULT_TEMPLATE: &str = "gh:dioxuslabs/dioxus-template";
  6. #[derive(Clone, Debug, Default, Deserialize, Parser)]
  7. #[clap(name = "new")]
  8. pub struct Create {
  9. /// Create a new Dioxus project at PATH
  10. path: PathBuf,
  11. /// Project name. Defaults to directory name
  12. #[arg(short, long)]
  13. name: Option<String>,
  14. /// Template path
  15. #[clap(short, long)]
  16. template: Option<String>,
  17. /// Branch to select when using `template` from a git repository.
  18. /// Mutually exclusive with: `--revision`, `--tag`.
  19. #[clap(long, conflicts_with_all(["revision", "tag"]))]
  20. branch: Option<String>,
  21. /// A commit hash to select when using `template` from a git repository.
  22. /// Mutually exclusive with: `--branch`, `--tag`.
  23. #[clap(long, conflicts_with_all(["branch", "tag"]))]
  24. revision: Option<String>,
  25. /// Tag to select when using `template` from a git repository.
  26. /// Mutually exclusive with: `--branch`, `--revision`.
  27. #[clap(long, conflicts_with_all(["branch", "revision"]))]
  28. tag: Option<String>,
  29. /// Specify a sub-template within the template repository to be used as the actual template
  30. #[clap(long)]
  31. subtemplate: Option<String>,
  32. /// Pass <option>=<value> for the used template (e.g., `foo=bar`)
  33. #[clap(short, long)]
  34. option: Vec<String>,
  35. /// Skip user interaction by using the default values for the used template.
  36. /// Default values can be overridden with `--option`
  37. #[clap(short, long)]
  38. yes: bool,
  39. }
  40. impl Create {
  41. pub fn create(mut self) -> Result<StructuredOutput> {
  42. // Project name defaults to directory name.
  43. if self.name.is_none() {
  44. self.name = Some(create::name_from_path(&self.path)?);
  45. }
  46. // If no template is specified, use the default one and set the branch to the latest release.
  47. resolve_template_and_branch(&mut self.template, &mut self.branch);
  48. let args = GenerateArgs {
  49. define: self.option,
  50. destination: Some(self.path),
  51. // NOTE: destination without init means base_dir + name, with —
  52. // means dest_dir. So use `init: true` and always handle
  53. // the dest_dir manually and carefully.
  54. // Cargo never adds name to the path. Name is solely for project name.
  55. // https://github.com/cargo-generate/cargo-generate/issues/1250
  56. init: true,
  57. name: self.name,
  58. silent: self.yes,
  59. template_path: TemplatePath {
  60. auto_path: self.template,
  61. branch: self.branch,
  62. revision: self.revision,
  63. subfolder: self.subtemplate,
  64. tag: self.tag,
  65. ..Default::default()
  66. },
  67. ..Default::default()
  68. };
  69. restore_cursor_on_sigint();
  70. let path = cargo_generate::generate(args)?;
  71. post_create(&path)?;
  72. Ok(StructuredOutput::Success)
  73. }
  74. }
  75. /// If no template is specified, use the default one and set the branch to the latest release.
  76. ///
  77. /// Allows us to version templates under the v0.5/v0.6 scheme on the templates repo.
  78. pub(crate) fn resolve_template_and_branch(
  79. template: &mut Option<String>,
  80. branch: &mut Option<String>,
  81. ) {
  82. if template.is_none() {
  83. use crate::dx_build_info::{PKG_VERSION_MAJOR, PKG_VERSION_MINOR};
  84. *template = Some(DEFAULT_TEMPLATE.to_string());
  85. if branch.is_none() {
  86. *branch = Some(format!("v{PKG_VERSION_MAJOR}.{PKG_VERSION_MINOR}"));
  87. }
  88. };
  89. }
  90. /// Prevent hidden cursor if Ctrl+C is pressed when interacting
  91. /// with cargo-generate's prompts.
  92. ///
  93. /// See https://github.com/DioxusLabs/dioxus/pull/2603.
  94. pub(crate) fn restore_cursor_on_sigint() {
  95. ctrlc::set_handler(move || {
  96. if let Err(err) = console::Term::stdout().show_cursor() {
  97. eprintln!("Error showing the cursor again: {err}");
  98. }
  99. std::process::exit(1); // Ideally should mimic the INT signal.
  100. })
  101. .expect("ctrlc::set_handler");
  102. }
  103. /// Extracts the last directory name from the `path`.
  104. pub(crate) fn name_from_path(path: &Path) -> Result<String> {
  105. use path_absolutize::Absolutize;
  106. Ok(path
  107. .absolutize()?
  108. .to_path_buf()
  109. .file_name()
  110. .ok_or("Current path does not include directory name".to_string())?
  111. .to_str()
  112. .ok_or("Current directory name is not a valid UTF-8 string".to_string())?
  113. .to_string())
  114. }
  115. /// Post-creation actions for newly setup crates.
  116. pub(crate) fn post_create(path: &Path) -> Result<()> {
  117. let parent_dir = path.parent();
  118. let metadata = if parent_dir.is_none() {
  119. None
  120. } else {
  121. match cargo_metadata::MetadataCommand::new()
  122. .current_dir(parent_dir.unwrap())
  123. .exec()
  124. {
  125. Ok(v) => Some(v),
  126. // Only 1 error means that CWD isn't a cargo project.
  127. Err(cargo_metadata::Error::CargoMetadata { .. }) => None,
  128. Err(err) => {
  129. return Err(Error::Other(anyhow::anyhow!(
  130. "Couldn't retrieve cargo metadata: {:?}",
  131. err
  132. )));
  133. }
  134. }
  135. };
  136. // 1. Add the new project to the workspace, if it exists.
  137. // This must be executed first in order to run `cargo fmt` on the new project.
  138. metadata.and_then(|metadata| {
  139. let cargo_toml_path = &metadata.workspace_root.join("Cargo.toml");
  140. let cargo_toml_str = std::fs::read_to_string(cargo_toml_path).ok()?;
  141. let relative_path = path.strip_prefix(metadata.workspace_root).ok()?;
  142. let mut cargo_toml: toml_edit::DocumentMut = cargo_toml_str.parse().ok()?;
  143. cargo_toml
  144. .get_mut("workspace")?
  145. .get_mut("members")?
  146. .as_array_mut()?
  147. .push(relative_path.display().to_string());
  148. std::fs::write(cargo_toml_path, cargo_toml.to_string()).ok()
  149. });
  150. // 2. Run `cargo fmt` on the new project.
  151. let mut cmd = Command::new("cargo");
  152. let cmd = cmd.arg("fmt").current_dir(path);
  153. let output = cmd.output().expect("failed to execute process");
  154. if !output.status.success() {
  155. tracing::error!(dx_src = ?TraceSrc::Dev, "cargo fmt failed");
  156. tracing::error!(dx_src = ?TraceSrc::Build, "stdout: {}", String::from_utf8_lossy(&output.stdout));
  157. tracing::error!(dx_src = ?TraceSrc::Build, "stderr: {}", String::from_utf8_lossy(&output.stderr));
  158. }
  159. // 3. Format the `Cargo.toml` and `Dioxus.toml` files.
  160. let toml_paths = [path.join("Cargo.toml"), path.join("Dioxus.toml")];
  161. for toml_path in &toml_paths {
  162. let toml = std::fs::read_to_string(toml_path)?;
  163. let mut toml = toml.parse::<toml_edit::DocumentMut>().map_err(|e| {
  164. anyhow::anyhow!(
  165. "failed to parse toml at {}: {}",
  166. toml_path.display(),
  167. e.to_string()
  168. )
  169. })?;
  170. toml.as_table_mut().fmt();
  171. let as_string = toml.to_string();
  172. let new_string = remove_triple_newlines(&as_string);
  173. let mut file = std::fs::File::create(toml_path)?;
  174. file.write_all(new_string.as_bytes())?;
  175. }
  176. // 4. Remove any triple newlines from the readme.
  177. let readme_path = path.join("README.md");
  178. let readme = std::fs::read_to_string(&readme_path)?;
  179. let new_readme = remove_triple_newlines(&readme);
  180. let mut file = std::fs::File::create(readme_path)?;
  181. file.write_all(new_readme.as_bytes())?;
  182. tracing::info!(dx_src = ?TraceSrc::Dev, "Generated project at {}", path.display());
  183. Ok(())
  184. }
  185. fn remove_triple_newlines(string: &str) -> String {
  186. let mut new_string = String::new();
  187. for char in string.chars() {
  188. if char == '\n' && new_string.ends_with("\n\n") {
  189. continue;
  190. }
  191. new_string.push(char);
  192. }
  193. new_string
  194. }
  195. // todo: re-enable these tests with better parallelization
  196. //
  197. // #[cfg(test)]
  198. // pub(crate) mod tests {
  199. // use escargot::{CargoBuild, CargoRun};
  200. // use once_cell::sync::Lazy;
  201. // use std::fs::{create_dir_all, read_to_string};
  202. // use std::path::{Path, PathBuf};
  203. // use std::process::Command;
  204. // use tempfile::tempdir;
  205. // use toml::Value;
  206. // static BINARY: Lazy<CargoRun> = Lazy::new(|| {
  207. // CargoBuild::new()
  208. // .bin(env!("CARGO_BIN_NAME"))
  209. // .current_release()
  210. // .run()
  211. // .expect("Couldn't build the binary for tests.")
  212. // });
  213. // // Note: tests below (at least 6 of them) were written to mainly test
  214. // // correctness of project's directory and its name, because previously it
  215. // // was broken and tests bring a peace of mind. And also so that I don't have
  216. // // to run my local hand-made tests every time.
  217. // pub(crate) type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
  218. // pub(crate) fn subcommand(name: &str) -> Command {
  219. // let mut command = BINARY.command();
  220. // command.arg(name).arg("--yes"); // Skip any questions by choosing default answers.
  221. // command
  222. // }
  223. // pub(crate) fn get_cargo_toml_path(project_path: &Path) -> PathBuf {
  224. // project_path.join("Cargo.toml")
  225. // }
  226. // pub(crate) fn get_project_name(cargo_toml_path: &Path) -> Result<String> {
  227. // Ok(toml::from_str::<Value>(&read_to_string(cargo_toml_path)?)?
  228. // .get("package")
  229. // .unwrap()
  230. // .get("name")
  231. // .unwrap()
  232. // .as_str()
  233. // .unwrap()
  234. // .to_string())
  235. // }
  236. // fn subcommand_new() -> Command {
  237. // subcommand("new")
  238. // }
  239. // #[test]
  240. // fn test_subcommand_new_with_dot_path() -> Result<()> {
  241. // let project_dir = "dir";
  242. // let project_name = project_dir;
  243. // let temp_dir = tempdir()?;
  244. // // Make current dir's name deterministic.
  245. // let current_dir = temp_dir.path().join(project_dir);
  246. // create_dir_all(&current_dir)?;
  247. // let project_path = &current_dir;
  248. // assert!(project_path.exists());
  249. // assert!(subcommand_new()
  250. // .arg(".")
  251. // .current_dir(&current_dir)
  252. // .status()
  253. // .is_ok());
  254. // let cargo_toml_path = get_cargo_toml_path(project_path);
  255. // assert!(cargo_toml_path.exists());
  256. // assert_eq!(get_project_name(&cargo_toml_path)?, project_name);
  257. // Ok(())
  258. // }
  259. // #[test]
  260. // fn test_subcommand_new_with_1_dir_path() -> Result<()> {
  261. // let project_dir = "dir";
  262. // let project_name = project_dir;
  263. // let current_dir = tempdir()?;
  264. // assert!(subcommand_new()
  265. // .arg(project_dir)
  266. // .current_dir(&current_dir)
  267. // .status()
  268. // .is_ok());
  269. // let project_path = current_dir.path().join(project_dir);
  270. // let cargo_toml_path = get_cargo_toml_path(&project_path);
  271. // assert!(project_path.exists());
  272. // assert!(cargo_toml_path.exists());
  273. // assert_eq!(get_project_name(&cargo_toml_path)?, project_name);
  274. // Ok(())
  275. // }
  276. // #[test]
  277. // fn test_subcommand_new_with_2_dir_path() -> Result<()> {
  278. // let project_dir = "a/b";
  279. // let project_name = "b";
  280. // let current_dir = tempdir()?;
  281. // assert!(subcommand_new()
  282. // .arg(project_dir)
  283. // .current_dir(&current_dir)
  284. // .status()
  285. // .is_ok());
  286. // let project_path = current_dir.path().join(project_dir);
  287. // let cargo_toml_path = get_cargo_toml_path(&project_path);
  288. // assert!(project_path.exists());
  289. // assert!(cargo_toml_path.exists());
  290. // assert_eq!(get_project_name(&cargo_toml_path)?, project_name);
  291. // Ok(())
  292. // }
  293. // #[test]
  294. // fn test_subcommand_new_with_dot_path_and_custom_name() -> Result<()> {
  295. // let project_dir = "dir";
  296. // let project_name = "project";
  297. // let temp_dir = tempdir()?;
  298. // // Make current dir's name deterministic.
  299. // let current_dir = temp_dir.path().join(project_dir);
  300. // create_dir_all(&current_dir)?;
  301. // let project_path = &current_dir;
  302. // assert!(project_path.exists());
  303. // assert!(subcommand_new()
  304. // .arg("--name")
  305. // .arg(project_name)
  306. // .arg(".")
  307. // .current_dir(&current_dir)
  308. // .status()
  309. // .is_ok());
  310. // let cargo_toml_path = get_cargo_toml_path(project_path);
  311. // assert!(cargo_toml_path.exists());
  312. // assert_eq!(get_project_name(&cargo_toml_path)?, project_name);
  313. // Ok(())
  314. // }
  315. // #[test]
  316. // fn test_subcommand_new_with_1_dir_path_and_custom_name() -> Result<()> {
  317. // let project_dir = "dir";
  318. // let project_name = "project";
  319. // let current_dir = tempdir()?;
  320. // assert!(subcommand_new()
  321. // .arg(project_dir)
  322. // .arg("--name")
  323. // .arg(project_name)
  324. // .current_dir(&current_dir)
  325. // .status()
  326. // .is_ok());
  327. // let project_path = current_dir.path().join(project_dir);
  328. // let cargo_toml_path = get_cargo_toml_path(&project_path);
  329. // assert!(project_path.exists());
  330. // assert!(cargo_toml_path.exists());
  331. // assert_eq!(get_project_name(&cargo_toml_path)?, project_name);
  332. // Ok(())
  333. // }
  334. // #[test]
  335. // fn test_subcommand_new_with_2_dir_path_and_custom_name() -> Result<()> {
  336. // let project_dir = "a/b";
  337. // let project_name = "project";
  338. // let current_dir = tempdir()?;
  339. // assert!(subcommand_new()
  340. // .arg(project_dir)
  341. // .arg("--name")
  342. // .arg(project_name)
  343. // .current_dir(&current_dir)
  344. // .status()
  345. // .is_ok());
  346. // let project_path = current_dir.path().join(project_dir);
  347. // let cargo_toml_path = get_cargo_toml_path(&project_path);
  348. // assert!(project_path.exists());
  349. // assert!(cargo_toml_path.exists());
  350. // assert_eq!(get_project_name(&cargo_toml_path)?, project_name);
  351. // Ok(())
  352. // }
  353. // }