Преглед изворни кода

Merge pull request #57 from mrxiaozhuox/master

Jon Kelley пре 2 година
родитељ
комит
2e804f71b3

+ 0 - 0
.fleet/settings.json


+ 3 - 1
.gitignore

@@ -1,2 +1,4 @@
 /target
-.idea/
+.idea/
+
+.DS_Store

+ 6 - 1
.vscode/settings.json

@@ -1 +1,6 @@
-{}
+{
+    "Lua.diagnostics.globals": [
+        "plugin_logger",
+        "PLUGIN_DOWNLOADER"
+    ]
+}

+ 113 - 1
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"

+ 5 - 1
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"

+ 8 - 1
docs/src/SUMMARY.md

@@ -8,4 +8,11 @@
   - [Build](./cmd/build.md)
   - [Serve](./cmd/serve.md)
   - [Clean](./cmd/clean.md)
-  - [Translate](./cmd/translate.md)
+  - [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)

+ 79 - 0
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 <mrxzx.info@gmail.com>"
+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
+```

+ 21 - 0
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.

+ 35 - 0
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/
+```

+ 48 - 0
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")
+```

+ 34 - 0
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
+```

+ 11 - 0
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()
+```

+ 35 - 0
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.

+ 0 - 0
examples/README.md


+ 18 - 0
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 <mrxzx.info@gmail.com>",
+}
+
+manager.onLoad = function ()
+    log.info("plugin loaded.")
+end
+
+manager.onStartBuild = function ()
+    log.warn("system start to build")
+end
+
+return manager

+ 25 - 0
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

+ 3 - 20
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
+required = []

+ 1 - 1
src/builder.rs

@@ -124,7 +124,7 @@ pub fn build(config: &CrateConfig, quiet: bool) -> Result<BuildResult> {
             .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

+ 6 - 0
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(())
     }
 }

+ 17 - 3
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()
+    }
+}

+ 36 - 0
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(())
+    }
+}

+ 13 - 17
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::<DioxusConfig>(&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<PathBuf>,
     pub asset_dir: Option<PathBuf>,
+
     pub tools: Option<HashMap<String, toml::Value>>,
+
     pub sub_package: Option<String>,
 }
 
@@ -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();
-    // }
 }

+ 1 - 1
src/hot_reload/mod.rs → src/hot_reload.rs

@@ -611,4 +611,4 @@ fn find_rsx_expr(
         (syn::Expr::Verbatim(_), syn::Expr::Verbatim(_)) => false,
         _ => true,
     }
-}
+}

+ 1 - 0
src/lib.rs

@@ -22,3 +22,4 @@ pub mod logging;
 pub use logging::*;
 
 pub mod hot_reload;
+pub mod plugin;

+ 12 - 4
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);
             }
         }
     }

+ 65 - 0
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<Self> {
+        if let mlua::Value::String(v) = lua_value {
+            let v = v.to_str().unwrap();
+            return Ok(match v.to_lowercase().as_str() {
+                "inhert" => Self::Inhert,
+                "piped" => Self::Piped,
+                "null" => Self::Null,
+                _ => Self::Inhert,
+            });
+        }
+        Ok(Self::Inhert)
+    }
+}
+impl StdioFromString {
+    pub fn to_stdio(self) -> Stdio {
+        match self {
+            StdioFromString::Inhert => Stdio::inherit(),
+            StdioFromString::Piped => Stdio::piped(),
+            StdioFromString::Null => Stdio::null(),
+        }
+    }
+}
+
+pub struct PluginCommander;
+impl UserData for PluginCommander {
+    fn add_methods<'lua, M: mlua::UserDataMethods<'lua, Self>>(methods: &mut M) {
+        methods.add_function(
+            "exec",
+            |_, args: (Vec<String>, StdioFromString, StdioFromString)| {
+                
+                let cmd = args.0;
+                let stdout = args.1;
+                let stderr = args.2;
+
+                if cmd.len() == 0 {
+                    return Ok(());
+                }
+                let cmd_name = cmd.get(0).unwrap();
+                let mut command = Command::new(cmd_name);
+                let t = cmd
+                    .iter()
+                    .enumerate()
+                    .filter(|(i, _)| *i > 0)
+                    .map(|v| v.1.clone())
+                    .collect::<Vec<String>>();
+                command.args(t);
+                command.stdout(stdout.to_stdio()).stderr(stderr.to_stdio());
+                command.output()?;
+                Ok(())
+            },
+        );
+    }
+
+    fn add_fields<'lua, F: mlua::UserDataFields<'lua, Self>>(_fields: &mut F) {}
+}

+ 13 - 0
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())
+        });
+    }
+}

+ 87 - 0
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)
+        });
+    }
+}

+ 28 - 0
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(())
+        });
+    }
+}

+ 233 - 0
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<Function<'lua>>,
+    pub build: PluginBuildInfo<'lua>,
+    pub serve: PluginServeInfo<'lua>,
+}
+
+impl<'lua> FromLua<'lua> for PluginInfo<'lua> {
+    fn from_lua(lua_value: mlua::Value<'lua>, _lua: &'lua mlua::Lua) -> mlua::Result<Self> {
+        let mut res = Self {
+            name: String::default(),
+            repository: String::default(),
+            author: String::default(),
+            version: String::from("0.1.0"),
+
+            inner: Default::default(),
+
+            on_init: None,
+            build: Default::default(),
+            serve: Default::default(),
+        };
+        if let mlua::Value::Table(tab) = lua_value {
+            if let Ok(v) = tab.get::<_, String>("name") {
+                res.name = v;
+            }
+            if let Ok(v) = tab.get::<_, String>("repository") {
+                res.repository = v;
+            }
+            if let Ok(v) = tab.get::<_, String>("author") {
+                res.author = v;
+            }
+            if let Ok(v) = tab.get::<_, String>("version") {
+                res.version = v;
+            }
+
+            if let Ok(v) = tab.get::<_, PluginInner>("inner") {
+                res.inner = v;
+            }
+
+            if let Ok(v) = tab.get::<_, Function>("on_init") {
+                res.on_init = Some(v);
+            }
+
+            if let Ok(v) = tab.get::<_, PluginBuildInfo>("build") {
+                res.build = v;
+            }
+
+            if let Ok(v) = tab.get::<_, PluginServeInfo>("serve") {
+                res.serve = v;
+            }
+        }
+
+        Ok(res)
+    }
+}
+
+impl<'lua> ToLua<'lua> for PluginInfo<'lua> {
+    fn to_lua(self, lua: &'lua mlua::Lua) -> mlua::Result<mlua::Value<'lua>> {
+        let res = lua.create_table()?;
+
+        res.set("name", self.name.to_string())?;
+        res.set("repository", self.repository.to_string())?;
+        res.set("author", self.author.to_string())?;
+        res.set("version", self.version.to_string())?;
+
+        res.set("inner", self.inner)?;
+
+        if let Some(e) = self.on_init {
+            res.set("on_init", e)?;
+        }
+        res.set("build", self.build)?;
+        res.set("serve", self.serve)?;
+
+        Ok(mlua::Value::Table(res))
+    }
+}
+
+#[derive(Debug, Clone, Default)]
+pub struct PluginInner {
+    pub plugin_dir: String,
+    pub from_loader: bool,
+}
+
+impl<'lua> FromLua<'lua> for PluginInner {
+    fn from_lua(lua_value: mlua::Value<'lua>, _lua: &'lua mlua::Lua) -> mlua::Result<Self> {
+        let mut res = Self {
+            plugin_dir: String::new(),
+            from_loader: false,
+        };
+
+        if let mlua::Value::Table(t) = lua_value {
+            if let Ok(v) = t.get::<_, String>("plugin_dir") {
+                res.plugin_dir = v;
+            }
+            if let Ok(v) = t.get::<_, bool>("from_loader") {
+                res.from_loader = v;
+            }
+        }
+        Ok(res)
+    }
+}
+
+impl<'lua> ToLua<'lua> for PluginInner {
+    fn to_lua(self, lua: &'lua mlua::Lua) -> mlua::Result<mlua::Value<'lua>> {
+        let res = lua.create_table()?;
+
+        res.set("plugin_dir", self.plugin_dir)?;
+        res.set("from_loader", self.from_loader)?;
+
+        Ok(mlua::Value::Table(res))
+    }
+}
+
+#[derive(Debug, Clone, Default)]
+pub struct PluginBuildInfo<'lua> {
+    pub on_start: Option<Function<'lua>>,
+    pub on_finish: Option<Function<'lua>>,
+}
+
+impl<'lua> FromLua<'lua> for PluginBuildInfo<'lua> {
+    fn from_lua(lua_value: mlua::Value<'lua>, _lua: &'lua mlua::Lua) -> mlua::Result<Self> {
+        let mut res = Self {
+            on_start: None,
+            on_finish: None,
+        };
+
+        if let mlua::Value::Table(t) = lua_value {
+            if let Ok(v) = t.get::<_, Function>("on_start") {
+                res.on_start = Some(v);
+            }
+            if let Ok(v) = t.get::<_, Function>("on_finish") {
+                res.on_finish = Some(v);
+            }
+        }
+
+        Ok(res)
+    }
+}
+
+impl<'lua> ToLua<'lua> for PluginBuildInfo<'lua> {
+    fn to_lua(self, lua: &'lua mlua::Lua) -> mlua::Result<mlua::Value<'lua>> {
+        let res = lua.create_table()?;
+
+        if let Some(v) = self.on_start {
+            res.set("on_start", v)?;
+        }
+
+        if let Some(v) = self.on_finish {
+            res.set("on_finish", v)?;
+        }
+
+        Ok(mlua::Value::Table(res))
+    }
+}
+
+#[derive(Debug, Clone, Default)]
+pub struct PluginServeInfo<'lua> {
+    pub interval: i32,
+
+    pub on_start: Option<Function<'lua>>,
+    pub on_interval: Option<Function<'lua>>,
+    pub on_rebuild: Option<Function<'lua>>,
+    pub on_shutdown: Option<Function<'lua>>,
+}
+
+impl<'lua> FromLua<'lua> for PluginServeInfo<'lua> {
+    fn from_lua(lua_value: mlua::Value<'lua>, _lua: &'lua mlua::Lua) -> mlua::Result<Self> {
+        let mut res = Self::default();
+
+        if let mlua::Value::Table(tab) = lua_value {
+            if let Ok(v) = tab.get::<_, i32>("interval") {
+                res.interval = v;
+            }
+            if let Ok(v) = tab.get::<_, Function>("on_start") {
+                res.on_start = Some(v);
+            }
+            if let Ok(v) = tab.get::<_, Function>("on_interval") {
+                res.on_interval = Some(v);
+            }
+            if let Ok(v) = tab.get::<_, Function>("on_rebuild") {
+                res.on_rebuild = Some(v);
+            }
+            if let Ok(v) = tab.get::<_, Function>("on_shutdown") {
+                res.on_shutdown = Some(v);
+            }
+        }
+
+        Ok(res)
+    }
+}
+
+impl<'lua> ToLua<'lua> for PluginServeInfo<'lua> {
+    fn to_lua(self, lua: &'lua mlua::Lua) -> mlua::Result<mlua::Value<'lua>> {
+        let res = lua.create_table()?;
+
+        res.set("interval", self.interval)?;
+
+        if let Some(v) = self.on_start {
+            res.set("on_start", v)?;
+        }
+
+        if let Some(v) = self.on_interval {
+            res.set("on_interval", v)?;
+        }
+
+        if let Some(v) = self.on_rebuild {
+            res.set("on_rebuild", v)?;
+        }
+
+        if let Some(v) = self.on_shutdown {
+            res.set("on_shutdown", v)?;
+        }
+
+        Ok(mlua::Value::Table(res))
+    }
+}

+ 27 - 0
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)
+        });
+    }
+}

+ 18 - 0
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");
+            }
+        });
+    }
+}

+ 40 - 0
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<String>| {
+            let mut path = PathBuf::new();
+            for i in args {
+                path = path.join(i);
+            }
+            Ok(path.to_str().unwrap().to_string())
+        });    
+
+        // parent function
+        methods.add_function("parent", |_, path: String| {
+            let current_path = PathBuf::from(&path);
+            let parent = current_path.parent();
+            if parent.is_none() {
+                return Ok(path);
+            } else {
+                return Ok(parent.unwrap().to_str().unwrap().to_string());
+            }
+        });
+        methods.add_function("exists", |_, path: String| {
+            let path = PathBuf::from(path);
+            Ok(path.exists())
+        });
+        methods.add_function("is_dir", |_, path: String| {
+            let path = PathBuf::from(path);
+            Ok(path.is_dir())
+        });
+        methods.add_function("is_file", |_, path: String| {
+            let path = PathBuf::from(path);
+            Ok(path.is_file())
+        });
+    }
+}

+ 330 - 0
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<Lua> = Mutex::new(Lua::new());
+}
+
+pub struct PluginManager;
+
+impl PluginManager {
+    pub fn init(config: toml::Value) -> anyhow::Result<()> {
+        let config = PluginConfig::from_toml_value(config);
+
+        if !config.available {
+            return Ok(());
+        }
+
+        let lua = LUA.lock().unwrap();
+
+        let manager = lua.create_table().unwrap();
+        let name_index = lua.create_table().unwrap();
+
+        let plugin_dir = Self::init_plugin_dir();
+
+        let api = lua.create_table().unwrap();
+
+        api.set("log", PluginLogger).unwrap();
+        api.set("command", PluginCommander).unwrap();
+        api.set("network", PluginNetwork).unwrap();
+        api.set("dirs", PluginDirs).unwrap();
+        api.set("fs", PluginFileSystem).unwrap();
+        api.set("path", PluginPath).unwrap();
+        api.set("os", PluginOS).unwrap();
+
+        lua.globals().set("plugin_lib", api).unwrap();
+        lua.globals()
+            .set("library_dir", plugin_dir.to_str().unwrap())
+            .unwrap();
+        lua.globals().set("config_info", config.clone())?;
+
+        let mut index: u32 = 1;
+        let dirs = std::fs::read_dir(&plugin_dir)?;
+
+        let mut path_list = dirs
+            .filter(|v| v.is_ok())
+            .map(|v| (v.unwrap().path(), false))
+            .collect::<Vec<(PathBuf, bool)>>();
+        for i in &config.loader {
+            let path = PathBuf::from(i);
+            if !path.is_dir() {
+                // for loader dir, we need check first, because we need give a error log.
+                log::error!("Plugin loader: {:?} path is not a exists directory.", path);
+            }
+            path_list.push((path, true));
+        }
+
+        for entry in path_list {
+            let plugin_dir = entry.0.to_path_buf();
+
+            if plugin_dir.is_dir() {
+                let init_file = plugin_dir.join("init.lua");
+                if init_file.is_file() {
+                    let mut file = std::fs::File::open(init_file).unwrap();
+                    let mut buffer = String::new();
+                    file.read_to_string(&mut buffer).unwrap();
+
+                    let current_plugin_dir = plugin_dir.to_str().unwrap().to_string();
+                    let from_loader = entry.1;
+
+                    lua.globals()
+                        .set("_temp_plugin_dir", current_plugin_dir.clone())?;
+                    lua.globals().set("_temp_from_loader", from_loader)?;
+
+                    let info = lua.load(&buffer).eval::<PluginInfo>();
+                    match info {
+                        Ok(mut info) => {
+                            if name_index.contains_key(info.name.clone()).unwrap_or(false)
+                                && !from_loader
+                            {
+                                // found same name plugin, intercept load
+                                log::warn!(
+                                    "Plugin {} has been intercepted. [mulit-load]",
+                                    info.name
+                                );
+                                continue;
+                            }
+                            info.inner.plugin_dir = current_plugin_dir;
+                            info.inner.from_loader = from_loader;
+
+                            // call `on_init` if file "dcp.json" not exists
+                            let dcp_file = plugin_dir.join("dcp.json");
+                            if !dcp_file.is_file() {
+                                if let Some(func) = info.clone().on_init {
+                                    let result = func.call::<_, bool>(());
+                                    match result {
+                                        Ok(true) => {
+                                            // plugin init success, create `dcp.json` file.
+                                            let mut file = std::fs::File::create(dcp_file).unwrap();
+                                            let value = json!({
+                                                "name": info.name,
+                                                "author": info.author,
+                                                "repository": info.repository,
+                                                "version": info.version,
+                                                "generate_time": chrono::Local::now().timestamp(),
+                                            });
+                                            let buffer =
+                                                serde_json::to_string_pretty(&value).unwrap();
+                                            let buffer = buffer.as_bytes();
+                                            file.write_all(buffer).unwrap();
+
+                                            // insert plugin-info into plugin-manager
+                                            if let Ok(index) =
+                                                name_index.get::<_, u32>(info.name.clone())
+                                            {
+                                                let _ = manager.set(index, info.clone());
+                                            } else {
+                                                let _ = manager.set(index, info.clone());
+                                                index += 1;
+                                                let _ = name_index.set(info.name, index);
+                                            }
+                                        }
+                                        Ok(false) => {
+                                            log::warn!("Plugin init function result is `false`, init failed.");
+                                        }
+                                        Err(e) => {
+                                            log::warn!("Plugin init failed: {e}");
+                                        }
+                                    }
+                                }
+                            } else {
+                                if let Ok(index) = name_index.get::<_, u32>(info.name.clone()) {
+                                    let _ = manager.set(index, info.clone());
+                                } else {
+                                    let _ = manager.set(index, info.clone());
+                                    index += 1;
+                                    let _ = name_index.set(info.name, index);
+                                }
+                            }
+                        }
+                        Err(_e) => {
+                            let dir_name = plugin_dir.file_name().unwrap().to_str().unwrap();
+                            log::error!("Plugin '{dir_name}' load failed.");
+                        }
+                    }
+                }
+            }
+        }
+
+        lua.globals().set("manager", manager).unwrap();
+
+        return Ok(());
+    }
+
+    pub fn on_build_start(crate_config: &CrateConfig, platform: &str) -> anyhow::Result<()> {
+        let lua = LUA.lock().unwrap();
+
+        if !lua.globals().contains_key("manager")? {
+            return Ok(());
+        }
+        let manager = lua.globals().get::<_, Table>("manager")?;
+
+        let args = lua.create_table()?;
+        args.set("name", crate_config.dioxus_config.application.name.clone())?;
+        args.set("platform", platform)?;
+        args.set("out_dir", crate_config.out_dir.to_str().unwrap())?;
+        args.set("asset_dir", crate_config.asset_dir.to_str().unwrap())?;
+
+        for i in 1..(manager.len()? as i32 + 1) {
+            let info = manager.get::<i32, PluginInfo>(i)?;
+            if let Some(func) = info.build.on_start {
+                func.call::<Table, ()>(args.clone())?;
+            }
+        }
+
+        Ok(())
+    }
+
+    pub fn on_build_finish(crate_config: &CrateConfig, platform: &str) -> anyhow::Result<()> {
+        let lua = LUA.lock().unwrap();
+
+        if !lua.globals().contains_key("manager")? {
+            return Ok(());
+        }
+        let manager = lua.globals().get::<_, Table>("manager")?;
+
+        let args = lua.create_table()?;
+        args.set("name", crate_config.dioxus_config.application.name.clone())?;
+        args.set("platform", platform)?;
+        args.set("out_dir", crate_config.out_dir.to_str().unwrap())?;
+        args.set("asset_dir", crate_config.asset_dir.to_str().unwrap())?;
+
+        for i in 1..(manager.len()? as i32 + 1) {
+            let info = manager.get::<i32, PluginInfo>(i)?;
+            if let Some(func) = info.build.on_finish {
+                func.call::<Table, ()>(args.clone())?;
+            }
+        }
+
+        Ok(())
+    }
+
+    pub fn on_serve_start(crate_config: &CrateConfig) -> anyhow::Result<()> {
+        let lua = LUA.lock().unwrap();
+
+        if !lua.globals().contains_key("manager")? {
+            return Ok(());
+        }
+        let manager = lua.globals().get::<_, Table>("manager")?;
+
+        let args = lua.create_table()?;
+        args.set("name", crate_config.dioxus_config.application.name.clone())?;
+
+        for i in 1..(manager.len()? as i32 + 1) {
+            let info = manager.get::<i32, PluginInfo>(i)?;
+            if let Some(func) = info.serve.on_start {
+                func.call::<Table, ()>(args.clone())?;
+            }
+        }
+
+        Ok(())
+    }
+
+    pub fn on_serve_rebuild(timestamp: i64, files: Vec<PathBuf>) -> anyhow::Result<()> {
+        let lua = LUA.lock().unwrap();
+
+        let manager = lua.globals().get::<_, Table>("manager")?;
+
+        let args = lua.create_table()?;
+        args.set("timestamp", timestamp)?;
+        let files: Vec<String> = files
+            .iter()
+            .map(|v| v.to_str().unwrap().to_string())
+            .collect();
+        args.set("changed_files", files)?;
+
+        for i in 1..(manager.len()? as i32 + 1) {
+            let info = manager.get::<i32, PluginInfo>(i)?;
+            if let Some(func) = info.serve.on_rebuild {
+                func.call::<Table, ()>(args.clone())?;
+            }
+        }
+
+        Ok(())
+    }
+
+    pub fn on_serve_shutdown(crate_config: &CrateConfig) -> anyhow::Result<()> {
+        let lua = LUA.lock().unwrap();
+
+        if !lua.globals().contains_key("manager")? {
+            return Ok(());
+        }
+        let manager = lua.globals().get::<_, Table>("manager")?;
+
+        let args = lua.create_table()?;
+        args.set("name", crate_config.dioxus_config.application.name.clone())?;
+
+        for i in 1..(manager.len()? as i32 + 1) {
+            let info = manager.get::<i32, PluginInfo>(i)?;
+            if let Some(func) = info.serve.on_shutdown {
+                func.call::<Table, ()>(args.clone())?;
+            }
+        }
+
+        Ok(())
+    }
+
+    pub fn init_plugin_dir() -> PathBuf {
+        let app_path = app_path();
+        let plugin_path = app_path.join("plugins");
+        if !plugin_path.is_dir() {
+            log::info!("📖 Start to init plugin library ...");
+            let url = "https://github.com/DioxusLabs/cli-plugin-library";
+            clone_repo(&plugin_path, url).unwrap();
+        }
+        plugin_path
+    }
+
+    pub fn plugin_list() -> Vec<String> {
+        let mut res = vec![];
+
+        if let Ok(lua) = LUA.lock() {
+            let list = lua
+                .load(mlua::chunk!(
+                    local list = {}
+                    for key, value in ipairs(manager) do
+                        table.insert(list, {name = value.name, loader = value.inner.from_loader})
+                    end
+                    return list
+                ))
+                .eval::<Vec<Table>>()
+                .unwrap_or_default();
+            for i in list {
+                let name = i.get::<_, String>("name").unwrap();
+                let loader = i.get::<_, bool>("loader").unwrap();
+
+                let text = if loader {
+                    format!("{name} [:loader]")
+                } else {
+                    name
+                };
+                res.push(text);
+            }
+        }
+
+        res
+    }
+}

+ 138 - 0
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<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)
+            }
+        })
+    }
+}

+ 37 - 23
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<notify::Event>| {
-            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<notify::Event>| {
+        let config = watcher_config.clone();
+        if let Ok(e) = info {
+            if chrono::Local::now().timestamp() > last_update_time {
+                match build_manager.rebuild() {
+                    Ok(res) => {
+                        last_update_time = chrono::Local::now().timestamp();
+                        print_console_info(
+                            port,
+                            &config,
+                            PrettierOptions {
+                                changed: e.paths.clone(),
+                                warnings: res.warnings,
+                                elapsed_time: res.elapsed_time,
+                            },
+                        );
+                        let _ = PluginManager::on_serve_rebuild(
+                            chrono::Local::now().timestamp(),
+                            e.paths,
+                        );
                     }
+                    Err(e) => log::error!("{}", e),
                 }
             }
-        },
-        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(

+ 14 - 4
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)?;