1use std::{collections::HashMap, fmt::Display, fs, io::Write, path, str::FromStr};
6
7use anyhow::Context;
8use rust_embed::Embed;
9
10use crate::{
11 args::TauriVersion,
12 manifest::Manifest,
13 package_manager::PackageManager,
14 utils::{self, colors::*, lte},
15};
16
17const CTA_MANIFEST_FILENAME: &str = ".manifest";
18
19#[derive(Embed)]
20#[folder = "templates"]
21#[allow(non_camel_case_types)]
22struct EMBEDDED_TEMPLATES;
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25#[non_exhaustive]
26pub enum Flavor {
27 JavaScript,
28 TypeScript,
29}
30
31impl Display for Flavor {
32 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33 match self {
34 Flavor::JavaScript => write!(f, "JavaScript"),
35 Flavor::TypeScript => write!(f, "TypeScript"),
36 }
37 }
38}
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41#[non_exhaustive]
42#[derive(Default)]
43pub enum Template {
44 #[default]
45 Vanilla,
46 VanillaTs,
47 Vue,
48 VueTs,
49 Svelte,
50 SvelteTs,
51 React,
52 ReactTs,
53 Solid,
54 SolidTs,
55 Yew,
56 Leptos,
57 Sycamore,
58 Angular,
59 Preact,
60 PreactTs,
61 Blazor,
62 Dioxus,
63}
64
65impl Display for Template {
66 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67 match self {
68 Template::Vanilla => write!(f, "vanilla"),
69 Template::VanillaTs => write!(f, "vanilla-ts"),
70 Template::Vue => write!(f, "vue"),
71 Template::VueTs => write!(f, "vue-ts"),
72 Template::Svelte => write!(f, "svelte"),
73 Template::SvelteTs => write!(f, "svelte-ts"),
74 Template::React => write!(f, "react"),
75 Template::ReactTs => write!(f, "react-ts"),
76 Template::Solid => write!(f, "solid"),
77 Template::SolidTs => write!(f, "solid-ts"),
78 Template::Yew => write!(f, "yew"),
79 Template::Leptos => write!(f, "leptos"),
80 Template::Sycamore => write!(f, "sycamore"),
81 Template::Angular => write!(f, "angular"),
82 Template::Preact => write!(f, "preact"),
83 Template::PreactTs => write!(f, "preact-ts"),
84 Template::Blazor => write!(f, "blazor"),
85 Template::Dioxus => write!(f, "dioxus"),
86 }
87 }
88}
89
90impl FromStr for Template {
91 type Err = String;
92 fn from_str(s: &str) -> Result<Self, Self::Err> {
93 match s {
94 "vanilla" => Ok(Template::Vanilla),
95 "vanilla-ts" => Ok(Template::VanillaTs),
96 "vue" => Ok(Template::Vue),
97 "vue-ts" => Ok(Template::VueTs),
98 "svelte" => Ok(Template::Svelte),
99 "svelte-ts" => Ok(Template::SvelteTs),
100 "react" => Ok(Template::React),
101 "react-ts" => Ok(Template::ReactTs),
102 "solid" => Ok(Template::Solid),
103 "solid-ts" => Ok(Template::SolidTs),
104 "yew" => Ok(Template::Yew),
105 "leptos" => Ok(Template::Leptos),
106 "sycamore" => Ok(Template::Sycamore),
107 "angular" => Ok(Template::Angular),
108 "preact" => Ok(Template::Preact),
109 "preact-ts" => Ok(Template::PreactTs),
110 "blazor" => Ok(Template::Blazor),
111 "dioxus" => Ok(Template::Dioxus),
112 _ => Err(format!(
113 "{YELLOW}{s}{RESET} is not a valid template. Valid templates are [{}]",
114 Template::ALL
115 .iter()
116 .map(|e| format!("{GREEN}{e}{RESET}"))
117 .collect::<Vec<_>>()
118 .join(", ")
119 )),
120 }
121 }
122}
123
124impl Template {
125 pub const fn select_text<'a>(&self) -> &'a str {
126 match self {
127 Template::Vanilla => "Vanilla",
128 Template::Vue => "Vue - (https://vuejs.org/)",
129 Template::Svelte => "Svelte - (https://svelte.dev/)",
130 Template::React => "React - (https://react.dev/)",
131 Template::Solid => "Solid - (https://solidjs.com/)",
132 Template::Yew => "Yew - (https://yew.rs/)",
133 Template::Leptos => "Leptos - (https://leptos.dev/)",
134 Template::Sycamore => "Sycamore - (https://sycamore.dev/)",
135 Template::Angular => "Angular - (https://angular.dev/)",
136 Template::Preact => "Preact - (https://preactjs.com/)",
137 Template::Blazor => {
138 "Blazor - (https://dotnet.microsoft.com/en-us/apps/aspnet/web-apps/blazor/)"
139 }
140 Template::Dioxus => "Dioxus - (https://dioxuslabs.com/)",
141 _ => unreachable!(),
142 }
143 }
144}
145
146impl Template {
147 pub const ALL: &[Template] = &[
148 Template::Vanilla,
149 Template::VanillaTs,
150 Template::Vue,
151 Template::VueTs,
152 Template::Svelte,
153 Template::SvelteTs,
154 Template::React,
155 Template::ReactTs,
156 Template::Solid,
157 Template::SolidTs,
158 Template::Yew,
159 Template::Leptos,
160 Template::Sycamore,
161 Template::Angular,
162 Template::Preact,
163 Template::PreactTs,
164 Template::Blazor,
165 Template::Dioxus,
166 ];
167
168 pub fn flavors<'b>(&self, pkg_manager: PackageManager) -> Option<&'b [Flavor]> {
169 match self {
170 Template::Vanilla => {
171 if pkg_manager == PackageManager::Cargo {
172 None
173 } else {
174 Some(&[Flavor::TypeScript, Flavor::JavaScript])
175 }
176 }
177 Template::Vue => Some(&[Flavor::TypeScript, Flavor::JavaScript]),
178 Template::Svelte => Some(&[Flavor::TypeScript, Flavor::JavaScript]),
179 Template::React => Some(&[Flavor::TypeScript, Flavor::JavaScript]),
180 Template::Solid => Some(&[Flavor::TypeScript, Flavor::JavaScript]),
181 Template::Preact => Some(&[Flavor::TypeScript, Flavor::JavaScript]),
182 _ => None,
183 }
184 }
185
186 pub fn from_flavor(&self, flavor: Flavor) -> Self {
187 match (self, flavor) {
188 (Template::Vanilla, Flavor::TypeScript) => Template::VanillaTs,
189 (Template::Vue, Flavor::TypeScript) => Template::VueTs,
190 (Template::Svelte, Flavor::TypeScript) => Template::SvelteTs,
191 (Template::React, Flavor::TypeScript) => Template::ReactTs,
192 (Template::Solid, Flavor::TypeScript) => Template::SolidTs,
193 (Template::Preact, Flavor::TypeScript) => Template::PreactTs,
194 _ => *self,
195 }
196 }
197
198 pub fn without_flavor(&self) -> Self {
199 match self {
200 Template::VanillaTs => Template::Vanilla,
201 Template::VueTs => Template::Vue,
202 Template::SvelteTs => Template::Svelte,
203 Template::ReactTs => Template::React,
204 Template::SolidTs => Template::Solid,
205 Template::PreactTs => Template::Preact,
206 _ => *self,
207 }
208 }
209
210 pub const fn possible_package_managers(&self) -> &[PackageManager] {
211 match self {
212 Template::Vanilla => &[
213 PackageManager::Cargo,
214 PackageManager::Pnpm,
215 PackageManager::Yarn,
216 PackageManager::Npm,
217 PackageManager::Bun,
218 ],
219 Template::VanillaTs
220 | Template::Vue
221 | Template::VueTs
222 | Template::Svelte
223 | Template::SvelteTs
224 | Template::React
225 | Template::ReactTs
226 | Template::Solid
227 | Template::SolidTs
228 | Template::Angular
229 | Template::Preact
230 | Template::PreactTs => PackageManager::NODE,
231 Template::Yew | Template::Leptos | Template::Sycamore | Template::Dioxus => {
232 &[PackageManager::Cargo]
233 }
234 Template::Blazor => &[PackageManager::Dotnet],
235 }
236 }
237
238 pub const fn needs_trunk(&self) -> bool {
239 matches!(self, Template::Sycamore | Template::Yew | Template::Leptos)
240 }
241
242 pub const fn needs_tauri_cli(&self) -> bool {
243 matches!(
244 self,
245 Template::Dioxus
246 | Template::Sycamore
247 | Template::Yew
248 | Template::Leptos
249 | Template::Vanilla
250 | Template::Blazor
251 )
252 }
253
254 pub const fn needs_dotnet(&self) -> bool {
255 matches!(self, Template::Blazor)
256 }
257
258 pub const fn needs_dioxus_cli(&self) -> bool {
259 matches!(self, Template::Dioxus)
260 }
261
262 pub const fn needs_wasm32_target(&self) -> bool {
263 matches!(self, Template::Sycamore | Template::Yew | Template::Leptos)
264 }
265
266 pub fn render(
267 &self,
268 target_dir: &path::Path,
269 pkg_manager: PackageManager,
270 project_name: &str,
271 package_name: &str,
272 identifier: &str,
273 tauri_version: TauriVersion,
274 ) -> anyhow::Result<()> {
275 let manifest_bytes =
276 EMBEDDED_TEMPLATES::get(&format!("template-{self}/{CTA_MANIFEST_FILENAME}"))
277 .with_context(|| "Failed to get manifest bytes")?
278 .data
279 .to_vec();
280 let manifest_str = String::from_utf8(manifest_bytes)?;
281 let manifest = Manifest::parse(&manifest_str)?;
282
283 let lib_name = format!("{}_lib", package_name.replace('-', "_"));
284 let project_name_pascal_case = utils::to_pascal_case(project_name);
285
286 let versions = TauriVersion::all()
287 .iter()
288 .map(|&v| {
289 (
290 format!("v{v}",),
291 match v == tauri_version {
292 true => "true",
293 false => "false",
294 },
295 )
296 })
297 .collect::<Vec<_>>();
298
299 let styles = String::from_utf8(
300 EMBEDDED_TEMPLATES::get("_assets_/styles.css")
301 .unwrap()
302 .data
303 .to_vec(),
304 )?;
305
306 let mut manifest_template_data: HashMap<&str, &str> = [
307 ("pkg_manager_run_command", pkg_manager.run_cmd()),
308 ("lib_name", &lib_name),
309 ("package_name", package_name),
310 ("project_name", project_name),
311 ("identifier", identifier),
312 ("project_name_pascal_case", &project_name_pascal_case),
313 ]
314 .into();
315
316 for (version, enabled) in &versions {
317 manifest_template_data.insert(version, enabled);
318 }
319
320 let mut template_data: HashMap<&str, String> = [
321 ("project_name", project_name.to_string()),
322 (
323 "project_name_pascal_case",
324 project_name_pascal_case.to_string(),
325 ),
326 ("package_name", package_name.to_string()),
327 ("identifier", identifier.to_string()),
328 (
329 "before_dev_command",
330 lte::render(
331 manifest.before_dev_command.unwrap_or_default(),
332 &manifest_template_data,
333 )?,
334 ),
335 (
336 "before_build_command",
337 lte::render(
338 manifest.before_build_command.unwrap_or_default(),
339 &manifest_template_data,
340 )?,
341 ),
342 (
343 "dev_url",
344 lte::render(
345 manifest.dev_url.unwrap_or_default(),
346 &manifest_template_data,
347 )?,
348 ),
349 (
350 "frontend_dist",
351 lte::render(
352 manifest.frontend_dist.unwrap_or_default(),
353 &manifest_template_data,
354 )?,
355 ),
356 (
357 "with_global_tauri",
358 manifest.with_global_tauri.unwrap_or_default().to_string(),
359 ),
360 ("lib_name", lib_name),
361 ("styles", styles),
362 ]
363 .into();
364
365 for (version, enabled) in &versions {
366 template_data.insert(version.as_str(), enabled.to_string());
367 }
368
369 let version_flags = TauriVersion::all()
370 .iter()
371 .map(|&v| (v, format!("v{v}")))
372 .collect::<Vec<_>>();
373
374 let write_file = |file: &str, template_data| -> anyhow::Result<()> {
375 let p = path::PathBuf::from(file)
377 .components()
378 .skip(1)
379 .collect::<Vec<_>>()
380 .iter()
381 .collect::<path::PathBuf>();
382
383 let p = target_dir.join(p);
384 let file_name = p.file_name().unwrap().to_string_lossy();
385
386 let file_name = match &*file_name {
387 "_gitignore" => ".gitignore",
388 CTA_MANIFEST_FILENAME => return Ok(()),
390 name if name.starts_with("%(") && name[1..].contains(")%") => {
396 let mut s = name.strip_prefix("%(").unwrap().split(")%");
397 let (mut flags, name) = (
398 s.next().unwrap().split('-').collect::<Vec<_>>(),
399 s.next().unwrap(),
400 );
401
402 let for_version = version_flags
403 .iter()
404 .find(|(_, flag)| flags.contains(&flag.as_str()))
405 .map(|(v, _)| *v);
406
407 flags.retain(|e| !version_flags.iter().any(|(_, flag)| e == flag));
409
410 if for_version.map(|v| v == tauri_version).unwrap_or(true)
413 && (flags.contains(&pkg_manager.to_string().as_str()) || flags.is_empty())
416 {
417 name
418 } else {
419 return Ok(());
421 }
422 }
423 name => name,
424 };
425
426 let (file_data, file_name) = if let Some(new_name) = file_name.strip_suffix(".lte") {
428 let data = EMBEDDED_TEMPLATES::get(file).unwrap().data.to_vec();
429 let data = lte::render(data, template_data)?.into_bytes();
430 (data, new_name)
431 } else {
432 let data = EMBEDDED_TEMPLATES::get(file).unwrap().data.to_vec();
433 (data, file_name)
434 };
435
436 let file_name = lte::render(file_name, template_data)?;
437
438 let parent = p.parent().unwrap();
439 fs::create_dir_all(parent)?;
440 fs::write(parent.join(file_name), file_data)?;
441 Ok(())
442 };
443
444 for file in EMBEDDED_TEMPLATES::iter().filter(|e| {
446 path::PathBuf::from(e.to_string())
447 .components()
448 .next()
449 .unwrap()
450 .as_os_str()
451 == "_base_"
452 }) {
453 write_file(&file, &template_data)?;
454 }
455
456 for file in EMBEDDED_TEMPLATES::iter().filter(|e| {
458 path::PathBuf::from(e.to_string())
459 .components()
460 .next()
461 .unwrap()
462 .as_os_str()
463 == path::PathBuf::from(format!("template-{self}"))
464 }) {
465 write_file(&file, &template_data)?;
466 }
467
468 for (src, dest) in manifest.files {
470 let data = EMBEDDED_TEMPLATES::get(&format!("_assets_/{src}"))
471 .with_context(|| format!("Failed to get asset file bytes: {src}"))?
472 .data;
473 let dest = target_dir.join(dest);
474 let parent = dest.parent().unwrap();
475 fs::create_dir_all(parent)?;
476 let mut file = fs::OpenOptions::new()
477 .append(true)
478 .create(true)
479 .open(dest)?;
480 file.write_all(&data)?;
481 }
482
483 Ok(())
484 }
485}