Objective of this Article

This article will guide you through using Rust and the clap library to create a scaffolding tool for generating
front-end Vite projects.

Git Repository

This article is adapted from my open-source project rust-init-cli.

Step One: Initializing the Project

Installation

1
2
3
4
cargo init rust-vite-cli
cd rust-vite-cli
# If you are using VSCode, you can use this command to open it.
code .

Dependency Installation

1
2
3
cargo add clap
# or
cargo add clap -F derive

Step Two: Using Clap to Read the Command Line

Args Struct

Create a struct to receive command line arguments.

1
2
3
4
5
6
#[derive(Parser)]
#[command()]
struct Args {
#[command(subcommand)]
command: Option<Commands>,
}

Regarding subcommands, they can generally be used with types such as structs and enumerations.

1
2
3
4
5
6
7
8
9
10
11
/// Comments found in the source code.
/// Parse a sub-command into a user-defined enum.
///
/// Implementing this trait lets a parent container delegate subcommand behavior to `Self`.
/// with:
/// - `#[command(subcommand)] field: SubCmd`: Attribute can be used with either struct fields or enum
/// variants that impl `Subcommand`.
/// - `#[command(flatten)] Variant(SubCmd)`: Attribute can only be used with enum variants that impl
/// `Subcommand`.
///
/// **NOTE:** Deriving requires the `derive` feature flag

Commands Struct

We implement the Commands struct, and its attributes will correspond to commands.

1
2
3
4
5
6
// Clap can read comments through reflection macros and print them out when help is requested.
#[derive(Subcommand)]
enum Commands {
/// create-vite
CreateVite
}

Next, read in the main function.

1
2
3
4
5
6
7
8
9
10
fn main() {
let cli = Args::parse();

match cli.command {
Some(Commands::CreateVite) => println!("create vite!"),
None => {
println!("Run with --help to see instructions.")
}
}
}

Run it!

1
2
3
cargo run -- create-vite
# or
cargo run --release create-vite
If you run the help command
If you run the help command

Step Three: Copying the Project Template from Vite Scaffold

Rather than retrieving front-end project templates from the internet, we opt to save the front-end project template in a
directory and copy it to the target directory when creating a new project.
This method has its downsides, such as increasing the size of the project and requiring manual placement of the
directory into the build folder. However, the advantage is that it is relatively simple and convenient to implement.

Creating a Project with Vite Scaffold

First, we create a project using the Vite scaffold, and then copy the project into our project’s public folder.

1
pnpm create vite

We will only create these four types of projects: Vue+JS, Vue+TS, React+JS, and React+TS.

example
example

After creation, copy them to the public folder of the project.

Directory structure as shown in Figure
Directory structure as shown in Figure

Step Four: Writing the Methods Needed

Create common.rs, where we will store commonly used utility class methods.

1
2
3
src/
common.rs
main.rs

Incorporate the common module into main.rs.

1
2
// main.rs
mod common;

Method for reading command line input.

1
2
3
4
5
6
7
8
9
10
11
12
// common.rs

// read inputs
pub fn read_line() -> String {
// Used to store the string input by the user.
let mut input = String::new();
// Reading command line input.
std::io::stdin()
.read_line(&mut input)
.expect("Stdin not working");
input.trim().to_string()
}

Method for copying files to a specified directory.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// common.rs
use std::{fs, io, path::Path};

