Merge branch 'master' into jk/formatting-and-extension

This commit is contained in:
Jonathan Kelley committed 2022-12-25 23:38:33 -05:00
commit fa52a8f71a
59 files changed
+6691 -252

No files matched your search

+7 -5
View File
@@ -17,7 +17,9 @@ asset_dir = "public"
[web.app]
# HTML title tag content
title = "dioxus | ⛺"
title = "Dioxus | An elegant GUI library for Rust"
index_on_404 = true
[web.watcher]
@@ -38,8 +40,8 @@ script = []
# serve: [dev-server] only
script = []
[application.tools]
[application.plugins]
# use binaryen.wasm-opt for output Wasm file
# binaryen just will trigger in `web` platform
binaryen = { wasm_opt = [".PROJECT_OUT"] }
available = true
required = []
+2 -3
View File
@@ -9,10 +9,9 @@
</head>
<body>
<div id="main"></div>
<!-- Note the usage of `type=module` here as this is an ES6 module -->
<script type="module">
import init from "{base_path}/assets/dioxus/{app_name}.js";
init("{base_path}/assets/dioxus/{app_name}_bg.wasm").then(wasm => {
import init from "/{base_path}/assets/dioxus/{app_name}.js";
init("/{base_path}/assets/dioxus/{app_name}_bg.wasm").then(wasm => {
if (wasm.__wbindgen_start == undefined) {
wasm.main();
}
+333 -31
View File
@@ -1,23 +1,35 @@
use crate::{
config::{CrateConfig, ExecutableType},
error::{Error, Result},
tools::Tool,
DioxusConfig,
};
use cargo_metadata::{diagnostic::Diagnostic, Message};
use indicatif::{ProgressBar, ProgressStyle};
use serde::Serialize;
use std::{
fs::{copy, create_dir_all, remove_dir_all, File},
fs::{copy, create_dir_all, File},
io::Read,
panic,
path::PathBuf,
process::Command,
time::Duration,
};
use wasm_bindgen_cli_support::Bindgen;
pub fn build(config: &CrateConfig) -> Result<()> {
#[derive(Serialize, Debug, Clone)]
pub struct BuildResult {
pub warnings: Vec<Diagnostic>,
pub elapsed_time: u128,
}
pub fn build(config: &CrateConfig, quiet: bool) -> Result<BuildResult> {
// [1] Build the project with cargo, generating a wasm32-unknown-unknown target (is there a more specific, better target to leverage?)
// [2] Generate the appropriate build folders
// [3] Wasm-bindgen the .wasm fiile, and move it into the {builddir}/modules/xxxx/xxxx_bg.wasm
// [4] Wasm-opt the .wasm file with whatever optimizations need to be done
// [5] Link up the html page to the wasm module
// [5][OPTIONAL] Builds the Tailwind CSS file using the Tailwind standalone binary
// [6] Link up the html page to the wasm module
let CrateConfig {
out_dir,
@@ -29,6 +41,9 @@ pub fn build(config: &CrateConfig) -> Result<()> {
..
} = config;
// start to build the assets
let ignore_files = build_assets(config)?;
let t_start = std::time::Instant::now();
// [1] Build the .wasm module
@@ -38,12 +53,32 @@ pub fn build(config: &CrateConfig) -> Result<()> {
.arg("build")
.arg("--target")
.arg("wasm32-unknown-unknown")
.stdout(std::process::Stdio::inherit())
.stderr(std::process::Stdio::inherit());
.arg("--message-format=json")
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped());
if config.release {
cmd.arg("--release");
}
if config.verbose {
cmd.arg("--verbose");
}
if quiet {
cmd.arg("--quiet");
}
if config.custom_profile.is_some() {
let custom_profile = config.custom_profile.as_ref().unwrap();
cmd.arg("--profile");
cmd.arg(custom_profile);
}
if config.features.is_some() {
let features_str = config.features.as_ref().unwrap().join(" ");
cmd.arg("--features");
cmd.arg(features_str);
}
match executable {
ExecutableType::Binary(name) => cmd.arg("--bin").arg(name),
@@ -51,13 +86,7 @@ pub fn build(config: &CrateConfig) -> Result<()> {
ExecutableType::Example(name) => cmd.arg("--example").arg(name),
};
let output = cmd.output()?;
if !output.status.success() {
log::error!("Build failed!");
let reason = String::from_utf8_lossy(&output.stderr).to_string();
return Err(Error::BuildFailed(reason));
}
let warning_messages = prettier_build(cmd)?;
// [2] Establish the output directory structure
let bindgen_outdir = out_dir.join("assets").join("dioxus");
@@ -95,8 +124,7 @@ pub fn build(config: &CrateConfig) -> Result<()> {
.unwrap();
});
if bindgen_result.is_err() {
log::error!("Bindgen build failed! \nThis is probably due to the Bindgen version, dioxus-cli using `0.2.79` Bindgen crate.");
return Ok(());
return Err(Error::BuildFailed("Bindgen build failed! \nThis is probably due to the Bindgen version, dioxus-cli using `0.2.81` Bindgen crate.".to_string()));
}
// check binaryen:wasm-opt tool
@@ -110,19 +138,21 @@ pub fn build(config: &CrateConfig) -> Result<()> {
if sub.contains_key("wasm_opt")
&& sub.get("wasm_opt").unwrap().as_bool().unwrap_or(false)
{
log::info!("Optimizing WASM size with wasm-opt...");
let target_file = out_dir
.join("assets")
.join("dioxus")
.join(format!("{}_bg.wasm", dioxus_config.application.name));
if target_file.is_file() {
binaryen.call(
"wasm-opt",
vec![
target_file.to_str().unwrap(),
"-o",
target_file.to_str().unwrap(),
],
)?;
let mut args = vec![
target_file.to_str().unwrap(),
"-o",
target_file.to_str().unwrap(),
];
if config.release == true {
args.push("-Oz");
}
binaryen.call("wasm-opt", args)?;
}
}
}
@@ -133,6 +163,45 @@ pub fn build(config: &CrateConfig) -> Result<()> {
}
}
// [5][OPTIONAL] If tailwind is enabled and installed we run it to generate the CSS
if dioxus_tools.contains_key("tailwindcss") {
let info = dioxus_tools.get("tailwindcss").unwrap();
let tailwind = crate::tools::Tool::Tailwind;
if tailwind.is_installed() {
if let Some(sub) = info.as_table() {
log::info!("Building Tailwind bundle CSS file...");
let input_path = match sub.get("input") {
Some(val) => val.as_str().unwrap(),
None => "./public",
};
let config_path = match sub.get("config") {
Some(val) => val.as_str().unwrap(),
None => "./src/tailwind.config.js",
};
let mut args = vec![
"-i",
input_path,
"-o",
"dist/tailwind.css",
"-c",
config_path,
];
if config.release == true {
args.push("--minify");
}
tailwind.call("tailwindcss", args)?;
}
} else {
log::warn!(
"Tailwind tool not found, you can use `dioxus tool add tailwindcss` to install it."
);
}
}
// this code will copy all public file to the output dir
let copy_options = fs_extra::dir::CopyOptions {
overwrite: true,
@@ -154,18 +223,29 @@ pub fn build(config: &CrateConfig) -> Result<()> {
log::warn!("Error copying dir: {}", _e);
}
}
for ignore in &ignore_files {
let ignore = ignore.strip_prefix(&config.asset_dir).unwrap();
let ignore = config.out_dir.join(ignore);
if ignore.is_file() {
std::fs::remove_file(ignore)?;
}
}
}
}
}
let t_end = std::time::Instant::now();
log::info!("🏁 Done in {}ms!", (t_end - t_start).as_millis());
Ok(())
Ok(BuildResult {
warnings: warning_messages,
elapsed_time: (t_end - t_start).as_millis(),
})
}
pub fn build_desktop(config: &CrateConfig, is_serve: bool) -> Result<()> {
pub fn build_desktop(config: &CrateConfig, _is_serve: bool) -> Result<()> {
log::info!("🚅 Running build [Desktop] command...");
let ignore_files = build_assets(config)?;
let mut cmd = Command::new("cargo");
cmd.current_dir(&config.crate_dir)
.arg("build")
@@ -175,6 +255,21 @@ pub fn build_desktop(config: &CrateConfig, is_serve: bool) -> Result<()> {
if config.release {
cmd.arg("--release");
}
if config.verbose {
cmd.arg("--verbose");
}
if config.custom_profile.is_some() {
let custom_profile = config.custom_profile.as_ref().unwrap();
cmd.arg("--profile");
cmd.arg(custom_profile);
}
if config.features.is_some() {
let features_str = config.features.as_ref().unwrap().join(" ");
cmd.arg("--features");
cmd.arg(features_str);
}
match &config.executable {
crate::ExecutableType::Binary(name) => cmd.arg("--bin").arg(name),
@@ -189,12 +284,6 @@ pub fn build_desktop(config: &CrateConfig, is_serve: bool) -> Result<()> {
}
if output.status.success() {
// this code will clean the output dir.
// if using the serve, we will not clean the out_dir.
if config.out_dir.is_dir() && !is_serve {
remove_dir_all(&config.out_dir)?;
}
let release_type = match config.release {
true => "release",
false => "debug",
@@ -250,6 +339,13 @@ pub fn build_desktop(config: &CrateConfig, is_serve: bool) -> Result<()> {
log::warn!("Error copying dir: {}", e);
}
}
for ignore in &ignore_files {
let ignore = ignore.strip_prefix(&config.asset_dir).unwrap();
let ignore = config.out_dir.join(ignore);
if ignore.is_file() {
std::fs::remove_file(ignore)?;
}
}
}
}
}
@@ -269,6 +365,57 @@ pub fn build_desktop(config: &CrateConfig, is_serve: bool) -> Result<()> {
Ok(())
}
fn prettier_build(mut cmd: Command) -> anyhow::Result<Vec<Diagnostic>> {
let mut warning_messages: Vec<Diagnostic> = vec![];
let pb = ProgressBar::new_spinner();
pb.enable_steady_tick(Duration::from_millis(200));
pb.set_style(
ProgressStyle::with_template("{spinner:.dim.bold} {wide_msg}")
.unwrap()
.tick_chars("/|\\- "),
);
pb.set_message("💼 Waiting to start build the project...");
let mut command = cmd.spawn()?;
let reader = std::io::BufReader::new(command.stdout.take().unwrap());
for message in cargo_metadata::Message::parse_stream(reader) {
match message.unwrap() {
Message::CompilerMessage(msg) => {
let message = msg.message;
match message.level {
cargo_metadata::diagnostic::DiagnosticLevel::Error => {
return Err(anyhow::anyhow!(message
.rendered
.unwrap_or("Unknown".into())));
}
cargo_metadata::diagnostic::DiagnosticLevel::Warning => {
warning_messages.push(message.clone());
}
_ => {}
}
}
Message::CompilerArtifact(artifact) => {
pb.set_message(format!("Compiling {} ", artifact.package_id));
pb.tick();
}
Message::BuildScriptExecuted(script) => {
let _package_id = script.package_id.to_string();
}
Message::BuildFinished(finished) => {
if finished.success {
pb.finish_and_clear();
log::info!("👑 Build done.");
} else {
std::process::exit(1);
}
}
_ => (), // Unknown message
}
}
Ok(warning_messages)
}
pub fn gen_page(config: &DioxusConfig, serve: bool) -> String {
let crate_root = crate::cargo::crate_root().unwrap();
let custom_html_file = crate_root.join("index.html");
@@ -303,6 +450,15 @@ pub fn gen_page(config: &DioxusConfig, serve: bool) -> String {
&style.to_str().unwrap(),
))
}
if config
.application
.tools
.clone()
.unwrap_or_default()
.contains_key("tailwindcss")
{
style_str.push_str("<link rel=\"stylesheet\" href=\"tailwind.css\">\n");
}
html = html.replace("{style_include}", &style_str);
let mut script_str = String::new();
@@ -339,6 +495,152 @@ pub fn gen_page(config: &DioxusConfig, serve: bool) -> String {
html.replace("{app_title}", &title)
}
// this function will build some assets file
// like sass tool resources
// this function will return a array which file don't need copy to out_dir.
fn build_assets(config: &CrateConfig) -> Result<Vec<PathBuf>> {
let mut result = vec![];
let dioxus_config = &config.dioxus_config;
let dioxus_tools = dioxus_config.application.tools.clone().unwrap_or_default();
// check sass tool state
let sass = Tool::Sass;
if sass.is_installed() && dioxus_tools.contains_key("sass") {
let sass_conf = dioxus_tools.get("sass").unwrap();
if let Some(tab) = sass_conf.as_table() {
let source_map = tab.contains_key("source_map");
let source_map = if source_map && tab.get("source_map").unwrap().is_bool() {
if tab.get("source_map").unwrap().as_bool().unwrap_or_default() {
"--source-map"
} else {
"--no-source-map"
}
} else {
"--source-map"
};
if tab.contains_key("input") {
if tab.get("input").unwrap().is_str() {
let file = tab.get("input").unwrap().as_str().unwrap().trim();
if file == "*" {
// if the sass open auto, we need auto-check the assets dir.
let asset_dir = config.asset_dir.clone();
if asset_dir.is_dir() {
for entry in walkdir::WalkDir::new(&asset_dir)
.into_iter()
.filter_map(|e| e.ok())
{
let temp = entry.path();
if temp.is_file() {
let suffix = temp.extension();
if suffix.is_none() {
continue;
}
let suffix = suffix.unwrap().to_str().unwrap();
if suffix == "scss" || suffix == "sass" {
// if file suffix is `scss` / `sass` we need transform it.
let out_file = format!(
"{}.css",
temp.file_stem().unwrap().to_str().unwrap()
);
let target_path = config
.out_dir
.join(
temp.strip_prefix(&asset_dir)
.unwrap()
.parent()
.unwrap(),
)
.join(out_file);
let res = sass.call(
"sass",
vec![
temp.to_str().unwrap(),
target_path.to_str().unwrap(),
source_map,
],
);
if res.is_ok() {
result.push(temp.to_path_buf());
}
}
}
}
}
} else {
// just transform one file.
let relative_path = if &file[0..1] == "/" {
&file[1..file.len()]
} else {
file
};
let path = config.asset_dir.join(relative_path);
let out_file =
format!("{}.css", path.file_stem().unwrap().to_str().unwrap());
let target_path = config
.out_dir
.join(PathBuf::from(relative_path).parent().unwrap())
.join(out_file);
if path.is_file() {
let res = sass.call(
"sass",
vec![
path.to_str().unwrap(),
target_path.to_str().unwrap(),
source_map,
],
);
if res.is_ok() {
result.push(path);
} else {
log::error!("{:?}", res);
}
}
}
} else if tab.get("input").unwrap().is_array() {
// check files list.
let list = tab.get("input").unwrap().as_array().unwrap();
for i in list {
if i.is_str() {
let path = i.as_str().unwrap();
let relative_path = if &path[0..1] == "/" {
&path[1..path.len()]
} else {
path
};
let path = config.asset_dir.join(relative_path);
let out_file =
format!("{}.css", path.file_stem().unwrap().to_str().unwrap());
let target_path = config
.out_dir
.join(PathBuf::from(relative_path).parent().unwrap())
.join(out_file);
if path.is_file() {
let res = sass.call(
"sass",
vec![
path.to_str().unwrap(),
target_path.to_str().unwrap(),
source_map,
],
);
if res.is_ok() {
result.push(path);
}
}
}
}
}
}
}
}
// SASS END
Ok(result)
}
// use binary_install::{Cache, Download};
// /// Attempts to find `wasm-opt` in `PATH` locally, or failing that downloads a
+13 -2
View File
@@ -1,3 +1,5 @@
use crate::plugin::PluginManager;
use super::*;
/// Build the Rust WASM app and all of its assets.
@@ -14,11 +16,16 @@ impl Build {
// change the release state.
crate_config.with_release(self.build.release);
crate_config.with_verbose(self.build.verbose);
if self.build.example.is_some() {
crate_config.as_example(self.build.example.unwrap());
}
if self.build.profile.is_some() {
crate_config.set_profile(self.build.profile.unwrap());
}
let platform = self.build.platform.unwrap_or_else(|| {
crate_config
.dioxus_config
@@ -27,15 +34,17 @@ impl Build {
.clone()
});
let _ = PluginManager::on_build_start(&crate_config, &platform);
match platform.as_str() {
"web" => {
crate::builder::build(&crate_config)?;
crate::builder::build(&crate_config, false)?;
}
"desktop" => {
crate::builder::build_desktop(&crate_config, false)?;
}
_ => {
return custom_error!("Unsoppurt platform target.");
return custom_error!("Unsupported platform target.");
}
}
@@ -56,6 +65,8 @@ impl Build {
)?;
file.write_all(temp.as_bytes())?;
let _ = PluginManager::on_build_finish(&crate_config, &platform);
Ok(())
}
}
+36
View File
@@ -12,13 +12,26 @@ pub struct ConfigOptsBuild {
#[serde(default)]
pub release: bool,
// Use verbose output [default: false]
#[clap(long)]
#[serde(default)]
pub verbose: bool,
/// Build a example [default: ""]
#[clap(long)]
pub example: Option<String>,
/// Build with custom profile
#[clap(long)]
pub profile: Option<String>,
/// Build platform: support Web & Desktop [default: "default_platform"]
#[clap(long)]
pub platform: Option<String>,
/// Space separated list of features to activate
#[clap(long)]
pub features: Option<Vec<String>>,
}
#[derive(Clone, Debug, Default, Deserialize, Parser)]
@@ -27,6 +40,11 @@ pub struct ConfigOptsServe {
#[clap(parse(from_os_str))]
pub target: Option<PathBuf>,
/// Port of dev server
#[clap(long)]
#[clap(default_value_t = 8080)]
pub port: u16,
/// Build a example [default: ""]
#[clap(long)]
pub example: Option<String>,
@@ -36,9 +54,27 @@ pub struct ConfigOptsServe {
#[serde(default)]
pub release: bool,
// Use verbose output [default: false]
#[clap(long)]
#[serde(default)]
pub verbose: bool,
/// Build with custom profile
#[clap(long)]
pub profile: Option<String>,
/// Build platform: support Web & Desktop [default: "default_platform"]
#[clap(long)]
pub platform: Option<String>,
/// Build with hot reloading rsx [default: false]
#[clap(long)]
#[serde(default)]
pub hot_reload: bool,
/// Space separated list of features to activate
#[clap(long)]
pub features: Option<Vec<String>>,
}
/// Ensure the given value for `--public-url` is formatted correctly.
+1 -2
View File
@@ -16,8 +16,7 @@ impl Clean {
.output()?;
if !output.status.success() {
log::error!("Cargo clean failed.");
return Ok(());
return custom_error!("Cargo clean failed.");
}
let out_dir = crate_config
+3 -2
View File
@@ -17,8 +17,7 @@ pub struct Create {
impl Create {
pub fn create(self) -> Result<()> {
if Self::name_vaild_check(self.name.clone()) {
log::error!("❗Unsupported project name.");
return Ok(());
return custom_error!("❗Unsupported project name.");
}
let project_path = PathBuf::from(&self.name);
@@ -54,6 +53,7 @@ impl Create {
.arg(&self.template)
.arg("--name")
.arg(&self.name)
.arg("--force")
.stdout(Stdio::piped())
.stderr(Stdio::inherit())
.output()?;
@@ -66,6 +66,7 @@ impl Create {
let mut meta_file = String::new();
dioxus_file.read_to_string(&mut meta_file)?;
meta_file = meta_file.replace("{{project-name}}", &self.name);
meta_file = meta_file.replace("{{default-platform}}", "web");
File::create(project_path.join("Dioxus.toml"))?.write_all(meta_file.as_bytes())?;
println!();
+21 -10
View File
@@ -4,8 +4,8 @@ pub mod cfg;
pub mod clean;
pub mod config;
pub mod create;
pub mod plugin;
pub mod serve;
pub mod tool;
pub mod translate;
pub mod version;
@@ -24,12 +24,12 @@ use std::{
fs::{remove_dir_all, File},
io::{Read, Write},
path::PathBuf,
process::{exit, Command, Stdio},
process::{Command, Stdio},
};
/// Build, bundle, & ship your Dioxus app.
/// Build, Bundle & Ship Dioxus Apps.
#[derive(Parser)]
#[clap(name = "dioxus")]
#[clap(name = "dioxus", version)]
pub struct Cli {
#[clap(subcommand)]
pub action: Commands,
@@ -37,10 +37,6 @@ pub struct Cli {
/// Enable verbose logging.
#[clap(short)]
pub v: bool,
// // note: dioxus is still roughly compatible with trunk
// /// Path to the Trunk config file [default: Trunk.toml]
// #[clap(long, parse(from_os_str), env = "TRUNK_CONFIG")]
// pub config: Option<PathBuf>,
}
#[derive(Parser)]
@@ -72,7 +68,22 @@ pub enum Commands {
#[clap(subcommand)]
Config(config::Config),
/// Install & Manage tools for Dioxus-cli.
/// Manage plugins for dioxus cli
#[clap(subcommand)]
Tool(tool::Tool),
Plugin(plugin::Plugin),
}
impl Commands {
pub fn to_string(&self) -> String {
match self {
Commands::Build(_) => "build",
Commands::Translate(_) => "translate",
Commands::Serve(_) => "sevre",
Commands::Create(_) => "create",
Commands::Clean(_) => "clean",
Commands::Config(_) => "config",
Commands::Plugin(_) => "plugin",
}
.to_string()
}
}
+36
View File
@@ -0,0 +1,36 @@
use super::*;
/// Build the Rust WASM app and all of its assets.
#[derive(Clone, Debug, Deserialize, Subcommand)]
#[clap(name = "plugin")]
pub enum Plugin {
/// Return all dioxus-cli support tools.
List {},
/// Get default app install path.
AppPath {},
/// Install a new tool.
Add { name: String },
}
impl Plugin {
pub async fn plugin(self) -> Result<()> {
match self {
Plugin::List {} => {
for item in crate::plugin::PluginManager::plugin_list() {
println!("- {item}");
}
}
Plugin::AppPath {} => {
if let Some(v) = crate::plugin::PluginManager::init_plugin_dir().to_str() {
println!("{}", v);
} else {
log::error!("Plugin path get failed.");
}
}
Plugin::Add { name: _ } => {
log::info!("You can use `dioxus plugin app-path` to get Installation position");
}
}
Ok(())
}
}
+35 -34
View File
@@ -1,5 +1,6 @@
use super::*;
use std::{
fs::create_dir_all,
io::Write,
path::PathBuf,
process::{Command, Stdio},
@@ -18,12 +19,18 @@ impl Serve {
let mut crate_config = crate::CrateConfig::new()?;
// change the relase state.
crate_config.with_hot_reload(self.serve.hot_reload);
crate_config.with_release(self.serve.release);
crate_config.with_verbose(self.serve.verbose);
if self.serve.example.is_some() {
crate_config.as_example(self.serve.example.unwrap());
}
if self.serve.profile.is_some() {
crate_config.set_profile(self.serve.profile.unwrap());
}
let platform = self.serve.platform.unwrap_or_else(|| {
crate_config
.dioxus_config
@@ -32,38 +39,32 @@ impl Serve {
.clone()
});
match platform.as_str() {
"web" => {
crate::builder::build(&crate_config)?;
}
"desktop" => {
crate::builder::build_desktop(&crate_config, true)?;
if platform.as_str() == "desktop" {
crate::builder::build_desktop(&crate_config, true)?;
match &crate_config.executable {
crate::ExecutableType::Binary(name)
| crate::ExecutableType::Lib(name)
| crate::ExecutableType::Example(name) => {
let mut file = crate_config.out_dir.join(name);
if cfg!(windows) {
file.set_extension("exe");
}
Command::new(crate_config.out_dir.join(file).to_str().unwrap())
.stdout(Stdio::inherit())
.output()?;
match &crate_config.executable {
crate::ExecutableType::Binary(name)
| crate::ExecutableType::Lib(name)
| crate::ExecutableType::Example(name) => {
let mut file = crate_config.out_dir.join(name);
if cfg!(windows) {
file.set_extension("exe");
}
Command::new(file.to_str().unwrap())
.stdout(Stdio::inherit())
.output()?;
}
return Ok(());
}
_ => {
return custom_error!("Unsoppurt platform target.");
}
return Ok(());
} else if platform != "web" {
return custom_error!("Unsupported platform target.");
}
// generate dev-index page
Serve::regen_dev_page(&crate_config)?;
// start the develop server
server::startup(crate_config.clone()).await?;
server::startup(self.serve.port, crate_config.clone()).await?;
Ok(())
}
@@ -71,19 +72,19 @@ impl Serve {
pub fn regen_dev_page(crate_config: &CrateConfig) -> Result<()> {
let serve_html = gen_page(&crate_config.dioxus_config, true);
let mut file = std::fs::File::create(
let dist_path = crate_config.crate_dir.join(
crate_config
.crate_dir
.join(
crate_config
.dioxus_config
.application
.out_dir
.clone()
.unwrap_or_else(|| PathBuf::from("dist")),
)
.join("index.html"),
)?;
.dioxus_config
.application
.out_dir
.clone()
.unwrap_or_else(|| PathBuf::from("dist")),
);
if !dist_path.is_dir() {
create_dir_all(&dist_path)?;
}
let index_path = dist_path.join("index.html");
let mut file = std::fs::File::create(index_path)?;
file.write_all(serve_html.as_bytes())?;
Ok(())
+11 -11
View File
@@ -20,21 +20,24 @@ impl Tool {
Tool::List {} => {
for item in tools::tool_list() {
if tools::Tool::from_str(item).unwrap().is_installed() {
println!("{item} [installed]");
println!("- {item} [installed]");
} else {
println!("{item}");
println!("- {item}");
}
}
}
Tool::AppPath {} => {
println!("{}", tools::tools_path().to_str().unwrap());
if let Some(v) = tools::tools_path().to_str() {
println!("{}", v);
} else {
return custom_error!("Tools path get failed.");
}
}
Tool::Add { name } => {
let tool_list = tools::tool_list();
if !tool_list.contains(&name.as_str()) {
log::error!("Tool {name} not found.");
return Ok(());
return custom_error!("Tool {name} not found.");
}
let target_tool = tools::Tool::from_str(&name).unwrap();
@@ -45,20 +48,17 @@ impl Tool {
log::info!("Start to download tool package...");
if let Err(e) = target_tool.download_package().await {
log::error!("Tool download failed: {e}");
return Ok(());
return custom_error!("Tool download failed: {e}");
}
log::info!("Start to install tool package...");
if let Err(e) = target_tool.install_package().await {
log::error!("Tool install failed: {e}");
return Ok(());
return custom_error!("Tool install failed: {e}");
}
log::info!("Tool {name} install successfully!");
log::info!("Tool {name} installed successfully!");
}
}
Ok(())
}
}
+1 -2
View File
@@ -43,8 +43,7 @@ impl Translate {
}
(None, None) => {
if atty::is(atty::Stream::Stdin) {
log::error!("No input file, source, or stdin to translate from.");
exit(0);
return custom_error!("No input file, source, or stdin to translate from.");
}
let mut buffer = String::new();
+55 -27
View File
@@ -6,6 +6,13 @@ use std::{collections::HashMap, fs::File, io::Read, path::PathBuf};
pub struct DioxusConfig {
pub application: ApplicationConfig,
pub web: WebConfig,
#[serde(default = "default_plugin")]
pub plugin: toml::Value,
}
fn default_plugin() -> toml::Value {
toml::Value::Boolean(true)
}
impl DioxusConfig {
@@ -22,7 +29,7 @@ impl DioxusConfig {
dioxus_conf_file.read_to_string(&mut meta_str)?;
toml::from_str::<DioxusConfig>(&meta_str)
.map_err(|_| crate::Error::Unique("Dioxus.toml parse failed".into()))
.map_err(|_| crate::Error::Unique("Dioxus.toml parse failed".into()))
}
}
@@ -34,21 +41,23 @@ impl Default for DioxusConfig {
default_platform: "web".to_string(),
out_dir: Some(PathBuf::from("dist")),
asset_dir: Some(PathBuf::from("public")),
tools: None,
sub_package: None,
},
web: WebConfig {
app: WebAppConfing {
app: WebAppConfig {
title: Some("dioxus | ⛺".into()),
base_path: None,
},
watcher: WebWatcherConfing {
watcher: WebWatcherConfig {
watch_path: Some(vec![PathBuf::from("src")]),
reload_html: Some(false),
index_on_404: Some(true),
},
resource: WebResourceConfing {
dev: WebDevResourceConfing {
resource: WebResourceConfig {
dev: WebDevResourceConfig {
style: Some(vec![]),
script: Some(vec![]),
},
@@ -56,6 +65,7 @@ impl Default for DioxusConfig {
script: Some(vec![]),
},
},
plugin: toml::Value::Table(toml::map::Map::new()),
}
}
}
@@ -66,39 +76,41 @@ pub struct ApplicationConfig {
pub default_platform: String,
pub out_dir: Option<PathBuf>,
pub asset_dir: Option<PathBuf>,
pub tools: Option<HashMap<String, toml::Value>>,
pub sub_package: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WebConfig {
pub app: WebAppConfing,
pub watcher: WebWatcherConfing,
pub resource: WebResourceConfing,
pub app: WebAppConfig,
pub watcher: WebWatcherConfig,
pub resource: WebResourceConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WebAppConfing {
pub struct WebAppConfig {
pub title: Option<String>,
pub base_path: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WebWatcherConfing {
pub struct WebWatcherConfig {
pub watch_path: Option<Vec<PathBuf>>,
pub reload_html: Option<bool>,
pub index_on_404: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WebResourceConfing {
pub dev: WebDevResourceConfing,
pub struct WebResourceConfig {
pub dev: WebDevResourceConfig,
pub style: Option<Vec<PathBuf>>,
pub script: Option<Vec<PathBuf>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WebDevResourceConfing {
pub struct WebDevResourceConfig {
pub style: Option<Vec<PathBuf>>,
pub script: Option<Vec<PathBuf>>,
}
@@ -114,6 +126,10 @@ pub struct CrateConfig {
pub executable: ExecutableType,
pub dioxus_config: DioxusConfig,
pub release: bool,
pub hot_reload: bool,
pub verbose: bool,
pub custom_profile: Option<String>,
pub features: Option<Vec<String>>,
}
#[derive(Debug, Clone)]
@@ -162,6 +178,10 @@ impl CrateConfig {
let executable = ExecutableType::Binary(output_filename);
let release = false;
let hot_reload = false;
let verbose = false;
let custom_profile = None;
let features = None;
Ok(Self {
out_dir,
@@ -173,6 +193,10 @@ impl CrateConfig {
executable,
release,
dioxus_config,
hot_reload,
custom_profile,
features,
verbose,
})
}
@@ -186,19 +210,23 @@ impl CrateConfig {
self
}
// pub fn with_build_options(&mut self, options: &BuildOptions) {
// if let Some(name) = &options.example {
// self.as_example(name.clone());
// }
// self.release = options.release;
// self.out_dir = options.outdir.clone().into();
// }
pub fn with_hot_reload(&mut self, hot_reload: bool) -> &mut Self {
self.hot_reload = hot_reload;
self
}
// pub fn with_develop_options(&mut self, options: &DevelopOptions) {
// if let Some(name) = &options.example {
// self.as_example(name.clone());
// }
// self.release = options.release;
// self.out_dir = tempfile::Builder::new().tempdir().expect("").into_path();
// }
pub fn with_verbose(&mut self, verbose: bool) -> &mut Self {
self.verbose = verbose;
self
}
pub fn set_profile(&mut self, profile: String) -> &mut Self {
self.custom_profile = Some(profile);
self
}
pub fn set_features(&mut self, features: Vec<String>) -> &mut Self {
self.features = Some(features);
self
}
}
+1 -1
View File
@@ -63,7 +63,7 @@ impl From<hyper::Error> for Error {
#[macro_export]
macro_rules! custom_error {
($msg:literal $(,)?) => {
Err(Error::CustomError($msg.to_string()))
Err(Error::CustomError(format!($msg)))
};
($err:expr $(,)?) => {
Err(Error::from($err))
+4
View File
@@ -1,3 +1,5 @@
pub const DIOXUS_CLI_VERSION: &str = "0.1.5";
pub mod builder;
pub mod server;
pub mod tools;
@@ -18,3 +20,5 @@ pub use error::*;
pub mod logging;
pub use logging::*;
pub mod plugin;
+24 -8
View File
@@ -1,50 +1,66 @@
use clap::Parser;
use dioxus_cli::*;
use dioxus_cli::{plugin::PluginManager, *};
use std::process::exit;
#[tokio::main]
async fn main() -> Result<()> {
let args = Cli::parse();
set_up_logging();
let dioxus_config = DioxusConfig::load().unwrap_or(DioxusConfig::default());
let plugin_state = PluginManager::init(dioxus_config.plugin);
if let Err(e) = plugin_state {
log::error!("🚫 Plugin system initialization failed: {e}");
exit(1);
}
match args.action {
Commands::Translate(opts) => {
if let Err(e) = opts.translate() {
log::error!("translate error: {}", e);
log::error!("🚫 Translate failed: {}", e);
exit(1);
}
}
Commands::Build(opts) => {
if let Err(e) = opts.build() {
log::error!("build error: {}", e);
log::error!("🚫 Build project failed: {}", e);
exit(1);
}
}
Commands::Clean(opts) => {
if let Err(e) = opts.clean() {
log::error!("clean error: {}", e);
log::error!("🚫 Clean project failed: {}", e);
exit(1);
}
}
Commands::Serve(opts) => {
if let Err(e) = opts.serve().await {
log::error!("serve error: {}", e);
log::error!("🚫 Serve startup failed: {}", e);
exit(1);
}
}
Commands::Create(opts) => {
if let Err(e) = opts.create() {
log::error!("create error: {}", e);
log::error!("🚫 Create project failed: {}", e);
exit(1);
}
}
Commands::Config(opts) => {
if let Err(e) = opts.config() {
log::error!("config error: {}", e);
exit(1);
}
}
Commands::Tool(opts) => {
if let Err(e) = opts.tool().await {
Commands::Plugin(opts) => {
if let Err(e) = opts.plugin().await {
log::error!("tool error: {}", e);
}
}
+65
View File
@@ -0,0 +1,65 @@
use std::process::{Command, Stdio};
use mlua::{FromLua, UserData};
enum StdioFromString {
Inhert,
Piped,
Null,
}
impl<'lua> FromLua<'lua> for StdioFromString {
fn from_lua(lua_value: mlua::Value<'lua>, _lua: &'lua mlua::Lua) -> mlua::Result<Self> {
if let mlua::Value::String(v) = lua_value {
let v = v.to_str().unwrap();
return Ok(match v.to_lowercase().as_str() {
"inhert" => Self::Inhert,
"piped" => Self::Piped,
"null" => Self::Null,
_ => Self::Inhert,
});
}
Ok(Self::Inhert)
}
}
impl StdioFromString {
pub fn to_stdio(self) -> Stdio {
match self {
StdioFromString::Inhert => Stdio::inherit(),
StdioFromString::Piped => Stdio::piped(),
StdioFromString::Null => Stdio::null(),
}
}
}
pub struct PluginCommander;
impl UserData for PluginCommander {
fn add_methods<'lua, M: mlua::UserDataMethods<'lua, Self>>(methods: &mut M) {
methods.add_function(
"exec",
|_, args: (Vec<String>, StdioFromString, StdioFromString)| {
let cmd = args.0;
let stdout = args.1;
let stderr = args.2;
if cmd.len() == 0 {
return Ok(());
}
let cmd_name = cmd.get(0).unwrap();
let mut command = Command::new(cmd_name);
let t = cmd
.iter()
.enumerate()
.filter(|(i, _)| *i > 0)
.map(|v| v.1.clone())
.collect::<Vec<String>>();
command.args(t);
command.stdout(stdout.to_stdio()).stderr(stderr.to_stdio());
command.output()?;
Ok(())
},
);
}
fn add_fields<'lua, F: mlua::UserDataFields<'lua, Self>>(_fields: &mut F) {}
}
+13
View File
@@ -0,0 +1,13 @@
use mlua::UserData;
use crate::tools::app_path;
pub struct PluginDirs;
impl UserData for PluginDirs {
fn add_methods<'lua, M: mlua::UserDataMethods<'lua, Self>>(methods: &mut M) {
methods.add_function("plugins_dir", |_, ()| {
let path = app_path().join("plugins");
Ok(path.to_str().unwrap().to_string())
});
}
}
+87
View File
@@ -0,0 +1,87 @@
use std::{
fs::{create_dir, create_dir_all, remove_dir_all},
path::PathBuf, io::{Read, Write},
};
use std::fs::File;
use mlua::UserData;
use flate2::read::GzDecoder;
use tar::Archive;
use crate::tools::extract_zip;
pub struct PluginFileSystem;
impl UserData for PluginFileSystem {
fn add_methods<'lua, M: mlua::UserDataMethods<'lua, Self>>(methods: &mut M) {
methods.add_function("create_dir", |_, args: (String, bool)| {
let path = args.0;
let recursive = args.1;
let path = PathBuf::from(path);
if !path.exists() {
let v = if recursive {
create_dir_all(path)
} else {
create_dir(path)
};
return Ok(v.is_ok());
}
Ok(true)
});
methods.add_function("remove_dir", |_, path: String| {
let path = PathBuf::from(path);
let r = remove_dir_all(path);
Ok(r.is_ok())
});
methods.add_function("file_get_content", |_, path: String| {
let path = PathBuf::from(path);
let mut file = std::fs::File::open(path)?;
let mut buffer = String::new();
file.read_to_string(&mut buffer)?;
Ok(buffer)
});
methods.add_function("file_set_content", |_, args: (String, String)| {
let path = args.0;
let content = args.1;
let path = PathBuf::from(path);
let file = std::fs::File::create(path);
if file.is_err() {
return Ok(false);
}
if file.unwrap().write_all(content.as_bytes()).is_err() {
return Ok(false)
}
Ok(true)
});
methods.add_function("unzip_file", |_, args: (String, String)| {
let file = PathBuf::from(args.0);
let target = PathBuf::from(args.1);
let res = extract_zip(&file, &target);
if let Err(_) = res {
return Ok(false);
}
Ok(true)
});
methods.add_function("untar_gz_file", |_, args: (String, String)| {
let file = PathBuf::from(args.0);
let target = PathBuf::from(args.1);
let tar_gz = if let Ok(v) = File::open(file) {
v
} else {
return Ok(false);
};
let tar = GzDecoder::new(tar_gz);
let mut archive = Archive::new(tar);
if archive.unpack(&target).is_err() {
return Ok(false);
}
Ok(true)
});
}
}
+28
View File
@@ -0,0 +1,28 @@
use log;
use mlua::UserData;
pub struct PluginLogger;
impl UserData for PluginLogger {
fn add_methods<'lua, M: mlua::UserDataMethods<'lua, Self>>(methods: &mut M) {
methods.add_function("trace", |_, info: String| {
log::trace!("{}", info);
Ok(())
});
methods.add_function("info", |_, info: String| {
log::info!("{}", info);
Ok(())
});
methods.add_function("debug", |_, info: String| {
log::debug!("{}", info);
Ok(())
});
methods.add_function("warn", |_, info: String| {
log::warn!("{}", info);
Ok(())
});
methods.add_function("error", |_, info: String| {
log::error!("{}", info);
Ok(())
});
}
}
+233
View File
@@ -0,0 +1,233 @@
use mlua::{FromLua, Function, ToLua};
pub mod command;
pub mod dirs;
pub mod fs;
pub mod log;
pub mod network;
pub mod os;
pub mod path;
#[derive(Debug, Clone)]
pub struct PluginInfo<'lua> {
pub name: String,
pub repository: String,
pub author: String,
pub version: String,
pub inner: PluginInner,
pub on_init: Option<Function<'lua>>,
pub build: PluginBuildInfo<'lua>,
pub serve: PluginServeInfo<'lua>,
}
impl<'lua> FromLua<'lua> for PluginInfo<'lua> {
fn from_lua(lua_value: mlua::Value<'lua>, _lua: &'lua mlua::Lua) -> mlua::Result<Self> {
let mut res = Self {
name: String::default(),
repository: String::default(),
author: String::default(),
version: String::from("0.1.0"),
inner: Default::default(),
on_init: None,
build: Default::default(),
serve: Default::default(),
};
if let mlua::Value::Table(tab) = lua_value {
if let Ok(v) = tab.get::<_, String>("name") {
res.name = v;
}
if let Ok(v) = tab.get::<_, String>("repository") {
res.repository = v;
}
if let Ok(v) = tab.get::<_, String>("author") {
res.author = v;
}
if let Ok(v) = tab.get::<_, String>("version") {
res.version = v;
}
if let Ok(v) = tab.get::<_, PluginInner>("inner") {
res.inner = v;
}
if let Ok(v) = tab.get::<_, Function>("on_init") {
res.on_init = Some(v);
}
if let Ok(v) = tab.get::<_, PluginBuildInfo>("build") {
res.build = v;
}
if let Ok(v) = tab.get::<_, PluginServeInfo>("serve") {
res.serve = v;
}
}
Ok(res)
}
}
impl<'lua> ToLua<'lua> for PluginInfo<'lua> {
fn to_lua(self, lua: &'lua mlua::Lua) -> mlua::Result<mlua::Value<'lua>> {
let res = lua.create_table()?;
res.set("name", self.name.to_string())?;
res.set("repository", self.repository.to_string())?;
res.set("author", self.author.to_string())?;
res.set("version", self.version.to_string())?;
res.set("inner", self.inner)?;
if let Some(e) = self.on_init {
res.set("on_init", e)?;
}
res.set("build", self.build)?;
res.set("serve", self.serve)?;
Ok(mlua::Value::Table(res))
}
}
#[derive(Debug, Clone, Default)]
pub struct PluginInner {
pub plugin_dir: String,
pub from_loader: bool,
}
impl<'lua> FromLua<'lua> for PluginInner {
fn from_lua(lua_value: mlua::Value<'lua>, _lua: &'lua mlua::Lua) -> mlua::Result<Self> {
let mut res = Self {
plugin_dir: String::new(),
from_loader: false,
};
if let mlua::Value::Table(t) = lua_value {
if let Ok(v) = t.get::<_, String>("plugin_dir") {
res.plugin_dir = v;
}
if let Ok(v) = t.get::<_, bool>("from_loader") {
res.from_loader = v;
}
}
Ok(res)
}
}
impl<'lua> ToLua<'lua> for PluginInner {
fn to_lua(self, lua: &'lua mlua::Lua) -> mlua::Result<mlua::Value<'lua>> {
let res = lua.create_table()?;
res.set("plugin_dir", self.plugin_dir)?;
res.set("from_loader", self.from_loader)?;
Ok(mlua::Value::Table(res))
}
}
#[derive(Debug, Clone, Default)]
pub struct PluginBuildInfo<'lua> {
pub on_start: Option<Function<'lua>>,
pub on_finish: Option<Function<'lua>>,
}
impl<'lua> FromLua<'lua> for PluginBuildInfo<'lua> {
fn from_lua(lua_value: mlua::Value<'lua>, _lua: &'lua mlua::Lua) -> mlua::Result<Self> {
let mut res = Self {
on_start: None,
on_finish: None,
};
if let mlua::Value::Table(t) = lua_value {
if let Ok(v) = t.get::<_, Function>("on_start") {
res.on_start = Some(v);
}
if let Ok(v) = t.get::<_, Function>("on_finish") {
res.on_finish = Some(v);
}
}
Ok(res)
}
}
impl<'lua> ToLua<'lua> for PluginBuildInfo<'lua> {
fn to_lua(self, lua: &'lua mlua::Lua) -> mlua::Result<mlua::Value<'lua>> {
let res = lua.create_table()?;
if let Some(v) = self.on_start {
res.set("on_start", v)?;
}
if let Some(v) = self.on_finish {
res.set("on_finish", v)?;
}
Ok(mlua::Value::Table(res))
}
}
#[derive(Debug, Clone, Default)]
pub struct PluginServeInfo<'lua> {
pub interval: i32,
pub on_start: Option<Function<'lua>>,
pub on_interval: Option<Function<'lua>>,
pub on_rebuild: Option<Function<'lua>>,
pub on_shutdown: Option<Function<'lua>>,
}
impl<'lua> FromLua<'lua> for PluginServeInfo<'lua> {
fn from_lua(lua_value: mlua::Value<'lua>, _lua: &'lua mlua::Lua) -> mlua::Result<Self> {
let mut res = Self::default();
if let mlua::Value::Table(tab) = lua_value {
if let Ok(v) = tab.get::<_, i32>("interval") {
res.interval = v;
}
if let Ok(v) = tab.get::<_, Function>("on_start") {
res.on_start = Some(v);
}
if let Ok(v) = tab.get::<_, Function>("on_interval") {
res.on_interval = Some(v);
}
if let Ok(v) = tab.get::<_, Function>("on_rebuild") {
res.on_rebuild = Some(v);
}
if let Ok(v) = tab.get::<_, Function>("on_shutdown") {
res.on_shutdown = Some(v);
}
}
Ok(res)
}
}
impl<'lua> ToLua<'lua> for PluginServeInfo<'lua> {
fn to_lua(self, lua: &'lua mlua::Lua) -> mlua::Result<mlua::Value<'lua>> {
let res = lua.create_table()?;
res.set("interval", self.interval)?;
if let Some(v) = self.on_start {
res.set("on_start", v)?;
}
if let Some(v) = self.on_interval {
res.set("on_interval", v)?;
}
if let Some(v) = self.on_rebuild {
res.set("on_rebuild", v)?;
}
if let Some(v) = self.on_shutdown {
res.set("on_shutdown", v)?;
}
Ok(mlua::Value::Table(res))
}
}
+27
View File
@@ -0,0 +1,27 @@
use std::{io::Cursor, path::PathBuf};
use mlua::UserData;
pub struct PluginNetwork;
impl UserData for PluginNetwork {
fn add_methods<'lua, M: mlua::UserDataMethods<'lua, Self>>(methods: &mut M) {
methods.add_function("download_file", |_, args: (String, String)| {
let url = args.0;
let path = args.1;
let resp = reqwest::blocking::get(url);
if let Ok(resp) = resp {
let mut content = Cursor::new(resp.bytes().unwrap());
let file = std::fs::File::create(PathBuf::from(path));
if file.is_err() {
return Ok(false);
}
let mut file = file.unwrap();
let res = std::io::copy(&mut content, &mut file);
return Ok(res.is_ok());
}
Ok(false)
});
}
}
+18
View File
@@ -0,0 +1,18 @@
use mlua::UserData;
pub struct PluginOS;
impl UserData for PluginOS {
fn add_methods<'lua, M: mlua::UserDataMethods<'lua, Self>>(methods: &mut M) {
methods.add_function("current_platform", |_, ()| {
if cfg!(target_os = "windows") {
Ok("windows")
} else if cfg!(target_os = "macos") {
Ok("macos")
} else if cfg!(target_os = "linux") {
Ok("linux")
} else {
panic!("unsupported platformm");
}
});
}
}
+40
View File
@@ -0,0 +1,40 @@
use std::path::PathBuf;
use mlua::{UserData, Variadic};
pub struct PluginPath;
impl UserData for PluginPath {
fn add_methods<'lua, M: mlua::UserDataMethods<'lua, Self>>(methods: &mut M) {
// join function
methods.add_function("join", |_, args: Variadic<String>| {
let mut path = PathBuf::new();
for i in args {
path = path.join(i);
}
Ok(path.to_str().unwrap().to_string())
});
// parent function
methods.add_function("parent", |_, path: String| {
let current_path = PathBuf::from(&path);
let parent = current_path.parent();
if parent.is_none() {
return Ok(path);
} else {
return Ok(parent.unwrap().to_str().unwrap().to_string());
}
});
methods.add_function("exists", |_, path: String| {
let path = PathBuf::from(path);
Ok(path.exists())
});
methods.add_function("is_dir", |_, path: String| {
let path = PathBuf::from(path);
Ok(path.is_dir())
});
methods.add_function("is_file", |_, path: String| {
let path = PathBuf::from(path);
Ok(path.is_file())
});
}
}
+330
View File
@@ -0,0 +1,330 @@
use std::{
io::{Read, Write},
path::PathBuf,
sync::Mutex,
};
use mlua::{Lua, Table};
use serde_json::json;
use crate::{
tools::{app_path, clone_repo},
CrateConfig,
};
use self::{
interface::PluginInfo,
interface::{
command::PluginCommander, dirs::PluginDirs, fs::PluginFileSystem, log::PluginLogger,
network::PluginNetwork, os::PluginOS, path::PluginPath,
},
types::PluginConfig,
};
pub mod interface;
mod types;
lazy_static::lazy_static! {
static ref LUA: Mutex<Lua> = Mutex::new(Lua::new());
}
pub struct PluginManager;
impl PluginManager {
pub fn init(config: toml::Value) -> anyhow::Result<()> {
let config = PluginConfig::from_toml_value(config);
if !config.available {
return Ok(());
}
let lua = LUA.lock().unwrap();
let manager = lua.create_table().unwrap();
let name_index = lua.create_table().unwrap();
let plugin_dir = Self::init_plugin_dir();
let api = lua.create_table().unwrap();
api.set("log", PluginLogger).unwrap();
api.set("command", PluginCommander).unwrap();
api.set("network", PluginNetwork).unwrap();
api.set("dirs", PluginDirs).unwrap();
api.set("fs", PluginFileSystem).unwrap();
api.set("path", PluginPath).unwrap();
api.set("os", PluginOS).unwrap();
lua.globals().set("plugin_lib", api).unwrap();
lua.globals()
.set("library_dir", plugin_dir.to_str().unwrap())
.unwrap();
lua.globals().set("config_info", config.clone())?;
let mut index: u32 = 1;
let dirs = std::fs::read_dir(&plugin_dir)?;
let mut path_list = dirs
.filter(|v| v.is_ok())
.map(|v| (v.unwrap().path(), false))
.collect::<Vec<(PathBuf, bool)>>();
for i in &config.loader {
let path = PathBuf::from(i);
if !path.is_dir() {
// for loader dir, we need check first, because we need give a error log.
log::error!("Plugin loader: {:?} path is not a exists directory.", path);
}
path_list.push((path, true));
}
for entry in path_list {
let plugin_dir = entry.0.to_path_buf();
if plugin_dir.is_dir() {
let init_file = plugin_dir.join("init.lua");
if init_file.is_file() {
let mut file = std::fs::File::open(init_file).unwrap();
let mut buffer = String::new();
file.read_to_string(&mut buffer).unwrap();
let current_plugin_dir = plugin_dir.to_str().unwrap().to_string();
let from_loader = entry.1;
lua.globals()
.set("_temp_plugin_dir", current_plugin_dir.clone())?;
lua.globals().set("_temp_from_loader", from_loader)?;
let info = lua.load(&buffer).eval::<PluginInfo>();
match info {
Ok(mut info) => {
if name_index.contains_key(info.name.clone()).unwrap_or(false)
&& !from_loader
{
// found same name plugin, intercept load
log::warn!(
"Plugin {} has been intercepted. [mulit-load]",
info.name
);
continue;
}
info.inner.plugin_dir = current_plugin_dir;
info.inner.from_loader = from_loader;
// call `on_init` if file "dcp.json" not exists
let dcp_file = plugin_dir.join("dcp.json");
if !dcp_file.is_file() {
if let Some(func) = info.clone().on_init {
let result = func.call::<_, bool>(());
match result {
Ok(true) => {
// plugin init success, create `dcp.json` file.
let mut file = std::fs::File::create(dcp_file).unwrap();
let value = json!({
"name": info.name,
"author": info.author,
"repository": info.repository,
"version": info.version,
"generate_time": chrono::Local::now().timestamp(),
});
let buffer =
serde_json::to_string_pretty(&value).unwrap();
let buffer = buffer.as_bytes();
file.write_all(buffer).unwrap();
// insert plugin-info into plugin-manager
if let Ok(index) =
name_index.get::<_, u32>(info.name.clone())
{
let _ = manager.set(index, info.clone());
} else {
let _ = manager.set(index, info.clone());
index += 1;
let _ = name_index.set(info.name, index);
}
}
Ok(false) => {
log::warn!("Plugin init function result is `false`, init failed.");
}
Err(e) => {
log::warn!("Plugin init failed: {e}");
}
}
}
} else {
if let Ok(index) = name_index.get::<_, u32>(info.name.clone()) {
let _ = manager.set(index, info.clone());
} else {
let _ = manager.set(index, info.clone());
index += 1;
let _ = name_index.set(info.name, index);
}
}
}
Err(_e) => {
let dir_name = plugin_dir.file_name().unwrap().to_str().unwrap();
log::error!("Plugin '{dir_name}' load failed.");
}
}
}
}
}
lua.globals().set("manager", manager).unwrap();
return Ok(());
}
pub fn on_build_start(crate_config: &CrateConfig, platform: &str) -> anyhow::Result<()> {
let lua = LUA.lock().unwrap();
if !lua.globals().contains_key("manager")? {
return Ok(());
}
let manager = lua.globals().get::<_, Table>("manager")?;
let args = lua.create_table()?;
args.set("name", crate_config.dioxus_config.application.name.clone())?;
args.set("platform", platform)?;
args.set("out_dir", crate_config.out_dir.to_str().unwrap())?;
args.set("asset_dir", crate_config.asset_dir.to_str().unwrap())?;
for i in 1..(manager.len()? as i32 + 1) {
let info = manager.get::<i32, PluginInfo>(i)?;
if let Some(func) = info.build.on_start {
func.call::<Table, ()>(args.clone())?;
}
}
Ok(())
}
pub fn on_build_finish(crate_config: &CrateConfig, platform: &str) -> anyhow::Result<()> {
let lua = LUA.lock().unwrap();
if !lua.globals().contains_key("manager")? {
return Ok(());
}
let manager = lua.globals().get::<_, Table>("manager")?;
let args = lua.create_table()?;
args.set("name", crate_config.dioxus_config.application.name.clone())?;
args.set("platform", platform)?;
args.set("out_dir", crate_config.out_dir.to_str().unwrap())?;
args.set("asset_dir", crate_config.asset_dir.to_str().unwrap())?;
for i in 1..(manager.len()? as i32 + 1) {
let info = manager.get::<i32, PluginInfo>(i)?;
if let Some(func) = info.build.on_finish {
func.call::<Table, ()>(args.clone())?;
}
}
Ok(())
}
pub fn on_serve_start(crate_config: &CrateConfig) -> anyhow::Result<()> {
let lua = LUA.lock().unwrap();
if !lua.globals().contains_key("manager")? {
return Ok(());
}
let manager = lua.globals().get::<_, Table>("manager")?;
let args = lua.create_table()?;
args.set("name", crate_config.dioxus_config.application.name.clone())?;
for i in 1..(manager.len()? as i32 + 1) {
let info = manager.get::<i32, PluginInfo>(i)?;
if let Some(func) = info.serve.on_start {
func.call::<Table, ()>(args.clone())?;
}
}
Ok(())
}
pub fn on_serve_rebuild(timestamp: i64, files: Vec<PathBuf>) -> anyhow::Result<()> {
let lua = LUA.lock().unwrap();
let manager = lua.globals().get::<_, Table>("manager")?;
let args = lua.create_table()?;
args.set("timestamp", timestamp)?;
let files: Vec<String> = files
.iter()
.map(|v| v.to_str().unwrap().to_string())
.collect();
args.set("changed_files", files)?;
for i in 1..(manager.len()? as i32 + 1) {
let info = manager.get::<i32, PluginInfo>(i)?;
if let Some(func) = info.serve.on_rebuild {
func.call::<Table, ()>(args.clone())?;
}
}
Ok(())
}
pub fn on_serve_shutdown(crate_config: &CrateConfig) -> anyhow::Result<()> {
let lua = LUA.lock().unwrap();
if !lua.globals().contains_key("manager")? {
return Ok(());
}
let manager = lua.globals().get::<_, Table>("manager")?;
let args = lua.create_table()?;
args.set("name", crate_config.dioxus_config.application.name.clone())?;
for i in 1..(manager.len()? as i32 + 1) {
let info = manager.get::<i32, PluginInfo>(i)?;
if let Some(func) = info.serve.on_shutdown {
func.call::<Table, ()>(args.clone())?;
}
}
Ok(())
}
pub fn init_plugin_dir() -> PathBuf {
let app_path = app_path();
let plugin_path = app_path.join("plugins");
if !plugin_path.is_dir() {
log::info!("📖 Start to init plugin library ...");
let url = "https://github.com/DioxusLabs/cli-plugin-library";
clone_repo(&plugin_path, url).unwrap();
}
plugin_path
}
pub fn plugin_list() -> Vec<String> {
let mut res = vec![];
if let Ok(lua) = LUA.lock() {
let list = lua
.load(mlua::chunk!(
local list = {}
for key, value in ipairs(manager) do
table.insert(list, {name = value.name, loader = value.inner.from_loader})
end
return list
))
.eval::<Vec<Table>>()
.unwrap_or_default();
for i in list {
let name = i.get::<_, String>("name").unwrap();
let loader = i.get::<_, bool>("loader").unwrap();
let text = if loader {
format!("{name} [:loader]")
} else {
name
};
res.push(text);
}
}
res
}
}
+138
View File
@@ -0,0 +1,138 @@
use std::collections::HashMap;
use mlua::ToLua;
#[derive(Debug, Clone)]
pub struct PluginConfig {
pub available: bool,
pub loader: Vec<String>,
pub config_info: HashMap<String, HashMap<String, Value>>,
}
impl<'lua> ToLua<'lua> for PluginConfig {
fn to_lua(self, lua: &'lua mlua::Lua) -> mlua::Result<mlua::Value<'lua>> {
let table = lua.create_table()?;
table.set("available", self.available)?;
table.set("loader", self.loader)?;
let config_info = lua.create_table()?;
for (name, data) in self.config_info {
config_info.set(name, data)?;
}
table.set("config_info", config_info)?;
Ok(mlua::Value::Table(table))
}
}
impl PluginConfig {
pub fn from_toml_value(val: toml::Value) -> Self {
if let toml::Value::Table(tab) = val {
let available = tab
.get::<_>("available")
.unwrap_or(&toml::Value::Boolean(true));
let available = available.as_bool().unwrap_or(true);
let mut loader = vec![];
if let Some(origin) = tab.get("loader") {
if origin.is_array() {
for i in origin.as_array().unwrap() {
loader.push(i.as_str().unwrap_or_default().to_string());
}
}
}
let mut config_info = HashMap::new();
for (name, value) in tab {
if name == "available" || name == "loader" {
continue;
}
if let toml::Value::Table(value) = value {
let mut map = HashMap::new();
for (item, info) in value {
map.insert(item, Value::from_toml(info));
}
config_info.insert(name, map);
}
}
Self {
available,
loader,
config_info,
}
} else {
Self {
available: false,
loader: vec![],
config_info: HashMap::new(),
}
}
}
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub enum Value {
String(String),
Integer(i64),
Float(f64),
Boolean(bool),
Array(Vec<Value>),
Table(HashMap<String, Value>),
}
impl Value {
pub fn from_toml(origin: toml::Value) -> Self {
match origin {
cargo_toml::Value::String(s) => Value::String(s),
cargo_toml::Value::Integer(i) => Value::Integer(i),
cargo_toml::Value::Float(f) => Value::Float(f),
cargo_toml::Value::Boolean(b) => Value::Boolean(b),
cargo_toml::Value::Datetime(d) => Value::String(d.to_string()),
cargo_toml::Value::Array(a) => {
let mut v = vec![];
for i in a {
v.push(Value::from_toml(i));
}
Value::Array(v)
}
cargo_toml::Value::Table(t) => {
let mut h = HashMap::new();
for (n, v) in t {
h.insert(n, Value::from_toml(v));
}
Value::Table(h)
}
}
}
}
impl<'lua> ToLua<'lua> for Value {
fn to_lua(self, lua: &'lua mlua::Lua) -> mlua::Result<mlua::Value<'lua>> {
Ok(match self {
Value::String(s) => mlua::Value::String(lua.create_string(&s)?),
Value::Integer(i) => mlua::Value::Integer(i),
Value::Float(f) => mlua::Value::Number(f),
Value::Boolean(b) => mlua::Value::Boolean(b),
Value::Array(a) => {
let table = lua.create_table()?;
for (i, v) in a.iter().enumerate() {
table.set(i, v.clone())?;
}
mlua::Value::Table(table)
}
Value::Table(t) => {
let table = lua.create_table()?;
for (i, v) in t.iter() {
table.set(i.clone(), v.clone())?;
}
mlua::Value::Table(table)
}
})
}
}
+522 -53
View File
@@ -1,3 +1,4 @@
use crate::{builder, plugin::PluginManager, serve::Serve, BuildResult, CrateConfig, Result};
use axum::{
body::{Full, HttpBody},
extract::{ws::Message, Extension, TypedHeader, WebSocketUpgrade},
@@ -6,58 +7,155 @@ use axum::{
routing::{get, get_service},
Router,
};
use cargo_metadata::diagnostic::Diagnostic;
use colored::Colorize;
use dioxus_core::Template;
use dioxus_html::HtmlCtx;
use dioxus_rsx::hot_reload::*;
use notify::{RecommendedWatcher, Watcher};
use std::{path::PathBuf, sync::Arc};
use std::{
net::UdpSocket,
path::PathBuf,
process::Command,
sync::{Arc, Mutex},
};
use tokio::sync::broadcast;
use tower::ServiceBuilder;
use tower_http::services::fs::{ServeDir, ServeFileSystemResponseBody};
use crate::{builder, serve::Serve, CrateConfig, Result};
use tokio::sync::broadcast;
struct WsRelodState {
update: broadcast::Sender<String>,
pub struct BuildManager {
config: CrateConfig,
reload_tx: broadcast::Sender<()>,
}
pub async fn startup(config: CrateConfig) -> Result<()> {
impl BuildManager {
fn rebuild(&self) -> Result<BuildResult> {
log::info!("🪁 Rebuild project");
let result = builder::build(&self.config, true)?;
// change the websocket reload state to true;
// the page will auto-reload.
if self
.config
.dioxus_config
.web
.watcher
.reload_html
.unwrap_or(false)
{
let _ = Serve::regen_dev_page(&self.config);
}
let _ = self.reload_tx.send(());
Ok(result)
}
}
struct WsReloadState {
update: broadcast::Sender<()>,
}
pub async fn startup(port: u16, config: CrateConfig) -> Result<()> {
// ctrl-c shutdown checker
let crate_config = config.clone();
let _ = ctrlc::set_handler(move || {
let _ = PluginManager::on_serve_shutdown(&crate_config);
std::process::exit(0);
});
if config.hot_reload {
startup_hot_reload(port, config).await?
} else {
startup_default(port, config).await?
}
Ok(())
}
pub struct HotReloadState {
pub messages: broadcast::Sender<Template<'static>>,
pub build_manager: Arc<BuildManager>,
pub file_map: Arc<Mutex<FileMap<HtmlCtx>>>,
pub watcher_config: CrateConfig,
}
pub async fn hot_reload_handler(
ws: WebSocketUpgrade,
_: Option<TypedHeader<headers::UserAgent>>,
Extension(state): Extension<Arc<HotReloadState>>,
) -> impl IntoResponse {
ws.on_upgrade(|mut socket| async move {
log::info!("🔥 Hot Reload WebSocket connected");
{
// update any rsx calls that changed before the websocket connected.
{
log::info!("🔮 Finding updates since last compile...");
let templates: Vec<_> = {
state
.file_map
.lock()
.unwrap()
.map
.values()
.filter_map(|(_, template_slot)| *template_slot)
.collect()
};
for template in templates {
if socket
.send(Message::Text(serde_json::to_string(&template).unwrap()))
.await
.is_err()
{
return;
}
}
}
log::info!("finished");
}
let mut rx = state.messages.subscribe();
loop {
if let Ok(rsx) = rx.recv().await {
if socket
.send(Message::Text(serde_json::to_string(&rsx).unwrap()))
.await
.is_err()
{
break;
};
}
}
})
}
#[allow(unused_assignments)]
pub async fn startup_hot_reload(port: u16, config: CrateConfig) -> Result<()> {
let first_build_result = crate::builder::build(&config, false)?;
log::info!("🚀 Starting development server...");
PluginManager::on_serve_start(&config)?;
let dist_path = config.out_dir.clone();
let (reload_tx, _) = broadcast::channel(100);
let file_map = Arc::new(Mutex::new(FileMap::<HtmlCtx>::new(
config.crate_dir.clone(),
)));
let build_manager = Arc::new(BuildManager {
config: config.clone(),
reload_tx: reload_tx.clone(),
});
let hot_reload_tx = broadcast::channel(100).0;
let hot_reload_state = Arc::new(HotReloadState {
messages: hot_reload_tx.clone(),
build_manager: build_manager.clone(),
file_map: file_map.clone(),
watcher_config: config.clone(),
});
let ws_reload_state = Arc::new(WsRelodState {
let crate_dir = config.crate_dir.clone();
let ws_reload_state = Arc::new(WsReloadState {
update: reload_tx.clone(),
});
let mut last_update_time = chrono::Local::now().timestamp();
// file watcher: check file change
let watcher_conf = config.clone();
let mut watcher = RecommendedWatcher::new(
move |_: notify::Result<notify::Event>| {
if chrono::Local::now().timestamp() > last_update_time {
log::info!("Start to rebuild project...");
if builder::build(&watcher_conf).is_ok() {
// change the websocket reload state to true;
// the page will auto-reload.
if watcher_conf
.dioxus_config
.web
.watcher
.reload_html
.unwrap_or(false)
{
let _ = Serve::regen_dev_page(&watcher_conf);
}
let _ = reload_tx.send("reload".into());
last_update_time = chrono::Local::now().timestamp();
}
}
},
notify::Config::default(),
)
.unwrap();
let allow_watch_path = config
.dioxus_config
.web
@@ -66,6 +164,60 @@ pub async fn startup(config: CrateConfig) -> Result<()> {
.clone()
.unwrap_or_else(|| vec![PathBuf::from("src")]);
let watcher_config = config.clone();
let mut last_update_time = chrono::Local::now().timestamp();
let mut watcher = RecommendedWatcher::new(
move |evt: notify::Result<notify::Event>| {
let config = watcher_config.clone();
// Give time for the change to take effect before reading the file
std::thread::sleep(std::time::Duration::from_millis(100));
if chrono::Local::now().timestamp() > last_update_time {
if let Ok(evt) = evt {
let mut messages: Vec<Template<'static>> = Vec::new();
for path in evt.paths.clone() {
if path.extension().and_then(|p| p.to_str()) != Some("rs") {
continue;
}
// find changes to the rsx in the file
let mut map = file_map.lock().unwrap();
match map.update_rsx(&path, &crate_dir) {
UpdateResult::UpdatedRsx(msgs) => {
messages.extend(msgs);
}
UpdateResult::NeedsRebuild => {
match build_manager.rebuild() {
Ok(res) => {
print_console_info(
port,
&config,
PrettierOptions {
changed: evt.paths,
warnings: res.warnings,
elapsed_time: res.elapsed_time,
},
);
}
Err(err) => {
log::error!("{}", err);
}
}
return;
}
}
}
for msg in messages {
let _ = hot_reload_tx.send(msg);
}
}
last_update_time = chrono::Local::now().timestamp();
}
},
notify::Config::default(),
)
.unwrap();
for sub_path in allow_watch_path {
watcher
.watch(
@@ -76,13 +228,20 @@ pub async fn startup(config: CrateConfig) -> Result<()> {
}
// start serve dev-server at 0.0.0.0:8080
let port = "8080";
log::info!("📡 Dev-Server is started at: http://127.0.0.1:{}/", port);
print_console_info(
port,
&config,
PrettierOptions {
changed: vec![],
warnings: first_build_result.warnings,
elapsed_time: first_build_result.elapsed_time,
},
);
let file_service_config = config.clone();
let file_service = ServiceBuilder::new()
.and_then(
|response: Response<ServeFileSystemResponseBody>| async move {
move |response: Response<ServeFileSystemResponseBody>| async move {
let response = if file_service_config
.dioxus_config
.web
@@ -114,7 +273,7 @@ pub async fn startup(config: CrateConfig) -> Result<()> {
Ok(response)
},
)
.service(ServeDir::new((&config.crate_dir).join(&dist_path)));
.service(ServeDir::new(config.crate_dir.join(&dist_path)));
let router = Router::new()
.route("/_dioxus/ws", get(ws_handler))
@@ -127,33 +286,343 @@ pub async fn startup(config: CrateConfig) -> Result<()> {
}),
);
let router = router
.route("/_dioxus/hot_reload", get(hot_reload_handler))
.layer(Extension(ws_reload_state))
.layer(Extension(hot_reload_state));
axum::Server::bind(&format!("0.0.0.0:{}", port).parse().unwrap())
.serve(router.layer(Extension(ws_reload_state)).into_make_service())
.serve(router.into_make_service())
.await?;
Ok(())
}
pub async fn startup_default(port: u16, config: CrateConfig) -> Result<()> {
let first_build_result = crate::builder::build(&config, false)?;
log::info!("🚀 Starting development server...");
let dist_path = config.out_dir.clone();
let (reload_tx, _) = broadcast::channel(100);
let build_manager = BuildManager {
config: config.clone(),
reload_tx: reload_tx.clone(),
};
let ws_reload_state = Arc::new(WsReloadState {
update: reload_tx.clone(),
});
let mut last_update_time = chrono::Local::now().timestamp();
// file watcher: check file change
let allow_watch_path = config
.dioxus_config
.web
.watcher
.watch_path
.clone()
.unwrap_or_else(|| vec![PathBuf::from("src")]);
let watcher_config = config.clone();
let mut watcher = notify::recommended_watcher(move |info: notify::Result<notify::Event>| {
let config = watcher_config.clone();
if let Ok(e) = info {
if chrono::Local::now().timestamp() > last_update_time {
match build_manager.rebuild() {
Ok(res) => {
last_update_time = chrono::Local::now().timestamp();
print_console_info(
port,
&config,
PrettierOptions {
changed: e.paths.clone(),
warnings: res.warnings,
elapsed_time: res.elapsed_time,
},
);
let _ = PluginManager::on_serve_rebuild(
chrono::Local::now().timestamp(),
e.paths,
);
}
Err(e) => log::error!("{}", e),
}
}
}
})
.unwrap();
for sub_path in allow_watch_path {
watcher
.watch(
&config.crate_dir.join(sub_path),
notify::RecursiveMode::Recursive,
)
.unwrap();
}
// start serve dev-server at 0.0.0.0
print_console_info(
port,
&config,
PrettierOptions {
changed: vec![],
warnings: first_build_result.warnings,
elapsed_time: first_build_result.elapsed_time,
},
);
PluginManager::on_serve_start(&config)?;
let file_service_config = config.clone();
let file_service = ServiceBuilder::new()
.and_then(
move |response: Response<ServeFileSystemResponseBody>| async move {
let response = if file_service_config
.dioxus_config
.web
.watcher
.index_on_404
.unwrap_or(false)
&& response.status() == StatusCode::NOT_FOUND
{
let body = Full::from(
// TODO: Cache/memoize this.
std::fs::read_to_string(
file_service_config
.crate_dir
.join(file_service_config.out_dir)
.join("index.html"),
)
.ok()
.unwrap(),
)
.map_err(|err| match err {})
.boxed();
Response::builder()
.status(StatusCode::OK)
.body(body)
.unwrap()
} else {
response.map(|body| body.boxed())
};
Ok(response)
},
)
.service(ServeDir::new(config.crate_dir.join(&dist_path)));
let router = Router::new()
.route("/_dioxus/ws", get(ws_handler))
.fallback(
get_service(file_service).handle_error(|error: std::io::Error| async move {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Unhandled internal error: {}", error),
)
}),
)
.layer(Extension(ws_reload_state));
axum::Server::bind(&format!("0.0.0.0:{}", port).parse().unwrap())
.serve(router.into_make_service())
.await?;
Ok(())
}
#[derive(Debug, Default)]
pub struct PrettierOptions {
changed: Vec<PathBuf>,
warnings: Vec<Diagnostic>,
elapsed_time: u128,
}
fn print_console_info(port: u16, config: &CrateConfig, options: PrettierOptions) {
if let Ok(native_clearseq) = Command::new(if cfg!(target_os = "windows") {
"cls"
} else {
"clear"
})
.output()
{
print!("{}", String::from_utf8_lossy(&native_clearseq.stdout));
} else {
// Try ANSI-Escape characters
print!("\x1b[2J\x1b[H");
}
// for path in &changed {
// let path = path
// .strip_prefix(crate::crate_root().unwrap())
// .unwrap()
// .to_path_buf();
// log::info!("Updated {}", format!("{}", path.to_str().unwrap()).green());
// }
let mut profile = if config.release { "Release" } else { "Debug" }.to_string();
if config.custom_profile.is_some() {
profile = config.custom_profile.as_ref().unwrap().to_string();
}
let hot_reload = if config.hot_reload { "RSX" } else { "Normal" };
let crate_root = crate::cargo::crate_root().unwrap();
let custom_html_file = if crate_root.join("index.html").is_file() {
"Custom [index.html]"
} else {
"Default"
};
let url_rewrite = if config
.dioxus_config
.web
.watcher
.index_on_404
.unwrap_or(false)
{
"True"
} else {
"False"
};
if options.changed.is_empty() {
println!(
"{} @ v{} [{}] \n",
"Dioxus".bold().green(),
crate::DIOXUS_CLI_VERSION,
chrono::Local::now().format("%H:%M:%S").to_string().dimmed()
);
} else {
println!(
"Project Reloaded: {}\n",
format!(
"Changed {} files. [{}]",
options.changed.len(),
chrono::Local::now().format("%H:%M:%S").to_string().dimmed()
)
.purple()
.bold()
);
}
println!(
"\t> Local : {}",
format!("http://localhost:{}/", port).blue()
);
println!(
"\t> NetWork : {}",
format!(
"http://{}:{}/",
get_ip().unwrap_or(String::from("0.0.0.0")),
port
)
.blue()
);
println!("");
println!("\t> Profile : {}", profile.green());
println!("\t> Hot Reload : {}", hot_reload.cyan());
println!("\t> Index Template : {}", custom_html_file.green());
println!("\t> URL Rewrite [index_on_404] : {}", url_rewrite.purple());
println!("");
println!(
"\t> Build Time Use : {} millis",
options.elapsed_time.to_string().green().bold()
);
println!("");
if options.warnings.len() == 0 {
log::info!("{}\n", "A perfect compilation!".green().bold());
} else {
log::warn!(
"{}",
format!(
"There were {} warning messages during the build.",
options.warnings.len() - 1
)
.yellow()
.bold()
);
// for info in &options.warnings {
// let message = info.message.clone();
// if message == format!("{} warnings emitted", options.warnings.len() - 1) {
// continue;
// }
// let mut console = String::new();
// for span in &info.spans {
// let file = &span.file_name;
// let line = (span.line_start, span.line_end);
// let line_str = if line.0 == line.1 {
// line.0.to_string()
// } else {
// format!("{}~{}", line.0, line.1)
// };
// let code = span.text.clone();
// let span_info = if code.len() == 1 {
// let code = code.get(0).unwrap().text.trim().blue().bold().to_string();
// format!(
// "[{}: {}]: '{}' --> {}",
// file,
// line_str,
// code,
// message.yellow().bold()
// )
// } else {
// let code = code
// .iter()
// .enumerate()
// .map(|(_i, s)| format!("\t{}\n", s.text).blue().bold().to_string())
// .collect::<String>();
// format!("[{}: {}]:\n{}\n#:{}", file, line_str, code, message)
// };
// console = format!("{console}\n\t{span_info}");
// }
// println!("{console}");
// }
// println!(
// "\n{}\n",
// "Resolving all warnings will help your code run better!".yellow()
// );
}
}
fn get_ip() -> Option<String> {
let socket = match UdpSocket::bind("0.0.0.0:0") {
Ok(s) => s,
Err(_) => return None,
};
match socket.connect("8.8.8.8:80") {
Ok(()) => (),
Err(_) => return None,
};
match socket.local_addr() {
Ok(addr) => return Some(addr.ip().to_string()),
Err(_) => return None,
};
}
async fn ws_handler(
ws: WebSocketUpgrade,
_: Option<TypedHeader<headers::UserAgent>>,
Extension(state): Extension<Arc<WsRelodState>>,
Extension(state): Extension<Arc<WsReloadState>>,
) -> impl IntoResponse {
ws.on_upgrade(|mut socket| async move {
let mut rx = state.update.subscribe();
let reload_watcher = tokio::spawn(async move {
loop {
let v = rx.recv().await.unwrap();
if v == "reload" {
// ignore the error
if socket
.send(Message::Text(String::from("reload")))
.await
.is_err()
{
break;
}
rx.recv().await.unwrap();
// ignore the error
if socket
.send(Message::Text(String::from("reload")))
.await
.is_err()
{
break;
}
// flush the errors after recompling
rx = rx.resubscribe();
}
});
+167 -11
View File
@@ -1,6 +1,7 @@
use std::{
fs::{create_dir_all, File},
path::PathBuf,
io::{Read, Write},
path::{Path, PathBuf},
process::Command,
};
@@ -13,11 +14,13 @@ use tokio::io::AsyncWriteExt;
#[derive(Debug, PartialEq, Eq)]
pub enum Tool {
Binaryen,
Sass,
Tailwind,
}
pub fn tool_list() -> Vec<&'static str> {
vec!["binaryen"]
}
// pub fn tool_list() -> Vec<&'static str> {
// vec!["binaryen", "sass", "tailwindcss"]
// }
pub fn app_path() -> PathBuf {
let data_local = dirs::data_local_dir().unwrap();
@@ -37,6 +40,16 @@ pub fn temp_path() -> PathBuf {
temp_path
}
pub fn clone_repo(dir: &Path, url: &str) -> anyhow::Result<()> {
let target_dir = dir.parent().unwrap();
let dir_name = dir.file_name().unwrap();
let mut cmd = Command::new("git");
let cmd = cmd.current_dir(target_dir);
let _res = cmd.arg("clone").arg(url).arg(dir_name).output()?;
Ok(())
}
pub fn tools_path() -> PathBuf {
let app_path = app_path();
let temp_path = app_path.join("tools");
@@ -52,6 +65,8 @@ impl Tool {
pub fn from_str(name: &str) -> Option<Self> {
match name {
"binaryen" => Some(Self::Binaryen),
"sass" => Some(Self::Sass),
"tailwindcss" => Some(Self::Tailwind),
_ => None,
}
}
@@ -60,6 +75,8 @@ impl Tool {
pub fn name(&self) -> &str {
match self {
Self::Binaryen => "binaryen",
Self::Sass => "sass",
Self::Tailwind => "tailwindcss",
}
}
@@ -67,6 +84,8 @@ impl Tool {
pub fn bin_path(&self) -> &str {
match self {
Self::Binaryen => "bin",
Self::Sass => ".",
Self::Tailwind => ".",
}
}
@@ -84,6 +103,37 @@ impl Tool {
panic!("unsupported platformm");
}
}
Self::Sass => {
if cfg!(target_os = "windows") {
"windows"
} else if cfg!(target_os = "macos") {
"macos"
} else if cfg!(target_os = "linux") {
"linux"
} else {
panic!("unsupported platformm");
}
}
Self::Tailwind => {
if cfg!(target_os = "windows") {
"windows"
} else if cfg!(target_os = "macos") {
"macos"
} else if cfg!(target_os = "linux") {
"linux"
} else {
panic!("unsupported platformm");
}
}
}
}
/// get tool version
pub fn tool_version(&self) -> &str {
match self {
Self::Binaryen => "version_105",
Self::Sass => "1.51.0",
Self::Tailwind => "v3.1.6",
}
}
@@ -92,10 +142,31 @@ impl Tool {
match self {
Self::Binaryen => {
format!(
"https://github.com/WebAssembly/binaryen/releases/download/version_105/binaryen-version_105-x86_64-{target}.tar.gz",
"https://github.com/WebAssembly/binaryen/releases/download/{version}/binaryen-{version}-x86_64-{target}.tar.gz",
version = self.tool_version(),
target = self.target_platform()
)
}
Self::Sass => {
format!(
"https://github.com/sass/dart-sass/releases/download/{version}/dart-sass-{version}-{target}-x64.{extension}",
version = self.tool_version(),
target = self.target_platform(),
extension = self.extension()
)
}
Self::Tailwind => {
let windows_extension = match self.target_platform() {
"windows" => ".exe",
_ => "",
};
format!(
"https://github.com/tailwindlabs/tailwindcss/releases/download/{version}/tailwindcss-{target}-x64{optional_ext}",
version = self.tool_version(),
target = self.target_platform(),
optional_ext = windows_extension
)
}
}
}
@@ -103,6 +174,14 @@ impl Tool {
pub fn extension(&self) -> &str {
match self {
Self::Binaryen => "tar.gz",
Self::Sass => {
if cfg!(target_os = "windows") {
"zip"
} else {
"tar.gz"
}
}
Self::Tailwind => "bin",
}
}
@@ -131,7 +210,7 @@ impl Tool {
let chunk = chunk_res.context("error reading chunk from download")?;
let _ = file.write(chunk.as_ref()).await;
}
// log::info!("temp file path: {:?}", temp_out);
Ok(temp_out)
}
@@ -140,10 +219,10 @@ impl Tool {
let temp_path = self.temp_out_path();
let tool_path = tools_path();
let dir_name = if self == &Tool::Binaryen {
"binaryen-version_105"
} else {
""
let dir_name = match self {
Self::Binaryen => format!("binaryen-{}", self.tool_version()),
Self::Sass => "dart-sass".to_string(),
Self::Tailwind => self.name().to_string(),
};
if self.extension() == "tar.gz" {
@@ -151,8 +230,42 @@ impl Tool {
let tar = GzDecoder::new(tar_gz);
let mut archive = Archive::new(tar);
archive.unpack(&tool_path)?;
// println!("{:?} -> {:?}", tool_path.join(dir_name), tool_path.join(self.name()));
std::fs::rename(tool_path.join(dir_name), tool_path.join(self.name()))?;
} else if self.extension() == "zip" {
// decompress the `zip` file
extract_zip(&temp_path, &tool_path)?;
std::fs::rename(tool_path.join(dir_name), tool_path.join(self.name()))?;
} else if self.extension() == "bin" {
let bin_path = match self.target_platform() {
"windows" => tool_path.join(&dir_name).join(self.name()).join(".exe"),
_ => tool_path.join(&dir_name).join(self.name()),
};
// Manualy creating tool directory because we directly download the binary via Github
std::fs::create_dir(tool_path.join(dir_name))?;
let mut final_file = std::fs::File::create(&bin_path)?;
let mut temp_file = File::open(&temp_path)?;
let mut content = Vec::new();
temp_file.read_to_end(&mut content)?;
final_file.write_all(&content)?;
if self.target_platform() == "linux" {
// This code does not update permissions idk why
/*let mut perms = final_file.metadata()?.permissions();
perms.set_mode(0o744);*/
// Adding to the binary execution rights with "chmod"
let mut command = Command::new("chmod");
let _ = command
.args(vec!["+x", bin_path.to_str().unwrap()])
.stdout(std::process::Stdio::inherit())
.stderr(std::process::Stdio::inherit())
.output()?;
}
std::fs::remove_file(&temp_path)?;
}
Ok(())
@@ -169,6 +282,20 @@ impl Tool {
command.to_string()
}
}
Tool::Sass => {
if cfg!(target_os = "windows") {
format!("{}.bat", command)
} else {
command.to_string()
}
}
Tool::Tailwind => {
if cfg!(target_os = "windows") {
format!("{}.exe", command)
} else {
command.to_string()
}
}
};
if !bin_path.join(&command_file).is_file() {
@@ -185,3 +312,32 @@ impl Tool {
Ok(output.stdout)
}
}
pub fn extract_zip(file: &Path, target: &Path) -> anyhow::Result<()> {
let zip_file = std::fs::File::open(&file)?;
let mut zip = zip::ZipArchive::new(zip_file)?;
if !target.exists() {
let _ = std::fs::create_dir_all(target)?;
}
for i in 0..zip.len() {
let mut file = zip.by_index(i)?;
if file.is_dir() {
// dir
let target = target.join(Path::new(&file.name().replace('\\', "")));
let _ = std::fs::create_dir_all(target)?;
} else {
// file
let file_path = target.join(Path::new(file.name()));
let mut target_file = if !file_path.exists() {
std::fs::File::create(file_path)?
} else {
std::fs::File::open(file_path)?
};
let _num = std::io::copy(&mut file, &mut target_file)?;
}
}
Ok(())
}