Merge pull request #1133 from DioxusLabs/jk/add-cli-to-mainline

Add CLI back into Dioxus Mainline
This commit is contained in:
Jon Kelley 2023-06-28 17:24:36 -07:00 committed by GitHub
commit 065cea8a6d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
83 changed files with 16304 additions and 0 deletions

View file

@ -0,0 +1,31 @@
# .github/workflows/build.yml
on:
release:
types: [created]
jobs:
release:
name: release ${{ matrix.target }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- target: x86_64-unknown-linux-gnu
archive: tar.gz tar.xz
- target: x86_64-unknown-linux-musl
archive: tar.gz tar.xz
- target: x86_64-apple-darwin
archive: tar.gz tar.xz
- target: x86_64-pc-windows-gnu
archive: zip
steps:
- uses: actions/checkout@master
- name: Compile and release
uses: rust-build/rust-build.action@latest
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RUSTTARGET: ${{ matrix.target }}
ARCHIVE_TYPES: ${{ matrix.archive }}

34
packages/cli/.github/workflows/docs.yml vendored Normal file
View file

@ -0,0 +1,34 @@
name: github pages
on:
push:
paths:
- docs/**
branches:
- master
jobs:
deploy:
runs-on: ubuntu-20.04
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
steps:
- uses: actions/checkout@v2
- name: Setup mdBook
uses: peaceiris/actions-mdbook@v1
with:
mdbook-version: '0.4.10'
# mdbook-version: 'latest'
- run: cd docs && mdbook build
- name: Deploy 🚀
uses: JamesIves/github-pages-deploy-action@v4.2.3
with:
branch: gh-pages # The branch the action should deploy to.
folder: docs/book # The folder the action should deploy.
target-folder: docs/nightly/cli
repository-name: dioxuslabs/docsite
clean: false
token: ${{ secrets.DEPLOY_KEY }} # let's pretend I don't need it for now

68
packages/cli/.github/workflows/main.yml vendored Normal file
View file

@ -0,0 +1,68 @@
on: [push, pull_request]
name: Rust CI
jobs:
check:
name: Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
- uses: Swatinem/rust-cache@v1
- uses: actions-rs/cargo@v1
with:
command: check
test:
name: Test Suite
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
- uses: Swatinem/rust-cache@v1
- uses: actions-rs/cargo@v1
with:
command: test
fmt:
name: Rustfmt
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
- uses: Swatinem/rust-cache@v1
- run: rustup component add rustfmt
- uses: actions-rs/cargo@v1
with:
command: fmt
args: --all -- --check
# clippy:
# name: Clippy
# runs-on: ubuntu-latest
# steps:
# - uses: actions/checkout@v2
# - uses: actions-rs/toolchain@v1
# with:
# profile: minimal
# toolchain: stable
# override: true
# - uses: Swatinem/rust-cache@v1
# - run: rustup component add clippy
# - uses: actions-rs/cargo@v1
# with:
# command: clippy
# args: -- -D warnings

4
packages/cli/.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
/target
Cargo.lock
.DS_Store
.idea/

6
packages/cli/.vscode/settings.json vendored Normal file
View file

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

4778
packages/cli/Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

92
packages/cli/Cargo.toml Normal file
View file

@ -0,0 +1,92 @@
[package]
name = "dioxus-cli"
version = "0.3.1"
authors = ["Jonathan Kelley"]
edition = "2021"
description = "CLI tool for developing, testing, and publishing Dioxus apps"
license = "MIT/Apache-2.0"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
# cli core
clap = { version = "4.2", features = ["derive"] }
thiserror = "1.0.30"
wasm-bindgen-cli-support = "0.2"
colored = "2.0.0"
# features
log = "0.4.14"
fern = { version = "0.6.0", features = ["colored"] }
serde = { version = "1.0.136", features = ["derive"] }
serde_json = "1.0.79"
toml = "0.5.8"
fs_extra = "1.2.0"
cargo_toml = "0.11.4"
futures = "0.3.21"
notify = { version = "5.0.0-pre.16", features = ["serde"] }
html_parser = "0.6.2"
binary-install = "0.0.2"
convert_case = "0.5.0"
cargo_metadata = "0.15.0"
tokio = { version = "1.16.1", features = ["full"] }
atty = "0.2.14"
regex = "1.5.4"
chrono = "0.4.19"
anyhow = "1.0.53"
hyper = "0.14.17"
hyper-rustls = "0.23.2"
indicatif = "0.17.0-rc.11"
subprocess = "0.2.9"
axum = { version = "0.5.1", features = ["ws", "headers"] }
tower-http = { version = "0.2.2", features = ["full"] }
headers = "0.3.7"
walkdir = "2"
# tools download
dirs = "4.0.0"
reqwest = { version = "0.11", features = [
"rustls-tls",
"stream",
"trust-dns",
"blocking",
] }
flate2 = "1.0.22"
tar = "0.4.38"
zip = "0.6.2"
tower = "0.4.12"
syn = { version = "1.0", features = ["full", "extra-traits"] }
proc-macro2 = { version = "1.0", features = ["span-locations"] }
lazy_static = "1.4.0"
# plugin packages
mlua = { version = "0.8.1", features = [
"lua54",
"vendored",
"async",
"send",
"macros",
] }
ctrlc = "3.2.3"
# dioxus-rsx = "0.0.1"
gitignore = "1.0.7"
dioxus-rsx = { git = "https://github.com/DioxusLabs/dioxus" }
dioxus-html = { git = "https://github.com/DioxusLabs/dioxus", features = ["hot-reload-context"] }
dioxus-core = { git = "https://github.com/DioxusLabs/dioxus", features = ["serialize"] }
dioxus-autofmt = { git = "https://github.com/DioxusLabs/dioxus" }
rsx-rosetta = { git = "https://github.com/DioxusLabs/dioxus" }
open = "4.1.0"
cargo-generate = "0.18.3"
toml_edit = "0.19.11"
[[bin]]
path = "src/main.rs"
name = "dioxus"

45
packages/cli/Dioxus.toml Normal file
View file

@ -0,0 +1,45 @@
[application]
# dioxus project name
name = "dioxus-cli"
# default platfrom
# you can also use `dioxus serve/build --platform XXX` to use other platform
# value: web | desktop
default_platform = "desktop"
# Web `build` & `serve` dist path
out_dir = "dist"
# resource (static) file folder
asset_dir = "public"
[web.app]
# HTML title tag content
title = "dioxus | ⛺"
[web.watcher]
watch_path = ["src"]
# include `assets` in web platform
[web.resource]
# CSS style file
style = []
# Javascript code file
script = []
[web.resource.dev]
# Javascript code file
# serve: [dev-server] only
script = []
[application.tools]
# use binaryen.wasm-opt for output Wasm file
# binaryen just will trigger in `web` platform
binaryen = { wasm_opt = true }

43
packages/cli/README.md Normal file
View file

@ -0,0 +1,43 @@
<div align="center">
<h1>📦✨ Dioxus CLI </h1>
<p><strong>Tooling to supercharge Dioxus projects</strong></p>
</div>
**dioxus-cli** (inspired by wasm-pack and webpack) is a tool for getting Dioxus projects up and running.
It handles all build, bundling, development and publishing to simplify web development.
## Installation
### Install stable version
```
cargo install dioxus-cli
```
### Install from git repository
```
cargo install --git https://github.com/DioxusLabs/cli
```
### Install from local folder
```
cargo install --path . --debug
```
## Get Started
Use `dioxus create project-name` to initialize a new Dioxus project. <br>
It will be cloned from the [dioxus-template](https://github.com/DioxusLabs/dioxus-template) repository.
<br>
Alternatively, you can specify the template path:
```
dioxus create hello --template gh:dioxuslabs/dioxus-template
```
## Dioxus Config File
Dioxus CLI will use `Dioxus.toml` file to Identify some project info and switch some cli feature.
You can get more configure information from [Dioxus CLI Document](https://dioxuslabs.com/cli/configure.html).

50
packages/cli/build.rs Normal file
View file

@ -0,0 +1,50 @@
//! Construct version in the `commit-hash date channel` format
use std::{env, path::PathBuf, process::Command};
fn main() {
set_rerun();
set_commit_info();
if option_env!("CFG_RELEASE").is_none() {
println!("cargo:rustc-env=POKE_RA_DEVS=1");
}
}
fn set_rerun() {
println!("cargo:rerun-if-env-changed=CFG_RELEASE");
let mut manifest_dir = PathBuf::from(
env::var("CARGO_MANIFEST_DIR").expect("`CARGO_MANIFEST_DIR` is always set by cargo."),
);
while manifest_dir.parent().is_some() {
let head_ref = manifest_dir.join(".git/HEAD");
if head_ref.exists() {
println!("cargo:rerun-if-changed={}", head_ref.display());
return;
}
manifest_dir.pop();
}
println!("cargo:warning=Could not find `.git/HEAD` from manifest dir!");
}
fn set_commit_info() {
let output = match Command::new("git")
.arg("log")
.arg("-1")
.arg("--date=short")
.arg("--format=%H %h %cd")
.output()
{
Ok(output) if output.status.success() => output,
_ => return,
};
let stdout = String::from_utf8(output.stdout).unwrap();
let mut parts = stdout.split_whitespace();
let mut next = || parts.next().unwrap();
println!("cargo:rustc-env=RA_COMMIT_HASH={}", next());
println!("cargo:rustc-env=RA_COMMIT_SHORT_HASH={}", next());
println!("cargo:rustc-env=RA_COMMIT_DATE={}", next())
}

1
packages/cli/docs/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
book

View file

@ -0,0 +1,6 @@
[book]
authors = ["YuKun Liu"]
language = "en"
multilingual = false
src = "src"
title = "Dioxus Cli"

View file

@ -0,0 +1,18 @@
# Summary
- [Introduction](./introduction.md)
- [Installation](./installation.md)
- [Create a Project](./creating.md)
- [Configure Project](./configure.md)
- [Commands](./cmd/README.md)
- [Build](./cmd/build.md)
- [Serve](./cmd/serve.md)
- [Clean](./cmd/clean.md)
- [Translate](./cmd/translate.md)
- [Plugin Development](./plugin/README.md)
- [API.Log](./plugin/interface/log.md)
- [API.Command](./plugin/interface/command.md)
- [API.OS](./plugin/interface/os.md)
- [API.Directories](./plugin/interface/dirs.md)
- [API.Network](./plugin/interface/network.md)
- [API.Path](./plugin/interface/path.md)

View file

@ -0,0 +1,26 @@
# Commands
In this chapter we will introduce all `dioxus-cli` commands.
> You can also use `dioxus --help` to get cli help info.
```
dioxus
Build, bundle, & ship your Dioxus app
USAGE:
dioxus [OPTIONS] <SUBCOMMAND>
OPTIONS:
-h, --help Print help information
-v Enable verbose logging
SUBCOMMANDS:
build Build the Dioxus application and all of its assets
clean Clean output artifacts
config Dioxus config file controls
create Init a new project for Dioxus
help Print this message or the help of the given subcommand(s)
serve Build, watch & serve the Rust WASM app and all of its assets
translate Translate some html file into a Dioxus component
```

View file

@ -0,0 +1,47 @@
# Build
The `dioxus build` command can help you `pack & build` a dioxus project.
```
dioxus-build
Build the Rust WASM app and all of its assets
USAGE:
dioxus build [OPTIONS]
OPTIONS:
--example <EXAMPLE> [default: ""]
--platform <PLATFORM> [default: "default_platform"]
--release [default: false]
```
You can use this command to build a project:
```
dioxus build --release
```
## Target platform
Use the `platform` option to choose your target platform:
```
# for desktop project
dioxus build --platform desktop
```
`platform` currently only supports `desktop` & `web`.
```
# for web project
dioxus build --platform web
```
## Build Example
You can use the `example` option to select a example to build:
```
# build the `test` example
dioxus build --exmaple test
```

View file

@ -0,0 +1,18 @@
# Clean
`dioxus clean` will clear the build artifacts (the out_dir and the cargo cache)
```
dioxus-clean
Clean build artifacts
USAGE:
dioxus clean
```
# Example
```
dioxus clean
```

View file

@ -0,0 +1,61 @@
# Serve
The `dioxus serve` can start a dev server with hot-reloading
```
dioxus-serve
Build, watch & serve the Rust WASM app and all of its assets
USAGE:
dioxus serve [OPTIONS]
OPTIONS:
--example <EXAMPLE> [default: ""]
--platform <PLATFORM> [default: "default_platform"]
--release [default: false]
--hot-reload [default: false]ß
```
You can use this command to build project and start a dev server:
```
dioxus serve
```
## Serve Example
You can use the `example` option to serve a example:
```
# serve the `test` example
dioxus serve --exmaple test
```
## Open Browser
You can add the `--open` option to open system default browser when server startup:
```
dioxus serve --open
```
## RSX Hot Reloading
You can add the `--hot-reload` flag to enable [rsx hot reloading](https://dioxuslabs.com/docs/0.3/guide/en/getting_started/hot_reload.html). This will allow you to reload some rsx changes without a full recompile:
```
dioxus serve --open
```
## Cross Origin Policy
You can add the `cross-origin-policy` option to change cross-origin header to:
```
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
```
```
dioxus serve --corss-origin-policy
```

View file

@ -0,0 +1,68 @@
# Translate
`dioxus translate` can translate some `html` file into a Dioxus compoent
```
dioxus-translate
Translate some source file into a Dioxus component
USAGE:
dioxus translate [OPTIONS] [OUTPUT]
ARGS:
<OUTPUT> Output file, defaults to stdout if not present
OPTIONS:
-c, --component Activate debug mode
-f, --file <FILE> Input file
```
## Translate HTML to stdout
You can use the `file` option to set path to the `html` file to translate:
```
dioxus transtale --file ./index.html
```
## Output rsx to a file
You can pass a file to the traslate command to set the path to write the output of the command to:
```
dioxus translate --file ./index.html component.rsx
```
## Output rsx to a file
Setting the `component` option will create a compoent from the HTML:
```
dioxus translate --file ./index.html --component
```
## Example
This HTML:
```html
<div>
<h1> Hello World </h1>
<a href="https://dioxuslabs.com/">Link</a>
</div>
```
Translates into this Dioxus component:
```rust
fn component(cx: Scope) -> Element {
cx.render(rsx! {
div {
h1 { "Hello World" },
a {
href: "https://dioxuslabs.com/",
"Link"
}
}
})
}
```

View file

@ -0,0 +1,208 @@
# Configure Project
This chapter will introduce you to how to configure the CLI with your `Dioxus.toml` file
Be aware that if the config file is present in the folder, some fields must be filled out, or the CLI tool will abort. The mandatory [table headers](https://toml.io/en/v1.0.0#table) and keys will have a '✍' sign beside it.
## Structure
The CLI uses a `Dioxus.toml` file in the root of your crate to define some configuration for your `dioxus` project.
### Application ✍
General application confiration:
```
[application]
# configuration
```
1. ***name*** ✍ - project name & title
2. ***default_platform*** ✍ - which platform target for this project.
```
name = "my-project"
```
2. ***default_platform*** - The platform this project targets
```ß
# current supported platforms: web, desktop
# default: web
default_platform = "web"
```
if you change this to `desktop`, the `dioxus build` will default building a desktop app
3. ***out_dir*** - The directory to place the build artifacts from `dioxus build` or `dioxus service` into. This is also where the `assets` directory will be copied to
```
out_dir = "dist"
```
4. ***asset_dir*** - The directory with your static assets. The CLI will automatically copy these assets into the ***out_dir*** after a build/serve.
```
asset_dir = "public"
```
5. ***sub_package*** - The sub package in the workspace to build by default
```
sub_package = "my-crate"
```
### Web.App ✍
Configeration specific to web applications:
```
[web.app]
# configuration
```
1. ***title*** - The title of the web page
```
# HTML title tag content
title = "dioxus app | ⛺"
```
2. ***base_path*** - The base path to build the appliation for serving at. This can be useful when serving your application in a subdirectory under a domain. For example when building a site to be served on github pages.
```
# The application will be served at domain.com/my_application/, so we need to modify the base_path to the path where the application will be served
base_path = "my_application"
```
### Web.Watcher ✍
Configeration related to the development server:
```
[web.watcher]
# configuration
```
1. ***reload_html*** - If this is true, the cli will rebuild the index.html file every time the application is rebuilt
```
reload_html = true
```
2. ***watch_path*** - The files & directories to moniter for changes
```
watch_path = ["src", "public"]
```
3. ***index_on_404*** - If enabled, Dioxus CLI will serve the root page when a route is not found. *This is needed when serving an application that uses the router*
```
index_on_404 = true
```
### Web.Resource ✍
Configeration related to static resources your application uses:
```
[web.resource]
# configuration
```
1. ***style*** - The styles (`.css` files) to include in your application
```
style = [
# include from public_dir.
"./assets/style.css",
# or some asset from online cdn.
"https://cdn.jsdelivr.net/npm/bootstrap/dist/css/bootstrap.css"
]
```
2. ***script*** - The additional scripts (`.js` files) to include in your application
```
style = [
# include from public_dir.
"./assets/index.js",
# or some asset from online cdn.
"https://cdn.jsdelivr.net/npm/bootstrap/dist/js/bootstrap.js"
]
```
### Web.Resource.Dev ✍
Configeration related to static resources your application uses in development:
```
[web.resource.dev]
# configuration
```
1. ***style*** - The styles (`.css` files) to include in your application
```
style = [
# include from public_dir.
"./assets/style.css",
# or some asset from online cdn.
"https://cdn.jsdelivr.net/npm/bootstrap/dist/css/bootstrap.css"
]
```
2. ***script*** - The additional scripts (`.js` files) to include in your application
```
style = [
# include from public_dir.
"./assets/index.js",
# or some asset from online cdn.
"https://cdn.jsdelivr.net/npm/bootstrap/dist/js/bootstrap.js"
]
```
### Web.Proxy
Configeration related to any proxies your application requires durring development. Proxies will forward requests to a new service
```
[web.proxy]
# configuration
```
1. ***backend*** - The URL to the server to proxy. The CLI will forward any requests under the backend relative route to the backend instead of returning 404
```
backend = "http://localhost:8000/api/"
```
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).
## Config example
```toml
[application]
# App (Project) Name
name = "{{project-name}}"
# The Dioxus platform to default to
default_platform = "web"
# `build` & `serve` output path
out_dir = "dist"
# the static resource path
asset_dir = "public"
[web.app]
# HTML title tag content
title = "dioxus | ⛺"
[web.watcher]
# when watcher is triggered, regenerate the `index.html`
reload_html = true
# which files or dirs will be monitored
watch_path = ["src", "public"]
# include `assets` in web platform
[web.resource]
# CSS style file
style = []
# Javascript code file
script = []
[web.resource.dev]
# serve: [dev-server] only
# CSS style file
style = []
# Javascript code file
script = []
[[web.proxy]]
backend = "http://localhost:8000/api/"
```

View file

@ -0,0 +1,39 @@
# Create a Project
Once you have the Dioxus CLI tool installed, you can use it to create dioxus project.
## Initializing a default project
First, run the `dioxus create` command to create a new project ready to be used with Dioxus and the Dioxus CLI:
```
dioxus create hello-dioxus
```
> It will clone a default template from github template: [DioxusLabs/dioxus-template](https://github.com/DioxusLabs/dioxus-template)
> This default template is use for `web` platform application.
>
> You can choose to create your project from a different template by passing the `template` argument:
> ```
> dioxus init hello-dioxus --template=gh:dioxuslabs/dioxus-template
> ```
Next, move the current directory into your new project:
```
cd hello-dioxus
```
> Make sure `wasm32 target` is installed before running the Web project.
> You can install the wasm target for rust using rustup:
> ```
> rustup target add wasm32-unknown-unknown
> ```
Finally, create serve your project with the Dioxus CLI:
```
dioxus serve
```
By default, the CLI serve your site at: [`http://127.0.0.1:8080/`](http://127.0.0.1:8080/)

View file

@ -0,0 +1,22 @@
# Installation
Choose any one of the methods below to install the Dioxus CLI:
## Install from latest git version
To get the most up to date bug fixes and features of the Dioxus CLI, you can install the development version from git.
```
cargo install --git https://github.com/Dioxuslabs/cli
```
This will automatically download `Dioxus-CLI` source from github master branch,
and install it in Cargo's global binary directory (`~/.cargo/bin/` by default).
## Install from `crates.io` version
The published version of the Dioxus CLI is updated less often, but should be more stable than the git version of the Dioxus CLI.
```
cargo install dioxus-cli
```

View file

@ -0,0 +1,21 @@
# Introduction
📦✨ **Dioxus-Cli** is a tool to help get dioxus projects off the ground.
![dioxus-logo](https://dioxuslabs.com/guide/images/dioxuslogo_full.png)
It includes `dev server`, `hot reload` and some `quick command` to help you use dioxus.
## Features
- [x] `html` to `rsx` conversion tool
- [x] hot reload for `web` platform
- [x] create dioxus project from `git` repo
- [x] build & pack dioxus project
- [ ] autoformat dioxus `rsx` code
## Contributors
Contributors to this guide:
- [mrxiaozhuox](https://github.com/mrxiaozhuox)

View file

@ -0,0 +1,79 @@
# CLI Plugin Development
> For Cli 0.2.0 we will add `plugin-develop` support.
Before the 0.2.0 we use `dioxus tool` to use & install some plugin, but we think that is not good for extend cli program, some people want tailwind support, some people want sass support, we can't add all this thing in to the cli source code and we don't have time to maintain a lot of tools that user request, so maybe user make plugin by themself is a good choice.
### Why Lua ?
We choose `Lua: 5.4` to be the plugin develop language, because cli plugin is not complex, just like a workflow, and user & developer can write some easy code for their plugin. We have **vendored** lua in cli program, and user don't need install lua runtime in their computer, and the lua parser & runtime doesn't take up much disk memory.
### Event Management
The plugin library have pre-define some important event you can control:
- `build.on_start`
- `build.on_finished`
- `serve.on_start`
- `serve.on_rebuild`
- `serve.on_shutdown`
### Plugin Template
```lua
package.path = library_dir .. "/?.lua"
local plugin = require("plugin")
local manager = require("manager")
-- deconstruct api functions
local log = plugin.log
-- plugin information
manager.name = "Hello Dixous Plugin"
manager.repository = "https://github.com/mrxiaozhuox/hello-dioxus-plugin"
manager.author = "YuKun Liu <mrxzx.info@gmail.com>"
manager.version = "0.0.1"
-- init manager info to plugin api
plugin.init(manager)
manager.on_init = function ()
-- when the first time plugin been load, this function will be execute.
-- system will create a `dcp.json` file to verify init state.
log.info("[plugin] Start to init plugin: " .. manager.name)
end
---@param info BuildInfo
manager.build.on_start = function (info)
-- before the build work start, system will execute this function.
log.info("[plugin] Build starting: " .. info.name)
end
---@param info BuildInfo
manager.build.on_finish = function (info)
-- when the build work is done, system will execute this function.
log.info("[plugin] Build finished: " .. info.name)
end
---@param info ServeStartInfo
manager.serve.on_start = function (info)
-- this function will after clean & print to run, so you can print some thing.
log.info("[plugin] Serve start: " .. info.name)
end
---@param info ServeRebuildInfo
manager.serve.on_rebuild = function (info)
-- this function will after clean & print to run, so you can print some thing.
local files = plugin.tool.dump(info.changed_files)
log.info("[plugin] Serve rebuild: '" .. files .. "'")
end
manager.serve.on_shutdown = function ()
log.info("[plugin] Serve shutdown")
end
manager.serve.interval = 1000
return manager
```

View file

@ -0,0 +1,21 @@
# Command Functions
> you can use command functions to execute some code & script
Type Define:
```
Stdio: "Inherit" | "Piped" | "Null"
```
### `exec(commands: [string], stdout: Stdio, stderr: Stdio)`
you can use this function to run some command on the current system.
```lua
local cmd = plugin.command
manager.test = function ()
cmd.exec({"git", "clone", "https://github.com/DioxusLabs/cli-plugin-library"})
end
```
> Warning: This function don't have exception catch.

View file

@ -0,0 +1,35 @@
# Dirs Functions
> you can use Dirs functions to get some directory path
### plugin_dir() -> string
You can get current plugin **root** directory path
```lua
local path = plugin.dirs.plugin_dir()
-- example: ~/Development/DioxusCli/plugin/test-plugin/
```
### bin_dir() -> string
You can get plugin **bin** direcotry path
Sometime you need install some binary file like `tailwind-cli` & `sass-cli` to help your plugin work, then you should put binary file in this directory.
```lua
local path = plugin.dirs.bin_dir()
-- example: ~/Development/DioxusCli/plugin/test-plugin/bin/
```
### temp_dir() -> string
You can get plugin **temp** direcotry path
Just put some temporary file in this directory.
```lua
local path = plugin.dirs.bin_dir()
-- example: ~/Development/DioxusCli/plugin/test-plugin/temp/
```

View file

@ -0,0 +1,48 @@
# Log Functions
> You can use log function to print some useful log info
### Trace(info: string)
Print trace log info
```lua
local log = plugin.log
log.trace("trace information")
```
### Debug(info: string)
Print debug log info
```lua
local log = plugin.log
log.debug("debug information")
```
### Info(info: string)
Print info log info
```lua
local log = plugin.log
log.info("info information")
```
### Warn(info: string)
Print warning log info
```lua
local log = plugin.log
log.warn("warn information")
```
### Error(info: string)
Print error log info
```lua
local log = plugin.log
log.error("error information")
```

View file

@ -0,0 +1,34 @@
# Network Functions
> you can use Network functions to download & read some data from internet
### download_file(url: string, path: string) -> boolean
This function can help you download some file from url, and it will return a *boolean* value to check the download status. (true: success | false: fail)
You need pass a target url and a local path (where you want to save this file)
```lua
-- this file will download to plugin temp directory
local status = plugin.network.download_file(
"http://xxx.com/xxx.zip",
plugin.dirs.temp_dir()
)
if status != true then
log.error("Download Failed")
end
```
### clone_repo(url: string, path: string) -> boolean
This function can help you use `git clone` command (this system must have been installed git)
```lua
local status = plugin.network.clone_repo(
"http://github.com/mrxiaozhuox/dioxus-starter",
plugin.dirs.bin_dir()
)
if status != true then
log.error("Clone Failed")
end
```

View file

@ -0,0 +1,11 @@
# OS Functions
> you can use OS functions to get some system information
### current_platform() -> string ("windows" | "macos" | "linux")
This function can help you get system & platform type:
```lua
local platform = plugin.os.current_platform()
```

View file

@ -0,0 +1,35 @@
# Path Functions
> you can use path functions to operate valid path string
### join(path: string, extra: string) -> string
This function can help you extend a path, you can extend any path, dirname or filename.
```lua
local current_path = "~/hello/dioxus"
local new_path = plugin.path.join(current_path, "world")
-- new_path = "~/hello/dioxus/world"
```
### parent(path: string) -> string
This function will return `path` parent-path string, back to the parent.
```lua
local current_path = "~/hello/dioxus"
local new_path = plugin.path.parent(current_path)
-- new_path = "~/hello/"
```
### exists(path: string) -> boolean
This function can check some path (dir & file) is exists.
### is_file(path: string) -> boolean
This function can check some path is a exist file.
### is_dir(path: string) -> boolean
This function can check some path is a exist dir.

View file

View file

@ -0,0 +1,18 @@
local Api = require("./interface")
local log = Api.log;
local manager = {
name = "Dioxus-CLI Plugin Demo",
repository = "http://github.com/DioxusLabs/cli",
author = "YuKun Liu <mrxzx.info@gmail.com>",
}
manager.onLoad = function ()
log.info("plugin loaded.")
end
manager.onStartBuild = function ()
log.warn("system start to build")
end
return manager

View file

@ -0,0 +1,25 @@
local interface = {}
if plugin_logger ~= nil then
interface.log = plugin_logger
else
interface.log = {
trace = function (info)
print("trace: " .. info)
end,
debug = function (info)
print("debug: " .. info)
end,
info = function (info)
print("info: " .. info)
end,
warn = function (info)
print("warn: " .. info)
end,
error = function (info)
print("error: " .. info)
end,
}
end
return interface

View file

@ -0,0 +1,20 @@
/**@type {import('eslint').Linter.Config} */
// eslint-disable-next-line no-undef
module.exports = {
root: true,
parser: '@typescript-eslint/parser',
plugins: [
'@typescript-eslint',
],
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
],
rules: {
'semi': [2, "always"],
'@typescript-eslint/no-unused-vars': 0,
'@typescript-eslint/no-explicit-any': 0,
'@typescript-eslint/explicit-module-boundary-types': 0,
'@typescript-eslint/no-non-null-assertion': 0,
}
};

