Skip to main content

create_tauri_app/
template.rs

1// Copyright 2019-2022 Tauri Programme within The Commons Conservancy
2// SPDX-License-Identifier: Apache-2.0
3// SPDX-License-Identifier: MIT
4
5use 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            // remove the first component, which is certainly the template directory they were in before getting embeded into the binary
376            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                // skip manifest
389                CTA_MANIFEST_FILENAME => return Ok(()),
390                // conditional files:
391                // are files that start with a special syntax
392                //          "%(<list of flags separated by `-`>%)<file_name>"
393                // flags are supported package managers, and `v-$versionNumber` (tauri version filter).
394                // example: "%(pnpm-npm-yarn-stable-v1)%package.json"
395                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                    // remove version flags to only keep package managers flags
408                    flags.retain(|e| !version_flags.iter().any(|(_, flag)| e == flag));
409
410                    // this file has a version flag and matches active version.
411                    // if doesn't have any version flag, it should be rendered
412                    if for_version.map(|v| v == tauri_version).unwrap_or(true)
413                        // this file has a package manager flag and matches active package manager.
414                        // if doesn't have any package manager flag, it should be rendered
415                        && (flags.contains(&pkg_manager.to_string().as_str()) || flags.is_empty())
416                    {
417                        name
418                    } else {
419                        // skip writing this file
420                        return Ok(());
421                    }
422                }
423                name => name,
424            };
425
426            // Only modify files that need to use the template engine
427            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        // 1. write base files
445        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        // 2. write template files which can override files from base
457        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        // 3. write extra files specified in the template manifest
469        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}