Prepare CLI for move into mainline
This commit is contained in:
1 parent
737910f8ae
commit
a9d7b4d32a
83 files changed
No files matched your search
@@ -0,0 +1,25 @@
|
||||
// Dioxus-CLI
|
||||
// https://github.com/DioxusLabs/cli
|
||||
|
||||
(function () {
|
||||
var protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
var url = protocol + '//' + window.location.host + '/_dioxus/ws';
|
||||
var poll_interval = 8080;
|
||||
var reload_upon_connect = () => {
|
||||
window.setTimeout(
|
||||
() => {
|
||||
var ws = new WebSocket(url);
|
||||
ws.onopen = () => window.location.reload();
|
||||
ws.onclose = reload_upon_connect;
|
||||
},
|
||||
poll_interval);
|
||||
};
|
||||
|
||||
var ws = new WebSocket(url);
|
||||
ws.onmessage = (ev) => {
|
||||
if (ev.data == "reload") {
|
||||
window.location.reload();
|
||||
}
|
||||
};
|
||||
ws.onclose = reload_upon_connect;
|
||||
})()
|
||||
@@ -0,0 +1,47 @@
|
||||
[application]
|
||||
|
||||
# dioxus project name
|
||||
name = "{{project-name}}"
|
||||
|
||||
# default platfrom
|
||||
# you can also use `dioxus serve/build --platform XXX` to use other platform
|
||||
# value: web | desktop
|
||||
default_platform = "{{default-platform}}"
|
||||
|
||||
# Web `build` & `serve` dist path
|
||||
out_dir = "dist"
|
||||
|
||||
# resource (static) file folder
|
||||
asset_dir = "public"
|
||||
|
||||
[web.app]
|
||||
|
||||
# HTML title tag content
|
||||
title = "Dioxus | An elegant GUI library for Rust"
|
||||
|
||||
[web.watcher]
|
||||
|
||||
index_on_404 = true
|
||||
|
||||
watch_path = ["src"]
|
||||
|
||||
# include `assets` in web platform
|
||||
[web.resource]
|
||||
|
||||
# CSS style file
|
||||
style = []
|
||||
|
||||
# Javascript code file
|
||||
script = []
|
||||
|
||||
[web.resource.dev]
|
||||
|
||||
# Javascript code file
|
||||
# serve: [dev-server] only
|
||||
script = []
|
||||
|
||||
[application.plugins]
|
||||
|
||||
available = true
|
||||
|
||||
required = []
|
||||
@@ -0,0 +1,22 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>{app_title}</title>
|
||||
<meta content="text/html;charset=utf-8" http-equiv="Content-Type" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta charset="UTF-8" />
|
||||
{style_include}
|
||||
</head>
|
||||
<body>
|
||||
<div id="main"></div>
|
||||
<script type="module">
|
||||
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();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{script_include}
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,725 @@
|
||||
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, File},
|
||||
io::Read,
|
||||
panic,
|
||||
path::PathBuf,
|
||||
process::Command,
|
||||
time::Duration,
|
||||
};
|
||||
use wasm_bindgen_cli_support::Bindgen;
|
||||
|
||||
#[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][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,
|
||||
crate_dir,
|
||||
target_dir,
|
||||
asset_dir,
|
||||
executable,
|
||||
dioxus_config,
|
||||
..
|
||||
} = config;
|
||||
|
||||
// start to build the assets
|
||||
let ignore_files = build_assets(config)?;
|
||||
|
||||
let t_start = std::time::Instant::now();
|
||||
|
||||
// [1] Build the .wasm module
|
||||
log::info!("🚅 Running build command...");
|
||||
let cmd = subprocess::Exec::cmd("cargo");
|
||||
let cmd = cmd
|
||||
.cwd(&crate_dir)
|
||||
.arg("build")
|
||||
.arg("--target")
|
||||
.arg("wasm32-unknown-unknown")
|
||||
.arg("--message-format=json");
|
||||
|
||||
let cmd = if config.release {
|
||||
cmd.arg("--release")
|
||||
} else {
|
||||
cmd
|
||||
};
|
||||
let cmd = if config.verbose {
|
||||
cmd.arg("--verbose")
|
||||
} else {
|
||||
cmd
|
||||
};
|
||||
|
||||
let cmd = if quiet { cmd.arg("--quiet") } else { cmd };
|
||||
|
||||
let cmd = if config.custom_profile.is_some() {
|
||||
let custom_profile = config.custom_profile.as_ref().unwrap();
|
||||
cmd.arg("--profile").arg(custom_profile)
|
||||
} else {
|
||||
cmd
|
||||
};
|
||||
|
||||
let cmd = if config.features.is_some() {
|
||||
let features_str = config.features.as_ref().unwrap().join(" ");
|
||||
cmd.arg("--features").arg(features_str)
|
||||
} else {
|
||||
cmd
|
||||
};
|
||||
|
||||
let cmd = match executable {
|
||||
ExecutableType::Binary(name) => cmd.arg("--bin").arg(name),
|
||||
ExecutableType::Lib(name) => cmd.arg("--lib").arg(name),
|
||||
ExecutableType::Example(name) => cmd.arg("--example").arg(name),
|
||||
};
|
||||
|
||||
let warning_messages = prettier_build(cmd)?;
|
||||
|
||||
// [2] Establish the output directory structure
|
||||
let bindgen_outdir = out_dir.join("assets").join("dioxus");
|
||||
|
||||
let release_type = match config.release {
|
||||
true => "release",
|
||||
false => "debug",
|
||||
};
|
||||
|
||||
let input_path = match executable {
|
||||
ExecutableType::Binary(name) | ExecutableType::Lib(name) => target_dir
|
||||
.join(format!("wasm32-unknown-unknown/{}", release_type))
|
||||
.join(format!("{}.wasm", name)),
|
||||
|
||||
ExecutableType::Example(name) => target_dir
|
||||
.join(format!("wasm32-unknown-unknown/{}/examples", release_type))
|
||||
.join(format!("{}.wasm", name)),
|
||||
};
|
||||
|
||||
let bindgen_result = panic::catch_unwind(move || {
|
||||
// [3] Bindgen the final binary for use easy linking
|
||||
let mut bindgen_builder = Bindgen::new();
|
||||
|
||||
bindgen_builder
|
||||
.input_path(input_path)
|
||||
.web(true)
|
||||
.unwrap()
|
||||
.debug(true)
|
||||
.demangle(true)
|
||||
.keep_debug(true)
|
||||
.remove_name_section(false)
|
||||
.remove_producers_section(false)
|
||||
.out_name(&dioxus_config.application.name)
|
||||
.generate(&bindgen_outdir)
|
||||
.unwrap();
|
||||
});
|
||||
if bindgen_result.is_err() {
|
||||
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
|
||||
let dioxus_tools = dioxus_config.application.tools.clone().unwrap_or_default();
|
||||
if dioxus_tools.contains_key("binaryen") {
|
||||
let info = dioxus_tools.get("binaryen").unwrap();
|
||||
let binaryen = crate::tools::Tool::Binaryen;
|
||||
|
||||
if binaryen.is_installed() {
|
||||
if let Some(sub) = info.as_table() {
|
||||
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() {
|
||||
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)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log::warn!(
|
||||
"Binaryen tool not found, you can use `dioxus tool add binaryen` to install it."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// [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,
|
||||
skip_exist: false,
|
||||
buffer_size: 64000,
|
||||
copy_inside: false,
|
||||
content_only: false,
|
||||
depth: 0,
|
||||
};
|
||||
if asset_dir.is_dir() {
|
||||
for entry in std::fs::read_dir(&asset_dir)? {
|
||||
let path = entry?.path();
|
||||
if path.is_file() {
|
||||
std::fs::copy(&path, out_dir.join(path.file_name().unwrap()))?;
|
||||
} else {
|
||||
match fs_extra::dir::copy(&path, out_dir, ©_options) {
|
||||
Ok(_) => {}
|
||||
Err(_e) => {
|
||||
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();
|
||||
Ok(BuildResult {
|
||||
warnings: warning_messages,
|
||||
elapsed_time: (t_end - t_start).as_millis(),
|
||||
})
|
||||
}
|
||||
|
||||
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")
|
||||
.stdout(std::process::Stdio::inherit())
|
||||
.stderr(std::process::Stdio::inherit());
|
||||
|
||||
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),
|
||||
crate::ExecutableType::Lib(name) => cmd.arg("--lib").arg(name),
|
||||
crate::ExecutableType::Example(name) => cmd.arg("--example").arg(name),
|
||||
};
|
||||
|
||||
let output = cmd.output()?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(Error::BuildFailed("Program build failed.".into()));
|
||||
}
|
||||
|
||||
if output.status.success() {
|
||||
let release_type = match config.release {
|
||||
true => "release",
|
||||
false => "debug",
|
||||
};
|
||||
|
||||
let file_name: String;
|
||||
let mut res_path = match &config.executable {
|
||||
crate::ExecutableType::Binary(name) | crate::ExecutableType::Lib(name) => {
|
||||
file_name = name.clone();
|
||||
config.target_dir.join(release_type).join(name)
|
||||
}
|
||||
crate::ExecutableType::Example(name) => {
|
||||
file_name = name.clone();
|
||||
config
|
||||
.target_dir
|
||||
.join(release_type)
|
||||
.join("examples")
|
||||
.join(name)
|
||||
}
|
||||
};
|
||||
|
||||
let target_file = if cfg!(windows) {
|
||||
res_path.set_extension("exe");
|
||||
format!("{}.exe", &file_name)
|
||||
} else {
|
||||
file_name
|
||||
};
|
||||
|
||||
if !config.out_dir.is_dir() {
|
||||
create_dir_all(&config.out_dir)?;
|
||||
}
|
||||
copy(res_path, &config.out_dir.join(target_file))?;
|
||||
|
||||
// this code will copy all public file to the output dir
|
||||
if config.asset_dir.is_dir() {
|
||||
let copy_options = fs_extra::dir::CopyOptions {
|
||||
overwrite: true,
|
||||
skip_exist: false,
|
||||
buffer_size: 64000,
|
||||
copy_inside: false,
|
||||
content_only: false,
|
||||
depth: 0,
|
||||
};
|
||||
|
||||
for entry in std::fs::read_dir(&config.asset_dir)? {
|
||||
let path = entry?.path();
|
||||
if path.is_file() {
|
||||
std::fs::copy(&path, &config.out_dir.join(path.file_name().unwrap()))?;
|
||||
} else {
|
||||
match fs_extra::dir::copy(&path, &config.out_dir, ©_options) {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
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)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log::info!(
|
||||
"🚩 Build completed: [./{}]",
|
||||
config
|
||||
.dioxus_config
|
||||
.application
|
||||
.out_dir
|
||||
.clone()
|
||||
.unwrap_or_else(|| PathBuf::from("dist"))
|
||||
.display()
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn prettier_build(cmd: subprocess::Exec) -> 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...");
|
||||
|
||||
struct StopSpinOnDrop(ProgressBar);
|
||||
|
||||
impl Drop for StopSpinOnDrop {
|
||||
fn drop(&mut self) {
|
||||
self.0.finish_and_clear();
|
||||
}
|
||||
}
|
||||
|
||||
StopSpinOnDrop(pb.clone());
|
||||
|
||||
let stdout = cmd.detached().stream_stdout()?;
|
||||
let reader = std::io::BufReader::new(stdout);
|
||||
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 {
|
||||
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");
|
||||
let mut html = if custom_html_file.is_file() {
|
||||
let mut buf = String::new();
|
||||
let mut file = File::open(custom_html_file).unwrap();
|
||||
if file.read_to_string(&mut buf).is_ok() {
|
||||
buf
|
||||
} else {
|
||||
String::from(include_str!("./assets/index.html"))
|
||||
}
|
||||
} else {
|
||||
String::from(include_str!("./assets/index.html"))
|
||||
};
|
||||
|
||||
let resouces = config.web.resource.clone();
|
||||
|
||||
let mut style_list = resouces.style.unwrap_or_default();
|
||||
let mut script_list = resouces.script.unwrap_or_default();
|
||||
|
||||
if serve {
|
||||
let mut dev_style = resouces.dev.style.clone().unwrap_or_default();
|
||||
let mut dev_script = resouces.dev.script.unwrap_or_default();
|
||||
style_list.append(&mut dev_style);
|
||||
script_list.append(&mut dev_script);
|
||||
}
|
||||
|
||||
let mut style_str = String::new();
|
||||
for style in style_list {
|
||||
style_str.push_str(&format!(
|
||||
"<link rel=\"stylesheet\" href=\"{}\">\n",
|
||||
&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");
|
||||
}
|
||||
|
||||
replace_or_insert_before("{style_include}", &style_str, "</head", &mut html);
|
||||
|
||||
let mut script_str = String::new();
|
||||
for script in script_list {
|
||||
script_str.push_str(&format!(
|
||||
"<script src=\"{}\"></script>\n",
|
||||
&script.to_str().unwrap(),
|
||||
))
|
||||
}
|
||||
|
||||
replace_or_insert_before("{script_include}", &script_str, "</body", &mut html);
|
||||
|
||||
if serve {
|
||||
html += &format!(
|
||||
"<script>{}</script>",
|
||||
include_str!("./assets/autoreload.js")
|
||||
);
|
||||
}
|
||||
|
||||
let base_path = match &config.web.app.base_path {
|
||||
Some(path) => path,
|
||||
None => ".",
|
||||
};
|
||||
let app_name = &config.application.name;
|
||||
// Check if a script already exists
|
||||
if html.contains("{app_name}") && html.contains("{base_path}") {
|
||||
html = html.replace("{app_name}", app_name);
|
||||
|
||||
html = html.replace("{base_path}", base_path);
|
||||
} else {
|
||||
// If not, insert the script
|
||||
html = html.replace(
|
||||
"</body",
|
||||
&format!(
|
||||
r#"<script type="module">
|
||||
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();
|
||||
}}
|
||||
}});
|
||||
</script>
|
||||
</body"#
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
let title = config
|
||||
.web
|
||||
.app
|
||||
.title
|
||||
.clone()
|
||||
.unwrap_or_else(|| "dioxus | ⛺".into());
|
||||
|
||||
replace_or_insert_before("{app_title}", &title, "</title", &mut html);
|
||||
|
||||
html
|
||||
}
|
||||
|
||||
fn replace_or_insert_before(
|
||||
replace: &str,
|
||||
with: &str,
|
||||
or_insert_before: &str,
|
||||
content: &mut String,
|
||||
) {
|
||||
if content.contains(replace) {
|
||||
*content = content.replace(replace, with);
|
||||
} else {
|
||||
*content = content.replace(or_insert_before, &format!("{}{}", with, or_insert_before));
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
// /// precompiled binary.
|
||||
// ///
|
||||
// /// Returns `Some` if a binary was found or it was successfully downloaded.
|
||||
// /// Returns `None` if a binary wasn't found in `PATH` and this platform doesn't
|
||||
// /// have precompiled binaries. Returns an error if we failed to download the
|
||||
// /// binary.
|
||||
// pub fn find_wasm_opt(
|
||||
// cache: &Cache,
|
||||
// install_permitted: bool,
|
||||
// ) -> Result<install::Status, failure::Error> {
|
||||
// // First attempt to look up in PATH. If found assume it works.
|
||||
// if let Ok(path) = which::which("wasm-opt") {
|
||||
// PBAR.info(&format!("found wasm-opt at {:?}", path));
|
||||
|
||||
// match path.as_path().parent() {
|
||||
// Some(path) => return Ok(install::Status::Found(Download::at(path))),
|
||||
// None => {}
|
||||
// }
|
||||
// }
|
||||
|
||||
// let version = "version_78";
|
||||
// Ok(install::download_prebuilt(
|
||||
// &install::Tool::WasmOpt,
|
||||
// cache,
|
||||
// version,
|
||||
// install_permitted,
|
||||
// )?)
|
||||
// }
|
||||
@@ -0,0 +1,94 @@
|
||||
//! Utilities for working with cargo and rust files
|
||||
use crate::error::{Error, Result};
|
||||
use std::{
|
||||
env, fs,
|
||||
path::{Path, PathBuf},
|
||||
process::Command,
|
||||
str,
|
||||
};
|
||||
|
||||
/// How many parent folders are searched for a `Cargo.toml`
|
||||
const MAX_ANCESTORS: u32 = 10;
|
||||
|
||||
/// Some fields parsed from `cargo metadata` command
|
||||
pub struct Metadata {
|
||||
pub workspace_root: PathBuf,
|
||||
pub target_directory: PathBuf,
|
||||
}
|
||||
|
||||
/// Returns the root of the crate that the command is run from
|
||||
///
|
||||
/// If the command is run from the workspace root, this will return the top-level Cargo.toml
|
||||
pub fn crate_root() -> Result<PathBuf> {
|
||||
// From the current directory we work our way up, looking for `Cargo.toml`
|
||||
env::current_dir()
|
||||
.ok()
|
||||
.and_then(|mut wd| {
|
||||
for _ in 0..MAX_ANCESTORS {
|
||||
if contains_manifest(&wd) {
|
||||
return Some(wd);
|
||||
}
|
||||
if !wd.pop() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
None
|
||||
})
|
||||
.ok_or_else(|| {
|
||||
Error::CargoError("Failed to find directory containing Cargo.toml".to_string())
|
||||
})
|
||||
}
|
||||
|
||||
/// Checks if the directory contains `Cargo.toml`
|
||||
fn contains_manifest(path: &Path) -> bool {
|
||||
fs::read_dir(path)
|
||||
.map(|entries| {
|
||||
entries
|
||||
.filter_map(Result::ok)
|
||||
.any(|ent| &ent.file_name() == "Cargo.toml")
|
||||
})
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
impl Metadata {
|
||||
/// Returns the struct filled from `cargo metadata` output
|
||||
/// TODO @Jon, find a different way that doesn't rely on the cargo metadata command (it's slow)
|
||||
pub fn get() -> Result<Self> {
|
||||
let output = Command::new("cargo")
|
||||
.args(&["metadata"])
|
||||
.output()
|
||||
.map_err(|_| Error::CargoError("Manifset".to_string()))?;
|
||||
|
||||
if !output.status.success() {
|
||||
let mut msg = str::from_utf8(&output.stderr).unwrap().trim();
|
||||
if msg.starts_with("error: ") {
|
||||
msg = &msg[7..];
|
||||
}
|
||||
|
||||
return Err(Error::CargoError(msg.to_string()));
|
||||
}
|
||||
|
||||
let stdout = str::from_utf8(&output.stdout).unwrap();
|
||||
if let Some(line) = stdout.lines().next() {
|
||||
let meta: serde_json::Value = serde_json::from_str(line)
|
||||
.map_err(|_| Error::CargoError("InvalidOutput".to_string()))?;
|
||||
|
||||
let workspace_root = meta["workspace_root"]
|
||||
.as_str()
|
||||
.ok_or_else(|| Error::CargoError("InvalidOutput".to_string()))?
|
||||
.into();
|
||||
|
||||
let target_directory = meta["target_directory"]
|
||||
.as_str()
|
||||
.ok_or_else(|| Error::CargoError("InvalidOutput".to_string()))?
|
||||
.into();
|
||||
|
||||
return Ok(Self {
|
||||
workspace_root,
|
||||
target_directory,
|
||||
});
|
||||
}
|
||||
|
||||
Err(Error::CargoError("InvalidOutput".to_string()))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
use futures::{stream::FuturesUnordered, StreamExt};
|
||||
use std::{fs, process::exit};
|
||||
|
||||
use super::*;
|
||||
|
||||
// For reference, the rustfmt main.rs file
|
||||
// https://github.com/rust-lang/rustfmt/blob/master/src/bin/main.rs
|
||||
|
||||
/// Build the Rust WASM app and all of its assets.
|
||||
#[derive(Clone, Debug, Parser)]
|
||||
pub struct Autoformat {
|
||||
/// Run in 'check' mode. Exits with 0 if input is formatted correctly. Exits
|
||||
/// with 1 and prints a diff if formatting is required.
|
||||
#[clap(short, long)]
|
||||
pub check: bool,
|
||||
|
||||
/// Input rsx (selection)
|
||||
#[clap(short, long)]
|
||||
pub raw: Option<String>,
|
||||
|
||||
/// Input file
|
||||
#[clap(short, long)]
|
||||
pub file: Option<String>,
|
||||
}
|
||||
|
||||
impl Autoformat {
|
||||
// Todo: autoformat the entire crate
|
||||
pub async fn autoformat(self) -> Result<()> {
|
||||
// Default to formatting the project
|
||||
if self.raw.is_none() && self.file.is_none() {
|
||||
if let Err(e) = autoformat_project(self.check).await {
|
||||
eprintln!("error formatting project: {}", e);
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(raw) = self.raw {
|
||||
if let Some(inner) = dioxus_autofmt::fmt_block(&raw, 0) {
|
||||
println!("{}", inner);
|
||||
} else {
|
||||
// exit process with error
|
||||
eprintln!("error formatting codeblock");
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Format single file
|
||||
if let Some(file) = self.file {
|
||||
let file_content = fs::read_to_string(&file);
|
||||
|
||||
match file_content {
|
||||
Ok(s) => {
|
||||
let edits = dioxus_autofmt::fmt_file(&s);
|
||||
let out = dioxus_autofmt::apply_formats(&s, edits);
|
||||
match fs::write(&file, out) {
|
||||
Ok(_) => {
|
||||
println!("formatted {}", file);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("failed to write formatted content to file: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("failed to open file: {}", e);
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Read every .rs file accessible when considering the .gitignore and try to format it
|
||||
///
|
||||
/// Runs using Tokio for multithreading, so it should be really really fast
|
||||
///
|
||||
/// Doesn't do mod-descending, so it will still try to format unreachable files. TODO.
|
||||
async fn autoformat_project(check: bool) -> Result<()> {
|
||||
let crate_config = crate::CrateConfig::new()?;
|
||||
|
||||
let mut files_to_format = vec![];
|
||||
collect_rs_files(&crate_config.crate_dir, &mut files_to_format);
|
||||
|
||||
let counts = files_to_format
|
||||
.into_iter()
|
||||
.filter(|file| {
|
||||
if file.components().any(|f| f.as_os_str() == "target") {
|
||||
return false;
|
||||
}
|
||||
|
||||
true
|
||||
})
|
||||
.map(|path| async {
|
||||
let _path = path.clone();
|
||||
let res = tokio::spawn(async move {
|
||||
let contents = tokio::fs::read_to_string(&path).await?;
|
||||
|
||||
let edits = dioxus_autofmt::fmt_file(&contents);
|
||||
let len = edits.len();
|
||||
|
||||
if !edits.is_empty() {
|
||||
let out = dioxus_autofmt::apply_formats(&contents, edits);
|
||||
tokio::fs::write(&path, out).await?;
|
||||
}
|
||||
|
||||
Ok(len) as Result<usize, tokio::io::Error>
|
||||
})
|
||||
.await;
|
||||
|
||||
if res.is_err() {
|
||||
eprintln!("error formatting file: {}", _path.display());
|
||||
}
|
||||
|
||||
res
|
||||
})
|
||||
.collect::<FuturesUnordered<_>>()
|
||||
.collect::<Vec<_>>()
|
||||
.await;
|
||||
|
||||
let files_formatted: usize = counts
|
||||
.into_iter()
|
||||
.map(|f| match f {
|
||||
Ok(Ok(res)) => res,
|
||||
_ => 0,
|
||||
})
|
||||
.sum();
|
||||
|
||||
if files_formatted > 0 && check {
|
||||
eprintln!("{} files needed formatting", files_formatted);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn collect_rs_files(folder: &PathBuf, files: &mut Vec<PathBuf>) {
|
||||
let Ok(folder) = folder.read_dir() else { return };
|
||||
|
||||
// load the gitignore
|
||||
|
||||
for entry in folder {
|
||||
let Ok(entry) = entry else { continue; };
|
||||
|
||||
let path = entry.path();
|
||||
|
||||
if path.is_dir() {
|
||||
collect_rs_files(&path, files);
|
||||
}
|
||||
|
||||
if let Some(ext) = path.extension() {
|
||||
if ext == "rs" {
|
||||
files.push(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spawn_properly() {
|
||||
let out = Command::new("dioxus")
|
||||
.args([
|
||||
"fmt",
|
||||
"-f",
|
||||
r#"
|
||||
//
|
||||
|
||||
rsx! {
|
||||
|
||||
div {}
|
||||
}
|
||||
|
||||
//
|
||||
//
|
||||
//
|
||||
|
||||
"#,
|
||||
])
|
||||
.output()
|
||||
.expect("failed to execute process");
|
||||
|
||||
dbg!(out);
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
use crate::plugin::PluginManager;
|
||||
|
||||
use super::*;
|
||||
|
||||
/// Build the Rust WASM app and all of its assets.
|
||||
#[derive(Clone, Debug, Parser)]
|
||||
#[clap(name = "build")]
|
||||
pub struct Build {
|
||||
#[clap(flatten)]
|
||||
pub build: ConfigOptsBuild,
|
||||
}
|
||||
|
||||
impl Build {
|
||||
pub fn build(self) -> Result<()> {
|
||||
let mut crate_config = crate::CrateConfig::new()?;
|
||||
|
||||
// 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());
|
||||
}
|
||||
|
||||
if self.build.features.is_some() {
|
||||
crate_config.set_features(self.build.features.unwrap());
|
||||
}
|
||||
|
||||
let platform = self.build.platform.unwrap_or_else(|| {
|
||||
crate_config
|
||||
.dioxus_config
|
||||
.application
|
||||
.default_platform
|
||||
.clone()
|
||||
});
|
||||
|
||||
let _ = PluginManager::on_build_start(&crate_config, &platform);
|
||||
|
||||
match platform.as_str() {
|
||||
"web" => {
|
||||
crate::builder::build(&crate_config, false)?;
|
||||
}
|
||||
"desktop" => {
|
||||
crate::builder::build_desktop(&crate_config, false)?;
|
||||
}
|
||||
_ => {
|
||||
return custom_error!("Unsupported platform target.");
|
||||
}
|
||||
}
|
||||
|
||||
let temp = gen_page(&crate_config.dioxus_config, false);
|
||||
|
||||
let mut file = std::fs::File::create(
|
||||
crate_config
|
||||
.crate_dir
|
||||
.join(
|
||||
crate_config
|
||||
.dioxus_config
|
||||
.application
|
||||
.out_dir
|
||||
.clone()
|
||||
.unwrap_or_else(|| PathBuf::from("dist")),
|
||||
)
|
||||
.join("index.html"),
|
||||
)?;
|
||||
file.write_all(temp.as_bytes())?;
|
||||
|
||||
let _ = PluginManager::on_build_finish(&crate_config, &platform);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
use super::*;
|
||||
|
||||
/// Config options for the build system.
|
||||
#[derive(Clone, Debug, Default, Deserialize, Parser)]
|
||||
pub struct ConfigOptsBuild {
|
||||
/// The index HTML file to drive the bundling process [default: index.html]
|
||||
#[arg(long)]
|
||||
pub target: Option<PathBuf>,
|
||||
|
||||
/// Build in release mode [default: false]
|
||||
#[clap(long)]
|
||||
#[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)]
|
||||
pub struct ConfigOptsServe {
|
||||
/// The index HTML file to drive the bundling process [default: index.html]
|
||||
#[arg(short, long)]
|
||||
pub target: Option<PathBuf>,
|
||||
|
||||
/// Port of dev server
|
||||
#[clap(long)]
|
||||
#[clap(default_value_t = 8080)]
|
||||
pub port: u16,
|
||||
|
||||
/// Open the app in the default browser [default: false]
|
||||
#[clap(long)]
|
||||
#[serde(default)]
|
||||
pub open: bool,
|
||||
|
||||
/// Build a example [default: ""]
|
||||
#[clap(long)]
|
||||
pub example: Option<String>,
|
||||
|
||||
/// Build in release mode [default: false]
|
||||
#[clap(long)]
|
||||
#[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,
|
||||
|
||||
/// Set cross-origin-policy to same-origin [default: false]
|
||||
#[clap(name = "cross-origin-policy")]
|
||||
#[clap(long)]
|
||||
#[serde(default)]
|
||||
pub cross_origin_policy: 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.
|
||||
pub fn parse_public_url(val: &str) -> String {
|
||||
let prefix = if !val.starts_with('/') { "/" } else { "" };
|
||||
let suffix = if !val.ends_with('/') { "/" } else { "" };
|
||||
format!("{}{}{}", prefix, val, suffix)
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
use super::*;
|
||||
|
||||
/// Build the Rust WASM app and all of its assets.
|
||||
#[derive(Clone, Debug, Parser)]
|
||||
#[clap(name = "clean")]
|
||||
pub struct Clean {}
|
||||
|
||||
impl Clean {
|
||||
pub fn clean(self) -> Result<()> {
|
||||
let crate_config = crate::CrateConfig::new()?;
|
||||
|
||||
let output = Command::new("cargo")
|
||||
.arg("clean")
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.output()?;
|
||||
|
||||
if !output.status.success() {
|
||||
return custom_error!("Cargo clean failed.");
|
||||
}
|
||||
|
||||
let out_dir = crate_config
|
||||
.dioxus_config
|
||||
.application
|
||||
.out_dir
|
||||
.unwrap_or_else(|| PathBuf::from("dist"));
|
||||
if crate_config.crate_dir.join(&out_dir).is_dir() {
|
||||
remove_dir_all(crate_config.crate_dir.join(&out_dir))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
use super::*;
|
||||
|
||||
/// Build the Rust WASM app and all of its assets.
|
||||
#[derive(Clone, Debug, Deserialize, Subcommand)]
|
||||
#[clap(name = "config")]
|
||||
pub enum Config {
|
||||
/// Init `Dioxus.toml` for project/folder.
|
||||
Init {
|
||||
/// Init project name
|
||||
name: String,
|
||||
|
||||
/// Cover old config
|
||||
#[clap(long)]
|
||||
#[serde(default)]
|
||||
force: bool,
|
||||
|
||||
/// Project default platform
|
||||
#[clap(long, default_value = "web")]
|
||||
platform: String,
|
||||
},
|
||||
/// Format print Dioxus config.
|
||||
FormatPrint {},
|
||||
/// Create a custom html file.
|
||||
CustomHtml {},
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn config(self) -> Result<()> {
|
||||
let crate_root = crate::cargo::crate_root()?;
|
||||
match self {
|
||||
Config::Init {
|
||||
name,
|
||||
force,
|
||||
platform,
|
||||
} => {
|
||||
let conf_path = crate_root.join("Dioxus.toml");
|
||||
if conf_path.is_file() && !force {
|
||||
log::warn!(
|
||||
"config file `Dioxus.toml` already exist, use `--force` to overwrite it."
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
let mut file = File::create(conf_path)?;
|
||||
let content = String::from(include_str!("../../assets/dioxus.toml"))
|
||||
.replace("{{project-name}}", &name)
|
||||
.replace("{{default-platform}}", &platform);
|
||||
file.write_all(content.as_bytes())?;
|
||||
log::info!("🚩 Init config file completed.");
|
||||
}
|
||||
Config::FormatPrint {} => {
|
||||
println!("{:#?}", crate::CrateConfig::new()?.dioxus_config);
|
||||
}
|
||||
Config::CustomHtml {} => {
|
||||
let html_path = crate_root.join("index.html");
|
||||
let mut file = File::create(html_path)?;
|
||||
let content = include_str!("../../assets/index.html");
|
||||
file.write_all(content.as_bytes())?;
|
||||
log::info!("🚩 Create custom html file done.");
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
use super::*;
|
||||
use cargo_generate::{GenerateArgs, TemplatePath};
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize, Parser)]
|
||||
#[clap(name = "create")]
|
||||
pub struct Create {
|
||||
/// Template path
|
||||
#[clap(default_value = "gh:dioxuslabs/dioxus-template", long)]
|
||||
template: String,
|
||||
}
|
||||
|
||||
impl Create {
|
||||
pub fn create(self) -> Result<()> {
|
||||
let mut args = GenerateArgs::default();
|
||||
args.template_path = TemplatePath {
|
||||
auto_path: Some(self.template),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let path = cargo_generate::generate(args)?;
|
||||
|
||||
// first run cargo fmt
|
||||
let mut cmd = Command::new("cargo");
|
||||
let cmd = cmd.arg("fmt").current_dir(&path);
|
||||
let output = cmd.output().expect("failed to execute process");
|
||||
if !output.status.success() {
|
||||
log::error!("cargo fmt failed");
|
||||
log::error!("stdout: {}", String::from_utf8_lossy(&output.stdout));
|
||||
log::error!("stderr: {}", String::from_utf8_lossy(&output.stderr));
|
||||
}
|
||||
|
||||
// then format the toml
|
||||
let toml_paths = [path.join("Cargo.toml"), path.join("Dioxus.toml")];
|
||||
for toml_path in &toml_paths {
|
||||
let toml = std::fs::read_to_string(toml_path)?;
|
||||
let mut toml = toml.parse::<toml_edit::Document>().map_err(|e| {
|
||||
anyhow::anyhow!(
|
||||
"failed to parse toml at {}: {}",
|
||||
toml_path.display(),
|
||||
e.to_string()
|
||||
)
|
||||
})?;
|
||||
|
||||
toml.as_table_mut().fmt();
|
||||
|
||||
let as_string = toml.to_string();
|
||||
let new_string = remove_tripple_newlines(&as_string);
|
||||
let mut file = std::fs::File::create(toml_path)?;
|
||||
file.write_all(new_string.as_bytes())?;
|
||||
}
|
||||
|
||||
// remove any tripple newlines from the readme
|
||||
let readme_path = path.join("README.md");
|
||||
let readme = std::fs::read_to_string(&readme_path)?;
|
||||
let new_readme = remove_tripple_newlines(&readme);
|
||||
let mut file = std::fs::File::create(readme_path)?;
|
||||
file.write_all(new_readme.as_bytes())?;
|
||||
|
||||
log::info!("Generated project at {}", path.display());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_tripple_newlines(string: &str) -> String {
|
||||
let mut new_string = String::new();
|
||||
for char in string.chars() {
|
||||
if char == '\n' {
|
||||
if new_string.ends_with("\n\n") {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
new_string.push(char);
|
||||
}
|
||||
new_string
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
pub mod autoformat;
|
||||
pub mod build;
|
||||
pub mod cfg;
|
||||
pub mod clean;
|
||||
pub mod config;
|
||||
pub mod create;
|
||||
pub mod plugin;
|
||||
pub mod serve;
|
||||
pub mod translate;
|
||||
pub mod version;
|
||||
|
||||
use crate::{
|
||||
cfg::{ConfigOptsBuild, ConfigOptsServe},
|
||||
custom_error,
|
||||
error::Result,
|
||||
gen_page, server, CrateConfig, Error,
|
||||
};
|
||||
use clap::{Parser, Subcommand};
|
||||
use html_parser::Dom;
|
||||
use serde::Deserialize;
|
||||
use std::{
|
||||
fs::{remove_dir_all, File},
|
||||
io::{Read, Write},
|
||||
path::PathBuf,
|
||||
process::{Command, Stdio},
|
||||
};
|
||||
|
||||
/// Build, Bundle & Ship Dioxus Apps.
|
||||
#[derive(Parser)]
|
||||
#[clap(name = "dioxus", version)]
|
||||
pub struct Cli {
|
||||
#[clap(subcommand)]
|
||||
pub action: Commands,
|
||||
|
||||
/// Enable verbose logging.
|
||||
#[clap(short)]
|
||||
pub v: bool,
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
pub enum Commands {
|
||||
/// Build the Rust WASM app and all of its assets.
|
||||
Build(build::Build),
|
||||
|
||||
/// Translate some source file into Dioxus code.
|
||||
Translate(translate::Translate),
|
||||
|
||||
/// Build, watch & serve the Rust WASM app and all of its assets.
|
||||
Serve(serve::Serve),
|
||||
|
||||
/// Init a new project for Dioxus.
|
||||
Create(create::Create),
|
||||
|
||||
/// Clean output artifacts.
|
||||
Clean(clean::Clean),
|
||||
|
||||
/// Print the version of this extension
|
||||
#[clap(name = "version")]
|
||||
Version(version::Version),
|
||||
|
||||
/// Format some rsx
|
||||
#[clap(name = "fmt")]
|
||||
Autoformat(autoformat::Autoformat),
|
||||
|
||||
/// Dioxus config file controls.
|
||||
#[clap(subcommand)]
|
||||
Config(config::Config),
|
||||
|
||||
/// Manage plugins for dioxus cli
|
||||
#[clap(subcommand)]
|
||||
Plugin(plugin::Plugin),
|
||||
}
|
||||
|
||||
impl Commands {
|
||||
pub fn to_string(&self) -> String {
|
||||
match self {
|
||||
Commands::Build(_) => "build",
|
||||
Commands::Translate(_) => "translate",
|
||||
Commands::Serve(_) => "serve",
|
||||
Commands::Create(_) => "create",
|
||||
Commands::Clean(_) => "clean",
|
||||
Commands::Config(_) => "config",
|
||||
Commands::Plugin(_) => "plugin",
|
||||
Commands::Version(_) => "version",
|
||||
Commands::Autoformat(_) => "fmt",
|
||||
}
|
||||
.to_string()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
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 {} => {
|
||||
let plugin_dir = crate::plugin::PluginManager::init_plugin_dir();
|
||||
if let Some(v) = 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(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
use super::*;
|
||||
use std::{
|
||||
fs::create_dir_all,
|
||||
io::Write,
|
||||
path::PathBuf,
|
||||
process::{Command, Stdio},
|
||||
};
|
||||
|
||||
/// Run the WASM project on dev-server
|
||||
#[derive(Clone, Debug, Parser)]
|
||||
#[clap(name = "serve")]
|
||||
pub struct Serve {
|
||||
#[clap(flatten)]
|
||||
pub serve: ConfigOptsServe,
|
||||
}
|
||||
|
||||
impl Serve {
|
||||
pub async fn serve(self) -> Result<()> {
|
||||
let mut crate_config = crate::CrateConfig::new()?;
|
||||
|
||||
// change the relase state.
|
||||
crate_config.with_hot_reload(self.serve.hot_reload);
|
||||
crate_config.with_cross_origin_policy(self.serve.cross_origin_policy);
|
||||
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());
|
||||
}
|
||||
|
||||
if self.serve.features.is_some() {
|
||||
crate_config.set_features(self.serve.features.unwrap());
|
||||
}
|
||||
|
||||
// Subdirectories don't work with the server
|
||||
crate_config.dioxus_config.web.app.base_path = None;
|
||||
|
||||
let platform = self.serve.platform.unwrap_or_else(|| {
|
||||
crate_config
|
||||
.dioxus_config
|
||||
.application
|
||||
.default_platform
|
||||
.clone()
|
||||
});
|
||||
|
||||
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(file.to_str().unwrap())
|
||||
.stdout(Stdio::inherit())
|
||||
.output()?;
|
||||
}
|
||||
}
|
||||
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(self.serve.port, crate_config.clone(), self.serve.open).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn regen_dev_page(crate_config: &CrateConfig) -> Result<()> {
|
||||
let serve_html = gen_page(&crate_config.dioxus_config, true);
|
||||
|
||||
let dist_path = crate_config.crate_dir.join(
|
||||
crate_config
|
||||
.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(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
use crate::tools;
|
||||
|
||||
use super::*;
|
||||
|
||||
/// Build the Rust WASM app and all of its assets.
|
||||
#[derive(Clone, Debug, Deserialize, Subcommand)]
|
||||
#[clap(name = "tool")]
|
||||
pub enum Tool {
|
||||
/// Return all dioxus-cli support tools.
|
||||
List {},
|
||||
/// Get default app install path.
|
||||
AppPath {},
|
||||
/// Install a new tool.
|
||||
Add { name: String },
|
||||
}
|
||||
|
||||
impl Tool {
|
||||
pub async fn tool(self) -> Result<()> {
|
||||
match self {
|
||||
Tool::List {} => {
|
||||
for item in tools::tool_list() {
|
||||
if tools::Tool::from_str(item).unwrap().is_installed() {
|
||||
println!("- {item} [installed]");
|
||||
} else {
|
||||
println!("- {item}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Tool::AppPath {} => {
|
||||
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()) {
|
||||
return custom_error!("Tool {name} not found.");
|
||||
}
|
||||
let target_tool = tools::Tool::from_str(&name).unwrap();
|
||||
|
||||
if target_tool.is_installed() {
|
||||
log::warn!("Tool {name} is installed.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
log::info!("Start to download tool package...");
|
||||
if let Err(e) = target_tool.download_package().await {
|
||||
return custom_error!("Tool download failed: {e}");
|
||||
}
|
||||
|
||||
log::info!("Start to install tool package...");
|
||||
if let Err(e) = target_tool.install_package().await {
|
||||
return custom_error!("Tool install failed: {e}");
|
||||
}
|
||||
|
||||
log::info!("Tool {name} installed successfully!");
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
use std::process::exit;
|
||||
|
||||
use dioxus_rsx::{BodyNode, CallBody};
|
||||
|
||||
use super::*;
|
||||
|
||||
/// Build the Rust WASM app and all of its assets.
|
||||
#[derive(Clone, Debug, Parser)]
|
||||
#[clap(name = "translate")]
|
||||
pub struct Translate {
|
||||
/// Activate debug mode
|
||||
// short and long flags (-d, --debug) will be deduced from the field's name
|
||||
#[clap(short, long)]
|
||||
pub component: bool,
|
||||
|
||||
/// Input file
|
||||
#[clap(short, long)]
|
||||
pub file: Option<String>,
|
||||
|
||||
/// Input file
|
||||
#[clap(short, long)]
|
||||
pub raw: Option<String>,
|
||||
|
||||
/// Output file, stdout if not present
|
||||
#[arg(short, long)]
|
||||
pub output: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl Translate {
|
||||
pub fn translate(self) -> Result<()> {
|
||||
// Get the right input for the translation
|
||||
let contents = determine_input(self.file, self.raw)?;
|
||||
|
||||
// Ensure we're loading valid HTML
|
||||
let dom = html_parser::Dom::parse(&contents)?;
|
||||
|
||||
// Convert the HTML to RSX
|
||||
let out = convert_html_to_formatted_rsx(&dom, self.component);
|
||||
|
||||
// Write the output
|
||||
match self.output {
|
||||
Some(output) => std::fs::write(&output, out)?,
|
||||
None => print!("{}", out),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn convert_html_to_formatted_rsx(dom: &Dom, component: bool) -> String {
|
||||
let callbody = rsx_rosetta::rsx_from_html(&dom);
|
||||
|
||||
match component {
|
||||
true => write_callbody_with_icon_section(callbody),
|
||||
false => dioxus_autofmt::write_block_out(callbody).unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
fn write_callbody_with_icon_section(mut callbody: CallBody) -> String {
|
||||
let mut svgs = vec![];
|
||||
|
||||
rsx_rosetta::collect_svgs(&mut callbody.roots, &mut svgs);
|
||||
|
||||
let mut out = write_component_body(dioxus_autofmt::write_block_out(callbody).unwrap());
|
||||
|
||||
if !svgs.is_empty() {
|
||||
write_svg_section(&mut out, svgs);
|
||||
}
|
||||
|
||||
out
|
||||
}
|
||||
|
||||
fn write_component_body(raw: String) -> String {
|
||||
let mut out = String::from("fn component(cx: Scope) -> Element {\n cx.render(rsx! {");
|
||||
indent_and_write(&raw, 1, &mut out);
|
||||
out.push_str(" })\n}");
|
||||
out
|
||||
}
|
||||
|
||||
fn write_svg_section(out: &mut String, svgs: Vec<BodyNode>) {
|
||||
out.push_str("\n\nmod icons {");
|
||||
out.push_str("\n use super::*;");
|
||||
for (idx, icon) in svgs.into_iter().enumerate() {
|
||||
let raw = dioxus_autofmt::write_block_out(CallBody { roots: vec![icon] }).unwrap();
|
||||
out.push_str("\n\n pub fn icon_");
|
||||
out.push_str(&idx.to_string());
|
||||
out.push_str("(cx: Scope) -> Element {\n cx.render(rsx! {");
|
||||
indent_and_write(&raw, 2, out);
|
||||
out.push_str(" })\n }");
|
||||
}
|
||||
|
||||
out.push_str("\n}");
|
||||
}
|
||||
|
||||
fn indent_and_write(raw: &str, idx: usize, out: &mut String) {
|
||||
for line in raw.lines() {
|
||||
for _ in 0..idx {
|
||||
out.push_str(" ");
|
||||
}
|
||||
out.push_str(line);
|
||||
out.push('\n');
|
||||
}
|
||||
}
|
||||
|
||||
fn determine_input(file: Option<String>, raw: Option<String>) -> Result<String> {
|
||||
// Make sure not both are specified
|
||||
if file.is_some() && raw.is_some() {
|
||||
log::error!("Only one of --file or --raw should be specified.");
|
||||
exit(0);
|
||||
}
|
||||
|
||||
if let Some(raw) = raw {
|
||||
return Ok(raw);
|
||||
}
|
||||
|
||||
if let Some(file) = file {
|
||||
return Ok(std::fs::read_to_string(&file)?);
|
||||
}
|
||||
|
||||
// If neither exist, we try to read from stdin
|
||||
if atty::is(atty::Stream::Stdin) {
|
||||
return custom_error!("No input file, source, or stdin to translate from.");
|
||||
}
|
||||
|
||||
let mut buffer = String::new();
|
||||
std::io::stdin().read_to_string(&mut buffer).unwrap();
|
||||
|
||||
Ok(buffer.trim().to_string())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generates_svgs() {
|
||||
let st = include_str!("../../../tests/svg.html");
|
||||
|
||||
let out = convert_html_to_formatted_rsx(&html_parser::Dom::parse(st).unwrap(), true);
|
||||
|
||||
println!("{}", out);
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
use super::*;
|
||||
|
||||
/// Build the Rust WASM app and all of its assets.
|
||||
#[derive(Clone, Debug, Parser)]
|
||||
#[clap(name = "version")]
|
||||
pub struct Version {}
|
||||
|
||||
impl Version {
|
||||
pub fn version(self) -> VersionInfo {
|
||||
version()
|
||||
}
|
||||
}
|
||||
|
||||
use std::fmt;
|
||||
|
||||
/// Information about the git repository where rust-analyzer was built from.
|
||||
pub struct CommitInfo {
|
||||
pub short_commit_hash: &'static str,
|
||||
pub commit_hash: &'static str,
|
||||
pub commit_date: &'static str,
|
||||
}
|
||||
|
||||
/// Cargo's version.
|
||||
pub struct VersionInfo {
|
||||
/// rust-analyzer's version, such as "1.57.0", "1.58.0-beta.1", "1.59.0-nightly", etc.
|
||||
pub version: &'static str,
|
||||
|
||||
/// The release channel we were built for (stable/beta/nightly/dev).
|
||||
///
|
||||
/// `None` if not built via rustbuild.
|
||||
pub release_channel: Option<&'static str>,
|
||||
|
||||
/// Information about the Git repository we may have been built from.
|
||||
///
|
||||
/// `None` if not built from a git repo.
|
||||
pub commit_info: Option<CommitInfo>,
|
||||
}
|
||||
|
||||
impl fmt::Display for VersionInfo {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.version)?;
|
||||
|
||||
if let Some(ci) = &self.commit_info {
|
||||
write!(f, " ({} {})", ci.short_commit_hash, ci.commit_date)?;
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns information about cargo's version.
|
||||
pub const fn version() -> VersionInfo {
|
||||
let version = match option_env!("CFG_RELEASE") {
|
||||
Some(x) => x,
|
||||
None => "0.0.0",
|
||||
};
|
||||
|
||||
let release_channel = option_env!("CFG_RELEASE_CHANNEL");
|
||||
let commit_info = match (
|
||||
option_env!("RA_COMMIT_SHORT_HASH"),
|
||||
option_env!("RA_COMMIT_HASH"),
|
||||
option_env!("RA_COMMIT_DATE"),
|
||||
) {
|
||||
(Some(short_commit_hash), Some(commit_hash), Some(commit_date)) => Some(CommitInfo {
|
||||
short_commit_hash,
|
||||
commit_hash,
|
||||
commit_date,
|
||||
}),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
VersionInfo {
|
||||
version,
|
||||
release_channel,
|
||||
commit_info,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,264 @@
|
||||
use crate::error::Result;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{collections::HashMap, path::PathBuf};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
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 {
|
||||
pub fn load() -> crate::error::Result<Option<DioxusConfig>> {
|
||||
let Ok(crate_dir) = crate::cargo::crate_root() else { return Ok(None); };
|
||||
|
||||
// we support either `Dioxus.toml` or `Cargo.toml`
|
||||
let Some(dioxus_conf_file) = acquire_dioxus_toml(crate_dir) else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
toml::from_str::<DioxusConfig>(&std::fs::read_to_string(dioxus_conf_file)?)
|
||||
.map_err(|_| crate::Error::Unique("Dioxus.toml parse failed".into()))
|
||||
.map(Some)
|
||||
}
|
||||
}
|
||||
|
||||
fn acquire_dioxus_toml(dir: PathBuf) -> Option<PathBuf> {
|
||||
// prefer uppercase
|
||||
if dir.join("Dioxus.toml").is_file() {
|
||||
return Some(dir.join("Dioxus.toml"));
|
||||
}
|
||||
|
||||
// lowercase is fine too
|
||||
if dir.join("dioxus.toml").is_file() {
|
||||
return Some(dir.join("Dioxus.toml"));
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
impl Default for DioxusConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
application: ApplicationConfig {
|
||||
name: "dioxus".into(),
|
||||
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: WebAppConfig {
|
||||
title: Some("dioxus | ⛺".into()),
|
||||
base_path: None,
|
||||
},
|
||||
proxy: Some(vec![]),
|
||||
watcher: WebWatcherConfig {
|
||||
watch_path: Some(vec![PathBuf::from("src")]),
|
||||
reload_html: Some(false),
|
||||
index_on_404: Some(true),
|
||||
},
|
||||
resource: WebResourceConfig {
|
||||
dev: WebDevResourceConfig {
|
||||
style: Some(vec![]),
|
||||
script: Some(vec![]),
|
||||
},
|
||||
style: Some(vec![]),
|
||||
script: Some(vec![]),
|
||||
},
|
||||
},
|
||||
plugin: toml::Value::Table(toml::map::Map::new()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ApplicationConfig {
|
||||
pub name: String,
|
||||
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: WebAppConfig,
|
||||
pub proxy: Option<Vec<WebProxyConfig>>,
|
||||
pub watcher: WebWatcherConfig,
|
||||
pub resource: WebResourceConfig,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct WebAppConfig {
|
||||
pub title: Option<String>,
|
||||
pub base_path: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct WebProxyConfig {
|
||||
pub backend: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
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 WebResourceConfig {
|
||||
pub dev: WebDevResourceConfig,
|
||||
pub style: Option<Vec<PathBuf>>,
|
||||
pub script: Option<Vec<PathBuf>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct WebDevResourceConfig {
|
||||
pub style: Option<Vec<PathBuf>>,
|
||||
pub script: Option<Vec<PathBuf>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CrateConfig {
|
||||
pub out_dir: PathBuf,
|
||||
pub crate_dir: PathBuf,
|
||||
pub workspace_dir: PathBuf,
|
||||
pub target_dir: PathBuf,
|
||||
pub asset_dir: PathBuf,
|
||||
pub manifest: cargo_toml::Manifest<cargo_toml::Value>,
|
||||
pub executable: ExecutableType,
|
||||
pub dioxus_config: DioxusConfig,
|
||||
pub release: bool,
|
||||
pub hot_reload: bool,
|
||||
pub cross_origin_policy: bool,
|
||||
pub verbose: bool,
|
||||
pub custom_profile: Option<String>,
|
||||
pub features: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ExecutableType {
|
||||
Binary(String),
|
||||
Lib(String),
|
||||
Example(String),
|
||||
}
|
||||
|
||||
impl CrateConfig {
|
||||
pub fn new() -> Result<Self> {
|
||||
let dioxus_config = DioxusConfig::load()?.unwrap_or_default();
|
||||
|
||||
let crate_dir = if let Some(package) = &dioxus_config.application.sub_package {
|
||||
crate::cargo::crate_root()?.join(package)
|
||||
} else {
|
||||
crate::cargo::crate_root()?
|
||||
};
|
||||
let meta = crate::cargo::Metadata::get()?;
|
||||
let workspace_dir = meta.workspace_root;
|
||||
let target_dir = meta.target_directory;
|
||||
|
||||
let out_dir = match dioxus_config.application.out_dir {
|
||||
Some(ref v) => crate_dir.join(v),
|
||||
None => crate_dir.join("dist"),
|
||||
};
|
||||
|
||||
let cargo_def = &crate_dir.join("Cargo.toml");
|
||||
|
||||
let asset_dir = match dioxus_config.application.asset_dir {
|
||||
Some(ref v) => crate_dir.join(v),
|
||||
None => crate_dir.join("public"),
|
||||
};
|
||||
|
||||
let manifest = cargo_toml::Manifest::from_path(&cargo_def).unwrap();
|
||||
|
||||
let output_filename = {
|
||||
match &manifest.package.as_ref().unwrap().default_run {
|
||||
Some(default_run_target) => {
|
||||
default_run_target.to_owned()
|
||||
},
|
||||
None => {
|
||||
manifest.bin.iter().find(|b| b.name == manifest.package.as_ref().map(|pkg| pkg.name.clone()))
|
||||
.or(manifest.bin.iter().find(|b| b.path == Some("src/main.rs".to_owned())))
|
||||
.or(manifest.bin.first())
|
||||
.or(manifest.lib.as_ref())
|
||||
.and_then(|prod| prod.name.clone())
|
||||
.expect("No executable or library found from cargo metadata.")
|
||||
}
|
||||
}
|
||||
};
|
||||
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,
|
||||
crate_dir,
|
||||
workspace_dir,
|
||||
target_dir,
|
||||
asset_dir,
|
||||
manifest,
|
||||
executable,
|
||||
release,
|
||||
dioxus_config,
|
||||
hot_reload,
|
||||
cross_origin_policy: false,
|
||||
custom_profile,
|
||||
features,
|
||||
verbose,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn as_example(&mut self, example_name: String) -> &mut Self {
|
||||
self.executable = ExecutableType::Example(example_name);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_release(&mut self, release: bool) -> &mut Self {
|
||||
self.release = release;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_hot_reload(&mut self, hot_reload: bool) -> &mut Self {
|
||||
self.hot_reload = hot_reload;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_cross_origin_policy(&mut self, cross_origin_policy: bool) -> &mut Self {
|
||||
self.cross_origin_policy = cross_origin_policy;
|
||||
self
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
use thiserror::Error as ThisError;
|
||||
|
||||
pub type Result<T, E = Error> = std::result::Result<T, E>;
|
||||
|
||||
#[derive(ThisError, Debug)]
|
||||
pub enum Error {
|
||||
/// Used when errors need to propogate but are too unique to be typed
|
||||
#[error("{0}")]
|
||||
Unique(String),
|
||||
|
||||
#[error("I/O Error: {0}")]
|
||||
IO(#[from] std::io::Error),
|
||||
|
||||
#[error("Format Error: {0}")]
|
||||
FormatError(#[from] std::fmt::Error),
|
||||
|
||||
#[error("Format failed: {0}")]
|
||||
ParseError(String),
|
||||
|
||||
#[error("Runtime Error: {0}")]
|
||||
RuntimeError(String),
|
||||
|
||||
#[error("Failed to write error")]
|
||||
FailedToWrite,
|
||||
|
||||
#[error("Build Failed: {0}")]
|
||||
BuildFailed(String),
|
||||
|
||||
#[error("Cargo Error: {0}")]
|
||||
CargoError(String),
|
||||
|
||||
#[error("{0}")]
|
||||
CustomError(String),
|
||||
|
||||
#[error("Invalid proxy URL: {0}")]
|
||||
InvalidProxy(#[from] hyper::http::uri::InvalidUri),
|
||||
|
||||
#[error("Error proxying request: {0}")]
|
||||
ProxyRequestError(hyper::Error),
|
||||
|
||||
#[error(transparent)]
|
||||
Other(#[from] anyhow::Error),
|
||||
}
|
||||
|
||||
impl From<&str> for Error {
|
||||
fn from(s: &str) -> Self {
|
||||
Error::Unique(s.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for Error {
|
||||
fn from(s: String) -> Self {
|
||||
Error::Unique(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<html_parser::Error> for Error {
|
||||
fn from(e: html_parser::Error) -> Self {
|
||||
Self::ParseError(e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<hyper::Error> for Error {
|
||||
fn from(e: hyper::Error) -> Self {
|
||||
Self::RuntimeError(e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! custom_error {
|
||||
($msg:literal $(,)?) => {
|
||||
Err(Error::CustomError(format!($msg)))
|
||||
};
|
||||
($err:expr $(,)?) => {
|
||||
Err(Error::from($err))
|
||||
};
|
||||
($fmt:expr, $($arg:tt)*) => {
|
||||
Err(Error::CustomError(format!($fmt, $($arg)*)))
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
pub const DIOXUS_CLI_VERSION: &str = "0.1.5";
|
||||
|
||||
pub mod builder;
|
||||
pub mod server;
|
||||
pub mod tools;
|
||||
|
||||
pub use builder::*;
|
||||
|
||||
pub mod cargo;
|
||||
pub use cargo::*;
|
||||
|
||||
pub mod cli;
|
||||
pub use cli::*;
|
||||
|
||||
pub mod config;
|
||||
pub use config::*;
|
||||
|
||||
pub mod error;
|
||||
pub use error::*;
|
||||
|
||||
pub mod logging;
|
||||
pub use logging::*;
|
||||
|
||||
pub mod plugin;
|
||||
@@ -0,0 +1,35 @@
|
||||
use fern::colors::{Color, ColoredLevelConfig};
|
||||
|
||||
pub fn set_up_logging() {
|
||||
// configure colors for the whole line
|
||||
let colors_line = ColoredLevelConfig::new()
|
||||
.error(Color::Red)
|
||||
.warn(Color::Yellow)
|
||||
// we actually don't need to specify the color for debug and info, they are white by default
|
||||
.info(Color::White)
|
||||
.debug(Color::White)
|
||||
// depending on the terminals color scheme, this is the same as the background color
|
||||
.trace(Color::BrightBlack);
|
||||
|
||||
// configure colors for the name of the level.
|
||||
// since almost all of them are the same as the color for the whole line, we
|
||||
// just clone `colors_line` and overwrite our changes
|
||||
let colors_level = colors_line.info(Color::Green);
|
||||
// here we set up our fern Dispatch
|
||||
fern::Dispatch::new()
|
||||
.format(move |out, message, record| {
|
||||
out.finish(format_args!(
|
||||
"{color_line}[{level}{color_line}] {message}\x1B[0m",
|
||||
color_line = format_args!(
|
||||
"\x1B[{}m",
|
||||
colors_line.get_color(&record.level()).to_fg_str()
|
||||
),
|
||||
level = colors_level.color(record.level()),
|
||||
message = message,
|
||||
));
|
||||
})
|
||||
.level(log::LevelFilter::Info)
|
||||
.chain(std::io::stdout())
|
||||
.apply()
|
||||
.unwrap();
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
use anyhow::anyhow;
|
||||
use clap::Parser;
|
||||
use dioxus_cli::{plugin::PluginManager, *};
|
||||
use Commands::*;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
let args = Cli::parse();
|
||||
|
||||
set_up_logging();
|
||||
|
||||
let dioxus_config = DioxusConfig::load()
|
||||
.map_err(|e| anyhow!("Failed to load `Dioxus.toml` because: {e}"))?
|
||||
.unwrap_or_else(|| {
|
||||
log::warn!("You appear to be creating a Dioxus project from scratch; we will use the default config");
|
||||
DioxusConfig::default()
|
||||
});
|
||||
|
||||
PluginManager::init(dioxus_config.plugin)
|
||||
.map_err(|e| anyhow!("🚫 Plugin system initialization failed: {e}"))?;
|
||||
|
||||
match args.action {
|
||||
Translate(opts) => opts
|
||||
.translate()
|
||||
.map_err(|e| anyhow!("🚫 Translation of HTML into RSX failed: {}", e)),
|
||||
|
||||
Build(opts) => opts
|
||||
.build()
|
||||
.map_err(|e| anyhow!("🚫 Building project failed: {}", e)),
|
||||
|
||||
Clean(opts) => opts
|
||||
.clean()
|
||||
.map_err(|e| anyhow!("🚫 Cleaning project failed: {}", e)),
|
||||
|
||||
Serve(opts) => opts
|
||||
.serve()
|
||||
.await
|
||||
.map_err(|e| anyhow!("🚫 Serving project failed: {}", e)),
|
||||
|
||||
Create(opts) => opts
|
||||
.create()
|
||||
.map_err(|e| anyhow!("🚫 Creating new project failed: {}", e)),
|
||||
|
||||
Config(opts) => opts
|
||||
.config()
|
||||
.map_err(|e| anyhow!("🚫 Configuring new project failed: {}", e)),
|
||||
|
||||
Plugin(opts) => opts
|
||||
.plugin()
|
||||
.await
|
||||
.map_err(|e| anyhow!("🚫 Error with plugin: {}", e)),
|
||||
|
||||
Autoformat(opts) => opts
|
||||
.autoformat()
|
||||
.await
|
||||
.map_err(|e| anyhow!("🚫 Error autoformatting RSX: {}", e)),
|
||||
|
||||
Version(opt) => {
|
||||
let version = opt.version();
|
||||
println!("{}", version);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
use std::process::{Command, Stdio};
|
||||
|
||||
use mlua::{FromLua, UserData};
|
||||
|
||||
enum StdioFromString {
|
||||
Inherit,
|
||||
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() {
|
||||
"inherit" => Self::Inherit,
|
||||
"piped" => Self::Piped,
|
||||
"null" => Self::Null,
|
||||
_ => Self::Inherit,
|
||||
});
|
||||
}
|
||||
Ok(Self::Inherit)
|
||||
}
|
||||
}
|
||||
impl StdioFromString {
|
||||
pub fn to_stdio(self) -> Stdio {
|
||||
match self {
|
||||
StdioFromString::Inherit => 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,85 @@
|
||||
use std::{
|
||||
fs::{create_dir, create_dir_all, remove_dir_all, File},
|
||||
io::{Read, Write},
|
||||
path::PathBuf,
|
||||
};
|
||||
|
||||
use crate::tools::extract_zip;
|
||||
use flate2::read::GzDecoder;
|
||||
use mlua::UserData;
|
||||
use tar::Archive;
|
||||
|
||||
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,333 @@
|
||||
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::{
|
||||
command::PluginCommander, dirs::PluginDirs, fs::PluginFileSystem, log::PluginLogger,
|
||||
network::PluginNetwork, os::PluginOS, path::PluginPath, PluginInfo,
|
||||
},
|
||||
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";
|
||||
if let Err(err) = clone_repo(&plugin_path, url) {
|
||||
log::error!("Failed to init plugin dir, error caused by {}. ", err);
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,752 @@
|
||||
use crate::{builder, plugin::PluginManager, serve::Serve, BuildResult, CrateConfig, Result};
|
||||
use axum::{
|
||||
body::{Full, HttpBody},
|
||||
extract::{ws::Message, Extension, TypedHeader, WebSocketUpgrade},
|
||||
http::{
|
||||
header::{HeaderName, HeaderValue},
|
||||
Method, Response, StatusCode,
|
||||
},
|
||||
response::IntoResponse,
|
||||
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::{
|
||||
net::UdpSocket,
|
||||
path::PathBuf,
|
||||
process::Command,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
use tokio::sync::broadcast;
|
||||
use tower::ServiceBuilder;
|
||||
use tower_http::services::fs::{ServeDir, ServeFileSystemResponseBody};
|
||||
use tower_http::{
|
||||
cors::{Any, CorsLayer},
|
||||
ServiceBuilderExt,
|
||||
};
|
||||
mod proxy;
|
||||
|
||||
pub struct BuildManager {
|
||||
config: CrateConfig,
|
||||
reload_tx: broadcast::Sender<()>,
|
||||
}
|
||||
|
||||
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, start_browser: bool) -> 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);
|
||||
});
|
||||
|
||||
let ip = get_ip().unwrap_or(String::from("0.0.0.0"));
|
||||
|
||||
if config.hot_reload {
|
||||
startup_hot_reload(ip, port, config, start_browser).await?
|
||||
} else {
|
||||
startup_default(ip, port, config, start_browser).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(
|
||||
ip: String,
|
||||
port: u16,
|
||||
config: CrateConfig,
|
||||
start_browser: bool,
|
||||
) -> 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 FileMapBuildResult { map, errors } =
|
||||
FileMap::<HtmlCtx>::create(config.crate_dir.clone()).unwrap();
|
||||
for err in errors {
|
||||
log::error!("{}", err);
|
||||
}
|
||||
let file_map = Arc::new(Mutex::new(map));
|
||||
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 crate_dir = config.crate_dir.clone();
|
||||
let ws_reload_state = Arc::new(WsReloadState {
|
||||
update: reload_tx.clone(),
|
||||
});
|
||||
|
||||
// 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 watcher_ip = ip.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 this is not a rust file, rebuild the whole project
|
||||
if path.extension().and_then(|p| p.to_str()) != Some("rs") {
|
||||
match build_manager.rebuild() {
|
||||
Ok(res) => {
|
||||
print_console_info(
|
||||
&watcher_ip,
|
||||
port,
|
||||
&config,
|
||||
PrettierOptions {
|
||||
changed: evt.paths,
|
||||
warnings: res.warnings,
|
||||
elapsed_time: res.elapsed_time,
|
||||
},
|
||||
);
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!("{}", err);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
// find changes to the rsx in the file
|
||||
let mut map = file_map.lock().unwrap();
|
||||
|
||||
match map.update_rsx(&path, &crate_dir) {
|
||||
Ok(UpdateResult::UpdatedRsx(msgs)) => {
|
||||
messages.extend(msgs);
|
||||
}
|
||||
Ok(UpdateResult::NeedsRebuild) => {
|
||||
match build_manager.rebuild() {
|
||||
Ok(res) => {
|
||||
print_console_info(
|
||||
&watcher_ip,
|
||||
port,
|
||||
&config,
|
||||
PrettierOptions {
|
||||
changed: evt.paths,
|
||||
warnings: res.warnings,
|
||||
elapsed_time: res.elapsed_time,
|
||||
},
|
||||
);
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!("{}", err);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!("{}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
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 {
|
||||
if let Err(err) = watcher.watch(
|
||||
&config.crate_dir.join(&sub_path),
|
||||
notify::RecursiveMode::Recursive,
|
||||
) {
|
||||
log::error!("error watching {sub_path:?}: \n{}", err);
|
||||
}
|
||||
}
|
||||
|
||||
// start serve dev-server at 0.0.0.0:8080
|
||||
print_console_info(
|
||||
&ip,
|
||||
port,
|
||||
&config,
|
||||
PrettierOptions {
|
||||
changed: vec![],
|
||||
warnings: first_build_result.warnings,
|
||||
elapsed_time: first_build_result.elapsed_time,
|
||||
},
|
||||
);
|
||||
|
||||
let cors = CorsLayer::new()
|
||||
// allow `GET` and `POST` when accessing the resource
|
||||
.allow_methods([Method::GET, Method::POST])
|
||||
// allow requests from any origin
|
||||
.allow_origin(Any)
|
||||
.allow_headers(Any);
|
||||
|
||||
let (coep, coop) = if config.cross_origin_policy {
|
||||
(
|
||||
HeaderValue::from_static("require-corp"),
|
||||
HeaderValue::from_static("same-origin"),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
HeaderValue::from_static("unsafe-none"),
|
||||
HeaderValue::from_static("unsafe-none"),
|
||||
)
|
||||
};
|
||||
|
||||
let file_service_config = config.clone();
|
||||
let file_service = ServiceBuilder::new()
|
||||
.override_response_header(
|
||||
HeaderName::from_static("cross-origin-embedder-policy"),
|
||||
coep,
|
||||
)
|
||||
.override_response_header(HeaderName::from_static("cross-origin-opener-policy"), coop)
|
||||
.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 mut router = Router::new().route("/_dioxus/ws", get(ws_handler));
|
||||
for proxy_config in config.dioxus_config.web.proxy.unwrap_or_default() {
|
||||
router = proxy::add_proxy(router, &proxy_config)?;
|
||||
}
|
||||
router = router.fallback(get_service(file_service).handle_error(
|
||||
|error: std::io::Error| async move {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Unhandled internal error: {}", error),
|
||||
)
|
||||
},
|
||||
));
|
||||
|
||||
let router = router
|
||||
.route("/_dioxus/hot_reload", get(hot_reload_handler))
|
||||
.layer(cors)
|
||||
.layer(Extension(ws_reload_state))
|
||||
.layer(Extension(hot_reload_state));
|
||||
|
||||
let addr = format!("0.0.0.0:{}", port).parse().unwrap();
|
||||
|
||||
let server = axum::Server::bind(&addr).serve(router.into_make_service());
|
||||
|
||||
if start_browser {
|
||||
let _ = open::that(format!("http://{}", addr));
|
||||
}
|
||||
|
||||
server.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn startup_default(
|
||||
ip: String,
|
||||
port: u16,
|
||||
config: CrateConfig,
|
||||
start_browser: bool,
|
||||
) -> 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 watcher_ip = ip.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(
|
||||
&watcher_ip,
|
||||
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(
|
||||
&ip,
|
||||
port,
|
||||
&config,
|
||||
PrettierOptions {
|
||||
changed: vec![],
|
||||
warnings: first_build_result.warnings,
|
||||
elapsed_time: first_build_result.elapsed_time,
|
||||
},
|
||||
);
|
||||
|
||||
PluginManager::on_serve_start(&config)?;
|
||||
|
||||
let cors = CorsLayer::new()
|
||||
// allow `GET` and `POST` when accessing the resource
|
||||
.allow_methods([Method::GET, Method::POST])
|
||||
// allow requests from any origin
|
||||
.allow_origin(Any)
|
||||
.allow_headers(Any);
|
||||
|
||||
let (coep, coop) = if config.cross_origin_policy {
|
||||
(
|
||||
HeaderValue::from_static("require-corp"),
|
||||
HeaderValue::from_static("same-origin"),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
HeaderValue::from_static("unsafe-none"),
|
||||
HeaderValue::from_static("unsafe-none"),
|
||||
)
|
||||
};
|
||||
|
||||
let file_service_config = config.clone();
|
||||
let file_service = ServiceBuilder::new()
|
||||
.override_response_header(
|
||||
HeaderName::from_static("cross-origin-embedder-policy"),
|
||||
coep,
|
||||
)
|
||||
.override_response_header(HeaderName::from_static("cross-origin-opener-policy"), coop)
|
||||
.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 mut router = Router::new().route("/_dioxus/ws", get(ws_handler));
|
||||
for proxy_config in config.dioxus_config.web.proxy.unwrap_or_default() {
|
||||
router = proxy::add_proxy(router, &proxy_config)?;
|
||||
}
|
||||
router = router
|
||||
.fallback(
|
||||
get_service(file_service).handle_error(|error: std::io::Error| async move {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Unhandled internal error: {}", error),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.layer(cors)
|
||||
.layer(Extension(ws_reload_state));
|
||||
|
||||
let addr = format!("0.0.0.0:{}", port).parse().unwrap();
|
||||
let server = axum::Server::bind(&addr).serve(router.into_make_service());
|
||||
|
||||
if start_browser {
|
||||
let _ = open::that(format!("http://{}", addr));
|
||||
}
|
||||
|
||||
server.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct PrettierOptions {
|
||||
changed: Vec<PathBuf>,
|
||||
warnings: Vec<Diagnostic>,
|
||||
elapsed_time: u128,
|
||||
}
|
||||
|
||||
fn print_console_info(ip: &String, 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"
|
||||
};
|
||||
|
||||
let proxies = config.dioxus_config.web.proxy.as_ref();
|
||||
|
||||
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://{}:{}/", ip, port).blue()
|
||||
);
|
||||
println!("");
|
||||
println!("\t> Profile : {}", profile.green());
|
||||
println!("\t> Hot Reload : {}", hot_reload.cyan());
|
||||
if let Some(proxies) = proxies {
|
||||
if !proxies.is_empty() {
|
||||
println!("\t> Proxies :");
|
||||
for proxy in proxies {
|
||||
println!("\t\t- {}", proxy.backend.blue());
|
||||
}
|
||||
}
|
||||
}
|
||||
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<WsReloadState>>,
|
||||
) -> impl IntoResponse {
|
||||
ws.on_upgrade(|mut socket| async move {
|
||||
let mut rx = state.update.subscribe();
|
||||
let reload_watcher = tokio::spawn(async move {
|
||||
loop {
|
||||
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();
|
||||
}
|
||||
});
|
||||
|
||||
reload_watcher.await.unwrap();
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
use crate::{Result, WebProxyConfig};
|
||||
|
||||
use anyhow::Context;
|
||||
use axum::{http::StatusCode, routing::any, Router};
|
||||
use hyper::{Request, Response, Uri};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct ProxyClient {
|
||||
inner: hyper::Client<hyper_rustls::HttpsConnector<hyper::client::HttpConnector>>,
|
||||
url: Uri,
|
||||
}
|
||||
|
||||
impl ProxyClient {
|
||||
fn new(url: Uri) -> Self {
|
||||
let https = hyper_rustls::HttpsConnectorBuilder::new()
|
||||
.with_native_roots()
|
||||
.https_or_http()
|
||||
.enable_http1()
|
||||
.build();
|
||||
Self {
|
||||
inner: hyper::Client::builder().build(https),
|
||||
url,
|
||||
}
|
||||
}
|
||||
|
||||
async fn send(
|
||||
&self,
|
||||
mut req: Request<hyper::body::Body>,
|
||||
) -> Result<Response<hyper::body::Body>> {
|
||||
let mut uri_parts = req.uri().clone().into_parts();
|
||||
uri_parts.authority = self.url.authority().cloned();
|
||||
uri_parts.scheme = self.url.scheme().cloned();
|
||||
*req.uri_mut() = Uri::from_parts(uri_parts).context("Invalid URI parts")?;
|
||||
self.inner
|
||||
.request(req)
|
||||
.await
|
||||
.map_err(crate::error::Error::ProxyRequestError)
|
||||
}
|
||||
}
|
||||
|
||||
/// Add routes to the router handling the specified proxy config.
|
||||
///
|
||||
/// We will proxy requests directed at either:
|
||||
///
|
||||
/// - the exact path of the proxy config's backend URL, e.g. /api
|
||||
/// - the exact path with a trailing slash, e.g. /api/
|
||||
/// - any subpath of the backend URL, e.g. /api/foo/bar
|
||||
pub fn add_proxy(mut router: Router, proxy: &WebProxyConfig) -> Result<Router> {
|
||||
let url: Uri = proxy.backend.parse()?;
|
||||
let path = url.path().to_string();
|
||||
let client = ProxyClient::new(url);
|
||||
|
||||
// We also match everything after the path using a wildcard matcher.
|
||||
let wildcard_client = client.clone();
|
||||
|
||||
router = router.route(
|
||||
// Always remove trailing /'s so that the exact route
|
||||
// matches.
|
||||
path.trim_end_matches('/'),
|
||||
any(move |req| async move {
|
||||
client
|
||||
.send(req)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))
|
||||
}),
|
||||
);
|
||||
|
||||
// Wildcard match anything else _after_ the backend URL's path.
|
||||
// Note that we know `path` ends with a trailing `/` in this branch,
|
||||
// so `wildcard` will look like `http://localhost/api/*proxywildcard`.
|
||||
let wildcard = format!("{}/*proxywildcard", path.trim_end_matches('/'));
|
||||
router = router.route(
|
||||
&wildcard,
|
||||
any(move |req| async move {
|
||||
wildcard_client
|
||||
.send(req)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))
|
||||
}),
|
||||
);
|
||||
Ok(router)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
|
||||
use super::*;
|
||||
|
||||
use axum::{extract::Path, Router};
|
||||
|
||||
fn setup_servers(
|
||||
mut config: WebProxyConfig,
|
||||
) -> (
|
||||
tokio::task::JoinHandle<()>,
|
||||
tokio::task::JoinHandle<()>,
|
||||
String,
|
||||
) {
|
||||
let backend_router = Router::new().route(
|
||||
"/*path",
|
||||
any(|path: Path<String>| async move { format!("backend: {}", path.0) }),
|
||||
);
|
||||
let backend_server = axum::Server::bind(&"127.0.0.1:0".parse().unwrap())
|
||||
.serve(backend_router.into_make_service());
|
||||
let backend_addr = backend_server.local_addr();
|
||||
let backend_handle = tokio::spawn(async move { backend_server.await.unwrap() });
|
||||
config.backend = format!("http://{}{}", backend_addr, config.backend);
|
||||
let router = super::add_proxy(Router::new(), &config);
|
||||
let server = axum::Server::bind(&"127.0.0.1:0".parse().unwrap())
|
||||
.serve(router.unwrap().into_make_service());
|
||||
let server_addr = server.local_addr();
|
||||
let server_handle = tokio::spawn(async move { server.await.unwrap() });
|
||||
(backend_handle, server_handle, server_addr.to_string())
|
||||
}
|
||||
|
||||
async fn test_proxy_requests(path: String) {
|
||||
let config = WebProxyConfig {
|
||||
// Normally this would be an absolute URL including scheme/host/port,
|
||||
// but in these tests we need to let the OS choose the port so tests
|
||||
// don't conflict, so we'll concatenate the final address and this
|
||||
// path together.
|
||||
// So in day to day usage, use `http://localhost:8000/api` instead!
|
||||
backend: path,
|
||||
};
|
||||
let (backend_handle, server_handle, server_addr) = setup_servers(config);
|
||||
let resp = hyper::Client::new()
|
||||
.get(format!("http://{}/api", server_addr).parse().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
assert_eq!(
|
||||
hyper::body::to_bytes(resp.into_body()).await.unwrap(),
|
||||
"backend: /api"
|
||||
);
|
||||
|
||||
let resp = hyper::Client::new()
|
||||
.get(format!("http://{}/api/", server_addr).parse().unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
assert_eq!(
|
||||
hyper::body::to_bytes(resp.into_body()).await.unwrap(),
|
||||
"backend: /api/"
|
||||
);
|
||||
|
||||
let resp = hyper::Client::new()
|
||||
.get(
|
||||
format!("http://{}/api/subpath", server_addr)
|
||||
.parse()
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
assert_eq!(
|
||||
hyper::body::to_bytes(resp.into_body()).await.unwrap(),
|
||||
"backend: /api/subpath"
|
||||
);
|
||||
backend_handle.abort();
|
||||
server_handle.abort();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn add_proxy() {
|
||||
test_proxy_requests("/api".to_string()).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn add_proxy_trailing_slash() {
|
||||
test_proxy_requests("/api/".to_string()).await;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,349 @@
|
||||
use std::{
|
||||
fs::{create_dir_all, File},
|
||||
io::{ErrorKind, Read, Write},
|
||||
path::{Path, PathBuf},
|
||||
process::Command,
|
||||
};
|
||||
|
||||
use anyhow::Context;
|
||||
use flate2::read::GzDecoder;
|
||||
use futures::StreamExt;
|
||||
use tar::Archive;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum Tool {
|
||||
Binaryen,
|
||||
Sass,
|
||||
Tailwind,
|
||||
}
|
||||
|
||||
// pub fn tool_list() -> Vec<&'static str> {
|
||||
// vec!["binaryen", "sass", "tailwindcss"]
|
||||
// }
|
||||
|
||||
pub fn app_path() -> PathBuf {
|
||||
let data_local = dirs::data_local_dir().unwrap();
|
||||
let dioxus_dir = data_local.join("dioxus");
|
||||
if !dioxus_dir.is_dir() {
|
||||
create_dir_all(&dioxus_dir).unwrap();
|
||||
}
|
||||
dioxus_dir
|
||||
}
|
||||
|
||||
pub fn temp_path() -> PathBuf {
|
||||
let app_path = app_path();
|
||||
let temp_path = app_path.join("temp");
|
||||
if !temp_path.is_dir() {
|
||||
create_dir_all(&temp_path).unwrap();
|
||||
}
|
||||
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();
|
||||
if let Err(err) = res {
|
||||
if ErrorKind::NotFound == err.kind() {
|
||||
log::warn!("Git program not found. Hint: Install git or check $PATH.");
|
||||
return Err(err.into());
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn tools_path() -> PathBuf {
|
||||
let app_path = app_path();
|
||||
let temp_path = app_path.join("tools");
|
||||
if !temp_path.is_dir() {
|
||||
create_dir_all(&temp_path).unwrap();
|
||||
}
|
||||
temp_path
|
||||
}
|
||||
|
||||
#[allow(clippy::should_implement_trait)]
|
||||
impl Tool {
|
||||
/// from str to tool enum
|
||||
pub fn from_str(name: &str) -> Option<Self> {
|
||||
match name {
|
||||
"binaryen" => Some(Self::Binaryen),
|
||||
"sass" => Some(Self::Sass),
|
||||
"tailwindcss" => Some(Self::Tailwind),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// get current tool name str
|
||||
pub fn name(&self) -> &str {
|
||||
match self {
|
||||
Self::Binaryen => "binaryen",
|
||||
Self::Sass => "sass",
|
||||
Self::Tailwind => "tailwindcss",
|
||||
}
|
||||
}
|
||||
|
||||
/// get tool bin dir path
|
||||
pub fn bin_path(&self) -> &str {
|
||||
match self {
|
||||
Self::Binaryen => "bin",
|
||||
Self::Sass => ".",
|
||||
Self::Tailwind => ".",
|
||||
}
|
||||
}
|
||||
|
||||
/// get target platform
|
||||
pub fn target_platform(&self) -> &str {
|
||||
match self {
|
||||
Self::Binaryen => {
|
||||
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::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",
|
||||
}
|
||||
}
|
||||
|
||||
/// get tool package download url
|
||||
pub fn download_url(&self) -> String {
|
||||
match self {
|
||||
Self::Binaryen => {
|
||||
format!(
|
||||
"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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// get package extension name
|
||||
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",
|
||||
}
|
||||
}
|
||||
|
||||
/// check tool state
|
||||
pub fn is_installed(&self) -> bool {
|
||||
tools_path().join(self.name()).is_dir()
|
||||
}
|
||||
|
||||
/// get download temp path
|
||||
pub fn temp_out_path(&self) -> PathBuf {
|
||||
temp_path().join(format!("{}-tool.tmp", self.name()))
|
||||
}
|
||||
|
||||
/// start to download package
|
||||
pub async fn download_package(&self) -> anyhow::Result<PathBuf> {
|
||||
let download_url = self.download_url();
|
||||
let temp_out = self.temp_out_path();
|
||||
let mut file = tokio::fs::File::create(&temp_out)
|
||||
.await
|
||||
.context("failed creating temporary output file")?;
|
||||
|
||||
let resp = reqwest::get(download_url).await.unwrap();
|
||||
|
||||
let mut res_bytes = resp.bytes_stream();
|
||||
while let Some(chunk_res) = res_bytes.next().await {
|
||||
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)
|
||||
}
|
||||
|
||||
/// start to install package
|
||||
pub async fn install_package(&self) -> anyhow::Result<()> {
|
||||
let temp_path = self.temp_out_path();
|
||||
let tool_path = tools_path();
|
||||
|
||||
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" {
|
||||
let tar_gz = File::open(temp_path)?;
|
||||
let tar = GzDecoder::new(tar_gz);
|
||||
let mut archive = Archive::new(tar);
|
||||
archive.unpack(&tool_path)?;
|
||||
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(())
|
||||
}
|
||||
|
||||
pub fn call(&self, command: &str, args: Vec<&str>) -> anyhow::Result<Vec<u8>> {
|
||||
let bin_path = tools_path().join(self.name()).join(self.bin_path());
|
||||
|
||||
let command_file = match self {
|
||||
Tool::Binaryen => {
|
||||
if cfg!(target_os = "windows") {
|
||||
format!("{}.exe", command)
|
||||
} else {
|
||||
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() {
|
||||
return Err(anyhow::anyhow!("Command file not found."));
|
||||
}
|
||||
|
||||
let mut command = Command::new(bin_path.join(&command_file).to_str().unwrap());
|
||||
|
||||
let output = command
|
||||
.args(&args[..])
|
||||
.stdout(std::process::Stdio::inherit())
|
||||
.stderr(std::process::Stdio::inherit())
|
||||
.output()?;
|
||||
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