13
packages/cli/extension/.gitignore vendored Normal file
View file

@ -0,0 +1,13 @@
.DS_Store
npm-debug.log
Thumbs.db
*/node_modules/
node_modules/
*/out/
out/
*/.vs/
.vs/
tsconfig.lsif.json
*.lsif
*.db
*.vsix

View file

@ -0,0 +1,10 @@
## packaging
```
$ cd myExtension
$ vsce package
# myExtension.vsix generated
$ vsce publish
# <publisherID>.myExtension published to VS Code Marketplace
```

View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 DioxusLabs
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -0,0 +1,14 @@
# Dioxus VSCode Extension
![Dioxus Logo](https://dioxuslabs.com/guide/images/dioxuslogo_full.png)
This extension wraps functionality in Dioxus CLI to be used in your editor! Make sure the dioxus-cli is installed before using this extension.
## Current commands:
### Convert HTML to RSX
Converts a selection of html to valid rsx.
### Convert HTML to Dioxus Component
Converts a selection of html to a valid Dioxus component with all SVGs factored out into their own module.

View file

@ -0,0 +1,2 @@
**
!.gitignore

5079
packages/cli/extension/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,102 @@
{
"name": "dioxus",
"displayName": "Dioxus",
"description": "Useful tools for working with Dioxus",
"version": "0.0.1",
"publisher": "DioxusLabs",
"private": true,
"license": "MIT",
"icon": "static/icon.png",
"repository": {
"type": "git",
"url": "https://github.com/DioxusLabs/cli"
},
"engines": {
"vscode": "^1.68.1"
},
"categories": [
"Programming Languages"
],
"activationEvents": [
"onCommand:extension.htmlToDioxusRsx",
"onCommand:extension.htmlToDioxusComponent",
"onCommand:extension.formatRsx",
"onCommand:extension.formatRsxDocument"
],
"main": "./out/main",
"contributes": {
"commands": [
{
"command": "extension.htmlToDioxusRsx",
"title": "Dioxus: Convert HTML to RSX"
},
{
"command": "extension.htmlToDioxusComponent",
"title": "Dioxus: Convert HTML to Component"
},
{
"command": "extension.formatRsx",
"title": "Dioxus: Format RSX"
},
{
"command": "extension.formatRsxDocument",
"title": "Dioxus: Format RSX Document"
}
],
"configuration": {
"properties": {
"dioxus.formatOnSave": {
"type": [
"string"
],
"default": "followFormatOnSave",
"enum": [
"followFormatOnSave",
"enabled",
"disabled"
],
"enumItemLabels": [
"Follow the normal formatOnSave config",
"Enabled",
"Disabled"
],
"enumDescriptions": [
"Only format Rsx when saving files if the editor.formatOnSave config is enabled",
"Always format Rsx when a Rust file is saved",
"Never format Rsx when a file is saved"
],
"description": "Format RSX when a file is saved."
}
}
}
},
"scripts": {
"vscode:prepublish": "npm run build-base -- --minify",
"package": "vsce package -o rust-analyzer.vsix",
"build-base": "esbuild ./src/main.ts --bundle --outfile=out/main.js --external:vscode --format=cjs --platform=node --target=node16",
"build": "npm run build-base -- --sourcemap",
"watch": "npm run build-base -- --sourcemap --watch",
"lint": "prettier --check . && eslint -c .eslintrc.js --ext ts ./src ./tests",
"fix": "prettier --write . && eslint -c .eslintrc.js --ext ts ./src ./tests --fix",
"pretest": "tsc && npm run build",
"test": "cross-env TEST_VARIABLE=test node ./out/tests/runTests.js"
},
"devDependencies": {
"@types/node": "^18.0.2",
"@types/vscode": "^1.68.1",
"@typescript-eslint/eslint-plugin": "^5.30.5",
"@typescript-eslint/parser": "^5.30.5",
"cross-env": "^7.0.3",
"esbuild": "^0.14.27",
"eslint": "^8.19.0",
"typescript": "^4.7.4",
"eslint-config-prettier": "^8.5.0",
"ovsx": "^0.5.1",
"prettier": "^2.6.2",
"tslib": "^2.3.0",
"vsce": "^2.7.0"
},
"dependencies": {
"vsce": "^2.9.2"
}
}

View file

@ -0,0 +1,3 @@
**
!Readme.md
!.gitignore

View file

@ -0,0 +1,282 @@
import * as vscode from 'vscode';
import { spawn } from "child_process";
import { TextEncoder } from 'util';
let serverPath: string = "dioxus";
export async function activate(context: vscode.ExtensionContext) {
let somePath = await bootstrap(context);
if (somePath == undefined) {
await vscode.window.showErrorMessage('Could not find bundled Dioxus-CLI. Please install it manually.');
return;
} else {
serverPath = somePath;
}
context.subscriptions.push(
// vscode.commands.registerTextEditorCommand('editor.action.clipboardPasteAction', onPasteHandler),
vscode.commands.registerCommand('extension.htmlToDioxusRsx', translateBlock),
vscode.commands.registerCommand('extension.htmlToDioxusComponent', translateComponent),
vscode.commands.registerCommand('extension.formatRsx', fmtSelection),
vscode.commands.registerCommand('extension.formatRsxDocument', formatRsxDocument),
vscode.workspace.onWillSaveTextDocument(fmtDocumentOnSave)
);
}
function translateComponent() {
translate(true)
}
function translateBlock() {
translate(false)
}
function translate(component: boolean) {
const editor = vscode.window.activeTextEditor;
if (!editor) return;
const html = editor.document.getText(editor.selection);
if (html.length == 0) {
vscode.window.showWarningMessage("Please select HTML fragment before invoking this command!");
return;
}
let params = ["translate"];
if (component) params.push("--component");
params.push("--raw", html);
const child_proc = spawn(serverPath, params);
let result = '';
child_proc.stdout?.on('data', data => result += data);
child_proc.on('close', () => {
if (result.length > 0) editor.edit(editBuilder => editBuilder.replace(editor.selection, result));
});
child_proc.on('error', (err) => {
vscode.window.showWarningMessage(`Errors occurred while translating. Make sure you have the most recent Dioxus-CLI installed! \n${err}`);
});
}
function onPasteHandler() {
// check settings to see if we should convert HTML to Rsx
if (vscode.workspace.getConfiguration('dioxus').get('convertOnPaste')) {
convertHtmlToRsxOnPaste();
}
}
function convertHtmlToRsxOnPaste() {
const editor = vscode.window.activeTextEditor;
if (!editor) return;
// get the cursor location
const cursor = editor.selection.active;
// try to parse the HTML at the cursor location
const html = editor.document.getText(new vscode.Range(cursor, cursor));
}
function formatRsxDocument() {
const editor = vscode.window.activeTextEditor;
if (!editor) return;
fmtDocument(editor.document);
}
function fmtSelection() {
const editor = vscode.window.activeTextEditor;
if (!editor) return;
const unformatted = editor.document.getText(editor.selection);
if (unformatted.length == 0) {
vscode.window.showWarningMessage("Please select rsx invoking this command!");
return;
}
const fileDir = editor.document.fileName.slice(0, editor.document.fileName.lastIndexOf('\\'));
const child_proc = spawn(serverPath, ["fmt", "--raw", unformatted.toString()], {
cwd: fileDir ? fileDir : undefined,
});
let result = '';
child_proc.stdout?.on('data', data => result += data);
child_proc.on('close', () => {
if (result.length > 0) editor.edit(editBuilder => editBuilder.replace(editor.selection, result));
});
child_proc.on('error', (err) => {
vscode.window.showWarningMessage(`Errors occurred while translating. Make sure you have the most recent Dioxus-CLI installed! \n${err}`);
});
}
function fmtDocumentOnSave(e: vscode.TextDocumentWillSaveEvent) {
// check the settings to make sure format on save is configured
const dioxusConfig = vscode.workspace.getConfiguration('dioxus', e.document).get('formatOnSave');
const globalConfig = vscode.workspace.getConfiguration('editor', e.document).get('formatOnSave');
if (
(dioxusConfig === 'enabled') ||
(dioxusConfig !== 'disabled' && globalConfig)
) {
fmtDocument(e.document);
}
}
function fmtDocument(document: vscode.TextDocument) {
try {
if (document.languageId !== "rust" || document.uri.scheme !== "file") {
return;
}
const [editor,] = vscode.window.visibleTextEditors.filter(editor => editor.document.fileName === document.fileName);
if (!editor) return; // Need an editor to apply text edits.
const fileDir = document.fileName.slice(0, document.fileName.lastIndexOf('\\'));
const child_proc = spawn(serverPath, ["fmt", "--file", document.fileName], {
cwd: fileDir ? fileDir : undefined,
});
let result = '';
child_proc.stdout?.on('data', data => result += data);
/*type RsxEdit = {
formatted: string,
start: number,
end: number
}*/
child_proc.on('close', () => {
if (child_proc.exitCode !== 0) {
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}`);
return;
}
/*if (result.length === 0) return;
// Used for error message:
const originalResult = result;
try {
// Only parse the last non empty line, to skip log warning messages:
const lines = result.replaceAll('\r\n', '\n').split('\n');
const nonEmptyLines = lines.filter(line => line.trim().length !== 0);
result = nonEmptyLines[nonEmptyLines.length - 1] ?? '';
if (result.length === 0) return;
const decoded: RsxEdit[] = JSON.parse(result);
if (decoded.length === 0) return;
// Preform edits at the end of the file
// first (to not change previous text file
// offsets):
decoded.sort((a, b) => b.start - a.start);
// Convert from utf8 offsets to utf16 offsets used by VS Code:
const utf8Text = new TextEncoder().encode(text);
const utf8ToUtf16Pos = (posUtf8: number) => {
// Find the line of the position as well as the utf8 and
// utf16 indexes for the start of that line:
let startOfLineUtf8 = 0;
let lineIndex = 0;
const newLineUtf8 = '\n'.charCodeAt(0);
// eslint-disable-next-line no-constant-condition
while (true) {
const nextLineAt = utf8Text.indexOf(newLineUtf8, startOfLineUtf8);
if (nextLineAt < 0 || posUtf8 <= nextLineAt) break;
startOfLineUtf8 = nextLineAt + 1;
lineIndex++;
}
const lineUtf16 = document.lineAt(lineIndex);
// Move forward from a synced position in the text until the
// target pos is found:
let currentUtf8 = startOfLineUtf8;
let currentUtf16 = document.offsetAt(lineUtf16.range.start);
const decodeBuffer = new Uint8Array(10);
const utf8Encoder = new TextEncoder();
while (currentUtf8 < posUtf8) {
const { written } = utf8Encoder.encodeInto(text.charAt(currentUtf16), decodeBuffer);
currentUtf8 += written;
currentUtf16++;
}
return currentUtf16;
};
type FixedEdit = {
range: vscode.Range,
formatted: string,
};
const edits: FixedEdit[] = [];
for (const edit of decoded) {
// Convert from utf8 to utf16:
const range = new vscode.Range(
document.positionAt(utf8ToUtf16Pos(edit.start)),
document.positionAt(utf8ToUtf16Pos(edit.end))
);
if (editor.document.getText(range) !== document.getText(range)) {
// The text that was formatted has changed while we were working.
vscode.window.showWarningMessage(`Dioxus formatting was ignored since the source file changed before the change could be applied.`);
continue;
}
edits.push({
range,
formatted: edit.formatted,
});
}
// Apply edits:
editor.edit(editBuilder => {
edits.forEach((edit) => editBuilder.replace(edit.range, edit.formatted));
}, {
undoStopAfter: false,
undoStopBefore: false
});
} catch (err) {
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}`);
}*/
});
child_proc.on('error', (err) => {
vscode.window.showWarningMessage(`Errors occurred while formatting. Make sure you have the most recent Dioxus-CLI installed! \n${err}`);
});
} catch (error) {
vscode.window.showWarningMessage(`Errors occurred while formatting. Make sure you have the most recent Dioxus-CLI installed! \n${error}`);
}
}
// I'm using the approach defined in rust-analyzer here
//
// We ship the server as part of the extension, but we need to handle external paths and such
//
// https://github.com/rust-lang/rust-analyzer/blob/fee5555cfabed4b8abbd40983fc4442df4007e49/editors/code/src/main.ts#L270
async function bootstrap(context: vscode.ExtensionContext): Promise<string | undefined> {
const ext = process.platform === "win32" ? ".exe" : "";
const bundled = vscode.Uri.joinPath(context.extensionUri, "server", `dioxus${ext}`);
const bundledExists = await vscode.workspace.fs.stat(bundled).then(
() => true,
() => false
);
// if bunddled doesn't exist, try using a locally-installed version
if (!bundledExists) {
return "dioxus";
}
return bundled.fsPath;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View file

@ -0,0 +1,12 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es2021",
"lib": ["ES2021"],
"outDir": "out",
"sourceMap": true,
"strict": true,
"rootDir": "src"
},
"exclude": ["node_modules", ".vscode-test"]
}

