Merge branch 'master' into jk/formatting-and-extension

This commit is contained in:
Jonathan Kelley committed 2022-12-25 23:38:33 -05:00
commit fa52a8f71a
59 files changed
+6691 -252

No files matched your search

View File
Whitespace-only changes.
+5 -2
View File
@@ -12,12 +12,15 @@ jobs:
fail-fast: false
matrix:
include:
- target: x86_64-pc-windows-gnu
archive: zip
- 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
+34
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
+1 -1
View File
@@ -1,4 +1,4 @@
/target
Cargo.lock
.idea/
.DS_Store
.idea/
+6 -1
View File
@@ -1 +1,6 @@
{}
{
"Lua.diagnostics.globals": [
"plugin_logger",
"PLUGIN_DOWNLOADER"
]
}
Generated
+3543
View File
File diff suppressed because it is too large. Load diff
+41 -9
View File
@@ -1,8 +1,8 @@
[package]
name = "dioxus-cli"
version = "0.1.4"
version = "0.3.0"
authors = ["Jonathan Kelley"]
edition = "2018"
edition = "2021"
description = "CLI tool for developing, testing, and publishing Dioxus apps"
license = "MIT/Apache-2.0"
@@ -14,6 +14,7 @@ license = "MIT/Apache-2.0"
clap = { version = "3.0.14", features = ["derive"] }
thiserror = "1.0.30"
wasm-bindgen-cli-support = "0.2.79"
colored = "2.0.0"
# features
log = "0.4.14"
@@ -24,33 +25,64 @@ toml = "0.5.8"
fs_extra = "1.2.0"
cargo_toml = "0.11.4"
futures = "0.3.21"
notify = { version = "5.0.0-pre.13", features = ["serde"] }
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.14.1"
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"
indicatif = "0.17.0-rc.11"
axum = { version = "0.5.1", features = ["ws", "headers"] }
tower-http = { version = "0.2.2", features = ["fs", "trace"] }
headers = "0.3.7"
walkdir = "2"
# tools download
dirs = "4.0.0"
reqwest = { version = "0.11", features = ["rustls-tls", "stream", "trust-dns"] }
reqwest = { version = "0.11", features = [
"rustls-tls",
"stream",
"trust-dns",
"blocking",
] }
flate2 = "1.0.22"
tar = "0.4.38"
zip = "0.6.2"
tower = "0.4.12"
dioxus-rsx = { path = "../../dioxus/packages/rsx" }
dioxus-autofmt = { path = "../../dioxus/packages/autofmt" }
# dioxus-rsx = { git = "https://github.com/dioxuslabs/dioxus.git" }
# dioxus-autofmt = { git = "https://github.com/dioxuslabs/dioxus.git" }
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"
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" }
[[bin]]
path = "src/main.rs"
name = "dioxus"
+26 -35
View File
@@ -1,51 +1,42 @@
<div align="center">
<h1>📦✨ dioxus-cli </h1>
<p><strong>Tooling to supercharge dioxus projects</strong></p>
<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 of the build, bundling, development and publishing to simplify web development.
dioxus-cli (inspired by wasm-pack and webpack) is a tool to help get dioxus projects off the ground. It handles all the build, development, bundling, and publishing to make web development simple.
## Installation
```shell
# for stable
$ cargo install dioxus-cli
# for latest
$ cargo install --git https://github.com/DioxusLabs/cli
# for developing
$ cargo install --path . --debug
### 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
```
Now, `dioxus` is in your path, you can use `dioxus --help` to get the help document.
```
dioxus 0.1.2
Build, bundle, & ship your Dioxus app
USAGE:
dioxus [FLAGS] <SUBCOMMAND>
FLAGS:
-h, --help Prints help information
-v Enable verbose logging
-V, --version Prints version information
SUBCOMMANDS:
build Build the Rust WASM app and all of its assets
clean Clean output artifacts
config Dioxus config file controls
create Init a new project for Dioxus
help Prints 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 source file into Dioxus code
```
## Get Started
You can use the `dioxus create project_name` to init a dioxus project, its will be clone from the [dioxus-template](https://github.com/DioxusLabs/dioxus-template).
Use `dioxus create project-name` to initialize a new Dioxus project. <br>
or you can assign the template path:
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).
+1
View File
@@ -0,0 +1 @@
book
+6
View File
@@ -0,0 +1,6 @@
[book]
authors = ["YuKun Liu"]
language = "en"
multilingual = false
src = "src"
title = "Dioxus Cli"
+18
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)
+26
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 Rust WASM app 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 source file into Dioxus code
```
+47
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 project to `out_dir` :
```
dioxus build --release
```
## Target platform
Use option `platform` choose build target platform:
```
# for desktop project
dioxus build --platform desktop
```
`platform` only supports `desktop` & `web`.
```
# for web project
dioxus build --platform web
```
## Build Example
You can use `--example {name}` to build a example code.
```
# build `example/test` code
dioxus build --exmaple test
```
+18
View File
@@ -0,0 +1,18 @@
# Clean
`dioxus clean` will call `target clean` and remove `out_dir` directory.
```
dioxus-clean
Clean output artifacts
USAGE:
dioxus clean
```
you can use this command to clean all build cache and the `out_dir` content.
```
dioxus clean
```
+49
View File
@@ -0,0 +1,49 @@
# Serve
The `dioxus serve` can start a dev server (include hot-reload tool) to run the project.
```
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]
```
You can use this command to build project and start a `dev server` :
```
dioxus serve
```
## Target platform
Use option `platform` choose build target platform:
```
# for desktop project
dioxus serve --platform desktop
```
`platform` only supports `desktop` & `web`.
`dev-server` only for `web` project.
```
# for web project
dioxus serve --platform web
```
## Serve Example
You can use `--example {name}` to start a example code.
```
# build `example/test` code
dioxus serve --exmaple test
```
+57
View File
@@ -0,0 +1,57 @@
# Translate
`dioxus translate` can translate some source file into Dioxus code.
```
dioxus-translate
Translate some source file into Dioxus code
USAGE:
dioxus translate [OPTIONS] [OUTPUT]
ARGS:
<OUTPUT> Output file, stdout if not present
OPTIONS:
-c, --component Activate debug mode
-f, --file <FILE> Input file
```
## Translate HTML to stdout
```
dioxus transtale --file ./index.html
```
## Output in a file
```
dioxus translate --component --file ./index.html component.rsx
```
set `component` flag will wrap `dioxus rsx` code in a component function.
## Example
```html
<div>
<h1> Hello World </h1>
<a href="https://dioxuslabs.com/">Link</a>
</div>
```
Translate HTML to Dioxus component code.
```rust
fn component(cx: Scope) -> Element {
cx.render(rsx! {
div {
h1 { "Hello World" },
a {
href: "https://dioxuslabs.com/",
"Link"
}
}
})
}
```
+171
View File
@@ -0,0 +1,171 @@
# Configure Project
This chapter will introduce `Dioxus.toml` and anatomy the config file.
## Structure
We use `toml` to define some info for `dioxus` project.
### Application
1. ***name*** - project name & title
2. ***default_platform*** - which platform target for this project.
```
# current support: web, desktop
# default: web
default_platform = "web"
```
change this to `desktop`, the `dioxus build & serve` will default build desktop app.
3. ***out_dir*** - which directory to put the output file; use `dioxus build & service`, the output will put at this directory, and the `assets` will be also copy to here.
```
out_dir = "dist"
```
4. ***asset_dir*** - which direcotry to put your `static, assets` file, cli will automatic copy all file to `out_dir`, so you can put some resource file in there, like `CSS, JS, Image` file.
```
asset_dir = "public"
```
### Application.Tools
You can combine different tools with `dioxus`.
1. ***binaryen*** - Use the `binaryen` tooling suite.
```
# current support: wasm-opt
# default: web
binaryen = { wasm_opt = true }
```
Use the `wasm_opt = true` key/pair value to activate optimization with wasm-opt.
When building on `release` profile, Dioxus will run `wasm_opt` with `-Oz` option.
2. ***tailwindcss*** - Use the `tailwindcss` standalone binary to generate a Tailwind CSS bundle file.
```
tailwindcss = { input = "main.css", config = "tailwind.config.js" }
```
You can set two optional keys :
- input: path of the input CSS file (default value is "public/tailwind.css")
- config: path to the config file for Tailwind (default value is "src/tailwind.config.js")
When building on `release` profile, Dioxus will run `tailwindcss` with the `--minify` option.
Note : Dioxus will automatically include the generated tailwind file in the `index.html`
### Web.App
Web platform application config:
1. ***title*** - this value will display on the web page title. like `<title></title>` tag.
```
# HTML title tag content
title = "dioxus app | ⛺"
```
### Web.Watcher
Web platform `dev-server` watcher config:
1. ***reload_html*** - a boolean value; when watcher trigger, regenerate `index.html` file.
```
# when watcher trigger, regenerate the `index.html`
reload_html = true
```
2. ***watch_path*** - which files & directories will be watcher monitoring.
```
watch_path = ["src", "public"]
```
### Web.Resource
Include some `CSS Javascript` resources into html file.
1. ***style*** - include some style(CSS) file into html.
```
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*** - include some script(JS) file into html.
```
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
Only include resources at `Dev` mode.
1. ***style*** - include some style(CSS) file into html.
```
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*** - include some script(JS) file into html.
```
style = [
# include from public_dir.
"./assets/index.js",
# or some asset from online cdn.
"https://cdn.jsdelivr.net/npm/bootstrap/dist/js/bootstrap.js"
]
```
## Config example
```toml
[application]
# App (Project) Name
name = "{{project-name}}"
# Dioxus App Default Platform
# desktop, web, mobile, ssr
default_platform = "web"
# `build` & `serve` dist path
out_dir = "dist"
# resource (public) file folder
asset_dir = "public"
[web.app]
# HTML title tag content
title = "dioxus | ⛺"
[web.watcher]
# when watcher trigger, regenerate the `index.html`
reload_html = true
# which files or dirs will be watcher monitoring
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 = []
```
+38
View File
@@ -0,0 +1,38 @@
# Create a Project
Once you have the Dioxus CLI tool installed, you can use it to create dioxus project.
## Initializing a default project
The `dioxus create` command will create a new directory containing a project template.
```
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.
then you can change the current directory in to the project:
```
cd hello-dioxus
```
> Make sure `wasm32 target` is installed before running the Web project.
now we can create a `dev server` to display the project:
```
dioxus serve
```
by default, the dioxus dev server will running at: [`http://127.0.0.1:8080/`](http://127.0.0.1:8080/)
## Initalizing from other repository
you can assign which repository you want to create:
```
dioxus init hello-dioxus --template=gh:dioxuslabs/dioxus-template
```
+24
View File
@@ -0,0 +1,24 @@
# Installation
There are multiple ways to install the Dioxus CLI tool. Choose any one of the methods below that best suit your needs.
## Install from latest master version
We suggest you use `github master` version to install it now.
We have not yet released the latest version to `crates.io`
```
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
```
cargo install dioxus-cli
```
Make sure to add the Cargo bin directory to your `PATH`.
+21
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)
+79
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
```
+21
View File
@@ -0,0 +1,21 @@
# Command Functions
> you can use command functions to execute some code & script
Type Define:
```
Stdio: "Inhert" | "Piped" | "Null"
```
### `exec(commands: [string], stdout: Stdio, stderr: Stdio)`
you can use this function to run some command on the current system.
```lua
local cmd = plugin.command
manager.test = function ()
cmd.exec({"git", "clone", "https://github.com/DioxusLabs/cli-plugin-library"})
end
```
> Warning: This function don't have exception catch.
+35
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/
```
+48
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")
```
+34
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
```
+11
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()
```
+35
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
Whitespace-only changes.
+18
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
+25
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
+2 -2
View File
@@ -1,8 +1,8 @@
version = "Two"
edition = "2018"
edition = "2021"
imports_granularity = "Crate"
#use_small_heuristics = "Max"
#control_brace_style = "ClosingNextLine"
normalize_comments = true
format_code_in_doc_comments = true
format_code_in_doc_comments = true
+7 -5
View File
@@ -17,7 +17,9 @@ asset_dir = "public"
[web.app]
# HTML title tag content
title = "dioxus | ⛺"
title = "Dioxus | An elegant GUI library for Rust"
index_on_404 = true
[web.watcher]
@@ -38,8 +40,8 @@ script = []
# serve: [dev-server] only
script = []
[application.tools]
[application.plugins]
# use binaryen.wasm-opt for output Wasm file
# binaryen just will trigger in `web` platform
binaryen = { wasm_opt = [".PROJECT_OUT"] }
available = true
required = []
+2 -3
View File
@@ -9,10 +9,9 @@
</head>
<body>
<div id="main"></div>
<!-- Note the usage of `type=module` here as this is an ES6 module -->
<script type="module">
import init from "{base_path}/assets/dioxus/{app_name}.js";
init("{base_path}/assets/dioxus/{app_name}_bg.wasm").then(wasm => {
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();
}
+333 -31
View File
@@ -1,23 +1,35 @@
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, remove_dir_all, File},
fs::{copy, create_dir_all, File},
io::Read,
panic,
path::PathBuf,
process::Command,
time::Duration,
};
use wasm_bindgen_cli_support::Bindgen;
pub fn build(config: &CrateConfig) -> Result<()> {
#[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] Link up the html page to the wasm module
// [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,
@@ -29,6 +41,9 @@ pub fn build(config: &CrateConfig) -> Result<()> {
..
} = config;
// start to build the assets
let ignore_files = build_assets(config)?;
let t_start = std::time::Instant::now();
// [1] Build the .wasm module
@@ -38,12 +53,32 @@ pub fn build(config: &CrateConfig) -> Result<()> {
.arg("build")
.arg("--target")
.arg("wasm32-unknown-unknown")
.stdout(std::process::Stdio::inherit())
.stderr(std::process::Stdio::inherit());
.arg("--message-format=json")
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped());
if config.release {
cmd.arg("--release");
}
if config.verbose {
cmd.arg("--verbose");
}
if quiet {
cmd.arg("--quiet");
}
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 executable {
ExecutableType::Binary(name) => cmd.arg("--bin").arg(name),
@@ -51,13 +86,7 @@ pub fn build(config: &CrateConfig) -> Result<()> {
ExecutableType::Example(name) => cmd.arg("--example").arg(name),
};
let output = cmd.output()?;
if !output.status.success() {
log::error!("Build failed!");
let reason = String::from_utf8_lossy(&output.stderr).to_string();
return Err(Error::BuildFailed(reason));
}
let warning_messages = prettier_build(cmd)?;
// [2] Establish the output directory structure
let bindgen_outdir = out_dir.join("assets").join("dioxus");
@@ -95,8 +124,7 @@ pub fn build(config: &CrateConfig) -> Result<()> {
.unwrap();
});
if bindgen_result.is_err() {
log::error!("Bindgen build failed! \nThis is probably due to the Bindgen version, dioxus-cli using `0.2.79` Bindgen crate.");
return Ok(());
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
@@ -110,19 +138,21 @@ pub fn build(config: &CrateConfig) -> Result<()> {
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() {
binaryen.call(
"wasm-opt",
vec![
target_file.to_str().unwrap(),
"-o",
target_file.to_str().unwrap(),
],
)?;
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)?;
}
}
}
@@ -133,6 +163,45 @@ pub fn build(config: &CrateConfig) -> Result<()> {
}
}
// [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,
@@ -154,18 +223,29 @@ pub fn build(config: &CrateConfig) -> Result<()> {
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();
log::info!("🏁 Done in {}ms!", (t_end - t_start).as_millis());
Ok(())
Ok(BuildResult {
warnings: warning_messages,
elapsed_time: (t_end - t_start).as_millis(),
})
}
pub fn build_desktop(config: &CrateConfig, is_serve: bool) -> Result<()> {
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")
@@ -175,6 +255,21 @@ pub fn build_desktop(config: &CrateConfig, is_serve: bool) -> Result<()> {
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),
@@ -189,12 +284,6 @@ pub fn build_desktop(config: &CrateConfig, is_serve: bool) -> Result<()> {
}
if output.status.success() {
// this code will clean the output dir.
// if using the serve, we will not clean the out_dir.
if config.out_dir.is_dir() && !is_serve {
remove_dir_all(&config.out_dir)?;
}
let release_type = match config.release {
true => "release",
false => "debug",
@@ -250,6 +339,13 @@ pub fn build_desktop(config: &CrateConfig, is_serve: bool) -> Result<()> {
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)?;
}
}
}
}
}
@@ -269,6 +365,57 @@ pub fn build_desktop(config: &CrateConfig, is_serve: bool) -> Result<()> {
Ok(())
}
fn prettier_build(mut cmd: Command) -> 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...");
let mut command = cmd.spawn()?;
let reader = std::io::BufReader::new(command.stdout.take().unwrap());
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 {
pb.finish_and_clear();
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");
@@ -303,6 +450,15 @@ pub fn gen_page(config: &DioxusConfig, serve: bool) -> String {
&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");
}
html = html.replace("{style_include}", &style_str);
let mut script_str = String::new();
@@ -339,6 +495,152 @@ pub fn gen_page(config: &DioxusConfig, serve: bool) -> String {
html.replace("{app_title}", &title)
}
// 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
+13 -2
View File
@@ -1,3 +1,5 @@
use crate::plugin::PluginManager;
use super::*;
/// Build the Rust WASM app and all of its assets.
@@ -14,11 +16,16 @@ impl Build {
// 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());
}
let platform = self.build.platform.unwrap_or_else(|| {
crate_config
.dioxus_config
@@ -27,15 +34,17 @@ impl Build {
.clone()
});
let _ = PluginManager::on_build_start(&crate_config, &platform);
match platform.as_str() {
"web" => {
crate::builder::build(&crate_config)?;
crate::builder::build(&crate_config, false)?;
}
"desktop" => {
crate::builder::build_desktop(&crate_config, false)?;
}
_ => {
return custom_error!("Unsoppurt platform target.");
return custom_error!("Unsupported platform target.");
}
}
@@ -56,6 +65,8 @@ impl Build {
)?;
file.write_all(temp.as_bytes())?;
let _ = PluginManager::on_build_finish(&crate_config, &platform);
Ok(())
}
}
+36
View File
@@ -12,13 +12,26 @@ pub struct ConfigOptsBuild {
#[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)]
@@ -27,6 +40,11 @@ pub struct ConfigOptsServe {
#[clap(parse(from_os_str))]
pub target: Option<PathBuf>,
/// Port of dev server
#[clap(long)]
#[clap(default_value_t = 8080)]
pub port: u16,
/// Build a example [default: ""]
#[clap(long)]
pub example: Option<String>,
@@ -36,9 +54,27 @@ pub struct ConfigOptsServe {
#[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,
/// Space separated list of features to activate
#[clap(long)]
pub features: Option<Vec<String>>,
}
/// Ensure the given value for `--public-url` is formatted correctly.
+1 -2
View File
@@ -16,8 +16,7 @@ impl Clean {
.output()?;
if !output.status.success() {
log::error!("Cargo clean failed.");
return Ok(());
return custom_error!("Cargo clean failed.");
}
let out_dir = crate_config
+3 -2
View File
@@ -17,8 +17,7 @@ pub struct Create {
impl Create {
pub fn create(self) -> Result<()> {
if Self::name_vaild_check(self.name.clone()) {
log::error!("❗Unsupported project name.");
return Ok(());
return custom_error!("❗Unsupported project name.");
}
let project_path = PathBuf::from(&self.name);
@@ -54,6 +53,7 @@ impl Create {
.arg(&self.template)
.arg("--name")
.arg(&self.name)
.arg("--force")
.stdout(Stdio::piped())
.stderr(Stdio::inherit())
.output()?;
@@ -66,6 +66,7 @@ impl Create {
let mut meta_file = String::new();
dioxus_file.read_to_string(&mut meta_file)?;
meta_file = meta_file.replace("{{project-name}}", &self.name);
meta_file = meta_file.replace("{{default-platform}}", "web");
File::create(project_path.join("Dioxus.toml"))?.write_all(meta_file.as_bytes())?;
println!();
+21 -10
View File
@@ -4,8 +4,8 @@ pub mod cfg;
pub mod clean;
pub mod config;
pub mod create;
pub mod plugin;
pub mod serve;
pub mod tool;
pub mod translate;
pub mod version;
@@ -24,12 +24,12 @@ use std::{
fs::{remove_dir_all, File},
io::{Read, Write},
path::PathBuf,
process::{exit, Command, Stdio},
process::{Command, Stdio},
};
/// Build, bundle, & ship your Dioxus app.
/// Build, Bundle & Ship Dioxus Apps.
#[derive(Parser)]
#[clap(name = "dioxus")]
#[clap(name = "dioxus", version)]
pub struct Cli {
#[clap(subcommand)]
pub action: Commands,
@@ -37,10 +37,6 @@ pub struct Cli {
/// Enable verbose logging.
#[clap(short)]
pub v: bool,
// // note: dioxus is still roughly compatible with trunk
// /// Path to the Trunk config file [default: Trunk.toml]
// #[clap(long, parse(from_os_str), env = "TRUNK_CONFIG")]
// pub config: Option<PathBuf>,
}
#[derive(Parser)]
@@ -72,7 +68,22 @@ pub enum Commands {
#[clap(subcommand)]
Config(config::Config),
/// Install & Manage tools for Dioxus-cli.
/// Manage plugins for dioxus cli
#[clap(subcommand)]
Tool(tool::Tool),
Plugin(plugin::Plugin),
}
impl Commands {
pub fn to_string(&self) -> String {
match self {
Commands::Build(_) => "build",
Commands::Translate(_) => "translate",
Commands::Serve(_) => "sevre",
Commands::Create(_) => "create",
Commands::Clean(_) => "clean",
Commands::Config(_) => "config",
Commands::Plugin(_) => "plugin",
}
.to_string()
}
}
+36
View File
@@ -0,0 +1,36 @@
use super::*;
/// Build the Rust WASM app and all of its assets.
#[derive(Clone, Debug, Deserialize, Subcommand)]
#[clap(name = "plugin")]
pub enum Plugin {
/// Return all dioxus-cli support tools.
List {},
/// Get default app install path.
AppPath {},
/// Install a new tool.
Add { name: String },
}
impl Plugin {
pub async fn plugin(self) -> Result<()> {
match self {
Plugin::List {} => {
for item in crate::plugin::PluginManager::plugin_list() {
println!("- {item}");
}
}
Plugin::AppPath {} => {
if let Some(v) = crate::plugin::PluginManager::init_plugin_dir().to_str() {
println!("{}", v);
} else {
log::error!("Plugin path get failed.");
}
}
Plugin::Add { name: _ } => {
log::info!("You can use `dioxus plugin app-path` to get Installation position");
}
}
Ok(())
}
}
+35 -34
View File
@@ -1,5 +1,6 @@
use super::*;
use std::{
fs::create_dir_all,
io::Write,
path::PathBuf,
process::{Command, Stdio},
@@ -18,12 +19,18 @@ impl Serve {
let mut crate_config = crate::CrateConfig::new()?;
// change the relase state.
crate_config.with_hot_reload(self.serve.hot_reload);
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());
}
let platform = self.serve.platform.unwrap_or_else(|| {
crate_config
.dioxus_config
@@ -32,38 +39,32 @@ impl Serve {
.clone()
});
match platform.as_str() {
"web" => {
crate::builder::build(&crate_config)?;
}
"desktop" => {
crate::builder::build_desktop(&crate_config, true)?;
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(crate_config.out_dir.join(file).to_str().unwrap())
.stdout(Stdio::inherit())
.output()?;
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(());
}
_ => {
return custom_error!("Unsoppurt platform target.");
}
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(crate_config.clone()).await?;
server::startup(self.serve.port, crate_config.clone()).await?;
Ok(())
}
@@ -71,19 +72,19 @@ impl Serve {
pub fn regen_dev_page(crate_config: &CrateConfig) -> Result<()> {
let serve_html = gen_page(&crate_config.dioxus_config, true);
let mut file = std::fs::File::create(
let dist_path = crate_config.crate_dir.join(
crate_config
.crate_dir
.join(
crate_config
.dioxus_config
.application
.out_dir
.clone()
.unwrap_or_else(|| PathBuf::from("dist")),
)
.join("index.html"),
)?;
.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(())
+11 -11
View File
@@ -20,21 +20,24 @@ impl Tool {
Tool::List {} => {
for item in tools::tool_list() {
if tools::Tool::from_str(item).unwrap().is_installed() {
println!("{item} [installed]");
println!("- {item} [installed]");
} else {
println!("{item}");
println!("- {item}");
}
}
}
Tool::AppPath {} => {
println!("{}", tools::tools_path().to_str().unwrap());
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()) {
log::error!("Tool {name} not found.");
return Ok(());
return custom_error!("Tool {name} not found.");
}
let target_tool = tools::Tool::from_str(&name).unwrap();
@@ -45,20 +48,17 @@ impl Tool {
log::info!("Start to download tool package...");
if let Err(e) = target_tool.download_package().await {
log::error!("Tool download failed: {e}");
return Ok(());
return custom_error!("Tool download failed: {e}");
}
log::info!("Start to install tool package...");
if let Err(e) = target_tool.install_package().await {
log::error!("Tool install failed: {e}");
return Ok(());
return custom_error!("Tool install failed: {e}");
}
log::info!("Tool {name} install successfully!");
log::info!("Tool {name} installed successfully!");
}
}
Ok(())
}
}
+1 -2
View File
@@ -43,8 +43,7 @@ impl Translate {
}
(None, None) => {
if atty::is(atty::Stream::Stdin) {
log::error!("No input file, source, or stdin to translate from.");
exit(0);
return custom_error!("No input file, source, or stdin to translate from.");
}
let mut buffer = String::new();
+55 -27
View File
@@ -6,6 +6,13 @@ use std::{collections::HashMap, fs::File, io::Read, path::PathBuf};
pub struct DioxusConfig {
pub application: ApplicationConfig,
pub web: WebConfig,
#[serde(default = "default_plugin")]
pub plugin: toml::Value,
}
fn default_plugin() -> toml::Value {
toml::Value::Boolean(true)
}
impl DioxusConfig {
@@ -22,7 +29,7 @@ impl DioxusConfig {
dioxus_conf_file.read_to_string(&mut meta_str)?;
toml::from_str::<DioxusConfig>(&meta_str)
.map_err(|_| crate::Error::Unique("Dioxus.toml parse failed".into()))
.map_err(|_| crate::Error::Unique("Dioxus.toml parse failed".into()))
}
}
@@ -34,21 +41,23 @@ impl Default for DioxusConfig {
default_platform: "web".to_string(),
out_dir: Some(PathBuf::from("dist")),
asset_dir: Some(PathBuf::from("public")),
tools: None,
sub_package: None,
},
web: WebConfig {
app: WebAppConfing {
app: WebAppConfig {
title: Some("dioxus | ⛺".into()),
base_path: None,
},
watcher: WebWatcherConfing {
watcher: WebWatcherConfig {
watch_path: Some(vec![PathBuf::from("src")]),
reload_html: Some(false),
index_on_404: Some(true),
},
resource: WebResourceConfing {
dev: WebDevResourceConfing {
resource: WebResourceConfig {
dev: WebDevResourceConfig {
style: Some(vec![]),
script: Some(vec![]),
},
@@ -56,6 +65,7 @@ impl Default for DioxusConfig {
script: Some(vec![]),
},
},
plugin: toml::Value::Table(toml::map::Map::new()),
}
}
}
@@ -66,39 +76,41 @@ pub struct ApplicationConfig {
pub default_platform: String,
pub out_dir: Option<PathBuf>,
pub asset_dir: Option<PathBuf>,
pub tools: Option<HashMap<String, toml::Value>>,
pub sub_package: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WebConfig {
pub app: WebAppConfing,
pub watcher: WebWatcherConfing,
pub resource: WebResourceConfing,
pub app: WebAppConfig,
pub watcher: WebWatcherConfig,
pub resource: WebResourceConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WebAppConfing {
pub struct WebAppConfig {
pub title: Option<String>,
pub base_path: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WebWatcherConfing {
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 WebResourceConfing {
pub dev: WebDevResourceConfing,
pub struct WebResourceConfig {
pub dev: WebDevResourceConfig,
pub style: Option<Vec<PathBuf>>,
pub script: Option<Vec<PathBuf>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WebDevResourceConfing {
pub struct WebDevResourceConfig {
pub style: Option<Vec<PathBuf>>,
pub script: Option<Vec<PathBuf>>,
}
@@ -114,6 +126,10 @@ pub struct CrateConfig {
pub executable: ExecutableType,
pub dioxus_config: DioxusConfig,
pub release: bool,
pub hot_reload: bool,
pub verbose: bool,
pub custom_profile: Option<String>,
pub features: Option<Vec<String>>,
}
#[derive(Debug, Clone)]
@@ -162,6 +178,10 @@ impl CrateConfig {
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,
@@ -173,6 +193,10 @@ impl CrateConfig {
executable,
release,
dioxus_config,
hot_reload,
custom_profile,
features,
verbose,
})
}
@@ -186,19 +210,23 @@ impl CrateConfig {
self
}
// pub fn with_build_options(&mut self, options: &BuildOptions) {
// if let Some(name) = &options.example {
// self.as_example(name.clone());
// }
// self.release = options.release;
// self.out_dir = options.outdir.clone().into();
// }
pub fn with_hot_reload(&mut self, hot_reload: bool) -> &mut Self {
self.hot_reload = hot_reload;
self
}
// pub fn with_develop_options(&mut self, options: &DevelopOptions) {
// if let Some(name) = &options.example {
// self.as_example(name.clone());
// }
// self.release = options.release;
// self.out_dir = tempfile::Builder::new().tempdir().expect("").into_path();
// }
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
}
}
+1 -1
View File
@@ -63,7 +63,7 @@ impl From<hyper::Error> for Error {
#[macro_export]
macro_rules! custom_error {
($msg:literal $(,)?) => {
Err(Error::CustomError($msg.to_string()))
Err(Error::CustomError(format!($msg)))
};
($err:expr $(,)?) => {
Err(Error::from($err))
+4
View File
@@ -1,3 +1,5 @@
pub const DIOXUS_CLI_VERSION: &str = "0.1.5";
pub mod builder;
pub mod server;
pub mod tools;
@@ -18,3 +20,5 @@ pub use error::*;
pub mod logging;
pub use logging::*;
pub mod plugin;
+24 -8
View File
@@ -1,50 +1,66 @@
use clap::Parser;
use dioxus_cli::*;
use dioxus_cli::{plugin::PluginManager, *};
use std::process::exit;
#[tokio::main]
async fn main() -> Result<()> {
let args = Cli::parse();
set_up_logging();
let dioxus_config = DioxusConfig::load().unwrap_or(DioxusConfig::default());
let plugin_state = PluginManager::init(dioxus_config.plugin);
if let Err(e) = plugin_state {
log::error!("🚫 Plugin system initialization failed: {e}");
exit(1);
}
match args.action {
Commands::Translate(opts) => {
if let Err(e) = opts.translate() {
log::error!("translate error: {}", e);
log::error!("🚫 Translate failed: {}", e);
exit(1);
}
}
Commands::Build(opts) => {
if let Err(e) = opts.build() {
log::error!("build error: {}", e);
log::error!("🚫 Build project failed: {}", e);
exit(1);
}
}
Commands::Clean(opts) => {
if let Err(e) = opts.clean() {
log::error!("clean error: {}", e);
log::error!("🚫 Clean project failed: {}", e);
exit(1);
}
}
Commands::Serve(opts) => {
if let Err(e) = opts.serve().await {
log::error!("serve error: {}", e);
log::error!("🚫 Serve startup failed: {}", e);
exit(1);
}
}
Commands::Create(opts) => {
if let Err(e) = opts.create() {
log::error!("create error: {}", e);
log::error!("🚫 Create project failed: {}", e);
exit(1);
}
}
Commands::Config(opts) => {
if let Err(e) = opts.config() {
log::error!("config error: {}", e);
exit(1);
}
}
Commands::Tool(opts) => {
if let Err(e) = opts.tool().await {
Commands::Plugin(opts) => {
if let Err(e) = opts.plugin().await {
log::error!("tool error: {}", e);
}
}
+65
View File
@@ -0,0 +1,65 @@
use std::process::{Command, Stdio};
use mlua::{FromLua, UserData};
enum StdioFromString {
Inhert,
Piped,
Null,
}
impl<'lua> FromLua<'lua> for StdioFromString {
fn from_lua(lua_value: mlua::Value<'lua>, _lua: &'lua mlua::Lua) -> mlua::Result<Self> {
if let mlua::Value::String(v) = lua_value {
let v = v.to_str().unwrap();
return Ok(match v.to_lowercase().as_str() {
"inhert" => Self::Inhert,
"piped" => Self::Piped,
"null" => Self::Null,
_ => Self::Inhert,
});
}
Ok(Self::Inhert)
}
}
impl StdioFromString {
pub fn to_stdio(self) -> Stdio {
match self {
StdioFromString::Inhert => Stdio::inherit(),
StdioFromString::Piped => Stdio::piped(),
StdioFromString::Null => Stdio::null(),
}
}
}
pub struct PluginCommander;
impl UserData for PluginCommander {
fn add_methods<'lua, M: mlua::UserDataMethods<'lua, Self>>(methods: &mut M) {
methods.add_function(
"exec",
|_, args: (Vec<String>, StdioFromString, StdioFromString)| {
let cmd = args.0;
let stdout = args.1;
let stderr = args.2;
if cmd.len() == 0 {
return Ok(());
}
let cmd_name = cmd.get(0).unwrap();
let mut command = Command::new(cmd_name);
let t = cmd
.iter()
.enumerate()
.filter(|(i, _)| *i > 0)
.map(|v| v.1.clone())
.collect::<Vec<String>>();
command.args(t);
command.stdout(stdout.to_stdio()).stderr(stderr.to_stdio());
command.output()?;
Ok(())
},
);
}
fn add_fields<'lua, F: mlua::UserDataFields<'lua, Self>>(_fields: &mut F) {}
}
+13
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())
});
}
}
+87
View File
@@ -0,0 +1,87 @@
use std::{
fs::{create_dir, create_dir_all, remove_dir_all},
path::PathBuf, io::{Read, Write},
};
use std::fs::File;
use mlua::UserData;
use flate2::read::GzDecoder;
use tar::Archive;
use crate::tools::extract_zip;
pub struct PluginFileSystem;
impl UserData for PluginFileSystem {
fn add_methods<'lua, M: mlua::UserDataMethods<'lua, Self>>(methods: &mut M) {
methods.add_function("create_dir", |_, args: (String, bool)| {
let path = args.0;
let recursive = args.1;
let path = PathBuf::from(path);
if !path.exists() {
let v = if recursive {
create_dir_all(path)
} else {
create_dir(path)
};
return Ok(v.is_ok());
}
Ok(true)
});
methods.add_function("remove_dir", |_, path: String| {
let path = PathBuf::from(path);
let r = remove_dir_all(path);
Ok(r.is_ok())
});
methods.add_function("file_get_content", |_, path: String| {
let path = PathBuf::from(path);
let mut file = std::fs::File::open(path)?;
let mut buffer = String::new();
file.read_to_string(&mut buffer)?;
Ok(buffer)
});
methods.add_function("file_set_content", |_, args: (String, String)| {
let path = args.0;
let content = args.1;
let path = PathBuf::from(path);
let file = std::fs::File::create(path);
if file.is_err() {
return Ok(false);
}
if file.unwrap().write_all(content.as_bytes()).is_err() {
return Ok(false)
}
Ok(true)
});
methods.add_function("unzip_file", |_, args: (String, String)| {
let file = PathBuf::from(args.0);
let target = PathBuf::from(args.1);
let res = extract_zip(&file, &target);
if let Err(_) = res {
return Ok(false);
}
Ok(true)
});
methods.add_function("untar_gz_file", |_, args: (String, String)| {
let file = PathBuf::from(args.0);
let target = PathBuf::from(args.1);
let tar_gz = if let Ok(v) = File::open(file) {
v
} else {
return Ok(false);
};
let tar = GzDecoder::new(tar_gz);
let mut archive = Archive::new(tar);
if archive.unpack(&target).is_err() {
return Ok(false);
}
Ok(true)
});
}
}
+28
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(())
});
}
}
+233
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))
}
}
+27
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)
});
}
}
+18
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");
}
});
}
}
+40
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())
});
}
}
+330
View File
@@ -0,0 +1,330 @@
use std::{
io::{Read, Write},
path::PathBuf,
sync::Mutex,
};
use mlua::{Lua, Table};
use serde_json::json;
use crate::{
tools::{app_path, clone_repo},
CrateConfig,
};
use self::{
interface::PluginInfo,
interface::{
command::PluginCommander, dirs::PluginDirs, fs::PluginFileSystem, log::PluginLogger,
network::PluginNetwork, os::PluginOS, path::PluginPath,
},
types::PluginConfig,
};
pub mod interface;
mod types;
lazy_static::lazy_static! {
static ref LUA: Mutex<Lua> = Mutex::new(Lua::new());
}
pub struct PluginManager;
impl PluginManager {
pub fn init(config: toml::Value) -> anyhow::Result<()> {
let config = PluginConfig::from_toml_value(config);
if !config.available {
return Ok(());
}
let lua = LUA.lock().unwrap();
let manager = lua.create_table().unwrap();
let name_index = lua.create_table().unwrap();
let plugin_dir = Self::init_plugin_dir();
let api = lua.create_table().unwrap();
api.set("log", PluginLogger).unwrap();
api.set("command", PluginCommander).unwrap();
api.set("network", PluginNetwork).unwrap();
api.set("dirs", PluginDirs).unwrap();
api.set("fs", PluginFileSystem).unwrap();
api.set("path", PluginPath).unwrap();
api.set("os", PluginOS).unwrap();
lua.globals().set("plugin_lib", api).unwrap();
lua.globals()
.set("library_dir", plugin_dir.to_str().unwrap())
.unwrap();
lua.globals().set("config_info", config.clone())?;
let mut index: u32 = 1;
let dirs = std::fs::read_dir(&plugin_dir)?;
let mut path_list = dirs
.filter(|v| v.is_ok())
.map(|v| (v.unwrap().path(), false))
.collect::<Vec<(PathBuf, bool)>>();
for i in &config.loader {
let path = PathBuf::from(i);
if !path.is_dir() {
// for loader dir, we need check first, because we need give a error log.
log::error!("Plugin loader: {:?} path is not a exists directory.", path);
}
path_list.push((path, true));
}
for entry in path_list {
let plugin_dir = entry.0.to_path_buf();
if plugin_dir.is_dir() {
let init_file = plugin_dir.join("init.lua");
if init_file.is_file() {
let mut file = std::fs::File::open(init_file).unwrap();
let mut buffer = String::new();
file.read_to_string(&mut buffer).unwrap();
let current_plugin_dir = plugin_dir.to_str().unwrap().to_string();
let from_loader = entry.1;
lua.globals()
.set("_temp_plugin_dir", current_plugin_dir.clone())?;
lua.globals().set("_temp_from_loader", from_loader)?;
let info = lua.load(&buffer).eval::<PluginInfo>();
match info {
Ok(mut info) => {
if name_index.contains_key(info.name.clone()).unwrap_or(false)
&& !from_loader
{
// found same name plugin, intercept load
log::warn!(
"Plugin {} has been intercepted. [mulit-load]",
info.name
);
continue;
}
info.inner.plugin_dir = current_plugin_dir;
info.inner.from_loader = from_loader;
// call `on_init` if file "dcp.json" not exists
let dcp_file = plugin_dir.join("dcp.json");
if !dcp_file.is_file() {
if let Some(func) = info.clone().on_init {
let result = func.call::<_, bool>(());
match result {
Ok(true) => {
// plugin init success, create `dcp.json` file.
let mut file = std::fs::File::create(dcp_file).unwrap();
let value = json!({
"name": info.name,
"author": info.author,
"repository": info.repository,
"version": info.version,
"generate_time": chrono::Local::now().timestamp(),
});
let buffer =
serde_json::to_string_pretty(&value).unwrap();
let buffer = buffer.as_bytes();
file.write_all(buffer).unwrap();
// insert plugin-info into plugin-manager
if let Ok(index) =
name_index.get::<_, u32>(info.name.clone())
{
let _ = manager.set(index, info.clone());
} else {
let _ = manager.set(index, info.clone());
index += 1;
let _ = name_index.set(info.name, index);
}
}
Ok(false) => {
log::warn!("Plugin init function result is `false`, init failed.");
}
Err(e) => {
log::warn!("Plugin init failed: {e}");
}
}
}
} else {
if let Ok(index) = name_index.get::<_, u32>(info.name.clone()) {
let _ = manager.set(index, info.clone());
} else {
let _ = manager.set(index, info.clone());
index += 1;
let _ = name_index.set(info.name, index);
}
}
}
Err(_e) => {
let dir_name = plugin_dir.file_name().unwrap().to_str().unwrap();
log::error!("Plugin '{dir_name}' load failed.");
}
}
}
}
}
lua.globals().set("manager", manager).unwrap();
return Ok(());
}
pub fn on_build_start(crate_config: &CrateConfig, platform: &str) -> anyhow::Result<()> {
let lua = LUA.lock().unwrap();
if !lua.globals().contains_key("manager")? {
return Ok(());
}
let manager = lua.globals().get::<_, Table>("manager")?;
let args = lua.create_table()?;
args.set("name", crate_config.dioxus_config.application.name.clone())?;
args.set("platform", platform)?;
args.set("out_dir", crate_config.out_dir.to_str().unwrap())?;
args.set("asset_dir", crate_config.asset_dir.to_str().unwrap())?;
for i in 1..(manager.len()? as i32 + 1) {
let info = manager.get::<i32, PluginInfo>(i)?;
if let Some(func) = info.build.on_start {
func.call::<Table, ()>(args.clone())?;
}
}
Ok(())
}
pub fn on_build_finish(crate_config: &CrateConfig, platform: &str) -> anyhow::Result<()> {
let lua = LUA.lock().unwrap();
if !lua.globals().contains_key("manager")? {
return Ok(());
}
let manager = lua.globals().get::<_, Table>("manager")?;
let args = lua.create_table()?;
args.set("name", crate_config.dioxus_config.application.name.clone())?;
args.set("platform", platform)?;
args.set("out_dir", crate_config.out_dir.to_str().unwrap())?;
args.set("asset_dir", crate_config.asset_dir.to_str().unwrap())?;
for i in 1..(manager.len()? as i32 + 1) {
let info = manager.get::<i32, PluginInfo>(i)?;
if let Some(func) = info.build.on_finish {
func.call::<Table, ()>(args.clone())?;
}
}
Ok(())
}
pub fn on_serve_start(crate_config: &CrateConfig) -> anyhow::Result<()> {
let lua = LUA.lock().unwrap();
if !lua.globals().contains_key("manager")? {
return Ok(());
}
let manager = lua.globals().get::<_, Table>("manager")?;
let args = lua.create_table()?;
args.set("name", crate_config.dioxus_config.application.name.clone())?;
for i in 1..(manager.len()? as i32 + 1) {
let info = manager.get::<i32, PluginInfo>(i)?;
if let Some(func) = info.serve.on_start {
func.call::<Table, ()>(args.clone())?;
}
}
Ok(())
}
pub fn on_serve_rebuild(timestamp: i64, files: Vec<PathBuf>) -> anyhow::Result<()> {
let lua = LUA.lock().unwrap();
let manager = lua.globals().get::<_, Table>("manager")?;
let args = lua.create_table()?;
args.set("timestamp", timestamp)?;
let files: Vec<String> = files
.iter()
.map(|v| v.to_str().unwrap().to_string())
.collect();
args.set("changed_files", files)?;
for i in 1..(manager.len()? as i32 + 1) {
let info = manager.get::<i32, PluginInfo>(i)?;
if let Some(func) = info.serve.on_rebuild {
func.call::<Table, ()>(args.clone())?;
}
}
Ok(())
}
pub fn on_serve_shutdown(crate_config: &CrateConfig) -> anyhow::Result<()> {
let lua = LUA.lock().unwrap();
if !lua.globals().contains_key("manager")? {
return Ok(());
}
let manager = lua.globals().get::<_, Table>("manager")?;
let args = lua.create_table()?;
args.set("name", crate_config.dioxus_config.application.name.clone())?;
for i in 1..(manager.len()? as i32 + 1) {
let info = manager.get::<i32, PluginInfo>(i)?;
if let Some(func) = info.serve.on_shutdown {
func.call::<Table, ()>(args.clone())?;
}
}
Ok(())
}
pub fn init_plugin_dir() -> PathBuf {
let app_path = app_path();
let plugin_path = app_path.join("plugins");
if !plugin_path.is_dir() {
log::info!("📖 Start to init plugin library ...");
let url = "https://github.com/DioxusLabs/cli-plugin-library";
clone_repo(&plugin_path, url).unwrap();
}
plugin_path
}
pub fn plugin_list() -> Vec<String> {
let mut res = vec![];
if let Ok(lua) = LUA.lock() {
let list = lua
.load(mlua::chunk!(
local list = {}
for key, value in ipairs(manager) do
table.insert(list, {name = value.name, loader = value.inner.from_loader})
end
return list
))
.eval::<Vec<Table>>()
.unwrap_or_default();
for i in list {
let name = i.get::<_, String>("name").unwrap();
let loader = i.get::<_, bool>("loader").unwrap();
let text = if loader {
format!("{name} [:loader]")
} else {
name
};
res.push(text);
}
}
res
}
}
+138
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)
}
})
}
}
+522 -53
View File
@@ -1,3 +1,4 @@
use crate::{builder, plugin::PluginManager, serve::Serve, BuildResult, CrateConfig, Result};
use axum::{
body::{Full, HttpBody},
extract::{ws::Message, Extension, TypedHeader, WebSocketUpgrade},
@@ -6,58 +7,155 @@ use axum::{
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::{path::PathBuf, sync::Arc};
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 crate::{builder, serve::Serve, CrateConfig, Result};
use tokio::sync::broadcast;
struct WsRelodState {
update: broadcast::Sender<String>,
pub struct BuildManager {
config: CrateConfig,
reload_tx: broadcast::Sender<()>,
}
pub async fn startup(config: CrateConfig) -> Result<()> {
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) -> Result<()> {
// ctrl-c shutdown checker
let crate_config = config.clone();
let _ = ctrlc::set_handler(move || {
let _ = PluginManager::on_serve_shutdown(&crate_config);
std::process::exit(0);
});
if config.hot_reload {
startup_hot_reload(port, config).await?
} else {
startup_default(port, config).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(port: u16, config: CrateConfig) -> Result<()> {
let first_build_result = crate::builder::build(&config, false)?;
log::info!("🚀 Starting development server...");
PluginManager::on_serve_start(&config)?;
let dist_path = config.out_dir.clone();
let (reload_tx, _) = broadcast::channel(100);
let file_map = Arc::new(Mutex::new(FileMap::<HtmlCtx>::new(
config.crate_dir.clone(),
)));
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 ws_reload_state = Arc::new(WsRelodState {
let crate_dir = config.crate_dir.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 watcher_conf = config.clone();
let mut watcher = RecommendedWatcher::new(
move |_: notify::Result<notify::Event>| {
if chrono::Local::now().timestamp() > last_update_time {
log::info!("Start to rebuild project...");
if builder::build(&watcher_conf).is_ok() {
// change the websocket reload state to true;
// the page will auto-reload.
if watcher_conf
.dioxus_config
.web
.watcher
.reload_html
.unwrap_or(false)
{
let _ = Serve::regen_dev_page(&watcher_conf);
}
let _ = reload_tx.send("reload".into());
last_update_time = chrono::Local::now().timestamp();
}
}
},
notify::Config::default(),
)
.unwrap();
let allow_watch_path = config
.dioxus_config
.web
@@ -66,6 +164,60 @@ pub async fn startup(config: CrateConfig) -> Result<()> {
.clone()
.unwrap_or_else(|| vec![PathBuf::from("src")]);
let watcher_config = config.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 path.extension().and_then(|p| p.to_str()) != Some("rs") {
continue;
}
// find changes to the rsx in the file
let mut map = file_map.lock().unwrap();
match map.update_rsx(&path, &crate_dir) {
UpdateResult::UpdatedRsx(msgs) => {
messages.extend(msgs);
}
UpdateResult::NeedsRebuild => {
match build_manager.rebuild() {
Ok(res) => {
print_console_info(
port,
&config,
PrettierOptions {
changed: evt.paths,
warnings: res.warnings,
elapsed_time: res.elapsed_time,
},
);
}
Err(err) => {
log::error!("{}", err);
}
}
return;
}
}
}
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 {
watcher
.watch(
@@ -76,13 +228,20 @@ pub async fn startup(config: CrateConfig) -> Result<()> {
}
// start serve dev-server at 0.0.0.0:8080
let port = "8080";
log::info!("📡 Dev-Server is started at: http://127.0.0.1:{}/", port);
print_console_info(
port,
&config,
PrettierOptions {
changed: vec![],
warnings: first_build_result.warnings,
elapsed_time: first_build_result.elapsed_time,
},
);
let file_service_config = config.clone();
let file_service = ServiceBuilder::new()
.and_then(
|response: Response<ServeFileSystemResponseBody>| async move {
move |response: Response<ServeFileSystemResponseBody>| async move {
let response = if file_service_config
.dioxus_config
.web
@@ -114,7 +273,7 @@ pub async fn startup(config: CrateConfig) -> Result<()> {
Ok(response)
},
)
.service(ServeDir::new((&config.crate_dir).join(&dist_path)));
.service(ServeDir::new(config.crate_dir.join(&dist_path)));
let router = Router::new()
.route("/_dioxus/ws", get(ws_handler))
@@ -127,33 +286,343 @@ pub async fn startup(config: CrateConfig) -> Result<()> {
}),
);
let router = router
.route("/_dioxus/hot_reload", get(hot_reload_handler))
.layer(Extension(ws_reload_state))
.layer(Extension(hot_reload_state));
axum::Server::bind(&format!("0.0.0.0:{}", port).parse().unwrap())
.serve(router.layer(Extension(ws_reload_state)).into_make_service())
.serve(router.into_make_service())
.await?;
Ok(())
}
pub async fn startup_default(port: u16, config: CrateConfig) -> 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 mut watcher = notify::recommended_watcher(move |info: notify::Result<notify::Event>| {
let config = watcher_config.clone();
if let Ok(e) = info {
if chrono::Local::now().timestamp() > last_update_time {
match build_manager.rebuild() {
Ok(res) => {
last_update_time = chrono::Local::now().timestamp();
print_console_info(
port,
&config,
PrettierOptions {
changed: e.paths.clone(),
warnings: res.warnings,
elapsed_time: res.elapsed_time,
},
);
let _ = PluginManager::on_serve_rebuild(
chrono::Local::now().timestamp(),
e.paths,
);
}
Err(e) => log::error!("{}", e),
}
}
}
})
.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(
port,
&config,
PrettierOptions {
changed: vec![],
warnings: first_build_result.warnings,
elapsed_time: first_build_result.elapsed_time,
},
);
PluginManager::on_serve_start(&config)?;
let file_service_config = config.clone();
let file_service = ServiceBuilder::new()
.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 router = Router::new()
.route("/_dioxus/ws", get(ws_handler))
.fallback(
get_service(file_service).handle_error(|error: std::io::Error| async move {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Unhandled internal error: {}", error),
)
}),
)
.layer(Extension(ws_reload_state));
axum::Server::bind(&format!("0.0.0.0:{}", port).parse().unwrap())
.serve(router.into_make_service())
.await?;
Ok(())
}
#[derive(Debug, Default)]
pub struct PrettierOptions {
changed: Vec<PathBuf>,
warnings: Vec<Diagnostic>,
elapsed_time: u128,
}
fn print_console_info(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"
};
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://{}:{}/",
get_ip().unwrap_or(String::from("0.0.0.0")),
port
)
.blue()
);
println!("");
println!("\t> Profile : {}", profile.green());
println!("\t> Hot Reload : {}", hot_reload.cyan());
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<WsRelodState>>,
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 {
let v = rx.recv().await.unwrap();
if v == "reload" {
// ignore the error
if socket
.send(Message::Text(String::from("reload")))
.await
.is_err()
{
break;
}
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();
}
});
+167 -11
View File
@@ -1,6 +1,7 @@
use std::{
fs::{create_dir_all, File},
path::PathBuf,
io::{Read, Write},
path::{Path, PathBuf},
process::Command,
};
@@ -13,11 +14,13 @@ use tokio::io::AsyncWriteExt;
#[derive(Debug, PartialEq, Eq)]
pub enum Tool {
Binaryen,
Sass,
Tailwind,
}
pub fn tool_list() -> Vec<&'static str> {
vec!["binaryen"]
}
// pub fn tool_list() -> Vec<&'static str> {
// vec!["binaryen", "sass", "tailwindcss"]
// }
pub fn app_path() -> PathBuf {
let data_local = dirs::data_local_dir().unwrap();
@@ -37,6 +40,16 @@ pub fn temp_path() -> PathBuf {
temp_path
}
pub fn clone_repo(dir: &Path, url: &str) -> anyhow::Result<()> {
let target_dir = dir.parent().unwrap();
let dir_name = dir.file_name().unwrap();
let mut cmd = Command::new("git");
let cmd = cmd.current_dir(target_dir);
let _res = cmd.arg("clone").arg(url).arg(dir_name).output()?;
Ok(())
}
pub fn tools_path() -> PathBuf {
let app_path = app_path();
let temp_path = app_path.join("tools");
@@ -52,6 +65,8 @@ impl Tool {
pub fn from_str(name: &str) -> Option<Self> {
match name {
"binaryen" => Some(Self::Binaryen),
"sass" => Some(Self::Sass),
"tailwindcss" => Some(Self::Tailwind),
_ => None,
}
}
@@ -60,6 +75,8 @@ impl Tool {
pub fn name(&self) -> &str {
match self {
Self::Binaryen => "binaryen",
Self::Sass => "sass",
Self::Tailwind => "tailwindcss",
}
}
@@ -67,6 +84,8 @@ impl Tool {
pub fn bin_path(&self) -> &str {
match self {
Self::Binaryen => "bin",
Self::Sass => ".",
Self::Tailwind => ".",
}
}
@@ -84,6 +103,37 @@ impl Tool {
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",
}
}
@@ -92,10 +142,31 @@ impl Tool {
match self {
Self::Binaryen => {
format!(
"https://github.com/WebAssembly/binaryen/releases/download/version_105/binaryen-version_105-x86_64-{target}.tar.gz",
"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
)
}
}
}
@@ -103,6 +174,14 @@ impl Tool {
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",
}
}
@@ -131,7 +210,7 @@ impl Tool {
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)
}
@@ -140,10 +219,10 @@ impl Tool {
let temp_path = self.temp_out_path();
let tool_path = tools_path();
let dir_name = if self == &Tool::Binaryen {
"binaryen-version_105"
} else {
""
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" {
@@ -151,8 +230,42 @@ impl Tool {
let tar = GzDecoder::new(tar_gz);
let mut archive = Archive::new(tar);
archive.unpack(&tool_path)?;
// println!("{:?} -> {:?}", tool_path.join(dir_name), tool_path.join(self.name()));
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(())
@@ -169,6 +282,20 @@ impl Tool {
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() {
@@ -185,3 +312,32 @@ impl Tool {
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(())
}