diff --git a/.fleet/settings.json b/.fleet/settings.json new file mode 100644 index 000000000..e69de29bb diff --git a/.gitignore b/.gitignore index 6b39d31a9..f01f6dacf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ /target -.idea/ \ No newline at end of file +.idea/ + +.DS_Store \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 0967ef424..40461adda 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1 +1,6 @@ -{} +{ + "Lua.diagnostics.globals": [ + "plugin_logger", + "PLUGIN_DOWNLOADER" + ] +} diff --git a/Cargo.lock b/Cargo.lock index d8e545d13..ce848c1f7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -222,6 +222,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bstr" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223" +dependencies = [ + "memchr", +] + [[package]] name = "bumpalo" version = "3.11.0" @@ -493,6 +502,16 @@ dependencies = [ "typenum", ] +[[package]] +name = "ctrlc" +version = "3.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d91974fbbe88ec1df0c24a4f00f99583667a7e2e6272b2b92d294d81e462173" +dependencies = [ + "nix", + "winapi", +] + [[package]] name = "curl" version = "0.4.44" @@ -553,7 +572,8 @@ dependencies = [ "chrono", "clap", "colored 2.0.0", - "convert_case", + "convert_case", + "ctrlc", "dioxus-core", "dioxus-rsx", "dirs 4.0.0", @@ -565,7 +585,9 @@ dependencies = [ "html_parser", "hyper", "indicatif", + "lazy_static", "log", + "mlua", "notify", "proc-macro2", "regex", @@ -594,6 +616,7 @@ dependencies = [ "dyn-clone", "futures-channel", "futures-util", + "fxhash", "indexmap", "log", "longest-increasing-subsequence", @@ -653,6 +676,12 @@ version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4f94fa09c2aeea5b8839e414b7b841bf429fd25b9c522116ac97ee87856d88b2" +[[package]] +name = "either" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" + [[package]] name = "encode_unicode" version = "0.3.6" @@ -878,6 +907,15 @@ dependencies = [ "slab", ] +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + [[package]] name = "generic-array" version = "0.14.6" @@ -1227,6 +1265,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.3" @@ -1341,6 +1388,24 @@ dependencies = [ "linked-hash-map", ] +[[package]] +name = "lua-src" +version = "544.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "708ba3c844d5e9d38def4a09dd871c17c370f519b3c4b7261fbabe4a613a814c" +dependencies = [ + "cc", +] + +[[package]] +name = "luajit-src" +version = "210.4.1+restyaa7a722" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92879345f9a97ee140cfe2e08eff49b101533d784527d46ce6d2dc0096d27b3" +dependencies = [ + "cc", +] + [[package]] name = "match_cfg" version = "0.1.0" @@ -1402,6 +1467,41 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "mlua" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10277581090f5cb7ecf814bc611152ce4db6dc8deffcaa08e24ed4c5197d9186" +dependencies = [ + "bstr", + "cc", + "futures-core", + "futures-task", + "futures-util", + "lua-src", + "luajit-src", + "mlua_derive", + "num-traits", + "once_cell", + "pkg-config", + "rustc-hash", +] + +[[package]] +name = "mlua_derive" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9214e60d3cf1643013b107330fcd374ccec1e4ba1eef76e7e5da5e8202e71c0" +dependencies = [ + "itertools", + "once_cell", + "proc-macro-error", + "proc-macro2", + "quote", + "regex", + "syn", +] + [[package]] name = "native-tls" version = "0.2.10" @@ -1420,6 +1520,18 @@ dependencies = [ "tempfile", ] +[[package]] +name = "nix" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e322c04a9e3440c327fca7b6c8a63e6890a32fa2ad689db972425f07e0d22abb" +dependencies = [ + "autocfg", + "bitflags", + "cfg-if", + "libc", +] + [[package]] name = "notify" version = "5.0.0-pre.16" diff --git a/Cargo.toml b/Cargo.toml index 6c79ffada..a63ae6005 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,7 +46,7 @@ walkdir = "2" # tools download dirs = "4.0.0" -reqwest = { version = "0.11", features = ["rustls-tls", "stream", "trust-dns"] } +reqwest = { version = "0.11", features = ["rustls-tls", "stream", "trust-dns", "blocking"] } flate2 = "1.0.22" tar = "0.4.38" zip = "0.6.2" @@ -62,7 +62,11 @@ dioxus-rsx = { git = "https://github.com/dioxuslabs/dioxus/", features = [ ] } proc-macro2 = { version = "1.0", features = ["span-locations"] } +lazy_static = "1.4.0" +# plugin packages +mlua = { version = "0.8.1", features = ["lua54", "vendored", "async", "send", "macros"] } +ctrlc = "3.2.3" [[bin]] path = "src/main.rs" diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index ed7bcbfaf..b5e3b91ae 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -8,4 +8,11 @@ - [Build](./cmd/build.md) - [Serve](./cmd/serve.md) - [Clean](./cmd/clean.md) - - [Translate](./cmd/translate.md) \ No newline at end of file + - [Translate](./cmd/translate.md) +- [Plugin Development](./plugin/README.md) + - [API.Log](./plugin/interface/log.md) + - [API.Command](./plugin/interface/command.md) + - [API.OS](./plugin/interface/os.md) + - [API.Directories](./plugin/interface/dirs.md) + - [API.Network](./plugin/interface/network.md) + - [API.Path](./plugin/interface/path.md) \ No newline at end of file diff --git a/docs/src/plugin/README.md b/docs/src/plugin/README.md new file mode 100644 index 000000000..9983f024b --- /dev/null +++ b/docs/src/plugin/README.md @@ -0,0 +1,79 @@ +# CLI Plugin Development + +> For Cli 0.2.0 we will add `plugin-develop` support. + +Before the 0.2.0 we use `dioxus tool` to use & install some plugin, but we think that is not good for extend cli program, some people want tailwind support, some people want sass support, we can't add all this thing in to the cli source code and we don't have time to maintain a lot of tools that user request, so maybe user make plugin by themself is a good choice. + +### Why Lua ? + +We choose `Lua: 5.4` to be the plugin develop language, because cli plugin is not complex, just like a workflow, and user & developer can write some easy code for their plugin. We have **vendored** lua in cli program, and user don't need install lua runtime in their computer, and the lua parser & runtime doesn't take up much disk memory. + +### Event Management + +The plugin library have pre-define some important event you can control: + +- `build.on_start` +- `build.on_finished` +- `serve.on_start` +- `serve.on_rebuild` +- `serve.on_shutdown` + +### Plugin Template + +```lua +package.path = library_dir .. "/?.lua" + +local plugin = require("plugin") +local manager = require("manager") + +-- deconstruct api functions +local log = plugin.log + +-- plugin information +manager.name = "Hello Dixous Plugin" +manager.repository = "https://github.com/mrxiaozhuox/hello-dioxus-plugin" +manager.author = "YuKun Liu " +manager.version = "0.0.1" + +-- init manager info to plugin api +plugin.init(manager) + +manager.on_init = function () + -- when the first time plugin been load, this function will be execute. + -- system will create a `dcp.json` file to verify init state. + log.info("[plugin] Start to init plugin: " .. manager.name) +end + +---@param info BuildInfo +manager.build.on_start = function (info) + -- before the build work start, system will execute this function. + log.info("[plugin] Build starting: " .. info.name) +end + +---@param info BuildInfo +manager.build.on_finish = function (info) + -- when the build work is done, system will execute this function. + log.info("[plugin] Build finished: " .. info.name) +end + +---@param info ServeStartInfo +manager.serve.on_start = function (info) + -- this function will after clean & print to run, so you can print some thing. + log.info("[plugin] Serve start: " .. info.name) +end + +---@param info ServeRebuildInfo +manager.serve.on_rebuild = function (info) + -- this function will after clean & print to run, so you can print some thing. + local files = plugin.tool.dump(info.changed_files) + log.info("[plugin] Serve rebuild: '" .. files .. "'") +end + +manager.serve.on_shutdown = function () + log.info("[plugin] Serve shutdown") +end + +manager.serve.interval = 1000 + +return manager +``` \ No newline at end of file diff --git a/docs/src/plugin/interface/command.md b/docs/src/plugin/interface/command.md new file mode 100644 index 000000000..c9b1dc48f --- /dev/null +++ b/docs/src/plugin/interface/command.md @@ -0,0 +1,21 @@ +# Command Functions + +> you can use command functions to execute some code & script + +Type Define: +``` +Stdio: "Inhert" | "Piped" | "Null" +``` + +### `exec(commands: [string], stdout: Stdio, stderr: Stdio)` + +you can use this function to run some command on the current system. + +```lua +local cmd = plugin.command + +manager.test = function () + cmd.exec({"git", "clone", "https://github.com/DioxusLabs/cli-plugin-library"}) +end +``` +> Warning: This function don't have exception catch. \ No newline at end of file diff --git a/docs/src/plugin/interface/dirs.md b/docs/src/plugin/interface/dirs.md new file mode 100644 index 000000000..4173c922f --- /dev/null +++ b/docs/src/plugin/interface/dirs.md @@ -0,0 +1,35 @@ +# Dirs Functions + +> you can use Dirs functions to get some directory path + + +### plugin_dir() -> string + +You can get current plugin **root** directory path + +```lua +local path = plugin.dirs.plugin_dir() +-- example: ~/Development/DioxusCli/plugin/test-plugin/ +``` + +### bin_dir() -> string + +You can get plugin **bin** direcotry path + +Sometime you need install some binary file like `tailwind-cli` & `sass-cli` to help your plugin work, then you should put binary file in this directory. + +```lua +local path = plugin.dirs.bin_dir() +-- example: ~/Development/DioxusCli/plugin/test-plugin/bin/ +``` + +### temp_dir() -> string + +You can get plugin **temp** direcotry path + +Just put some temporary file in this directory. + +```lua +local path = plugin.dirs.bin_dir() +-- example: ~/Development/DioxusCli/plugin/test-plugin/temp/ +``` \ No newline at end of file diff --git a/docs/src/plugin/interface/log.md b/docs/src/plugin/interface/log.md new file mode 100644 index 000000000..fcd72595b --- /dev/null +++ b/docs/src/plugin/interface/log.md @@ -0,0 +1,48 @@ +# Log Functions + +> You can use log function to print some useful log info + +### Trace(info: string) + +Print trace log info + +```lua +local log = plugin.log +log.trace("trace information") +``` + +### Debug(info: string) + +Print debug log info + +```lua +local log = plugin.log +log.debug("debug information") +``` + +### Info(info: string) + +Print info log info + +```lua +local log = plugin.log +log.info("info information") +``` + +### Warn(info: string) + +Print warning log info + +```lua +local log = plugin.log +log.warn("warn information") +``` + +### Error(info: string) + +Print error log info + +```lua +local log = plugin.log +log.error("error information") +``` \ No newline at end of file diff --git a/docs/src/plugin/interface/network.md b/docs/src/plugin/interface/network.md new file mode 100644 index 000000000..358c9c498 --- /dev/null +++ b/docs/src/plugin/interface/network.md @@ -0,0 +1,34 @@ +# Network Functions + +> you can use Network functions to download & read some data from internet + +### download_file(url: string, path: string) -> boolean + +This function can help you download some file from url, and it will return a *boolean* value to check the download status. (true: success | false: fail) + +You need pass a target url and a local path (where you want to save this file) + +```lua +-- this file will download to plugin temp directory +local status = plugin.network.download_file( + "http://xxx.com/xxx.zip", + plugin.dirs.temp_dir() +) +if status != true then + log.error("Download Failed") +end +``` + +### clone_repo(url: string, path: string) -> boolean + +This function can help you use `git clone` command (this system must have been installed git) + +```lua +local status = plugin.network.clone_repo( + "http://github.com/mrxiaozhuox/dioxus-starter", + plugin.dirs.bin_dir() +) +if status != true then + log.error("Clone Failed") +end +``` \ No newline at end of file diff --git a/docs/src/plugin/interface/os.md b/docs/src/plugin/interface/os.md new file mode 100644 index 000000000..72a17c336 --- /dev/null +++ b/docs/src/plugin/interface/os.md @@ -0,0 +1,11 @@ +# OS Functions + +> you can use OS functions to get some system information + +### current_platform() -> string ("windows" | "macos" | "linux") + +This function can help you get system & platform type: + +```lua +local platform = plugin.os.current_platform() +``` \ No newline at end of file diff --git a/docs/src/plugin/interface/path.md b/docs/src/plugin/interface/path.md new file mode 100644 index 000000000..ee787ecf9 --- /dev/null +++ b/docs/src/plugin/interface/path.md @@ -0,0 +1,35 @@ +# Path Functions + +> you can use path functions to operate valid path string + +### join(path: string, extra: string) -> string + +This function can help you extend a path, you can extend any path, dirname or filename. + +```lua +local current_path = "~/hello/dioxus" +local new_path = plugin.path.join(current_path, "world") +-- new_path = "~/hello/dioxus/world" +``` + +### parent(path: string) -> string + +This function will return `path` parent-path string, back to the parent. + +```lua +local current_path = "~/hello/dioxus" +local new_path = plugin.path.parent(current_path) +-- new_path = "~/hello/" +``` + +### exists(path: string) -> boolean + +This function can check some path (dir & file) is exists. + +### is_file(path: string) -> boolean + +This function can check some path is a exist file. + +### is_dir(path: string) -> boolean + +This function can check some path is a exist dir. \ No newline at end of file diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 000000000..e69de29bb diff --git a/examples/plugin/init.lua b/examples/plugin/init.lua new file mode 100644 index 000000000..7d6b710ef --- /dev/null +++ b/examples/plugin/init.lua @@ -0,0 +1,18 @@ +local Api = require("./interface") +local log = Api.log; + +local manager = { + name = "Dioxus-CLI Plugin Demo", + repository = "http://github.com/DioxusLabs/cli", + author = "YuKun Liu ", +} + +manager.onLoad = function () + log.info("plugin loaded.") +end + +manager.onStartBuild = function () + log.warn("system start to build") +end + +return manager \ No newline at end of file diff --git a/examples/plugin/interface.lua b/examples/plugin/interface.lua new file mode 100644 index 000000000..bc6c91cb1 --- /dev/null +++ b/examples/plugin/interface.lua @@ -0,0 +1,25 @@ +local interface = {} + +if plugin_logger ~= nil then + interface.log = plugin_logger +else + interface.log = { + trace = function (info) + print("trace: " .. info) + end, + debug = function (info) + print("debug: " .. info) + end, + info = function (info) + print("info: " .. info) + end, + warn = function (info) + print("warn: " .. info) + end, + error = function (info) + print("error: " .. info) + end, + } +end + +return interface \ No newline at end of file diff --git a/src/assets/dioxus.toml b/src/assets/dioxus.toml index 649aa2c3c..42a1bd3f1 100644 --- a/src/assets/dioxus.toml +++ b/src/assets/dioxus.toml @@ -40,25 +40,8 @@ script = [] # serve: [dev-server] only script = [] -[application.tools] +[application.plugins] -# use binaryen.wasm-opt for output Wasm file -# binaryen just will trigger in `web` platform -binaryen = { wasm_opt = true } +available = true -# use sass auto will auto check sass file and build it. - - -# [application.tools.sass] - -# auto will check the assets dirs, and auto to transform all scss file to css file. -# input = "*" - -# or you can specify some scss file -> css file -# input = [ -# # some sass file path -# # this file will translate to `/css/test.css` -# "/css/test.scss" -# ] - -# source_map = true \ No newline at end of file +required = [] \ No newline at end of file diff --git a/src/builder.rs b/src/builder.rs index de59373c0..5d192dfc0 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -124,7 +124,7 @@ pub fn build(config: &CrateConfig, quiet: bool) -> Result { .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.79` Bindgen crate.".to_string())); + 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 diff --git a/src/cli/build/mod.rs b/src/cli/build/mod.rs index 90ac2d0ba..384bdfff1 100644 --- a/src/cli/build/mod.rs +++ b/src/cli/build/mod.rs @@ -1,3 +1,5 @@ +use crate::plugin::PluginManager; + use super::*; /// Build the Rust WASM app and all of its assets. @@ -32,6 +34,8 @@ impl Build { .clone() }); + let _ = PluginManager::on_build_start(&crate_config, &platform); + match platform.as_str() { "web" => { crate::builder::build(&crate_config, false)?; @@ -61,6 +65,8 @@ impl Build { )?; file.write_all(temp.as_bytes())?; + let _ = PluginManager::on_build_finish(&crate_config, &platform); + Ok(()) } } diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 8fbbd9e76..3eb242bf5 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -4,7 +4,7 @@ pub mod clean; pub mod config; pub mod create; pub mod serve; -pub mod tool; +pub mod plugin; pub mod translate; use crate::{ @@ -52,7 +52,21 @@ pub enum Commands { /// Dioxus config file controls. #[clap(subcommand)] Config(config::Config), - /// Install & Manage tools for Dioxus-cli. #[clap(subcommand)] - Tool(tool::Tool), + /// Manage plugins for dioxus cli + Plugin(plugin::Plugin), } + +impl Commands { + pub fn to_string(&self) -> String { + match self { + Commands::Build(_) => "build", + Commands::Translate(_) => "translate", + Commands::Serve(_) => "sevre", + Commands::Create(_) => "create", + Commands::Clean(_) => "clean", + Commands::Config(_) => "config", + Commands::Plugin(_) => "plugin", + }.to_string() + } +} \ No newline at end of file diff --git a/src/cli/plugin/mod.rs b/src/cli/plugin/mod.rs new file mode 100644 index 000000000..9179f5bd6 --- /dev/null +++ b/src/cli/plugin/mod.rs @@ -0,0 +1,36 @@ +use super::*; + +/// Build the Rust WASM app and all of its assets. +#[derive(Clone, Debug, Deserialize, Subcommand)] +#[clap(name = "plugin")] +pub enum Plugin { + /// Return all dioxus-cli support tools. + List {}, + /// Get default app install path. + AppPath {}, + /// Install a new tool. + Add { name: String }, +} + +impl Plugin { + pub async fn plugin(self) -> Result<()> { + match self { + Plugin::List {} => { + for item in crate::plugin::PluginManager::plugin_list() { + println!("- {item}"); + } + } + Plugin::AppPath {} => { + if let Some(v) = crate::plugin::PluginManager::init_plugin_dir().to_str() { + println!("{}", v); + } else { + log::error!("Plugin path get failed."); + } + } + Plugin::Add { name: _ } => { + log::info!("You can use `dioxus plugin app-path` to get Installation position"); + } + } + Ok(()) + } +} diff --git a/src/config.rs b/src/config.rs index 3952f973e..5170f8756 100644 --- a/src/config.rs +++ b/src/config.rs @@ -6,6 +6,13 @@ use std::{collections::HashMap, fs::File, io::Read, path::PathBuf}; pub struct DioxusConfig { pub application: ApplicationConfig, pub web: WebConfig, + + #[serde(default = "default_plugin")] + pub plugin: toml::Value, +} + +fn default_plugin() -> toml::Value { + toml::Value::Boolean(true) } impl DioxusConfig { @@ -22,7 +29,7 @@ impl DioxusConfig { dioxus_conf_file.read_to_string(&mut meta_str)?; toml::from_str::(&meta_str) - .map_err(|_| crate::Error::Unique("Dioxus.toml parse failed".into())) + .map_err(|_| crate::Error::Unique("Dioxus.toml parse failed".into())) } } @@ -34,7 +41,9 @@ impl Default for DioxusConfig { default_platform: "web".to_string(), out_dir: Some(PathBuf::from("dist")), asset_dir: Some(PathBuf::from("public")), + tools: None, + sub_package: None, }, web: WebConfig { @@ -56,6 +65,7 @@ impl Default for DioxusConfig { script: Some(vec![]), }, }, + plugin: toml::Value::Table(toml::map::Map::new()), } } } @@ -66,7 +76,9 @@ pub struct ApplicationConfig { pub default_platform: String, pub out_dir: Option, pub asset_dir: Option, + pub tools: Option>, + pub sub_package: Option, } @@ -217,20 +229,4 @@ impl CrateConfig { self.features = Some(features); self } - - // pub fn with_build_options(&mut self, options: &BuildOptions) { - // if let Some(name) = &options.example { - // self.as_example(name.clone()); - // } - // self.release = options.release; - // self.out_dir = options.outdir.clone().into(); - // } - - // pub fn with_develop_options(&mut self, options: &DevelopOptions) { - // if let Some(name) = &options.example { - // self.as_example(name.clone()); - // } - // self.release = options.release; - // self.out_dir = tempfile::Builder::new().tempdir().expect("").into_path(); - // } } diff --git a/src/hot_reload/mod.rs b/src/hot_reload.rs similarity index 99% rename from src/hot_reload/mod.rs rename to src/hot_reload.rs index ac4a537b8..c26e6bca7 100644 --- a/src/hot_reload/mod.rs +++ b/src/hot_reload.rs @@ -611,4 +611,4 @@ fn find_rsx_expr( (syn::Expr::Verbatim(_), syn::Expr::Verbatim(_)) => false, _ => true, } -} +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 1b651acee..e01786047 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -22,3 +22,4 @@ pub mod logging; pub use logging::*; pub mod hot_reload; +pub mod plugin; \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 556572659..db8bbb6b7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,5 @@ use clap::Parser; -use dioxus_cli::*; +use dioxus_cli::{plugin::PluginManager, *}; use std::process::exit; #[tokio::main] @@ -7,6 +7,15 @@ async fn main() -> Result<()> { let args = Cli::parse(); set_up_logging(); + let dioxus_config = DioxusConfig::load()?; + + let plugin_state = PluginManager::init(dioxus_config.plugin); + + if let Err(e) = plugin_state { + log::error!("🚫 Plugin system initialization failed: {e}"); + exit(1); + } + match args.action { Commands::Translate(opts) => { if let Err(e) = opts.translate() { @@ -50,10 +59,9 @@ async fn main() -> Result<()> { } } - Commands::Tool(opts) => { - if let Err(e) = opts.tool().await { + Commands::Plugin(opts) => { + if let Err(e) = opts.plugin().await { log::error!("tool error: {}", e); - exit(1); } } } diff --git a/src/plugin/interface/command.rs b/src/plugin/interface/command.rs new file mode 100644 index 000000000..34b6f9082 --- /dev/null +++ b/src/plugin/interface/command.rs @@ -0,0 +1,65 @@ +use std::process::{Command, Stdio}; + +use mlua::{FromLua, UserData}; + +enum StdioFromString { + Inhert, + Piped, + Null, +} +impl<'lua> FromLua<'lua> for StdioFromString { + fn from_lua(lua_value: mlua::Value<'lua>, _lua: &'lua mlua::Lua) -> mlua::Result { + if let mlua::Value::String(v) = lua_value { + let v = v.to_str().unwrap(); + return Ok(match v.to_lowercase().as_str() { + "inhert" => Self::Inhert, + "piped" => Self::Piped, + "null" => Self::Null, + _ => Self::Inhert, + }); + } + Ok(Self::Inhert) + } +} +impl StdioFromString { + pub fn to_stdio(self) -> Stdio { + match self { + StdioFromString::Inhert => Stdio::inherit(), + StdioFromString::Piped => Stdio::piped(), + StdioFromString::Null => Stdio::null(), + } + } +} + +pub struct PluginCommander; +impl UserData for PluginCommander { + fn add_methods<'lua, M: mlua::UserDataMethods<'lua, Self>>(methods: &mut M) { + methods.add_function( + "exec", + |_, args: (Vec, 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::>(); + 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) {} +} diff --git a/src/plugin/interface/dirs.rs b/src/plugin/interface/dirs.rs new file mode 100644 index 000000000..7837482dc --- /dev/null +++ b/src/plugin/interface/dirs.rs @@ -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()) + }); + } +} \ No newline at end of file diff --git a/src/plugin/interface/fs.rs b/src/plugin/interface/fs.rs new file mode 100644 index 000000000..d72e41375 --- /dev/null +++ b/src/plugin/interface/fs.rs @@ -0,0 +1,87 @@ +use std::{ + fs::{create_dir, create_dir_all, remove_dir_all}, + path::PathBuf, io::{Read, Write}, +}; +use std::fs::File; + +use mlua::UserData; +use flate2::read::GzDecoder; +use tar::Archive; +use crate::tools::extract_zip; + +pub struct PluginFileSystem; +impl UserData for PluginFileSystem { + fn add_methods<'lua, M: mlua::UserDataMethods<'lua, Self>>(methods: &mut M) { + methods.add_function("create_dir", |_, args: (String, bool)| { + let path = args.0; + let recursive = args.1; + let path = PathBuf::from(path); + if !path.exists() { + let v = if recursive { + create_dir_all(path) + } else { + create_dir(path) + }; + return Ok(v.is_ok()); + } + Ok(true) + }); + methods.add_function("remove_dir", |_, path: String| { + let path = PathBuf::from(path); + let r = remove_dir_all(path); + Ok(r.is_ok()) + }); + methods.add_function("file_get_content", |_, path: String| { + let path = PathBuf::from(path); + let mut file = std::fs::File::open(path)?; + let mut buffer = String::new(); + file.read_to_string(&mut buffer)?; + Ok(buffer) + }); + methods.add_function("file_set_content", |_, args: (String, String)| { + let path = args.0; + let content = args.1; + let path = PathBuf::from(path); + + let file = std::fs::File::create(path); + if file.is_err() { + return Ok(false); + } + + if file.unwrap().write_all(content.as_bytes()).is_err() { + return Ok(false) + } + + Ok(true) + }); + methods.add_function("unzip_file", |_, args: (String, String)| { + let file = PathBuf::from(args.0); + let target = PathBuf::from(args.1); + let res = extract_zip(&file, &target); + if let Err(_) = res { + return Ok(false); + } + Ok(true) + }); + methods.add_function("untar_gz_file", |_, args: (String, String)| { + + let file = PathBuf::from(args.0); + let target = PathBuf::from(args.1); + + let tar_gz = if let Ok(v) = File::open(file) { + v + } else { + return Ok(false); + }; + + let tar = GzDecoder::new(tar_gz); + let mut archive = Archive::new(tar); + if archive.unpack(&target).is_err() { + return Ok(false); + } + + + Ok(true) + }); + } +} diff --git a/src/plugin/interface/log.rs b/src/plugin/interface/log.rs new file mode 100644 index 000000000..d67ae0316 --- /dev/null +++ b/src/plugin/interface/log.rs @@ -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(()) + }); + } +} \ No newline at end of file diff --git a/src/plugin/interface/mod.rs b/src/plugin/interface/mod.rs new file mode 100644 index 000000000..5ed2ed827 --- /dev/null +++ b/src/plugin/interface/mod.rs @@ -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>, + 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 { + 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> { + 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 { + 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> { + 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>, + pub on_finish: Option>, +} + +impl<'lua> FromLua<'lua> for PluginBuildInfo<'lua> { + fn from_lua(lua_value: mlua::Value<'lua>, _lua: &'lua mlua::Lua) -> mlua::Result { + 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> { + 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>, + pub on_interval: Option>, + pub on_rebuild: Option>, + pub on_shutdown: Option>, +} + +impl<'lua> FromLua<'lua> for PluginServeInfo<'lua> { + fn from_lua(lua_value: mlua::Value<'lua>, _lua: &'lua mlua::Lua) -> mlua::Result { + 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> { + 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)) + } +} diff --git a/src/plugin/interface/network.rs b/src/plugin/interface/network.rs new file mode 100644 index 000000000..8d940ddc4 --- /dev/null +++ b/src/plugin/interface/network.rs @@ -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) + }); + } +} diff --git a/src/plugin/interface/os.rs b/src/plugin/interface/os.rs new file mode 100644 index 000000000..33368f38a --- /dev/null +++ b/src/plugin/interface/os.rs @@ -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"); + } + }); + } +} \ No newline at end of file diff --git a/src/plugin/interface/path.rs b/src/plugin/interface/path.rs new file mode 100644 index 000000000..f0d36bd78 --- /dev/null +++ b/src/plugin/interface/path.rs @@ -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| { + 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()) + }); + } +} diff --git a/src/plugin/mod.rs b/src/plugin/mod.rs new file mode 100644 index 000000000..c77764fd4 --- /dev/null +++ b/src/plugin/mod.rs @@ -0,0 +1,330 @@ +use std::{ + io::{Read, Write}, + path::PathBuf, + sync::Mutex, +}; + +use mlua::{Lua, Table}; +use serde_json::json; + +use crate::{ + tools::{app_path, clone_repo}, + CrateConfig, +}; + +use self::{ + interface::PluginInfo, + interface::{ + command::PluginCommander, dirs::PluginDirs, fs::PluginFileSystem, log::PluginLogger, + network::PluginNetwork, os::PluginOS, path::PluginPath, + }, + types::PluginConfig, +}; + +pub mod interface; +mod types; + +lazy_static::lazy_static! { + static ref LUA: Mutex = 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::>(); + 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::(); + 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::(i)?; + if let Some(func) = info.build.on_start { + func.call::(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::(i)?; + if let Some(func) = info.build.on_finish { + func.call::(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::(i)?; + if let Some(func) = info.serve.on_start { + func.call::(args.clone())?; + } + } + + Ok(()) + } + + pub fn on_serve_rebuild(timestamp: i64, files: Vec) -> 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 = 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::(i)?; + if let Some(func) = info.serve.on_rebuild { + func.call::(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::(i)?; + if let Some(func) = info.serve.on_shutdown { + func.call::(args.clone())?; + } + } + + Ok(()) + } + + pub fn init_plugin_dir() -> PathBuf { + let app_path = app_path(); + let plugin_path = app_path.join("plugins"); + if !plugin_path.is_dir() { + log::info!("📖 Start to init plugin library ..."); + let url = "https://github.com/DioxusLabs/cli-plugin-library"; + clone_repo(&plugin_path, url).unwrap(); + } + plugin_path + } + + pub fn plugin_list() -> Vec { + 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::>() + .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 + } +} diff --git a/src/plugin/types.rs b/src/plugin/types.rs new file mode 100644 index 000000000..d22c1423c --- /dev/null +++ b/src/plugin/types.rs @@ -0,0 +1,138 @@ +use std::collections::HashMap; + +use mlua::ToLua; + +#[derive(Debug, Clone)] +pub struct PluginConfig { + pub available: bool, + pub loader: Vec, + pub config_info: HashMap>, +} + +impl<'lua> ToLua<'lua> for PluginConfig { + fn to_lua(self, lua: &'lua mlua::Lua) -> mlua::Result> { + 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), + Table(HashMap), +} + +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> { + 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) + } + }) + } +} diff --git a/src/server/mod.rs b/src/server/mod.rs index 9cc693452..8bf7f3cb8 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -17,7 +17,7 @@ use std::{net::UdpSocket, path::PathBuf, process::Command, sync::Arc}; use tower::ServiceBuilder; use tower_http::services::fs::{ServeDir, ServeFileSystemResponseBody}; -use crate::{builder, serve::Serve, BuildResult, CrateConfig, Result}; +use crate::{builder, plugin::PluginManager, serve::Serve, BuildResult, CrateConfig, Result}; use tokio::sync::broadcast; mod hot_reload; @@ -54,6 +54,13 @@ struct WsReloadState { } pub async fn startup(port: u16, config: CrateConfig) -> Result<()> { + // ctrl-c shutdown checker + let crate_config = config.clone(); + let _ = ctrlc::set_handler(move || { + let _ = PluginManager::on_serve_shutdown(&crate_config); + std::process::exit(0); + }); + if config.hot_reload { startup_hot_reload(port, config).await? } else { @@ -62,11 +69,14 @@ pub async fn startup(port: u16, config: CrateConfig) -> Result<()> { Ok(()) } +#[allow(unused_assignments)] pub async fn startup_hot_reload(port: u16, config: CrateConfig) -> Result<()> { let first_build_result = crate::builder::build(&config, false)?; log::info!("🚀 Starting development server..."); + PluginManager::on_serve_start(&config)?; + let dist_path = config.out_dir.clone(); let (reload_tx, _) = broadcast::channel(100); let last_file_rebuild = Arc::new(Mutex::new(FileMap::new(config.crate_dir.clone()))); @@ -227,6 +237,7 @@ pub async fn startup_hot_reload(port: u16, config: CrateConfig) -> Result<()> { .unwrap(); } + // start serve dev-server at 0.0.0.0:8080 print_console_info( port, @@ -328,31 +339,32 @@ pub async fn startup_default(port: u16, config: CrateConfig) -> Result<()> { .unwrap_or_else(|| vec![PathBuf::from("src")]); let watcher_config = config.clone(); - let mut watcher = RecommendedWatcher::new( - move |info: notify::Result| { - let config = watcher_config.clone(); - if info.is_ok() { - if chrono::Local::now().timestamp() > last_update_time { - match build_manager.rebuild() { - Ok(res) => { - last_update_time = chrono::Local::now().timestamp(); - print_console_info( - port, - &config, - PrettierOptions { - changed: info.unwrap().paths, - warnings: res.warnings, - elapsed_time: res.elapsed_time, - }, - ); - } - Err(e) => log::error!("{}", e), + let mut watcher = notify::recommended_watcher(move |info: notify::Result| { + let config = watcher_config.clone(); + if let Ok(e) = info { + if chrono::Local::now().timestamp() > last_update_time { + match build_manager.rebuild() { + Ok(res) => { + last_update_time = chrono::Local::now().timestamp(); + print_console_info( + port, + &config, + PrettierOptions { + changed: e.paths.clone(), + warnings: res.warnings, + elapsed_time: res.elapsed_time, + }, + ); + let _ = PluginManager::on_serve_rebuild( + chrono::Local::now().timestamp(), + e.paths, + ); } + Err(e) => log::error!("{}", e), } } - }, - notify::Config::default(), - ) + } + }) .unwrap(); for sub_path in allow_watch_path { @@ -375,6 +387,8 @@ pub async fn startup_default(port: u16, config: CrateConfig) -> Result<()> { }, ); + PluginManager::on_serve_start(&config)?; + let file_service_config = config.clone(); let file_service = ServiceBuilder::new() .and_then( diff --git a/src/tools.rs b/src/tools.rs index a9b0edf45..de762d364 100644 --- a/src/tools.rs +++ b/src/tools.rs @@ -18,9 +18,9 @@ pub enum Tool { Tailwind, } -pub fn tool_list() -> Vec<&'static str> { - vec!["binaryen", "sass", "tailwindcss"] -} +// pub fn tool_list() -> Vec<&'static str> { +// vec!["binaryen", "sass", "tailwindcss"] +// } pub fn app_path() -> PathBuf { let data_local = dirs::data_local_dir().unwrap(); @@ -40,6 +40,16 @@ pub fn temp_path() -> PathBuf { temp_path } +pub fn clone_repo(dir: &Path, url: &str) -> anyhow::Result<()> { + let target_dir = dir.parent().unwrap(); + let dir_name = dir.file_name().unwrap(); + + let mut cmd = Command::new("git"); + let cmd = cmd.current_dir(target_dir); + let _res = cmd.arg("clone").arg(url).arg(dir_name).output()?; + Ok(()) +} + pub fn tools_path() -> PathBuf { let app_path = app_path(); let temp_path = app_path.join("tools"); @@ -303,7 +313,7 @@ impl Tool { } } -fn extract_zip(file: &Path, target: &Path) -> anyhow::Result<()> { +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)?;