View file

@ -0,0 +1,8 @@
version = "Two"
edition = "2021"
imports_granularity = "Crate"
#use_small_heuristics = "Max"
#control_brace_style = "ClosingNextLine"
normalize_comments = true
format_code_in_doc_comments = true

View file

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

View file

@ -0,0 +1,47 @@
[application]
# dioxus project name
name = "{{project-name}}"
# default platfrom
# you can also use `dioxus serve/build --platform XXX` to use other platform
# value: web | desktop
default_platform = "{{default-platform}}"
# Web `build` & `serve` dist path
out_dir = "dist"
# resource (static) file folder
asset_dir = "public"
[web.app]
# HTML title tag content
title = "Dioxus | An elegant GUI library for Rust"
[web.watcher]
index_on_404 = true
watch_path = ["src"]
# include `assets` in web platform
[web.resource]
# CSS style file
style = []
# Javascript code file
script = []
[web.resource.dev]
# Javascript code file
# serve: [dev-server] only
script = []
[application.plugins]
available = true
required = []

View file

@ -0,0 +1,22 @@
<!DOCTYPE html>
<html>
<head>
<title>{app_title}</title>
<meta content="text/html;charset=utf-8" http-equiv="Content-Type" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta charset="UTF-8" />
{style_include}
</head>
<body>
<div id="main"></div>
<script type="module">
import init from "/{base_path}/assets/dioxus/{app_name}.js";
init("/{base_path}/assets/dioxus/{app_name}_bg.wasm").then(wasm => {
if (wasm.__wbindgen_start == undefined) {
wasm.main();
}
});
</script>
{script_include}
</body>
</html>

725
packages/cli/src/builder.rs Normal file
View file

@ -0,0 +1,725 @@
use crate::{
config::{CrateConfig, ExecutableType},
error::{Error, Result},
tools::Tool,
DioxusConfig,
};
use cargo_metadata::{diagnostic::Diagnostic, Message};
use indicatif::{ProgressBar, ProgressStyle};
use serde::Serialize;
use std::{
fs::{copy, create_dir_all, File},
io::Read,
panic,
path::PathBuf,
process::Command,
time::Duration,
};
use wasm_bindgen_cli_support::Bindgen;
#[derive(Serialize, Debug, Clone)]
pub struct BuildResult {
pub warnings: Vec<Diagnostic>,
pub elapsed_time: u128,
}
pub fn build(config: &CrateConfig, quiet: bool) -> Result<BuildResult> {
// [1] Build the project with cargo, generating a wasm32-unknown-unknown target (is there a more specific, better target to leverage?)
// [2] Generate the appropriate build folders
// [3] Wasm-bindgen the .wasm fiile, and move it into the {builddir}/modules/xxxx/xxxx_bg.wasm
// [4] Wasm-opt the .wasm file with whatever optimizations need to be done
// [5][OPTIONAL] Builds the Tailwind CSS file using the Tailwind standalone binary
// [6] Link up the html page to the wasm module
let CrateConfig {
out_dir,
crate_dir,
target_dir,
asset_dir,
executable,
dioxus_config,
..
} = config;
// start to build the assets
let ignore_files = build_assets(config)?;
let t_start = std::time::Instant::now();
// [1] Build the .wasm module
log::info!("🚅 Running build command...");
let cmd = subprocess::Exec::cmd("cargo");
let cmd = cmd
.cwd(&crate_dir)
.arg("build")
.arg("--target")
.arg("wasm32-unknown-unknown")
.arg("--message-format=json");
let cmd = if config.release {
cmd.arg("--release")
} else {
cmd
};
let cmd = if config.verbose {
cmd.arg("--verbose")
} else {
cmd
};
let cmd = if quiet { cmd.arg("--quiet") } else { cmd };
let cmd = if config.custom_profile.is_some() {
let custom_profile = config.custom_profile.as_ref().unwrap();
cmd.arg("--profile").arg(custom_profile)
} else {
cmd
};
let cmd = if config.features.is_some() {
let features_str = config.features.as_ref().unwrap().join(" ");
cmd.arg("--features").arg(features_str)
} else {
cmd
};
let cmd = match executable {
ExecutableType::Binary(name) => cmd.arg("--bin").arg(name),
ExecutableType::Lib(name) => cmd.arg("--lib").arg(name),
ExecutableType::Example(name) => cmd.arg("--example").arg(name),
};
let warning_messages = prettier_build(cmd)?;
// [2] Establish the output directory structure
let bindgen_outdir = out_dir.join("assets").join("dioxus");
let release_type = match config.release {
true => "release",
false => "debug",
};
let input_path = match executable {
ExecutableType::Binary(name) | ExecutableType::Lib(name) => target_dir
.join(format!("wasm32-unknown-unknown/{}", release_type))
.join(format!("{}.wasm", name)),
ExecutableType::Example(name) => target_dir
.join(format!("wasm32-unknown-unknown/{}/examples", release_type))
.join(format!("{}.wasm", name)),
};
let bindgen_result = panic::catch_unwind(move || {
// [3] Bindgen the final binary for use easy linking
let mut bindgen_builder = Bindgen::new();
bindgen_builder
.input_path(input_path)
.web(true)
.unwrap()
.debug(true)
.demangle(true)
.keep_debug(true)
.remove_name_section(false)
.remove_producers_section(false)
.out_name(&dioxus_config.application.name)
.generate(&bindgen_outdir)
.unwrap();
});
if bindgen_result.is_err() {
return Err(Error::BuildFailed("Bindgen build failed! \nThis is probably due to the Bindgen version, dioxus-cli using `0.2.81` Bindgen crate.".to_string()));
}
// check binaryen:wasm-opt tool
let dioxus_tools = dioxus_config.application.tools.clone().unwrap_or_default();
if dioxus_tools.contains_key("binaryen") {
let info = dioxus_tools.get("binaryen").unwrap();
let binaryen = crate::tools::Tool::Binaryen;
if binaryen.is_installed() {
if let Some(sub) = info.as_table() {
if sub.contains_key("wasm_opt")
&& sub.get("wasm_opt").unwrap().as_bool().unwrap_or(false)
{
log::info!("Optimizing WASM size with wasm-opt...");
let target_file = out_dir
.join("assets")
.join("dioxus")
.join(format!("{}_bg.wasm", dioxus_config.application.name));
if target_file.is_file() {
let mut args = vec![
target_file.to_str().unwrap(),
"-o",
target_file.to_str().unwrap(),
];
if config.release == true {
args.push("-Oz");
}
binaryen.call("wasm-opt", args)?;
}
}
}
} else {
log::warn!(
"Binaryen tool not found, you can use `dioxus tool add binaryen` to install it."
);
}
}
// [5][OPTIONAL] If tailwind is enabled and installed we run it to generate the CSS
if dioxus_tools.contains_key("tailwindcss") {
let info = dioxus_tools.get("tailwindcss").unwrap();
let tailwind = crate::tools::Tool::Tailwind;
if tailwind.is_installed() {
if let Some(sub) = info.as_table() {
log::info!("Building Tailwind bundle CSS file...");
let input_path = match sub.get("input") {
Some(val) => val.as_str().unwrap(),
None => "./public",
};
let config_path = match sub.get("config") {
Some(val) => val.as_str().unwrap(),
None => "./src/tailwind.config.js",
};
let mut args = vec![
"-i",
input_path,
"-o",
"dist/tailwind.css",
"-c",
config_path,
];
if config.release == true {
args.push("--minify");
}
tailwind.call("tailwindcss", args)?;
}
} else {
log::warn!(
"Tailwind tool not found, you can use `dioxus tool add tailwindcss` to install it."
);
}
}
// this code will copy all public file to the output dir
let copy_options = fs_extra::dir::CopyOptions {
overwrite: true,
skip_exist: false,
buffer_size: 64000,
copy_inside: false,
content_only: false,
depth: 0,
};
if asset_dir.is_dir() {
for entry in std::fs::read_dir(&asset_dir)? {
let path = entry?.path();
if path.is_file() {
std::fs::copy(&path, out_dir.join(path.file_name().unwrap()))?;
} else {
match fs_extra::dir::copy(&path, out_dir, &copy_options) {
Ok(_) => {}
Err(_e) => {
log::warn!("Error copying dir: {}", _e);
}
}
for ignore in &ignore_files {
let ignore = ignore.strip_prefix(&config.asset_dir).unwrap();
let ignore = config.out_dir.join(ignore);
if ignore.is_file() {
std::fs::remove_file(ignore)?;
}
}
}
}
}
let t_end = std::time::Instant::now();
Ok(BuildResult {
warnings: warning_messages,
elapsed_time: (t_end - t_start).as_millis(),
})
}
pub fn build_desktop(config: &CrateConfig, _is_serve: bool) -> Result<()> {
log::info!("🚅 Running build [Desktop] command...");
let ignore_files = build_assets(config)?;
let mut cmd = Command::new("cargo");
cmd.current_dir(&config.crate_dir)
.arg("build")
.stdout(std::process::Stdio::inherit())
.stderr(std::process::Stdio::inherit());
if config.release {
cmd.arg("--release");
}
if config.verbose {
cmd.arg("--verbose");
}
if config.custom_profile.is_some() {
let custom_profile = config.custom_profile.as_ref().unwrap();
cmd.arg("--profile");
cmd.arg(custom_profile);
}
if config.features.is_some() {
let features_str = config.features.as_ref().unwrap().join(" ");
cmd.arg("--features");
cmd.arg(features_str);
}
match &config.executable {
crate::ExecutableType::Binary(name) => cmd.arg("--bin").arg(name),
crate::ExecutableType::Lib(name) => cmd.arg("--lib").arg(name),
crate::ExecutableType::Example(name) => cmd.arg("--example").arg(name),
};
let output = cmd.output()?;
if !output.status.success() {
return Err(Error::BuildFailed("Program build failed.".into()));
}
if output.status.success() {
let release_type = match config.release {
true => "release",
false => "debug",
};
let file_name: String;
let mut res_path = match &config.executable {
crate::ExecutableType::Binary(name) | crate::ExecutableType::Lib(name) => {
file_name = name.clone();
config.target_dir.join(release_type).join(name)
}
crate::ExecutableType::Example(name) => {
file_name = name.clone();
config
.target_dir
.join(release_type)
.join("examples")
.join(name)
}
};
let target_file = if cfg!(windows) {
res_path.set_extension("exe");
format!("{}.exe", &file_name)
} else {
file_name
};
if !config.out_dir.is_dir() {
create_dir_all(&config.out_dir)?;
}
copy(res_path, &config.out_dir.join(target_file))?;
// this code will copy all public file to the output dir
if config.asset_dir.is_dir() {
let copy_options = fs_extra::dir::CopyOptions {
overwrite: true,
skip_exist: false,
buffer_size: 64000,
copy_inside: false,
content_only: false,
depth: 0,
};
for entry in std::fs::read_dir(&config.asset_dir)? {
let path = entry?.path();
if path.is_file() {
std::fs::copy(&path, &config.out_dir.join(path.file_name().unwrap()))?;
} else {
match fs_extra::dir::copy(&path, &config.out_dir, &copy_options) {
Ok(_) => {}
Err(e) => {
log::warn!("Error copying dir: {}", e);
}
}
for ignore in &ignore_files {
let ignore = ignore.strip_prefix(&config.asset_dir).unwrap();
let ignore = config.out_dir.join(ignore);
if ignore.is_file() {
std::fs::remove_file(ignore)?;
}
}
}
}
}
log::info!(
"🚩 Build completed: [./{}]",
config
.dioxus_config
.application
.out_dir
.clone()
.unwrap_or_else(|| PathBuf::from("dist"))
.display()
);
}
Ok(())
}
fn prettier_build(cmd: subprocess::Exec) -> anyhow::Result<Vec<Diagnostic>> {
let mut warning_messages: Vec<Diagnostic> = vec![];
let pb = ProgressBar::new_spinner();
pb.enable_steady_tick(Duration::from_millis(200));
pb.set_style(
ProgressStyle::with_template("{spinner:.dim.bold} {wide_msg}")
.unwrap()
.tick_chars("/|\\- "),
);
pb.set_message("💼 Waiting to start build the project...");
struct StopSpinOnDrop(ProgressBar);
impl Drop for StopSpinOnDrop {
fn drop(&mut self) {
self.0.finish_and_clear();
}
}
StopSpinOnDrop(pb.clone());
let stdout = cmd.detached().stream_stdout()?;
let reader = std::io::BufReader::new(stdout);
for message in cargo_metadata::Message::parse_stream(reader) {
match message.unwrap() {
Message::CompilerMessage(msg) => {
let message = msg.message;
match message.level {
cargo_metadata::diagnostic::DiagnosticLevel::Error => {
return {
Err(anyhow::anyhow!(message
.rendered
.unwrap_or("Unknown".into())))
};
}
cargo_metadata::diagnostic::DiagnosticLevel::Warning => {
warning_messages.push(message.clone());
}
_ => {}
}
}
Message::CompilerArtifact(artifact) => {
pb.set_message(format!("Compiling {} ", artifact.package_id));
pb.tick();
}
Message::BuildScriptExecuted(script) => {
let _package_id = script.package_id.to_string();
}
Message::BuildFinished(finished) => {
if finished.success {
log::info!("👑 Build done.");
} else {
std::process::exit(1);
}
}
_ => (), // Unknown message
}
}
Ok(warning_messages)
}
pub fn gen_page(config: &DioxusConfig, serve: bool) -> String {
let crate_root = crate::cargo::crate_root().unwrap();
let custom_html_file = crate_root.join("index.html");
let mut html = if custom_html_file.is_file() {
let mut buf = String::new();
let mut file = File::open(custom_html_file).unwrap();
if file.read_to_string(&mut buf).is_ok() {
buf
} else {
String::from(include_str!("./assets/index.html"))
}
} else {
String::from(include_str!("./assets/index.html"))
};
let resouces = config.web.resource.clone();
let mut style_list = resouces.style.unwrap_or_default();
let mut script_list = resouces.script.unwrap_or_default();
if serve {
let mut dev_style = resouces.dev.style.clone().unwrap_or_default();
let mut dev_script = resouces.dev.script.unwrap_or_default();
style_list.append(&mut dev_style);
script_list.append(&mut dev_script);
}
let mut style_str = String::new();
for style in style_list {
style_str.push_str(&format!(
"<link rel=\"stylesheet\" href=\"{}\">\n",
&style.to_str().unwrap(),
))
}
if config
.application
.tools
.clone()
.unwrap_or_default()
.contains_key("tailwindcss")
{
style_str.push_str("<link rel=\"stylesheet\" href=\"tailwind.css\">\n");
}
replace_or_insert_before("{style_include}", &style_str, "</head", &mut html);
let mut script_str = String::new();
for script in script_list {
script_str.push_str(&format!(
"<script src=\"{}\"></script>\n",
&script.to_str().unwrap(),
))
}
replace_or_insert_before("{script_include}", &script_str, "</body", &mut html);
if serve {
html += &format!(
"<script>{}</script>",
include_str!("./assets/autoreload.js")
);
}
let base_path = match &config.web.app.base_path {
Some(path) => path,
None => ".",
};
let app_name = &config.application.name;
// Check if a script already exists
if html.contains("{app_name}") && html.contains("{base_path}") {
html = html.replace("{app_name}", app_name);
html = html.replace("{base_path}", base_path);
} else {
// If not, insert the script
html = html.replace(
"</body",
&format!(
r#"<script type="module">
import init from "/{base_path}/assets/dioxus/{app_name}.js";
init("/{base_path}/assets/dioxus/{app_name}_bg.wasm").then(wasm => {{
if (wasm.__wbindgen_start == undefined) {{
wasm.main();
}}
}});
</script>
</body"#
),
);
}
let title = config
.web
.app
.title
.clone()
.unwrap_or_else(|| "dioxus | ⛺".into());
replace_or_insert_before("{app_title}", &title, "</title", &mut html);
html
}
fn replace_or_insert_before(
replace: &str,
with: &str,
or_insert_before: &str,
content: &mut String,
) {
if content.contains(replace) {
*content = content.replace(replace, with);
} else {
*content = content.replace(or_insert_before, &format!("{}{}", with, or_insert_before));
}
}
// this function will build some assets file
// like sass tool resources
// this function will return a array which file don't need copy to out_dir.
fn build_assets(config: &CrateConfig) -> Result<Vec<PathBuf>> {
let mut result = vec![];
let dioxus_config = &config.dioxus_config;
let dioxus_tools = dioxus_config.application.tools.clone().unwrap_or_default();
// check sass tool state
let sass = Tool::Sass;
if sass.is_installed() && dioxus_tools.contains_key("sass") {
let sass_conf = dioxus_tools.get("sass").unwrap();
if let Some(tab) = sass_conf.as_table() {
let source_map = tab.contains_key("source_map");
let source_map = if source_map && tab.get("source_map").unwrap().is_bool() {
if tab.get("source_map").unwrap().as_bool().unwrap_or_default() {
"--source-map"
} else {
"--no-source-map"
}
} else {
"--source-map"
};
if tab.contains_key("input") {
if tab.get("input").unwrap().is_str() {
let file = tab.get("input").unwrap().as_str().unwrap().trim();
if file == "*" {
// if the sass open auto, we need auto-check the assets dir.
let asset_dir = config.asset_dir.clone();
if asset_dir.is_dir() {
for entry in walkdir::WalkDir::new(&asset_dir)
.into_iter()
.filter_map(|e| e.ok())
{
let temp = entry.path();
if temp.is_file() {
let suffix = temp.extension();
if suffix.is_none() {
continue;
}
let suffix = suffix.unwrap().to_str().unwrap();
if suffix == "scss" || suffix == "sass" {
// if file suffix is `scss` / `sass` we need transform it.
let out_file = format!(
"{}.css",
temp.file_stem().unwrap().to_str().unwrap()
);
let target_path = config
.out_dir
.join(
temp.strip_prefix(&asset_dir)
.unwrap()
.parent()
.unwrap(),
)
.join(out_file);
let res = sass.call(
"sass",
vec![
temp.to_str().unwrap(),
target_path.to_str().unwrap(),
source_map,
],
);
if res.is_ok() {
result.push(temp.to_path_buf());
}
}
}
}
}
} else {
// just transform one file.
let relative_path = if &file[0..1] == "/" {
&file[1..file.len()]
} else {
file
};
let path = config.asset_dir.join(relative_path);
let out_file =
format!("{}.css", path.file_stem().unwrap().to_str().unwrap());
let target_path = config
.out_dir
.join(PathBuf::from(relative_path).parent().unwrap())
.join(out_file);
if path.is_file() {
let res = sass.call(
"sass",
vec![
path.to_str().unwrap(),
target_path.to_str().unwrap(),
source_map,
],
);
if res.is_ok() {
result.push(path);
} else {
log::error!("{:?}", res);
}
}
}
} else if tab.get("input").unwrap().is_array() {
// check files list.
let list = tab.get("input").unwrap().as_array().unwrap();
for i in list {
if i.is_str() {
let path = i.as_str().unwrap();
let relative_path = if &path[0..1] == "/" {
&path[1..path.len()]
} else {
path
};
let path = config.asset_dir.join(relative_path);
let out_file =
format!("{}.css", path.file_stem().unwrap().to_str().unwrap());
let target_path = config
.out_dir
.join(PathBuf::from(relative_path).parent().unwrap())
.join(out_file);
if path.is_file() {
let res = sass.call(
"sass",
vec![
path.to_str().unwrap(),
target_path.to_str().unwrap(),
source_map,
],
);
if res.is_ok() {
result.push(path);
}
}
}
}
}
}
}
}
// SASS END
Ok(result)
}
// use binary_install::{Cache, Download};
// /// Attempts to find `wasm-opt` in `PATH` locally, or failing that downloads a
// /// precompiled binary.
// ///
// /// Returns `Some` if a binary was found or it was successfully downloaded.
// /// Returns `None` if a binary wasn't found in `PATH` and this platform doesn't
// /// have precompiled binaries. Returns an error if we failed to download the
// /// binary.
// pub fn find_wasm_opt(
// cache: &Cache,
// install_permitted: bool,
// ) -> Result<install::Status, failure::Error> {
// // First attempt to look up in PATH. If found assume it works.
// if let Ok(path) = which::which("wasm-opt") {
// PBAR.info(&format!("found wasm-opt at {:?}", path));
// match path.as_path().parent() {
// Some(path) => return Ok(install::Status::Found(Download::at(path))),
// None => {}
// }
// }
// let version = "version_78";
// Ok(install::download_prebuilt(
// &install::Tool::WasmOpt,
// cache,
// version,
// install_permitted,
// )?)
// }

