Merge branch 'master' into jk/formatting-and-extension
This commit is contained in:
commit
fa52a8f71a
59 files changed
+6691
-252
No files matched your search
@@ -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 = []
|
||||
@@ -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
@@ -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
@@ -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(())
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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(())
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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))
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {}
|
||||
}
|
||||
@@ -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())
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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(())
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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(())
|
||||
}
|
||||
Reference in new issue
Block a user