// Copy a folder to a specified path.
pub fn copy_dir_all(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> io::Result<()> {
// fs::create_dir_all(&dst)?;
fs::create_dir_all(&dst).expect("创建dst目录失败");

// println!("遍历");
fs::read_dir(&src).expect("找不到src目录");
for entry in fs::read_dir(src)? {
let entry = entry?;
let ty = entry.file_type()?;
if ty.is_dir() {
copy_dir_all(entry.path(), dst.as_ref().join(entry.file_name()))?;
} else {
fs::copy(entry.path(), dst.as_ref().join(entry.file_name()))?;
}
}

// println!("copy dir done.");
Ok(())
}

Method for obtaining the runtime directory location.

I initially hoped to enable the program to locate the public folder under src at runtime, but found that it was not
feasible. Later, I discovered a method to obtain the runtime directory.
This allows the public files to be copied into the target’s debug or release directories, ensuring that the program can
locate the files in the public folder regardless of where it is running and begin the copying process.

1
2
3
4
5
6
7
8
9
src/
main.rs
target/
cargo run运行时的current_exe目录
debug/
public/...
cargo build之后运行exe时的current_exe目录
release/
public/...
1
2
3
4
5
6
7
8
9
10
11
12
13
// common.rs
use std::{fs, path::Path, env::current_exe};

// Get runtime directory
pub fn current_exe_pkg() -> String {
let pkg_name = env!("CARGO_PKG_NAME");
// println!("{pkg_name}.exe");
let pkg_name = pkg_name.to_string() + ".exe";

// Get the path of the current directory
let current_exe = current_exe().unwrap();
current_exe.display().to_string().replace(&pkg_name, "")
}

Step Five: Logic for Project Creation

After completing the preliminary preparations, we can finally begin to write the logic for creating projects. This step
involves content that is all placed within vite.rs.

1
2
3
src/
main.rs
vite.rs
1
2
// main.rs
mod vite;

Struct for Storing User Input

In order to save user input for subsequent creation of different projects based on the input options, we need a struct
to store this data.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// vite.rs

// Which framework the user uses
#[derive(Debug)]
enum FrameworkType {
React,
Vue,
}

// whether the user uses TS.
#[derive(Debug)]
enum VariantType {
Javascript,
Typescript,
}

#[derive(Debug)]
struct UserSelected {
framework_type: FrameworkType,
variant_type: VariantType,
project_name: String, // project name
}

Adding Methods to the Struct

We need two methods: one for creating the struct, and another for creating the project.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
// vite.rs
use std::path::Path;
use crate::common::{copy_dir_all, current_exe_pkg, read_line};

impl UserSelected {
fn new(project_name: &str, framework: &str, variant: &str) -> Self {
let framework_type = match framework {
"react" => {
FrameworkType::React
}
"vue" => {
FrameworkType::Vue
}
_ => {
// panic!("No such framework type")
// default
FrameworkType::React
}
};

let variant_type = match variant {
"javascript" => {
VariantType::Javascript
}
"typescript" => {
VariantType::Typescript
}
"js" => {
VariantType::Javascript
}
"ts" => {
VariantType::Typescript
}
_ => {
// panic!("No such variant type")
VariantType::Typescript
}
};

let project_name = if project_name.is_empty() {
"vite-project"
} else { project_name };

UserSelected {
project_name: project_name.to_string(),
framework_type: framework_type,
variant_type: variant_type,
// file_name: "".to_string(),
}
}

// 创建文件
fn init(&self) {
let mut path = "public/vite/".to_string();

match self.framework_type {
FrameworkType::React => {
path += "react";
}
FrameworkType::Vue => {
path += "vue";
}
}

path += "-";

match self.variant_type {
VariantType::Javascript => {
path += "js";
}
VariantType::Typescript => {
path += "ts";
}
}

println!("复制 {}", &(current_exe_pkg() + &path));

// todo: 从网络上下载 或 调用cmd git clone

copy_dir_all(
Path::new(
&(current_exe_pkg() + &path)
),
Path::new(&self.project_name),
).unwrap();
}
}

Ask and receive user input.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// vite.rs

pub fn create_vite_project() {
// project name
println!("your project name? {}", "vite-project");
let project_name = read_line();
let project_name = if project_name.is_empty() {
"vite-project"
} else {
&project_name
};
println!("{}", (&project_name));

// select a framework
// react vue ...
println!("select a framework: (default: react)");
println!("react");
println!("vue");
let mut framework = read_line().to_lowercase();
framework = match framework.as_str() {
"r" => "react".to_string(),
"react" => "react".to_string(),
"v" => "vue".to_string(),
"vue" => "vue".to_string(),
_ => "react".to_string(),
};
println!("{}", (&framework));

// select a variant
// javascript typescript ...
println!("select a variant: (default: ts)");
println!("typescript(ts)");
println!("javascript(js)");
let mut variant = read_line().to_lowercase();
variant = match variant.as_str() {
"ts" => "typescript".to_string(),
"typescript" => "typescript".to_string(),
"js" => "javascript".to_string(),
"javascript" => "javascript".to_string(),
_ => "typescript".to_string(),
};
println!("{}", (&variant));

let user_select = UserSelected::new(&project_name, &framework, &variant);

user_select.init();
}

Step Six: Testing

Modify main.rs

1
2
3
4
5
6
7
8
9
10
11
12
// main.rs

fn main() {
let cli = Args::parse();

match cli.command {
Some(Commands::CreateVite) => create_vite_project(),
None => {
println!("Run with --help to see instructions.")
}
}
}

Test cargo run

Move the public directory to the target/debug directory.

Test whether cargo run can be successful.

1
cargo run -- create-vite

It can be seen that the vite-project directory was successfully created in the root directory of the project.

Test the exe after cargo build.

Move the public directory to the target/release directory.

1
2
cargo build --release
.\target\release\rust-vite-cli.exe create-vite

end

Next, you can copy the files in the release directory to any directory on your system, then configure it into the system
environment variables, and you can run rust-vite-cli create-vite to create front-end projects anytime and anywhere.

Thank you for watching.


本站总访问量