94
packages/cli/src/cargo.rs Normal file
View file

@ -0,0 +1,94 @@
//! Utilities for working with cargo and rust files
use crate::error::{Error, Result};
use std::{
env, fs,
path::{Path, PathBuf},
process::Command,
str,
};
/// How many parent folders are searched for a `Cargo.toml`
const MAX_ANCESTORS: u32 = 10;
/// Some fields parsed from `cargo metadata` command
pub struct Metadata {
pub workspace_root: PathBuf,
pub target_directory: PathBuf,
}
/// Returns the root of the crate that the command is run from
///
/// If the command is run from the workspace root, this will return the top-level Cargo.toml
pub fn crate_root() -> Result<PathBuf> {
// From the current directory we work our way up, looking for `Cargo.toml`
env::current_dir()
.ok()
.and_then(|mut wd| {
for _ in 0..MAX_ANCESTORS {
if contains_manifest(&wd) {
return Some(wd);
}
if !wd.pop() {
break;
}
}
None
})
.ok_or_else(|| {
Error::CargoError("Failed to find directory containing Cargo.toml".to_string())
})
}
/// Checks if the directory contains `Cargo.toml`
fn contains_manifest(path: &Path) -> bool {
fs::read_dir(path)
.map(|entries| {
entries
.filter_map(Result::ok)
.any(|ent| &ent.file_name() == "Cargo.toml")
})
.unwrap_or(false)
}
impl Metadata {
/// Returns the struct filled from `cargo metadata` output
/// TODO @Jon, find a different way that doesn't rely on the cargo metadata command (it's slow)
pub fn get() -> Result<Self> {
let output = Command::new("cargo")
.args(&["metadata"])
.output()
.map_err(|_| Error::CargoError("Manifset".to_string()))?;
if !output.status.success() {
let mut msg = str::from_utf8(&output.stderr).unwrap().trim();
if msg.starts_with("error: ") {
msg = &msg[7..];
}
return Err(Error::CargoError(msg.to_string()));
}
let stdout = str::from_utf8(&output.stdout).unwrap();
if let Some(line) = stdout.lines().next() {
let meta: serde_json::Value = serde_json::from_str(line)
.map_err(|_| Error::CargoError("InvalidOutput".to_string()))?;
let workspace_root = meta["workspace_root"]
.as_str()
.ok_or_else(|| Error::CargoError("InvalidOutput".to_string()))?
.into();
let target_directory = meta["target_directory"]
.as_str()
.ok_or_else(|| Error::CargoError("InvalidOutput".to_string()))?
.into();
return Ok(Self {
workspace_root,
target_directory,
});
}
Err(Error::CargoError("InvalidOutput".to_string()))
}
}

View file

@ -0,0 +1,184 @@
use futures::{stream::FuturesUnordered, StreamExt};
use std::{fs, process::exit};
use super::*;
// For reference, the rustfmt main.rs file
// https://github.com/rust-lang/rustfmt/blob/master/src/bin/main.rs
/// Build the Rust WASM app and all of its assets.
#[derive(Clone, Debug, Parser)]
pub struct Autoformat {
/// Run in 'check' mode. Exits with 0 if input is formatted correctly. Exits
/// with 1 and prints a diff if formatting is required.
#[clap(short, long)]
pub check: bool,
/// Input rsx (selection)
#[clap(short, long)]
pub raw: Option<String>,
/// Input file
#[clap(short, long)]
pub file: Option<String>,
}
impl Autoformat {
// Todo: autoformat the entire crate
pub async fn autoformat(self) -> Result<()> {
// Default to formatting the project
if self.raw.is_none() && self.file.is_none() {
if let Err(e) = autoformat_project(self.check).await {
eprintln!("error formatting project: {}", e);
exit(1);
}
}
if let Some(raw) = self.raw {
if let Some(inner) = dioxus_autofmt::fmt_block(&raw, 0) {
println!("{}", inner);
} else {
// exit process with error
eprintln!("error formatting codeblock");
exit(1);
}
}
// Format single file
if let Some(file) = self.file {
let file_content = fs::read_to_string(&file);
match file_content {
Ok(s) => {
let edits = dioxus_autofmt::fmt_file(&s);
let out = dioxus_autofmt::apply_formats(&s, edits);
match fs::write(&file, out) {
Ok(_) => {
println!("formatted {}", file);
}
Err(e) => {
eprintln!("failed to write formatted content to file: {}", e);
}
}
}
Err(e) => {
eprintln!("failed to open file: {}", e);
exit(1);
}
}
}
Ok(())
}
}
/// Read every .rs file accessible when considering the .gitignore and try to format it
///
/// Runs using Tokio for multithreading, so it should be really really fast
///
/// Doesn't do mod-descending, so it will still try to format unreachable files. TODO.
async fn autoformat_project(check: bool) -> Result<()> {
let crate_config = crate::CrateConfig::new()?;
let mut files_to_format = vec![];
collect_rs_files(&crate_config.crate_dir, &mut files_to_format);
let counts = files_to_format
.into_iter()
.filter(|file| {
if file.components().any(|f| f.as_os_str() == "target") {
return false;
}
true
})
.map(|path| async {
let _path = path.clone();
let res = tokio::spawn(async move {
let contents = tokio::fs::read_to_string(&path).await?;
let edits = dioxus_autofmt::fmt_file(&contents);
let len = edits.len();
if !edits.is_empty() {
let out = dioxus_autofmt::apply_formats(&contents, edits);
tokio::fs::write(&path, out).await?;
}
Ok(len) as Result<usize, tokio::io::Error>
})
.await;
if res.is_err() {
eprintln!("error formatting file: {}", _path.display());
}
res
})
.collect::<FuturesUnordered<_>>()
.collect::<Vec<_>>()
.await;
let files_formatted: usize = counts
.into_iter()
.map(|f| match f {
Ok(Ok(res)) => res,
_ => 0,
})
.sum();
if files_formatted > 0 && check {
eprintln!("{} files needed formatting", files_formatted);
exit(1);
}
Ok(())
}
fn collect_rs_files(folder: &PathBuf, files: &mut Vec<PathBuf>) {
let Ok(folder) = folder.read_dir() else { return };
// load the gitignore
for entry in folder {
let Ok(entry) = entry else { continue; };
let path = entry.path();
if path.is_dir() {
collect_rs_files(&path, files);
}
if let Some(ext) = path.extension() {
if ext == "rs" {
files.push(path);
}
}
}
}
#[test]
fn spawn_properly() {
let out = Command::new("dioxus")
.args([
"fmt",
"-f",
r#"
//
rsx! {
div {}
}
//
//
//
"#,
])
.output()
.expect("failed to execute process");
dbg!(out);
}

View file

@ -0,0 +1,76 @@
use crate::plugin::PluginManager;
use super::*;
/// Build the Rust WASM app and all of its assets.
#[derive(Clone, Debug, Parser)]
#[clap(name = "build")]
pub struct Build {
#[clap(flatten)]
pub build: ConfigOptsBuild,
}
impl Build {
pub fn build(self) -> Result<()> {
let mut crate_config = crate::CrateConfig::new()?;
// change the release state.
crate_config.with_release(self.build.release);
crate_config.with_verbose(self.build.verbose);
if self.build.example.is_some() {
crate_config.as_example(self.build.example.unwrap());
}
if self.build.profile.is_some() {
crate_config.set_profile(self.build.profile.unwrap());
}
if self.build.features.is_some() {
crate_config.set_features(self.build.features.unwrap());
}
let platform = self.build.platform.unwrap_or_else(|| {
crate_config
.dioxus_config
.application
.default_platform
.clone()
});
let _ = PluginManager::on_build_start(&crate_config, &platform);
match platform.as_str() {
"web" => {
crate::builder::build(&crate_config, false)?;
}
"desktop" => {
crate::builder::build_desktop(&crate_config, false)?;
}
_ => {
return custom_error!("Unsupported platform target.");
}
}
let temp = gen_page(&crate_config.dioxus_config, false);
let mut file = std::fs::File::create(
crate_config
.crate_dir
.join(
crate_config
.dioxus_config
.application
.out_dir
.clone()
.unwrap_or_else(|| PathBuf::from("dist")),
)
.join("index.html"),
)?;
file.write_all(temp.as_bytes())?;
let _ = PluginManager::on_build_finish(&crate_config, &platform);
Ok(())
}
}

View file

@ -0,0 +1,96 @@
use super::*;
/// Config options for the build system.
#[derive(Clone, Debug, Default, Deserialize, Parser)]
pub struct ConfigOptsBuild {
/// The index HTML file to drive the bundling process [default: index.html]
#[arg(long)]
pub target: Option<PathBuf>,
/// Build in release mode [default: false]
#[clap(long)]
#[serde(default)]
pub release: bool,
// Use verbose output [default: false]
#[clap(long)]
#[serde(default)]
pub verbose: bool,
/// Build a example [default: ""]
#[clap(long)]
pub example: Option<String>,
/// Build with custom profile
#[clap(long)]
pub profile: Option<String>,
/// Build platform: support Web & Desktop [default: "default_platform"]
#[clap(long)]
pub platform: Option<String>,
/// Space separated list of features to activate
#[clap(long)]
pub features: Option<Vec<String>>,
}
#[derive(Clone, Debug, Default, Deserialize, Parser)]
pub struct ConfigOptsServe {
/// The index HTML file to drive the bundling process [default: index.html]
#[arg(short, long)]
pub target: Option<PathBuf>,
/// Port of dev server
#[clap(long)]
#[clap(default_value_t = 8080)]
pub port: u16,
/// Open the app in the default browser [default: false]
#[clap(long)]
#[serde(default)]
pub open: bool,
/// Build a example [default: ""]
#[clap(long)]
pub example: Option<String>,
/// Build in release mode [default: false]
#[clap(long)]
#[serde(default)]
pub release: bool,
// Use verbose output [default: false]
#[clap(long)]
#[serde(default)]
pub verbose: bool,
/// Build with custom profile
#[clap(long)]
pub profile: Option<String>,
/// Build platform: support Web & Desktop [default: "default_platform"]
#[clap(long)]
pub platform: Option<String>,
/// Build with hot reloading rsx [default: false]
#[clap(long)]
#[serde(default)]
pub hot_reload: bool,
/// Set cross-origin-policy to same-origin [default: false]
#[clap(name = "cross-origin-policy")]
#[clap(long)]
#[serde(default)]
pub cross_origin_policy: bool,
/// Space separated list of features to activate
#[clap(long)]
pub features: Option<Vec<String>>,
}
/// Ensure the given value for `--public-url` is formatted correctly.
pub fn parse_public_url(val: &str) -> String {
let prefix = if !val.starts_with('/') { "/" } else { "" };
let suffix = if !val.ends_with('/') { "/" } else { "" };
format!("{}{}{}", prefix, val, suffix)
}

View file

