浏览代码

Merge pull request #7 from mrxiaozhuox/master

Complete basic version development
YuKun Liu 3 年之前
父节点
当前提交
51bc2ff0a4

+ 11 - 5
Cargo.toml

@@ -1,6 +1,6 @@
 [package]
 name = "dioxus-cli"
-version = "0.1.1"
+version = "0.1.2"
 authors = ["Jonathan Kelley"]
 edition = "2018"
 description = "CLI tool for developing, testing, and publishing Dioxus apps"
@@ -12,23 +12,29 @@ license = "MIT/Apache-2.0"
 thiserror = "1.0.23"
 log = "0.4.13"
 fern = { version = "0.6.0", features = ["colored"] }
-wasm-bindgen-cli-support = "0.2.78"
+wasm-bindgen-cli-support = "0.2.79"
 anyhow = "1.0.38"
 serde = { version = "1.0.133", features = ["derive"] }
 serde_json = "1"
+toml = "0.5.8"
 fs_extra = "1.2.0"
 cargo_toml = "0.10.0"
 futures = "0.3.12"
-notify = "4.0.17"
+notify = { version = "5.0.0-pre.13", features = ["serde"] }
 html_parser = "0.6.2"
-tui = { version = "0.16.0", default-features = false, features = ["crossterm"] }
-crossterm = "0.22.1"
 binary-install = "0.0.2"
 convert_case = "0.5.0"
 structopt = "0.3.25"
 cargo_metadata = "0.14.1"
 tokio = { version = "1.15.0", features = ["full"] }
 atty = "0.2.14"
+regex = "1.5.4"
+chrono = "0.4.19"
+
+axum = { version = "0.4.4", features = ["ws", "headers"] }
+tower-http = { version = "0.2.0", features = ["fs", "trace"] }
+headers = "0.3"
+# hyper = { version = "0.14.11", features = ["full"] }
 
 [[bin]]
 path = "src/main.rs"

+ 2 - 3
README.md

@@ -10,7 +10,7 @@
 
 # About
 
-dioxus-studio (inspired by wasm-pack and webpack) is a tool to help get dioxus projects off the ground. It handles all the build, development, bundling, and publishing to make web development simple.
+dioxus-cli (inspired by wasm-pack and webpack) is a tool to help get dioxus projects off the ground. It handles all the build, development, bundling, and publishing to make web development simple.
 
 
 ## Installation
