├── examples ├── README.md └── plugin │ ├── init.lua │ └── interface.lua ├── .fleet └── settings.json ├── docs ├── .gitignore ├── book.toml └── src │ ├── plugin │ ├── interface │ │ ├── os.md │ │ ├── command.md │ │ ├── log.md │ │ ├── dirs.md │ │ ├── path.md │ │ └── network.md │ └── README.md │ ├── cmd │ ├── clean.md │ ├── README.md │ ├── build.md │ ├── translate.md │ └── serve.md │ ├── introduction.md │ ├── SUMMARY.md │ ├── installation.md │ ├── creating.md │ └── configure.md ├── extension ├── dist │ └── .gitignore ├── server │ └── .gitignore ├── static │ └── icon.png ├── .gitignore ├── DEV.md ├── tsconfig.json ├── README.md ├── .eslintrc.js ├── LICENSE.txt ├── package.json └── src │ └── main.ts ├── .gitignore ├── tests ├── main.rs ├── svg.html └── test.html ├── .vscode └── settings.json ├── rustfmt.toml ├── src ├── lib.rs ├── plugin │ ├── interface │ │ ├── dirs.rs │ │ ├── os.rs │ │ ├── log.rs │ │ ├── network.rs │ │ ├── path.rs │ │ ├── command.rs │ │ ├── fs.rs │ │ └── mod.rs │ ├── types.rs │ └── mod.rs ├── assets │ ├── index.html │ ├── autoreload.js │ └── dioxus.toml ├── cli │ ├── clean │ │ └── mod.rs │ ├── plugin │ │ └── mod.rs │ ├── version.rs │ ├── tool │ │ └── mod.rs │ ├── config │ │ └── mod.rs │ ├── build │ │ └── mod.rs │ ├── mod.rs │ ├── cfg.rs │ ├── create │ │ └── mod.rs │ ├── serve │ │ └── mod.rs │ ├── translate │ │ └── mod.rs │ └── autoformat │ │ └── mod.rs ├── logging.rs ├── error.rs ├── main.rs ├── cargo.rs ├── server │ ├── proxy.rs │ └── mod.rs ├── config.rs ├── tools.rs └── builder.rs ├── Dioxus.toml ├── .github └── workflows │ ├── build.yml │ ├── docs.yml │ └── main.yml ├── README.md └── Cargo.toml /examples/README.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.fleet/settings.json: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | book 2 | -------------------------------------------------------------------------------- /extension/dist/.gitignore: -------------------------------------------------------------------------------- 1 | ** 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | .DS_Store 4 | .idea/ 5 | -------------------------------------------------------------------------------- /extension/server/.gitignore: -------------------------------------------------------------------------------- 1 | ** 2 | !Readme.md 3 | !.gitignore 4 | -------------------------------------------------------------------------------- /tests/main.rs: -------------------------------------------------------------------------------- 1 | #[test] 2 | fn ready() { 3 | println!("Compiled successfully!") 4 | } 5 | -------------------------------------------------------------------------------- /extension/static/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timokoesters/dioxus-cli/master/extension/static/icon.png -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Lua.diagnostics.globals": [ 3 | "plugin_logger", 4 | "PLUGIN_DOWNLOADER" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /docs/book.toml: -------------------------------------------------------------------------------- 1 | [book] 2 | authors = ["YuKun Liu"] 3 | language = "en" 4 | multilingual = false 5 | src = "src" 6 | title = "Dioxus Cli" 7 | -------------------------------------------------------------------------------- /extension/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | npm-debug.log 3 | Thumbs.db 4 | */node_modules/ 5 | node_modules/ 6 | */out/ 7 | out/ 8 | */.vs/ 9 | .vs/ 10 | tsconfig.lsif.json 11 | *.lsif 12 | *.db 13 | *.vsix 14 | -------------------------------------------------------------------------------- /extension/DEV.md: -------------------------------------------------------------------------------- 1 | 2 | ## packaging 3 | 4 | ``` 5 | $ cd myExtension 6 | $ vsce package 7 | # myExtension.vsix generated 8 | $ vsce publish 9 | # .myExtension published to VS Code Marketplace 10 | ``` 11 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | version = "Two" 2 | edition = "2021" 3 | 4 | imports_granularity = "Crate" 5 | #use_small_heuristics = "Max" 6 | #control_brace_style = "ClosingNextLine" 7 | normalize_comments = true 8 | format_code_in_doc_comments = true 9 | -------------------------------------------------------------------------------- /extension/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es2021", 5 | "lib": ["ES2021"], 6 | "outDir": "out", 7 | "sourceMap": true, 8 | "strict": true, 9 | "rootDir": "src" 10 | }, 11 | "exclude": ["node_modules", ".vscode-test"] 12 | } 13 | -------------------------------------------------------------------------------- /docs/src/plugin/interface/os.md: -------------------------------------------------------------------------------- 1 | # OS Functions 2 | 3 | > you can use OS functions to get some system information 4 | 5 | ### current_platform() -> string ("windows" | "macos" | "linux") 6 | 7 | This function can help you get system & platform type: 8 | 9 | ```lua 10 | local platform = plugin.os.current_platform() 11 | ``` -------------------------------------------------------------------------------- /docs/src/cmd/clean.md: -------------------------------------------------------------------------------- 1 | # Clean 2 | 3 | `dioxus clean` will call `target clean` and remove `out_dir` directory. 4 | 5 | ``` 6 | dioxus-clean 7 | Clean output artifacts 8 | 9 | USAGE: 10 | dioxus clean 11 | ``` 12 | 13 | you can use this command to clean all build cache and the `out_dir` content. 14 | 15 | ``` 16 | dioxus clean 17 | ``` 18 | 19 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub const DIOXUS_CLI_VERSION: &str = "0.1.5"; 2 | 3 | pub mod builder; 4 | pub mod server; 5 | pub mod tools; 6 | 7 | pub use builder::*; 8 | 9 | pub mod cargo; 10 | pub use cargo::*; 11 | 12 | pub mod cli; 13 | pub use cli::*; 14 | 15 | pub mod config; 16 | pub use config::*; 17 | 18 | pub mod error; 19 | pub use error::*; 20 | 21 | pub mod logging; 22 | pub use logging::*; 23 | 24 | pub mod plugin; 25 | -------------------------------------------------------------------------------- /src/plugin/interface/dirs.rs: -------------------------------------------------------------------------------- 1 | use mlua::UserData; 2 | 3 | use crate::tools::app_path; 4 | 5 | pub struct PluginDirs; 6 | impl UserData for PluginDirs { 7 | fn add_methods<'lua, M: mlua::UserDataMethods<'lua, Self>>(methods: &mut M) { 8 | methods.add_function("plugins_dir", |_, ()| { 9 | let path = app_path().join("plugins"); 10 | Ok(path.to_str().unwrap().to_string()) 11 | }); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /examples/plugin/init.lua: -------------------------------------------------------------------------------- 1 | local Api = require("./interface") 2 | local log = Api.log; 3 | 4 | local manager = { 5 | name = "Dioxus-CLI Plugin Demo", 6 | repository = "http://github.com/DioxusLabs/cli", 7 | author = "YuKun Liu ", 8 | } 9 | 10 | manager.onLoad = function () 11 | log.info("plugin loaded.") 12 | end 13 | 14 | manager.onStartBuild = function () 15 | log.warn("system start to build") 16 | end 17 | 18 | return manager -------------------------------------------------------------------------------- /extension/README.md: -------------------------------------------------------------------------------- 1 | # Dioxus VSCode Extension 2 | 3 | ![Dioxus Logo](https://dioxuslabs.com/guide/images/dioxuslogo_full.png) 4 | 5 | This extension wraps functionality in Dioxus CLI to be used in your editor! Make sure the dioxus-cli is installed before using this extension. 6 | 7 | ## Current commands: 8 | 9 | ### Convert HTML to RSX 10 | Converts a selection of html to valid rsx. 11 | 12 | ### Convert HTML to Dioxus Component 13 | 14 | Converts a selection of html to a valid Dioxus component with all SVGs factored out into their own module. 15 | -------------------------------------------------------------------------------- /docs/src/plugin/interface/command.md: -------------------------------------------------------------------------------- 1 | # Command Functions 2 | 3 | > you can use command functions to execute some code & script 4 | 5 | Type Define: 6 | ``` 7 | Stdio: "Inhert" | "Piped" | "Null" 8 | ``` 9 | 10 | ### `exec(commands: [string], stdout: Stdio, stderr: Stdio)` 11 | 12 | you can use this function to run some command on the current system. 13 | 14 | ```lua 15 | local cmd = plugin.command 16 | 17 | manager.test = function () 18 | cmd.exec({"git", "clone", "https://github.com/DioxusLabs/cli-plugin-library"}) 19 | end 20 | ``` 21 | > Warning: This function don't have exception catch. -------------------------------------------------------------------------------- /extension/.eslintrc.js: -------------------------------------------------------------------------------- 1 | /**@type {import('eslint').Linter.Config} */ 2 | // eslint-disable-next-line no-undef 3 | module.exports = { 4 | root: true, 5 | parser: '@typescript-eslint/parser', 6 | plugins: [ 7 | '@typescript-eslint', 8 | ], 9 | extends: [ 10 | 'eslint:recommended', 11 | 'plugin:@typescript-eslint/recommended', 12 | ], 13 | rules: { 14 | 'semi': [2, "always"], 15 | '@typescript-eslint/no-unused-vars': 0, 16 | '@typescript-eslint/no-explicit-any': 0, 17 | '@typescript-eslint/explicit-module-boundary-types': 0, 18 | '@typescript-eslint/no-non-null-assertion': 0, 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /src/plugin/interface/os.rs: -------------------------------------------------------------------------------- 1 | use mlua::UserData; 2 | 3 | pub struct PluginOS; 4 | impl UserData for PluginOS { 5 | fn add_methods<'lua, M: mlua::UserDataMethods<'lua, Self>>(methods: &mut M) { 6 | methods.add_function("current_platform", |_, ()| { 7 | if cfg!(target_os = "windows") { 8 | Ok("windows") 9 | } else if cfg!(target_os = "macos") { 10 | Ok("macos") 11 | } else if cfg!(target_os = "linux") { 12 | Ok("linux") 13 | } else { 14 | panic!("unsupported platformm"); 15 | } 16 | }); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /docs/src/introduction.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | 📦✨ **Dioxus-Cli** is a tool to help get dioxus projects off the ground. 4 | 5 | ![dioxus-logo](https://dioxuslabs.com/guide/images/dioxuslogo_full.png) 6 | 7 | It includes `dev server`, `hot reload` and some `quick command` to help you use dioxus. 8 | 9 | ## Features 10 | 11 | - [x] `html` to `rsx` conversion tool 12 | - [x] hot reload for `web` platform 13 | - [x] create dioxus project from `git` repo 14 | - [x] build & pack dioxus project 15 | - [ ] autoformat dioxus `rsx` code 16 | 17 | ## Contributors 18 | 19 | Contributors to this guide: 20 | 21 | - [mrxiaozhuox](https://github.com/mrxiaozhuox) -------------------------------------------------------------------------------- /examples/plugin/interface.lua: -------------------------------------------------------------------------------- 1 | local interface = {} 2 | 3 | if plugin_logger ~= nil then 4 | interface.log = plugin_logger 5 | else 6 | interface.log = { 7 | trace = function (info) 8 | print("trace: " .. info) 9 | end, 10 | debug = function (info) 11 | print("debug: " .. info) 12 | end, 13 | info = function (info) 14 | print("info: " .. info) 15 | end, 16 | warn = function (info) 17 | print("warn: " .. info) 18 | end, 19 | error = function (info) 20 | print("error: " .. info) 21 | end, 22 | } 23 | end 24 | 25 | return interface -------------------------------------------------------------------------------- /src/assets/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {app_title} 5 | 6 | 7 | 8 | {style_include} 9 | 10 | 11 |
12 | 20 | {script_include} 21 | 22 | -------------------------------------------------------------------------------- /docs/src/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | - [Introduction](./introduction.md) 4 | - [Installation](./installation.md) 5 | - [Create a Project](./creating.md) 6 | - [Configure Project](./configure.md) 7 | - [Commands](./cmd/README.md) 8 | - [Build](./cmd/build.md) 9 | - [Serve](./cmd/serve.md) 10 | - [Clean](./cmd/clean.md) 11 | - [Translate](./cmd/translate.md) 12 | - [Plugin Development](./plugin/README.md) 13 | - [API.Log](./plugin/interface/log.md) 14 | - [API.Command](./plugin/interface/command.md) 15 | - [API.OS](./plugin/interface/os.md) 16 | - [API.Directories](./plugin/interface/dirs.md) 17 | - [API.Network](./plugin/interface/network.md) 18 | - [API.Path](./plugin/interface/path.md) -------------------------------------------------------------------------------- /docs/src/installation.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | There are multiple ways to install the Dioxus CLI tool. Choose any one of the methods below that best suit your needs. 4 | 5 | ## Install from latest master version 6 | 7 | We suggest you use `github master` version to install it now. 8 | 9 | We have not yet released the latest version to `crates.io` 10 | 11 | ``` 12 | cargo install --git https://github.com/Dioxuslabs/cli 13 | ``` 14 | 15 | This will automatically download `Dioxus-CLI` source from github master branch, 16 | and install it in Cargo's global binary directory (`~/.cargo/bin/` by default). 17 | 18 | ## Install from `crates.io` version 19 | 20 | ``` 21 | cargo install dioxus-cli 22 | ``` 23 | 24 | Make sure to add the Cargo bin directory to your `PATH`. -------------------------------------------------------------------------------- /src/assets/autoreload.js: -------------------------------------------------------------------------------- 1 | // Dioxus-CLI 2 | // https://github.com/DioxusLabs/cli 3 | 4 | (function () { 5 | var protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; 6 | var url = protocol + '//' + window.location.host + '/_dioxus/ws'; 7 | var poll_interval = 8080; 8 | var reload_upon_connect = () => { 9 | window.setTimeout( 10 | () => { 11 | var ws = new WebSocket(url); 12 | ws.onopen = () => window.location.reload(); 13 | ws.onclose = reload_upon_connect; 14 | }, 15 | poll_interval); 16 | }; 17 | 18 | var ws = new WebSocket(url); 19 | ws.onmessage = (ev) => { 20 | if (ev.data == "reload") { 21 | window.location.reload(); 22 | } 23 | }; 24 | ws.onclose = reload_upon_connect; 25 | })() -------------------------------------------------------------------------------- /docs/src/cmd/README.md: -------------------------------------------------------------------------------- 1 | # Commands 2 | 3 | In this chapter we will introduce all `dioxus-cli` commands. 4 | 5 | > you can also use `dioxus --help` to get cli help info. 6 | 7 | ``` 8 | dioxus 9 | Build, bundle, & ship your Dioxus app 10 | 11 | USAGE: 12 | dioxus [OPTIONS] 13 | 14 | OPTIONS: 15 | -h, --help Print help information 16 | -v Enable verbose logging 17 | 18 | SUBCOMMANDS: 19 | build Build the Rust WASM app and all of its assets 20 | clean Clean output artifacts 21 | config Dioxus config file controls 22 | create Init a new project for Dioxus 23 | help Print this message or the help of the given subcommand(s) 24 | serve Build, watch & serve the Rust WASM app and all of its assets 25 | translate Translate some source file into Dioxus code 26 | ``` -------------------------------------------------------------------------------- /docs/src/plugin/interface/log.md: -------------------------------------------------------------------------------- 1 | # Log Functions 2 | 3 | > You can use log function to print some useful log info 4 | 5 | ### Trace(info: string) 6 | 7 | Print trace log info 8 | 9 | ```lua 10 | local log = plugin.log 11 | log.trace("trace information") 12 | ``` 13 | 14 | ### Debug(info: string) 15 | 16 | Print debug log info 17 | 18 | ```lua 19 | local log = plugin.log 20 | log.debug("debug information") 21 | ``` 22 | 23 | ### Info(info: string) 24 | 25 | Print info log info 26 | 27 | ```lua 28 | local log = plugin.log 29 | log.info("info information") 30 | ``` 31 | 32 | ### Warn(info: string) 33 | 34 | Print warning log info 35 | 36 | ```lua 37 | local log = plugin.log 38 | log.warn("warn information") 39 | ``` 40 | 41 | ### Error(info: string) 42 | 43 | Print error log info 44 | 45 | ```lua 46 | local log = plugin.log 47 | log.error("error information") 48 | ``` -------------------------------------------------------------------------------- /src/plugin/interface/log.rs: -------------------------------------------------------------------------------- 1 | use log; 2 | use mlua::UserData; 3 | 4 | pub struct PluginLogger; 5 | impl UserData for PluginLogger { 6 | fn add_methods<'lua, M: mlua::UserDataMethods<'lua, Self>>(methods: &mut M) { 7 | methods.add_function("trace", |_, info: String| { 8 | log::trace!("{}", info); 9 | Ok(()) 10 | }); 11 | methods.add_function("info", |_, info: String| { 12 | log::info!("{}", info); 13 | Ok(()) 14 | }); 15 | methods.add_function("debug", |_, info: String| { 16 | log::debug!("{}", info); 17 | Ok(()) 18 | }); 19 | methods.add_function("warn", |_, info: String| { 20 | log::warn!("{}", info); 21 | Ok(()) 22 | }); 23 | methods.add_function("error", |_, info: String| { 24 | log::error!("{}", info); 25 | Ok(()) 26 | }); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Dioxus.toml: -------------------------------------------------------------------------------- 1 | [application] 2 | 3 | # dioxus project name 4 | name = "dioxus-cli" 5 | 6 | # default platfrom 7 | # you can also use `dioxus serve/build --platform XXX` to use other platform 8 | # value: web | desktop 9 | default_platform = "desktop" 10 | 11 | # Web `build` & `serve` dist path 12 | out_dir = "dist" 13 | 14 | # resource (static) file folder 15 | asset_dir = "public" 16 | 17 | [web.app] 18 | 19 | # HTML title tag content 20 | title = "dioxus | ⛺" 21 | 22 | [web.watcher] 23 | 24 | watch_path = ["src"] 25 | 26 | # include `assets` in web platform 27 | [web.resource] 28 | 29 | # CSS style file 30 | style = [] 31 | 32 | # Javascript code file 33 | script = [] 34 | 35 | [web.resource.dev] 36 | 37 | # Javascript code file 38 | # serve: [dev-server] only 39 | script = [] 40 | 41 | [application.tools] 42 | 43 | # use binaryen.wasm-opt for output Wasm file 44 | # binaryen just will trigger in `web` platform 45 | binaryen = { wasm_opt = true } -------------------------------------------------------------------------------- /src/assets/dioxus.toml: -------------------------------------------------------------------------------- 1 | [application] 2 | 3 | # dioxus project name 4 | name = "{{project-name}}" 5 | 6 | # default platfrom 7 | # you can also use `dioxus serve/build --platform XXX` to use other platform 8 | # value: web | desktop 9 | default_platform = "{{default-platform}}" 10 | 11 | # Web `build` & `serve` dist path 12 | out_dir = "dist" 13 | 14 | # resource (static) file folder 15 | asset_dir = "public" 16 | 17 | [web.app] 18 | 19 | # HTML title tag content 20 | title = "Dioxus | An elegant GUI library for Rust" 21 | 22 | [web.watcher] 23 | 24 | index_on_404 = true 25 | 26 | watch_path = ["src"] 27 | 28 | # include `assets` in web platform 29 | [web.resource] 30 | 31 | # CSS style file 32 | style = [] 33 | 34 | # Javascript code file 35 | script = [] 36 | 37 | [web.resource.dev] 38 | 39 | # Javascript code file 40 | # serve: [dev-server] only 41 | script = [] 42 | 43 | [application.plugins] 44 | 45 | available = true 46 | 47 | required = [] 48 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # .github/workflows/build.yml 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | release: 9 | name: release ${{ matrix.target }} 10 | runs-on: ubuntu-latest 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | include: 15 | - target: x86_64-unknown-linux-gnu 16 | archive: tar.gz tar.xz 17 | - target: x86_64-unknown-linux-musl 18 | archive: tar.gz tar.xz 19 | - target: x86_64-apple-darwin 20 | archive: tar.gz tar.xz 21 | - target: x86_64-pc-windows-gnu 22 | archive: zip 23 | 24 | steps: 25 | - uses: actions/checkout@master 26 | - name: Compile and release 27 | uses: rust-build/rust-build.action@latest 28 | env: 29 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 30 | RUSTTARGET: ${{ matrix.target }} 31 | ARCHIVE_TYPES: ${{ matrix.archive }} 32 | -------------------------------------------------------------------------------- /docs/src/plugin/interface/dirs.md: -------------------------------------------------------------------------------- 1 | # Dirs Functions 2 | 3 | > you can use Dirs functions to get some directory path 4 | 5 | 6 | ### plugin_dir() -> string 7 | 8 | You can get current plugin **root** directory path 9 | 10 | ```lua 11 | local path = plugin.dirs.plugin_dir() 12 | -- example: ~/Development/DioxusCli/plugin/test-plugin/ 13 | ``` 14 | 15 | ### bin_dir() -> string 16 | 17 | You can get plugin **bin** direcotry path 18 | 19 | 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. 20 | 21 | ```lua 22 | local path = plugin.dirs.bin_dir() 23 | -- example: ~/Development/DioxusCli/plugin/test-plugin/bin/ 24 | ``` 25 | 26 | ### temp_dir() -> string 27 | 28 | You can get plugin **temp** direcotry path 29 | 30 | Just put some temporary file in this directory. 31 | 32 | ```lua 33 | local path = plugin.dirs.bin_dir() 34 | -- example: ~/Development/DioxusCli/plugin/test-plugin/temp/ 35 | ``` -------------------------------------------------------------------------------- /src/cli/clean/mod.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | /// Build the Rust WASM app and all of its assets. 4 | #[derive(Clone, Debug, Parser)] 5 | #[clap(name = "clean")] 6 | pub struct Clean {} 7 | 8 | impl Clean { 9 | pub fn clean(self) -> Result<()> { 10 | let crate_config = crate::CrateConfig::new()?; 11 | 12 | let output = Command::new("cargo") 13 | .arg("clean") 14 | .stdout(Stdio::piped()) 15 | .stderr(Stdio::piped()) 16 | .output()?; 17 | 18 | if !output.status.success() { 19 | return custom_error!("Cargo clean failed."); 20 | } 21 | 22 | let out_dir = crate_config 23 | .dioxus_config 24 | .application 25 | .out_dir 26 | .unwrap_or_else(|| PathBuf::from("dist")); 27 | if crate_config.crate_dir.join(&out_dir).is_dir() { 28 | remove_dir_all(crate_config.crate_dir.join(&out_dir))?; 29 | } 30 | 31 | Ok(()) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/plugin/interface/network.rs: -------------------------------------------------------------------------------- 1 | use std::{io::Cursor, path::PathBuf}; 2 | 3 | use mlua::UserData; 4 | 5 | pub struct PluginNetwork; 6 | impl UserData for PluginNetwork { 7 | fn add_methods<'lua, M: mlua::UserDataMethods<'lua, Self>>(methods: &mut M) { 8 | methods.add_function("download_file", |_, args: (String, String)| { 9 | let url = args.0; 10 | let path = args.1; 11 | 12 | let resp = reqwest::blocking::get(url); 13 | if let Ok(resp) = resp { 14 | let mut content = Cursor::new(resp.bytes().unwrap()); 15 | let file = std::fs::File::create(PathBuf::from(path)); 16 | if file.is_err() { 17 | return Ok(false); 18 | } 19 | let mut file = file.unwrap(); 20 | let res = std::io::copy(&mut content, &mut file); 21 | return Ok(res.is_ok()); 22 | } 23 | 24 | Ok(false) 25 | }); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: github pages 2 | 3 | on: 4 | push: 5 | paths: 6 | - docs/** 7 | branches: 8 | - master 9 | 10 | jobs: 11 | deploy: 12 | runs-on: ubuntu-20.04 13 | concurrency: 14 | group: ${{ github.workflow }}-${{ github.ref }} 15 | steps: 16 | - uses: actions/checkout@v2 17 | 18 | - name: Setup mdBook 19 | uses: peaceiris/actions-mdbook@v1 20 | with: 21 | mdbook-version: '0.4.10' 22 | # mdbook-version: 'latest' 23 | 24 | - run: cd docs && mdbook build 25 | 26 | - name: Deploy 🚀 27 | uses: JamesIves/github-pages-deploy-action@v4.2.3 28 | with: 29 | branch: gh-pages # The branch the action should deploy to. 30 | folder: docs/book # The folder the action should deploy. 31 | target-folder: docs/nightly/cli 32 | repository-name: dioxuslabs/docsite 33 | clean: false 34 | token: ${{ secrets.DEPLOY_KEY }} # let's pretend I don't need it for now 35 | -------------------------------------------------------------------------------- /docs/src/cmd/build.md: -------------------------------------------------------------------------------- 1 | # Build 2 | 3 | The `dioxus build` command can help you `pack & build` a dioxus project. 4 | 5 | ``` 6 | dioxus-build 7 | Build the Rust WASM app and all of its assets 8 | 9 | USAGE: 10 | dioxus build [OPTIONS] 11 | 12 | OPTIONS: 13 | --example [default: ""] 14 | --platform [default: "default_platform"] 15 | --release [default: false] 16 | ``` 17 | 18 | You can use this command to build project to `out_dir` : 19 | 20 | ``` 21 | dioxus build --release 22 | ``` 23 | 24 | ## Target platform 25 | 26 | Use option `platform` choose build target platform: 27 | 28 | ``` 29 | # for desktop project 30 | dioxus build --platform desktop 31 | ``` 32 | 33 | `platform` only supports `desktop` & `web`. 34 | 35 | ``` 36 | # for web project 37 | dioxus build --platform web 38 | ``` 39 | 40 | ## Build Example 41 | 42 | You can use `--example {name}` to build a example code. 43 | 44 | ``` 45 | # build `example/test` code 46 | dioxus build --exmaple test 47 | ``` -------------------------------------------------------------------------------- /docs/src/plugin/interface/path.md: -------------------------------------------------------------------------------- 1 | # Path Functions 2 | 3 | > you can use path functions to operate valid path string 4 | 5 | ### join(path: string, extra: string) -> string 6 | 7 | This function can help you extend a path, you can extend any path, dirname or filename. 8 | 9 | ```lua 10 | local current_path = "~/hello/dioxus" 11 | local new_path = plugin.path.join(current_path, "world") 12 | -- new_path = "~/hello/dioxus/world" 13 | ``` 14 | 15 | ### parent(path: string) -> string 16 | 17 | This function will return `path` parent-path string, back to the parent. 18 | 19 | ```lua 20 | local current_path = "~/hello/dioxus" 21 | local new_path = plugin.path.parent(current_path) 22 | -- new_path = "~/hello/" 23 | ``` 24 | 25 | ### exists(path: string) -> boolean 26 | 27 | This function can check some path (dir & file) is exists. 28 | 29 | ### is_file(path: string) -> boolean 30 | 31 | This function can check some path is a exist file. 32 | 33 | ### is_dir(path: string) -> boolean 34 | 35 | This function can check some path is a exist dir. -------------------------------------------------------------------------------- /docs/src/plugin/interface/network.md: -------------------------------------------------------------------------------- 1 | # Network Functions 2 | 3 | > you can use Network functions to download & read some data from internet 4 | 5 | ### download_file(url: string, path: string) -> boolean 6 | 7 | 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) 8 | 9 | You need pass a target url and a local path (where you want to save this file) 10 | 11 | ```lua 12 | -- this file will download to plugin temp directory 13 | local status = plugin.network.download_file( 14 | "http://xxx.com/xxx.zip", 15 | plugin.dirs.temp_dir() 16 | ) 17 | if status != true then 18 | log.error("Download Failed") 19 | end 20 | ``` 21 | 22 | ### clone_repo(url: string, path: string) -> boolean 23 | 24 | This function can help you use `git clone` command (this system must have been installed git) 25 | 26 | ```lua 27 | local status = plugin.network.clone_repo( 28 | "http://github.com/mrxiaozhuox/dioxus-starter", 29 | plugin.dirs.bin_dir() 30 | ) 31 | if status != true then 32 | log.error("Clone Failed") 33 | end 34 | ``` -------------------------------------------------------------------------------- /docs/src/creating.md: -------------------------------------------------------------------------------- 1 | # Create a Project 2 | 3 | Once you have the Dioxus CLI tool installed, you can use it to create dioxus project. 4 | 5 | ## Initializing a default project 6 | 7 | The `dioxus create` command will create a new directory containing a project template. 8 | ``` 9 | dioxus create hello-dioxus 10 | ``` 11 | 12 | It will clone a default template from github template: [DioxusLabs/dioxus-template](https://github.com/DioxusLabs/dioxus-template) 13 | 14 | > This default template is use for `web` platform application. 15 | 16 | then you can change the current directory in to the project: 17 | 18 | ``` 19 | cd hello-dioxus 20 | ``` 21 | 22 | > Make sure `wasm32 target` is installed before running the Web project. 23 | 24 | now we can create a `dev server` to display the project: 25 | 26 | ``` 27 | dioxus serve 28 | ``` 29 | 30 | by default, the dioxus dev server will running at: [`http://127.0.0.1:8080/`](http://127.0.0.1:8080/) 31 | 32 | ## Initalizing from other repository 33 | 34 | you can assign which repository you want to create: 35 | 36 | ``` 37 | dioxus init hello-dioxus --template=gh:dioxuslabs/dioxus-template 38 | ``` -------------------------------------------------------------------------------- /extension/LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 DioxusLabs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /docs/src/cmd/translate.md: -------------------------------------------------------------------------------- 1 | # Translate 2 | 3 | `dioxus translate` can translate some source file into Dioxus code. 4 | 5 | ``` 6 | dioxus-translate 7 | Translate some source file into Dioxus code 8 | 9 | USAGE: 10 | dioxus translate [OPTIONS] [OUTPUT] 11 | 12 | ARGS: 13 | Output file, stdout if not present 14 | 15 | OPTIONS: 16 | -c, --component Activate debug mode 17 | -f, --file Input file 18 | ``` 19 | 20 | ## Translate HTML to stdout 21 | 22 | ``` 23 | dioxus transtale --file ./index.html 24 | ``` 25 | 26 | ## Output in a file 27 | 28 | ``` 29 | dioxus translate --component --file ./index.html component.rsx 30 | ``` 31 | 32 | set `component` flag will wrap `dioxus rsx` code in a component function. 33 | 34 | ## Example 35 | 36 | ```html 37 |
38 |