@ -0,0 +1,33 @@
use super::*;
/// Build the Rust WASM app and all of its assets.
#[derive(Clone, Debug, Parser)]
#[clap(name = "clean")]
pub struct Clean {}
impl Clean {
pub fn clean(self) -> Result<()> {
let crate_config = crate::CrateConfig::new()?;
let output = Command::new("cargo")
.arg("clean")
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()?;
if !output.status.success() {
return custom_error!("Cargo clean failed.");
}
let out_dir = crate_config
.dioxus_config
.application
.out_dir
.unwrap_or_else(|| PathBuf::from("dist"));
if crate_config.crate_dir.join(&out_dir).is_dir() {
remove_dir_all(crate_config.crate_dir.join(&out_dir))?;
}
Ok(())
}
}

View file

@ -0,0 +1,63 @@
use super::*;
/// Build the Rust WASM app and all of its assets.
#[derive(Clone, Debug, Deserialize, Subcommand)]
#[clap(name = "config")]
pub enum Config {
/// Init `Dioxus.toml` for project/folder.
Init {
/// Init project name
name: String,
/// Cover old config
#[clap(long)]
#[serde(default)]
force: bool,
/// Project default platform
#[clap(long, default_value = "web")]
platform: String,
},
/// Format print Dioxus config.
FormatPrint {},
/// Create a custom html file.
CustomHtml {},
}
impl Config {
pub fn config(self) -> Result<()> {
let crate_root = crate::cargo::crate_root()?;
match self {
Config::Init {
name,
force,
platform,
} => {
let conf_path = crate_root.join("Dioxus.toml");
if conf_path.is_file() && !force {
log::warn!(
"config file `Dioxus.toml` already exist, use `--force` to overwrite it."
);
return Ok(());
}
let mut file = File::create(conf_path)?;
let content = String::from(include_str!("../../assets/dioxus.toml"))
.replace("{{project-name}}", &name)
.replace("{{default-platform}}", &platform);
file.write_all(content.as_bytes())?;
log::info!("🚩 Init config file completed.");
}
Config::FormatPrint {} => {
println!("{:#?}", crate::CrateConfig::new()?.dioxus_config);
}
Config::CustomHtml {} => {
let html_path = crate_root.join("index.html");
let mut file = File::create(html_path)?;
let content = include_str!("../../assets/index.html");
file.write_all(content.as_bytes())?;
log::info!("🚩 Create custom html file done.");
}
}
Ok(())
}
}

View file

@ -0,0 +1,76 @@
use super::*;
use cargo_generate::{GenerateArgs, TemplatePath};
#[derive(Clone, Debug, Default, Deserialize, Parser)]
#[clap(name = "create")]
pub struct Create {
/// Template path
#[clap(default_value = "gh:dioxuslabs/dioxus-template", long)]
template: String,
}
impl Create {
pub fn create(self) -> Result<()> {
let mut args = GenerateArgs::default();
args.template_path = TemplatePath {
auto_path: Some(self.template),
..Default::default()
};
let path = cargo_generate::generate(args)?;
// first run cargo fmt
let mut cmd = Command::new("cargo");
let cmd = cmd.arg("fmt").current_dir(&path);
let output = cmd.output().expect("failed to execute process");
if !output.status.success() {
log::error!("cargo fmt failed");
log::error!("stdout: {}", String::from_utf8_lossy(&output.stdout));
log::error!("stderr: {}", String::from_utf8_lossy(&output.stderr));
}
// then format the toml
let toml_paths = [path.join("Cargo.toml"), path.join("Dioxus.toml")];
for toml_path in &toml_paths {
let toml = std::fs::read_to_string(toml_path)?;
let mut toml = toml.parse::<toml_edit::Document>().map_err(|e| {
anyhow::anyhow!(
"failed to parse toml at {}: {}",
toml_path.display(),
e.to_string()
)
})?;
toml.as_table_mut().fmt();
let as_string = toml.to_string();
let new_string = remove_tripple_newlines(&as_string);
let mut file = std::fs::File::create(toml_path)?;
file.write_all(new_string.as_bytes())?;
}
// remove any tripple newlines from the readme
let readme_path = path.join("README.md");
let readme = std::fs::read_to_string(&readme_path)?;
let new_readme = remove_tripple_newlines(&readme);
let mut file = std::fs::File::create(readme_path)?;
file.write_all(new_readme.as_bytes())?;
log::info!("Generated project at {}", path.display());
Ok(())
}
}
fn remove_tripple_newlines(string: &str) -> String {
let mut new_string = String::new();
for char in string.chars() {
if char == '\n' {
if new_string.ends_with("\n\n") {
continue;
}
}
new_string.push(char);
}
new_string
}

View file

@ -0,0 +1,89 @@
pub mod autoformat;
pub mod build;
pub mod cfg;
pub mod clean;
pub mod config;
pub mod create;
pub mod plugin;
pub mod serve;
pub mod translate;
pub mod version;
use crate::{
cfg::{ConfigOptsBuild, ConfigOptsServe},
custom_error,
error::Result,
gen_page, server, CrateConfig, Error,
};
use clap::{Parser, Subcommand};
use html_parser::Dom;
use serde::Deserialize;
use std::{
fs::{remove_dir_all, File},
io::{Read, Write},
path::PathBuf,
process::{Command, Stdio},
};
/// Build, Bundle & Ship Dioxus Apps.
#[derive(Parser)]
#[clap(name = "dioxus", version)]
pub struct Cli {
#[clap(subcommand)]
pub action: Commands,
/// Enable verbose logging.
#[clap(short)]
pub v: bool,
}
#[derive(Parser)]
pub enum Commands {
/// Build the Rust WASM app and all of its assets.
Build(build::Build),
/// Translate some source file into Dioxus code.
Translate(translate::Translate),
/// Build, watch & serve the Rust WASM app and all of its assets.
Serve(serve::Serve),
/// Init a new project for Dioxus.
Create(create::Create),
/// Clean output artifacts.
Clean(clean::Clean),
/// Print the version of this extension
#[clap(name = "version")]
Version(version::Version),
/// Format some rsx
#[clap(name = "fmt")]
Autoformat(autoformat::Autoformat),
/// Dioxus config file controls.
#[clap(subcommand)]
Config(config::Config),
/// Manage plugins for dioxus cli
#[clap(subcommand)]
Plugin(plugin::Plugin),
}
impl Commands {
pub fn to_string(&self) -> String {
match self {
Commands::Build(_) => "build",
Commands::Translate(_) => "translate",
Commands::Serve(_) => "serve",
Commands::Create(_) => "create",
Commands::Clean(_) => "clean",
Commands::Config(_) => "config",
Commands::Plugin(_) => "plugin",
Commands::Version(_) => "version",
Commands::Autoformat(_) => "fmt",
}
.to_string()
}
}

View file

@ -0,0 +1,37 @@
use super::*;
/// Build the Rust WASM app and all of its assets.
#[derive(Clone, Debug, Deserialize, Subcommand)]
#[clap(name = "plugin")]
pub enum Plugin {
/// Return all dioxus-cli support tools.
List {},
/// Get default app install path.
AppPath {},
/// Install a new tool.
Add { name: String },
}
impl Plugin {
pub async fn plugin(self) -> Result<()> {
match self {
Plugin::List {} => {
for item in crate::plugin::PluginManager::plugin_list() {
println!("- {item}");
}
}
Plugin::AppPath {} => {
let plugin_dir = crate::plugin::PluginManager::init_plugin_dir();
if let Some(v) = plugin_dir.to_str() {
println!("{}", v);
} else {
log::error!("Plugin path get failed.");
}
}
Plugin::Add { name: _ } => {
log::info!("You can use `dioxus plugin app-path` to get Installation position");
}
}
Ok(())
}
}

View file

@ -0,0 +1,100 @@
use super::*;
use std::{
fs::create_dir_all,
io::Write,
path::PathBuf,
process::{Command, Stdio},
};
/// Run the WASM project on dev-server
#[derive(Clone, Debug, Parser)]
#[clap(name = "serve")]
pub struct Serve {
#[clap(flatten)]
pub serve: ConfigOptsServe,
}
impl Serve {
pub async fn serve(self) -> Result<()> {
let mut crate_config = crate::CrateConfig::new()?;
// change the relase state.
crate_config.with_hot_reload(self.serve.hot_reload);
crate_config.with_cross_origin_policy(self.serve.cross_origin_policy);
crate_config.with_release(self.serve.release);
crate_config.with_verbose(self.serve.verbose);
if self.serve.example.is_some() {
crate_config.as_example(self.serve.example.unwrap());
}
if self.serve.profile.is_some() {
crate_config.set_profile(self.serve.profile.unwrap());
}
if self.serve.features.is_some() {
crate_config.set_features(self.serve.features.unwrap());
}
// Subdirectories don't work with the server
crate_config.dioxus_config.web.app.base_path = None;
let platform = self.serve.platform.unwrap_or_else(|| {
crate_config
.dioxus_config
.application
.default_platform
.clone()
});
if platform.as_str() == "desktop" {
crate::builder::build_desktop(&crate_config, true)?;
match &crate_config.executable {
crate::ExecutableType::Binary(name)
| crate::ExecutableType::Lib(name)
| crate::ExecutableType::Example(name) => {
let mut file = crate_config.out_dir.join(name);
if cfg!(windows) {
file.set_extension("exe");
}
Command::new(file.to_str().unwrap())
.stdout(Stdio::inherit())
.output()?;
}
}
return Ok(());
} else if platform != "web" {
return custom_error!("Unsupported platform target.");
}
// generate dev-index page
Serve::regen_dev_page(&crate_config)?;
// start the develop server
server::startup(self.serve.port, crate_config.clone(), self.serve.open).await?;
Ok(())
}
pub fn regen_dev_page(crate_config: &CrateConfig) -> Result<()> {
let serve_html = gen_page(&crate_config.dioxus_config, true);
let dist_path = crate_config.crate_dir.join(
crate_config
.dioxus_config
.application
.out_dir
.clone()
.unwrap_or_else(|| PathBuf::from("dist")),
);
if !dist_path.is_dir() {
create_dir_all(&dist_path)?;
}
let index_path = dist_path.join("index.html");
let mut file = std::fs::File::create(index_path)?;
file.write_all(serve_html.as_bytes())?;
Ok(())
}
}

View file

@ -0,0 +1,64 @@
use crate::tools;
use super::*;
/// Build the Rust WASM app and all of its assets.
#[derive(Clone, Debug, Deserialize, Subcommand)]
#[clap(name = "tool")]
pub enum Tool {
/// Return all dioxus-cli support tools.
List {},
/// Get default app install path.
AppPath {},
/// Install a new tool.
Add { name: String },
}
impl Tool {
pub async fn tool(self) -> Result<()> {
match self {
Tool::List {} => {
for item in tools::tool_list() {
if tools::Tool::from_str(item).unwrap().is_installed() {
println!("- {item} [installed]");
} else {
println!("- {item}");
}
}
}
Tool::AppPath {} => {
if let Some(v) = tools::tools_path().to_str() {
println!("{}", v);
} else {
return custom_error!("Tools path get failed.");
}
}
Tool::Add { name } => {
let tool_list = tools::tool_list();
if !tool_list.contains(&name.as_str()) {
return custom_error!("Tool {name} not found.");
}
let target_tool = tools::Tool::from_str(&name).unwrap();
if target_tool.is_installed() {
log::warn!("Tool {name} is installed.");
return Ok(());
}
log::info!("Start to download tool package...");
if let Err(e) = target_tool.download_package().await {
return custom_error!("Tool download failed: {e}");
}
log::info!("Start to install tool package...");
if let Err(e) = target_tool.install_package().await {
return custom_error!("Tool install failed: {e}");
}
log::info!("Tool {name} installed successfully!");
}
}
Ok(())
}
}

View file

@ -0,0 +1,138 @@
use std::process::exit;
use dioxus_rsx::{BodyNode, CallBody};
use super::*;
/// Build the Rust WASM app and all of its assets.
#[derive(Clone, Debug, Parser)]
#[clap(name = "translate")]
pub struct Translate {
/// Activate debug mode
// short and long flags (-d, --debug) will be deduced from the field's name
#[clap(short, long)]
pub component: bool,
/// Input file
#[clap(short, long)]
pub file: Option<String>,
/// Input file
#[clap(short, long)]
pub raw: Option<String>,
/// Output file, stdout if not present
#[arg(short, long)]
pub output: Option<PathBuf>,
}
impl Translate {
pub fn translate(self) -> Result<()> {
// Get the right input for the translation
let contents = determine_input(self.file, self.raw)?;
// Ensure we're loading valid HTML
let dom = html_parser::Dom::parse(&contents)?;
// Convert the HTML to RSX
let out = convert_html_to_formatted_rsx(&dom, self.component);
// Write the output
match self.output {
Some(output) => std::fs::write(&output, out)?,
None => print!("{}", out),
}
Ok(())
}
}
pub fn convert_html_to_formatted_rsx(dom: &Dom, component: bool) -> String {
let callbody = rsx_rosetta::rsx_from_html(&dom);
match component {
true => write_callbody_with_icon_section(callbody),
false => dioxus_autofmt::write_block_out(callbody).unwrap(),
}
}
fn write_callbody_with_icon_section(mut callbody: CallBody) -> String {
let mut svgs = vec![];
rsx_rosetta::collect_svgs(&mut callbody.roots, &mut svgs);
let mut out = write_component_body(dioxus_autofmt::write_block_out(callbody).unwrap());
if !svgs.is_empty() {
write_svg_section(&mut out, svgs);
}
out
}
fn write_component_body(raw: String) -> String {
let mut out = String::from("fn component(cx: Scope) -> Element {\n cx.render(rsx! {");
indent_and_write(&raw, 1, &mut out);
out.push_str(" })\n}");
out
}
fn write_svg_section(out: &mut String, svgs: Vec<BodyNode>) {
out.push_str("\n\nmod icons {");
out.push_str("\n use super::*;");
for (idx, icon) in svgs.into_iter().enumerate() {
let raw = dioxus_autofmt::write_block_out(CallBody { roots: vec![icon] }).unwrap();
out.push_str("\n\n pub fn icon_");
out.push_str(&idx.to_string());
out.push_str("(cx: Scope) -> Element {\n cx.render(rsx! {");
indent_and_write(&raw, 2, out);
out.push_str(" })\n }");
}
out.push_str("\n}");
}
fn indent_and_write(raw: &str, idx: usize, out: &mut String) {
for line in raw.lines() {
for _ in 0..idx {
out.push_str(" ");
}
out.push_str(line);
out.push('\n');
}
}
fn determine_input(file: Option<String>, raw: Option<String>) -> Result<String> {
// Make sure not both are specified
if file.is_some() && raw.is_some() {
log::error!("Only one of --file or --raw should be specified.");
exit(0);
}
if let Some(raw) = raw {
return Ok(raw);
}
if let Some(file) = file {
return Ok(std::fs::read_to_string(&file)?);
}
// If neither exist, we try to read from stdin
if atty::is(atty::Stream::Stdin) {
return custom_error!("No input file, source, or stdin to translate from.");
}
let mut buffer = String::new();
std::io::stdin().read_to_string(&mut buffer).unwrap();
Ok(buffer.trim().to_string())
}
#[test]
fn generates_svgs() {
let st = include_str!("../../../tests/svg.html");
let out = convert_html_to_formatted_rsx(&html_parser::Dom::parse(st).unwrap(), true);
println!("{}", out);
}

View file

@ -0,0 +1,76 @@
use super::*;
/// Build the Rust WASM app and all of its assets.
#[derive(Clone, Debug, Parser)]
#[clap(name = "version")]
pub struct Version {}
impl Version {
pub fn version(self) -> VersionInfo {
version()
}
}
use std::fmt;
/// Information about the git repository where rust-analyzer was built from.
pub struct CommitInfo {
pub short_commit_hash: &'static str,
pub commit_hash: &'static str,
pub commit_date: &'static str,
}
/// Cargo's version.
pub struct VersionInfo {
/// rust-analyzer's version, such as "1.57.0", "1.58.0-beta.1", "1.59.0-nightly", etc.
pub version: &'static str,
/// The release channel we were built for (stable/beta/nightly/dev).
///
/// `None` if not built via rustbuild.
pub release_channel: Option<&'static str>,
/// Information about the Git repository we may have been built from.
///
/// `None` if not built from a git repo.
pub commit_info: Option<CommitInfo>,
}
impl fmt::Display for VersionInfo {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.version)?;
if let Some(ci) = &self.commit_info {
write!(f, " ({} {})", ci.short_commit_hash, ci.commit_date)?;
};
Ok(())
}
}
/// Returns information about cargo's version.
pub const fn version() -> VersionInfo {
let version = match option_env!("CFG_RELEASE") {
Some(x) => x,
None => "0.0.0",
};
let release_channel = option_env!("CFG_RELEASE_CHANNEL");
let commit_info = match (
option_env!("RA_COMMIT_SHORT_HASH"),
option_env!("RA_COMMIT_HASH"),
option_env!("RA_COMMIT_DATE"),
) {
(Some(short_commit_hash), Some(commit_hash), Some(commit_date)) => Some(CommitInfo {
short_commit_hash,
commit_hash,
commit_date,
}),
_ => None,
};
VersionInfo {
version,
release_channel,
commit_info,
}
}

264
packages/cli/src/config.rs Normal file
View file

