generate_from_path/
lib.rs1use crate::template::create_liquid_object;
31use crate::template_variables::{
32 CrateName, ProjectDir, ProjectName, ProjectNameInput, set_project_name_variables,
33};
34use anyhow::bail;
35use liquid::ParserBuilder;
36use std::collections::HashMap;
37use std::path::{Path, PathBuf};
38use tempfile::TempDir;
39use tracing::info;
40
41mod filenames;
42mod ignore_me;
43mod progressbar;
44mod template;
45mod template_variables;
46
47#[derive(Debug)]
48pub struct GenerateArgs {
49 pub name: String,
50 pub template_dir: PathBuf,
51 pub destination: PathBuf,
52 pub define: HashMap<String, liquid_core::Value>,
53 pub ignore: Option<Vec<String>>,
54 pub overwrite: bool,
55 pub verbose: bool,
56}
57
58pub fn generate(args: GenerateArgs) -> Result<PathBuf, anyhow::Error> {
59 let template_dir = get_source_template_into_temp(&args.template_dir)?;
60 let project_dir = expand_template(template_dir.path(), &args)?;
61
62 copy_expanded_template(template_dir.path(), &project_dir, &args)
63}
64
65fn expand_template(template_dir: &Path, args: &GenerateArgs) -> anyhow::Result<PathBuf> {
66 let mut liquid_object = create_liquid_object(args)?;
67
68 let project_name_input = ProjectNameInput::from(&liquid_object);
69 let destination = ProjectDir::try_from(args)?;
70 let project_name = ProjectName::from(&project_name_input);
71 let crate_name = CrateName::from(&project_name_input);
72 set_project_name_variables(&mut liquid_object, &destination, &project_name, &crate_name)?;
73
74 info!("Destination: {destination}");
75 info!("project-name: {project_name}");
76 info!("Generating template");
77
78 for (key, value) in &args.define {
79 liquid_object.insert(key.into(), value.to_owned());
80 }
81
82 ignore_me::remove_unneeded_files(template_dir, &args.ignore, args.verbose)?;
83 let mut pbar = progressbar::new();
84 let liquid_engine = ParserBuilder::with_stdlib().build()?;
85
86 template::walk_dir(
87 template_dir,
88 &mut liquid_object,
89 &liquid_engine,
90 &mut pbar,
91 args.verbose,
92 )?;
93 Ok(destination.as_ref().to_owned())
94}
95
96fn copy_expanded_template(
97 template_dir: &Path,
98 project_dir: &Path,
99 args: &GenerateArgs,
100) -> anyhow::Result<PathBuf> {
101 info!("Moving generated files into: {}", project_dir.display());
102 copy_dir_all(template_dir, project_dir, args.overwrite)?;
103 info!("Initializing a fresh Git repository");
104 git_init(project_dir)?;
105 info!("Done! New project created in {}", project_dir.display());
106 Ok(project_dir.to_owned())
107}
108
109fn git_init(project_dir: &Path) -> anyhow::Result<()> {
112 let output = std::process::Command::new("git")
113 .arg("init")
114 .arg("-b")
115 .arg("main")
116 .arg(project_dir)
117 .output()?;
118 if !output.status.success() {
119 bail!(
120 "Failed to initialize git repository at {}: {}",
121 project_dir.display(),
122 String::from_utf8_lossy(&output.stderr)
123 );
124 }
125 Ok(())
126}
127
128fn get_source_template_into_temp(template_dir: &Path) -> anyhow::Result<TempDir> {
129 let temp_dir = tempfile::Builder::new().prefix("pavex-new").tempdir()?;
130 copy_dir_all(template_dir, temp_dir.path(), false)?;
131 Ok(temp_dir)
132}
133
134pub(crate) fn copy_dir_all(
135 src: impl AsRef<Path>,
136 dst: impl AsRef<Path>,
137 overwrite: bool,
138) -> anyhow::Result<()> {
139 fn check_dir_all(
140 src: impl AsRef<Path>,
141 dst: impl AsRef<Path>,
142 overwrite: bool,
143 ) -> anyhow::Result<()> {
144 if !dst.as_ref().exists() {
145 return Ok(());
146 }
147
148 for src_entry in fs_err::read_dir(src.as_ref())? {
149 let src_entry = src_entry?;
150 let filename = src_entry.file_name().to_string_lossy().to_string();
151 let entry_type = src_entry.file_type()?;
152
153 if entry_type.is_dir() {
154 if filename == ".git" {
155 continue;
156 }
157 let dst_path = dst.as_ref().join(filename);
158 check_dir_all(src_entry.path(), dst_path, overwrite)?;
159 } else if entry_type.is_file() {
160 let filename = filename.strip_suffix(".liquid").unwrap_or(&filename);
161 let dst_path = dst.as_ref().join(filename);
162 match (dst_path.exists(), overwrite) {
163 (true, false) => {
164 bail!("File already exists: {}", dst_path.display())
165 }
166 (true, true) => {
167 tracing::warn!("Overwriting file: {}", dst_path.display());
168 }
169 _ => {}
170 };
171 } else {
172 bail!("Symbolic links not supported")
173 }
174 }
175 Ok(())
176 }
177 fn copy_all(
178 src: impl AsRef<Path>,
179 dst: impl AsRef<Path>,
180 overwrite: bool,
181 ) -> anyhow::Result<()> {
182 fs_err::create_dir_all(&dst)?;
183 for src_entry in fs_err::read_dir(src.as_ref())? {
184 let src_entry = src_entry?;
185 let filename = src_entry.file_name().to_string_lossy().to_string();
186 let entry_type = src_entry.file_type()?;
187 if entry_type.is_dir() {
188 let dst_path = dst.as_ref().join(filename);
189 if ".git" == src_entry.file_name() {
190 continue;
191 }
192 copy_dir_all(src_entry.path(), dst_path, overwrite)?;
193 } else if entry_type.is_file() {
194 let filename = filename.strip_suffix(".liquid").unwrap_or(&filename);
195 let dst_path = dst.as_ref().join(filename);
196 if dst_path.exists() && overwrite {
197 fs_err::remove_file(&dst_path)?;
198 }
199 fs_err::copy(src_entry.path(), dst_path)?;
200 }
201 }
202 Ok(())
203 }
204
205 check_dir_all(&src, &dst, overwrite)?;
206 copy_all(src, dst, overwrite)
207}