Hello World

39 | Link 40 |
41 | ``` 42 | 43 | Translate HTML to Dioxus component code. 44 | 45 | ```rust 46 | fn component(cx: Scope) -> Element { 47 | cx.render(rsx! { 48 | div { 49 | h1 { "Hello World" }, 50 | a { 51 | href: "https://dioxuslabs.com/", 52 | "Link" 53 | } 54 | } 55 | }) 56 | } 57 | ``` -------------------------------------------------------------------------------- /src/cli/plugin/mod.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | /// Build the Rust WASM app and all of its assets. 4 | #[derive(Clone, Debug, Deserialize, Subcommand)] 5 | #[clap(name = "plugin")] 6 | pub enum Plugin { 7 | /// Return all dioxus-cli support tools. 8 | List {}, 9 | /// Get default app install path. 10 | AppPath {}, 11 | /// Install a new tool. 12 | Add { name: String }, 13 | } 14 | 15 | impl Plugin { 16 | pub async fn plugin(self) -> Result<()> { 17 | match self { 18 | Plugin::List {} => { 19 | for item in crate::plugin::PluginManager::plugin_list() { 20 | println!("- {item}"); 21 | } 22 | } 23 | Plugin::AppPath {} => { 24 | let plugin_dir = crate::plugin::PluginManager::init_plugin_dir(); 25 | if let Some(v) = plugin_dir.to_str() { 26 | println!("{}", v); 27 | } else { 28 | log::error!("Plugin path get failed."); 29 | } 30 | } 31 | Plugin::Add { name: _ } => { 32 | log::info!("You can use `dioxus plugin app-path` to get Installation position"); 33 | } 34 | } 35 | Ok(()) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/svg.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 8 | 9 | 10 | 15 | 16 | 17 | 22 | 23 | 24 | 29 | 30 |
-------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

📦✨ Dioxus CLI

3 |

Tooling to supercharge Dioxus projects

4 |
5 | **dioxus-cli** (inspired by wasm-pack and webpack) is a tool for getting Dioxus projects up and running. 6 | It handles all build, bundling, development and publishing to simplify web development. 7 | 8 | 9 | ## Installation 10 | 11 | ### Install stable version 12 | ``` 13 | cargo install dioxus-cli 14 | ``` 15 | ### Install from git repository 16 | ``` 17 | cargo install --git https://github.com/DioxusLabs/cli 18 | ``` 19 | ### Install from local folder 20 | ``` 21 | cargo install --path . --debug 22 | ``` 23 | 24 | 25 | ## Get Started 26 | 27 | Use `dioxus create project-name` to initialize a new Dioxus project.
28 | 29 | It will be cloned from the [dioxus-template](https://github.com/DioxusLabs/dioxus-template) repository. 30 | 31 |
32 | 33 | Alternatively, you can specify the template path: 34 | 35 | ``` 36 | dioxus create hello --template gh:dioxuslabs/dioxus-template 37 | ``` 38 | 39 | ## Dioxus Config File 40 | 41 | Dioxus CLI will use `Dioxus.toml` file to Identify some project info and switch some cli feature. 42 | 43 | You can get more configure information from [Dioxus CLI Document](https://dioxuslabs.com/cli/configure.html). -------------------------------------------------------------------------------- /src/plugin/interface/path.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use mlua::{UserData, Variadic}; 4 | 5 | pub struct PluginPath; 6 | impl UserData for PluginPath { 7 | fn add_methods<'lua, M: mlua::UserDataMethods<'lua, Self>>(methods: &mut M) { 8 | // join function 9 | methods.add_function("join", |_, args: Variadic| { 10 | let mut path = PathBuf::new(); 11 | for i in args { 12 | path = path.join(i); 13 | } 14 | Ok(path.to_str().unwrap().to_string()) 15 | }); 16 | 17 | // parent function 18 | methods.add_function("parent", |_, path: String| { 19 | let current_path = PathBuf::from(&path); 20 | let parent = current_path.parent(); 21 | if parent.is_none() { 22 | return Ok(path); 23 | } else { 24 | return Ok(parent.unwrap().to_str().unwrap().to_string()); 25 | } 26 | }); 27 | methods.add_function("exists", |_, path: String| { 28 | let path = PathBuf::from(path); 29 | Ok(path.exists()) 30 | }); 31 | methods.add_function("is_dir", |_, path: String| { 32 | let path = PathBuf::from(path); 33 | Ok(path.is_dir()) 34 | }); 35 | methods.add_function("is_file", |_, path: String| { 36 | let path = PathBuf::from(path); 37 | Ok(path.is_file()) 38 | }); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/logging.rs: -------------------------------------------------------------------------------- 1 | use fern::colors::{Color, ColoredLevelConfig}; 2 | 3 | pub fn set_up_logging() { 4 | // configure colors for the whole line 5 | let colors_line = ColoredLevelConfig::new() 6 | .error(Color::Red) 7 | .warn(Color::Yellow) 8 | // we actually don't need to specify the color for debug and info, they are white by default 9 | .info(Color::White) 10 | .debug(Color::White) 11 | // depending on the terminals color scheme, this is the same as the background color 12 | .trace(Color::BrightBlack); 13 | 14 | // configure colors for the name of the level. 15 | // since almost all of them are the same as the color for the whole line, we 16 | // just clone `colors_line` and overwrite our changes 17 | let colors_level = colors_line.info(Color::Green); 18 | // here we set up our fern Dispatch 19 | fern::Dispatch::new() 20 | .format(move |out, message, record| { 21 | out.finish(format_args!( 22 | "{color_line}[{level}{color_line}] {message}\x1B[0m", 23 | color_line = format_args!( 24 | "\x1B[{}m", 25 | colors_line.get_color(&record.level()).to_fg_str() 26 | ), 27 | level = colors_level.color(record.level()), 28 | message = message, 29 | )); 30 | }) 31 | .level(log::LevelFilter::Info) 32 | .chain(std::io::stdout()) 33 | .apply() 34 | .unwrap(); 35 | } 36 | -------------------------------------------------------------------------------- /docs/src/cmd/serve.md: -------------------------------------------------------------------------------- 1 | # Serve 2 | 3 | The `dioxus serve` can start a dev server (include hot-reload tool) to run the project. 4 | 5 | ``` 6 | dioxus-serve 7 | Build, watch & serve the Rust WASM app and all of its assets 8 | 9 | USAGE: 10 | dioxus serve [OPTIONS] 11 | 12 | OPTIONS: 13 | --example [default: ""] 14 | --platform [default: "default_platform"] 15 | --release [default: false] 16 | ``` 17 | 18 | You can use this command to build project and start a `dev server` : 19 | 20 | ``` 21 | dioxus serve 22 | ``` 23 | 24 | ## Target platform 25 | 26 | Use option `platform` choose build target platform: 27 | 28 | ``` 29 | # for desktop project 30 | dioxus serve --platform desktop 31 | ``` 32 | 33 | `platform` only supports `desktop` & `web`. 34 | 35 | `dev-server` only for `web` project. 36 | 37 | ``` 38 | # for web project 39 | dioxus serve --platform web 40 | ``` 41 | 42 | ## Serve Example 43 | 44 | You can use `--example {name}` to start a example code. 45 | 46 | ``` 47 | # build `example/test` code 48 | dioxus serve --exmaple test 49 | ``` 50 | 51 | ## Open Browser 52 | 53 | You can add `--open` flag to open system default browser when server startup. 54 | 55 | ``` 56 | dioxus serve --open 57 | ``` 58 | 59 | 60 | ## Cross Origin Policy 61 | 62 | use `cross-origin-policy` can change corss-origin header in serverside. 63 | 64 | ``` 65 | Cross-Origin-Opener-Policy: same-origin 66 | Cross-Origin-Embedder-Policy: require-corp 67 | ``` 68 | 69 | ``` 70 | dioxus serve --corss-origin-policy 71 | ``` -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | 3 | name: Rust CI 4 | 5 | jobs: 6 | check: 7 | name: Check 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - uses: actions-rs/toolchain@v1 12 | with: 13 | profile: minimal 14 | toolchain: stable 15 | override: true 16 | - uses: Swatinem/rust-cache@v1 17 | - uses: actions-rs/cargo@v1 18 | with: 19 | command: check 20 | 21 | test: 22 | name: Test Suite 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@v2 26 | - uses: actions-rs/toolchain@v1 27 | with: 28 | profile: minimal 29 | toolchain: stable 30 | override: true 31 | - uses: Swatinem/rust-cache@v1 32 | - uses: actions-rs/cargo@v1 33 | with: 34 | command: test 35 | 36 | fmt: 37 | name: Rustfmt 38 | runs-on: ubuntu-latest 39 | steps: 40 | - uses: actions/checkout@v2 41 | - uses: actions-rs/toolchain@v1 42 | with: 43 | profile: minimal 44 | toolchain: stable 45 | override: true 46 | - uses: Swatinem/rust-cache@v1 47 | - run: rustup component add rustfmt 48 | - uses: actions-rs/cargo@v1 49 | with: 50 | command: fmt 51 | args: --all -- --check 52 | 53 | # clippy: 54 | # name: Clippy 55 | # runs-on: ubuntu-latest 56 | # steps: 57 | # - uses: actions/checkout@v2 58 | # - uses: actions-rs/toolchain@v1 59 | # with: 60 | # profile: minimal 61 | # toolchain: stable 62 | # override: true 63 | # - uses: Swatinem/rust-cache@v1 64 | # - run: rustup component add clippy 65 | # - uses: actions-rs/cargo@v1 66 | # with: 67 | # command: clippy 68 | # args: -- -D warnings 69 | -------------------------------------------------------------------------------- /tests/test.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 | content 11 |
12 |

13 | Buy YouTube Videos 14 |

15 |

16 | Williamsburg occupy sustainable snackwave gochujang. Pinterest 17 | cornhole brunch, slow-carb neutra irony. 18 |

19 | 24 |
25 |
26 |
27 | content 32 |
33 |

34 | The Catalyzer 35 |

36 |

37 | Williamsburg occupy sustainable snackwave gochujang. Pinterest 38 | cornhole brunch, slow-carb neutra irony. 39 |

40 | 45 |
46 |
47 |
48 |
49 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error as ThisError; 2 | 3 | pub type Result = std::result::Result; 4 | 5 | #[derive(ThisError, Debug)] 6 | pub enum Error { 7 | /// Used when errors need to propogate but are too unique to be typed 8 | #[error("{0}")] 9 | Unique(String), 10 | 11 | #[error("I/O Error: {0}")] 12 | IO(#[from] std::io::Error), 13 | 14 | #[error("Format Error: {0}")] 15 | FormatError(#[from] std::fmt::Error), 16 | 17 | #[error("Format failed: {0}")] 18 | ParseError(String), 19 | 20 | #[error("Runtime Error: {0}")] 21 | RuntimeError(String), 22 | 23 | #[error("Failed to write error")] 24 | FailedToWrite, 25 | 26 | #[error("Build Failed: {0}")] 27 | BuildFailed(String), 28 | 29 | #[error("Cargo Error: {0}")] 30 | CargoError(String), 31 | 32 | #[error("{0}")] 33 | CustomError(String), 34 | 35 | #[error("Invalid proxy URL: {0}")] 36 | InvalidProxy(#[from] hyper::http::uri::InvalidUri), 37 | 38 | #[error("Error proxying request: {0}")] 39 | ProxyRequestError(hyper::Error), 40 | 41 | #[error(transparent)] 42 | Other(#[from] anyhow::Error), 43 | } 44 | 45 | impl From<&str> for Error { 46 | fn from(s: &str) -> Self { 47 | Error::Unique(s.to_string()) 48 | } 49 | } 50 | 51 | impl From for Error { 52 | fn from(s: String) -> Self { 53 | Error::Unique(s) 54 | } 55 | } 56 | 57 | impl From for Error { 58 | fn from(e: html_parser::Error) -> Self { 59 | Self::ParseError(e.to_string()) 60 | } 61 | } 62 | 63 | impl From for Error { 64 | fn from(e: hyper::Error) -> Self { 65 | Self::RuntimeError(e.to_string()) 66 | } 67 | } 68 | 69 | #[macro_export] 70 | macro_rules! custom_error { 71 | ($msg:literal $(,)?) => { 72 | Err(Error::CustomError(format!($msg))) 73 | }; 74 | ($err:expr $(,)?) => { 75 | Err(Error::from($err)) 76 | }; 77 | ($fmt:expr, $($arg:tt)*) => { 78 | Err(Error::CustomError(format!($fmt, $($arg)*))) 79 | }; 80 | } 81 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use anyhow::anyhow; 2 | use clap::Parser; 3 | use dioxus_cli::{plugin::PluginManager, *}; 4 | use Commands::*; 5 | 6 | #[tokio::main] 7 | async fn main() -> anyhow::Result<()> { 8 | let args = Cli::parse(); 9 | 10 | set_up_logging(); 11 | 12 | let dioxus_config = DioxusConfig::load() 13 | .map_err(|e| anyhow!("Failed to load `Dioxus.toml` because: {e}"))? 14 | .unwrap_or_else(|| { 15 | log::warn!("You appear to be creating a Dioxus project from scratch; we will use the default config"); 16 | DioxusConfig::default() 17 | }); 18 | 19 | PluginManager::init(dioxus_config.plugin) 20 | .map_err(|e| anyhow!("🚫 Plugin system initialization failed: {e}"))?; 21 | 22 | match args.action { 23 | Translate(opts) => opts 24 | .translate() 25 | .map_err(|e| anyhow!("🚫 Translation of HTML into RSX failed: {}", e)), 26 | 27 | Build(opts) => opts 28 | .build() 29 | .map_err(|e| anyhow!("🚫 Building project failed: {}", e)), 30 | 31 | Clean(opts) => opts 32 | .clean() 33 | .map_err(|e| anyhow!("🚫 Cleaning project failed: {}", e)), 34 | 35 | Serve(opts) => opts 36 | .serve() 37 | .await 38 | .map_err(|e| anyhow!("🚫 Serving project failed: {}", e)), 39 | 40 | Create(opts) => opts 41 | .create() 42 | .map_err(|e| anyhow!("🚫 Creating new project failed: {}", e)), 43 | 44 | Config(opts) => opts 45 | .config() 46 | .map_err(|e| anyhow!("🚫 Configuring new project failed: {}", e)), 47 | 48 | Plugin(opts) => opts 49 | .plugin() 50 | .await 51 | .map_err(|e| anyhow!("🚫 Error with plugin: {}", e)), 52 | 53 | Autoformat(opts) => opts 54 | .autoformat() 55 | .await 56 | .map_err(|e| anyhow!("🚫 Error autoformatting RSX: {}", e)), 57 | 58 | Version(opt) => { 59 | let version = opt.version(); 60 | println!("{}", version); 61 | 62 | Ok(()) 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/plugin/interface/command.rs: -------------------------------------------------------------------------------- 1 | use std::process::{Command, Stdio}; 2 | 3 | use mlua::{FromLua, UserData}; 4 | 5 | enum StdioFromString { 6 | Inhert, 7 | Piped, 8 | Null, 9 | } 10 | impl<'lua> FromLua<'lua> for StdioFromString { 11 | fn from_lua(lua_value: mlua::Value<'lua>, _lua: &'lua mlua::Lua) -> mlua::Result { 12 | if let mlua::Value::String(v) = lua_value { 13 | let v = v.to_str().unwrap(); 14 | return Ok(match v.to_lowercase().as_str() { 15 | "inhert" => Self::Inhert, 16 | "piped" => Self::Piped, 17 | "null" => Self::Null, 18 | _ => Self::Inhert, 19 | }); 20 | } 21 | Ok(Self::Inhert) 22 | } 23 | } 24 | impl StdioFromString { 25 | pub fn to_stdio(self) -> Stdio { 26 | match self { 27 | StdioFromString::Inhert => Stdio::inherit(), 28 | StdioFromString::Piped => Stdio::piped(), 29 | StdioFromString::Null => Stdio::null(), 30 | } 31 | } 32 | } 33 | 34 | pub struct PluginCommander; 35 | impl UserData for PluginCommander { 36 | fn add_methods<'lua, M: mlua::UserDataMethods<'lua, Self>>(methods: &mut M) { 37 | methods.add_function( 38 | "exec", 39 | |_, args: (Vec, StdioFromString, StdioFromString)| { 40 | let cmd = args.0; 41 | let stdout = args.1; 42 | let stderr = args.2; 43 | 44 | if cmd.len() == 0 { 45 | return Ok(()); 46 | } 47 | let cmd_name = cmd.get(0).unwrap(); 48 | let mut command = Command::new(cmd_name); 49 | let t = cmd 50 | .iter() 51 | .enumerate() 52 | .filter(|(i, _)| *i > 0) 53 | .map(|v| v.1.clone()) 54 | .collect::>(); 55 | command.args(t); 56 | command.stdout(stdout.to_stdio()).stderr(stderr.to_stdio()); 57 | command.output()?; 58 | Ok(()) 59 | }, 60 | ); 61 | } 62 | 63 | fn add_fields<'lua, F: mlua::UserDataFields<'lua, Self>>(_fields: &mut F) {} 64 | } 65 | -------------------------------------------------------------------------------- /src/cli/version.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | /// Build the Rust WASM app and all of its assets. 4 | #[derive(Clone, Debug, Parser)] 5 | #[clap(name = "version")] 6 | pub struct Version {} 7 | 8 | impl Version { 9 | pub fn version(self) -> VersionInfo { 10 | version() 11 | } 12 | } 13 | 14 | use std::fmt; 15 | 16 | /// Information about the git repository where rust-analyzer was built from. 17 | pub struct CommitInfo { 18 | pub short_commit_hash: &'static str, 19 | pub commit_hash: &'static str, 20 | pub commit_date: &'static str, 21 | } 22 | 23 | /// Cargo's version. 24 | pub struct VersionInfo { 25 | /// rust-analyzer's version, such as "1.57.0", "1.58.0-beta.1", "1.59.0-nightly", etc. 26 | pub version: &'static str, 27 | 28 | /// The release channel we were built for (stable/beta/nightly/dev). 29 | /// 30 | /// `None` if not built via rustbuild. 31 | pub release_channel: Option<&'static str>, 32 | 33 | /// Information about the Git repository we may have been built from. 34 | /// 35 | /// `None` if not built from a git repo. 36 | pub commit_info: Option, 37 | } 38 | 39 | impl fmt::Display for VersionInfo { 40 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 41 | write!(f, "{}", self.version)?; 42 | 43 | if let Some(ci) = &self.commit_info { 44 | write!(f, " ({} {})", ci.short_commit_hash, ci.commit_date)?; 45 | }; 46 | Ok(()) 47 | } 48 | } 49 | 50 | /// Returns information about cargo's version. 51 | pub const fn version() -> VersionInfo { 52 | let version = match option_env!("CFG_RELEASE") { 53 | Some(x) => x, 54 | None => "0.0.0", 55 | }; 56 | 57 | let release_channel = option_env!("CFG_RELEASE_CHANNEL"); 58 | let commit_info = match ( 59 | option_env!("RA_COMMIT_SHORT_HASH"), 60 | option_env!("RA_COMMIT_HASH"), 61 | option_env!("RA_COMMIT_DATE"), 62 | ) { 63 | (Some(short_commit_hash), Some(commit_hash), Some(commit_date)) => Some(CommitInfo { 64 | short_commit_hash, 65 | commit_hash, 66 | commit_date, 67 | }), 68 | _ => None, 69 | }; 70 | 71 | VersionInfo { 72 | version, 73 | release_channel, 74 | commit_info, 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/cli/tool/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::tools; 2 | 3 | use super::*; 4 | 5 | /// Build the Rust WASM app and all of its assets. 6 | #[derive(Clone, Debug, Deserialize, Subcommand)] 7 | #[clap(name = "tool")] 8 | pub enum Tool { 9 | /// Return all dioxus-cli support tools. 10 | List {}, 11 | /// Get default app install path. 12 | AppPath {}, 13 | /// Install a new tool. 14 | Add { name: String }, 15 | } 16 | 17 | impl Tool { 18 | pub async fn tool(self) -> Result<()> { 19 | match self { 20 | Tool::List {} => { 21 | for item in tools::tool_list() { 22 | if tools::Tool::from_str(item).unwrap().is_installed() { 23 | println!("- {item} [installed]"); 24 | } else { 25 | println!("- {item}"); 26 | } 27 | } 28 | } 29 | Tool::AppPath {} => { 30 | if let Some(v) = tools::tools_path().to_str() { 31 | println!("{}", v); 32 | } else { 33 | return custom_error!("Tools path get failed."); 34 | } 35 | } 36 | Tool::Add { name } => { 37 | let tool_list = tools::tool_list(); 38 | 39 | if !tool_list.contains(&name.as_str()) { 40 | return custom_error!("Tool {name} not found."); 41 | } 42 | let target_tool = tools::Tool::from_str(&name).unwrap(); 43 | 44 | if target_tool.is_installed() { 45 | log::warn!("Tool {name} is installed."); 46 | return Ok(()); 47 | } 48 | 49 | log::info!("Start to download tool package..."); 50 | if let Err(e) = target_tool.download_package().await { 51 | return custom_error!("Tool download failed: {e}"); 52 | } 53 | 54 | log::info!("Start to install tool package..."); 55 | if let Err(e) = target_tool.install_package().await { 56 | return custom_error!("Tool install failed: {e}"); 57 | } 58 | 59 | log::info!("Tool {name} installed successfully!"); 60 | } 61 | } 62 | Ok(()) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/cli/config/mod.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | /// Build the Rust WASM app and all of its assets. 4 | #[derive(Clone, Debug, Deserialize, Subcommand)] 5 | #[clap(name = "config")] 6 | pub enum Config { 7 | /// Init `Dioxus.toml` for project/folder. 8 | Init { 9 | /// Init project name 10 | name: String, 11 | 12 | /// Cover old config 13 | #[clap(long)] 14 | #[serde(default)] 15 | force: bool, 16 | 17 | /// Project default platform 18 | #[clap(long, default_value = "web")] 19 | platform: String, 20 | }, 21 | /// Format print Dioxus config. 22 | FormatPrint {}, 23 | /// Create a custom html file. 24 | CustomHtml {}, 25 | } 26 | 27 | impl Config { 28 | pub fn config(self) -> Result<()> { 29 | let crate_root = crate::cargo::crate_root()?; 30 | match self { 31 | Config::Init { 32 | name, 33 | force, 34 | platform, 35 | } => { 36 | let conf_path = crate_root.join("Dioxus.toml"); 37 | if conf_path.is_file() && !force { 38 | log::warn!( 39 | "config file `Dioxus.toml` already exist, use `--force` to overwrite it." 40 | ); 41 | return Ok(()); 42 | } 43 | let mut file = File::create(conf_path)?; 44 | let content = String::from(include_str!("../../assets/dioxus.toml")) 45 | .replace("{{project-name}}", &name) 46 | .replace("{{default-platform}}", &platform); 47 | file.write_all(content.as_bytes())?; 48 | log::info!("🚩 Init config file completed."); 49 | } 50 | Config::FormatPrint {} => { 51 | println!("{:#?}", crate::CrateConfig::new()?.dioxus_config); 52 | } 53 | Config::CustomHtml {} => { 54 | let html_path = crate_root.join("index.html"); 55 | let mut file = File::create(html_path)?; 56 | let content = include_str!("../../assets/index.html"); 57 | file.write_all(content.as_bytes())?; 58 | log::info!("🚩 Create custom html file done."); 59 | } 60 | } 61 | Ok(()) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "dioxus-cli" 3 | version = "0.3.1" 4 | authors = ["Jonathan Kelley"] 5 | edition = "2021" 6 | description = "CLI tool for developing, testing, and publishing Dioxus apps" 7 | license = "MIT/Apache-2.0" 8 | 9 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 10 | 11 | [dependencies] 12 | 13 | # cli core 14 | clap = { version = "3.0.14", features = ["derive"] } 15 | thiserror = "1.0.30" 16 | wasm-bindgen-cli-support = "0.2" 17 | colored = "2.0.0" 18 | 19 | # features 20 | log = "0.4.14" 21 | fern = { version = "0.6.0", features = ["colored"] } 22 | serde = { version = "1.0.136", features = ["derive"] } 23 | serde_json = "1.0.79" 24 | toml = "0.5.8" 25 | fs_extra = "1.2.0" 26 | cargo_toml = "0.11.4" 27 | futures = "0.3.21" 28 | notify = { version = "5.0.0-pre.16", features = ["serde"] } 29 | html_parser = "0.6.2" 30 | binary-install = "0.0.2" 31 | convert_case = "0.5.0" 32 | cargo_metadata = "0.15.0" 33 | tokio = { version = "1.16.1", features = ["full"] } 34 | atty = "0.2.14" 35 | regex = "1.5.4" 36 | chrono = "0.4.19" 37 | anyhow = "1.0.53" 38 | hyper = "0.14.17" 39 | hyper-rustls = "0.23.2" 40 | indicatif = "0.17.0-rc.11" 41 | subprocess = "0.2.9" 42 | 43 | axum = { version = "0.5.1", features = ["ws", "headers"] } 44 | tower-http = { version = "0.2.2", features = ["full"] } 45 | headers = "0.3.7" 46 | 47 | walkdir = "2" 48 | 49 | # tools download 50 | dirs = "4.0.0" 51 | reqwest = { version = "0.11", features = [ 52 | "rustls-tls", 53 | "stream", 54 | "trust-dns", 55 | "blocking", 56 | ] } 57 | flate2 = "1.0.22" 58 | tar = "0.4.38" 59 | zip = "0.6.2" 60 | tower = "0.4.12" 61 | 62 | syn = { version = "1.0", features = ["full", "extra-traits"] } 63 | 64 | 65 | proc-macro2 = { version = "1.0", features = ["span-locations"] } 66 | lazy_static = "1.4.0" 67 | 68 | # plugin packages 69 | mlua = { version = "0.8.1", features = [ 70 | "lua54", 71 | "vendored", 72 | "async", 73 | "send", 74 | "macros", 75 | ] } 76 | ctrlc = "3.2.3" 77 | # dioxus-rsx = "0.0.1" 78 | gitignore = "1.0.7" 79 | 80 | dioxus-rsx = { version = "0.0.3" } 81 | dioxus-html = { version = "0.3", features = ["hot-reload-context"] } 82 | dioxus-core = { version = "0.3", features = ["serialize"] } 83 | dioxus-autofmt = "0.3.0" 84 | rsx-rosetta = { version = "0.3" } 85 | open = "4.1.0" 86 | 87 | [[bin]] 88 | path = "src/main.rs" 89 | 90 | name = "dioxus" 91 | -------------------------------------------------------------------------------- /src/cli/build/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::plugin::PluginManager; 2 | 3 | use super::*; 4 | 5 | /// Build the Rust WASM app and all of its assets. 6 | #[derive(Clone, Debug, Parser)] 7 | #[clap(name = "build")] 8 | pub struct Build { 9 | #[clap(flatten)] 10 | pub build: ConfigOptsBuild, 11 | } 12 | 13 | impl Build { 14 | pub fn build(self) -> Result<()> { 15 | let mut crate_config = crate::CrateConfig::new()?; 16 | 17 | // change the release state. 18 | crate_config.with_release(self.build.release); 19 | crate_config.with_verbose(self.build.verbose); 20 | 21 | if self.build.example.is_some() { 22 | crate_config.as_example(self.build.example.unwrap()); 23 | } 24 | 25 | if self.build.profile.is_some() { 26 | crate_config.set_profile(self.build.profile.unwrap()); 27 | } 28 | 29 | if self.build.features.is_some() { 30 | crate_config.set_features(self.build.features.unwrap()); 31 | } 32 | 33 | let platform = self.build.platform.unwrap_or_else(|| { 34 | crate_config 35 | .dioxus_config 36 | .application 37 | .default_platform 38 | .clone() 39 | }); 40 | 41 | let _ = PluginManager::on_build_start(&crate_config, &platform); 42 | 43 | match platform.as_str() { 44 | "web" => { 45 | crate::builder::build(&crate_config, false)?; 46 | } 47 | "desktop" => { 48 | crate::builder::build_desktop(&crate_config, false)?; 49 | } 50 | _ => { 51 | return custom_error!("Unsupported platform target."); 52 | } 53 | } 54 | 55 | let temp = gen_page(&crate_config.dioxus_config, false); 56 | 57 | let mut file = std::fs::File::create( 58 | crate_config 59 | .crate_dir 60 | .join( 61 | crate_config 62 | .dioxus_config 63 | .application 64 | .out_dir 65 | .clone() 66 | .unwrap_or_else(|| PathBuf::from("dist")), 67 | ) 68 | .join("index.html"), 69 | )?; 70 | file.write_all(temp.as_bytes())?; 71 | 72 | let _ = PluginManager::on_build_finish(&crate_config, &platform); 73 | 74 | Ok(()) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/cli/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod autoformat; 2 | pub mod build; 3 | pub mod cfg; 4 | pub mod clean; 5 | pub mod config; 6 | pub mod create; 7 | pub mod plugin; 8 | pub mod serve; 9 | pub mod translate; 10 | pub mod version; 11 | 12 | use crate::{ 13 | cfg::{ConfigOptsBuild, ConfigOptsServe}, 14 | custom_error, 15 | error::Result, 16 | gen_page, server, CrateConfig, Error, 17 | }; 18 | use clap::{Parser, Subcommand}; 19 | use html_parser::Dom; 20 | use regex::Regex; 21 | use serde::Deserialize; 22 | use std::{ 23 | fs::{remove_dir_all, File}, 24 | io::{Read, Write}, 25 | path::PathBuf, 26 | process::{Command, Stdio}, 27 | }; 28 | 29 | /// Build, Bundle & Ship Dioxus Apps. 30 | #[derive(Parser)] 31 | #[clap(name = "dioxus", version)] 32 | pub struct Cli { 33 | #[clap(subcommand)] 34 | pub action: Commands, 35 | 36 | /// Enable verbose logging. 37 | #[clap(short)] 38 | pub v: bool, 39 | } 40 | 41 | #[derive(Parser)] 42 | pub enum Commands { 43 | /// Build the Rust WASM app and all of its assets. 44 | Build(build::Build), 45 | 46 | /// Translate some source file into Dioxus code. 47 | Translate(translate::Translate), 48 | 49 | /// Build, watch & serve the Rust WASM app and all of its assets. 50 | Serve(serve::Serve), 51 | 52 | /// Init a new project for Dioxus. 53 | Create(create::Create), 54 | 55 | /// Clean output artifacts. 56 | Clean(clean::Clean), 57 | 58 | /// Print the version of this extension 59 | #[clap(name = "version")] 60 | Version(version::Version), 61 | 62 | /// Format some rsx 63 | #[clap(name = "fmt")] 64 | Autoformat(autoformat::Autoformat), 65 | 66 | /// Dioxus config file controls. 67 | #[clap(subcommand)] 68 | Config(config::Config), 69 | 70 | /// Manage plugins for dioxus cli 71 | #[clap(subcommand)] 72 | Plugin(plugin::Plugin), 73 | } 74 | 75 | impl Commands { 76 | pub fn to_string(&self) -> String { 77 | match self { 78 | Commands::Build(_) => "build", 79 | Commands::Translate(_) => "translate", 80 | Commands::Serve(_) => "serve", 81 | Commands::Create(_) => "create", 82 | Commands::Clean(_) => "clean", 83 | Commands::Config(_) => "config", 84 | Commands::Plugin(_) => "plugin", 85 | Commands::Version(_) => "version", 86 | Commands::Autoformat(_) => "fmt", 87 | } 88 | .to_string() 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/cli/cfg.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | /// Config options for the build system. 4 | #[derive(Clone, Debug, Default, Deserialize, Parser)] 5 | pub struct ConfigOptsBuild { 6 | /// The index HTML file to drive the bundling process [default: index.html] 7 | #[clap(parse(from_os_str))] 8 | pub target: Option, 9 | 10 | /// Build in release mode [default: false] 11 | #[clap(long)] 12 | #[serde(default)] 13 | pub release: bool, 14 | 15 | // Use verbose output [default: false] 16 | #[clap(long)] 17 | #[serde(default)] 18 | pub verbose: bool, 19 | 20 | /// Build a example [default: ""] 21 | #[clap(long)] 22 | pub example: Option, 23 | 24 | /// Build with custom profile 25 | #[clap(long)] 26 | pub profile: Option, 27 | 28 | /// Build platform: support Web & Desktop [default: "default_platform"] 29 | #[clap(long)] 30 | pub platform: Option, 31 | 32 | /// Space separated list of features to activate 33 | #[clap(long)] 34 | pub features: Option>, 35 | } 36 | 37 | #[derive(Clone, Debug, Default, Deserialize, Parser)] 38 | pub struct ConfigOptsServe { 39 | /// The index HTML file to drive the bundling process [default: index.html] 40 | #[clap(parse(from_os_str))] 41 | pub target: Option, 42 | 43 | /// Port of dev server 44 | #[clap(long)] 45 | #[clap(default_value_t = 8080)] 46 | pub port: u16, 47 | 48 | /// Open the app in the default browser [default: false] 49 | #[clap(long)] 50 | #[serde(default)] 51 | pub open: bool, 52 | 53 | /// Build a example [default: ""] 54 | #[clap(long)] 55 | pub example: Option, 56 | 57 | /// Build in release mode [default: false] 58 | #[clap(long)] 59 | #[serde(default)] 60 | pub release: bool, 61 | 62 | // Use verbose output [default: false] 63 | #[clap(long)] 64 | #[serde(default)] 65 | pub verbose: bool, 66 | 67 | /// Build with custom profile 68 | #[clap(long)] 69 | pub profile: Option, 70 | 71 | /// Build platform: support Web & Desktop [default: "default_platform"] 72 | #[clap(long)] 73 | pub platform: Option, 74 | 75 | /// Build with hot reloading rsx [default: false] 76 | #[clap(long)] 77 | #[serde(default)] 78 | pub hot_reload: bool, 79 | 80 | /// Set cross-origin-policy to same-origin [default: false] 81 | #[clap(name = "cross-origin-policy")] 82 | #[clap(long)] 83 | #[serde(default)] 84 | pub cross_origin_policy: bool, 85 | 86 | /// Space separated list of features to activate 87 | #[clap(long)] 88 | pub features: Option>, 89 | } 90 | 91 | /// Ensure the given value for `--public-url` is formatted correctly. 92 | pub fn parse_public_url(val: &str) -> String { 93 | let prefix = if !val.starts_with('/') { "/" } else { "" }; 94 | let suffix = if !val.ends_with('/') { "/" } else { "" }; 95 | format!("{}{}{}", prefix, val, suffix) 96 | } 97 | -------------------------------------------------------------------------------- /src/plugin/interface/fs.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fs::{create_dir, create_dir_all, remove_dir_all, File}, 3 | io::{Read, Write}, 4 | path::PathBuf, 5 | }; 6 | 7 | use crate::tools::extract_zip; 8 | use flate2::read::GzDecoder; 9 | use mlua::UserData; 10 | use tar::Archive; 11 | 12 | pub struct PluginFileSystem; 13 | impl UserData for PluginFileSystem { 14 | fn add_methods<'lua, M: mlua::UserDataMethods<'lua, Self>>(methods: &mut M) { 15 | methods.add_function("create_dir", |_, args: (String, bool)| { 16 | let path = args.0; 17 | let recursive = args.1; 18 | let path = PathBuf::from(path); 19 | if !path.exists() { 20 | let v = if recursive { 21 | create_dir_all(path) 22 | } else { 23 | create_dir(path) 24 | }; 25 | return Ok(v.is_ok()); 26 | } 27 | Ok(true) 28 | }); 29 | methods.add_function("remove_dir", |_, path: String| { 30 | let path = PathBuf::from(path); 31 | let r = remove_dir_all(path); 32 | Ok(r.is_ok()) 33 | }); 34 | methods.add_function("file_get_content", |_, path: String| { 35 | let path = PathBuf::from(path); 36 | let mut file = std::fs::File::open(path)?; 37 | let mut buffer = String::new(); 38 | file.read_to_string(&mut buffer)?; 39 | Ok(buffer) 40 | }); 41 | methods.add_function("file_set_content", |_, args: (String, String)| { 42 | let path = args.0; 43 | let content = args.1; 44 | let path = PathBuf::from(path); 45 | 46 | let file = std::fs::File::create(path); 47 | if file.is_err() { 48 | return Ok(false); 49 | } 50 | 51 | if file.unwrap().write_all(content.as_bytes()).is_err() { 52 | return Ok(false); 53 | } 54 | 55 | Ok(true) 56 | }); 57 | methods.add_function("unzip_file", |_, args: (String, String)| { 58 | let file = PathBuf::from(args.0); 59 | let target = PathBuf::from(args.1); 60 | let res = extract_zip(&file, &target); 61 | if let Err(_) = res { 62 | return Ok(false); 63 | } 64 | Ok(true) 65 | }); 66 | methods.add_function("untar_gz_file", |_, args: (String, String)| { 67 | let file = PathBuf::from(args.0); 68 | let target = PathBuf::from(args.1); 69 | 70 | let tar_gz = if let Ok(v) = File::open(file) { 71 | v 72 | } else { 73 | return Ok(false); 74 | }; 75 | 76 | let tar = GzDecoder::new(tar_gz); 77 | let mut archive = Archive::new(tar); 78 | if archive.unpack(&target).is_err() { 79 | return Ok(false); 80 | } 81 | 82 | Ok(true) 83 | }); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /docs/src/plugin/README.md: -------------------------------------------------------------------------------- 1 | # CLI Plugin Development 2 | 3 | > For Cli 0.2.0 we will add `plugin-develop` support. 4 | 5 | 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. 6 | 7 | ### Why Lua ? 8 | 9 | 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. 10 | 11 | ### Event Management 12 | 13 | The plugin library have pre-define some important event you can control: 14 | 15 | - `build.on_start` 16 | - `build.on_finished` 17 | - `serve.on_start` 18 | - `serve.on_rebuild` 19 | - `serve.on_shutdown` 20 | 21 | ### Plugin Template 22 | 23 | ```lua 24 | package.path = library_dir .. "/?.lua" 25 | 26 | local plugin = require("plugin") 27 | local manager = require("manager") 28 | 29 | -- deconstruct api functions 30 | local log = plugin.log 31 | 32 | -- plugin information 33 | manager.name = "Hello Dixous Plugin" 34 | manager.repository = "https://github.com/mrxiaozhuox/hello-dioxus-plugin" 35 | manager.author = "YuKun Liu " 36 | manager.version = "0.0.1" 37 | 38 | -- init manager info to plugin api 39 | plugin.init(manager) 40 | 41 | manager.on_init = function () 42 | -- when the first time plugin been load, this function will be execute. 43 | -- system will create a `dcp.json` file to verify init state. 44 | log.info("[plugin] Start to init plugin: " .. manager.name) 45 | end 46 | 47 | ---@param info BuildInfo 48 | manager.build.on_start = function (info) 49 | -- before the build work start, system will execute this function. 50 | log.info("[plugin] Build starting: " .. info.name) 51 | end 52 | 53 | ---@param info BuildInfo 54 | manager.build.on_finish = function (info) 55 | -- when the build work is done, system will execute this function. 56 | log.info("[plugin] Build finished: " .. info.name) 57 | end 58 | 59 | ---@param info ServeStartInfo 60 | manager.serve.on_start = function (info) 61 | -- this function will after clean & print to run, so you can print some thing. 62 | log.info("[plugin] Serve start: " .. info.name) 63 | end 64 | 65 | ---@param info ServeRebuildInfo 66 | manager.serve.on_rebuild = function (info) 67 | -- this function will after clean & print to run, so you can print some thing. 68 | local files = plugin.tool.dump(info.changed_files) 69 | log.info("[plugin] Serve rebuild: '" .. files .. "'") 70 | end 71 | 72 | manager.serve.on_shutdown = function () 73 | log.info("[plugin] Serve shutdown") 74 | end 75 | 76 | manager.serve.interval = 1000 77 | 78 | return manager 79 | ``` -------------------------------------------------------------------------------- /src/cli/create/mod.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use crate::custom_error; 3 | 4 | /// Build the Rust WASM app and all of its assets. 5 | #[derive(Clone, Debug, Default, Deserialize, Parser)] 6 | #[clap(name = "create")] 7 | pub struct Create { 8 | /// Init project name 9 | #[clap(default_value = ".")] 10 | name: String, 11 | 12 | /// Template path 13 | #[clap(default_value = "gh:dioxuslabs/dioxus-template", long)] 14 | template: String, 15 | } 16 | 17 | impl Create { 18 | pub fn create(self) -> Result<()> { 19 | if Self::name_valid_check(self.name.clone()) { 20 | return custom_error!("❗Unsupported project name: '{}'.", &self.name); 21 | } 22 | 23 | let project_path = PathBuf::from(&self.name); 24 | 25 | if project_path.join("Dioxus.toml").is_file() || project_path.join("Cargo.toml").is_file() { 26 | return custom_error!("🧨 Folder '{}' is initialized.", &self.name); 27 | } 28 | 29 | log::info!("🔧 Start: Creating new project '{}'.", self.name); 30 | 31 | let output = Command::new("cargo") 32 | .arg("generate") 33 | .arg("--help") 34 | .stdout(Stdio::piped()) 35 | .stderr(Stdio::piped()) 36 | .output()?; 37 | 38 | if !output.status.success() { 39 | log::warn!("Tool is not installed: cargo-generate, try to install it."); 40 | let install_output = Command::new("cargo") 41 | .arg("install") 42 | .arg("cargo-generate") 43 | .stdout(Stdio::inherit()) 44 | .stderr(Stdio::inherit()) 45 | .output()?; 46 | if !install_output.status.success() { 47 | return custom_error!("Try to install cargo-generate failed."); 48 | } 49 | } 50 | 51 | let generate_output = Command::new("cargo") 52 | .arg("generate") 53 | .arg(&self.template) 54 | .arg("--name") 55 | .arg(&self.name) 56 | .arg("--force") 57 | .stdout(Stdio::piped()) 58 | .stderr(Stdio::inherit()) 59 | .output()?; 60 | 61 | if !generate_output.status.success() { 62 | return custom_error!("Generate project failed. Try to update cargo-generate."); 63 | } 64 | 65 | let mut dioxus_file = File::open(project_path.join("Dioxus.toml"))?; 66 | let mut meta_file = String::new(); 67 | dioxus_file.read_to_string(&mut meta_file)?; 68 | meta_file = meta_file.replace("{{project-name}}", &self.name); 69 | meta_file = meta_file.replace("{{default-platform}}", "web"); 70 | File::create(project_path.join("Dioxus.toml"))?.write_all(meta_file.as_bytes())?; 71 | 72 | println!(); 73 | log::info!("💡 Project initialized:"); 74 | log::info!("🎯> cd ./{}", self.name); 75 | log::info!("🎯> dioxus serve"); 76 | 77 | Ok(()) 78 | } 79 | 80 | fn name_valid_check(name: String) -> bool { 81 | let r = Regex::new(r"^[a-zA-Z][a-zA-Z0-9\-_]$").unwrap(); 82 | r.is_match(&name) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/cargo.rs: -------------------------------------------------------------------------------- 1 | //! Utilities for working with cargo and rust files 2 | use crate::error::{Error, Result}; 3 | use std::{ 4 | env, fs, 5 | path::{Path, PathBuf}, 6 | process::Command, 7 | str, 8 | }; 9 | 10 | /// How many parent folders are searched for a `Cargo.toml` 11 | const MAX_ANCESTORS: u32 = 10; 12 | 13 | /// Some fields parsed from `cargo metadata` command 14 | pub struct Metadata { 15 | pub workspace_root: PathBuf, 16 | pub target_directory: PathBuf, 17 | } 18 | 19 | /// Returns the root of the crate that the command is run from 20 | /// 21 | /// If the command is run from the workspace root, this will return the top-level Cargo.toml 22 | pub fn crate_root() -> Result { 23 | // From the current directory we work our way up, looking for `Cargo.toml` 24 | env::current_dir() 25 | .ok() 26 | .and_then(|mut wd| { 27 | for _ in 0..MAX_ANCESTORS { 28 | if contains_manifest(&wd) { 29 | return Some(wd); 30 | } 31 | if !wd.pop() { 32 | break; 33 | } 34 | } 35 | None 36 | }) 37 | .ok_or_else(|| { 38 | Error::CargoError("Failed to find directory containing Cargo.toml".to_string()) 39 | }) 40 | } 41 | 42 | /// Checks if the directory contains `Cargo.toml` 43 | fn contains_manifest(path: &Path) -> bool { 44 | fs::read_dir(path) 45 | .map(|entries| { 46 | entries 47 | .filter_map(Result::ok) 48 | .any(|ent| &ent.file_name() == "Cargo.toml") 49 | }) 50 | .unwrap_or(false) 51 | } 52 | 53 | impl Metadata { 54 | /// Returns the struct filled from `cargo metadata` output 55 | /// TODO @Jon, find a different way that doesn't rely on the cargo metadata command (it's slow) 56 | pub fn get() -> Result { 57 | let output = Command::new("cargo") 58 | .args(&["metadata"]) 59 | .output() 60 | .map_err(|_| Error::CargoError("Manifset".to_string()))?; 61 | 62 | if !output.status.success() { 63 | let mut msg = str::from_utf8(&output.stderr).unwrap().trim(); 64 | if msg.starts_with("error: ") { 65 | msg = &msg[7..]; 66 | } 67 | 68 | return Err(Error::CargoError(msg.to_string())); 69 | } 70 | 71 | let stdout = str::from_utf8(&output.stdout).unwrap(); 72 | if let Some(line) = stdout.lines().next() { 73 | let meta: serde_json::Value = serde_json::from_str(line) 74 | .map_err(|_| Error::CargoError("InvalidOutput".to_string()))?; 75 | 76 | let workspace_root = meta["workspace_root"] 77 | .as_str() 78 | .ok_or_else(|| Error::CargoError("InvalidOutput".to_string()))? 79 | .into(); 80 | 81 | let target_directory = meta["target_directory"] 82 | .as_str() 83 | .ok_or_else(|| Error::CargoError("InvalidOutput".to_string()))? 84 | .into(); 85 | 86 | return Ok(Self { 87 | workspace_root, 88 | target_directory, 89 | }); 90 | } 91 | 92 | Err(Error::CargoError("InvalidOutput".to_string())) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/cli/serve/mod.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use std::{ 3 | fs::create_dir_all, 4 | io::Write, 5 | path::PathBuf, 6 | process::{Command, Stdio}, 7 | }; 8 | 9 | /// Run the WASM project on dev-server 10 | #[derive(Clone, Debug, Parser)] 11 | #[clap(name = "serve")] 12 | pub struct Serve { 13 | #[clap(flatten)] 14 | pub serve: ConfigOptsServe, 15 | } 16 | 17 | impl Serve { 18 | pub async fn serve(self) -> Result<()> { 19 | let mut crate_config = crate::CrateConfig::new()?; 20 | 21 | // change the relase state. 22 | crate_config.with_hot_reload(self.serve.hot_reload); 23 | crate_config.with_cross_origin_policy(self.serve.cross_origin_policy); 24 | crate_config.with_release(self.serve.release); 25 | crate_config.with_verbose(self.serve.verbose); 26 | 27 | if self.serve.example.is_some() { 28 | crate_config.as_example(self.serve.example.unwrap()); 29 | } 30 | 31 | if self.serve.profile.is_some() { 32 | crate_config.set_profile(self.serve.profile.unwrap()); 33 | } 34 | 35 | if self.serve.features.is_some() { 36 | crate_config.set_features(self.serve.features.unwrap()); 37 | } 38 | 39 | let platform = self.serve.platform.unwrap_or_else(|| { 40 | crate_config 41 | .dioxus_config 42 | .application 43 | .default_platform 44 | .clone() 45 | }); 46 | 47 | if platform.as_str() == "desktop" { 48 | crate::builder::build_desktop(&crate_config, true)?; 49 | 50 | match &crate_config.executable { 51 | crate::ExecutableType::Binary(name) 52 | | crate::ExecutableType::Lib(name) 53 | | crate::ExecutableType::Example(name) => { 54 | let mut file = crate_config.out_dir.join(name); 55 | if cfg!(windows) { 56 | file.set_extension("exe"); 57 | } 58 | Command::new(file.to_str().unwrap()) 59 | .stdout(Stdio::inherit()) 60 | .output()?; 61 | } 62 | } 63 | return Ok(()); 64 | } else if platform != "web" { 65 | return custom_error!("Unsupported platform target."); 66 | } 67 | 68 | // generate dev-index page 69 | Serve::regen_dev_page(&crate_config)?; 70 | 71 | // start the develop server 72 | server::startup(self.serve.port, crate_config.clone(), self.serve.open).await?; 73 | 74 | Ok(()) 75 | } 76 | 77 | pub fn regen_dev_page(crate_config: &CrateConfig) -> Result<()> { 78 | let serve_html = gen_page(&crate_config.dioxus_config, true); 79 | 80 | let dist_path = crate_config.crate_dir.join( 81 | crate_config 82 | .dioxus_config 83 | .application 84 | .out_dir 85 | .clone() 86 | .unwrap_or_else(|| PathBuf::from("dist")), 87 | ); 88 | if !dist_path.is_dir() { 89 | create_dir_all(&dist_path)?; 90 | } 91 | let index_path = dist_path.join("index.html"); 92 | let mut file = std::fs::File::create(index_path)?; 93 | file.write_all(serve_html.as_bytes())?; 94 | 95 | Ok(()) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /extension/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dioxus", 3 | "displayName": "Dioxus", 4 | "description": "Useful tools for working with Dioxus", 5 | "version": "0.0.1", 6 | "publisher": "DioxusLabs", 7 | "private": true, 8 | "license": "MIT", 9 | "icon": "static/icon.png", 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/DioxusLabs/cli" 13 | }, 14 | "engines": { 15 | "vscode": "^1.68.1" 16 | }, 17 | "categories": [ 18 | "Programming Languages" 19 | ], 20 | "activationEvents": [ 21 | "onCommand:extension.htmlToDioxusRsx", 22 | "onCommand:extension.htmlToDioxusComponent", 23 | "onCommand:extension.formatRsx", 24 | "onCommand:extension.formatRsxDocument" 25 | ], 26 | "main": "./out/main", 27 | "contributes": { 28 | "commands": [ 29 | { 30 | "command": "extension.htmlToDioxusRsx", 31 | "title": "Dioxus: Convert HTML to RSX" 32 | }, 33 | { 34 | "command": "extension.htmlToDioxusComponent", 35 | "title": "Dioxus: Convert HTML to Component" 36 | }, 37 | { 38 | "command": "extension.formatRsx", 39 | "title": "Dioxus: Format RSX" 40 | }, 41 | { 42 | "command": "extension.formatRsxDocument", 43 | "title": "Dioxus: Format RSX Document" 44 | } 45 | ], 46 | "configuration": { 47 | "properties": { 48 | "dioxus.formatOnSave": { 49 | "type": [ 50 | "string" 51 | ], 52 | "default": "followFormatOnSave", 53 | "enum": [ 54 | "followFormatOnSave", 55 | "enabled", 56 | "disabled" 57 | ], 58 | "enumItemLabels": [ 59 | "Follow the normal formatOnSave config", 60 | "Enabled", 61 | "Disabled" 62 | ], 63 | "enumDescriptions": [ 64 | "Only format Rsx when saving files if the editor.formatOnSave config is enabled", 65 | "Always format Rsx when a Rust file is saved", 66 | "Never format Rsx when a file is saved" 67 | ], 68 | "description": "Format RSX when a file is saved." 69 | } 70 | } 71 | } 72 | }, 73 | "scripts": { 74 | "vscode:prepublish": "npm run build-base -- --minify", 75 | "package": "vsce package -o rust-analyzer.vsix", 76 | "build-base": "esbuild ./src/main.ts --bundle --outfile=out/main.js --external:vscode --format=cjs --platform=node --target=node16", 77 | "build": "npm run build-base -- --sourcemap", 78 | "watch": "npm run build-base -- --sourcemap --watch", 79 | "lint": "prettier --check . && eslint -c .eslintrc.js --ext ts ./src ./tests", 80 | "fix": "prettier --write . && eslint -c .eslintrc.js --ext ts ./src ./tests --fix", 81 | "pretest": "tsc && npm run build", 82 | "test": "cross-env TEST_VARIABLE=test node ./out/tests/runTests.js" 83 | }, 84 | "devDependencies": { 85 | "@types/node": "^18.0.2", 86 | "@types/vscode": "^1.68.1", 87 | "@typescript-eslint/eslint-plugin": "^5.30.5", 88 | "@typescript-eslint/parser": "^5.30.5", 89 | "cross-env": "^7.0.3", 90 | "esbuild": "^0.14.27", 91 | "eslint": "^8.19.0", 92 | "typescript": "^4.7.4", 93 | "eslint-config-prettier": "^8.5.0", 94 | "ovsx": "^0.5.1", 95 | "prettier": "^2.6.2", 96 | "tslib": "^2.3.0", 97 | "vsce": "^2.7.0" 98 | }, 99 | "dependencies": { 100 | "vsce": "^2.9.2" 101 | } 102 | } -------------------------------------------------------------------------------- /src/cli/translate/mod.rs: -------------------------------------------------------------------------------- 1 | use std::process::exit; 2 | 3 | use dioxus_rsx::{BodyNode, CallBody}; 4 | 5 | use super::*; 6 | 7 | /// Build the Rust WASM app and all of its assets. 8 | #[derive(Clone, Debug, Parser)] 9 | #[clap(name = "translate")] 10 | pub struct Translate { 11 | /// Activate debug mode 12 | // short and long flags (-d, --debug) will be deduced from the field's name 13 | #[clap(short, long)] 14 | pub component: bool, 15 | 16 | /// Input file 17 | #[clap(short, long)] 18 | pub file: Option, 19 | 20 | /// Input file 21 | #[clap(short, long)] 22 | pub raw: Option, 23 | 24 | /// Output file, stdout if not present 25 | #[clap(parse(from_os_str))] 26 | pub output: Option, 27 | } 28 | 29 | impl Translate { 30 | pub fn translate(self) -> Result<()> { 31 | // Get the right input for the translation 32 | let contents = determine_input(self.file, self.raw)?; 33 | 34 | // Ensure we're loading valid HTML 35 | let dom = html_parser::Dom::parse(&contents)?; 36 | 37 | // Convert the HTML to RSX 38 | let out = convert_html_to_formatted_rsx(&dom, self.component); 39 | 40 | // Write the output 41 | match self.output { 42 | Some(output) => std::fs::write(&output, out)?, 43 | None => print!("{}", out), 44 | } 45 | 46 | Ok(()) 47 | } 48 | } 49 | 50 | pub fn convert_html_to_formatted_rsx(dom: &Dom, component: bool) -> String { 51 | let callbody = rsx_rosetta::rsx_from_html(&dom); 52 | 53 | match component { 54 | true => write_callbody_with_icon_section(callbody), 55 | false => dioxus_autofmt::write_block_out(callbody).unwrap(), 56 | } 57 | } 58 | 59 | fn write_callbody_with_icon_section(mut callbody: CallBody) -> String { 60 | let mut svgs = vec![]; 61 | 62 | rsx_rosetta::collect_svgs(&mut callbody.roots, &mut svgs); 63 | 64 | let mut out = write_component_body(dioxus_autofmt::write_block_out(callbody).unwrap()); 65 | 66 | if !svgs.is_empty() { 67 | write_svg_section(&mut out, svgs); 68 | } 69 | 70 | out 71 | } 72 | 73 | fn write_component_body(raw: String) -> String { 74 | let mut out = String::from("fn component(cx: Scope) -> Element {\n cx.render(rsx! {"); 75 | indent_and_write(&raw, 1, &mut out); 76 | out.push_str(" })\n}"); 77 | out 78 | } 79 | 80 | fn write_svg_section(out: &mut String, svgs: Vec) { 81 | out.push_str("\n\nmod icons {"); 82 | out.push_str("\n use super::*;"); 83 | for (idx, icon) in svgs.into_iter().enumerate() { 84 | let raw = dioxus_autofmt::write_block_out(CallBody { roots: vec![icon] }).unwrap(); 85 | out.push_str("\n\n pub fn icon_"); 86 | out.push_str(&idx.to_string()); 87 | out.push_str("(cx: Scope) -> Element {\n cx.render(rsx! {"); 88 | indent_and_write(&raw, 2, out); 89 | out.push_str(" })\n }"); 90 | } 91 | 92 | out.push_str("\n}"); 93 | } 94 | 95 | fn indent_and_write(raw: &str, idx: usize, out: &mut String) { 96 | for line in raw.lines() { 97 | for _ in 0..idx { 98 | out.push_str(" "); 99 | } 100 | out.push_str(line); 101 | out.push('\n'); 102 | } 103 | } 104 | 105 | fn determine_input(file: Option, raw: Option) -> Result { 106 | // Make sure not both are specified 107 | if file.is_some() && raw.is_some() { 108 | log::error!("Only one of --file or --raw should be specified."); 109 | exit(0); 110 | } 111 | 112 | if let Some(raw) = raw { 113 | return Ok(raw); 114 | } 115 | 116 | if let Some(file) = file { 117 | return Ok(std::fs::read_to_string(&file)?); 118 | } 119 | 120 | // If neither exist, we try to read from stdin 121 | if atty::is(atty::Stream::Stdin) { 122 | return custom_error!("No input file, source, or stdin to translate from."); 123 | } 124 | 125 | let mut buffer = String::new(); 126 | std::io::stdin().read_to_string(&mut buffer).unwrap(); 127 | 128 | Ok(buffer.trim().to_string()) 129 | } 130 | 131 | #[test] 132 | fn generates_svgs() { 133 | let st = include_str!("../../../tests/svg.html"); 134 | 135 | let out = convert_html_to_formatted_rsx(&html_parser::Dom::parse(st).unwrap(), true); 136 | 137 | println!("{}", out); 138 | } 139 | -------------------------------------------------------------------------------- /src/plugin/types.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use mlua::ToLua; 4 | 5 | #[derive(Debug, Clone)] 6 | pub struct PluginConfig { 7 | pub available: bool, 8 | pub loader: Vec, 9 | pub config_info: HashMap>, 10 | } 11 | 12 | impl<'lua> ToLua<'lua> for PluginConfig { 13 | fn to_lua(self, lua: &'lua mlua::Lua) -> mlua::Result> { 14 | let table = lua.create_table()?; 15 | 16 | table.set("available", self.available)?; 17 | table.set("loader", self.loader)?; 18 | 19 | let config_info = lua.create_table()?; 20 | 21 | for (name, data) in self.config_info { 22 | config_info.set(name, data)?; 23 | } 24 | 25 | table.set("config_info", config_info)?; 26 | 27 | Ok(mlua::Value::Table(table)) 28 | } 29 | } 30 | 31 | impl PluginConfig { 32 | pub fn from_toml_value(val: toml::Value) -> Self { 33 | if let toml::Value::Table(tab) = val { 34 | let available = tab 35 | .get::<_>("available") 36 | .unwrap_or(&toml::Value::Boolean(true)); 37 | let available = available.as_bool().unwrap_or(true); 38 | 39 | let mut loader = vec![]; 40 | if let Some(origin) = tab.get("loader") { 41 | if origin.is_array() { 42 | for i in origin.as_array().unwrap() { 43 | loader.push(i.as_str().unwrap_or_default().to_string()); 44 | } 45 | } 46 | } 47 | 48 | let mut config_info = HashMap::new(); 49 | 50 | for (name, value) in tab { 51 | if name == "available" || name == "loader" { 52 | continue; 53 | } 54 | if let toml::Value::Table(value) = value { 55 | let mut map = HashMap::new(); 56 | for (item, info) in value { 57 | map.insert(item, Value::from_toml(info)); 58 | } 59 | config_info.insert(name, map); 60 | } 61 | } 62 | 63 | Self { 64 | available, 65 | loader, 66 | config_info, 67 | } 68 | } else { 69 | Self { 70 | available: false, 71 | loader: vec![], 72 | config_info: HashMap::new(), 73 | } 74 | } 75 | } 76 | } 77 | 78 | #[allow(dead_code)] 79 | #[derive(Debug, Clone)] 80 | pub enum Value { 81 | String(String), 82 | Integer(i64), 83 | Float(f64), 84 | Boolean(bool), 85 | Array(Vec), 86 | Table(HashMap), 87 | } 88 | 89 | impl Value { 90 | pub fn from_toml(origin: toml::Value) -> Self { 91 | match origin { 92 | cargo_toml::Value::String(s) => Value::String(s), 93 | cargo_toml::Value::Integer(i) => Value::Integer(i), 94 | cargo_toml::Value::Float(f) => Value::Float(f), 95 | cargo_toml::Value::Boolean(b) => Value::Boolean(b), 96 | cargo_toml::Value::Datetime(d) => Value::String(d.to_string()), 97 | cargo_toml::Value::Array(a) => { 98 | let mut v = vec![]; 99 | for i in a { 100 | v.push(Value::from_toml(i)); 101 | } 102 | Value::Array(v) 103 | } 104 | cargo_toml::Value::Table(t) => { 105 | let mut h = HashMap::new(); 106 | for (n, v) in t { 107 | h.insert(n, Value::from_toml(v)); 108 | } 109 | Value::Table(h) 110 | } 111 | } 112 | } 113 | } 114 | 115 | impl<'lua> ToLua<'lua> for Value { 116 | fn to_lua(self, lua: &'lua mlua::Lua) -> mlua::Result> { 117 | Ok(match self { 118 | Value::String(s) => mlua::Value::String(lua.create_string(&s)?), 119 | Value::Integer(i) => mlua::Value::Integer(i), 120 | Value::Float(f) => mlua::Value::Number(f), 121 | Value::Boolean(b) => mlua::Value::Boolean(b), 122 | Value::Array(a) => { 123 | let table = lua.create_table()?; 124 | for (i, v) in a.iter().enumerate() { 125 | table.set(i, v.clone())?; 126 | } 127 | mlua::Value::Table(table) 128 | } 129 | Value::Table(t) => { 130 | let table = lua.create_table()?; 131 | for (i, v) in t.iter() { 132 | table.set(i.clone(), v.clone())?; 133 | } 134 | mlua::Value::Table(table) 135 | } 136 | }) 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/cli/autoformat/mod.rs: -------------------------------------------------------------------------------- 1 | use futures::{stream::FuturesUnordered, StreamExt}; 2 | use std::process::exit; 3 | 4 | use super::*; 5 | 6 | // For reference, the rustfmt main.rs file 7 | // https://github.com/rust-lang/rustfmt/blob/master/src/bin/main.rs 8 | 9 | /// Build the Rust WASM app and all of its assets. 10 | #[derive(Clone, Debug, Parser)] 11 | pub struct Autoformat { 12 | /// Run in 'check' mode. Exits with 0 if input is formatted correctly. Exits 13 | /// with 1 and prints a diff if formatting is required. 14 | #[clap(short, long)] 15 | pub check: bool, 16 | 17 | /// Input rsx (selection) 18 | #[clap(short, long)] 19 | pub raw: Option, 20 | 21 | /// Input file 22 | #[clap(short, long)] 23 | pub file: Option, 24 | } 25 | 26 | impl Autoformat { 27 | // Todo: autoformat the entire crate 28 | pub async fn autoformat(self) -> Result<()> { 29 | // Default to formatting the project 30 | if self.raw.is_none() && self.file.is_none() { 31 | if let Err(e) = autoformat_project(self.check).await { 32 | eprintln!("error formatting project: {}", e); 33 | exit(1); 34 | } 35 | } 36 | 37 | if let Some(raw) = self.raw { 38 | if let Some(inner) = dioxus_autofmt::fmt_block(&raw, 0) { 39 | println!("{}", inner); 40 | } else { 41 | // exit process with error 42 | eprintln!("error formatting codeblock"); 43 | exit(1); 44 | } 45 | } 46 | 47 | if let Some(file) = self.file { 48 | let edits = dioxus_autofmt::fmt_file(&file); 49 | let as_json = serde_json::to_string(&edits).unwrap(); 50 | println!("{}", as_json); 51 | } 52 | 53 | Ok(()) 54 | } 55 | } 56 | 57 | /// Read every .rs file accessible when considering the .gitignore and try to format it 58 | /// 59 | /// Runs using Tokio for multithreading, so it should be really really fast 60 | /// 61 | /// Doesn't do mod-descending, so it will still try to format unreachable files. TODO. 62 | async fn autoformat_project(check: bool) -> Result<()> { 63 | let crate_config = crate::CrateConfig::new()?; 64 | 65 | let mut files_to_format = vec![]; 66 | collect_rs_files(&crate_config.crate_dir, &mut files_to_format); 67 | 68 | let counts = files_to_format 69 | .into_iter() 70 | .filter(|file| { 71 | if file.components().any(|f| f.as_os_str() == "target") { 72 | return false; 73 | } 74 | 75 | true 76 | }) 77 | .map(|path| async { 78 | let _path = path.clone(); 79 | let res = tokio::spawn(async move { 80 | let contents = tokio::fs::read_to_string(&path).await?; 81 | 82 | let edits = dioxus_autofmt::fmt_file(&contents); 83 | let len = edits.len(); 84 | 85 | if !edits.is_empty() { 86 | let out = dioxus_autofmt::apply_formats(&contents, edits); 87 | tokio::fs::write(&path, out).await?; 88 | } 89 | 90 | Ok(len) as Result 91 | }) 92 | .await; 93 | 94 | if res.is_err() { 95 | eprintln!("error formatting file: {}", _path.display()); 96 | } 97 | 98 | res 99 | }) 100 | .collect::>() 101 | .collect::>() 102 | .await; 103 | 104 | let files_formatted: usize = counts 105 | .into_iter() 106 | .map(|f| match f { 107 | Ok(Ok(res)) => res, 108 | _ => 0, 109 | }) 110 | .sum(); 111 | 112 | if files_formatted > 0 && check { 113 | eprintln!("{} files needed formatting", files_formatted); 114 | exit(1); 115 | } 116 | 117 | Ok(()) 118 | } 119 | 120 | fn collect_rs_files(folder: &PathBuf, files: &mut Vec) { 121 | let Ok(folder) = folder.read_dir() else { return }; 122 | 123 | // load the gitignore 124 | 125 | for entry in folder { 126 | let Ok(entry) = entry else { continue; }; 127 | 128 | let path = entry.path(); 129 | 130 | if path.is_dir() { 131 | collect_rs_files(&path, files); 132 | } 133 | 134 | if let Some(ext) = path.extension() { 135 | if ext == "rs" { 136 | files.push(path); 137 | } 138 | } 139 | } 140 | } 141 | 142 | #[test] 143 | fn spawn_properly() { 144 | let out = Command::new("dioxus") 145 | .args([ 146 | "fmt", 147 | "-f", 148 | r#" 149 | // 150 | 151 | rsx! { 152 | 153 | div {} 154 | } 155 | 156 | // 157 | // 158 | // 159 | 160 | "#, 161 | ]) 162 | .output() 163 | .expect("failed to execute process"); 164 | 165 | dbg!(out); 166 | } 167 | -------------------------------------------------------------------------------- /docs/src/configure.md: -------------------------------------------------------------------------------- 1 | # Configure Project 2 | 3 | This chapter will introduce `Dioxus.toml` and anatomy the config file. 4 | 5 | ## Structure 6 | 7 | We use `toml` to define some info for `dioxus` project. 8 | 9 | ### Application 10 | 11 | 1. ***name*** - project name & title 12 | 2. ***default_platform*** - which platform target for this project. 13 | ``` 14 | # current support: web, desktop 15 | # default: web 16 | default_platform = "web" 17 | ``` 18 | change this to `desktop`, the `dioxus build & serve` will default build desktop app. 19 | 3. ***out_dir*** - which directory to put the output file; use `dioxus build & service`, the output will put at this directory, and the `assets` will be also copy to here. 20 | ``` 21 | out_dir = "dist" 22 | ``` 23 | 4. ***asset_dir*** - which direcotry to put your `static, assets` file, cli will automatic copy all file to `out_dir`, so you can put some resource file in there, like `CSS, JS, Image` file. 24 | ``` 25 | asset_dir = "public" 26 | ``` 27 | 28 | ### Application.Tools 29 | 30 | You can combine different tools with `dioxus`. 31 | 32 | 1. ***binaryen*** - Use the `binaryen` tooling suite. 33 | ``` 34 | # current support: wasm-opt 35 | # default: web 36 | binaryen = { wasm_opt = true } 37 | ``` 38 | Use the `wasm_opt = true` key/pair value to activate optimization with wasm-opt. 39 | When building on `release` profile, Dioxus will run `wasm_opt` with `-Oz` option. 40 | 2. ***tailwindcss*** - Use the `tailwindcss` standalone binary to generate a Tailwind CSS bundle file. 41 | ``` 42 | tailwindcss = { input = "main.css", config = "tailwind.config.js" } 43 | ``` 44 | You can set two optional keys : 45 | - input: path of the input CSS file (default value is "public/tailwind.css") 46 | - config: path to the config file for Tailwind (default value is "src/tailwind.config.js") 47 | 48 | When building on `release` profile, Dioxus will run `tailwindcss` with the `--minify` option. 49 | 50 | Note : Dioxus will automatically include the generated tailwind file in the `index.html` 51 | 52 | ### Web.App 53 | 54 | Web platform application config: 55 | 56 | 1. ***title*** - this value will display on the web page title. like `` tag. 57 | ``` 58 | # HTML title tag content 59 | title = "dioxus app | ⛺" 60 | ``` 61 | 62 | ### Web.Watcher 63 | 64 | Web platform `dev-server` watcher config: 65 | 66 | 1. ***reload_html*** - a boolean value; when watcher trigger, regenerate `index.html` file. 67 | ``` 68 | # when watcher trigger, regenerate the `index.html` 69 | reload_html = true 70 | ``` 71 | 2. ***watch_path*** - which files & directories will be watcher monitoring. 72 | ``` 73 | watch_path = ["src", "public"] 74 | ``` 75 | 76 | ### Web.Resource 77 | 78 | Include some `CSS Javascript` resources into html file. 79 | 80 | 1. ***style*** - include some style(CSS) file into html. 81 | ``` 82 | style = [ 83 | # include from public_dir. 84 | "./assets/style.css", 85 | # or some asset from online cdn. 86 | "https://cdn.jsdelivr.net/npm/bootstrap/dist/css/bootstrap.css" 87 | ] 88 | ``` 89 | 2. ***script*** - include some script(JS) file into html. 90 | ``` 91 | style = [ 92 | # include from public_dir. 93 | "./assets/index.js", 94 | # or some asset from online cdn. 95 | "https://cdn.jsdelivr.net/npm/bootstrap/dist/js/bootstrap.js" 96 | ] 97 | ``` 98 | 99 | ### Web.Resource.Dev 100 | 101 | Only include resources at `Dev` mode. 102 | 103 | 1. ***style*** - include some style(CSS) file into html. 104 | ``` 105 | style = [ 106 | # include from public_dir. 107 | "./assets/style.css", 108 | # or some asset from online cdn. 109 | "https://cdn.jsdelivr.net/npm/bootstrap/dist/css/bootstrap.css" 110 | ] 111 | ``` 112 | 2. ***script*** - include some script(JS) file into html. 113 | ``` 114 | style = [ 115 | # include from public_dir. 116 | "./assets/index.js", 117 | # or some asset from online cdn. 118 | "https://cdn.jsdelivr.net/npm/bootstrap/dist/js/bootstrap.js" 119 | ] 120 | ``` 121 | 122 | ### Web.Proxy 123 | 124 | Proxy requests matching a path to a backend server. 125 | 126 | 1. ***backend*** - the URL to the backend server. 127 | ``` 128 | backend = "http://localhost:8000/api/" 129 | ``` 130 | This will cause any requests made to the dev server with prefix /api/ to be redirected to the backend server at http://localhost:8000. The path and query parameters will be passed on as-is (path rewriting is not currently supported). 131 | 132 | ## Config example 133 | 134 | ```toml 135 | [application] 136 | 137 | # App (Project) Name 138 | name = "{{project-name}}" 139 | 140 | # Dioxus App Default Platform 141 | # desktop, web, mobile, ssr 142 | default_platform = "web" 143 | 144 | # `build` & `serve` dist path 145 | out_dir = "dist" 146 | 147 | # resource (public) file folder 148 | asset_dir = "public" 149 | 150 | [web.app] 151 | 152 | # HTML title tag content 153 | title = "dioxus | ⛺" 154 | 155 | [web.watcher] 156 | 157 | # when watcher trigger, regenerate the `index.html` 158 | reload_html = true 159 | 160 | # which files or dirs will be watcher monitoring 161 | watch_path = ["src", "public"] 162 | 163 | # include `assets` in web platform 164 | [web.resource] 165 | 166 | # CSS style file 167 | style = [] 168 | 169 | # Javascript code file 170 | script = [] 171 | 172 | [web.resource.dev] 173 | 174 | # serve: [dev-server] only 175 | 176 | # CSS style file 177 | style = [] 178 | 179 | # Javascript code file 180 | script = [] 181 | 182 | [[web.proxy]] 183 | backend = "http://localhost:8000/api/" 184 | ``` 185 | -------------------------------------------------------------------------------- /src/server/proxy.rs: -------------------------------------------------------------------------------- 1 | use crate::{Result, WebProxyConfig}; 2 | 3 | use anyhow::Context; 4 | use axum::{http::StatusCode, routing::any, Router}; 5 | use hyper::{Request, Response, Uri}; 6 | 7 | #[derive(Debug, Clone)] 8 | struct ProxyClient { 9 | inner: hyper::Client>, 10 | url: Uri, 11 | } 12 | 13 | impl ProxyClient { 14 | fn new(url: Uri) -> Self { 15 | let https = hyper_rustls::HttpsConnectorBuilder::new() 16 | .with_native_roots() 17 | .https_or_http() 18 | .enable_http1() 19 | .build(); 20 | Self { 21 | inner: hyper::Client::builder().build(https), 22 | url, 23 | } 24 | } 25 | 26 | async fn send( 27 | &self, 28 | mut req: Request, 29 | ) -> Result> { 30 | let mut uri_parts = req.uri().clone().into_parts(); 31 | uri_parts.authority = self.url.authority().cloned(); 32 | uri_parts.scheme = self.url.scheme().cloned(); 33 | *req.uri_mut() = Uri::from_parts(uri_parts).context("Invalid URI parts")?; 34 | self.inner 35 | .request(req) 36 | .await 37 | .map_err(crate::error::Error::ProxyRequestError) 38 | } 39 | } 40 | 41 | /// Add routes to the router handling the specified proxy config. 42 | /// 43 | /// We will proxy requests directed at either: 44 | /// 45 | /// - the exact path of the proxy config's backend URL, e.g. /api 46 | /// - the exact path with a trailing slash, e.g. /api/ 47 | /// - any subpath of the backend URL, e.g. /api/foo/bar 48 | pub fn add_proxy(mut router: Router, proxy: &WebProxyConfig) -> Result { 49 | let url: Uri = proxy.backend.parse()?; 50 | let path = url.path().to_string(); 51 | let client = ProxyClient::new(url); 52 | 53 | // We also match everything after the path using a wildcard matcher. 54 | let wildcard_client = client.clone(); 55 | 56 | router = router.route( 57 | // Always remove trailing /'s so that the exact route 58 | // matches. 59 | path.trim_end_matches('/'), 60 | any(move |req| async move { 61 | client 62 | .send(req) 63 | .await 64 | .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string())) 65 | }), 66 | ); 67 | 68 | // Wildcard match anything else _after_ the backend URL's path. 69 | // Note that we know `path` ends with a trailing `/` in this branch, 70 | // so `wildcard` will look like `http://localhost/api/*proxywildcard`. 71 | let wildcard = format!("{}/*proxywildcard", path.trim_end_matches('/')); 72 | router = router.route( 73 | &wildcard, 74 | any(move |req| async move { 75 | wildcard_client 76 | .send(req) 77 | .await 78 | .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string())) 79 | }), 80 | ); 81 | Ok(router) 82 | } 83 | 84 | #[cfg(test)] 85 | mod test { 86 | 87 | use super::*; 88 | 89 | use axum::{extract::Path, Router}; 90 | 91 | fn setup_servers( 92 | mut config: WebProxyConfig, 93 | ) -> ( 94 | tokio::task::JoinHandle<()>, 95 | tokio::task::JoinHandle<()>, 96 | String, 97 | ) { 98 | let backend_router = Router::new().route( 99 | "/*path", 100 | any(|path: Path| async move { format!("backend: {}", path.0) }), 101 | ); 102 | let backend_server = axum::Server::bind(&"127.0.0.1:0".parse().unwrap()) 103 | .serve(backend_router.into_make_service()); 104 | let backend_addr = backend_server.local_addr(); 105 | let backend_handle = tokio::spawn(async move { backend_server.await.unwrap() }); 106 | config.backend = format!("http://{}{}", backend_addr, config.backend); 107 | let router = super::add_proxy(Router::new(), &config); 108 | let server = axum::Server::bind(&"127.0.0.1:0".parse().unwrap()) 109 | .serve(router.unwrap().into_make_service()); 110 | let server_addr = server.local_addr(); 111 | let server_handle = tokio::spawn(async move { server.await.unwrap() }); 112 | (backend_handle, server_handle, server_addr.to_string()) 113 | } 114 | 115 | async fn test_proxy_requests(path: String) { 116 | let config = WebProxyConfig { 117 | // Normally this would be an absolute URL including scheme/host/port, 118 | // but in these tests we need to let the OS choose the port so tests 119 | // don't conflict, so we'll concatenate the final address and this 120 | // path together. 121 | // So in day to day usage, use `http://localhost:8000/api` instead! 122 | backend: path, 123 | }; 124 | let (backend_handle, server_handle, server_addr) = setup_servers(config); 125 | let resp = hyper::Client::new() 126 | .get(format!("http://{}/api", server_addr).parse().unwrap()) 127 | .await 128 | .unwrap(); 129 | assert_eq!(resp.status(), StatusCode::OK); 130 | assert_eq!( 131 | hyper::body::to_bytes(resp.into_body()).await.unwrap(), 132 | "backend: /api" 133 | ); 134 | 135 | let resp = hyper::Client::new() 136 | .get(format!("http://{}/api/", server_addr).parse().unwrap()) 137 | .await 138 | .unwrap(); 139 | assert_eq!(resp.status(), StatusCode::OK); 140 | assert_eq!( 141 | hyper::body::to_bytes(resp.into_body()).await.unwrap(), 142 | "backend: /api/" 143 | ); 144 | 145 | let resp = hyper::Client::new() 146 | .get( 147 | format!("http://{}/api/subpath", server_addr) 148 | .parse() 149 | .unwrap(), 150 | ) 151 | .await 152 | .unwrap(); 153 | assert_eq!(resp.status(), StatusCode::OK); 154 | assert_eq!( 155 | hyper::body::to_bytes(resp.into_body()).await.unwrap(), 156 | "backend: /api/subpath" 157 | ); 158 | backend_handle.abort(); 159 | server_handle.abort(); 160 | } 161 | 162 | #[tokio::test] 163 | async fn add_proxy() { 164 | test_proxy_requests("/api".to_string()).await; 165 | } 166 | 167 | #[tokio::test] 168 | async fn add_proxy_trailing_slash() { 169 | test_proxy_requests("/api/".to_string()).await; 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /src/plugin/interface/mod.rs: -------------------------------------------------------------------------------- 1 | use mlua::{FromLua, Function, ToLua}; 2 | 3 | pub mod command; 4 | pub mod dirs; 5 | pub mod fs; 6 | pub mod log; 7 | pub mod network; 8 | pub mod os; 9 | pub mod path; 10 | 11 | #[derive(Debug, Clone)] 12 | pub struct PluginInfo<'lua> { 13 | pub name: String, 14 | pub repository: String, 15 | pub author: String, 16 | pub version: String, 17 | 18 | pub inner: PluginInner, 19 | 20 | pub on_init: Option>, 21 | pub build: PluginBuildInfo<'lua>, 22 | pub serve: PluginServeInfo<'lua>, 23 | } 24 | 25 | impl<'lua> FromLua<'lua> for PluginInfo<'lua> { 26 | fn from_lua(lua_value: mlua::Value<'lua>, _lua: &'lua mlua::Lua) -> mlua::Result { 27 | let mut res = Self { 28 | name: String::default(), 29 | repository: String::default(), 30 | author: String::default(), 31 | version: String::from("0.1.0"), 32 | 33 | inner: Default::default(), 34 | 35 | on_init: None, 36 | build: Default::default(), 37 | serve: Default::default(), 38 | }; 39 | if let mlua::Value::Table(tab) = lua_value { 40 | if let Ok(v) = tab.get::<_, String>("name") { 41 | res.name = v; 42 | } 43 | if let Ok(v) = tab.get::<_, String>("repository") { 44 | res.repository = v; 45 | } 46 | if let Ok(v) = tab.get::<_, String>("author") { 47 | res.author = v; 48 | } 49 | if let Ok(v) = tab.get::<_, String>("version") { 50 | res.version = v; 51 | } 52 | 53 | if let Ok(v) = tab.get::<_, PluginInner>("inner") { 54 | res.inner = v; 55 | } 56 | 57 | if let Ok(v) = tab.get::<_, Function>("on_init") { 58 | res.on_init = Some(v); 59 | } 60 | 61 | if let Ok(v) = tab.get::<_, PluginBuildInfo>("build") { 62 | res.build = v; 63 | } 64 | 65 | if let Ok(v) = tab.get::<_, PluginServeInfo>("serve") { 66 | res.serve = v; 67 | } 68 | } 69 | 70 | Ok(res) 71 | } 72 | } 73 | 74 | impl<'lua> ToLua<'lua> for PluginInfo<'lua> { 75 | fn to_lua(self, lua: &'lua mlua::Lua) -> mlua::Result> { 76 | let res = lua.create_table()?; 77 | 78 | res.set("name", self.name.to_string())?; 79 | res.set("repository", self.repository.to_string())?; 80 | res.set("author", self.author.to_string())?; 81 | res.set("version", self.version.to_string())?; 82 | 83 | res.set("inner", self.inner)?; 84 | 85 | if let Some(e) = self.on_init { 86 | res.set("on_init", e)?; 87 | } 88 | res.set("build", self.build)?; 89 | res.set("serve", self.serve)?; 90 | 91 | Ok(mlua::Value::Table(res)) 92 | } 93 | } 94 | 95 | #[derive(Debug, Clone, Default)] 96 | pub struct PluginInner { 97 | pub plugin_dir: String, 98 | pub from_loader: bool, 99 | } 100 | 101 | impl<'lua> FromLua<'lua> for PluginInner { 102 | fn from_lua(lua_value: mlua::Value<'lua>, _lua: &'lua mlua::Lua) -> mlua::Result { 103 | let mut res = Self { 104 | plugin_dir: String::new(), 105 | from_loader: false, 106 | }; 107 | 108 | if let mlua::Value::Table(t) = lua_value { 109 | if let Ok(v) = t.get::<_, String>("plugin_dir") { 110 | res.plugin_dir = v; 111 | } 112 | if let Ok(v) = t.get::<_, bool>("from_loader") { 113 | res.from_loader = v; 114 | } 115 | } 116 | Ok(res) 117 | } 118 | } 119 | 120 | impl<'lua> ToLua<'lua> for PluginInner { 121 | fn to_lua(self, lua: &'lua mlua::Lua) -> mlua::Result> { 122 | let res = lua.create_table()?; 123 | 124 | res.set("plugin_dir", self.plugin_dir)?; 125 | res.set("from_loader", self.from_loader)?; 126 | 127 | Ok(mlua::Value::Table(res)) 128 | } 129 | } 130 | 131 | #[derive(Debug, Clone, Default)] 132 | pub struct PluginBuildInfo<'lua> { 133 | pub on_start: Option>, 134 | pub on_finish: Option>, 135 | } 136 | 137 | impl<'lua> FromLua<'lua> for PluginBuildInfo<'lua> { 138 | fn from_lua(lua_value: mlua::Value<'lua>, _lua: &'lua mlua::Lua) -> mlua::Result { 139 | let mut res = Self { 140 | on_start: None, 141 | on_finish: None, 142 | }; 143 | 144 | if let mlua::Value::Table(t) = lua_value { 145 | if let Ok(v) = t.get::<_, Function>("on_start") { 146 | res.on_start = Some(v); 147 | } 148 | if let Ok(v) = t.get::<_, Function>("on_finish") { 149 | res.on_finish = Some(v); 150 | } 151 | } 152 | 153 | Ok(res) 154 | } 155 | } 156 | 157 | impl<'lua> ToLua<'lua> for PluginBuildInfo<'lua> { 158 | fn to_lua(self, lua: &'lua mlua::Lua) -> mlua::Result> { 159 | let res = lua.create_table()?; 160 | 161 | if let Some(v) = self.on_start { 162 | res.set("on_start", v)?; 163 | } 164 | 165 | if let Some(v) = self.on_finish { 166 | res.set("on_finish", v)?; 167 | } 168 | 169 | Ok(mlua::Value::Table(res)) 170 | } 171 | } 172 | 173 | #[derive(Debug, Clone, Default)] 174 | pub struct PluginServeInfo<'lua> { 175 | pub interval: i32, 176 | 177 | pub on_start: Option>, 178 | pub on_interval: Option>, 179 | pub on_rebuild: Option>, 180 | pub on_shutdown: Option>, 181 | } 182 | 183 | impl<'lua> FromLua<'lua> for PluginServeInfo<'lua> { 184 | fn from_lua(lua_value: mlua::Value<'lua>, _lua: &'lua mlua::Lua) -> mlua::Result { 185 | let mut res = Self::default(); 186 | 187 | if let mlua::Value::Table(tab) = lua_value { 188 | if let Ok(v) = tab.get::<_, i32>("interval") { 189 | res.interval = v; 190 | } 191 | if let Ok(v) = tab.get::<_, Function>("on_start") { 192 | res.on_start = Some(v); 193 | } 194 | if let Ok(v) = tab.get::<_, Function>("on_interval") { 195 | res.on_interval = Some(v); 196 | } 197 | if let Ok(v) = tab.get::<_, Function>("on_rebuild") { 198 | res.on_rebuild = Some(v); 199 | } 200 | if let Ok(v) = tab.get::<_, Function>("on_shutdown") { 201 | res.on_shutdown = Some(v); 202 | } 203 | } 204 | 205 | Ok(res) 206 | } 207 | } 208 | 209 | impl<'lua> ToLua<'lua> for PluginServeInfo<'lua> { 210 | fn to_lua(self, lua: &'lua mlua::Lua) -> mlua::Result> { 211 | let res = lua.create_table()?; 212 | 213 | res.set("interval", self.interval)?; 214 | 215 | if let Some(v) = self.on_start { 216 | res.set("on_start", v)?; 217 | } 218 | 219 | if let Some(v) = self.on_interval { 220 | res.set("on_interval", v)?; 221 | } 222 | 223 | if let Some(v) = self.on_rebuild { 224 | res.set("on_rebuild", v)?; 225 | } 226 | 227 | if let Some(v) = self.on_shutdown { 228 | res.set("on_shutdown", v)?; 229 | } 230 | 231 | Ok(mlua::Value::Table(res)) 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | use crate::error::Result; 2 | use serde::{Deserialize, Serialize}; 3 | use std::{collections::HashMap, path::PathBuf}; 4 | 5 | #[derive(Debug, Clone, Serialize, Deserialize)] 6 | pub struct DioxusConfig { 7 | pub application: ApplicationConfig, 8 | 9 | pub web: WebConfig, 10 | 11 | #[serde(default = "default_plugin")] 12 | pub plugin: toml::Value, 13 | } 14 | 15 | fn default_plugin() -> toml::Value { 16 | toml::Value::Boolean(true) 17 | } 18 | 19 | impl DioxusConfig { 20 | pub fn load() -> crate::error::Result> { 21 | let Ok(crate_dir) = crate::cargo::crate_root() else { return Ok(None); }; 22 | 23 | // we support either `Dioxus.toml` or `Cargo.toml` 24 | let Some(dioxus_conf_file) = acquire_dioxus_toml(crate_dir) else { 25 | return Ok(None); 26 | }; 27 | 28 | toml::from_str::(&std::fs::read_to_string(dioxus_conf_file)?) 29 | .map_err(|_| crate::Error::Unique("Dioxus.toml parse failed".into())) 30 | .map(Some) 31 | } 32 | } 33 | 34 | fn acquire_dioxus_toml(dir: PathBuf) -> Option { 35 | // prefer uppercase 36 | if dir.join("Dioxus.toml").is_file() { 37 | return Some(dir.join("Dioxus.toml")); 38 | } 39 | 40 | // lowercase is fine too 41 | if dir.join("dioxus.toml").is_file() { 42 | return Some(dir.join("Dioxus.toml")); 43 | } 44 | 45 | None 46 | } 47 | 48 | impl Default for DioxusConfig { 49 | fn default() -> Self { 50 | Self { 51 | application: ApplicationConfig { 52 | name: "dioxus".into(), 53 | default_platform: "web".to_string(), 54 | out_dir: Some(PathBuf::from("dist")), 55 | asset_dir: Some(PathBuf::from("public")), 56 | 57 | tools: None, 58 | 59 | sub_package: None, 60 | }, 61 | web: WebConfig { 62 | app: WebAppConfig { 63 | title: Some("dioxus | ⛺".into()), 64 | base_path: None, 65 | }, 66 | proxy: Some(vec![]), 67 | watcher: WebWatcherConfig { 68 | watch_path: Some(vec![PathBuf::from("src")]), 69 | reload_html: Some(false), 70 | index_on_404: Some(true), 71 | }, 72 | resource: WebResourceConfig { 73 | dev: WebDevResourceConfig { 74 | style: Some(vec![]), 75 | script: Some(vec![]), 76 | }, 77 | style: Some(vec![]), 78 | script: Some(vec![]), 79 | }, 80 | }, 81 | plugin: toml::Value::Table(toml::map::Map::new()), 82 | } 83 | } 84 | } 85 | 86 | #[derive(Debug, Clone, Serialize, Deserialize)] 87 | pub struct ApplicationConfig { 88 | pub name: String, 89 | pub default_platform: String, 90 | pub out_dir: Option, 91 | pub asset_dir: Option, 92 | 93 | pub tools: Option>, 94 | 95 | pub sub_package: Option, 96 | } 97 | 98 | #[derive(Debug, Clone, Serialize, Deserialize)] 99 | pub struct WebConfig { 100 | pub app: WebAppConfig, 101 | pub proxy: Option>, 102 | pub watcher: WebWatcherConfig, 103 | pub resource: WebResourceConfig, 104 | } 105 | 106 | #[derive(Debug, Clone, Serialize, Deserialize)] 107 | pub struct WebAppConfig { 108 | pub title: Option, 109 | pub base_path: Option, 110 | } 111 | 112 | #[derive(Debug, Clone, Serialize, Deserialize)] 113 | pub struct WebProxyConfig { 114 | pub backend: String, 115 | } 116 | 117 | #[derive(Debug, Clone, Serialize, Deserialize)] 118 | pub struct WebWatcherConfig { 119 | pub watch_path: Option>, 120 | pub reload_html: Option, 121 | pub index_on_404: Option, 122 | } 123 | 124 | #[derive(Debug, Clone, Serialize, Deserialize)] 125 | pub struct WebResourceConfig { 126 | pub dev: WebDevResourceConfig, 127 | pub style: Option>, 128 | pub script: Option>, 129 | } 130 | 131 | #[derive(Debug, Clone, Serialize, Deserialize)] 132 | pub struct WebDevResourceConfig { 133 | pub style: Option>, 134 | pub script: Option>, 135 | } 136 | 137 | #[derive(Debug, Clone)] 138 | pub struct CrateConfig { 139 | pub out_dir: PathBuf, 140 | pub crate_dir: PathBuf, 141 | pub workspace_dir: PathBuf, 142 | pub target_dir: PathBuf, 143 | pub asset_dir: PathBuf, 144 | pub manifest: cargo_toml::Manifest, 145 | pub executable: ExecutableType, 146 | pub dioxus_config: DioxusConfig, 147 | pub release: bool, 148 | pub hot_reload: bool, 149 | pub cross_origin_policy: bool, 150 | pub verbose: bool, 151 | pub custom_profile: Option, 152 | pub features: Option>, 153 | } 154 | 155 | #[derive(Debug, Clone)] 156 | pub enum ExecutableType { 157 | Binary(String), 158 | Lib(String), 159 | Example(String), 160 | } 161 | 162 | impl CrateConfig { 163 | pub fn new() -> Result { 164 | let dioxus_config = DioxusConfig::load()?.unwrap_or_default(); 165 | 166 | let crate_dir = if let Some(package) = &dioxus_config.application.sub_package { 167 | crate::cargo::crate_root()?.join(package) 168 | } else { 169 | crate::cargo::crate_root()? 170 | }; 171 | let meta = crate::cargo::Metadata::get()?; 172 | let workspace_dir = meta.workspace_root; 173 | let target_dir = meta.target_directory; 174 | 175 | let out_dir = match dioxus_config.application.out_dir { 176 | Some(ref v) => crate_dir.join(v), 177 | None => crate_dir.join("dist"), 178 | }; 179 | 180 | let cargo_def = &crate_dir.join("Cargo.toml"); 181 | 182 | let asset_dir = match dioxus_config.application.asset_dir { 183 | Some(ref v) => crate_dir.join(v), 184 | None => crate_dir.join("public"), 185 | }; 186 | 187 | let manifest = cargo_toml::Manifest::from_path(&cargo_def).unwrap(); 188 | 189 | // We just assume they're using a 'main.rs' 190 | // Anyway, we've already parsed the manifest, so it should be easy to change the type 191 | let output_filename = manifest 192 | .bin 193 | .first() 194 | .or(manifest.lib.as_ref()) 195 | .and_then(|product| product.name.clone()) 196 | .or_else(|| manifest.package.as_ref().map(|pkg| pkg.name.clone())) 197 | .expect("No lib found from cargo metadata"); 198 | let executable = ExecutableType::Binary(output_filename); 199 | 200 | let release = false; 201 | let hot_reload = false; 202 | let verbose = false; 203 | let custom_profile = None; 204 | let features = None; 205 | 206 | Ok(Self { 207 | out_dir, 208 | crate_dir, 209 | workspace_dir, 210 | target_dir, 211 | asset_dir, 212 | manifest, 213 | executable, 214 | release, 215 | dioxus_config, 216 | hot_reload, 217 | cross_origin_policy: false, 218 | custom_profile, 219 | features, 220 | verbose, 221 | }) 222 | } 223 | 224 | pub fn as_example(&mut self, example_name: String) -> &mut Self { 225 | self.executable = ExecutableType::Example(example_name); 226 | self 227 | } 228 | 229 | pub fn with_release(&mut self, release: bool) -> &mut Self { 230 | self.release = release; 231 | self 232 | } 233 | 234 | pub fn with_hot_reload(&mut self, hot_reload: bool) -> &mut Self { 235 | self.hot_reload = hot_reload; 236 | self 237 | } 238 | 239 | pub fn with_cross_origin_policy(&mut self, cross_origin_policy: bool) -> &mut Self { 240 | self.cross_origin_policy = cross_origin_policy; 241 | self 242 | } 243 | 244 | pub fn with_verbose(&mut self, verbose: bool) -> &mut Self { 245 | self.verbose = verbose; 246 | self 247 | } 248 | 249 | pub fn set_profile(&mut self, profile: String) -> &mut Self { 250 | self.custom_profile = Some(profile); 251 | self 252 | } 253 | 254 | pub fn set_features(&mut self, features: Vec) -> &mut Self { 255 | self.features = Some(features); 256 | self 257 | } 258 | } 259 | -------------------------------------------------------------------------------- /extension/src/main.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { spawn } from "child_process"; 3 | import { TextEncoder } from 'util'; 4 | 5 | let serverPath: string = "dioxus"; 6 | 7 | export async function activate(context: vscode.ExtensionContext) { 8 | let somePath = await bootstrap(context); 9 | 10 | if (somePath == undefined) { 11 | await vscode.window.showErrorMessage('Could not find bundled Dioxus-CLI. Please install it manually.'); 12 | return; 13 | } else { 14 | serverPath = somePath; 15 | } 16 | 17 | context.subscriptions.push( 18 | // vscode.commands.registerTextEditorCommand('editor.action.clipboardPasteAction', onPasteHandler), 19 | vscode.commands.registerCommand('extension.htmlToDioxusRsx', translateBlock), 20 | vscode.commands.registerCommand('extension.htmlToDioxusComponent', translateComponent), 21 | vscode.commands.registerCommand('extension.formatRsx', fmtSelection), 22 | vscode.commands.registerCommand('extension.formatRsxDocument', formatRsxDocument), 23 | vscode.workspace.onWillSaveTextDocument(fmtDocumentOnSave) 24 | ); 25 | } 26 | 27 | 28 | function translateComponent() { 29 | translate(true) 30 | } 31 | 32 | function translateBlock() { 33 | translate(false) 34 | } 35 | 36 | function translate(component: boolean) { 37 | const editor = vscode.window.activeTextEditor; 38 | 39 | if (!editor) return; 40 | 41 | const html = editor.document.getText(editor.selection); 42 | if (html.length == 0) { 43 | vscode.window.showWarningMessage("Please select HTML fragment before invoking this command!"); 44 | return; 45 | } 46 | 47 | let params = ["translate"]; 48 | if (component) params.push("--component"); 49 | params.push("--raw", html); 50 | 51 | const child_proc = spawn(serverPath, params); 52 | 53 | let result = ''; 54 | child_proc.stdout?.on('data', data => result += data); 55 | 56 | child_proc.on('close', () => { 57 | if (result.length > 0) editor.edit(editBuilder => editBuilder.replace(editor.selection, result)); 58 | }); 59 | 60 | child_proc.on('error', (err) => { 61 | vscode.window.showWarningMessage(`Errors occurred while translating. Make sure you have the most recent Dioxus-CLI installed! \n${err}`); 62 | }); 63 | } 64 | 65 | function onPasteHandler() { 66 | // check settings to see if we should convert HTML to Rsx 67 | if (vscode.workspace.getConfiguration('dioxus').get('convertOnPaste')) { 68 | convertHtmlToRsxOnPaste(); 69 | } 70 | } 71 | 72 | function convertHtmlToRsxOnPaste() { 73 | const editor = vscode.window.activeTextEditor; 74 | if (!editor) return; 75 | 76 | // get the cursor location 77 | const cursor = editor.selection.active; 78 | 79 | // try to parse the HTML at the cursor location 80 | const html = editor.document.getText(new vscode.Range(cursor, cursor)); 81 | } 82 | 83 | function formatRsxDocument() { 84 | const editor = vscode.window.activeTextEditor; 85 | if (!editor) return; 86 | fmtDocument(editor.document); 87 | } 88 | 89 | function fmtSelection() { 90 | const editor = vscode.window.activeTextEditor; 91 | if (!editor) return; 92 | 93 | const unformatted = editor.document.getText(editor.selection); 94 | 95 | if (unformatted.length == 0) { 96 | vscode.window.showWarningMessage("Please select rsx invoking this command!"); 97 | return; 98 | } 99 | 100 | const fileDir = editor.document.fileName.slice(0, editor.document.fileName.lastIndexOf('\\')); 101 | 102 | const child_proc = spawn(serverPath, ["fmt", "--raw", unformatted.toString()], { 103 | cwd: fileDir ? fileDir : undefined, 104 | }); 105 | let result = ''; 106 | 107 | child_proc.stdout?.on('data', data => result += data); 108 | 109 | child_proc.on('close', () => { 110 | if (result.length > 0) editor.edit(editBuilder => editBuilder.replace(editor.selection, result)); 111 | }); 112 | 113 | child_proc.on('error', (err) => { 114 | vscode.window.showWarningMessage(`Errors occurred while translating. Make sure you have the most recent Dioxus-CLI installed! \n${err}`); 115 | }); 116 | } 117 | 118 | function fmtDocumentOnSave(e: vscode.TextDocumentWillSaveEvent) { 119 | // check the settings to make sure format on save is configured 120 | const dioxusConfig = vscode.workspace.getConfiguration('dioxus', e.document).get('formatOnSave'); 121 | const globalConfig = vscode.workspace.getConfiguration('editor', e.document).get('formatOnSave'); 122 | if ( 123 | (dioxusConfig === 'enabled') || 124 | (dioxusConfig !== 'disabled' && globalConfig) 125 | ) { 126 | fmtDocument(e.document); 127 | } 128 | } 129 | 130 | function fmtDocument(document: vscode.TextDocument) { 131 | try { 132 | if (document.languageId !== "rust" || document.uri.scheme !== "file") { 133 | return; 134 | } 135 | 136 | const [editor,] = vscode.window.visibleTextEditors.filter(editor => editor.document.fileName === document.fileName); 137 | if (!editor) return; // Need an editor to apply text edits. 138 | 139 | const text = document.getText(); 140 | const fileDir = document.fileName.slice(0, document.fileName.lastIndexOf('\\')); 141 | 142 | const child_proc = spawn(serverPath, ["fmt", "--file", text], { 143 | cwd: fileDir ? fileDir : undefined, 144 | }); 145 | 146 | let result = ''; 147 | child_proc.stdout?.on('data', data => result += data); 148 | 149 | type RsxEdit = { 150 | formatted: string, 151 | start: number, 152 | end: number 153 | } 154 | 155 | child_proc.on('close', () => { 156 | if (child_proc.exitCode !== 0) { 157 | vscode.window.showWarningMessage(`Errors occurred while formatting. Make sure you have the most recent Dioxus-CLI installed!\nDioxus-CLI exited with exit code ${child_proc.exitCode}\n\nData from Dioxus-CLI:\n${result}`); 158 | return; 159 | } 160 | if (result.length === 0) return; 161 | 162 | // Used for error message: 163 | const originalResult = result; 164 | try { 165 | // Only parse the last non empty line, to skip log warning messages: 166 | const lines = result.replaceAll('\r\n', '\n').split('\n'); 167 | const nonEmptyLines = lines.filter(line => line.trim().length !== 0); 168 | result = nonEmptyLines[nonEmptyLines.length - 1] ?? ''; 169 | 170 | if (result.length === 0) return; 171 | 172 | const decoded: RsxEdit[] = JSON.parse(result); 173 | if (decoded.length === 0) return; 174 | 175 | // Preform edits at the end of the file 176 | // first (to not change previous text file 177 | // offsets): 178 | decoded.sort((a, b) => b.start - a.start); 179 | 180 | 181 | // Convert from utf8 offsets to utf16 offsets used by VS Code: 182 | 183 | const utf8Text = new TextEncoder().encode(text); 184 | const utf8ToUtf16Pos = (posUtf8: number) => { 185 | // Find the line of the position as well as the utf8 and 186 | // utf16 indexes for the start of that line: 187 | let startOfLineUtf8 = 0; 188 | let lineIndex = 0; 189 | const newLineUtf8 = '\n'.charCodeAt(0); 190 | // eslint-disable-next-line no-constant-condition 191 | while (true) { 192 | const nextLineAt = utf8Text.indexOf(newLineUtf8, startOfLineUtf8); 193 | if (nextLineAt < 0 || posUtf8 <= nextLineAt) break; 194 | startOfLineUtf8 = nextLineAt + 1; 195 | lineIndex++; 196 | } 197 | const lineUtf16 = document.lineAt(lineIndex); 198 | 199 | // Move forward from a synced position in the text until the 200 | // target pos is found: 201 | let currentUtf8 = startOfLineUtf8; 202 | let currentUtf16 = document.offsetAt(lineUtf16.range.start); 203 | 204 | const decodeBuffer = new Uint8Array(10); 205 | const utf8Encoder = new TextEncoder(); 206 | while (currentUtf8 < posUtf8) { 207 | const { written } = utf8Encoder.encodeInto(text.charAt(currentUtf16), decodeBuffer); 208 | currentUtf8 += written; 209 | currentUtf16++; 210 | } 211 | return currentUtf16; 212 | }; 213 | 214 | 215 | type FixedEdit = { 216 | range: vscode.Range, 217 | formatted: string, 218 | }; 219 | 220 | const edits: FixedEdit[] = []; 221 | for (const edit of decoded) { 222 | // Convert from utf8 to utf16: 223 | const range = new vscode.Range( 224 | document.positionAt(utf8ToUtf16Pos(edit.start)), 225 | document.positionAt(utf8ToUtf16Pos(edit.end)) 226 | ); 227 | 228 | if (editor.document.getText(range) !== document.getText(range)) { 229 | // The text that was formatted has changed while we were working. 230 | vscode.window.showWarningMessage(`Dioxus formatting was ignored since the source file changed before the change could be applied.`); 231 | continue; 232 | } 233 | 234 | edits.push({ 235 | range, 236 | formatted: edit.formatted, 237 | }); 238 | } 239 | 240 | 241 | // Apply edits: 242 | editor.edit(editBuilder => { 243 | edits.forEach((edit) => editBuilder.replace(edit.range, edit.formatted)); 244 | }, { 245 | undoStopAfter: false, 246 | undoStopBefore: false 247 | }); 248 | } catch (err) { 249 | vscode.window.showWarningMessage(`Errors occurred while formatting. Make sure you have the most recent Dioxus-CLI installed!\n${err}\n\nData from Dioxus-CLI:\n${originalResult}`); 250 | } 251 | }); 252 | 253 | child_proc.on('error', (err) => { 254 | vscode.window.showWarningMessage(`Errors occurred while formatting. Make sure you have the most recent Dioxus-CLI installed! \n${err}`); 255 | }); 256 | } catch (error) { 257 | vscode.window.showWarningMessage(`Errors occurred while formatting. Make sure you have the most recent Dioxus-CLI installed! \n${error}`); 258 | } 259 | } 260 | 261 | 262 | // I'm using the approach defined in rust-analyzer here 263 | // 264 | // We ship the server as part of the extension, but we need to handle external paths and such 265 | // 266 | // https://github.com/rust-lang/rust-analyzer/blob/fee5555cfabed4b8abbd40983fc4442df4007e49/editors/code/src/main.ts#L270 267 | async function bootstrap(context: vscode.ExtensionContext): Promise { 268 | 269 | const ext = process.platform === "win32" ? ".exe" : ""; 270 | const bundled = vscode.Uri.joinPath(context.extensionUri, "server", `dioxus${ext}`); 271 | const bundledExists = await vscode.workspace.fs.stat(bundled).then( 272 | () => true, 273 | () => false 274 | ); 275 | 276 | // if bunddled doesn't exist, try using a locally-installed version 277 | if (!bundledExists) { 278 | return "dioxus"; 279 | } 280 | 281 | return bundled.fsPath; 282 | } 283 | -------------------------------------------------------------------------------- /src/tools.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fs::{create_dir_all, File}, 3 | io::{ErrorKind, Read, Write}, 4 | path::{Path, PathBuf}, 5 | process::Command, 6 | }; 7 | 8 | use anyhow::Context; 9 | use flate2::read::GzDecoder; 10 | use futures::StreamExt; 11 | use tar::Archive; 12 | use tokio::io::AsyncWriteExt; 13 | 14 | #[derive(Debug, PartialEq, Eq)] 15 | pub enum Tool { 16 | Binaryen, 17 | Sass, 18 | Tailwind, 19 | } 20 | 21 | // pub fn tool_list() -> Vec<&'static str> { 22 | // vec!["binaryen", "sass", "tailwindcss"] 23 | // } 24 | 25 | pub fn app_path() -> PathBuf { 26 | let data_local = dirs::data_local_dir().unwrap(); 27 | let dioxus_dir = data_local.join("dioxus"); 28 | if !dioxus_dir.is_dir() { 29 | create_dir_all(&dioxus_dir).unwrap(); 30 | } 31 | dioxus_dir 32 | } 33 | 34 | pub fn temp_path() -> PathBuf { 35 | let app_path = app_path(); 36 | let temp_path = app_path.join("temp"); 37 | if !temp_path.is_dir() { 38 | create_dir_all(&temp_path).unwrap(); 39 | } 40 | temp_path 41 | } 42 | 43 | pub fn clone_repo(dir: &Path, url: &str) -> anyhow::Result<()> { 44 | let target_dir = dir.parent().unwrap(); 45 | let dir_name = dir.file_name().unwrap(); 46 | 47 | let mut cmd = Command::new("git"); 48 | let cmd = cmd.current_dir(target_dir); 49 | let res = cmd.arg("clone").arg(url).arg(dir_name).output(); 50 | if let Err(err) = res { 51 | if ErrorKind::NotFound == err.kind() { 52 | log::warn!("Git program not found. Hint: Install git or check $PATH."); 53 | return Err(err.into()); 54 | } 55 | } 56 | Ok(()) 57 | } 58 | 59 | pub fn tools_path() -> PathBuf { 60 | let app_path = app_path(); 61 | let temp_path = app_path.join("tools"); 62 | if !temp_path.is_dir() { 63 | create_dir_all(&temp_path).unwrap(); 64 | } 65 | temp_path 66 | } 67 | 68 | #[allow(clippy::should_implement_trait)] 69 | impl Tool { 70 | /// from str to tool enum 71 | pub fn from_str(name: &str) -> Option { 72 | match name { 73 | "binaryen" => Some(Self::Binaryen), 74 | "sass" => Some(Self::Sass), 75 | "tailwindcss" => Some(Self::Tailwind), 76 | _ => None, 77 | } 78 | } 79 | 80 | /// get current tool name str 81 | pub fn name(&self) -> &str { 82 | match self { 83 | Self::Binaryen => "binaryen", 84 | Self::Sass => "sass", 85 | Self::Tailwind => "tailwindcss", 86 | } 87 | } 88 | 89 | /// get tool bin dir path 90 | pub fn bin_path(&self) -> &str { 91 | match self { 92 | Self::Binaryen => "bin", 93 | Self::Sass => ".", 94 | Self::Tailwind => ".", 95 | } 96 | } 97 | 98 | /// get target platform 99 | pub fn target_platform(&self) -> &str { 100 | match self { 101 | Self::Binaryen => { 102 | if cfg!(target_os = "windows") { 103 | "windows" 104 | } else if cfg!(target_os = "macos") { 105 | "macos" 106 | } else if cfg!(target_os = "linux") { 107 | "linux" 108 | } else { 109 | panic!("unsupported platformm"); 110 | } 111 | } 112 | Self::Sass => { 113 | if cfg!(target_os = "windows") { 114 | "windows" 115 | } else if cfg!(target_os = "macos") { 116 | "macos" 117 | } else if cfg!(target_os = "linux") { 118 | "linux" 119 | } else { 120 | panic!("unsupported platformm"); 121 | } 122 | } 123 | Self::Tailwind => { 124 | if cfg!(target_os = "windows") { 125 | "windows" 126 | } else if cfg!(target_os = "macos") { 127 | "macos" 128 | } else if cfg!(target_os = "linux") { 129 | "linux" 130 | } else { 131 | panic!("unsupported platformm"); 132 | } 133 | } 134 | } 135 | } 136 | 137 | /// get tool version 138 | pub fn tool_version(&self) -> &str { 139 | match self { 140 | Self::Binaryen => "version_105", 141 | Self::Sass => "1.51.0", 142 | Self::Tailwind => "v3.1.6", 143 | } 144 | } 145 | 146 | /// get tool package download url 147 | pub fn download_url(&self) -> String { 148 | match self { 149 | Self::Binaryen => { 150 | format!( 151 | "https://github.com/WebAssembly/binaryen/releases/download/{version}/binaryen-{version}-x86_64-{target}.tar.gz", 152 | version = self.tool_version(), 153 | target = self.target_platform() 154 | ) 155 | } 156 | Self::Sass => { 157 | format!( 158 | "https://github.com/sass/dart-sass/releases/download/{version}/dart-sass-{version}-{target}-x64.{extension}", 159 | version = self.tool_version(), 160 | target = self.target_platform(), 161 | extension = self.extension() 162 | ) 163 | } 164 | Self::Tailwind => { 165 | let windows_extension = match self.target_platform() { 166 | "windows" => ".exe", 167 | _ => "", 168 | }; 169 | format!( 170 | "https://github.com/tailwindlabs/tailwindcss/releases/download/{version}/tailwindcss-{target}-x64{optional_ext}", 171 | version = self.tool_version(), 172 | target = self.target_platform(), 173 | optional_ext = windows_extension 174 | ) 175 | } 176 | } 177 | } 178 | 179 | /// get package extension name 180 | pub fn extension(&self) -> &str { 181 | match self { 182 | Self::Binaryen => "tar.gz", 183 | Self::Sass => { 184 | if cfg!(target_os = "windows") { 185 | "zip" 186 | } else { 187 | "tar.gz" 188 | } 189 | } 190 | Self::Tailwind => "bin", 191 | } 192 | } 193 | 194 | /// check tool state 195 | pub fn is_installed(&self) -> bool { 196 | tools_path().join(self.name()).is_dir() 197 | } 198 | 199 | /// get download temp path 200 | pub fn temp_out_path(&self) -> PathBuf { 201 | temp_path().join(format!("{}-tool.tmp", self.name())) 202 | } 203 | 204 | /// start to download package 205 | pub async fn download_package(&self) -> anyhow::Result { 206 | let download_url = self.download_url(); 207 | let temp_out = self.temp_out_path(); 208 | let mut file = tokio::fs::File::create(&temp_out) 209 | .await 210 | .context("failed creating temporary output file")?; 211 | 212 | let resp = reqwest::get(download_url).await.unwrap(); 213 | 214 | let mut res_bytes = resp.bytes_stream(); 215 | while let Some(chunk_res) = res_bytes.next().await { 216 | let chunk = chunk_res.context("error reading chunk from download")?; 217 | let _ = file.write(chunk.as_ref()).await; 218 | } 219 | // log::info!("temp file path: {:?}", temp_out); 220 | Ok(temp_out) 221 | } 222 | 223 | /// start to install package 224 | pub async fn install_package(&self) -> anyhow::Result<()> { 225 | let temp_path = self.temp_out_path(); 226 | let tool_path = tools_path(); 227 | 228 | let dir_name = match self { 229 | Self::Binaryen => format!("binaryen-{}", self.tool_version()), 230 | Self::Sass => "dart-sass".to_string(), 231 | Self::Tailwind => self.name().to_string(), 232 | }; 233 | 234 | if self.extension() == "tar.gz" { 235 | let tar_gz = File::open(temp_path)?; 236 | let tar = GzDecoder::new(tar_gz); 237 | let mut archive = Archive::new(tar); 238 | archive.unpack(&tool_path)?; 239 | std::fs::rename(tool_path.join(dir_name), tool_path.join(self.name()))?; 240 | } else if self.extension() == "zip" { 241 | // decompress the `zip` file 242 | extract_zip(&temp_path, &tool_path)?; 243 | std::fs::rename(tool_path.join(dir_name), tool_path.join(self.name()))?; 244 | } else if self.extension() == "bin" { 245 | let bin_path = match self.target_platform() { 246 | "windows" => tool_path.join(&dir_name).join(self.name()).join(".exe"), 247 | _ => tool_path.join(&dir_name).join(self.name()), 248 | }; 249 | // Manualy creating tool directory because we directly download the binary via Github 250 | std::fs::create_dir(tool_path.join(dir_name))?; 251 | 252 | let mut final_file = std::fs::File::create(&bin_path)?; 253 | let mut temp_file = File::open(&temp_path)?; 254 | let mut content = Vec::new(); 255 | 256 | temp_file.read_to_end(&mut content)?; 257 | final_file.write_all(&content)?; 258 | 259 | if self.target_platform() == "linux" { 260 | // This code does not update permissions idk why 261 | // let mut perms = final_file.metadata()?.permissions(); 262 | // perms.set_mode(0o744); 263 | 264 | // Adding to the binary execution rights with "chmod" 265 | let mut command = Command::new("chmod"); 266 | 267 | let _ = command 268 | .args(vec!["+x", bin_path.to_str().unwrap()]) 269 | .stdout(std::process::Stdio::inherit()) 270 | .stderr(std::process::Stdio::inherit()) 271 | .output()?; 272 | } 273 | 274 | std::fs::remove_file(&temp_path)?; 275 | } 276 | 277 | Ok(()) 278 | } 279 | 280 | pub fn call(&self, command: &str, args: Vec<&str>) -> anyhow::Result> { 281 | let bin_path = tools_path().join(self.name()).join(self.bin_path()); 282 | 283 | let command_file = match self { 284 | Tool::Binaryen => { 285 | if cfg!(target_os = "windows") { 286 | format!("{}.exe", command) 287 | } else { 288 | command.to_string() 289 | } 290 | } 291 | Tool::Sass => { 292 | if cfg!(target_os = "windows") { 293 | format!("{}.bat", command) 294 | } else { 295 | command.to_string() 296 | } 297 | } 298 | Tool::Tailwind => { 299 | if cfg!(target_os = "windows") { 300 | format!("{}.exe", command) 301 | } else { 302 | command.to_string() 303 | } 304 | } 305 | }; 306 | 307 | if !bin_path.join(&command_file).is_file() { 308 | return Err(anyhow::anyhow!("Command file not found.")); 309 | } 310 | 311 | let mut command = Command::new(bin_path.join(&command_file).to_str().unwrap()); 312 | 313 | let output = command 314 | .args(&args[..]) 315 | .stdout(std::process::Stdio::inherit()) 316 | .stderr(std::process::Stdio::inherit()) 317 | .output()?; 318 | Ok(output.stdout) 319 | } 320 | } 321 | 322 | pub fn extract_zip(file: &Path, target: &Path) -> anyhow::Result<()> { 323 | let zip_file = std::fs::File::open(&file)?; 324 | let mut zip = zip::ZipArchive::new(zip_file)?; 325 | 326 | if !target.exists() { 327 | let _ = std::fs::create_dir_all(target)?; 328 | } 329 | 330 | for i in 0..zip.len() { 331 | let mut file = zip.by_index(i)?; 332 | if file.is_dir() { 333 | // dir 334 | let target = target.join(Path::new(&file.name().replace('\\', ""))); 335 | let _ = std::fs::create_dir_all(target)?; 336 | } else { 337 | // file 338 | let file_path = target.join(Path::new(file.name())); 339 | let mut target_file = if !file_path.exists() { 340 | std::fs::File::create(file_path)? 341 | } else { 342 | std::fs::File::open(file_path)? 343 | }; 344 | let _num = std::io::copy(&mut file, &mut target_file)?; 345 | } 346 | } 347 | 348 | Ok(()) 349 | } 350 | -------------------------------------------------------------------------------- /src/plugin/mod.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | io::{Read, Write}, 3 | path::PathBuf, 4 | sync::Mutex, 5 | }; 6 | 7 | use mlua::{Lua, Table}; 8 | use serde_json::json; 9 | 10 | use crate::{ 11 | tools::{app_path, clone_repo}, 12 | CrateConfig, 13 | }; 14 | 15 | use self::{ 16 | interface::{ 17 | command::PluginCommander, dirs::PluginDirs, fs::PluginFileSystem, log::PluginLogger, 18 | network::PluginNetwork, os::PluginOS, path::PluginPath, PluginInfo, 19 | }, 20 | types::PluginConfig, 21 | }; 22 | 23 | pub mod interface; 24 | mod types; 25 | 26 | lazy_static::lazy_static! { 27 | static ref LUA: Mutex = Mutex::new(Lua::new()); 28 | } 29 | 30 | pub struct PluginManager; 31 | 32 | impl PluginManager { 33 | pub fn init(config: toml::Value) -> anyhow::Result<()> { 34 | let config = PluginConfig::from_toml_value(config); 35 | 36 | if !config.available { 37 | return Ok(()); 38 | } 39 | 40 | let lua = LUA.lock().unwrap(); 41 | 42 | let manager = lua.create_table().unwrap(); 43 | let name_index = lua.create_table().unwrap(); 44 | 45 | let plugin_dir = Self::init_plugin_dir(); 46 | 47 | let api = lua.create_table().unwrap(); 48 | 49 | api.set("log", PluginLogger).unwrap(); 50 | api.set("command", PluginCommander).unwrap(); 51 | api.set("network", PluginNetwork).unwrap(); 52 | api.set("dirs", PluginDirs).unwrap(); 53 | api.set("fs", PluginFileSystem).unwrap(); 54 | api.set("path", PluginPath).unwrap(); 55 | api.set("os", PluginOS).unwrap(); 56 | 57 | lua.globals().set("plugin_lib", api).unwrap(); 58 | lua.globals() 59 | .set("library_dir", plugin_dir.to_str().unwrap()) 60 | .unwrap(); 61 | lua.globals().set("config_info", config.clone())?; 62 | 63 | let mut index: u32 = 1; 64 | let dirs = std::fs::read_dir(&plugin_dir)?; 65 | 66 | let mut path_list = dirs 67 | .filter(|v| v.is_ok()) 68 | .map(|v| (v.unwrap().path(), false)) 69 | .collect::>(); 70 | for i in &config.loader { 71 | let path = PathBuf::from(i); 72 | if !path.is_dir() { 73 | // for loader dir, we need check first, because we need give a error log. 74 | log::error!("Plugin loader: {:?} path is not a exists directory.", path); 75 | } 76 | path_list.push((path, true)); 77 | } 78 | 79 | for entry in path_list { 80 | let plugin_dir = entry.0.to_path_buf(); 81 | 82 | if plugin_dir.is_dir() { 83 | let init_file = plugin_dir.join("init.lua"); 84 | if init_file.is_file() { 85 | let mut file = std::fs::File::open(init_file).unwrap(); 86 | let mut buffer = String::new(); 87 | file.read_to_string(&mut buffer).unwrap(); 88 | 89 | let current_plugin_dir = plugin_dir.to_str().unwrap().to_string(); 90 | let from_loader = entry.1; 91 | 92 | lua.globals() 93 | .set("_temp_plugin_dir", current_plugin_dir.clone())?; 94 | lua.globals().set("_temp_from_loader", from_loader)?; 95 | 96 | let info = lua.load(&buffer).eval::(); 97 | match info { 98 | Ok(mut info) => { 99 | if name_index.contains_key(info.name.clone()).unwrap_or(false) 100 | && !from_loader 101 | { 102 | // found same name plugin, intercept load 103 | log::warn!( 104 | "Plugin {} has been intercepted. [mulit-load]", 105 | info.name 106 | ); 107 | continue; 108 | } 109 | info.inner.plugin_dir = current_plugin_dir; 110 | info.inner.from_loader = from_loader; 111 | 112 | // call `on_init` if file "dcp.json" not exists 113 | let dcp_file = plugin_dir.join("dcp.json"); 114 | if !dcp_file.is_file() { 115 | if let Some(func) = info.clone().on_init { 116 | let result = func.call::<_, bool>(()); 117 | match result { 118 | Ok(true) => { 119 | // plugin init success, create `dcp.json` file. 120 | let mut file = std::fs::File::create(dcp_file).unwrap(); 121 | let value = json!({ 122 | "name": info.name, 123 | "author": info.author, 124 | "repository": info.repository, 125 | "version": info.version, 126 | "generate_time": chrono::Local::now().timestamp(), 127 | }); 128 | let buffer = 129 | serde_json::to_string_pretty(&value).unwrap(); 130 | let buffer = buffer.as_bytes(); 131 | file.write_all(buffer).unwrap(); 132 | 133 | // insert plugin-info into plugin-manager 134 | if let Ok(index) = 135 | name_index.get::<_, u32>(info.name.clone()) 136 | { 137 | let _ = manager.set(index, info.clone()); 138 | } else { 139 | let _ = manager.set(index, info.clone()); 140 | index += 1; 141 | let _ = name_index.set(info.name, index); 142 | } 143 | } 144 | Ok(false) => { 145 | log::warn!( 146 | "Plugin init function result is `false`, init failed." 147 | ); 148 | } 149 | Err(e) => { 150 | log::warn!("Plugin init failed: {e}"); 151 | } 152 | } 153 | } 154 | } else { 155 | if let Ok(index) = name_index.get::<_, u32>(info.name.clone()) { 156 | let _ = manager.set(index, info.clone()); 157 | } else { 158 | let _ = manager.set(index, info.clone()); 159 | index += 1; 160 | let _ = name_index.set(info.name, index); 161 | } 162 | } 163 | } 164 | Err(_e) => { 165 | let dir_name = plugin_dir.file_name().unwrap().to_str().unwrap(); 166 | log::error!("Plugin '{dir_name}' load failed."); 167 | } 168 | } 169 | } 170 | } 171 | } 172 | 173 | lua.globals().set("manager", manager).unwrap(); 174 | 175 | return Ok(()); 176 | } 177 | 178 | pub fn on_build_start(crate_config: &CrateConfig, platform: &str) -> anyhow::Result<()> { 179 | let lua = LUA.lock().unwrap(); 180 | 181 | if !lua.globals().contains_key("manager")? { 182 | return Ok(()); 183 | } 184 | let manager = lua.globals().get::<_, Table>("manager")?; 185 | 186 | let args = lua.create_table()?; 187 | args.set("name", crate_config.dioxus_config.application.name.clone())?; 188 | args.set("platform", platform)?; 189 | args.set("out_dir", crate_config.out_dir.to_str().unwrap())?; 190 | args.set("asset_dir", crate_config.asset_dir.to_str().unwrap())?; 191 | 192 | for i in 1..(manager.len()? as i32 + 1) { 193 | let info = manager.get::(i)?; 194 | if let Some(func) = info.build.on_start { 195 | func.call::(args.clone())?; 196 | } 197 | } 198 | 199 | Ok(()) 200 | } 201 | 202 | pub fn on_build_finish(crate_config: &CrateConfig, platform: &str) -> anyhow::Result<()> { 203 | let lua = LUA.lock().unwrap(); 204 | 205 | if !lua.globals().contains_key("manager")? { 206 | return Ok(()); 207 | } 208 | let manager = lua.globals().get::<_, Table>("manager")?; 209 | 210 | let args = lua.create_table()?; 211 | args.set("name", crate_config.dioxus_config.application.name.clone())?; 212 | args.set("platform", platform)?; 213 | args.set("out_dir", crate_config.out_dir.to_str().unwrap())?; 214 | args.set("asset_dir", crate_config.asset_dir.to_str().unwrap())?; 215 | 216 | for i in 1..(manager.len()? as i32 + 1) { 217 | let info = manager.get::(i)?; 218 | if let Some(func) = info.build.on_finish { 219 | func.call::(args.clone())?; 220 | } 221 | } 222 | 223 | Ok(()) 224 | } 225 | 226 | pub fn on_serve_start(crate_config: &CrateConfig) -> anyhow::Result<()> { 227 | let lua = LUA.lock().unwrap(); 228 | 229 | if !lua.globals().contains_key("manager")? { 230 | return Ok(()); 231 | } 232 | let manager = lua.globals().get::<_, Table>("manager")?; 233 | 234 | let args = lua.create_table()?; 235 | args.set("name", crate_config.dioxus_config.application.name.clone())?; 236 | 237 | for i in 1..(manager.len()? as i32 + 1) { 238 | let info = manager.get::(i)?; 239 | if let Some(func) = info.serve.on_start { 240 | func.call::(args.clone())?; 241 | } 242 | } 243 | 244 | Ok(()) 245 | } 246 | 247 | pub fn on_serve_rebuild(timestamp: i64, files: Vec) -> anyhow::Result<()> { 248 | let lua = LUA.lock().unwrap(); 249 | 250 | let manager = lua.globals().get::<_, Table>("manager")?; 251 | 252 | let args = lua.create_table()?; 253 | args.set("timestamp", timestamp)?; 254 | let files: Vec = files 255 | .iter() 256 | .map(|v| v.to_str().unwrap().to_string()) 257 | .collect(); 258 | args.set("changed_files", files)?; 259 | 260 | for i in 1..(manager.len()? as i32 + 1) { 261 | let info = manager.get::(i)?; 262 | if let Some(func) = info.serve.on_rebuild { 263 | func.call::(args.clone())?; 264 | } 265 | } 266 | 267 | Ok(()) 268 | } 269 | 270 | pub fn on_serve_shutdown(crate_config: &CrateConfig) -> anyhow::Result<()> { 271 | let lua = LUA.lock().unwrap(); 272 | 273 | if !lua.globals().contains_key("manager")? { 274 | return Ok(()); 275 | } 276 | let manager = lua.globals().get::<_, Table>("manager")?; 277 | 278 | let args = lua.create_table()?; 279 | args.set("name", crate_config.dioxus_config.application.name.clone())?; 280 | 281 | for i in 1..(manager.len()? as i32 + 1) { 282 | let info = manager.get::(i)?; 283 | if let Some(func) = info.serve.on_shutdown { 284 | func.call::(args.clone())?; 285 | } 286 | } 287 | 288 | Ok(()) 289 | } 290 | 291 | pub fn init_plugin_dir() -> PathBuf { 292 | let app_path = app_path(); 293 | let plugin_path = app_path.join("plugins"); 294 | if !plugin_path.is_dir() { 295 | log::info!("📖 Start to init plugin library ..."); 296 | let url = "https://github.com/DioxusLabs/cli-plugin-library"; 297 | if let Err(err) = clone_repo(&plugin_path, url) { 298 | log::error!("Failed to init plugin dir, error caused by {}. ", err); 299 | } 300 | } 301 | plugin_path 302 | } 303 | 304 | pub fn plugin_list() -> Vec { 305 | let mut res = vec![]; 306 | 307 | if let Ok(lua) = LUA.lock() { 308 | let list = lua 309 | .load(mlua::chunk!( 310 | local list = {} 311 | for key, value in ipairs(manager) do 312 | table.insert(list, {name = value.name, loader = value.inner.from_loader}) 313 | end 314 | return list 315 | )) 316 | .eval::>() 317 | .unwrap_or_default(); 318 | for i in list { 319 | let name = i.get::<_, String>("name").unwrap(); 320 | let loader = i.get::<_, bool>("loader").unwrap(); 321 | 322 | let text = if loader { 323 | format!("{name} [:loader]") 324 | } else { 325 | name 326 | }; 327 | res.push(text); 328 | } 329 | } 330 | 331 | res 332 | } 333 | } 334 | -------------------------------------------------------------------------------- /src/builder.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | config::{CrateConfig, ExecutableType}, 3 | error::{Error, Result}, 4 | tools::Tool, 5 | DioxusConfig, 6 | }; 7 | use cargo_metadata::{diagnostic::Diagnostic, Message}; 8 | use indicatif::{ProgressBar, ProgressStyle}; 9 | use serde::Serialize; 10 | use std::{ 11 | fs::{copy, create_dir_all, File}, 12 | io::Read, 13 | panic, 14 | path::PathBuf, 15 | process::Command, 16 | time::Duration, 17 | }; 18 | use wasm_bindgen_cli_support::Bindgen; 19 | 20 | #[derive(Serialize, Debug, Clone)] 21 | pub struct BuildResult { 22 | pub warnings: Vec, 23 | pub elapsed_time: u128, 24 | } 25 | 26 | pub fn build(config: &CrateConfig, quiet: bool) -> Result { 27 | // [1] Build the project with cargo, generating a wasm32-unknown-unknown target (is there a more specific, better target to leverage?) 28 | // [2] Generate the appropriate build folders 29 | // [3] Wasm-bindgen the .wasm fiile, and move it into the {builddir}/modules/xxxx/xxxx_bg.wasm 30 | // [4] Wasm-opt the .wasm file with whatever optimizations need to be done 31 | // [5][OPTIONAL] Builds the Tailwind CSS file using the Tailwind standalone binary 32 | // [6] Link up the html page to the wasm module 33 | 34 | let CrateConfig { 35 | out_dir, 36 | crate_dir, 37 | target_dir, 38 | asset_dir, 39 | executable, 40 | dioxus_config, 41 | .. 42 | } = config; 43 | 44 | // start to build the assets 45 | let ignore_files = build_assets(config)?; 46 | 47 | let t_start = std::time::Instant::now(); 48 | 49 | // [1] Build the .wasm module 50 | log::info!("🚅 Running build command..."); 51 | let cmd = subprocess::Exec::cmd("cargo"); 52 | let cmd = cmd 53 | .cwd(&crate_dir) 54 | .arg("build") 55 | .arg("--target") 56 | .arg("wasm32-unknown-unknown") 57 | .arg("--message-format=json"); 58 | 59 | let cmd = if config.release { 60 | cmd.arg("--release") 61 | } else { 62 | cmd 63 | }; 64 | let cmd = if config.verbose { 65 | cmd.arg("--verbose") 66 | } else { 67 | cmd 68 | }; 69 | 70 | let cmd = if quiet { cmd.arg("--quiet") } else { cmd }; 71 | 72 | let cmd = if config.custom_profile.is_some() { 73 | let custom_profile = config.custom_profile.as_ref().unwrap(); 74 | cmd.arg("--profile").arg(custom_profile) 75 | } else { 76 | cmd 77 | }; 78 | 79 | let cmd = if config.features.is_some() { 80 | let features_str = config.features.as_ref().unwrap().join(" "); 81 | cmd.arg("--features").arg(features_str) 82 | } else { 83 | cmd 84 | }; 85 | 86 | let cmd = match executable { 87 | ExecutableType::Binary(name) => cmd.arg("--bin").arg(name), 88 | ExecutableType::Lib(name) => cmd.arg("--lib").arg(name), 89 | ExecutableType::Example(name) => cmd.arg("--example").arg(name), 90 | }; 91 | 92 | let warning_messages = prettier_build(cmd)?; 93 | 94 | // [2] Establish the output directory structure 95 | let bindgen_outdir = out_dir.join("assets").join("dioxus"); 96 | 97 | let release_type = match config.release { 98 | true => "release", 99 | false => "debug", 100 | }; 101 | 102 | let input_path = match executable { 103 | ExecutableType::Binary(name) | ExecutableType::Lib(name) => target_dir 104 | .join(format!("wasm32-unknown-unknown/{}", release_type)) 105 | .join(format!("{}.wasm", name)), 106 | 107 | ExecutableType::Example(name) => target_dir 108 | .join(format!("wasm32-unknown-unknown/{}/examples", release_type)) 109 | .join(format!("{}.wasm", name)), 110 | }; 111 | 112 | let bindgen_result = panic::catch_unwind(move || { 113 | // [3] Bindgen the final binary for use easy linking 114 | let mut bindgen_builder = Bindgen::new(); 115 | 116 | bindgen_builder 117 | .input_path(input_path) 118 | .web(true) 119 | .unwrap() 120 | .debug(true) 121 | .demangle(true) 122 | .keep_debug(true) 123 | .remove_name_section(false) 124 | .remove_producers_section(false) 125 | .out_name(&dioxus_config.application.name) 126 | .generate(&bindgen_outdir) 127 | .unwrap(); 128 | }); 129 | if bindgen_result.is_err() { 130 | 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())); 131 | } 132 | 133 | // check binaryen:wasm-opt tool 134 | let dioxus_tools = dioxus_config.application.tools.clone().unwrap_or_default(); 135 | if dioxus_tools.contains_key("binaryen") { 136 | let info = dioxus_tools.get("binaryen").unwrap(); 137 | let binaryen = crate::tools::Tool::Binaryen; 138 | 139 | if binaryen.is_installed() { 140 | if let Some(sub) = info.as_table() { 141 | if sub.contains_key("wasm_opt") 142 | && sub.get("wasm_opt").unwrap().as_bool().unwrap_or(false) 143 | { 144 | log::info!("Optimizing WASM size with wasm-opt..."); 145 | let target_file = out_dir 146 | .join("assets") 147 | .join("dioxus") 148 | .join(format!("{}_bg.wasm", dioxus_config.application.name)); 149 | if target_file.is_file() { 150 | let mut args = vec![ 151 | target_file.to_str().unwrap(), 152 | "-o", 153 | target_file.to_str().unwrap(), 154 | ]; 155 | if config.release == true { 156 | args.push("-Oz"); 157 | } 158 | binaryen.call("wasm-opt", args)?; 159 | } 160 | } 161 | } 162 | } else { 163 | log::warn!( 164 | "Binaryen tool not found, you can use `dioxus tool add binaryen` to install it." 165 | ); 166 | } 167 | } 168 | 169 | // [5][OPTIONAL] If tailwind is enabled and installed we run it to generate the CSS 170 | if dioxus_tools.contains_key("tailwindcss") { 171 | let info = dioxus_tools.get("tailwindcss").unwrap(); 172 | let tailwind = crate::tools::Tool::Tailwind; 173 | 174 | if tailwind.is_installed() { 175 | if let Some(sub) = info.as_table() { 176 | log::info!("Building Tailwind bundle CSS file..."); 177 | 178 | let input_path = match sub.get("input") { 179 | Some(val) => val.as_str().unwrap(), 180 | None => "./public", 181 | }; 182 | let config_path = match sub.get("config") { 183 | Some(val) => val.as_str().unwrap(), 184 | None => "./src/tailwind.config.js", 185 | }; 186 | let mut args = vec![ 187 | "-i", 188 | input_path, 189 | "-o", 190 | "dist/tailwind.css", 191 | "-c", 192 | config_path, 193 | ]; 194 | 195 | if config.release == true { 196 | args.push("--minify"); 197 | } 198 | 199 | tailwind.call("tailwindcss", args)?; 200 | } 201 | } else { 202 | log::warn!( 203 | "Tailwind tool not found, you can use `dioxus tool add tailwindcss` to install it." 204 | ); 205 | } 206 | } 207 | 208 | // this code will copy all public file to the output dir 209 | let copy_options = fs_extra::dir::CopyOptions { 210 | overwrite: true, 211 | skip_exist: false, 212 | buffer_size: 64000, 213 | copy_inside: false, 214 | content_only: false, 215 | depth: 0, 216 | }; 217 | if asset_dir.is_dir() { 218 | for entry in std::fs::read_dir(&asset_dir)? { 219 | let path = entry?.path(); 220 | if path.is_file() { 221 | std::fs::copy(&path, out_dir.join(path.file_name().unwrap()))?; 222 | } else { 223 | match fs_extra::dir::copy(&path, out_dir, ©_options) { 224 | Ok(_) => {} 225 | Err(_e) => { 226 | log::warn!("Error copying dir: {}", _e); 227 | } 228 | } 229 | for ignore in &ignore_files { 230 | let ignore = ignore.strip_prefix(&config.asset_dir).unwrap(); 231 | let ignore = config.out_dir.join(ignore); 232 | if ignore.is_file() { 233 | std::fs::remove_file(ignore)?; 234 | } 235 | } 236 | } 237 | } 238 | } 239 | 240 | let t_end = std::time::Instant::now(); 241 | Ok(BuildResult { 242 | warnings: warning_messages, 243 | elapsed_time: (t_end - t_start).as_millis(), 244 | }) 245 | } 246 | 247 | pub fn build_desktop(config: &CrateConfig, _is_serve: bool) -> Result<()> { 248 | log::info!("🚅 Running build [Desktop] command..."); 249 | 250 | let ignore_files = build_assets(config)?; 251 | 252 | let mut cmd = Command::new("cargo"); 253 | cmd.current_dir(&config.crate_dir) 254 | .arg("build") 255 | .stdout(std::process::Stdio::inherit()) 256 | .stderr(std::process::Stdio::inherit()); 257 | 258 | if config.release { 259 | cmd.arg("--release"); 260 | } 261 | if config.verbose { 262 | cmd.arg("--verbose"); 263 | } 264 | 265 | if config.custom_profile.is_some() { 266 | let custom_profile = config.custom_profile.as_ref().unwrap(); 267 | cmd.arg("--profile"); 268 | cmd.arg(custom_profile); 269 | } 270 | 271 | if config.features.is_some() { 272 | let features_str = config.features.as_ref().unwrap().join(" "); 273 | cmd.arg("--features"); 274 | cmd.arg(features_str); 275 | } 276 | 277 | match &config.executable { 278 | crate::ExecutableType::Binary(name) => cmd.arg("--bin").arg(name), 279 | crate::ExecutableType::Lib(name) => cmd.arg("--lib").arg(name), 280 | crate::ExecutableType::Example(name) => cmd.arg("--example").arg(name), 281 | }; 282 | 283 | let output = cmd.output()?; 284 | 285 | if !output.status.success() { 286 | return Err(Error::BuildFailed("Program build failed.".into())); 287 | } 288 | 289 | if output.status.success() { 290 | let release_type = match config.release { 291 | true => "release", 292 | false => "debug", 293 | }; 294 | 295 | let file_name: String; 296 | let mut res_path = match &config.executable { 297 | crate::ExecutableType::Binary(name) | crate::ExecutableType::Lib(name) => { 298 | file_name = name.clone(); 299 | config.target_dir.join(release_type).join(name) 300 | } 301 | crate::ExecutableType::Example(name) => { 302 | file_name = name.clone(); 303 | config 304 | .target_dir 305 | .join(release_type) 306 | .join("examples") 307 | .join(name) 308 | } 309 | }; 310 | 311 | let target_file = if cfg!(windows) { 312 | res_path.set_extension("exe"); 313 | format!("{}.exe", &file_name) 314 | } else { 315 | file_name 316 | }; 317 | 318 | if !config.out_dir.is_dir() { 319 | create_dir_all(&config.out_dir)?; 320 | } 321 | copy(res_path, &config.out_dir.join(target_file))?; 322 | 323 | // this code will copy all public file to the output dir 324 | if config.asset_dir.is_dir() { 325 | let copy_options = fs_extra::dir::CopyOptions { 326 | overwrite: true, 327 | skip_exist: false, 328 | buffer_size: 64000, 329 | copy_inside: false, 330 | content_only: false, 331 | depth: 0, 332 | }; 333 | 334 | for entry in std::fs::read_dir(&config.asset_dir)? { 335 | let path = entry?.path(); 336 | if path.is_file() { 337 | std::fs::copy(&path, &config.out_dir.join(path.file_name().unwrap()))?; 338 | } else { 339 | match fs_extra::dir::copy(&path, &config.out_dir, ©_options) { 340 | Ok(_) => {} 341 | Err(e) => { 342 | log::warn!("Error copying dir: {}", e); 343 | } 344 | } 345 | for ignore in &ignore_files { 346 | let ignore = ignore.strip_prefix(&config.asset_dir).unwrap(); 347 | let ignore = config.out_dir.join(ignore); 348 | if ignore.is_file() { 349 | std::fs::remove_file(ignore)?; 350 | } 351 | } 352 | } 353 | } 354 | } 355 | 356 | log::info!( 357 | "🚩 Build completed: [./{}]", 358 | config 359 | .dioxus_config 360 | .application 361 | .out_dir 362 | .clone() 363 | .unwrap_or_else(|| PathBuf::from("dist")) 364 | .display() 365 | ); 366 | } 367 | 368 | Ok(()) 369 | } 370 | 371 | fn prettier_build(cmd: subprocess::Exec) -> anyhow::Result> { 372 | let mut warning_messages: Vec = vec![]; 373 | 374 | let pb = ProgressBar::new_spinner(); 375 | pb.enable_steady_tick(Duration::from_millis(200)); 376 | pb.set_style( 377 | ProgressStyle::with_template("{spinner:.dim.bold} {wide_msg}") 378 | .unwrap() 379 | .tick_chars("/|\\- "), 380 | ); 381 | pb.set_message("💼 Waiting to start build the project..."); 382 | 383 | let stdout = cmd.stream_stdout()?; 384 | let reader = std::io::BufReader::new(stdout); 385 | for message in cargo_metadata::Message::parse_stream(reader) { 386 | match message.unwrap() { 387 | Message::CompilerMessage(msg) => { 388 | let message = msg.message; 389 | match message.level { 390 | cargo_metadata::diagnostic::DiagnosticLevel::Error => { 391 | return Err(anyhow::anyhow!(message 392 | .rendered 393 | .unwrap_or("Unknown".into()))); 394 | } 395 | cargo_metadata::diagnostic::DiagnosticLevel::Warning => { 396 | warning_messages.push(message.clone()); 397 | } 398 | _ => {} 399 | } 400 | } 401 | Message::CompilerArtifact(artifact) => { 402 | pb.set_message(format!("Compiling {} ", artifact.package_id)); 403 | pb.tick(); 404 | } 405 | Message::BuildScriptExecuted(script) => { 406 | let _package_id = script.package_id.to_string(); 407 | } 408 | Message::BuildFinished(finished) => { 409 | if finished.success { 410 | pb.finish_and_clear(); 411 | log::info!("👑 Build done."); 412 | } else { 413 | std::process::exit(1); 414 | } 415 | } 416 | _ => (), // Unknown message 417 | } 418 | } 419 | Ok(warning_messages) 420 | } 421 | 422 | pub fn gen_page(config: &DioxusConfig, serve: bool) -> String { 423 | let crate_root = crate::cargo::crate_root().unwrap(); 424 | let custom_html_file = crate_root.join("index.html"); 425 | let mut html = if custom_html_file.is_file() { 426 | let mut buf = String::new(); 427 | let mut file = File::open(custom_html_file).unwrap(); 428 | if file.read_to_string(&mut buf).is_ok() { 429 | buf 430 | } else { 431 | String::from(include_str!("./assets/index.html")) 432 | } 433 | } else { 434 | String::from(include_str!("./assets/index.html")) 435 | }; 436 | 437 | let resouces = config.web.resource.clone(); 438 | 439 | let mut style_list = resouces.style.unwrap_or_default(); 440 | let mut script_list = resouces.script.unwrap_or_default(); 441 | 442 | if serve { 443 | let mut dev_style = resouces.dev.style.clone().unwrap_or_default(); 444 | let mut dev_script = resouces.dev.script.unwrap_or_default(); 445 | style_list.append(&mut dev_style); 446 | script_list.append(&mut dev_script); 447 | } 448 | 449 | let mut style_str = String::new(); 450 | for style in style_list { 451 | style_str.push_str(&format!( 452 | "\n", 453 | &style.to_str().unwrap(), 454 | )) 455 | } 456 | if config 457 | .application 458 | .tools 459 | .clone() 460 | .unwrap_or_default() 461 | .contains_key("tailwindcss") 462 | { 463 | style_str.push_str("\n"); 464 | } 465 | html = html.replace("{style_include}", &style_str); 466 | 467 | let mut script_str = String::new(); 468 | for script in script_list { 469 | script_str.push_str(&format!( 470 | "\n", 471 | &script.to_str().unwrap(), 472 | )) 473 | } 474 | 475 | html = html.replace("{script_include}", &script_str); 476 | 477 | if serve { 478 | html += &format!( 479 | "", 480 | include_str!("./assets/autoreload.js") 481 | ); 482 | } 483 | 484 | html = html.replace("{app_name}", &config.application.name); 485 | 486 | html = match &config.web.app.base_path { 487 | Some(path) => html.replace("{base_path}", path), 488 | None => html.replace("{base_path}", "."), 489 | }; 490 | 491 | let title = config 492 | .web 493 | .app 494 | .title 495 | .clone() 496 | .unwrap_or_else(|| "dioxus | ⛺".into()); 497 | 498 | html.replace("{app_title}", &title) 499 | } 500 | 501 | // this function will build some assets file 502 | // like sass tool resources 503 | // this function will return a array which file don't need copy to out_dir. 504 | fn build_assets(config: &CrateConfig) -> Result> { 505 | let mut result = vec![]; 506 | 507 | let dioxus_config = &config.dioxus_config; 508 | let dioxus_tools = dioxus_config.application.tools.clone().unwrap_or_default(); 509 | 510 | // check sass tool state 511 | let sass = Tool::Sass; 512 | if sass.is_installed() && dioxus_tools.contains_key("sass") { 513 | let sass_conf = dioxus_tools.get("sass").unwrap(); 514 | if let Some(tab) = sass_conf.as_table() { 515 | let source_map = tab.contains_key("source_map"); 516 | let source_map = if source_map && tab.get("source_map").unwrap().is_bool() { 517 | if tab.get("source_map").unwrap().as_bool().unwrap_or_default() { 518 | "--source-map" 519 | } else { 520 | "--no-source-map" 521 | } 522 | } else { 523 | "--source-map" 524 | }; 525 | 526 | if tab.contains_key("input") { 527 | if tab.get("input").unwrap().is_str() { 528 | let file = tab.get("input").unwrap().as_str().unwrap().trim(); 529 | 530 | if file == "*" { 531 | // if the sass open auto, we need auto-check the assets dir. 532 | let asset_dir = config.asset_dir.clone(); 533 | if asset_dir.is_dir() { 534 | for entry in walkdir::WalkDir::new(&asset_dir) 535 | .into_iter() 536 | .filter_map(|e| e.ok()) 537 | { 538 | let temp = entry.path(); 539 | if temp.is_file() { 540 | let suffix = temp.extension(); 541 | if suffix.is_none() { 542 | continue; 543 | } 544 | let suffix = suffix.unwrap().to_str().unwrap(); 545 | if suffix == "scss" || suffix == "sass" { 546 | // if file suffix is `scss` / `sass` we need transform it. 547 | let out_file = format!( 548 | "{}.css", 549 | temp.file_stem().unwrap().to_str().unwrap() 550 | ); 551 | let target_path = config 552 | .out_dir 553 | .join( 554 | temp.strip_prefix(&asset_dir) 555 | .unwrap() 556 | .parent() 557 | .unwrap(), 558 | ) 559 | .join(out_file); 560 | let res = sass.call( 561 | "sass", 562 | vec![ 563 | temp.to_str().unwrap(), 564 | target_path.to_str().unwrap(), 565 | source_map, 566 | ], 567 | ); 568 | if res.is_ok() { 569 | result.push(temp.to_path_buf()); 570 | } 571 | } 572 | } 573 | } 574 | } 575 | } else { 576 | // just transform one file. 577 | let relative_path = if &file[0..1] == "/" { 578 | &file[1..file.len()] 579 | } else { 580 | file 581 | }; 582 | let path = config.asset_dir.join(relative_path); 583 | let out_file = 584 | format!("{}.css", path.file_stem().unwrap().to_str().unwrap()); 585 | let target_path = config 586 | .out_dir 587 | .join(PathBuf::from(relative_path).parent().unwrap()) 588 | .join(out_file); 589 | if path.is_file() { 590 | let res = sass.call( 591 | "sass", 592 | vec![ 593 | path.to_str().unwrap(), 594 | target_path.to_str().unwrap(), 595 | source_map, 596 | ], 597 | ); 598 | if res.is_ok() { 599 | result.push(path); 600 | } else { 601 | log::error!("{:?}", res); 602 | } 603 | } 604 | } 605 | } else if tab.get("input").unwrap().is_array() { 606 | // check files list. 607 | let list = tab.get("input").unwrap().as_array().unwrap(); 608 | for i in list { 609 | if i.is_str() { 610 | let path = i.as_str().unwrap(); 611 | let relative_path = if &path[0..1] == "/" { 612 | &path[1..path.len()] 613 | } else { 614 | path 615 | }; 616 | let path = config.asset_dir.join(relative_path); 617 | let out_file = 618 | format!("{}.css", path.file_stem().unwrap().to_str().unwrap()); 619 | let target_path = config 620 | .out_dir 621 | .join(PathBuf::from(relative_path).parent().unwrap()) 622 | .join(out_file); 623 | if path.is_file() { 624 | let res = sass.call( 625 | "sass", 626 | vec![ 627 | path.to_str().unwrap(), 628 | target_path.to_str().unwrap(), 629 | source_map, 630 | ], 631 | ); 632 | if res.is_ok() { 633 | result.push(path); 634 | } 635 | } 636 | } 637 | } 638 | } 639 | } 640 | } 641 | } 642 | // SASS END 643 | 644 | Ok(result) 645 | } 646 | 647 | // use binary_install::{Cache, Download}; 648 | 649 | // /// Attempts to find `wasm-opt` in `PATH` locally, or failing that downloads a 650 | // /// precompiled binary. 651 | // /// 652 | // /// Returns `Some` if a binary was found or it was successfully downloaded. 653 | // /// Returns `None` if a binary wasn't found in `PATH` and this platform doesn't 654 | // /// have precompiled binaries. Returns an error if we failed to download the 655 | // /// binary. 656 | // pub fn find_wasm_opt( 657 | // cache: &Cache, 658 | // install_permitted: bool, 659 | // ) -> Result { 660 | // // First attempt to look up in PATH. If found assume it works. 661 | // if let Ok(path) = which::which("wasm-opt") { 662 | // PBAR.info(&format!("found wasm-opt at {:?}", path)); 663 | 664 | // match path.as_path().parent() { 665 | // Some(path) => return Ok(install::Status::Found(Download::at(path))), 666 | // None => {} 667 | // } 668 | // } 669 | 670 | // let version = "version_78"; 671 | // Ok(install::download_prebuilt( 672 | // &install::Tool::WasmOpt, 673 | // cache, 674 | // version, 675 | // install_permitted, 676 | // )?) 677 | // } 678 | -------------------------------------------------------------------------------- /src/server/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::{builder, plugin::PluginManager, serve::Serve, BuildResult, CrateConfig, Result}; 2 | use axum::{ 3 | body::{Full, HttpBody}, 4 | extract::{ws::Message, Extension, TypedHeader, WebSocketUpgrade}, 5 | http::{ 6 | header::{HeaderName, HeaderValue}, 7 | Method, Response, StatusCode, 8 | }, 9 | response::IntoResponse, 10 | routing::{get, get_service}, 11 | Router, 12 | }; 13 | use cargo_metadata::diagnostic::Diagnostic; 14 | use colored::Colorize; 15 | use dioxus_core::Template; 16 | use dioxus_html::HtmlCtx; 17 | use dioxus_rsx::hot_reload::*; 18 | use notify::{RecommendedWatcher, Watcher}; 19 | use std::{ 20 | net::UdpSocket, 21 | path::PathBuf, 22 | process::Command, 23 | sync::{Arc, Mutex}, 24 | }; 25 | use tokio::sync::broadcast; 26 | use tower::ServiceBuilder; 27 | use tower_http::services::fs::{ServeDir, ServeFileSystemResponseBody}; 28 | use tower_http::{ 29 | cors::{Any, CorsLayer}, 30 | ServiceBuilderExt, 31 | }; 32 | mod proxy; 33 | 34 | pub struct BuildManager { 35 | config: CrateConfig, 36 | reload_tx: broadcast::Sender<()>, 37 | } 38 | 39 | impl BuildManager { 40 | fn rebuild(&self) -> Result { 41 | log::info!("🪁 Rebuild project"); 42 | let result = builder::build(&self.config, true)?; 43 | // change the websocket reload state to true; 44 | // the page will auto-reload. 45 | if self 46 | .config 47 | .dioxus_config 48 | .web 49 | .watcher 50 | .reload_html 51 | .unwrap_or(false) 52 | { 53 | let _ = Serve::regen_dev_page(&self.config); 54 | } 55 | let _ = self.reload_tx.send(()); 56 | Ok(result) 57 | } 58 | } 59 | 60 | struct WsReloadState { 61 | update: broadcast::Sender<()>, 62 | } 63 | 64 | pub async fn startup(port: u16, config: CrateConfig, start_browser: bool) -> Result<()> { 65 | // ctrl-c shutdown checker 66 | let crate_config = config.clone(); 67 | let _ = ctrlc::set_handler(move || { 68 | let _ = PluginManager::on_serve_shutdown(&crate_config); 69 | std::process::exit(0); 70 | }); 71 | 72 | let ip = get_ip().unwrap_or(String::from("0.0.0.0")); 73 | 74 | if config.hot_reload { 75 | startup_hot_reload(ip, port, config, start_browser).await? 76 | } else { 77 | startup_default(ip, port, config, start_browser).await? 78 | } 79 | Ok(()) 80 | } 81 | 82 | pub struct HotReloadState { 83 | pub messages: broadcast::Sender>, 84 | pub build_manager: Arc, 85 | pub file_map: Arc>>, 86 | pub watcher_config: CrateConfig, 87 | } 88 | 89 | pub async fn hot_reload_handler( 90 | ws: WebSocketUpgrade, 91 | _: Option>, 92 | Extension(state): Extension>, 93 | ) -> impl IntoResponse { 94 | ws.on_upgrade(|mut socket| async move { 95 | log::info!("🔥 Hot Reload WebSocket connected"); 96 | { 97 | // update any rsx calls that changed before the websocket connected. 98 | { 99 | log::info!("🔮 Finding updates since last compile..."); 100 | let templates: Vec<_> = { 101 | state 102 | .file_map 103 | .lock() 104 | .unwrap() 105 | .map 106 | .values() 107 | .filter_map(|(_, template_slot)| *template_slot) 108 | .collect() 109 | }; 110 | for template in templates { 111 | if socket 112 | .send(Message::Text(serde_json::to_string(&template).unwrap())) 113 | .await 114 | .is_err() 115 | { 116 | return; 117 | } 118 | } 119 | } 120 | log::info!("finished"); 121 | } 122 | 123 | let mut rx = state.messages.subscribe(); 124 | loop { 125 | if let Ok(rsx) = rx.recv().await { 126 | if socket 127 | .send(Message::Text(serde_json::to_string(&rsx).unwrap())) 128 | .await 129 | .is_err() 130 | { 131 | break; 132 | }; 133 | } 134 | } 135 | }) 136 | } 137 | 138 | #[allow(unused_assignments)] 139 | pub async fn startup_hot_reload( 140 | ip: String, 141 | port: u16, 142 | config: CrateConfig, 143 | start_browser: bool, 144 | ) -> Result<()> { 145 | let first_build_result = crate::builder::build(&config, false)?; 146 | 147 | log::info!("🚀 Starting development server..."); 148 | 149 | PluginManager::on_serve_start(&config)?; 150 | 151 | let dist_path = config.out_dir.clone(); 152 | let (reload_tx, _) = broadcast::channel(100); 153 | let map = FileMap::::new(config.crate_dir.clone()); 154 | // for err in errors { 155 | // log::error!("{}", err); 156 | // } 157 | let file_map = Arc::new(Mutex::new(map)); 158 | let build_manager = Arc::new(BuildManager { 159 | config: config.clone(), 160 | reload_tx: reload_tx.clone(), 161 | }); 162 | let hot_reload_tx = broadcast::channel(100).0; 163 | let hot_reload_state = Arc::new(HotReloadState { 164 | messages: hot_reload_tx.clone(), 165 | build_manager: build_manager.clone(), 166 | file_map: file_map.clone(), 167 | watcher_config: config.clone(), 168 | }); 169 | 170 | let crate_dir = config.crate_dir.clone(); 171 | let ws_reload_state = Arc::new(WsReloadState { 172 | update: reload_tx.clone(), 173 | }); 174 | 175 | // file watcher: check file change 176 | let allow_watch_path = config 177 | .dioxus_config 178 | .web 179 | .watcher 180 | .watch_path 181 | .clone() 182 | .unwrap_or_else(|| vec![PathBuf::from("src")]); 183 | 184 | let watcher_config = config.clone(); 185 | let watcher_ip = ip.clone(); 186 | let mut last_update_time = chrono::Local::now().timestamp(); 187 | 188 | let mut watcher = RecommendedWatcher::new( 189 | move |evt: notify::Result| { 190 | let config = watcher_config.clone(); 191 | // Give time for the change to take effect before reading the file 192 | std::thread::sleep(std::time::Duration::from_millis(100)); 193 | if chrono::Local::now().timestamp() > last_update_time { 194 | if let Ok(evt) = evt { 195 | let mut messages: Vec> = Vec::new(); 196 | for path in evt.paths.clone() { 197 | // if this is not a rust file, rebuild the whole project 198 | if path.extension().and_then(|p| p.to_str()) != Some("rs") { 199 | match build_manager.rebuild() { 200 | Ok(res) => { 201 | print_console_info( 202 | &watcher_ip, 203 | port, 204 | &config, 205 | PrettierOptions { 206 | changed: evt.paths, 207 | warnings: res.warnings, 208 | elapsed_time: res.elapsed_time, 209 | }, 210 | ); 211 | } 212 | Err(err) => { 213 | log::error!("{}", err); 214 | } 215 | } 216 | return; 217 | } 218 | // find changes to the rsx in the file 219 | let mut map = file_map.lock().unwrap(); 220 | 221 | match map.update_rsx(&path, &crate_dir) { 222 | UpdateResult::UpdatedRsx(msgs) => { 223 | messages.extend(msgs); 224 | } 225 | UpdateResult::NeedsRebuild => { 226 | match build_manager.rebuild() { 227 | Ok(res) => { 228 | print_console_info( 229 | &watcher_ip, 230 | port, 231 | &config, 232 | PrettierOptions { 233 | changed: evt.paths, 234 | warnings: res.warnings, 235 | elapsed_time: res.elapsed_time, 236 | }, 237 | ); 238 | } 239 | Err(err) => { 240 | log::error!("{}", err); 241 | } 242 | } 243 | return; 244 | } 245 | } 246 | } 247 | for msg in messages { 248 | let _ = hot_reload_tx.send(msg); 249 | } 250 | } 251 | last_update_time = chrono::Local::now().timestamp(); 252 | } 253 | }, 254 | notify::Config::default(), 255 | ) 256 | .unwrap(); 257 | 258 | for sub_path in allow_watch_path { 259 | if let Err(err) = watcher.watch( 260 | &config.crate_dir.join(&sub_path), 261 | notify::RecursiveMode::Recursive, 262 | ) { 263 | log::error!("error watching {sub_path:?}: \n{}", err); 264 | } 265 | } 266 | 267 | // start serve dev-server at 0.0.0.0:8080 268 | print_console_info( 269 | &ip, 270 | port, 271 | &config, 272 | PrettierOptions { 273 | changed: vec![], 274 | warnings: first_build_result.warnings, 275 | elapsed_time: first_build_result.elapsed_time, 276 | }, 277 | ); 278 | 279 | let cors = CorsLayer::new() 280 | // allow `GET` and `POST` when accessing the resource 281 | .allow_methods([Method::GET, Method::POST]) 282 | // allow requests from any origin 283 | .allow_origin(Any) 284 | .allow_headers(Any); 285 | 286 | let (coep, coop) = if config.cross_origin_policy { 287 | ( 288 | HeaderValue::from_static("require-corp"), 289 | HeaderValue::from_static("same-origin"), 290 | ) 291 | } else { 292 | ( 293 | HeaderValue::from_static("unsafe-none"), 294 | HeaderValue::from_static("unsafe-none"), 295 | ) 296 | }; 297 | 298 | let file_service_config = config.clone(); 299 | let file_service = ServiceBuilder::new() 300 | .override_response_header( 301 | HeaderName::from_static("cross-origin-embedder-policy"), 302 | coep, 303 | ) 304 | .override_response_header(HeaderName::from_static("cross-origin-opener-policy"), coop) 305 | .and_then( 306 | move |response: Response| async move { 307 | let response = if file_service_config 308 | .dioxus_config 309 | .web 310 | .watcher 311 | .index_on_404 312 | .unwrap_or(false) 313 | && response.status() == StatusCode::NOT_FOUND 314 | { 315 | let body = Full::from( 316 | // TODO: Cache/memoize this. 317 | std::fs::read_to_string( 318 | file_service_config 319 | .crate_dir 320 | .join(file_service_config.out_dir) 321 | .join("index.html"), 322 | ) 323 | .ok() 324 | .unwrap(), 325 | ) 326 | .map_err(|err| match err {}) 327 | .boxed(); 328 | Response::builder() 329 | .status(StatusCode::OK) 330 | .body(body) 331 | .unwrap() 332 | } else { 333 | response.map(|body| body.boxed()) 334 | }; 335 | Ok(response) 336 | }, 337 | ) 338 | .service(ServeDir::new(config.crate_dir.join(&dist_path))); 339 | 340 | let mut router = Router::new().route("/_dioxus/ws", get(ws_handler)); 341 | for proxy_config in config.dioxus_config.web.proxy.unwrap_or_default() { 342 | router = proxy::add_proxy(router, &proxy_config)?; 343 | } 344 | router = router.fallback(get_service(file_service).handle_error( 345 | |error: std::io::Error| async move { 346 | ( 347 | StatusCode::INTERNAL_SERVER_ERROR, 348 | format!("Unhandled internal error: {}", error), 349 | ) 350 | }, 351 | )); 352 | 353 | let router = router 354 | .route("/_dioxus/hot_reload", get(hot_reload_handler)) 355 | .layer(cors) 356 | .layer(Extension(ws_reload_state)) 357 | .layer(Extension(hot_reload_state)); 358 | 359 | let addr = format!("0.0.0.0:{}", port).parse().unwrap(); 360 | 361 | let server = axum::Server::bind(&addr).serve(router.into_make_service()); 362 | 363 | if start_browser { 364 | let _ = open::that(format!("http://{}", addr)); 365 | } 366 | 367 | server.await?; 368 | 369 | Ok(()) 370 | } 371 | 372 | pub async fn startup_default( 373 | ip: String, 374 | port: u16, 375 | config: CrateConfig, 376 | start_browser: bool, 377 | ) -> Result<()> { 378 | let first_build_result = crate::builder::build(&config, false)?; 379 | 380 | log::info!("🚀 Starting development server..."); 381 | 382 | let dist_path = config.out_dir.clone(); 383 | 384 | let (reload_tx, _) = broadcast::channel(100); 385 | 386 | let build_manager = BuildManager { 387 | config: config.clone(), 388 | reload_tx: reload_tx.clone(), 389 | }; 390 | 391 | let ws_reload_state = Arc::new(WsReloadState { 392 | update: reload_tx.clone(), 393 | }); 394 | 395 | let mut last_update_time = chrono::Local::now().timestamp(); 396 | 397 | // file watcher: check file change 398 | let allow_watch_path = config 399 | .dioxus_config 400 | .web 401 | .watcher 402 | .watch_path 403 | .clone() 404 | .unwrap_or_else(|| vec![PathBuf::from("src")]); 405 | 406 | let watcher_config = config.clone(); 407 | let watcher_ip = ip.clone(); 408 | let mut watcher = notify::recommended_watcher(move |info: notify::Result| { 409 | let config = watcher_config.clone(); 410 | if let Ok(e) = info { 411 | if chrono::Local::now().timestamp() > last_update_time { 412 | match build_manager.rebuild() { 413 | Ok(res) => { 414 | last_update_time = chrono::Local::now().timestamp(); 415 | print_console_info( 416 | &watcher_ip, 417 | port, 418 | &config, 419 | PrettierOptions { 420 | changed: e.paths.clone(), 421 | warnings: res.warnings, 422 | elapsed_time: res.elapsed_time, 423 | }, 424 | ); 425 | let _ = PluginManager::on_serve_rebuild( 426 | chrono::Local::now().timestamp(), 427 | e.paths, 428 | ); 429 | } 430 | Err(e) => log::error!("{}", e), 431 | } 432 | } 433 | } 434 | }) 435 | .unwrap(); 436 | 437 | for sub_path in allow_watch_path { 438 | watcher 439 | .watch( 440 | &config.crate_dir.join(sub_path), 441 | notify::RecursiveMode::Recursive, 442 | ) 443 | .unwrap(); 444 | } 445 | 446 | // start serve dev-server at 0.0.0.0 447 | print_console_info( 448 | &ip, 449 | port, 450 | &config, 451 | PrettierOptions { 452 | changed: vec![], 453 | warnings: first_build_result.warnings, 454 | elapsed_time: first_build_result.elapsed_time, 455 | }, 456 | ); 457 | 458 | PluginManager::on_serve_start(&config)?; 459 | 460 | let cors = CorsLayer::new() 461 | // allow `GET` and `POST` when accessing the resource 462 | .allow_methods([Method::GET, Method::POST]) 463 | // allow requests from any origin 464 | .allow_origin(Any) 465 | .allow_headers(Any); 466 | 467 | let (coep, coop) = if config.cross_origin_policy { 468 | ( 469 | HeaderValue::from_static("require-corp"), 470 | HeaderValue::from_static("same-origin"), 471 | ) 472 | } else { 473 | ( 474 | HeaderValue::from_static("unsafe-none"), 475 | HeaderValue::from_static("unsafe-none"), 476 | ) 477 | }; 478 | 479 | let file_service_config = config.clone(); 480 | let file_service = ServiceBuilder::new() 481 | .override_response_header( 482 | HeaderName::from_static("cross-origin-embedder-policy"), 483 | coep, 484 | ) 485 | .override_response_header(HeaderName::from_static("cross-origin-opener-policy"), coop) 486 | .and_then( 487 | move |response: Response| async move { 488 | let response = if file_service_config 489 | .dioxus_config 490 | .web 491 | .watcher 492 | .index_on_404 493 | .unwrap_or(false) 494 | && response.status() == StatusCode::NOT_FOUND 495 | { 496 | let body = Full::from( 497 | // TODO: Cache/memoize this. 498 | std::fs::read_to_string( 499 | file_service_config 500 | .crate_dir 501 | .join(file_service_config.out_dir) 502 | .join("index.html"), 503 | ) 504 | .ok() 505 | .unwrap(), 506 | ) 507 | .map_err(|err| match err {}) 508 | .boxed(); 509 | Response::builder() 510 | .status(StatusCode::OK) 511 | .body(body) 512 | .unwrap() 513 | } else { 514 | response.map(|body| body.boxed()) 515 | }; 516 | Ok(response) 517 | }, 518 | ) 519 | .service(ServeDir::new(config.crate_dir.join(&dist_path))); 520 | 521 | let mut router = Router::new().route("/_dioxus/ws", get(ws_handler)); 522 | for proxy_config in config.dioxus_config.web.proxy.unwrap_or_default() { 523 | router = proxy::add_proxy(router, &proxy_config)?; 524 | } 525 | router = router 526 | .fallback( 527 | get_service(file_service).handle_error(|error: std::io::Error| async move { 528 | ( 529 | StatusCode::INTERNAL_SERVER_ERROR, 530 | format!("Unhandled internal error: {}", error), 531 | ) 532 | }), 533 | ) 534 | .layer(cors) 535 | .layer(Extension(ws_reload_state)); 536 | 537 | let addr = format!("0.0.0.0:{}", port).parse().unwrap(); 538 | let server = axum::Server::bind(&addr).serve(router.into_make_service()); 539 | 540 | if start_browser { 541 | let _ = open::that(format!("http://{}", addr)); 542 | } 543 | 544 | server.await?; 545 | 546 | Ok(()) 547 | } 548 | 549 | #[derive(Debug, Default)] 550 | pub struct PrettierOptions { 551 | changed: Vec, 552 | warnings: Vec, 553 | elapsed_time: u128, 554 | } 555 | 556 | fn print_console_info(ip: &String, port: u16, config: &CrateConfig, options: PrettierOptions) { 557 | if let Ok(native_clearseq) = Command::new(if cfg!(target_os = "windows") { 558 | "cls" 559 | } else { 560 | "clear" 561 | }) 562 | .output() 563 | { 564 | print!("{}", String::from_utf8_lossy(&native_clearseq.stdout)); 565 | } else { 566 | // Try ANSI-Escape characters 567 | print!("\x1b[2J\x1b[H"); 568 | } 569 | 570 | // for path in &changed { 571 | // let path = path 572 | // .strip_prefix(crate::crate_root().unwrap()) 573 | // .unwrap() 574 | // .to_path_buf(); 575 | // log::info!("Updated {}", format!("{}", path.to_str().unwrap()).green()); 576 | // } 577 | 578 | let mut profile = if config.release { "Release" } else { "Debug" }.to_string(); 579 | if config.custom_profile.is_some() { 580 | profile = config.custom_profile.as_ref().unwrap().to_string(); 581 | } 582 | let hot_reload = if config.hot_reload { "RSX" } else { "Normal" }; 583 | let crate_root = crate::cargo::crate_root().unwrap(); 584 | let custom_html_file = if crate_root.join("index.html").is_file() { 585 | "Custom [index.html]" 586 | } else { 587 | "Default" 588 | }; 589 | let url_rewrite = if config 590 | .dioxus_config 591 | .web 592 | .watcher 593 | .index_on_404 594 | .unwrap_or(false) 595 | { 596 | "True" 597 | } else { 598 | "False" 599 | }; 600 | 601 | let proxies = config.dioxus_config.web.proxy.as_ref(); 602 | 603 | if options.changed.is_empty() { 604 | println!( 605 | "{} @ v{} [{}] \n", 606 | "Dioxus".bold().green(), 607 | crate::DIOXUS_CLI_VERSION, 608 | chrono::Local::now().format("%H:%M:%S").to_string().dimmed() 609 | ); 610 | } else { 611 | println!( 612 | "Project Reloaded: {}\n", 613 | format!( 614 | "Changed {} files. [{}]", 615 | options.changed.len(), 616 | chrono::Local::now().format("%H:%M:%S").to_string().dimmed() 617 | ) 618 | .purple() 619 | .bold() 620 | ); 621 | } 622 | println!( 623 | "\t> Local : {}", 624 | format!("http://localhost:{}/", port).blue() 625 | ); 626 | println!( 627 | "\t> NetWork : {}", 628 | format!("http://{}:{}/", ip, port).blue() 629 | ); 630 | println!(""); 631 | println!("\t> Profile : {}", profile.green()); 632 | println!("\t> Hot Reload : {}", hot_reload.cyan()); 633 | if let Some(proxies) = proxies { 634 | if !proxies.is_empty() { 635 | println!("\t> Proxies :"); 636 | for proxy in proxies { 637 | println!("\t\t- {}", proxy.backend.blue()); 638 | } 639 | } 640 | } 641 | println!("\t> Index Template : {}", custom_html_file.green()); 642 | println!("\t> URL Rewrite [index_on_404] : {}", url_rewrite.purple()); 643 | println!(""); 644 | println!( 645 | "\t> Build Time Use : {} millis", 646 | options.elapsed_time.to_string().green().bold() 647 | ); 648 | println!(""); 649 | 650 | if options.warnings.len() == 0 { 651 | log::info!("{}\n", "A perfect compilation!".green().bold()); 652 | } else { 653 | log::warn!( 654 | "{}", 655 | format!( 656 | "There were {} warning messages during the build.", 657 | options.warnings.len() - 1 658 | ) 659 | .yellow() 660 | .bold() 661 | ); 662 | // for info in &options.warnings { 663 | // let message = info.message.clone(); 664 | // if message == format!("{} warnings emitted", options.warnings.len() - 1) { 665 | // continue; 666 | // } 667 | // let mut console = String::new(); 668 | // for span in &info.spans { 669 | // let file = &span.file_name; 670 | // let line = (span.line_start, span.line_end); 671 | // let line_str = if line.0 == line.1 { 672 | // line.0.to_string() 673 | // } else { 674 | // format!("{}~{}", line.0, line.1) 675 | // }; 676 | // let code = span.text.clone(); 677 | // let span_info = if code.len() == 1 { 678 | // let code = code.get(0).unwrap().text.trim().blue().bold().to_string(); 679 | // format!( 680 | // "[{}: {}]: '{}' --> {}", 681 | // file, 682 | // line_str, 683 | // code, 684 | // message.yellow().bold() 685 | // ) 686 | // } else { 687 | // let code = code 688 | // .iter() 689 | // .enumerate() 690 | // .map(|(_i, s)| format!("\t{}\n", s.text).blue().bold().to_string()) 691 | // .collect::(); 692 | // format!("[{}: {}]:\n{}\n#:{}", file, line_str, code, message) 693 | // }; 694 | // console = format!("{console}\n\t{span_info}"); 695 | // } 696 | // println!("{console}"); 697 | // } 698 | // println!( 699 | // "\n{}\n", 700 | // "Resolving all warnings will help your code run better!".yellow() 701 | // ); 702 | } 703 | } 704 | 705 | fn get_ip() -> Option { 706 | let socket = match UdpSocket::bind("0.0.0.0:0") { 707 | Ok(s) => s, 708 | Err(_) => return None, 709 | }; 710 | 711 | match socket.connect("8.8.8.8:80") { 712 | Ok(()) => (), 713 | Err(_) => return None, 714 | }; 715 | 716 | match socket.local_addr() { 717 | Ok(addr) => return Some(addr.ip().to_string()), 718 | Err(_) => return None, 719 | }; 720 | } 721 | 722 | async fn ws_handler( 723 | ws: WebSocketUpgrade, 724 | _: Option>, 725 | Extension(state): Extension>, 726 | ) -> impl IntoResponse { 727 | ws.on_upgrade(|mut socket| async move { 728 | let mut rx = state.update.subscribe(); 729 | let reload_watcher = tokio::spawn(async move { 730 | loop { 731 | rx.recv().await.unwrap(); 732 | // ignore the error 733 | if socket 734 | .send(Message::Text(String::from("reload"))) 735 | .await 736 | .is_err() 737 | { 738 | break; 739 | } 740 | 741 | // flush the errors after recompling 742 | rx = rx.resubscribe(); 743 | } 744 | }); 745 | 746 | reload_watcher.await.unwrap(); 747 | }) 748 | } 749 | --------------------------------------------------------------------------------