@ -0,0 +1,264 @@
use crate::error::Result;
use serde::{Deserialize, Serialize};
use std::{collections::HashMap, path::PathBuf};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DioxusConfig {
pub application: ApplicationConfig,
pub web: WebConfig,
#[serde(default = "default_plugin")]
pub plugin: toml::Value,
}
fn default_plugin() -> toml::Value {
toml::Value::Boolean(true)
}
impl DioxusConfig {
pub fn load() -> crate::error::Result<Option<DioxusConfig>> {
let Ok(crate_dir) = crate::cargo::crate_root() else { return Ok(None); };
// we support either `Dioxus.toml` or `Cargo.toml`
let Some(dioxus_conf_file) = acquire_dioxus_toml(crate_dir) else {
return Ok(None);
};
toml::from_str::<DioxusConfig>(&std::fs::read_to_string(dioxus_conf_file)?)
.map_err(|_| crate::Error::Unique("Dioxus.toml parse failed".into()))
.map(Some)
}
}
fn acquire_dioxus_toml(dir: PathBuf) -> Option<PathBuf> {
// prefer uppercase
if dir.join("Dioxus.toml").is_file() {
return Some(dir.join("Dioxus.toml"));
}
// lowercase is fine too
if dir.join("dioxus.toml").is_file() {
return Some(dir.join("Dioxus.toml"));
}
None
}
impl Default for DioxusConfig {
fn default() -> Self {
Self {
application: ApplicationConfig {
name: "dioxus".into(),
default_platform: "web".to_string(),
out_dir: Some(PathBuf::from("dist")),
asset_dir: Some(PathBuf::from("public")),
tools: None,
sub_package: None,
},
web: WebConfig {
app: WebAppConfig {
title: Some("dioxus | ⛺".into()),
base_path: None,
},
proxy: Some(vec![]),
watcher: WebWatcherConfig {
watch_path: Some(vec![PathBuf::from("src")]),
reload_html: Some(false),
index_on_404: Some(true),
},
resource: WebResourceConfig {
dev: WebDevResourceConfig {
style: Some(vec![]),
script: Some(vec![]),
},
style: Some(vec![]),
script: Some(vec![]),
},
},
plugin: toml::Value::Table(toml::map::Map::new()),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ApplicationConfig {
pub name: String,
pub default_platform: String,
pub out_dir: Option<PathBuf>,
pub asset_dir: Option<PathBuf>,
pub tools: Option<HashMap<String, toml::Value>>,
pub sub_package: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WebConfig {
pub app: WebAppConfig,
pub proxy: Option<Vec<WebProxyConfig>>,
pub watcher: WebWatcherConfig,
pub resource: WebResourceConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WebAppConfig {
pub title: Option<String>,
pub base_path: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WebProxyConfig {
pub backend: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WebWatcherConfig {
pub watch_path: Option<Vec<PathBuf>>,
pub reload_html: Option<bool>,
pub index_on_404: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WebResourceConfig {
pub dev: WebDevResourceConfig,
pub style: Option<Vec<PathBuf>>,
pub script: Option<Vec<PathBuf>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WebDevResourceConfig {
pub style: Option<Vec<PathBuf>>,
pub script: Option<Vec<PathBuf>>,
}
#[derive(Debug, Clone)]
pub struct CrateConfig {
pub out_dir: PathBuf,
pub crate_dir: PathBuf,
pub workspace_dir: PathBuf,
pub target_dir: PathBuf,
pub asset_dir: PathBuf,
pub manifest: cargo_toml::Manifest<cargo_toml::Value>,
pub executable: ExecutableType,
pub dioxus_config: DioxusConfig,
pub release: bool,
pub hot_reload: bool,
pub cross_origin_policy: bool,
pub verbose: bool,
pub custom_profile: Option<String>,
pub features: Option<Vec<String>>,
}
#[derive(Debug, Clone)]
pub enum ExecutableType {
Binary(String),
Lib(String),
Example(String),
}
impl CrateConfig {
pub fn new() -> Result<Self> {
let dioxus_config = DioxusConfig::load()?.unwrap_or_default();
let crate_dir = if let Some(package) = &dioxus_config.application.sub_package {
crate::cargo::crate_root()?.join(package)
} else {
crate::cargo::crate_root()?
};
let meta = crate::cargo::Metadata::get()?;
let workspace_dir = meta.workspace_root;
let target_dir = meta.target_directory;
let out_dir = match dioxus_config.application.out_dir {
Some(ref v) => crate_dir.join(v),
None => crate_dir.join("dist"),
};
let cargo_def = &crate_dir.join("Cargo.toml");
let asset_dir = match dioxus_config.application.asset_dir {
Some(ref v) => crate_dir.join(v),
None => crate_dir.join("public"),
};
let manifest = cargo_toml::Manifest::from_path(&cargo_def).unwrap();
let output_filename = {
match &manifest.package.as_ref().unwrap().default_run {
Some(default_run_target) => {
default_run_target.to_owned()
},
None => {
manifest.bin.iter().find(|b| b.name == manifest.package.as_ref().map(|pkg| pkg.name.clone()))
.or(manifest.bin.iter().find(|b| b.path == Some("src/main.rs".to_owned())))
.or(manifest.bin.first())
.or(manifest.lib.as_ref())
.and_then(|prod| prod.name.clone())
.expect("No executable or library found from cargo metadata.")
}
}
};
let executable = ExecutableType::Binary(output_filename);
let release = false;
let hot_reload = false;
let verbose = false;
let custom_profile = None;
let features = None;
Ok(Self {
out_dir,
crate_dir,
workspace_dir,
target_dir,
asset_dir,
manifest,
executable,
release,
dioxus_config,
hot_reload,
cross_origin_policy: false,
custom_profile,
features,
verbose,
})
}
pub fn as_example(&mut self, example_name: String) -> &mut Self {
self.executable = ExecutableType::Example(example_name);
self
}
pub fn with_release(&mut self, release: bool) -> &mut Self {
self.release = release;
self
}
pub fn with_hot_reload(&mut self, hot_reload: bool) -> &mut Self {
self.hot_reload = hot_reload;
self
}
pub fn with_cross_origin_policy(&mut self, cross_origin_policy: bool) -> &mut Self {
self.cross_origin_policy = cross_origin_policy;
self
}
pub fn with_verbose(&mut self, verbose: bool) -> &mut Self {
self.verbose = verbose;
self
}
pub fn set_profile(&mut self, profile: String) -> &mut Self {
self.custom_profile = Some(profile);
self
}
pub fn set_features(&mut self, features: Vec<String>) -> &mut Self {
self.features = Some(features);
self
}
}

80
packages/cli/src/error.rs Normal file
View file

@ -0,0 +1,80 @@
use thiserror::Error as ThisError;
pub type Result<T, E = Error> = std::result::Result<T, E>;
#[derive(ThisError, Debug)]
pub enum Error {
/// Used when errors need to propogate but are too unique to be typed
#[error("{0}")]
Unique(String),
#[error("I/O Error: {0}")]
IO(#[from] std::io::Error),
#[error("Format Error: {0}")]
FormatError(#[from] std::fmt::Error),
#[error("Format failed: {0}")]
ParseError(String),
#[error("Runtime Error: {0}")]
RuntimeError(String),
#[error("Failed to write error")]
FailedToWrite,
#[error("Build Failed: {0}")]
BuildFailed(String),
#[error("Cargo Error: {0}")]
CargoError(String),
#[error("{0}")]
CustomError(String),
#[error("Invalid proxy URL: {0}")]
InvalidProxy(#[from] hyper::http::uri::InvalidUri),
#[error("Error proxying request: {0}")]
ProxyRequestError(hyper::Error),
#[error(transparent)]
Other(#[from] anyhow::Error),
}
impl From<&str> for Error {
fn from(s: &str) -> Self {
Error::Unique(s.to_string())
}
}
impl From<String> for Error {
fn from(s: String) -> Self {
Error::Unique(s)
}
}
impl From<html_parser::Error> for Error {
fn from(e: html_parser::Error) -> Self {
Self::ParseError(e.to_string())
}
}
impl From<hyper::Error> for Error {
fn from(e: hyper::Error) -> Self {
Self::RuntimeError(e.to_string())
}
}
#[macro_export]
macro_rules! custom_error {
($msg:literal $(,)?) => {
Err(Error::CustomError(format!($msg)))
};
($err:expr $(,)?) => {
Err(Error::from($err))
};
($fmt:expr, $($arg:tt)*) => {
Err(Error::CustomError(format!($fmt, $($arg)*)))
};
}

24
packages/cli/src/lib.rs Normal file
View file

@ -0,0 +1,24 @@
pub const DIOXUS_CLI_VERSION: &str = "0.1.5";
pub mod builder;
pub mod server;
pub mod tools;
pub use builder::*;
pub mod cargo;
pub use cargo::*;
pub mod cli;
pub use cli::*;
pub mod config;
pub use config::*;
pub mod error;
pub use error::*;
pub mod logging;
pub use logging::*;
pub mod plugin;

View file

@ -0,0 +1,35 @@
use fern::colors::{Color, ColoredLevelConfig};
pub fn set_up_logging() {
// configure colors for the whole line
let colors_line = ColoredLevelConfig::new()
.error(Color::Red)
.warn(Color::Yellow)
// we actually don't need to specify the color for debug and info, they are white by default
.info(Color::White)
.debug(Color::White)
// depending on the terminals color scheme, this is the same as the background color
.trace(Color::BrightBlack);
// configure colors for the name of the level.
// since almost all of them are the same as the color for the whole line, we
// just clone `colors_line` and overwrite our changes
let colors_level = colors_line.info(Color::Green);
// here we set up our fern Dispatch
fern::Dispatch::new()
.format(move |out, message, record| {
out.finish(format_args!(
"{color_line}[{level}{color_line}] {message}\x1B[0m",
color_line = format_args!(
"\x1B[{}m",
colors_line.get_color(&record.level()).to_fg_str()
),
level = colors_level.color(record.level()),
message = message,
));
})
.level(log::LevelFilter::Info)
.chain(std::io::stdout())
.apply()
.unwrap();
}

65
packages/cli/src/main.rs Normal file
View file

@ -0,0 +1,65 @@
use anyhow::anyhow;
use clap::Parser;
use dioxus_cli::{plugin::PluginManager, *};
use Commands::*;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let args = Cli::parse();
set_up_logging();
let dioxus_config = DioxusConfig::load()
.map_err(|e| anyhow!("Failed to load `Dioxus.toml` because: {e}"))?
.unwrap_or_else(|| {
log::warn!("You appear to be creating a Dioxus project from scratch; we will use the default config");
DioxusConfig::default()
});
PluginManager::init(dioxus_config.plugin)
.map_err(|e| anyhow!("🚫 Plugin system initialization failed: {e}"))?;
match args.action {
Translate(opts) => opts
.translate()
.map_err(|e| anyhow!("🚫 Translation of HTML into RSX failed: {}", e)),
Build(opts) => opts
.build()
.map_err(|e| anyhow!("🚫 Building project failed: {}", e)),
Clean(opts) => opts
.clean()
.map_err(|e| anyhow!("🚫 Cleaning project failed: {}", e)),
Serve(opts) => opts
.serve()
.await
.map_err(|e| anyhow!("🚫 Serving project failed: {}", e)),
Create(opts) => opts
.create()
.map_err(|e| anyhow!("🚫 Creating new project failed: {}", e)),
Config(opts) => opts
.config()
.map_err(|e| anyhow!("🚫 Configuring new project failed: {}", e)),
Plugin(opts) => opts
.plugin()
.await
.map_err(|e| anyhow!("🚫 Error with plugin: {}", e)),
Autoformat(opts) => opts
.autoformat()
.await
.map_err(|e| anyhow!("🚫 Error autoformatting RSX: {}", e)),
Version(opt) => {
let version = opt.version();
println!("{}", version);
Ok(())
}
}
}

View file

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

View file

@ -0,0 +1,13 @@
use mlua::UserData;
use crate::tools::app_path;
pub struct PluginDirs;
impl UserData for PluginDirs {
fn add_methods<'lua, M: mlua::UserDataMethods<'lua, Self>>(methods: &mut M) {
methods.add_function("plugins_dir", |_, ()| {
let path = app_path().join("plugins");
Ok(path.to_str().unwrap().to_string())
});
}
}

View file

@ -0,0 +1,85 @@
use std::{
fs::{create_dir, create_dir_all, remove_dir_all, File},
io::{Read, Write},
path::PathBuf,
};
use crate::tools::extract_zip;
use flate2::read::GzDecoder;
use mlua::UserData;
use tar::Archive;
pub struct PluginFileSystem;
impl UserData for PluginFileSystem {
fn add_methods<'lua, M: mlua::UserDataMethods<'lua, Self>>(methods: &mut M) {
methods.add_function("create_dir", |_, args: (String, bool)| {
let path = args.0;
let recursive = args.1;
let path = PathBuf::from(path);
if !path.exists() {
let v = if recursive {
create_dir_all(path)
} else {
create_dir(path)
};
return Ok(v.is_ok());
}
Ok(true)
});
methods.add_function("remove_dir", |_, path: String| {
let path = PathBuf::from(path);
let r = remove_dir_all(path);
Ok(r.is_ok())
});
methods.add_function("file_get_content", |_, path: String| {
let path = PathBuf::from(path);
let mut file = std::fs::File::open(path)?;
let mut buffer = String::new();
file.read_to_string(&mut buffer)?;
Ok(buffer)
});
methods.add_function("file_set_content", |_, args: (String, String)| {
let path = args.0;
let content = args.1;
let path = PathBuf::from(path);
let file = std::fs::File::create(path);
if file.is_err() {
return Ok(false);
}
if file.unwrap().write_all(content.as_bytes()).is_err() {
return Ok(false);
}
Ok(true)
});
methods.add_function("unzip_file", |_, args: (String, String)| {
let file = PathBuf::from(args.0);
let target = PathBuf::from(args.1);
let res = extract_zip(&file, &target);
if let Err(_) = res {
return Ok(false);
}
Ok(true)
});
methods.add_function("untar_gz_file", |_, args: (String, String)| {
let file = PathBuf::from(args.0);
let target = PathBuf::from(args.1);
let tar_gz = if let Ok(v) = File::open(file) {
v
} else {
return Ok(false);
};
let tar = GzDecoder::new(tar_gz);
let mut archive = Archive::new(tar);
if archive.unpack(&target).is_err() {
return Ok(false);
}
Ok(true)
});
}
}

View file

@ -0,0 +1,28 @@
use log;
use mlua::UserData;
pub struct PluginLogger;
impl UserData for PluginLogger {
fn add_methods<'lua, M: mlua::UserDataMethods<'lua, Self>>(methods: &mut M) {
methods.add_function("trace", |_, info: String| {
log::trace!("{}", info);
Ok(())
});
methods.add_function("info", |_, info: String| {
log::info!("{}", info);
Ok(())
});
methods.add_function("debug", |_, info: String| {
log::debug!("{}", info);
Ok(())
});
methods.add_function("warn", |_, info: String| {
log::warn!("{}", info);
Ok(())
});
methods.add_function("error", |_, info: String| {
log::error!("{}", info);
Ok(())
});
}
}

View file

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

View file

@ -0,0 +1,27 @@
use std::{io::Cursor, path::PathBuf};
use mlua::UserData;
pub struct PluginNetwork;
impl UserData for PluginNetwork {
fn add_methods<'lua, M: mlua::UserDataMethods<'lua, Self>>(methods: &mut M) {
methods.add_function("download_file", |_, args: (String, String)| {
let url = args.0;
let path = args.1;
let resp = reqwest::blocking::get(url);
if let Ok(resp) = resp {
let mut content = Cursor::new(resp.bytes().unwrap());
let file = std::fs::File::create(PathBuf::from(path));
if file.is_err() {
return Ok(false);
}
let mut file = file.unwrap();
let res = std::io::copy(&mut content, &mut file);
return Ok(res.is_ok());
}
Ok(false)
});
}
}

View file

@ -0,0 +1,18 @@
use mlua::UserData;
pub struct PluginOS;
impl UserData for PluginOS {
fn add_methods<'lua, M: mlua::UserDataMethods<'lua, Self>>(methods: &mut M) {
methods.add_function("current_platform", |_, ()| {
if cfg!(target_os = "windows") {
Ok("windows")
} else if cfg!(target_os = "macos") {
Ok("macos")
} else if cfg!(target_os = "linux") {
Ok("linux")
} else {
panic!("unsupported platformm");
}
});
}
}

View file

@ -0,0 +1,40 @@
use std::path::PathBuf;
use mlua::{UserData, Variadic};
pub struct PluginPath;
impl UserData for PluginPath {
fn add_methods<'lua, M: mlua::UserDataMethods<'lua, Self>>(methods: &mut M) {
// join function
methods.add_function("join", |_, args: Variadic<String>| {
let mut path = PathBuf::new();
for i in args {
path = path.join(i);
}
Ok(path.to_str().unwrap().to_string())
});
// parent function
methods.add_function("parent", |_, path: String| {
let current_path = PathBuf::from(&path);
let parent = current_path.parent();
if parent.is_none() {
return Ok(path);
} else {
return Ok(parent.unwrap().to_str().unwrap().to_string());
}
});
methods.add_function("exists", |_, path: String| {
let path = PathBuf::from(path);
Ok(path.exists())
});
methods.add_function("is_dir", |_, path: String| {
let path = PathBuf::from(path);
Ok(path.is_dir())
});
methods.add_function("is_file", |_, path: String| {
let path = PathBuf::from(path);
Ok(path.is_file())
});
}
}

View file

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

View file

@ -0,0 +1,138 @@
use std::collections::HashMap;
use mlua::ToLua;
#[derive(Debug, Clone)]
pub struct PluginConfig {
pub available: bool,
pub loader: Vec<String>,
pub config_info: HashMap<String, HashMap<String, Value>>,
}
impl<'lua> ToLua<'lua> for PluginConfig {
fn to_lua(self, lua: &'lua mlua::Lua) -> mlua::Result<mlua::Value<'lua>> {
let table = lua.create_table()?;
table.set("available", self.available)?;
table.set("loader", self.loader)?;
let config_info = lua.create_table()?;
for (name, data) in self.config_info {
config_info.set(name, data)?;
}
table.set("config_info", config_info)?;
Ok(mlua::Value::Table(table))
}
}
impl PluginConfig {
pub fn from_toml_value(val: toml::Value) -> Self {
if let toml::Value::Table(tab) = val {
let available = tab
.get::<_>("available")
.unwrap_or(&toml::Value::Boolean(true));
let available = available.as_bool().unwrap_or(true);
let mut loader = vec![];
if let Some(origin) = tab.get("loader") {
if origin.is_array() {
for i in origin.as_array().unwrap() {
loader.push(i.as_str().unwrap_or_default().to_string());
}
}
}
let mut config_info = HashMap::new();
for (name, value) in tab {
if name == "available" || name == "loader" {
continue;
}
if let toml::Value::Table(value) = value {
let mut map = HashMap::new();
for (item, info) in value {
map.insert(item, Value::from_toml(info));
}
config_info.insert(name, map);
}
}
Self {
available,
loader,
config_info,
}
} else {
Self {
available: false,
loader: vec![],
config_info: HashMap::new(),
}
}
}
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub enum Value {
String(String),
Integer(i64),
Float(f64),
Boolean(bool),
Array(Vec<Value>),
Table(HashMap<String, Value>),
}
impl Value {
pub fn from_toml(origin: toml::Value) -> Self {
match origin {
cargo_toml::Value::String(s) => Value::String(s),
cargo_toml::Value::Integer(i) => Value::Integer(i),
cargo_toml::Value::Float(f) => Value::Float(f),
cargo_toml::Value::Boolean(b) => Value::Boolean(b),
cargo_toml::Value::Datetime(d) => Value::String(d.to_string()),
cargo_toml::Value::Array(a) => {
let mut v = vec![];
for i in a {
v.push(Value::from_toml(i));
}
Value::Array(v)
}
cargo_toml::Value::Table(t) => {
let mut h = HashMap::new();
for (n, v) in t {
h.insert(n, Value::from_toml(v));
}
Value::Table(h)
}
}
}
}
impl<'lua> ToLua<'lua> for Value {
fn to_lua(self, lua: &'lua mlua::Lua) -> mlua::Result<mlua::Value<'lua>> {
Ok(match self {
Value::String(s) => mlua::Value::String(lua.create_string(&s)?),
Value::Integer(i) => mlua::Value::Integer(i),
Value::Float(f) => mlua::Value::Number(f),
Value::Boolean(b) => mlua::Value::Boolean(b),
Value::Array(a) => {
let table = lua.create_table()?;
for (i, v) in a.iter().enumerate() {
table.set(i, v.clone())?;
}
mlua::Value::Table(table)
}
Value::Table(t) => {
let table = lua.create_table()?;
for (i, v) in t.iter() {
table.set(i.clone(), v.clone())?;
}
mlua::Value::Table(table)
}
})
}
}

View file

@ -0,0 +1,752 @@
use crate::{builder, plugin::PluginManager, serve::Serve, BuildResult, CrateConfig, Result};
use axum::{
body::{Full, HttpBody},
extract::{ws::Message, Extension, TypedHeader, WebSocketUpgrade},
http::{
header::{HeaderName, HeaderValue},
Method, Response, StatusCode,
},
response::IntoResponse,
routing::{get, get_service},
Router,
};
use cargo_metadata::diagnostic::Diagnostic;
use colored::Colorize;
use dioxus_core::Template;
use dioxus_html::HtmlCtx;
use dioxus_rsx::hot_reload::*;
use notify::{RecommendedWatcher, Watcher};
use std::{
net::UdpSocket,
path::PathBuf,
process::Command,
sync::{Arc, Mutex},
};
use tokio::sync::broadcast;
use tower::ServiceBuilder;
use tower_http::services::fs::{ServeDir, ServeFileSystemResponseBody};
use tower_http::{
cors::{Any, CorsLayer},
ServiceBuilderExt,
};
mod proxy;
pub struct BuildManager {
config: CrateConfig,
reload_tx: broadcast::Sender<()>,
}
impl BuildManager {
fn rebuild(&self) -> Result<BuildResult> {
log::info!("🪁 Rebuild project");
let result = builder::build(&self.config, true)?;
// change the websocket reload state to true;
// the page will auto-reload.
if self
.config
.dioxus_config
.web
.watcher
.reload_html
.unwrap_or(false)
{
let _ = Serve::regen_dev_page(&self.config);
}
let _ = self.reload_tx.send(());
Ok(result)
}
}
struct WsReloadState {
update: broadcast::Sender<()>,
}
pub async fn startup(port: u16, config: CrateConfig, start_browser: bool) -> Result<()> {
// ctrl-c shutdown checker
let crate_config = config.clone();
let _ = ctrlc::set_handler(move || {
let _ = PluginManager::on_serve_shutdown(&crate_config);
std::process::exit(0);
});
let ip = get_ip().unwrap_or(String::from("0.0.0.0"));
if config.hot_reload {
startup_hot_reload(ip, port, config, start_browser).await?
} else {
startup_default(ip, port, config, start_browser).await?
}
Ok(())
}
pub struct HotReloadState {
pub messages: broadcast::Sender<Template<'static>>,
pub build_manager: Arc<BuildManager>,
pub file_map: Arc<Mutex<FileMap<HtmlCtx>>>,
pub watcher_config: CrateConfig,
}
pub async fn hot_reload_handler(
ws: WebSocketUpgrade,
_: Option<TypedHeader<headers::UserAgent>>,
Extension(state): Extension<Arc<HotReloadState>>,
) -> impl IntoResponse {
ws.on_upgrade(|mut socket| async move {
log::info!("🔥 Hot Reload WebSocket connected");
{
// update any rsx calls that changed before the websocket connected.
{
log::info!("🔮 Finding updates since last compile...");
let templates: Vec<_> = {
state
.file_map
.lock()
.unwrap()
.map
.values()
.filter_map(|(_, template_slot)| *template_slot)
.collect()
};
for template in templates {
if socket
.send(Message::Text(serde_json::to_string(&template).unwrap()))
.await
.is_err()
{
return;
}
}
}
log::info!("finished");
}
let mut rx = state.messages.subscribe();
loop {
if let Ok(rsx) = rx.recv().await {
if socket
.send(Message::Text(serde_json::to_string(&rsx).unwrap()))
.await
.is_err()
{
break;
};
}
}
})
}
#[allow(unused_assignments)]
pub async fn startup_hot_reload(
ip: String,
port: u16,
config: CrateConfig,
start_browser: bool,
) -> Result<()> {
let first_build_result = crate::builder::build(&config, false)?;
log::info!("🚀 Starting development server...");
PluginManager::on_serve_start(&config)?;
let dist_path = config.out_dir.clone();
let (reload_tx, _) = broadcast::channel(100);
let FileMapBuildResult { map, errors } =
FileMap::<HtmlCtx>::create(config.crate_dir.clone()).unwrap();
for err in errors {
log::error!("{}", err);
}
let file_map = Arc::new(Mutex::new(map));
let build_manager = Arc::new(BuildManager {
config: config.clone(),
reload_tx: reload_tx.clone(),
});
let hot_reload_tx = broadcast::channel(100).0;
let hot_reload_state = Arc::new(HotReloadState {
messages: hot_reload_tx.clone(),
build_manager: build_manager.clone(),
file_map: file_map.clone(),
watcher_config: config.clone(),
});
let crate_dir = config.crate_dir.clone();
let ws_reload_state = Arc::new(WsReloadState {
update: reload_tx.clone(),
});
// file watcher: check file change
let allow_watch_path = config
.dioxus_config
.web
.watcher
.watch_path
.clone()
.unwrap_or_else(|| vec![PathBuf::from("src")]);
let watcher_config = config.clone();
let watcher_ip = ip.clone();
let mut last_update_time = chrono::Local::now().timestamp();
let mut watcher = RecommendedWatcher::new(
move |evt: notify::Result<notify::Event>| {
let config = watcher_config.clone();
// Give time for the change to take effect before reading the file
std::thread::sleep(std::time::Duration::from_millis(100));
if chrono::Local::now().timestamp() > last_update_time {
if let Ok(evt) = evt {
let mut messages: Vec<Template<'static>> = Vec::new();
for path in evt.paths.clone() {
// if this is not a rust file, rebuild the whole project
if path.extension().and_then(|p| p.to_str()) != Some("rs") {
match build_manager.rebuild() {
Ok(res) => {
print_console_info(
&watcher_ip,
port,
&config,
PrettierOptions {
changed: evt.paths,
warnings: res.warnings,
elapsed_time: res.elapsed_time,
},
);
}
Err(err) => {
log::error!("{}", err);
}
}
return;
}
// find changes to the rsx in the file
let mut map = file_map.lock().unwrap();
match map.update_rsx(&path, &crate_dir) {
Ok(UpdateResult::UpdatedRsx(msgs)) => {
messages.extend(msgs);
}
Ok(UpdateResult::NeedsRebuild) => {
match build_manager.rebuild() {
Ok(res) => {
print_console_info(
&watcher_ip,
port,
&config,
PrettierOptions {
changed: evt.paths,
warnings: res.warnings,
elapsed_time: res.elapsed_time,
},
);
}
Err(err) => {
log::error!("{}", err);
}
}
return;
}
Err(err) => {
log::error!("{}", err);
}
}
}
for msg in messages {
let _ = hot_reload_tx.send(msg);
}
}
last_update_time = chrono::Local::now().timestamp();
}
},
notify::Config::default(),
)
.unwrap();
for sub_path in allow_watch_path {
if let Err(err) = watcher.watch(
&config.crate_dir.join(&sub_path),
notify::RecursiveMode::Recursive,
) {
log::error!("error watching {sub_path:?}: \n{}", err);
}
}
// start serve dev-server at 0.0.0.0:8080
print_console_info(
&ip,
port,
&config,
PrettierOptions {
changed: vec![],
warnings: first_build_result.warnings,
elapsed_time: first_build_result.elapsed_time,
},
);
let cors = CorsLayer::new()
// allow `GET` and `POST` when accessing the resource
.allow_methods([Method::GET, Method::POST])
// allow requests from any origin
.allow_origin(Any)
.allow_headers(Any);
let (coep, coop) = if config.cross_origin_policy {
(
HeaderValue::from_static("require-corp"),
HeaderValue::from_static("same-origin"),
)
} else {
(
HeaderValue::from_static("unsafe-none"),
HeaderValue::from_static("unsafe-none"),
)
};
let file_service_config = config.clone();
let file_service = ServiceBuilder::new()
.override_response_header(
HeaderName::from_static("cross-origin-embedder-policy"),
coep,
)
.override_response_header(HeaderName::from_static("cross-origin-opener-policy"), coop)
.and_then(
move |response: Response<ServeFileSystemResponseBody>| async move {
let response = if file_service_config
.dioxus_config
.web
.watcher
.index_on_404
.unwrap_or(false)
&& response.status() == StatusCode::NOT_FOUND
{
let body = Full::from(
// TODO: Cache/memoize this.
std::fs::read_to_string(
file_service_config
.crate_dir
.join(file_service_config.out_dir)
.join("index.html"),
)
.ok()
.unwrap(),
)
.map_err(|err| match err {})
.boxed();
Response::builder()
.status(StatusCode::OK)
.body(body)
.unwrap()
} else {
response.map(|body| body.boxed())
};
Ok(response)
},
)
.service(ServeDir::new(config.crate_dir.join(&dist_path)));
let mut router = Router::new().route("/_dioxus/ws", get(ws_handler));
for proxy_config in config.dioxus_config.web.proxy.unwrap_or_default() {
router = proxy::add_proxy(router, &proxy_config)?;
}
router = router.fallback(get_service(file_service).handle_error(
|error: std::io::Error| async move {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Unhandled internal error: {}", error),
)
},
));
let router = router
.route("/_dioxus/hot_reload", get(hot_reload_handler))
.layer(cors)
.layer(Extension(ws_reload_state))
.layer(Extension(hot_reload_state));
let addr = format!("0.0.0.0:{}", port).parse().unwrap();
let server = axum::Server::bind(&addr).serve(router.into_make_service());
if start_browser {
let _ = open::that(format!("http://{}", addr));
}
server.await?;
Ok(())
}
pub async fn startup_default(
ip: String,
port: u16,
config: CrateConfig,
start_browser: bool,
) -> Result<()> {
let first_build_result = crate::builder::build(&config, false)?;
log::info!("🚀 Starting development server...");
let dist_path = config.out_dir.clone();
let (reload_tx, _) = broadcast::channel(100);
let build_manager = BuildManager {
config: config.clone(),
reload_tx: reload_tx.clone(),
};
let ws_reload_state = Arc::new(WsReloadState {
update: reload_tx.clone(),
});
let mut last_update_time = chrono::Local::now().timestamp();
// file watcher: check file change
let allow_watch_path = config
.dioxus_config
.web
.watcher
.watch_path
.clone()
.unwrap_or_else(|| vec![PathBuf::from("src")]);
let watcher_config = config.clone();
let watcher_ip = ip.clone();
let mut watcher = notify::recommended_watcher(move |info: notify::Result<notify::Event>| {
let config = watcher_config.clone();
if let Ok(e) = info {
if chrono::Local::now().timestamp() > last_update_time {
match build_manager.rebuild() {
Ok(res) => {
last_update_time = chrono::Local::now().timestamp();
print_console_info(
&watcher_ip,
port,
&config,
PrettierOptions {
changed: e.paths.clone(),
warnings: res.warnings,
elapsed_time: res.elapsed_time,
},
);
let _ = PluginManager::on_serve_rebuild(
chrono::Local::now().timestamp(),
e.paths,
);
}
Err(e) => log::error!("{}", e),
}
}
}
})
.unwrap();
for sub_path in allow_watch_path {
watcher
.watch(
&config.crate_dir.join(sub_path),
notify::RecursiveMode::Recursive,
)
.unwrap();
}
// start serve dev-server at 0.0.0.0
print_console_info(
&ip,
port,
&config,
PrettierOptions {
changed: vec![],
warnings: first_build_result.warnings,
elapsed_time: first_build_result.elapsed_time,
},
);
PluginManager::on_serve_start(&config)?;
let cors = CorsLayer::new()
// allow `GET` and `POST` when accessing the resource
.allow_methods([Method::GET, Method::POST])
// allow requests from any origin
.allow_origin(Any)
.allow_headers(Any);
let (coep, coop) = if config.cross_origin_policy {
(
HeaderValue::from_static("require-corp"),
HeaderValue::from_static("same-origin"),
)
} else {
(
HeaderValue::from_static("unsafe-none"),
HeaderValue::from_static("unsafe-none"),
)
};
let file_service_config = config.clone();
let file_service = ServiceBuilder::new()
.override_response_header(
HeaderName::from_static("cross-origin-embedder-policy"),
coep,
)
.override_response_header(HeaderName::from_static("cross-origin-opener-policy"), coop)
.and_then(
move |response: Response<ServeFileSystemResponseBody>| async move {
let response = if file_service_config
.dioxus_config
.web
.watcher
.index_on_404
.unwrap_or(false)
&& response.status() == StatusCode::NOT_FOUND
{
let body = Full::from(
// TODO: Cache/memoize this.
std::fs::read_to_string(
file_service_config
.crate_dir
.join(file_service_config.out_dir)
.join("index.html"),
)
.ok()
.unwrap(),
)
.map_err(|err| match err {})
.boxed();
Response::builder()
.status(StatusCode::OK)
.body(body)
.unwrap()
} else {
response.map(|body| body.boxed())
};
Ok(response)
},
)
.service(ServeDir::new(config.crate_dir.join(&dist_path)));
let mut router = Router::new().route("/_dioxus/ws", get(ws_handler));
for proxy_config in config.dioxus_config.web.proxy.unwrap_or_default() {
router = proxy::add_proxy(router, &proxy_config)?;
}
router = router
.fallback(
get_service(file_service).handle_error(|error: std::io::Error| async move {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Unhandled internal error: {}", error),
)
}),
)
.layer(cors)
.layer(Extension(ws_reload_state));
let addr = format!("0.0.0.0:{}", port).parse().unwrap();
let server = axum::Server::bind(&addr).serve(router.into_make_service());
if start_browser {
let _ = open::that(format!("http://{}", addr));
}
server.await?;
Ok(())
}
#[derive(Debug, Default)]
pub struct PrettierOptions {
changed: Vec<PathBuf>,
warnings: Vec<Diagnostic>,
elapsed_time: u128,
}
fn print_console_info(ip: &String, port: u16, config: &CrateConfig, options: PrettierOptions) {
if let Ok(native_clearseq) = Command::new(if cfg!(target_os = "windows") {
"cls"
} else {
"clear"
})
.output()
{
print!("{}", String::from_utf8_lossy(&native_clearseq.stdout));
} else {
// Try ANSI-Escape characters
print!("\x1b[2J\x1b[H");
}
// for path in &changed {
// let path = path
// .strip_prefix(crate::crate_root().unwrap())
// .unwrap()
// .to_path_buf();
// log::info!("Updated {}", format!("{}", path.to_str().unwrap()).green());
// }
let mut profile = if config.release { "Release" } else { "Debug" }.to_string();
if config.custom_profile.is_some() {
profile = config.custom_profile.as_ref().unwrap().to_string();
}
let hot_reload = if config.hot_reload { "RSX" } else { "Normal" };
let crate_root = crate::cargo::crate_root().unwrap();
let custom_html_file = if crate_root.join("index.html").is_file() {
"Custom [index.html]"
} else {
"Default"
};
let url_rewrite = if config
.dioxus_config
.web
.watcher
.index_on_404
.unwrap_or(false)
{
"True"
} else {
"False"
};
let proxies = config.dioxus_config.web.proxy.as_ref();
if options.changed.is_empty() {
println!(
"{} @ v{} [{}] \n",
"Dioxus".bold().green(),
crate::DIOXUS_CLI_VERSION,
chrono::Local::now().format("%H:%M:%S").to_string().dimmed()
);
} else {
println!(
"Project Reloaded: {}\n",
format!(
"Changed {} files. [{}]",
options.changed.len(),
chrono::Local::now().format("%H:%M:%S").to_string().dimmed()
)
.purple()
.bold()
);
}
println!(
"\t> Local : {}",
format!("http://localhost:{}/", port).blue()
);
println!(
"\t> Network : {}",
format!("http://{}:{}/", ip, port).blue()
);
println!("");
println!("\t> Profile : {}", profile.green());
println!("\t> Hot Reload : {}", hot_reload.cyan());
if let Some(proxies) = proxies {
if !proxies.is_empty() {
println!("\t> Proxies :");
for proxy in proxies {
println!("\t\t- {}", proxy.backend.blue());
}
}
}
println!("\t> Index Template : {}", custom_html_file.green());
println!("\t> URL Rewrite [index_on_404] : {}", url_rewrite.purple());
println!("");
println!(
"\t> Build Time Use : {} millis",
options.elapsed_time.to_string().green().bold()
);
println!("");
if options.warnings.len() == 0 {
log::info!("{}\n", "A perfect compilation!".green().bold());
} else {
log::warn!(
"{}",
format!(
"There were {} warning messages during the build.",
options.warnings.len() - 1
)
.yellow()
.bold()
);
// for info in &options.warnings {
// let message = info.message.clone();
// if message == format!("{} warnings emitted", options.warnings.len() - 1) {
// continue;
// }
// let mut console = String::new();
// for span in &info.spans {
// let file = &span.file_name;
// let line = (span.line_start, span.line_end);
// let line_str = if line.0 == line.1 {
// line.0.to_string()
// } else {
// format!("{}~{}", line.0, line.1)
// };
// let code = span.text.clone();
// let span_info = if code.len() == 1 {
// let code = code.get(0).unwrap().text.trim().blue().bold().to_string();
// format!(
// "[{}: {}]: '{}' --> {}",
// file,
// line_str,
// code,
// message.yellow().bold()
// )
// } else {
// let code = code
// .iter()
// .enumerate()
// .map(|(_i, s)| format!("\t{}\n", s.text).blue().bold().to_string())
// .collect::<String>();
// format!("[{}: {}]:\n{}\n#:{}", file, line_str, code, message)
// };
// console = format!("{console}\n\t{span_info}");
// }
// println!("{console}");
// }
// println!(
// "\n{}\n",
// "Resolving all warnings will help your code run better!".yellow()
// );
}
}
fn get_ip() -> Option<String> {
let socket = match UdpSocket::bind("0.0.0.0:0") {
Ok(s) => s,
Err(_) => return None,
};
match socket.connect("8.8.8.8:80") {
Ok(()) => (),
Err(_) => return None,
};
match socket.local_addr() {
Ok(addr) => return Some(addr.ip().to_string()),
Err(_) => return None,
};
}
async fn ws_handler(
ws: WebSocketUpgrade,
_: Option<TypedHeader<headers::UserAgent>>,
Extension(state): Extension<Arc<WsReloadState>>,
) -> impl IntoResponse {
ws.on_upgrade(|mut socket| async move {
let mut rx = state.update.subscribe();
let reload_watcher = tokio::spawn(async move {
loop {
rx.recv().await.unwrap();
// ignore the error
if socket
.send(Message::Text(String::from("reload")))
.await
.is_err()
{
break;
}
// flush the errors after recompling
rx = rx.resubscribe();
}
});
reload_watcher.await.unwrap();
})
}

View file

@ -0,0 +1,171 @@
use crate::{Result, WebProxyConfig};
use anyhow::Context;
use axum::{http::StatusCode, routing::any, Router};
use hyper::{Request, Response, Uri};
#[derive(Debug, Clone)]
struct ProxyClient {
inner: hyper::Client<hyper_rustls::HttpsConnector<hyper::client::HttpConnector>>,
url: Uri,
}
impl ProxyClient {
fn new(url: Uri) -> Self {
let https = hyper_rustls::HttpsConnectorBuilder::new()
.with_native_roots()
.https_or_http()
.enable_http1()
.build();
Self {
inner: hyper::Client::builder().build(https),
url,
}
}
async fn send(
&self,
mut req: Request<hyper::body::Body>,
) -> Result<Response<hyper::body::Body>> {
let mut uri_parts = req.uri().clone().into_parts();
uri_parts.authority = self.url.authority().cloned();
uri_parts.scheme = self.url.scheme().cloned();
*req.uri_mut() = Uri::from_parts(uri_parts).context("Invalid URI parts")?;
self.inner
.request(req)
.await
.map_err(crate::error::Error::ProxyRequestError)
}
}
/// Add routes to the router handling the specified proxy config.
///
/// We will proxy requests directed at either:
///
/// - the exact path of the proxy config's backend URL, e.g. /api
/// - the exact path with a trailing slash, e.g. /api/
/// - any subpath of the backend URL, e.g. /api/foo/bar
pub fn add_proxy(mut router: Router, proxy: &WebProxyConfig) -> Result<Router> {
let url: Uri = proxy.backend.parse()?;
let path = url.path().to_string();
let client = ProxyClient::new(url);
// We also match everything after the path using a wildcard matcher.
let wildcard_client = client.clone();
router = router.route(
// Always remove trailing /'s so that the exact route
// matches.
path.trim_end_matches('/'),
any(move |req| async move {
client
.send(req)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))
}),
);
// Wildcard match anything else _after_ the backend URL's path.
// Note that we know `path` ends with a trailing `/` in this branch,
// so `wildcard` will look like `http://localhost/api/*proxywildcard`.
let wildcard = format!("{}/*proxywildcard", path.trim_end_matches('/'));
router = router.route(
&wildcard,
any(move |req| async move {
wildcard_client
.send(req)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))
}),
);
Ok(router)
}
#[cfg(test)]
mod test {
use super::*;
use axum::{extract::Path, Router};
fn setup_servers(
mut config: WebProxyConfig,
) -> (
tokio::task::JoinHandle<()>,
tokio::task::JoinHandle<()>,
String,
) {
let backend_router = Router::new().route(
"/*path",
any(|path: Path<String>| async move { format!("backend: {}", path.0) }),
);
let backend_server = axum::Server::bind(&"127.0.0.1:0".parse().unwrap())
.serve(backend_router.into_make_service());
let backend_addr = backend_server.local_addr();
let backend_handle = tokio::spawn(async move { backend_server.await.unwrap() });
config.backend = format!("http://{}{}", backend_addr, config.backend);
let router = super::add_proxy(Router::new(), &config);
let server = axum::Server::bind(&"127.0.0.1:0".parse().unwrap())
.serve(router.unwrap().into_make_service());
let server_addr = server.local_addr();
let server_handle = tokio::spawn(async move { server.await.unwrap() });
(backend_handle, server_handle, server_addr.to_string())
}
async fn test_proxy_requests(path: String) {
let config = WebProxyConfig {
// Normally this would be an absolute URL including scheme/host/port,
// but in these tests we need to let the OS choose the port so tests
// don't conflict, so we'll concatenate the final address and this
// path together.
// So in day to day usage, use `http://localhost:8000/api` instead!
backend: path,
};
let (backend_handle, server_handle, server_addr) = setup_servers(config);
let resp = hyper::Client::new()
.get(format!("http://{}/api", server_addr).parse().unwrap())
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(
hyper::body::to_bytes(resp.into_body()).await.unwrap(),
"backend: /api"
);
let resp = hyper::Client::new()
.get(format!("http://{}/api/", server_addr).parse().unwrap())
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(
hyper::body::to_bytes(resp.into_body()).await.unwrap(),
"backend: /api/"
);
let resp = hyper::Client::new()
.get(
format!("http://{}/api/subpath", server_addr)
.parse()
.unwrap(),
)
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(
hyper::body::to_bytes(resp.into_body()).await.unwrap(),
"backend: /api/subpath"
);
backend_handle.abort();
server_handle.abort();
}
#[tokio::test]
async fn add_proxy() {
test_proxy_requests("/api".to_string()).await;
}
#[tokio::test]
async fn add_proxy_trailing_slash() {
test_proxy_requests("/api/".to_string()).await;
}
}

349
packages/cli/src/tools.rs Normal file
View file

@ -0,0 +1,349 @@
use std::{
fs::{create_dir_all, File},
io::{ErrorKind, Read, Write},
path::{Path, PathBuf},
process::Command,
};
use anyhow::Context;
use flate2::read::GzDecoder;
use futures::StreamExt;
use tar::Archive;
use tokio::io::AsyncWriteExt;
#[derive(Debug, PartialEq, Eq)]
pub enum Tool {
Binaryen,
Sass,
Tailwind,
}
// pub fn tool_list() -> Vec<&'static str> {
// vec!["binaryen", "sass", "tailwindcss"]
// }
pub fn app_path() -> PathBuf {
let data_local = dirs::data_local_dir().unwrap();
let dioxus_dir = data_local.join("dioxus");
if !dioxus_dir.is_dir() {
create_dir_all(&dioxus_dir).unwrap();
}
dioxus_dir
}
pub fn temp_path() -> PathBuf {
let app_path = app_path();
let temp_path = app_path.join("temp");
if !temp_path.is_dir() {
create_dir_all(&temp_path).unwrap();
}
temp_path
}
pub fn clone_repo(dir: &Path, url: &str) -> anyhow::Result<()> {
let target_dir = dir.parent().unwrap();
let dir_name = dir.file_name().unwrap();
let mut cmd = Command::new("git");
let cmd = cmd.current_dir(target_dir);
let res = cmd.arg("clone").arg(url).arg(dir_name).output();
if let Err(err) = res {
if ErrorKind::NotFound == err.kind() {
log::warn!("Git program not found. Hint: Install git or check $PATH.");
return Err(err.into());
}
}
Ok(())
}
pub fn tools_path() -> PathBuf {
let app_path = app_path();
let temp_path = app_path.join("tools");
if !temp_path.is_dir() {
create_dir_all(&temp_path).unwrap();
}
temp_path
}
#[allow(clippy::should_implement_trait)]
impl Tool {
/// from str to tool enum
pub fn from_str(name: &str) -> Option<Self> {
match name {
"binaryen" => Some(Self::Binaryen),
"sass" => Some(Self::Sass),
"tailwindcss" => Some(Self::Tailwind),
_ => None,
}
}
/// get current tool name str
pub fn name(&self) -> &str {
match self {
Self::Binaryen => "binaryen",
Self::Sass => "sass",
Self::Tailwind => "tailwindcss",
}
}
/// get tool bin dir path
pub fn bin_path(&self) -> &str {
match self {
Self::Binaryen => "bin",
Self::Sass => ".",
Self::Tailwind => ".",
}
}
/// get target platform
pub fn target_platform(&self) -> &str {
match self {
Self::Binaryen => {
if cfg!(target_os = "windows") {
"windows"
} else if cfg!(target_os = "macos") {
"macos"
} else if cfg!(target_os = "linux") {
"linux"
} else {
panic!("unsupported platformm");
}
}
Self::Sass => {
if cfg!(target_os = "windows") {
"windows"
} else if cfg!(target_os = "macos") {
"macos"
} else if cfg!(target_os = "linux") {
"linux"
} else {
panic!("unsupported platformm");
}
}
Self::Tailwind => {
if cfg!(target_os = "windows") {
"windows"
} else if cfg!(target_os = "macos") {
"macos"
} else if cfg!(target_os = "linux") {
"linux"
} else {
panic!("unsupported platformm");
}
}
}
}
/// get tool version
pub fn tool_version(&self) -> &str {
match self {
Self::Binaryen => "version_105",
Self::Sass => "1.51.0",
Self::Tailwind => "v3.1.6",
}
}
/// get tool package download url
pub fn download_url(&self) -> String {
match self {
Self::Binaryen => {
format!(
"https://github.com/WebAssembly/binaryen/releases/download/{version}/binaryen-{version}-x86_64-{target}.tar.gz",
version = self.tool_version(),
target = self.target_platform()
)
}
Self::Sass => {
format!(
"https://github.com/sass/dart-sass/releases/download/{version}/dart-sass-{version}-{target}-x64.{extension}",
version = self.tool_version(),
target = self.target_platform(),
extension = self.extension()
)
}
Self::Tailwind => {
let windows_extension = match self.target_platform() {
"windows" => ".exe",
_ => "",
};
format!(
"https://github.com/tailwindlabs/tailwindcss/releases/download/{version}/tailwindcss-{target}-x64{optional_ext}",
version = self.tool_version(),
target = self.target_platform(),
optional_ext = windows_extension
)
}
}
}
/// get package extension name
pub fn extension(&self) -> &str {
match self {
Self::Binaryen => "tar.gz",
Self::Sass => {
if cfg!(target_os = "windows") {
"zip"
} else {
"tar.gz"
}
}
Self::Tailwind => "bin",
}
}
/// check tool state
pub fn is_installed(&self) -> bool {
tools_path().join(self.name()).is_dir()
}
/// get download temp path
pub fn temp_out_path(&self) -> PathBuf {
temp_path().join(format!("{}-tool.tmp", self.name()))
}
/// start to download package
pub async fn download_package(&self) -> anyhow::Result<PathBuf> {
let download_url = self.download_url();
let temp_out = self.temp_out_path();
let mut file = tokio::fs::File::create(&temp_out)
.await
.context("failed creating temporary output file")?;
let resp = reqwest::get(download_url).await.unwrap();
let mut res_bytes = resp.bytes_stream();
while let Some(chunk_res) = res_bytes.next().await {
let chunk = chunk_res.context("error reading chunk from download")?;
let _ = file.write(chunk.as_ref()).await;
}
// log::info!("temp file path: {:?}", temp_out);
Ok(temp_out)
}
/// start to install package
pub async fn install_package(&self) -> anyhow::Result<()> {
let temp_path = self.temp_out_path();
let tool_path = tools_path();
let dir_name = match self {
Self::Binaryen => format!("binaryen-{}", self.tool_version()),
Self::Sass => "dart-sass".to_string(),
Self::Tailwind => self.name().to_string(),
};
if self.extension() == "tar.gz" {
let tar_gz = File::open(temp_path)?;
let tar = GzDecoder::new(tar_gz);
let mut archive = Archive::new(tar);
archive.unpack(&tool_path)?;
std::fs::rename(tool_path.join(dir_name), tool_path.join(self.name()))?;
} else if self.extension() == "zip" {
// decompress the `zip` file
extract_zip(&temp_path, &tool_path)?;
std::fs::rename(tool_path.join(dir_name), tool_path.join(self.name()))?;
} else if self.extension() == "bin" {
let bin_path = match self.target_platform() {
"windows" => tool_path.join(&dir_name).join(self.name()).join(".exe"),
_ => tool_path.join(&dir_name).join(self.name()),
};
// Manualy creating tool directory because we directly download the binary via Github
std::fs::create_dir(tool_path.join(dir_name))?;
let mut final_file = std::fs::File::create(&bin_path)?;
let mut temp_file = File::open(&temp_path)?;
let mut content = Vec::new();
temp_file.read_to_end(&mut content)?;
final_file.write_all(&content)?;
if self.target_platform() == "linux" {
// This code does not update permissions idk why
// let mut perms = final_file.metadata()?.permissions();
// perms.set_mode(0o744);
// Adding to the binary execution rights with "chmod"
let mut command = Command::new("chmod");
let _ = command
.args(vec!["+x", bin_path.to_str().unwrap()])
.stdout(std::process::Stdio::inherit())
.stderr(std::process::Stdio::inherit())
.output()?;
}
std::fs::remove_file(&temp_path)?;
}
Ok(())
}
pub fn call(&self, command: &str, args: Vec<&str>) -> anyhow::Result<Vec<u8>> {
let bin_path = tools_path().join(self.name()).join(self.bin_path());
let command_file = match self {
Tool::Binaryen => {
if cfg!(target_os = "windows") {
format!("{}.exe", command)
} else {
command.to_string()
}
}
Tool::Sass => {
if cfg!(target_os = "windows") {
format!("{}.bat", command)
} else {
command.to_string()
}
}
Tool::Tailwind => {
if cfg!(target_os = "windows") {
format!("{}.exe", command)
} else {
command.to_string()
}
}
};
if !bin_path.join(&command_file).is_file() {
return Err(anyhow::anyhow!("Command file not found."));
}
let mut command = Command::new(bin_path.join(&command_file).to_str().unwrap());
let output = command
.args(&args[..])
.stdout(std::process::Stdio::inherit())
.stderr(std::process::Stdio::inherit())
.output()?;
Ok(output.stdout)
}
}
pub fn extract_zip(file: &Path, target: &Path) -> anyhow::Result<()> {
let zip_file = std::fs::File::open(&file)?;
let mut zip = zip::ZipArchive::new(zip_file)?;
if !target.exists() {
let _ = std::fs::create_dir_all(target)?;
}
for i in 0..zip.len() {
let mut file = zip.by_index(i)?;
if file.is_dir() {
// dir
let target = target.join(Path::new(&file.name().replace('\\', "")));
let _ = std::fs::create_dir_all(target)?;
} else {
// file
let file_path = target.join(Path::new(file.name()));
let mut target_file = if !file_path.exists() {
std::fs::File::create(file_path)?
} else {
std::fs::File::open(file_path)?
};
let _num = std::io::copy(&mut file, &mut target_file)?;
}
}
Ok(())
}

View file

@ -0,0 +1,4 @@
#[test]
fn ready() {
println!("Compiled successfully!")
}

View file

@ -0,0 +1,30 @@
<div>
<svg class="h-5 w-5 text-gray-500" fill="currentColor" viewBox="0 0 20 20">
<path
fill-rule="evenodd"
d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z"
clip-rule="evenodd"
/>
</svg>
<svg class="h-5 w-5 text-gray-500" fill="currentColor" viewBox="0 0 20 20">
<path
fill-rule="evenodd"
d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z"
clip-rule="evenodd"
/>
</svg>
<svg class="h-5 w-5 text-gray-500" fill="currentColor" viewBox="0 0 20 20">
<path
fill-rule="evenodd"
d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z"
clip-rule="evenodd"
/>
</svg>
<svg class="h-5 w-5 text-gray-500" fill="currentColor" viewBox="0 0 20 20">
<path
fill-rule="evenodd"
d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z"
clip-rule="evenodd"
/>
</svg>
</div>

View file

@ -0,0 +1,48 @@
<section class="text-gray-600 body-font">
<div class="container px-5 py-24 mx-auto">
<div class="flex flex-wrap -mx-4 -mb-10 text-center">
<div class="sm:w-1/2 mb-10 px-4">
<div class="rounded-lg h-64 overflow-hidden">
<img
alt="content"
class="object-cover object-center h-full w-full"
src="https://dummyimage.com/1201x501"
/>
</div>
<h2 class="title-font text-2xl font-medium text-gray-900 mt-6 mb-3">
Buy YouTube Videos
</h2>
<p class="leading-relaxed text-base">
Williamsburg occupy sustainable snackwave gochujang. Pinterest
cornhole brunch, slow-carb neutra irony.
</p>
<button
class="flex mx-auto mt-6 text-white bg-indigo-500 border-0 py-2 px-5 focus:outline-none hover:bg-indigo-600 rounded"
>
Button
</button>
</div>
<div class="sm:w-1/2 mb-10 px-4">
<div class="rounded-lg h-64 overflow-hidden">
<img
alt="content"
class="object-cover object-center h-full w-full"
src="https://dummyimage.com/1202x502"
/>
</div>
<h2 class="title-font text-2xl font-medium text-gray-900 mt-6 mb-3">
The Catalyzer
</h2>
<p class="leading-relaxed text-base">
Williamsburg occupy sustainable snackwave gochujang. Pinterest
cornhole brunch, slow-carb neutra irony.
</p>
<button
class="flex mx-auto mt-6 text-white bg-indigo-500 border-0 py-2 px-5 focus:outline-none hover:bg-indigo-600 rounded"
>
Button
</button>
</div>
</div>
</div>
</section>