@@ -19,5 +19,4 @@ dioxus-studio (inspired by wasm-pack and webpack) is a tool to help get dioxus p
 $ cargo install dioxus-cli
 ```
 
-Now, `dioxus` is in your path.
-
+Now, `dioxus` is in your path.

+ 0 - 8
packages/rsx-translate/Cargo.toml

@@ -1,8 +0,0 @@
-[package]
-name = "rsx-translate"
-version = "0.1.0"
-edition = "2021"
-
-# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
-
-[dependencies]

+ 0 - 8
packages/rsx-translate/src/lib.rs

@@ -1,8 +0,0 @@
-#[cfg(test)]
-mod tests {
-    #[test]
-    fn it_works() {
-        let result = 2 + 2;
-        assert_eq!(result, 4);
-    }
-}

+ 23 - 0
src/assets/autoreload.js

@@ -0,0 +1,23 @@
+(function () {
+  var protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
+  var url = protocol + '//' + window.location.host + '/_dioxus/ws';
+  var poll_interval = 8080;
+  var reload_upon_connect = () => {
+      window.setTimeout(
+          () => {
+              var ws = new WebSocket(url);
+              ws.onopen = () => window.location.reload();
+              ws.onclose = reload_upon_connect;
+          },
+          poll_interval);
+  };
+
+  var ws = new WebSocket(url);
+  ws.onmessage = (ev) => {
+      if (ev.data == "reload") {
+        //   alert("reload!!!");
+          window.location.reload();
+      }
+  };
+  ws.onclose = reload_upon_connect;
+})()

+ 38 - 0
src/assets/dioxus.toml

@@ -0,0 +1,38 @@
+[application]
+
+# App (Project) Name
+name = "{{project-name}}"
+
+# Dioxus App Support Platform
+# desktop, web, mobile, ssr
+platforms = ["web"]
+
+# Web `build` & `serve` dist path
+out_dir = "dist"
+
+[web.app]
+
+# HTML title tag content
+title = "dioxus | ⛺"
+
+# resource (static) file folder
+public_dir = "public"
+
+[web.watcher]
+
+watch_path = ["src"]
+
+# include `assets` in web platform
+[web.resource]
+
+# CSS style file
+style = []
+
+# Javascript code file
+script = []
+
+[web.resource.dev]
+
+# Javascript code file
+# serve: [dev-server] only
+script = []

+ 18 - 0
src/assets/index.html

@@ -0,0 +1,18 @@
+<html>
+  <head>
+    <title>{app_title}</title>
+    <meta content="text/html;charset=utf-8" http-equiv="Content-Type" />
+    <meta charset="UTF-8" />
+    {style_include}
+    
+  </head>
+  <body>
+    <div id="main"></div>
+    <!-- Note the usage of `type=module` here as this is an ES6 module -->
+    <script type="module">
+      import init from "./assets/dioxus/{app_name}.js";
+      init("./assets/dioxus/{app_name}_bg.wasm");
+    </script>
+    {script_include}
+  </body>
+</html>

+ 164 - 50
src/builder.rs

@@ -1,9 +1,11 @@
 use crate::{
     config::{CrateConfig, ExecutableType},
     error::{Error, Result},
+    DioxusConfig,
 };
 use std::{
-    io::{Read, Write},
+    fs::{copy, create_dir_all, remove_dir_all},
+    path::PathBuf,
     process::Command,
 };
 use wasm_bindgen_cli_support::Bindgen;
@@ -21,22 +23,23 @@ pub fn build(config: &CrateConfig) -> Result<()> {
         out_dir,
         crate_dir,
         target_dir,
-        static_dir,
+        public_dir,
         executable,
+        dioxus_config,
         ..
     } = config;
 
     let t_start = std::time::Instant::now();
 
     // [1] Build the .wasm module
-    log::info!("Running build commands...");
+    log::info!("🚅 Running build command...");
     let mut cmd = Command::new("cargo");
     cmd.current_dir(&crate_dir)
         .arg("build")
         .arg("--target")
         .arg("wasm32-unknown-unknown")
-        .stdout(std::process::Stdio::piped())
-        .stderr(std::process::Stdio::piped());
+        .stdout(std::process::Stdio::inherit())
+        .stderr(std::process::Stdio::inherit());
 
     if config.release {
         cmd.arg("--release");
@@ -48,20 +51,16 @@ pub fn build(config: &CrateConfig) -> Result<()> {
         ExecutableType::Example(name) => cmd.arg("--example").arg(name),
     };
 
-    let mut child = cmd.spawn()?;
-    let output = child.wait()?;
+    let output = cmd.output()?;
 
-    if output.success() {
-        log::info!("Build complete!");
-    } else {
+    if !output.status.success() {
         log::error!("Build failed!");
-        let mut reason = String::new();
-        child.stderr.unwrap().read_to_string(&mut reason)?;
+        let reason = String::from_utf8_lossy(&output.stderr).to_string();
         return Err(Error::BuildFailed(reason));
     }
 
     // [2] Establish the output directory structure
-    let bindgen_outdir = out_dir.join("wasm");
+    let bindgen_outdir = out_dir.join("assets").join("dioxus");
 
     // [3] Bindgen the final binary for use easy linking
     let mut bindgen_builder = Bindgen::new();
@@ -89,53 +88,168 @@ pub fn build(config: &CrateConfig) -> Result<()> {
         .keep_debug(true)
         .remove_name_section(false)
         .remove_producers_section(false)
-        .out_name("module")
+        .out_name(&dioxus_config.application.name)
         .generate(&bindgen_outdir)?;
 
-    // [4]
-    // TODO: wasm-opt
+    let copy_options = fs_extra::dir::CopyOptions {
+        overwrite: true,
+        skip_exist: false,
+        buffer_size: 64000,
+        copy_inside: false,
+        content_only: false,
+        depth: 0,
+    };
+    if public_dir.is_dir() {
+        for entry in std::fs::read_dir(&public_dir)? {
+            let path = entry?.path();
+            if path.is_file() {
+                std::fs::copy(&path, out_dir.join(path.file_name().unwrap()))?;
+            } else {
+                match fs_extra::dir::copy(&path, out_dir, &copy_options) {
+                    Ok(_) => {}
+                    Err(_e) => {
+                        log::warn!("Error copying dir: {}", _e);
+                    }
+                }
+            }
+        }
+    }
+
+    let t_end = std::time::Instant::now();
+    log::info!("🏁 Done in {}ms!", (t_end - t_start).as_millis());
+    Ok(())
+}
+
+pub fn build_desktop(config: &CrateConfig) -> Result<()> {
+    log::info!("🚅 Running build [Desktop] command...");
+
+    let mut cmd = Command::new("cargo");
+    cmd.current_dir(&config.crate_dir)
+        .arg("build")
+        .stdout(std::process::Stdio::inherit())
+        .stderr(std::process::Stdio::inherit());
+
+    if config.release {
+        cmd.arg("--release");
+    }
 
-    // [5] Generate the html file with the module name
-    // TODO: support names via options
-    log::info!("Writing to '{:#?}' directory...", out_dir);
-    let mut file = std::fs::File::create(out_dir.join("index.html"))?;
-    file.write_all(gen_page("./wasm/module.js").as_str().as_bytes())?;
+    match &config.executable {
+        crate::ExecutableType::Binary(name) => cmd.arg("--bin").arg(name),
+        crate::ExecutableType::Lib(name) => cmd.arg("--lib").arg(name),
+        crate::ExecutableType::Example(name) => cmd.arg("--example").arg(name),
+    };
+
+    let output = cmd.output()?;
 
-    let copy_options = fs_extra::dir::CopyOptions::new();
-    match fs_extra::dir::copy(static_dir, out_dir, &copy_options) {
-        Ok(_) => {}
-        Err(_e) => {
-            log::warn!("Error copying dir");
+    if !output.status.success() {
+        return Err(Error::BuildFailed("Program build failed.".into()));
+    }
+
+    if output.status.success() {
+        if config.out_dir.is_dir() {
+            remove_dir_all(&config.out_dir)?;
         }
+
+        let release_type = match config.release {
+            true => "release",
+            false => "debug",
+        };
+
+        let file_name: String;
+        let mut res_path = match &config.executable {
+            crate::ExecutableType::Binary(name) | crate::ExecutableType::Lib(name) => {
+                file_name = name.clone();
+                config
+                    .target_dir
+                    .join(release_type.to_string())
+                    .join(name.to_string())
+            }
+            crate::ExecutableType::Example(name) => {
+                file_name = name.clone();
+                config
+                    .target_dir
+                    .join(release_type.to_string())
+                    .join("examples")
+                    .join(name.to_string())
+            }
+        };
+
+        let target_file = if cfg!(windows) {
+            res_path.set_extension("exe");
+            format!("{}.exe", &file_name)
+        } else {
+            file_name
+        };
+
+        create_dir_all(&config.out_dir)?;
+        copy(res_path, &config.out_dir.join(target_file))?;
+
+        log::info!(
+            "🚩 Build completed: [./{}]",
+            config
+                .dioxus_config
+                .application
+                .out_dir
+                .clone()
+                .unwrap_or_else(|| PathBuf::from("dist"))
+                .display()
+        );
     }
 
-    let t_end = std::time::Instant::now();
-    log::info!("Done in {}ms! 🎉", (t_end - t_start).as_millis());
     Ok(())
 }
 
-fn gen_page(module: &str) -> String {
-    format!(
-        r#"
-<html>
-  <head>
-    <meta content="text/html;charset=utf-8" http-equiv="Content-Type" />
-    <meta charset="UTF-8" />
-  </head>
-  <body>
-    <div id="main">
-    </div>
-    <!-- Note the usage of `type=module` here as this is an ES6 module -->
-    <script type="module">
-      import init from "{}";
-      init("./wasm/module_bg.wasm");
-    </script>
-    <div id="dioxusroot"> </div>
-  </body>
-</html>
-"#,
-        module
-    )
+pub fn gen_page(config: &DioxusConfig, serve: bool) -> String {
+    let mut html = String::from(include_str!("./assets/index.html"));
+
+    let resouces = config.web.resource.clone();
+
+    let mut style_list = resouces.style.unwrap_or_default();
+    let mut script_list = resouces.script.unwrap_or_default();
+
+    if serve {
+        let mut dev_style = resouces.dev.style.clone().unwrap_or_default();
+        let mut dev_script = resouces.dev.script.unwrap_or_default();
+        style_list.append(&mut dev_style);
+        script_list.append(&mut dev_script);
+    }
+
+    let mut style_str = String::new();
+    for style in style_list {
+        style_str.push_str(&format!(
+            "<link rel=\"stylesheet\" href=\"{}\">\n",
+            &style.to_str().unwrap(),
+        ))
+    }
+    html = html.replace("{style_include}", &style_str);
+
+    let mut script_str = String::new();
+    for script in script_list {
+        script_str.push_str(&format!(
+            "<script src=\"{}\"></script>\n",
+            &script.to_str().unwrap(),
+        ))
+    }
+
+    html = html.replace("{script_include}", &script_str);
+
+    if serve {
+        html += &format!(
+            "<script>{}</script>",
+            include_str!("./assets/autoreload.js")
+        );
+    }
+
+    html = html.replace("{app_name}", &config.application.name);
+
+    let title = config
+        .web
+        .app
+        .title
+        .clone()
+        .unwrap_or_else(|| "dioxus | ⛺".into());
+
+    html.replace("{app_title}", &title)
 }
 
 // use binary_install::{Cache, Download};

+ 0 - 4
src/buildsystem/assets.rs

@@ -1,4 +0,0 @@
-/*
-
-todo: wasm splitting
-*/

+ 0 - 1
src/buildsystem/wasm.rs

@@ -1 +0,0 @@
-

+ 8 - 3
src/cargo.rs

@@ -1,6 +1,11 @@
 //! Utilities for working with cargo and rust files
 use crate::error::{Error, Result};
-use std::{env, fs, path::PathBuf, process::Command, str};
+use std::{
+    env, fs,
+    path::{Path, PathBuf},
+    process::Command,
+    str,
+};
 
 /// How many parent folders are searched for a `Cargo.toml`
 const MAX_ANCESTORS: u32 = 10;
@@ -44,7 +49,7 @@ pub fn workspace_root() -> Result<PathBuf> {
     }
 
     let stdout = str::from_utf8(&output.stdout).unwrap();
-    for line in stdout.lines() {
+    if let Some(line) = stdout.lines().next() {
         let meta: serde_json::Value = serde_json::from_str(line)
             .map_err(|_| Error::CargoError("InvalidOutput".to_string()))?;
 
@@ -58,7 +63,7 @@ pub fn workspace_root() -> Result<PathBuf> {
 }
 
 /// Checks if the directory contains `Cargo.toml`
-fn contains_manifest(path: &PathBuf) -> bool {
+fn contains_manifest(path: &Path) -> bool {
     fs::read_dir(path)
         .map(|entries| {
             entries

+ 46 - 4
src/cli/build/mod.rs

@@ -1,6 +1,6 @@
-use crate::cfg::ConfigOptsBuild;
-use anyhow::Result;
-use std::path::PathBuf;
+use std::{io::Write, path::PathBuf};
+
+use crate::{cfg::ConfigOptsBuild, gen_page};
 use structopt::StructOpt;
 
 /// Build the Rust WASM app and all of its assets.
@@ -11,4 +11,46 @@ pub struct Build {
     pub build: ConfigOptsBuild,
 }
 
-pub async fn perform_build() {}
+impl Build {
+    pub fn build(self) -> anyhow::Result<()> {
+        let mut crate_config = crate::CrateConfig::new()?;
+
+        // change the relase state.
+        crate_config.with_release(self.build.release);
+
+        if self.build.example.is_some() {
+            crate_config.as_example(self.build.example.unwrap());
+        }
+
+        match self.build.platform.as_str() {
+            "web" => {
+                crate::builder::build(&crate_config)?;
+            }
+            "desktop" => {
+                crate::builder::build_desktop(&crate_config)?;
+            }
+            _ => {
+                return Err(anyhow::anyhow!("Unsoppurt platform target."));
+            }
+        }
+
+        let temp = gen_page(&crate_config.dioxus_config, false);
+
+        let mut file = std::fs::File::create(
+            crate_config
+                .crate_dir
+                .join(
+                    crate_config
+                        .dioxus_config
+                        .application
+                        .out_dir
+                        .clone()
+                        .unwrap_or_else(|| PathBuf::from("dist")),
+                )
+                .join("index.html"),
+        )?;
+        file.write_all(temp.as_bytes())?;
+
+        Ok(())
+    }
+}

+ 23 - 46
src/cli/cfg.rs

@@ -1,11 +1,7 @@
-use anyhow::Result;
 use std::path::PathBuf;
 use structopt::StructOpt;
 
-use serde::{Deserialize, Serialize};
-use std::collections::HashMap;
-use std::str::FromStr;
-use std::sync::Arc;
+use serde::Deserialize;
 
 /// Config options for the build system.
 #[derive(Clone, Debug, Default, Deserialize, StructOpt)]
@@ -19,52 +15,33 @@ pub struct ConfigOptsBuild {
     #[serde(default)]
     pub release: bool,
 
-    /// The output dir for all final assets [default: dist]
-    #[structopt(short, long, parse(from_os_str))]
-    pub dist: Option<PathBuf>,
+    /// Build a example [default: ""]
+    #[structopt(long)]
+    pub example: Option<String>,
 
-    /// The public URL from which assets are to be served [default: /]
-    #[structopt(long, parse(from_str=parse_public_url))]
-    pub public_url: Option<String>,
+    /// Build platform: support Web & Desktop [default: "web"]
+    #[structopt(long, default_value = "web")]
+    pub platform: String,
+}
 
-    /// Optional pattern for the app loader script [default: None]
-    ///
-    /// Patterns should include the sequences `{base}`, `{wasm}`, and `{js}` in order to
-    /// properly load the application. Other sequences may be included corresponding
-    /// to key/value pairs provided in `pattern_params`.
-    ///
-    /// These values can only be provided via config file.
-    #[structopt(skip)]
-    #[serde(default)]
-    pub pattern_script: Option<String>,
+#[derive(Clone, Debug, Default, Deserialize, StructOpt)]
+pub struct ConfigOptsServe {
+    /// The index HTML file to drive the bundling process [default: index.html]
+    #[structopt(parse(from_os_str))]
+    pub target: Option<PathBuf>,
 
-    /// Optional pattern for the app preload element [default: None]
-    ///
-    /// Patterns should include the sequences `{base}`, `{wasm}`, and `{js}` in order to
-    /// properly preload the application. Other sequences may be included corresponding
-    /// to key/value pairs provided in `pattern_params`.
-    ///
-    /// These values can only be provided via config file.
-    #[structopt(skip)]
-    #[serde(default)]
-    pub pattern_preload: Option<String>,
+    /// Build a example [default: ""]
+    #[structopt(long)]
+    pub example: Option<String>,
 
-    #[structopt(skip)]
+    /// Build in release mode [default: false]
+    #[structopt(long)]
     #[serde(default)]
-    /// Optional replacement parameters corresponding to the patterns provided in
-    /// `pattern_script` and `pattern_preload`.
-    ///
-    /// When a pattern is being replaced with its corresponding value from this map, if the value is
-    /// prefixed with the symbol `@`, then the value is expected to be a file path, and the pattern
-    /// will be replaced with the contents of the target file. This allows insertion of some big JSON
-    /// state or even HTML files as a part of the `index.html` build.
-    ///
-    /// Trunk will automatically insert the `base`, `wasm` and `js` key/values into this map. In order
-    //// for the app to be loaded properly, the patterns `{base}`, `{wasm}` and `{js}` should be used
-    /// in `pattern_script` and `pattern_preload`.
-    ///
-    /// These values can only be provided via config file.
-    pub pattern_params: Option<HashMap<String, String>>,
+    pub release: bool,
+
+    /// Build platform: support Web & Desktop [default: "web"]
+    #[structopt(long, default_value = "web")]
+    pub platform: String,
 }
 
 /// Ensure the given value for `--public-url` is formatted correctly.

+ 34 - 3
src/cli/clean/mod.rs

@@ -1,9 +1,40 @@
-use crate::cfg::ConfigOptsBuild;
-use anyhow::Result;
-use std::path::PathBuf;
+use std::{
+    fs::remove_dir_all,
+    path::PathBuf,
+    process::{Command, Stdio},
+};
+
 use structopt::StructOpt;
 
 /// Build the Rust WASM app and all of its assets.
 #[derive(Clone, Debug, StructOpt)]
 #[structopt(name = "clean")]
 pub struct Clean {}
+
+impl Clean {
+    pub fn clean(self) -> anyhow::Result<()> {
+        let crate_config = crate::CrateConfig::new()?;
+
+        let output = Command::new("cargo")
+            .arg("clean")
+            .stdout(Stdio::piped())
+            .stderr(Stdio::piped())
+            .output()?;
+
+        if !output.status.success() {
+            log::error!("Cargo clean failed.");
+            return Ok(());
+        }
+
+        let out_dir = crate_config
+            .dioxus_config
+            .application
+            .out_dir
+            .unwrap_or_else(|| PathBuf::from("dist"));
+        if crate_config.crate_dir.join(&out_dir).is_dir() {
+            remove_dir_all(crate_config.crate_dir.join(&out_dir))?;
+        }
+
+        Ok(())
+    }
+}

+ 39 - 5
src/cli/config/mod.rs

@@ -1,9 +1,43 @@
-use crate::cfg::ConfigOptsBuild;
-use anyhow::Result;
-use std::path::PathBuf;
+use std::{fs::File, io::Write};
+
+use serde::Deserialize;
 use structopt::StructOpt;
 
 /// Build the Rust WASM app and all of its assets.
-#[derive(Clone, Debug, StructOpt)]
+#[derive(Clone, Debug, Deserialize, StructOpt)]
 #[structopt(name = "config")]
-pub struct Config {}
+pub enum Config {
+    /// Init `Dioxus.toml` for project/folder.
+    Init {
+        /// Init project name
+        name: String,
+
+        /// Cover old config
+        #[structopt(long)]
+        #[serde(default)]
+        force: bool,
+    },
+}
+
+impl Config {
+    pub fn config(self) -> anyhow::Result<()> {
+        let crate_root = crate::cargo::crate_root()?;
+        match self {
+            Config::Init { name, force } => {
+                let conf_path = crate_root.join("Dioxus.toml");
+                if conf_path.is_file() && !force {
+                    log::warn!(
+                        "config file `Dioxus.toml` already exist, use `--force` to overwrite it."
+                    );
+                    return Ok(());
+                }
+                let mut file = File::create(conf_path)?;
+                let content = String::from(include_str!("../../assets/dioxus.toml"))
+                    .replace("{{project-name}}", &name);
+                file.write_all(content.as_bytes())?;
+                log::info!("🚩 Init config file completed.");
+            }
+        }
+        Ok(())
+    }
+}

+ 98 - 0
src/cli/create/mod.rs

@@ -0,0 +1,98 @@
+use std::{
+    fs::File,
+    io::{Read, Write},
+    path::PathBuf,
+    process::{Command, Stdio},
+};
+
+use regex::Regex;
+use serde::Deserialize;
+use structopt::StructOpt;
+
+use crate::{error::Result, Error};
+
+/// Build the Rust WASM app and all of its assets.
+#[derive(Clone, Debug, Default, Deserialize, StructOpt)]
+#[structopt(name = "create")]
+pub struct Create {
+    /// Init project name
+    #[structopt(default_value = ".")]
+    name: String,
+
+    /// Template path
+    #[structopt(default_value = "gh:dioxuslabs/dioxus-template", long)]
+    template: String,
+}
+
+impl Create {
+    pub fn create(self) -> Result<()> {
+        if Self::name_vaild_check(self.name.clone()) {
+            log::error!("❗Unsupported project name.");
+            return Ok(());
+        }
+
+        let project_path = PathBuf::from(&self.name);
+
+        if project_path.join("Dioxus.toml").is_file() || project_path.join("Cargo.toml").is_file() {
+            return Err(Error::Other(anyhow::anyhow!(
+                "🧨 Folder '{}' is initialized.",
+                &self.name
+            )));
+        }
+
+        log::info!("🔧 Start to create a new project '{}'.", self.name);
+
+        let output = Command::new("cargo")
+            .arg("generate")
+            .arg("--help")
+            .stdout(Stdio::piped())
+            .stderr(Stdio::piped())
+            .output()?;
+
+        if !output.status.success() {
+            log::warn!("Tool is not installed: cargo-generate, try to install it.");
+            let install_output = Command::new("cargo")
+                .arg("install")
+                .arg("cargo-generate")
+                .stdout(Stdio::inherit())
+                .stderr(Stdio::inherit())
+                .output()?;
+            if !install_output.status.success() {
+                return Err(Error::Other(anyhow::anyhow!(
+                    "Try to install cargo-generate failed."
+                )));
+            }
+        }
+
+        let generate_output = Command::new("cargo")
+            .arg("generate")
+            .arg(&self.template)
+            .arg("--name")
+            .arg(&self.name)
+            .stdout(Stdio::piped())
+            .stderr(Stdio::inherit())
+            .output()?;
+
+        if !generate_output.status.success() {
+            return Err(Error::Other(anyhow::anyhow!("Generate project failed.")));
+        }
+
+        let mut dioxus_file = File::open(project_path.join("Dioxus.toml"))?;
+        let mut meta_file = String::new();
+        dioxus_file.read_to_string(&mut meta_file)?;
+        meta_file = meta_file.replace("{{project-name}}", &self.name);
+        File::create(project_path.join("Dioxus.toml"))?.write_all(meta_file.as_bytes())?;
+
+        println!();
+        log::info!("💡 Project initialized:");
+        log::info!("🎯> cd ./{}", self.name);
+        log::info!("🎯> dioxus serve");
+
+        Ok(())
+    }
+
+    fn name_vaild_check(name: String) -> bool {
+        let r = Regex::new(r"^[a-zA-Z][a-zA-Z0-9\-_]$").unwrap();
+        r.is_match(&name)
+    }
+}

+ 11 - 13
src/cli/mod.rs

@@ -1,14 +1,12 @@
-use anyhow::{Context, Result};
-use std::path::PathBuf;
 use structopt::StructOpt;
 
 pub mod build;
 pub mod cfg;
 pub mod clean;
 pub mod config;
+pub mod create;
 pub mod serve;
 pub mod translate;
-pub mod watch;
 
 /// Build, bundle, & ship your Dioxus app.
 ///
@@ -31,16 +29,16 @@ pub struct Cli {
 
 #[derive(StructOpt)]
 pub enum Commands {
-    // /// Build the Rust WASM app and all of its assets.
-    // Build(build::Build),
+    /// Build the Rust WASM app and all of its assets.
+    Build(build::Build),
     /// Translate some source file into Dioxus code.
     Translate(translate::Translate),
-    // /// Build, watch & serve the Rust WASM app and all of its assets.
-    // Serve(serve::Serve),
-
-    // /// Clean output artifacts.
-    // Clean(clean::Clean),
-
-    // /// Trunk config controls.
-    // Config(config::Config),
+    /// Build, watch & serve the Rust WASM app and all of its assets.
+    Serve(serve::Serve),
+    /// Init a new project for Dioxus.
+    Create(create::Create),
+    /// Clean output artifacts.
+    Clean(clean::Clean),
+    /// Dioxus config file controls.
+    Config(config::Config),
 }

+ 0 - 145
src/cli/serve/develop.rs

@@ -1,145 +0,0 @@
-use crate::{cli::DevelopOptions, config::CrateConfig, error::Result};
-use async_std::prelude::FutureExt;
-
-use log::info;
-use notify::{RecommendedWatcher, RecursiveMode, Watcher};
-use std::path::PathBuf;
-use std::sync::atomic::AtomicBool;
-use std::sync::Arc;
-use tide::http::mime::HTML;
-use tide::http::Mime;
-
-pub struct DevelopState {
-    //
-    reload_on_change: bool,
-}
-
-pub async fn develop(options: DevelopOptions) -> Result<()> {
-    //
-    log::info!("Starting development server 🚀");
-    let mut cfg = CrateConfig::new()?;
-    cfg.with_develop_options(&options);
-
-    let out_dir = cfg.out_dir.clone();
-
-    let is_err = Arc::new(AtomicBool::new(false));
-
-    // Spawn the server onto a seperate task
-    // This lets the task progress while we handle file updates
-    let server = async_std::task::spawn(launch_server(out_dir, is_err.clone()));
-    let watcher = async_std::task::spawn(watch_directory(cfg.clone(), is_err.clone()));
-
-    match server.race(watcher).await {
-        Err(e) => log::warn!("Error running development server, {:?}", e),
-        _ => {}
-    }
-
-    Ok(())
-}
-
-async fn watch_directory(config: CrateConfig, is_err: ErrStatus) -> Result<()> {
-    // Create a channel to receive the events.
-    let (watcher_tx, watcher_rx) = async_std::channel::bounded(100);
-
-    // Automatically select the best implementation for your platform.
-    // You can also access each implementation directly e.g. INotifyWatcher.
-    let mut watcher: RecommendedWatcher = Watcher::new(move |res| {
-        async_std::task::block_on(watcher_tx.send(res));
-        // send an event
-        let _ = async_std::task::block_on(watcher_tx.send(res));
-    })
-    .expect("failed to make watcher");
-
-    let src_dir = crate::cargo::crate_root()?;
-
-    // Add a path to be watched. All files and directories at that path and
-    // below will be monitored for changes.
-    watcher
-        .watch(&src_dir.join("src"), RecursiveMode::Recursive)
-        .expect("Failed to watch dir");
-
-    match watcher.watch(&src_dir.join("examples"), RecursiveMode::Recursive) {
-        Ok(_) => {}
-        Err(e) => log::warn!("Failed to watch examples dir, {:?}", e),
-    }
-
-    'run: loop {
-        match crate::builder::build(&config) {
-            Ok(_) => {
-                is_err.store(false, std::sync::atomic::Ordering::Relaxed);
-                async_std::task::sleep(std::time::Duration::from_millis(500)).await;
-            }
-            Err(err) => is_err.store(true, std::sync::atomic::Ordering::Relaxed),
-        };
-
-        let mut msg = None;
-        loop {
-            let new_msg = watcher_rx.recv().await.unwrap().unwrap();
-            if !watcher_rx.is_empty() {
-                msg = Some(new_msg);
-                break;
-            }
-        }
-
-        info!("File updated, rebuilding app");
-    }
-    Ok(())
-}
-
-async fn launch_server(outdir: PathBuf, is_err: ErrStatus) -> Result<()> {
-    let _crate_dir = crate::cargo::crate_root()?;
-    let _workspace_dir = crate::cargo::workspace_root()?;
-
-    let mut app = tide::with_state(ServerState::new(outdir.to_owned(), is_err));
-
-    let file_path = format!("{}/index.html", outdir.display());
-    log::info!("Serving {}", file_path);
-    let p = outdir.display().to_string();
-
-    app.at("/")
-        .get(|req: tide::Request<ServerState>| async move {
-            log::info!("Connected to development server");
-            let state = req.state();
-
-            match state.is_err.load(std::sync::atomic::Ordering::Relaxed) {
-                true => {
-                    //
-                    let mut resp =
-                        tide::Body::from_string(format!(include_str!("../err.html"), err = "_"));
-                    resp.set_mime(HTML);
-
-                    Ok(resp)
-                }
-                false => {
-                    Ok(tide::Body::from_file(state.serv_path.clone().join("index.html")).await?)
-                }
-            }
-        })
-        .serve_dir(p)?;
-    // .serve_file(file_path)
-    // .unwrap();
-
-    let port = "8080";
-    let serve_addr = format!("127.0.0.1:{}", port);
-
-    info!("App available at http://{}/", serve_addr);
-    app.listen(serve_addr).await?;
-    Ok(())
-}
-
-/// https://github.com/http-rs/tide/blob/main/examples/state.rs
-/// Tide seems to prefer using state instead of injecting into the app closure
-/// The app closure needs to be static and
-#[derive(Clone)]
-struct ServerState {
-    serv_path: PathBuf,
-    is_err: ErrStatus,
-}
-
-type ErrStatus = Arc<AtomicBool>;
-
-impl ServerState {
-    fn new(serv_path: PathBuf, is_err: ErrStatus) -> Self {
-        Self { serv_path, is_err }
-    }
-}

+ 0 - 0
src/cli/serve/draw.rs


+ 0 - 0
src/cli/serve/events.rs


+ 83 - 5
src/cli/serve/mod.rs

@@ -1,9 +1,87 @@
-use crate::cfg::ConfigOptsBuild;
-use anyhow::Result;
-use std::path::PathBuf;
+use crate::{cfg::ConfigOptsServe, gen_page, server, CrateConfig};
+use std::{io::Write, path::PathBuf, process::{Command, Stdio}};
 use structopt::StructOpt;
 
-/// Build the Rust WASM app and all of its assets.
+/// Run the WASM project on dev-server
 #[derive(Clone, Debug, StructOpt)]
 #[structopt(name = "serve")]
-pub struct Serve {}
+pub struct Serve {
+    #[structopt(flatten)]
+    pub serve: ConfigOptsServe,
+}
+
+impl Serve {
+    pub async fn serve(self) -> anyhow::Result<()> {
+        let mut crate_config = crate::CrateConfig::new()?;
+
+        // change the relase state.
+        crate_config.with_release(self.serve.release);
+
+        if self.serve.example.is_some() {
+            crate_config.as_example(self.serve.example.unwrap());
+        }
+
+        match self.serve.platform.as_str() {
+            "web" => {
+                crate::builder::build(&crate_config)?;
+            }
+            "desktop" => {
+                crate::builder::build_desktop(&crate_config)?;
+
+                match &crate_config.executable {
+                    crate::ExecutableType::Binary(name)
+                    | crate::ExecutableType::Lib(name)
+                    | crate::ExecutableType::Example(name) => {
+                        let mut file = crate_config.out_dir.join(name);
+                        if cfg!(windows) {
+                            file.set_extension("exe");
+                        }
+                        Command::new(
+                            crate_config
+                                .out_dir
+                                .join(file)
+                                .to_str()
+                                .unwrap()
+                                .to_string(),
+                        )
+                        .stdout(Stdio::inherit())
+                        .output()?;
+                    }
+                }
+                return Ok(());
+            }
+            _ => {
+                return Err(anyhow::anyhow!("Unsoppurt platform target."));
+            }
+        }
+
+        // generate dev-index page
+        Serve::regen_dev_page(&crate_config)?;
+
+        // start the develop server
+        server::startup(crate_config.clone()).await?;
+
+        Ok(())
+    }
+
+    pub fn regen_dev_page(crate_config: &CrateConfig) -> anyhow::Result<()> {
+        let serve_html = gen_page(&crate_config.dioxus_config, true);
+
+        let mut file = std::fs::File::create(
+            crate_config
+                .crate_dir
+                .join(
+                    crate_config
+                        .dioxus_config
+                        .application
+                        .out_dir
+                        .clone()
+                        .unwrap_or_else(|| PathBuf::from("dist")),
+                )
+                .join("index.html"),
+        )?;
+        file.write_all(serve_html.as_bytes())?;
+
+        Ok(())
+    }
+}

+ 0 - 164
src/cli/serve/studio.rs

@@ -1,164 +0,0 @@
-//! It's better to store all the configuration in one spot
-
-use tui::{
-    backend::{Backend, CrosstermBackend},
-    layout::{Constraint, Direction, Layout, Rect},
-    style::{Color, Modifier, Style},
-    symbols,
-    text::{Span, Spans},
-    widgets::canvas::{Canvas, Line, Map, MapResolution, Rectangle},
-    widgets::{
-        Axis, BarChart, Block, BorderType, Borders, Cell, Chart, Dataset, Gauge, LineGauge, List,
-        ListItem, Paragraph, Row, Sparkline, Table, Tabs, Wrap,
-    },
-    Frame, Terminal,
-};
-
-use crate::*;
-use std::{any::Any, io::Write, path::PathBuf, process::Command};
-
-pub struct Cfg {
-    command: LaunchOptions,
-    headless: bool,
-    example: Option<String>,
-    outdir: Option<String>,
-    release: bool,
-    hydrate: Option<String>,
-    template: Option<String>,
-    translate_file: Option<String>,
-    crate_config: Option<CrateConfig>,
-    should_quit: bool,
-}
-
-pub async fn start(options: DevelopOptions) -> Result<()> {
-    let mut state = Cfg {
-        command: todo!(),
-        headless: todo!(),
-        example: todo!(),
-        outdir: todo!(),
-        release: todo!(),
-        hydrate: todo!(),
-        template: todo!(),
-        translate_file: todo!(),
-        crate_config: todo!(),
-        should_quit: false,
-    };
-
-    crossterm::terminal::enable_raw_mode()?;
-
-    let backend = CrosstermBackend::new(std::io::stdout());
-    let mut terminal = Terminal::new(backend).unwrap();
-
-    // Setup input handling
-    // let (tx, rx) = futures::channel::mpsc::unbounded();
-    let tick_rate = std::time::Duration::from_millis(100);
-
-    let mut prev_time = std::time::Instant::now();
-    while !state.should_quit {
-        let next_time = prev_time + tick_rate;
-        let now = std::time::Instant::now();
-
-        let diff = next_time - std::time::Instant::now();
-    }
-
-    Ok(())
-}
-
-struct TuiStudio {
-    cfg: Cfg,
-    hook_idx: usize,
-    hooks: Vec<Box<dyn Any>>,
-}
-impl TuiStudio {
-    fn use_hook<F: 'static>(&mut self, f: impl FnOnce() -> F) -> &mut F {
-        if self.hook_idx == self.hooks.len() {
-            self.hooks.push(Box::new(f()));
-        }
-        let idx = self.hook_idx;
-        self.hook_idx += 1;
-        let hook = self.hooks.get_mut(idx).unwrap();
-        let r = hook.downcast_mut::<F>().unwrap();
-        r
-    }
-}
-
-impl TuiStudio {
-    fn event_handler(&self, action: crossterm::event::Event) -> anyhow::Result<()> {
-        match action {
-            crossterm::event::Event::Key(_) => {}
-            crossterm::event::Event::Mouse(_) => {}
-            crossterm::event::Event::Resize(_, _) => {}
-        }
-        Ok(())
-    }
-
-    fn handle_key(&mut self, key: crossterm::event::KeyEvent) {}
-
-    fn tick(&mut self) {}
-
-    fn should_quit(&self) -> bool {
-        false
-    }
-
-    fn render<B: tui::backend::Backend>(&mut self, f: &mut tui::Frame<B>) {
-        self.hook_idx = 0;
-
-        // Wrapping block for a group
-        // Just draw the block and the group on the same area and build the group
-        // with at least a margin of 1
-        let size = f.size();
-        let block = Block::default()
-            .borders(Borders::ALL)
-            .title("Main block with round corners")
-            .border_type(BorderType::Rounded);
-        f.render_widget(block, size);
-        let chunks = Layout::default()
-            .direction(Direction::Vertical)
-            .margin(4)
-            .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
-            .split(f.size());
-
-        let top_chunks = Layout::default()
-            .direction(Direction::Horizontal)
-            .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
-            .split(chunks[0]);
-        let block = Block::default()
-            .title(vec![
-                Span::styled("With", Style::default().fg(Color::Yellow)),
-                Span::from(" background"),
-            ])
-            .style(Style::default().bg(Color::Green));
-        f.render_widget(block, top_chunks[0]);
-
-        let block = Block::default().title(Span::styled(
-            "Styled title",
-            Style::default()
-                .fg(Color::White)
-                .bg(Color::Red)
-                .add_modifier(Modifier::BOLD),
-        ));
-        f.render_widget(block, top_chunks[1]);
-
-        let bottom_chunks = Layout::default()
-            .direction(Direction::Horizontal)
-            .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
-            .split(chunks[1]);
-        let block = Block::default().title("With borders").borders(Borders::ALL);
-        f.render_widget(block, bottom_chunks[0]);
-        let block = Block::default()
-            .title("With styled borders and doubled borders")
-            .border_style(Style::default().fg(Color::Cyan))
-            .borders(Borders::LEFT | Borders::RIGHT)
-            .border_type(BorderType::Double);
-        f.render_widget(block, bottom_chunks[1]);
-    }
-}
-
-impl TuiStudio {
-    fn render_list<B: tui::backend::Backend>(&mut self, f: &mut tui::Frame<B>) {
-        let options = [
-            "Bundle", "Develop",
-            //
-        ];
-    }
-}

+ 2 - 2
src/cli/translate/mod.rs

@@ -48,7 +48,7 @@ impl Translate {
                 exit(0);
             })
         });
-        if let None = temp {
+        if let Some(..) = temp {
             if let Some(s) = source {
                 contents = s;
             } else {
@@ -112,7 +112,7 @@ fn render_child(f: &mut impl Write, child: &Node, il: u32) -> std::fmt::Result {
                 }
                 write!(f, "\",")?;
             }
-            write!(f, "\n")?;
+            writeln!(f)?;
 
             // write the attributes
             if let Some(id) = &el.id {

+ 3 - 1
src/cli/translate/to_component.rs

@@ -4,6 +4,7 @@
 //! - [ ] Extracts svgs
 //! - [ ] Attempts to extract lists
 
+#[allow(unused_imports)]
 use std::{
     fmt::{Display, Formatter},
     io::Write,
@@ -20,6 +21,7 @@ pub fn convert_html_to_component(html: &str) -> Result<ComponentRenderer> {
     })
 }
 
+#[allow(dead_code)]
 pub struct ComponentRenderer {
     dom: Dom,
     icon_index: usize,
@@ -118,7 +120,7 @@ fn render_element<'a>(
         }
         write!(f, "\",")?;
     }
-    write!(f, "\n")?;
+    writeln!(f)?;
 
     // write the attributes
     if let Some(id) = &el.id {

+ 0 - 1
src/cli/watch/mod.rs

@@ -1 +0,0 @@
-

+ 114 - 13
src/config.rs

@@ -1,5 +1,99 @@
 use crate::error::Result;
-use std::{io::Write, path::PathBuf, process::Command};
+use serde::{Deserialize, Serialize};
+use std::{fs::File, io::Read, path::PathBuf};
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct DioxusConfig {
+    pub application: ApplicationConfig,
+    pub web: WebConfig,
+}
+
+impl DioxusConfig {
+    pub fn load() -> crate::error::Result<DioxusConfig> {
+        let crate_dir = crate::cargo::crate_root()?;
+
+        if !crate_dir.join("Dioxus.toml").is_file() {
+            log::warn!("Config file: `Dioxus.toml` not found; using default config.");
+            return Ok(DioxusConfig::default());
+        }
+
+        let mut dioxus_conf_file = File::open(crate_dir.join("Dioxus.toml"))?;
+        let mut meta_str = String::new();
+        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()))
+    }
+}
+
+impl Default for DioxusConfig {
+    fn default() -> Self {
+        Self {
+            application: ApplicationConfig {
+                name: "dioxus".into(),
+                platforms: vec![String::from("web")],
+                out_dir: Some(PathBuf::from("dist")),
+            },
+            web: WebConfig {
+                app: WebAppConfing {
+                    title: Some("dioxus | ⛺".into()),
+                    public_dir: Some(PathBuf::from("public")),
+                },
+                watcher: WebWatcherConfing {
+                    watch_path: Some(vec![PathBuf::from("src")]),
+                    reload_html: Some(false),
+                },
+                resource: WebResourceConfing {
+                    dev: WebDevResourceConfing {
+                        style: Some(vec![]),
+                        script: Some(vec![]),
+                    },
+                    style: Some(vec![]),
+                    script: Some(vec![]),
+                },
+            },
+        }
+    }
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct ApplicationConfig {
+    pub name: String,
+    pub platforms: Vec<String>,
+    pub out_dir: Option<PathBuf>,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct WebConfig {
+    pub app: WebAppConfing,
+    pub watcher: WebWatcherConfing,
+    pub resource: WebResourceConfing,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct WebAppConfing {
+    pub title: Option<String>,
+    pub public_dir: Option<PathBuf>,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct WebWatcherConfing {
+    pub watch_path: Option<Vec<PathBuf>>,
+    pub reload_html: Option<bool>,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct WebResourceConfing {
+    pub dev: WebDevResourceConfing,
+    pub style: Option<Vec<PathBuf>>,
+    pub script: Option<Vec<PathBuf>>,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct WebDevResourceConfing {
+    pub style: Option<Vec<PathBuf>>,
+    pub script: Option<Vec<PathBuf>>,
+}
 
 #[derive(Debug, Clone)]
 pub struct CrateConfig {
@@ -7,9 +101,10 @@ pub struct CrateConfig {
     pub crate_dir: PathBuf,
     pub workspace_dir: PathBuf,
     pub target_dir: PathBuf,
-    pub static_dir: PathBuf,
+    pub public_dir: PathBuf,
     pub manifest: cargo_toml::Manifest<cargo_toml::Value>,
     pub executable: ExecutableType,
+    pub dioxus_config: DioxusConfig,
     pub release: bool,
 }
 
@@ -22,28 +117,33 @@ pub enum ExecutableType {
 
 impl CrateConfig {
     pub fn new() -> Result<Self> {
+        let dioxus_config = DioxusConfig::load()?;
+
         let crate_dir = crate::cargo::crate_root()?;
         let workspace_dir = crate::cargo::workspace_root()?;
         let target_dir = workspace_dir.join("target");
-        let out_dir = crate_dir.join("public");
+
+        let out_dir = match dioxus_config.application.out_dir {
+            Some(ref v) => crate_dir.join(v),
+            None => crate_dir.join("dist"),
+        };
+
         let cargo_def = &crate_dir.join("Cargo.toml");
-        let static_dir = crate_dir.join("static");
+
+        let public_dir = match dioxus_config.web.app.public_dir {
+            Some(ref v) => crate_dir.join(v),
+            None => crate_dir.join("public"),
+        };
 
         let manifest = cargo_toml::Manifest::from_path(&cargo_def).unwrap();
 
         // We just assume they're using a 'main.rs'
         // Anyway, we've already parsed the manifest, so it should be easy to change the type
-        let output_filename = (&manifest)
+        let output_filename = manifest
             .lib
             .as_ref()
             .and_then(|lib| lib.name.clone())
-            .or_else(|| {
-                (&manifest)
-                    .package
-                    .as_ref()
-                    .and_then(|pkg| Some(pkg.name.replace("-", "_")))
-                    .clone()
-            })
+            .or_else(|| manifest.package.as_ref().map(|pkg| pkg.name.clone()))
             .expect("No lib found from cargo metadata");
         let executable = ExecutableType::Binary(output_filename);
 
@@ -54,10 +154,11 @@ impl CrateConfig {
             crate_dir,
             workspace_dir,
             target_dir,
-            static_dir,
+            public_dir,
             manifest,
             executable,
             release,
+            dioxus_config,
         })
     }
 

+ 0 - 19
src/err.html

@@ -1,19 +0,0 @@
-<html>
-
-<head></head>
-
-<body>
-    <div>
-        <h1>
-            Sorry, but building your application failed.
-        </h1>
-        <p>
-            Here's the error:
-        </p>
-        <code>
-            {err}
-        </code>
-    </div>
-</body>
-
-</html>

+ 1 - 10
src/lib.rs

@@ -1,4 +1,5 @@
 pub mod builder;
+pub mod server;
 pub use builder::*;
 
 pub mod cargo;
@@ -15,13 +16,3 @@ pub use error::*;
 
 pub mod logging;
 pub use logging::*;
-
-pub mod watch;
-
-pub mod buildsystem {
-    pub mod wasm;
-    pub use wasm::*;
-
-    pub mod assets;
-    pub use assets::*;
-}

+ 1 - 1
src/logging.rs

@@ -14,7 +14,7 @@ pub fn set_up_logging() {
     // configure colors for the name of the level.
     // since almost all of them are the same as the color for the whole line, we
     // just clone `colors_line` and overwrite our changes
-    let colors_level = colors_line.clone().info(Color::Green);
+    let colors_level = colors_line.info(Color::Green);
     // here we set up our fern Dispatch
     fern::Dispatch::new()
         .format(move |out, message, record| {

+ 34 - 16
src/main.rs

@@ -8,22 +8,40 @@ async fn main() -> Result<()> {
 
     match args.action {
         Commands::Translate(opts) => {
-            opts.translate();
-        } // Commands::Build(_) => {
-          //     //
-          // }
-
-          // Commands::Clean(_) => {
-          //     //
-          // }
-
-          // Commands::Config(_) => {
-          //     //
-          // }
-
-          // Commands::Serve(_) => {
-          //     //
-          // }
+            if let Err(e) = opts.translate() {
+                log::error!("translate error: {}", e);
+            }
+        }
+
+        Commands::Build(opts) => {
+            if let Err(e) = opts.build() {
+                log::error!("build error: {}", e);
+            }
+        }
+
+        Commands::Clean(opts) => {
+            if let Err(e) = opts.clean() {
+                log::error!("clean error: {}", e);
+            }
+        }
+
+        Commands::Serve(opts) => {
+            if let Err(e) = opts.serve().await {
+                log::error!("serve error: {}", e);
+            }
+        }
+
+        Commands::Create(opts) => {
+            if let Err(e) = opts.create() {
+                log::error!("create error: {}", e);
+            }
+        }
+
+        Commands::Config(opts) => {
+            if let Err(e) = opts.config() {
+                log::error!("config error: {}", e);
+            }
+        }
     }
 
     Ok(())

+ 124 - 0
src/server/mod.rs

@@ -0,0 +1,124 @@
+use axum::{
+    extract::{ws::Message, Extension, TypedHeader, WebSocketUpgrade},
+    http::StatusCode,
+    response::IntoResponse,
+    routing::{get, get_service},
+    AddExtensionLayer, Router,
+};
+use notify::{RecommendedWatcher, Watcher};
+
+use std::{path::PathBuf, sync::Arc};
+use tower_http::services::ServeDir;
+
+use crate::{builder, serve::Serve, CrateConfig};
+use tokio::sync::broadcast;
+
+struct WsRelodState {
+    update: broadcast::Sender<String>,
+}
+
+pub async fn startup(config: CrateConfig) -> anyhow::Result<()> {
+    log::info!("🚀 Starting development server...");
+
+    let dist_path = config.out_dir.clone();
+
+    let (reload_tx, _) = broadcast::channel(100);
+
+    let ws_reload_state = Arc::new(WsRelodState {
+        update: reload_tx.clone(),
+    });
+
+    let mut last_update_time = chrono::Local::now().timestamp();
+
+    // file watcher: check file change
+    let watcher_conf = config.clone();
+    let mut watcher = RecommendedWatcher::new(move |_: notify::Result<notify::Event>| {
+        if chrono::Local::now().timestamp() > last_update_time
+            && builder::build(&watcher_conf).is_ok()
+        {
+            // change the websocket reload state to true;
+            // the page will auto-reload.
+            if watcher_conf
+                .dioxus_config
+                .web
+                .watcher
+                .reload_html
+                .unwrap_or(false)
+            {
+                let _ = Serve::regen_dev_page(&watcher_conf);
+            }
+            println!("watcher send reload");
+            reload_tx.send("reload".into()).unwrap();
+            last_update_time = chrono::Local::now().timestamp();
+        }
+    })
+    .unwrap();
+    let allow_watch_path = config
+        .dioxus_config
+        .web
+        .watcher
+        .watch_path
+        .clone()
+        .unwrap_or_else(|| vec![PathBuf::from("src")]);
+
+    for sub_path in allow_watch_path {
+        watcher
+            .watch(
+                &config.crate_dir.join(sub_path),
+                notify::RecursiveMode::Recursive,
+            )
+            .unwrap();
+    }
+
+    // start serve dev-server at 0.0.0.0:8080
+    let port = "8080";
+    log::info!("📡 Dev-Server is started at: http://127.0.0.1:{}/", port);
+
+    axum::Server::bind(&format!("0.0.0.0:{}", port).parse().unwrap())
+        .serve(
+            Router::new()
+                .route("/_dioxus/ws", get(ws_handler))
+                .fallback(
+                    get_service(ServeDir::new(config.crate_dir.join(&dist_path))).handle_error(
+                        |error: std::io::Error| async move {
+                            (
+                                StatusCode::INTERNAL_SERVER_ERROR,
+                                format!("Unhandled internal error: {}", error),
+                            )
+                        },
+                    ),
+                )
+                .layer(AddExtensionLayer::new(ws_reload_state))
+                .into_make_service(),
+        )
+        .await?;
+
+    Ok(())
+}
+
+async fn ws_handler(
+    ws: WebSocketUpgrade,
+    _: Option<TypedHeader<headers::UserAgent>>,
+    Extension(state): Extension<Arc<WsRelodState>>,
+) -> impl IntoResponse {
+    ws.on_upgrade(|mut socket| async move {
+        let mut rx = state.update.subscribe();
+        let reload_watcher = tokio::spawn(async move {
+            loop {
+                let v = rx.recv().await.unwrap();
+                if v == "reload" {
+                    // ignore the error
+                    if socket
+                        .send(Message::Text(String::from("reload")))
+                        .await
+                        .is_err()
+                    {
+                        break;
+                    }
+                }
+            }
+        });
+
+        reload_watcher.await.unwrap();
+    })
+}

+ 0 - 1
src/watch.rs

@@ -1 +0,0 @@
-