Merge branch 'master' into bump-wry

This commit is contained in:
ealmloff authored and GitHub committed 2023-10-31 13:40:03 -05:00
commit c84a13f164
448 files changed
+3926 -19342

No files matched your search

+2 -8
View File
@@ -1,6 +1,5 @@
# dioxus-autofmt
[![Crates.io][crates-badge]][crates-url]
[![MIT licensed][mit-badge]][mit-url]
[![Build Status][actions-badge]][actions-url]
@@ -8,38 +7,33 @@
[crates-badge]: https://img.shields.io/crates/v/dioxus-autofmt.svg
[crates-url]: https://crates.io/crates/dioxus-autofmt
[mit-badge]: https://img.shields.io/badge/license-MIT-blue.svg
[mit-url]: https://github.com/dioxuslabs/dioxus/blob/master/LICENSE
[actions-badge]: https://github.com/dioxuslabs/dioxus/actions/workflows/main.yml/badge.svg
[actions-url]: https://github.com/dioxuslabs/dioxus/actions?query=workflow%3ACI+branch%3Amaster
[discord-badge]: https://img.shields.io/discord/899851952891002890.svg?logo=discord&style=flat-square
[discord-url]: https://discord.gg/XgGxMSkvUM
[Website](https://dioxuslabs.com) |
[Guides](https://dioxuslabs.com/docs/0.3/guide/en/) |
[Guides](https://dioxuslabs.com/learn/0.4/) |
[API Docs](https://docs.rs/dioxus-autofmt/latest/dioxus_autofmt) |
[Chat](https://discord.gg/XgGxMSkvUM)
## Overview
`dioxus-autofmt` provides a pretty printer for the `rsx` syntax tree.
This is done manually with a via set of formatting rules. The output is not guaranteed to be stable between minor versions of the crate as we might tweak the output.
`dioxus-autofmt` provides an API to perform precision edits as well as just spit out a block of formatted RSX from any RSX syntax tree. This is used by the `rsx-rosetta` crate which can accept various input languages and output valid RSX.
## Contributing
- Report issues on our [issue tracker](https://github.com/dioxuslabs/dioxus/issues).
- Join the discord and ask questions!
## License
This project is licensed under the [MIT license].
[mit license]: https://github.com/DioxusLabs/dioxus/blob/master/LICENSE-MIT
+4
View File
@@ -1,3 +1,7 @@
#![doc = include_str!("../README.md")]
#![doc(html_logo_url = "https://avatars.githubusercontent.com/u/79236386")]
#![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")]
use std::fmt::{Display, Write};
use crate::writer::*;
+1 -1
View File
@@ -1,6 +1,6 @@
use dioxus::prelude::*;
#[inline_props]
#[component]
pub fn Explainer<'a>(
cx: Scope<'a>,
invert: bool,
-3
View File
@@ -11,13 +11,10 @@ keywords = ["dom", "ui", "gui", "react"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
dioxus-rsx = { workspace = true }
proc-macro2 = { version = "1.0.6", features = ["span-locations"] }
quote = "1.0"
syn = { version = "1.0.11", features = ["full", "extra-traits", "visit"] }
serde = { version = "1.0.136", features = ["derive"] }
owo-colors = { version = "3.5.0", features = ["supports-colors"] }
prettyplease = { workspace = true }
[dev-dependencies]
indoc = "2.0.3"
+5 -9
View File
@@ -1,32 +1,27 @@
# dioxus-check
[![Crates.io][crates-badge]][crates-url]
[![MIT licensed][mit-badge]][mit-url]
[![Build Status][actions-badge]][actions-url]
[![Discord chat][discord-badge]][discord-url]
[crates-badge]: https://img.shields.io/crates/v/dioxus-autofmt.svg
[crates-url]: https://crates.io/crates/dioxus-autofmt
[crates-url]: https://crates.io/crates/dioxus-check
[mit-badge]: https://img.shields.io/badge/license-MIT-blue.svg
[mit-url]: https://github.com/dioxuslabs/dioxus/blob/master/LICENSE
[actions-badge]: https://github.com/dioxuslabs/dioxus/actions/workflows/main.yml/badge.svg
[actions-url]: https://github.com/dioxuslabs/dioxus/actions?query=workflow%3ACI+branch%3Amaster
[discord-badge]: https://img.shields.io/discord/899851952891002890.svg?logo=discord&style=flat-square
[discord-url]: https://discord.gg/XgGxMSkvUM
[Website](https://dioxuslabs.com) |
[Guides](https://dioxuslabs.com/docs/0.3/guide/en/) |
[API Docs](https://docs.rs/dioxus-autofmt/latest/dioxus_autofmt) |
[Guides](https://dioxuslabs.com/learn/0.4/) |
[API Docs](https://docs.rs/dioxus-check) |
[Chat](https://discord.gg/XgGxMSkvUM)
## Overview
`dioxus-check` analyzes Dioxus source code and reports errors and warnings. Primarily, it enforces the [Rules of Hooks](https://dioxuslabs.com/docs/0.3/guide/en/interactivity/hooks.html#no-hooks-in-conditionals).
`dioxus-check` analyzes Dioxus source code and reports errors and warnings. Primarily, it enforces the [Rules of Hooks](https://dioxuslabs.com/learn/0.4/reference/hooks#rules-of-hooks).
## Contributing
@@ -34,6 +29,7 @@
- Join the discord and ask questions!
## License
This project is licensed under the [MIT license].
[mit license]: https://github.com/DioxusLabs/dioxus/blob/master/LICENSE-MIT
+3 -3
View File
@@ -158,11 +158,11 @@ impl Display for IssueReport {
#[allow(clippy::enum_variant_names)] // we'll add non-hook ones in the future
/// Issues that might be found via static analysis of a Dioxus file.
pub enum Issue {
/// https://dioxuslabs.com/docs/0.3/guide/en/interactivity/hooks.html#no-hooks-in-conditionals
/// https://dioxuslabs.com/learn/0.4/reference/hooks#no-hooks-in-conditionals
HookInsideConditional(HookInfo, ConditionalInfo),
/// https://dioxuslabs.com/docs/0.3/guide/en/interactivity/hooks.html#no-hooks-in-loops
/// https://dioxuslabs.com/learn/0.4/reference/hooks#no-hooks-in-loops
HookInsideLoop(HookInfo, AnyLoopInfo),
/// https://dioxuslabs.com/docs/0.3/guide/en/interactivity/hooks.html#no-hooks-in-closures
/// https://dioxuslabs.com/learn/0.4/reference/hooks#no-hooks-in-closures
HookInsideClosure(HookInfo, ClosureInfo),
HookOutsideComponent(HookInfo),
}
+4
View File
@@ -1,3 +1,7 @@
#![doc = include_str!("../README.md")]
#![doc(html_logo_url = "https://avatars.githubusercontent.com/u/79236386")]
#![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")]
mod check;
mod issues;
mod metadata;
-31
View File
@@ -1,31 +0,0 @@
# .github/workflows/build.yml
on:
release:
types: [created]
jobs:
release:
name: release ${{ matrix.target }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- target: x86_64-unknown-linux-gnu
archive: tar.gz tar.xz
- target: x86_64-unknown-linux-musl
archive: tar.gz tar.xz
- target: x86_64-apple-darwin
archive: tar.gz tar.xz
- target: x86_64-pc-windows-gnu
archive: zip
steps:
- uses: actions/checkout@master
- name: Compile and release
uses: rust-build/rust-build.action@latest
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RUSTTARGET: ${{ matrix.target }}
ARCHIVE_TYPES: ${{ matrix.archive }}
-34
View File
@@ -1,34 +0,0 @@
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
-52
View File
@@ -1,52 +0,0 @@
on: [push, pull_request]
name: Rust CI
jobs:
check:
name: Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v1
- run: cargo check
test:
name: Test Suite
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v1
- run: cargo test
fmt:
name: Rustfmt
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v1
- run: rustup component add rustfmt
- run: cargo fmt --all -- --check
# clippy:
# name: Clippy
# runs-on: ubuntu-latest
# steps:
# - uses: actions/checkout@v2
# - uses: actions-rs/toolchain@v1
# with:
# profile: minimal
# toolchain: stable
# override: true
# - uses: Swatinem/rust-cache@v1
# - run: rustup component add clippy
# - uses: actions-rs/cargo@v1
# with:
# command: clippy
# args: -- -D warnings
-4778
View File
File diff suppressed because it is too large. Load diff
+2 -7
View File
@@ -22,16 +22,13 @@ serde = { version = "1.0.136", features = ["derive"] }
serde_json = "1.0.79"
toml = "0.5.8"
fs_extra = "1.2.0"
cargo_toml = "0.11.4"
cargo_toml = "0.16.0"
futures = "0.3.21"
notify = { version = "5.0.0-pre.16", features = ["serde"] }
html_parser = { workspace = true }
binary-install = "0.0.2"
convert_case = "0.5.0"
cargo_metadata = "0.15.0"
tokio = { version = "1.16.1", features = ["full"] }
tokio = { version = "1.16.1", features = ["fs", "sync", "rt", "macros"] }
atty = "0.2.14"
regex = "1.5.4"
chrono = "0.4.19"
anyhow = "1.0.53"
hyper = "0.14.17"
@@ -59,7 +56,6 @@ tar = "0.4.38"
zip = "0.6.2"
tower = "0.4.12"
syn = { version = "2.0", features = ["full", "extra-traits"] }
proc-macro2 = { version = "1.0", features = ["span-locations"] }
lazy_static = "1.4.0"
# plugin packages
@@ -71,7 +67,6 @@ mlua = { version = "0.8.1", features = [
"macros",
], optional = true }
ctrlc = "3.2.3"
gitignore = "1.0.7"
open = "4.1.0"
cargo-generate = "0.18"
toml_edit = "0.19.11"
+1 -1
View File
@@ -4,7 +4,7 @@
</div>
The **dioxus-cli** (inspired by wasm-pack and webpack) is a tool for getting Dioxus projects up and running.
It handles all building, bundling, development and publishing to simplify development.
It handles building, bundling, development and publishing to simplify development.
## Installation
-1
View File
@@ -1 +0,0 @@
book
-6
View File
@@ -1,6 +0,0 @@
[book]
authors = ["YuKun Liu"]
language = "en"
multilingual = false
src = "src"
title = "Dioxus CLI"
-13
View File
@@ -1,13 +0,0 @@
# Summary
- [Introduction](./introduction.md)
- [Installation](./installation.md)
- [Create a project](./creating.md)
- [Configure a project](./configure.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)
-197
View File
@@ -1,197 +0,0 @@
# Configure Project
This chapter will teach you how to configure the CLI with the `Dioxus.toml` file.
There's an [example](#config-example) which has comments to describe individual keys.
You can copy that or view this documentation for a more complete learning experience.
"🔒" indicates a mandatory item. Some headers are mandatory, but none of the keys inside them are. It might look weird, but it's normal. Simply don't include any keys.
## Structure
Each header has it's TOML form directly under it.
### Application 🔒
```toml
[application]
```
Application-wide configuration. Applies to both web and desktop.
1. **name** 🔒 - Project name & title.
```toml
name = "my_project"
```
2. **default_platform** 🔒 - The platform this project targets
```toml
# Currently supported platforms: web, desktop
default_platform = "web"
```
3. **out_dir** - The directory to place the build artifacts from `dx build` or `dx serve` into. This is also where the `assets` directory will be copied into.
```toml
out_dir = "dist"
```
4. **asset_dir** - The directory with your static assets. The CLI will automatically copy these assets into the **out_dir** after a build/serve.
```toml
asset_dir = "public"
```
5. **sub_package** - The sub package in the workspace to build by default.
```toml
sub_package = "my-crate"
```
### Web.App 🔒
```toml
[web.app]
```
Web-specific configuration.
1. **title** - The title of the web page.
```toml
# HTML title tag content
title = "project_name"
```
2. **base_path** - The base path to build the application for serving at. This can be useful when serving your application in a subdirectory under a domain. For example when building a site to be served on GitHub Pages.
```toml
# The application will be served at domain.com/my_application/, so we need to modify the base_path to the path where the application will be served
base_path = "my_application"
```
### Web.Watcher ✍
```toml
[web.watcher]
```
Development server configuration.
1. **reload_html** - If this is true, the cli will rebuild the index.html file every time the application is rebuilt
```toml
reload_html = true
```
2. **watch_path** - The files & directories to monitor for changes
```toml
watch_path = ["src", "public"]
```
3. **index_on_404** - If enabled, Dioxus will serve the root page when a route is not found.
*This is needed when serving an application that uses the router*.
However, when serving your app using something else than Dioxus (e.g. GitHub Pages), you will have to check how to configure it on that platform.
In GitHub Pages, you can make a copy of `index.html` named `404.html` in the same directory.
```toml
index_on_404 = true
```
### Web.Resource 🔒
```toml
[web.resource]
```
Static resource configuration.
1. **style** - CSS files to include in your application.
```toml
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** - JavaScript files to include in your application.
```toml
script = [
# Include from asset_dir.
"./public/index.js",
# Or from an online CDN.
"https://cdn.jsdelivr.net/npm/bootstrap/dist/js/bootstrap.js"
]
```
### Web.Resource.Dev 🔒
```toml
[web.resource.dev]
```
This is the same as [`[web.resource]`](#webresource-), but it only works in development servers.
For example, if you want to include a file in a `dx serve` server, but not a `dx serve --release` server, put it here.
### Web.Proxy
```toml
[[web.proxy]]
```
Configuration related to any proxies your application requires during development. Proxies will forward requests to a new service.
1. **backend** - The URL to the server to proxy. The CLI will forward any requests under the backend relative route to the backend instead of returning 404
```toml
backend = "http://localhost:8000/api/"
```
This will cause any requests made to the dev server with prefix /api/ to be redirected to the backend server at http://localhost:8000. The path and query parameters will be passed on as-is (path rewriting is currently not supported).
## Config example
This includes all fields, mandatory or not.
```toml
[application]
# App name
name = "project_name"
# The Dioxus platform to default to
default_platform = "web"
# `build` & `serve` output path
out_dir = "dist"
# The static resource path
asset_dir = "public"
[web.app]
# HTML title tag content
title = "project_name"
[web.watcher]
# When watcher is triggered, regenerate the `index.html`
reload_html = true
# Which files or dirs will be monitored
watch_path = ["src", "public"]
# Include style or script assets
[web.resource]
# CSS style file
style = []
# Javascript code file
script = []
[web.resource.dev]
# Same as [web.resource], but for development servers
# CSS style file
style = []
# JavaScript files
script = []
[[web.proxy]]
backend = "http://localhost:8000/api/"
```
-37
View File
@@ -1,37 +0,0 @@
# Create a Project
Once you have the Dioxus CLI installed, you can use it to create your own project!
## Initializing a default project
First, run the `dx create` command to create a new project:
```
dx create hello-dioxus
```
> It will clone this [template](https://github.com/DioxusLabs/dioxus-template).
> This default template is used for `web` platform application.
>
> You can choose to create your project from a different template by passing the `template` argument:
> ```
> dx init hello-dioxus --template=gh:dioxuslabs/dioxus-template
> ```
Next, navigate into your new project:
```
cd hello-dioxus
```
> Make sure the WASM target is installed before running the projects.
> You can install the WASM target for rust using rustup:
> ```
> rustup target add wasm32-unknown-unknown
> ```
Finally, serve your project:
```
dx serve
```
By default, the CLI serves your website at [`http://127.0.0.1:8080/`](http://127.0.0.1:8080/).
-23
View File
@@ -1,23 +0,0 @@
# Installation
## Install the latest development build through git
To get the latest bug fixes and features, you can install the development version from git.
```
cargo install --git https://github.com/Dioxuslabs/cli
```
This will download `Dioxus-CLI` source from GitHub master branch,
and install it in Cargo's global binary directory (`~/.cargo/bin/` by default).
## Install stable through `crates.io`
The published version of the Dioxus CLI is updated less often, but is more stable than the git version.
```
cargo install dioxus-cli --locked
```
Run `dx --help` for a list of all the available commands.
Furthermore, you can run `dx <COMMAND> --help` to get help with a specific command.
-18
View File
@@ -1,18 +0,0 @@
# Introduction
The 📦✨ **Dioxus CLI** is a tool to help get Dioxus projects off the ground.
## Features
* Build and pack a Dioxus project
* `html` to `rsx` conversion tool
* Hot Reload for `web` platform
* Create a Dioxus project from `git` repo
* And more!
<!-- Checkmarks don't render on the website, so I've just made a normal list. You can uncomment this if the website rendering is fixed.
- [x] `html` to `rsx` conversion tool
- [x] Hot Reload for `web` platform
- [x] Create a Dioxus project from `git` repo
- [x] Build & pack Dioxus project
- [ ] Automatically format Dioxus `rsx` code
-->
-140
View File
@@ -1,140 +0,0 @@
# CLI Plugin development
**IMPORTANT: Ignore this documentation. Plugins are yet to be released and chances are it won't work for you. This is just what plugins *could* look like.**
In the past we used `dx tool` to use and install tools, but it was a flawed system.
Tools were hard-coded by us, but people want more tools than we could code, so this plugin system was made to let
anyone develop plugins and use them in Dioxus projects.
Plugin resources:
* [Source code](https://github.com/DioxusLabs/dioxus/tree/master/packages/cli/src/plugin)
* [Unofficial Dioxus plugin community](https://github.com/DioxusPluginCommunity). Contains certain plugins you can use right now.
### Why Lua?
We chose Lua `5.4` to be the plugin developing language,
because it's extremely lightweight, embeddable and easy to learn.
We installed Lua into the CLI, so you don't need to do it yourself.
Lua resources:
* [Official website](https://www.lua.org/). You can basically find everything here.
* [Awesome Lua](https://github.com/LewisJEllis/awesome-lua). Additional resources (such as Lua plugins for your favorite IDE), and other *awesome* tools!
## Creating a plugin
A plugin is just an `init.lua` file.
You can include other files using `dofile(path)`.
You need to have a plugin and a manager instance, which you can get using `require`:
```lua
local plugin = require("plugin")
local manager = require("manager")
```
You need to set some `manager` fields and then initialize the plugin:
```lua
manager.name = "My first plugin"
manager.repository = "https://github.com/john-doe/my-first-plugin" -- The repository URL.
manager.author = "John Doe <john.doe@example.com>"
manager.version = "0.1.0"
plugin.init(manager)
```
You also need to return the `manager`, which basically represents your plugin:
```lua
-- Your code here.
-- End of file.
manager.serve.interval = 1000
return manager
```
And you're ready to go. Now, go and have a look at the stuff below and the API documentation.
### Plugin info
You will encounter this type in the events below. The keys are as follows:
* `name: string` - The name of the plugin.
* `repository: string` - The plugin repository URL.
* `author: string` - The author of the plugin.
* `version: string` - The plugin version.
### Event management
The plugin library has certain events that you can subscribe to.
* `manager.on_init` - Triggers the first time the plugin is loaded.
* `manager.build.on_start(info)` - Triggers before the build process. E.g., before `dx build`.
* `manager.build.on_finish(info)` - Triggers after the build process. E.g., after `dx build`.
* `manager.serve.on_start(info)` - Triggers before the serving process. E.g., before `dx serve`.
* `manager.serve.on_rebuild_start(info)` - Triggers before the server rebuilds the web with hot reload.
* `manager.serve.on_rebuild_end(info)` - Triggers after the server rebuilds the web with hot reload.
* `manager.serve.on_shutdown` - Triggers when the server is shutdown. E.g., when the `dx serve` process is terminated.
To subscribe to an event, you simply need to assign it to a function:
```lua
manager.build.on_start = function (info)
log.info("[plugin] Build starting: " .. info.name)
end
```
### 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
```
@@ -1,21 +0,0 @@
# Command Functions
You can use command functions to execute code and scripts.
Type definition:
```
Stdio: "Inherit" | "Piped" | "Null"
```
### `exec(commands: [string], stdout: Stdio, stderr: Stdio)`
You can use this function to run some commands 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 doesn't catch exceptions.
@@ -1,30 +0,0 @@
# Dirs Functions
Dirs functions are for getting various directory paths. Not to be confused with `plugin.path`.
### `plugin_dir() -> string`
Get the plugin's root directory path.
```lua
local path = plugin.dirs.plugin_dir()
-- example: ~/Development/DioxusCli/plugin/test-plugin/
```
### `bin_dir() -> string`
Get the plugin's binary directory path. Put binary files like `tailwind-cli` or `sass-cli` in this directory.
```lua
local path = plugin.dirs.bin_dir()
-- example: ~/Development/DioxusCli/plugin/test-plugin/bin/
```
### `temp_dir() -> string`
Get the plugin's temporary directory path. Put any temporary files here.
```lua
local path = plugin.dirs.bin_dir()
-- example: ~/Development/DioxusCli/plugin/test-plugin/temp/
```
@@ -1,48 +0,0 @@
# Log Functions
You can use log functions to print various logging information.
### `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")
```
@@ -1,37 +0,0 @@
# Network Functions
You can use Network functions to download & read some data from the internet.
### `download_file(url: string, path: string) -> boolean`
Downloads a file from the specified URL,
and returns a `boolean` that represents the download status (true: success, false: failure).
You need to 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`
Clone a repository from the given URL into the given path.
Returns a `boolean` that represents the clone status (true: success, false: failure).
The system executing this function must have git installed.
```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
```
@@ -1,11 +0,0 @@
# OS Functions
OS functions are for getting system information.
### `current_platform() -> string ("windows" | "macos" | "linux")`
Get the current OS platform.
```lua
local platform = plugin.os.current_platform()
```
@@ -1,38 +0,0 @@
# Path Functions
You can use path functions to perform operations on valid path strings.
### `join(path: string, extra: string) -> string`
<!-- TODO: Add specifics.
From the example given, it seems like it just creates a subdirectory path.
What would it do when "extending" file paths? -->
Extend a path; you can extend both directory and file paths.
```lua
local current_path = "~/hello/dioxus"
local new_path = plugin.path.join(current_path, "world")
-- new_path = "~/hello/dioxus/world"
```
### `parent(path: string) -> string`
Return the parent path of the specified path. The parent path is always a directory.
```lua
local current_path = "~/hello/dioxus"
local new_path = plugin.path.parent(current_path)
-- new_path = "~/hello/"
```
### `exists(path: string) -> boolean`
Check if the specified path exists, as either a file or a directory.
### `is_file(path: string) -> boolean`
Check if the specified path is a file.
### `is_dir(path: string) -> boolean`
Check if the specified path is a directory.
-18
View File
@@ -1,18 +0,0 @@
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
@@ -1,25 +0,0 @@
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
+1 -1
View File
@@ -23,7 +23,7 @@ title = "Dioxus | An elegant GUI library for Rust"
index_on_404 = true
watch_path = ["src"]
watch_path = ["src", "examples"]
# include `assets` in web platform
[web.resource]
+7 -38
View File
@@ -254,6 +254,7 @@ pub fn build_desktop(config: &CrateConfig, _is_serve: bool) -> Result<BuildResul
let mut cmd = subprocess::Exec::cmd("cargo")
.cwd(&config.crate_dir)
.arg("build")
.arg("--quiet")
.arg("--message-format=json");
if config.release {
@@ -312,7 +313,7 @@ pub fn build_desktop(config: &CrateConfig, _is_serve: bool) -> Result<BuildResul
if !config.out_dir.is_dir() {
create_dir_all(&config.out_dir)?;
}
copy(res_path, &config.out_dir.join(target_file))?;
copy(res_path, config.out_dir.join(target_file))?;
// this code will copy all public file to the output dir
if config.asset_dir.is_dir() {
@@ -442,14 +443,14 @@ pub fn gen_page(config: &DioxusConfig, serve: bool) -> String {
String::from(include_str!("./assets/index.html"))
};
let resouces = config.web.resource.clone();
let resources = config.web.resource.clone();
let mut style_list = resouces.style.unwrap_or_default();
let mut script_list = resouces.script.unwrap_or_default();
let mut style_list = resources.style.unwrap_or_default();
let mut script_list = resources.script.unwrap_or_default();
if serve {
let mut dev_style = resouces.dev.style.clone().unwrap_or_default();
let mut dev_script = resouces.dev.script.unwrap_or_default();
let mut dev_style = resources.dev.style.clone().unwrap_or_default();
let mut dev_script = resources.dev.script.unwrap_or_default();
style_list.append(&mut dev_style);
script_list.append(&mut dev_script);
}
@@ -688,35 +689,3 @@ fn build_assets(config: &CrateConfig) -> Result<Vec<PathBuf>> {
Ok(result)
}
// use binary_install::{Cache, Download};
// /// Attempts to find `wasm-opt` in `PATH` locally, or failing that downloads a
// /// precompiled binary.
// ///
// /// Returns `Some` if a binary was found or it was successfully downloaded.
// /// Returns `None` if a binary wasn't found in `PATH` and this platform doesn't
// /// have precompiled binaries. Returns an error if we failed to download the
// /// binary.
// pub fn find_wasm_opt(
// cache: &Cache,
// install_permitted: bool,
// ) -> Result<install::Status, failure::Error> {
// // First attempt to look up in PATH. If found assume it works.
// if let Ok(path) = which::which("wasm-opt") {
// PBAR.info(&format!("found wasm-opt at {:?}", path));
// match path.as_path().parent() {
// Some(path) => return Ok(install::Status::Found(Download::at(path))),
// None => {}
// }
// }
// let version = "version_78";
// Ok(install::download_prebuilt(
// &install::Tool::WasmOpt,
// cache,
// version,
// install_permitted,
// )?)
// }
+17 -7
View File
@@ -46,18 +46,28 @@ impl Autoformat {
// Format single file
if let Some(file) = self.file {
let file_content = fs::read_to_string(&file);
let file_content = if file == "-" {
let mut contents = String::new();
std::io::stdin().read_to_string(&mut contents)?;
Ok(contents)
} else {
fs::read_to_string(&file)
};
match file_content {
Ok(s) => {
let edits = dioxus_autofmt::fmt_file(&s);
let out = dioxus_autofmt::apply_formats(&s, edits);
match fs::write(&file, out) {
Ok(_) => {
println!("formatted {}", file);
}
Err(e) => {
eprintln!("failed to write formatted content to file: {}", e);
if file == "-" {
print!("{}", out);
} else {
match fs::write(&file, out) {
Ok(_) => {
println!("formatted {}", file);
}
Err(e) => {
eprintln!("failed to write formatted content to file: {}", e);
}
}
}
}
+4 -4
View File
@@ -132,10 +132,10 @@ impl Bundle {
.project_out_directory(crate_config.out_dir)
.package_settings(PackageSettings {
product_name: crate_config.dioxus_config.application.name.clone(),
version: package.version,
description: package.description.unwrap_or_default(),
homepage: package.homepage,
authors: Some(package.authors),
version: package.version().to_string(),
description: package.description().unwrap_or_default().to_string(),
homepage: Some(package.homepage().unwrap_or_default().to_string()),
authors: Some(Vec::from(package.authors())),
default_run: Some(crate_config.dioxus_config.application.name.clone()),
})
.binaries(binaries)
+1 -1
View File
@@ -105,7 +105,7 @@ impl Default for DioxusConfig {
},
proxy: Some(vec![]),
watcher: WebWatcherConfig {
watch_path: Some(vec![PathBuf::from("src")]),
watch_path: Some(vec![PathBuf::from("src"), PathBuf::from("examples")]),
reload_html: Some(false),
index_on_404: Some(true),
},
+4
View File
@@ -1,3 +1,7 @@
#![doc = include_str!("../README.md")]
#![doc(html_logo_url = "https://avatars.githubusercontent.com/u/79236386")]
#![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")]
pub const DIOXUS_CLI_VERSION: &str = "0.4.1";
pub mod builder;
+70 -88
View File
@@ -1,7 +1,7 @@
use crate::{
server::{
output::{print_console_info, PrettierOptions},
setup_file_watcher, setup_file_watcher_hot_reload,
setup_file_watcher,
},
BuildResult, CrateConfig, Result,
};
@@ -19,6 +19,8 @@ use tokio::sync::broadcast::{self};
#[cfg(feature = "plugin")]
use plugin::PluginManager;
use super::HotReloadState;
pub async fn startup(config: CrateConfig) -> Result<()> {
// ctrl-c shutdown checker
let _crate_config = config.clone();
@@ -28,16 +30,36 @@ pub async fn startup(config: CrateConfig) -> Result<()> {
std::process::exit(0);
});
match config.hot_reload {
true => serve_hot_reload(config).await?,
false => serve_default(config).await?,
}
let hot_reload_state = match config.hot_reload {
true => {
let FileMapBuildResult { map, errors } =
FileMap::<HtmlCtx>::create(config.crate_dir.clone()).unwrap();
for err in errors {
log::error!("{}", err);
}
let file_map = Arc::new(Mutex::new(map));
let hot_reload_tx = broadcast::channel(100).0;
clear_paths();
Some(HotReloadState {
messages: hot_reload_tx.clone(),
file_map: file_map.clone(),
})
}
false => None,
};
serve(config, hot_reload_state).await?;
Ok(())
}
/// Start the server without hot reload
pub async fn serve_default(config: CrateConfig) -> Result<()> {
pub async fn serve(config: CrateConfig, hot_reload_state: Option<HotReloadState>) -> Result<()> {
let (child, first_build_result) = start_desktop(&config)?;
let currently_running_child: RwLock<Child> = RwLock::new(child);
@@ -59,6 +81,7 @@ pub async fn serve_default(config: CrateConfig) -> Result<()> {
},
&config,
None,
hot_reload_state.clone(),
)
.await?;
@@ -73,105 +96,62 @@ pub async fn serve_default(config: CrateConfig) -> Result<()> {
None,
);
std::future::pending::<()>().await;
match hot_reload_state {
Some(hot_reload_state) => {
start_desktop_hot_reload(hot_reload_state).await?;
}
None => {
std::future::pending::<()>().await;
}
}
Ok(())
}
/// Start the server without hot reload
/// Start dx serve with hot reload
pub async fn serve_hot_reload(config: CrateConfig) -> Result<()> {
let (_, first_build_result) = start_desktop(&config)?;
println!("🚀 Starting development server...");
// Setup hot reload
let FileMapBuildResult { map, errors } =
FileMap::<HtmlCtx>::create(config.crate_dir.clone()).unwrap();
println!("🚀 Starting development server...");
for err in errors {
log::error!("{}", err);
}
let file_map = Arc::new(Mutex::new(map));
let (hot_reload_tx, mut hot_reload_rx) = broadcast::channel(100);
// States
// The open interprocess sockets
let channels = Arc::new(Mutex::new(Vec::new()));
// Setup file watcher
// We got to own watcher so that it exists for the duration of serve
// Otherwise hot reload won't work.
let _watcher = setup_file_watcher_hot_reload(
&config,
hot_reload_tx,
file_map.clone(),
{
let config = config.clone();
let channels = channels.clone();
move || {
for channel in &mut *channels.lock().unwrap() {
send_msg(HotReloadMsg::Shutdown, channel);
}
Ok(start_desktop(&config)?.1)
}
},
None,
)
.await?;
// Print serve info
print_console_info(
&config,
PrettierOptions {
changed: vec![],
warnings: first_build_result.warnings,
elapsed_time: first_build_result.elapsed_time,
},
None,
);
clear_paths();
async fn start_desktop_hot_reload(hot_reload_state: HotReloadState) -> Result<()> {
match LocalSocketListener::bind("@dioxusin") {
Ok(local_socket_stream) => {
let aborted = Arc::new(Mutex::new(false));
// States
// The open interprocess sockets
let channels = Arc::new(Mutex::new(Vec::new()));
// listen for connections
std::thread::spawn({
let file_map = file_map.clone();
let file_map = hot_reload_state.file_map.clone();
let channels = channels.clone();
let aborted = aborted.clone();
let _ = local_socket_stream.set_nonblocking(true);
move || {
loop {
if let Ok(mut connection) = local_socket_stream.accept() {
// send any templates than have changed before the socket connected
let templates: Vec<_> = {
file_map
.lock()
.unwrap()
.map
.values()
.filter_map(|(_, template_slot)| *template_slot)
.collect()
};
for template in templates {
if !send_msg(
HotReloadMsg::UpdateTemplate(template),
&mut connection,
) {
continue;
match local_socket_stream.accept() {
Ok(mut connection) => {
// send any templates than have changed before the socket connected
let templates: Vec<_> = {
file_map
.lock()
.unwrap()
.map
.values()
.filter_map(|(_, template_slot)| *template_slot)
.collect()
};
for template in templates {
if !send_msg(
HotReloadMsg::UpdateTemplate(template),
&mut connection,
) {
continue;
}
}
channels.lock().unwrap().push(connection);
println!("Connected to hot reloading 🚀");
}
Err(err) => {
if err.kind() != std::io::ErrorKind::WouldBlock {
println!("Error connecting to hot reloading: {} (Hot reloading is a feature of the dioxus-cli. If you are not using the CLI, this error can be ignored)", err);
}
}
channels.lock().unwrap().push(connection);
println!("Connected to hot reloading 🚀");
}
if *aborted.lock().unwrap() {
break;
@@ -180,6 +160,8 @@ pub async fn serve_hot_reload(config: CrateConfig) -> Result<()> {
}
});
let mut hot_reload_rx = hot_reload_state.messages.subscribe();
while let Ok(template) = hot_reload_rx.recv().await {
let channels = &mut *channels.lock().unwrap();
let mut i = 0;
+83 -127
View File
@@ -9,7 +9,7 @@ use std::{
path::PathBuf,
sync::{Arc, Mutex},
};
use tokio::sync::broadcast::Sender;
use tokio::sync::broadcast::{self};
mod output;
use output::*;
@@ -21,6 +21,7 @@ async fn setup_file_watcher<F: Fn() -> Result<BuildResult> + Send + 'static>(
build_with: F,
config: &CrateConfig,
web_info: Option<WebServerInfo>,
hot_reload: Option<HotReloadState>,
) -> Result<RecommendedWatcher> {
let mut last_update_time = chrono::Local::now().timestamp();
@@ -31,152 +32,107 @@ async fn setup_file_watcher<F: Fn() -> Result<BuildResult> + Send + 'static>(
.watcher
.watch_path
.clone()
.unwrap_or_else(|| vec![PathBuf::from("src")]);
.unwrap_or_else(|| vec![PathBuf::from("src"), PathBuf::from("examples")]);
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_with() {
Ok(res) => {
last_update_time = chrono::Local::now().timestamp();
let mut needs_full_rebuild;
if let Some(hot_reload) = &hot_reload {
// find changes to the rsx in the file
let mut rsx_file_map = hot_reload.file_map.lock().unwrap();
let mut messages: Vec<Template<'static>> = Vec::new();
#[allow(clippy::redundant_clone)]
print_console_info(
&config,
PrettierOptions {
changed: e.paths.clone(),
warnings: res.warnings,
elapsed_time: res.elapsed_time,
},
web_info.clone(),
);
// In hot reload mode, we only need to rebuild if non-rsx code is changed
needs_full_rebuild = false;
#[cfg(feature = "plugin")]
let _ = PluginManager::on_serve_rebuild(
chrono::Local::now().timestamp(),
e.paths,
);
for path in &e.paths {
// if this is not a rust file, rebuild the whole project
if path.extension().and_then(|p| p.to_str()) != Some("rs") {
needs_full_rebuild = true;
break;
}
match rsx_file_map.update_rsx(path, &config.crate_dir) {
Ok(UpdateResult::UpdatedRsx(msgs)) => {
messages.extend(msgs);
needs_full_rebuild = false;
}
Ok(UpdateResult::NeedsRebuild) => {
needs_full_rebuild = true;
}
Err(err) => {
log::error!("{}", err);
}
}
}
if needs_full_rebuild {
// Reset the file map to the new state of the project
let FileMapBuildResult {
map: new_file_map,
errors,
} = FileMap::<HtmlCtx>::create(config.crate_dir.clone()).unwrap();
for err in errors {
log::error!("{}", err);
}
*rsx_file_map = new_file_map;
} else {
for msg in messages {
let _ = hot_reload.messages.send(msg);
}
}
} else {
needs_full_rebuild = true;
}
if needs_full_rebuild {
match build_with() {
Ok(res) => {
last_update_time = chrono::Local::now().timestamp();
#[allow(clippy::redundant_clone)]
print_console_info(
&config,
PrettierOptions {
changed: e.paths.clone(),
warnings: res.warnings,
elapsed_time: res.elapsed_time,
},
web_info.clone(),
);
#[cfg(feature = "plugin")]
let _ = PluginManager::on_serve_rebuild(
chrono::Local::now().timestamp(),
e.paths,
);
}
Err(e) => log::error!("{}", e),
}
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();
}
Ok(watcher)
}
// Todo: reduce duplication and merge with setup_file_watcher()
/// Sets up a file watcher with hot reload
async fn setup_file_watcher_hot_reload<F: Fn() -> Result<BuildResult> + Send + 'static>(
config: &CrateConfig,
hot_reload_tx: Sender<Template<'static>>,
file_map: Arc<Mutex<FileMap<HtmlCtx>>>,
build_with: F,
web_info: Option<WebServerInfo>,
) -> Result<RecommendedWatcher> {
// 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 last_update_time = chrono::Local::now().timestamp();
let mut watcher = RecommendedWatcher::new(
move |evt: notify::Result<notify::Event>| {
let config = watcher_config.clone();
// Give time for the change to take effect before reading the file
std::thread::sleep(std::time::Duration::from_millis(100));
if chrono::Local::now().timestamp() > last_update_time {
if let Ok(evt) = evt {
let mut messages: Vec<Template<'static>> = Vec::new();
for path in evt.paths.clone() {
// if this is not a rust file, rebuild the whole project
if path.extension().and_then(|p| p.to_str()) != Some("rs") {
match build_with() {
Ok(res) => {
print_console_info(
&config,
PrettierOptions {
changed: evt.paths,
warnings: res.warnings,
elapsed_time: res.elapsed_time,
},
web_info.clone(),
);
}
Err(err) => {
log::error!("{}", err);
}
}
return;
}
// find changes to the rsx in the file
let mut map = file_map.lock().unwrap();
match map.update_rsx(&path, &config.crate_dir) {
Ok(UpdateResult::UpdatedRsx(msgs)) => {
messages.extend(msgs);
}
Ok(UpdateResult::NeedsRebuild) => {
match build_with() {
Ok(res) => {
print_console_info(
&config,
PrettierOptions {
changed: evt.paths,
warnings: res.warnings,
elapsed_time: res.elapsed_time,
},
web_info.clone(),
);
}
Err(err) => {
log::error!("{}", err);
}
}
return;
}
Err(err) => {
log::error!("{}", err);
}
}
}
for msg in messages {
let _ = hot_reload_tx.send(msg);
}
}
last_update_time = chrono::Local::now().timestamp();
}
},
notify::Config::default(),
)
.unwrap();
for sub_path in allow_watch_path {
if let Err(err) = watcher.watch(
&config.crate_dir.join(&sub_path),
&config.crate_dir.join(sub_path),
notify::RecursiveMode::Recursive,
) {
log::error!("error watching {sub_path:?}: \n{}", err);
log::error!("Failed to watch path: {}", err);
}
}
Ok(watcher)
}
#[derive(Clone)]
pub struct HotReloadState {
pub messages: broadcast::Sender<Template<'static>>,
pub file_map: Arc<Mutex<FileMap<HtmlCtx>>>,
}
+1 -1
View File
@@ -129,7 +129,7 @@ pub fn print_console_info(
log::warn!(
"{}",
format!(
"There were {} warning messages during the build.",
"There were {} warning messages during the build. Run `cargo check` to see them.",
options.warnings.len() - 1
)
.yellow()
+2 -14
View File
@@ -1,27 +1,15 @@
use std::sync::{Arc, Mutex};
use axum::{
extract::{ws::Message, WebSocketUpgrade},
response::IntoResponse,
Extension, TypedHeader,
};
use dioxus_core::Template;
use dioxus_html::HtmlCtx;
use dioxus_rsx::hot_reload::FileMap;
use tokio::sync::broadcast;
use crate::CrateConfig;
pub struct HotReloadState {
pub messages: broadcast::Sender<Template<'static>>,
pub file_map: Arc<Mutex<FileMap<HtmlCtx>>>,
pub watcher_config: CrateConfig,
}
use crate::server::HotReloadState;
pub async fn hot_reload_handler(
ws: WebSocketUpgrade,
_: Option<TypedHeader<headers::UserAgent>>,
Extension(state): Extension<Arc<HotReloadState>>,
Extension(state): Extension<HotReloadState>,
) -> impl IntoResponse {
ws.on_upgrade(|mut socket| async move {
log::info!("🔥 Hot Reload WebSocket connected");
+28 -89
View File
@@ -3,7 +3,7 @@ use crate::{
serve::Serve,
server::{
output::{print_console_info, PrettierOptions, WebServerInfo},
setup_file_watcher, setup_file_watcher_hot_reload,
setup_file_watcher, HotReloadState,
},
BuildResult, CrateConfig, Result, WebHttpsConfig,
};
@@ -58,20 +58,39 @@ pub async fn startup(port: u16, config: CrateConfig, start_browser: bool) -> Res
let ip = get_ip().unwrap_or(String::from("0.0.0.0"));
match config.hot_reload {
true => serve_hot_reload(ip, port, config, start_browser).await?,
false => serve_default(ip, port, config, start_browser).await?,
}
let hot_reload_state = match config.hot_reload {
true => {
let FileMapBuildResult { map, errors } =
FileMap::<HtmlCtx>::create(config.crate_dir.clone()).unwrap();
for err in errors {
log::error!("{}", err);
}
let file_map = Arc::new(Mutex::new(map));
let hot_reload_tx = broadcast::channel(100).0;
Some(HotReloadState {
messages: hot_reload_tx.clone(),
file_map: file_map.clone(),
})
}
false => None,
};
serve(ip, port, config, start_browser, hot_reload_state).await?;
Ok(())
}
/// Start the server without hot reload
pub async fn serve_default(
pub async fn serve(
ip: String,
port: u16,
config: CrateConfig,
start_browser: bool,
hot_reload_state: Option<HotReloadState>,
) -> Result<()> {
let first_build_result = crate::builder::build(&config, true)?;
@@ -93,6 +112,7 @@ pub async fn serve_default(
ip: ip.clone(),
port,
}),
hot_reload_state.clone(),
)
.await?;
@@ -119,88 +139,7 @@ pub async fn serve_default(
);
// Router
let router = setup_router(config, ws_reload_state, None).await?;
// Start server
start_server(port, router, start_browser, rustls_config).await?;
Ok(())
}
/// Start dx serve with hot reload
pub async fn serve_hot_reload(
ip: String,
port: u16,
config: CrateConfig,
start_browser: bool,
) -> Result<()> {
let first_build_result = crate::builder::build(&config, true)?;
log::info!("🚀 Starting development server...");
// Setup hot reload
let (reload_tx, _) = broadcast::channel(100);
let FileMapBuildResult { map, errors } =
FileMap::<HtmlCtx>::create(config.crate_dir.clone()).unwrap();
for err in errors {
log::error!("{}", err);
}
let file_map = Arc::new(Mutex::new(map));
let hot_reload_tx = broadcast::channel(100).0;
// States
let hot_reload_state = Arc::new(HotReloadState {
messages: hot_reload_tx.clone(),
file_map: file_map.clone(),
watcher_config: config.clone(),
});
let ws_reload_state = Arc::new(WsReloadState {
update: reload_tx.clone(),
});
// Setup file watcher
// We got to own watcher so that it exists for the duration of serve
// Otherwise hot reload won't work.
let _watcher = setup_file_watcher_hot_reload(
&config,
hot_reload_tx,
file_map,
{
let config = config.clone();
let reload_tx = reload_tx.clone();
move || build(&config, &reload_tx)
},
Some(WebServerInfo {
ip: ip.clone(),
port,
}),
)
.await?;
// HTTPS
// Before console info so it can stop if mkcert isn't installed or fails
let rustls_config = get_rustls(&config).await?;
// Print serve info
print_console_info(
&config,
PrettierOptions {
changed: vec![],
warnings: first_build_result.warnings,
elapsed_time: first_build_result.elapsed_time,
},
Some(WebServerInfo {
ip: ip.clone(),
port,
}),
);
// Router
let router = setup_router(config, ws_reload_state, Some(hot_reload_state)).await?;
let router = setup_router(config, ws_reload_state, hot_reload_state).await?;
// Start server
start_server(port, router, start_browser, rustls_config).await?;
@@ -291,7 +230,7 @@ fn get_rustls_without_mkcert(web_config: &WebHttpsConfig) -> Result<(String, Str
async fn setup_router(
config: CrateConfig,
ws_reload: Arc<WsReloadState>,
hot_reload: Option<Arc<HotReloadState>>,
hot_reload: Option<HotReloadState>,
) -> Result<Router> {
// Setup cors
let cors = CorsLayer::new()
+3
View File
@@ -17,6 +17,9 @@ proc-macro2 = { version = "1.0" }
quote = "1.0"
syn = { version = "2.0", features = ["full", "extra-traits"] }
dioxus-rsx = { workspace = true }
dioxus-core = { workspace = true }
constcat = "0.3.0"
prettyplease = "0.2.15"
# testing
[dev-dependencies]
+8 -10
View File
@@ -1,6 +1,5 @@
# dioxus-core-macro
[![Crates.io][crates-badge]][crates-url]
[![MIT licensed][mit-badge]][mit-url]
[![Build Status][actions-badge]][actions-url]
@@ -8,30 +7,28 @@
[crates-badge]: https://img.shields.io/crates/v/dioxus-core-macro.svg
[crates-url]: https://crates.io/crates/dioxus-core-macro
[mit-badge]: https://img.shields.io/badge/license-MIT-blue.svg
[mit-url]: https://github.com/dioxuslabs/dioxus/blob/master/LICENSE
[actions-badge]: https://github.com/dioxuslabs/dioxus/actions/workflows/main.yml/badge.svg
[actions-url]: https://github.com/dioxuslabs/dioxus/actions?query=workflow%3ACI+branch%3Amaster
[discord-badge]: https://img.shields.io/discord/899851952891002890.svg?logo=discord&style=flat-square
[discord-url]: https://discord.gg/XgGxMSkvUM
[Website](https://dioxuslabs.com) |
[Guides](https://dioxuslabs.com/docs/0.3/guide/en/) |
[Guides](https://dioxuslabs.com/learn/0.4/) |
[API Docs](https://docs.rs/dioxus-core-macro/latest/dioxus_core_macro) |
[Chat](https://discord.gg/XgGxMSkvUM)
## Overview
`dioxus-core-macro` provides a handful of helpful macros used by the `dioxus` crate. These include:
- The `rsx!` macro that underpins templates and node creation
- The `inline_props` macro transforms function arguments into an auto-derived struct
- The `format_args_f` macro which allows f-string formatting with support for expressions
- The `rsx!` macro that underpins templates and node creation.
- The `component` attribute macro denotes a function as a Dioxus component. Currently, this:
- Transforms function arguments into an auto-derived struct.
- Ensures that your component name uses PascalCase.
- Probably more stuff in the future. This macro allows us to have a way of distinguishing functions and components, which can be quite handy.
- The `format_args_f` macro which allows f-string formatting with support for expressions.
## Contributing
@@ -39,6 +36,7 @@
- Join the discord and ask questions!
## License
This project is licensed under the [MIT license].
[mit license]: https://github.com/DioxusLabs/dioxus/blob/master/LICENSE-MIT
@@ -0,0 +1,237 @@
//! This module is used for parsing a component function into a struct that is subsequently
//! deserialized into something useful using deserializer arguments.
//!
//! Let's break that down with a term glossary and examples which show usage and implementing.
//!
//! # Glossary
//! * `component body` - The [`ComponentBody`] struct. It's used to parse a component function [`proc_macro::TokenStream`]
//! to a reusable struct that deserializers use to modify the token stream.
//! * `deserializer` - A struct that deserializes the [`ComponentBody`] into a [`DeserializerOutput`].
//! It implements the [`DeserializerArgs`] trait, but as you can see, it's called "DeserializerArgs",
//! not "Deserializer". Why?
//! Because "args" makes more sense to the caller of [`ComponentBody::deserialize`], which
//! takes an [`DeserializerArgs`] argument. However, you can think of "DeserializerArgs" as the deserializer.
//! * `deserializer output` - A struct that implements the [`DeserializerOutput`] trait.
//! This struct is what enables deserializers to use each other, since it contains the fields that
//! a deserializer needs to turn a token stream to a different token stream.
//! This means a deserializer can get the output of another deserializer, and use that output,
//! thereby using the functionality of a different deserializer.
//! This struct also implements [`ToTokens`], which means that this is the final stage of the whole process.
//!
//! # Examples
//! *Not all imports might be included.*
//!
//! ## Usage in a procedural macro attribute
//! ```rs,ignore
//! use proc_macro::TokenStream;
//!
//! // Some documentation. You can reuse this in your deserializer structs.
//! /// This attribute changes the name of a component function to whatever the first argument is.
//! #[proc_macro_attribute]
//! pub fn name_changer(args: TokenStream, input: TokenStream) -> TokenStream {
//! // Parse the component body.
//! let component_body = parse_macro_input!(input as ComponentBody);
//!
//! // Parse the first argument, which is going to be the components new name.
//! let new_name: String = match Punctuated::<Path, Token![,]>::parse_terminated.parse(args) {
//! Err(e) => return e.to_compile_error().into(), // Convert to a compile error and return
//! Ok(args) => {
//! // If the argument exists, then convert it to a string
//! if let Some(first) = args.first() {
//! first.to_token_stream().to_string()
//! } else {
//! // If the argument doesn't exist, return an error with the appropriate message.
//! // The "span" is the location of some code.
//! // The error occurred in the "args" token stream, so point the error there.
//! return Error::new(args.span(), "No new name provided").to_compile_error().into();
//! }
//! }
//! };
//!
//! let new_name = &*new_name;
//!
//! // Deserialize the component body to an output with the given args.
//! let output = component_body.deserialize(NameChangerDeserializerArgs { new_name });
//!
//! // Error handling like before, except now you're ready to return the final value.
//! match output {
//! Err(e) => e.to_compile_error().into(),
//! Ok(output) => output.to_token_stream().into(),
//! }
//! }
//! ```
//! ## Using the macro in Dioxus code:
//! ```rs
//! use your_proc_macro_library::name_changer;
//! use dioxus::prelude::*;
//!
//! #[name_changer(CoolName)]
//! pub fn LameName(cx: Scope) -> Element {
//! render! { "I want a cool name!" }
//! }
//!
//! pub fn App(cx: Scope) -> Element {
//! render! { CoolName {} } // Renders: "I want a cool name!"
//! }
//! ```
//! ## Implementing a component body deserializer
//! ```rs
//! use syn::{Result, ItemFn, Signature, Ident};
//! use quote::quote;
//!
//! // Create a list of arguments.
//! // If there was no args, just make it empty. The "args" struct is also the deserializer struct.
//! // For the docs, you can basically copy paste this text and replace "name_changer" with your macro path.
//! // Although unfortunately, the link does not work
//! // Just make sure that your macro is well documented.
//! /// The args and deserializing implementation for the [`name_changer`] macro.
//! #[derive(Clone)]
//! pub struct NameChangerDeserializerArgs<'a> {
//! pub new_name: &'a str,
//! }
//!
//! // Create an output struct.
//! // The ItemFn represents a modified component function.
//! // To read what fields should be here, check out the `DeserializerOutput` struct docs.
//! // For the docs, you can basically copy paste this text and replace "name_changer" with your macro path.
//! // Just make sure that your macro is well documented.
//! /// The output fields and [`ToTokens`] implementation for the [`name_changer`] macro.
//! #[derive(Clone)]
//! pub struct NameChangerDeserializerOutput {
//! pub comp_fn: ItemFn,
//! }
//!
//! // Implement `ToTokens`, which is forced by `DeserializerOutput`.
//! // This will usually be very simple like this, even for complex deserializers.
//! // That's because of the way the `DeserializerOutput` is designed.
//! impl ToTokens for NameChangerDeserializerOutput {
//! fn to_tokens(&self, tokens: &mut TokenStream) {
//! let comp_fn = &self.comp_fn;
//!
//! tokens.append_all(quote! {
//! #comp_fn
//! });
//! }
//! }
//!
//! impl DeserializerOutput for NameChangerDeserializerOutput {}
//!
//! // Implement `DeserializerArgs`. This is the core part of deserializers.
//! impl<'a> DeserializerArgs<NameChangerDeserializerOutput> for NameChangerDeserializerArgs<'a> {
//! fn to_output(&self, component_body: &ComponentBody) -> Result<NameChangerDeserializerOutput> {
//! let old_fn = &component_body.item_fn;
//! let old_sig = &old_fn.sig;
//!
//! // For more complex uses, you will probably use `quote::parse_quote!` in combination with
//! // creating the structs manually.
//! // However, create the structs manually if you can.
//! // It's more reliable, because you only modify a certain struct field
//! // and set the others to be the clone of the original component body.
//! // That ensures that no information will be accidentally removed.
//! let new_sig = Signature {
//! ident: Ident::new(self.new_name, old_sig.ident.span()),
//! ..old_sig.clone()
//! };
//! let new_fn = ItemFn {
//! sig: new_sig,
//! ..old_fn.clone()
//! };
//!
//! Ok(NameChangerDeserializerOutput {
//! comp_fn: new_fn
//! })
//! }
//! ```
pub mod utils;
pub use utils::DeserializerArgs;
pub use utils::DeserializerOutput;
pub use utils::TypeHelper;
use dioxus_core::{Element, Scope};
use syn::parse::{Parse, ParseStream};
use syn::spanned::Spanned;
use syn::*;
/// General struct for parsing a component body.
/// However, because it's ambiguous, it does not implement [`ToTokens`](quote::to_tokens::ToTokens).
///
/// Refer to the [module documentation](crate::component_body) for more.
pub struct ComponentBody {
/// The component function definition. You can parse this back into a [`ComponentBody`].
/// For example, you might modify it, parse it into a [`ComponentBody`], and deserialize that
/// using some deserializer. This is how deserializers use other deserializers.
///
/// **`item_fn.sig.inputs` includes the context argument!**
/// Keep this in mind when creating deserializers, because you often might want to ignore it.
/// That might be annoying, but it would be bad design for this kind of struct to not be parsable from itself.
pub item_fn: ItemFn,
/// Parsing tries to ensure that this argument will be a [`Scope`].
/// **However, macros have limitations that prevent this from always working,
/// so don't take this for granted!**
pub cx_arg: FnArg,
/// The pattern (name) and type of the context argument.
pub cx_pat_type: PatType,
/// If the function has any arguments other than the context.
pub has_extra_args: bool,
}
impl ComponentBody {
/// Deserializes the body into the [`TOutput`] with the specific [`TArgs`].
/// Even if the args are empty, the [`TArg`] type still determines what [`TOutput`] will be generated.
pub fn deserialize<TOutput, TArgs>(&self, args: TArgs) -> Result<TOutput>
where
TOutput: DeserializerOutput,
TArgs: DeserializerArgs<TOutput>,
{
args.to_output(self)
}
}
impl Parse for ComponentBody {
fn parse(input: ParseStream) -> Result<Self> {
let item_fn: ItemFn = input.parse()?;
let scope_type_path = Scope::get_path_string();
let (cx_arg, cx_pat_type) = if let Some(first_arg) = item_fn.sig.inputs.first() {
let incorrect_first_arg_err = Err(Error::new(
first_arg.span(),
format!("First argument must be a <{}>", scope_type_path),
));
match first_arg.to_owned() {
FnArg::Receiver(_) => {
return incorrect_first_arg_err;
}
FnArg::Typed(f) => (first_arg.to_owned(), f),
}
} else {
return Err(Error::new(
item_fn.sig.ident.span(), // Or maybe just item_f.sig.span()?
format!(
"Must have at least one argument that's a <{}>",
scope_type_path
),
));
};
let element_type_path = Element::get_path_string();
if item_fn.sig.output == ReturnType::Default {
return Err(Error::new(
item_fn.sig.output.span(),
format!("Must return a <{}>", element_type_path),
));
}
let has_extra_args = item_fn.sig.inputs.len() > 1;
Ok(Self {
item_fn,
cx_arg,
cx_pat_type,
has_extra_args,
})
}
}
@@ -0,0 +1,57 @@
use crate::component_body::ComponentBody;
use dioxus_core::{Element, Scope};
use quote::ToTokens;
use syn::{parse_quote, Path};
/// The output produced by a deserializer.
///
/// # For implementors
/// Struct field guidelines:
/// * Must be public, so that other deserializers can utilize them.
/// * Should usually be [`Item`]s that you then simply combine in a [`quote!`]
/// in the [`ComponentBodyDeserializer::output_to_token_stream2`] function.
/// * If an [`Item`] might not be included, wrap it in an [`Option`].
/// * Must be at the component function "level"/"context".
/// For example, the [`InlinePropsDeserializer`](crate::component_body_deserializers::inline_props::InlinePropsDeserializer)
/// produces two [`Item`]s; the function but with arguments turned into props, and the props struct.
/// It does not return any [`Item`]s inside the struct or function.
pub trait DeserializerOutput: ToTokens {}
impl<T: ToTokens> DeserializerOutput for T {}
/// The args passed to a [`ComponentBody`] when deserializing it.
///
/// It's also the struct that does the deserializing.
/// It's called "DeserializerArgs", not "Deserializer". Why?
/// Because "args" makes more sense to the caller of [`ComponentBody::deserialize`], which
/// takes an [`DeserializerArgs`] argument. However, you can think of "DeserializerArgs" as the deserializer.
pub trait DeserializerArgs<TOutput>: Clone
where
TOutput: ToTokens,
{
// There's a lot of Results out there... let's make sure that this is a syn::Result.
// Let's also make sure there's not a warning.
/// Creates a [`ToTokens`] struct from the `self` args and a [`ComponentBody`].
/// The [`ComponentBody::deserialize`] provides a cleaner way of calling this function.
#[allow(unused_qualifications)]
fn to_output(&self, component_body: &ComponentBody) -> syn::Result<TOutput>;
}
pub trait TypeHelper {
fn get_path() -> Path;
fn get_path_string() -> String {
Self::get_path().to_token_stream().to_string()
}
}
impl<'a> TypeHelper for Scope<'a> {
fn get_path() -> Path {
parse_quote!(::dioxus::core::Scope)
}
}
impl<'a> TypeHelper for Element<'a> {
fn get_path() -> Path {
parse_quote!(::dioxus::core::Element)
}
}
@@ -0,0 +1,152 @@
use crate::component_body::{ComponentBody, DeserializerArgs};
use crate::component_body_deserializers::inline_props::InlinePropsDeserializerArgs;
use constcat::concat;
use proc_macro2::TokenStream as TokenStream2;
use quote::{quote, ToTokens, TokenStreamExt};
use syn::*;
pub(crate) const COMPONENT_ARG_CASE_CHECK_ERROR: &str = concat!(
"This component does not use PascalCase. \
To ignore this check, pass the \"",
crate::COMPONENT_ARG_CASE_CHECK_OFF,
"\" argument, like so: #[component(",
crate::COMPONENT_ARG_CASE_CHECK_OFF,
")]"
);
const INNER_FN_NAME: &str = "__dx_inner_comp";
fn get_out_comp_fn(orig_comp_fn: &ItemFn, cx_pat: &Pat) -> ItemFn {
let inner_comp_ident = Ident::new(INNER_FN_NAME, orig_comp_fn.sig.ident.span());
let inner_comp_fn = ItemFn {
sig: Signature {
ident: inner_comp_ident.clone(),
..orig_comp_fn.sig.clone()
},
..orig_comp_fn.clone()
};
ItemFn {
block: parse_quote! {
{
#[warn(non_snake_case)]
#[inline(always)]
#inner_comp_fn
#inner_comp_ident (#cx_pat)
}
},
..orig_comp_fn.clone()
}
}
/// The args and deserializing implementation for the [`crate::component`] macro.
#[derive(Clone)]
pub struct ComponentDeserializerArgs {
pub case_check: bool,
}
/// The output fields and [`ToTokens`] implementation for the [`crate::component`] macro.
#[derive(Clone)]
pub struct ComponentDeserializerOutput {
pub comp_fn: ItemFn,
pub props_struct: Option<ItemStruct>,
}
impl ToTokens for ComponentDeserializerOutput {
fn to_tokens(&self, tokens: &mut TokenStream2) {
let comp_fn = &self.comp_fn;
let props_struct = &self.props_struct;
tokens.append_all(quote! {
#props_struct
#[allow(non_snake_case)]
#comp_fn
});
}
}
impl DeserializerArgs<ComponentDeserializerOutput> for ComponentDeserializerArgs {
fn to_output(&self, component_body: &ComponentBody) -> Result<ComponentDeserializerOutput> {
let Signature { ident, .. } = &component_body.item_fn.sig;
if self.case_check && !is_pascal_case(&ident.to_string()) {
return Err(Error::new(ident.span(), COMPONENT_ARG_CASE_CHECK_ERROR));
}
if component_body.has_extra_args {
Self::deserialize_with_props(component_body)
} else {
Ok(Self::deserialize_no_props(component_body))
}
}
}
impl ComponentDeserializerArgs {
fn deserialize_no_props(component_body: &ComponentBody) -> ComponentDeserializerOutput {
let ComponentBody {
item_fn,
cx_pat_type,
..
} = component_body;
let cx_pat = &cx_pat_type.pat;
let comp_fn = get_out_comp_fn(item_fn, cx_pat);
ComponentDeserializerOutput {
comp_fn,
props_struct: None,
}
}
fn deserialize_with_props(
component_body: &ComponentBody,
) -> Result<ComponentDeserializerOutput> {
let ComponentBody {
item_fn,
cx_pat_type,
..
} = component_body;
let cx_pat = &cx_pat_type.pat;
let comp_parsed = match parse2::<ComponentBody>(quote!(#item_fn)) {
Ok(comp_body) => comp_body,
Err(e) => {
return Err(Error::new(
e.span(),
format!(
"This is probably a bug in our code, please report it! Error: {}",
e
),
))
}
};
let inlined_props_output = comp_parsed.deserialize(InlinePropsDeserializerArgs {})?;
let props_struct = inlined_props_output.props_struct;
let props_fn = inlined_props_output.comp_fn;
let comp_fn = get_out_comp_fn(&props_fn, cx_pat);
Ok(ComponentDeserializerOutput {
comp_fn,
props_struct: Some(props_struct),
})
}
}
fn is_pascal_case(input: &str) -> bool {
let mut is_next_lowercase = false;
for c in input.chars() {
let is_upper = c.is_ascii_uppercase();
if (c == '_') || (is_upper && is_next_lowercase) {
return false;
}
is_next_lowercase = is_upper;
}
true
}
@@ -0,0 +1,341 @@
use crate::component_body::{ComponentBody, DeserializerArgs};
use proc_macro2::TokenStream as TokenStream2;
use quote::{quote, ToTokens, TokenStreamExt};
use syn::token::Comma;
use syn::{punctuated::Punctuated, *};
/// The args and deserializing implementation for the [`crate::inline_props`] macro.
#[derive(Clone)]
pub struct InlinePropsDeserializerArgs;
/// The output fields and [`ToTokens`] implementation for the [`crate::inline_props`] macro.
#[derive(Clone)]
pub struct InlinePropsDeserializerOutput {
pub comp_fn: ItemFn,
pub props_struct: ItemStruct,
}
impl ToTokens for InlinePropsDeserializerOutput {
fn to_tokens(&self, tokens: &mut TokenStream2) {
let function = &self.comp_fn;
let props_struct = &self.props_struct;
tokens.append_all(quote! {
#function
#props_struct
});
}
}
impl DeserializerArgs<InlinePropsDeserializerOutput> for InlinePropsDeserializerArgs {
fn to_output(&self, component_body: &ComponentBody) -> Result<InlinePropsDeserializerOutput> {
Ok(InlinePropsDeserializerOutput {
comp_fn: get_function(component_body),
props_struct: get_props_struct(component_body),
})
}
}
fn get_props_struct(component_body: &ComponentBody) -> ItemStruct {
let ComponentBody { item_fn, .. } = component_body;
let ItemFn { vis, sig, .. } = item_fn;
let Signature {
inputs,
ident: fn_ident,
generics,
..
} = sig;
// Skip first arg since that's the context
let struct_fields = inputs.iter().skip(1).map(move |f| {
match f {
FnArg::Receiver(_) => unreachable!(), // Unreachable because of ComponentBody parsing
FnArg::Typed(pt) => {
let arg_pat = &pt.pat; // Pattern (identifier)
let arg_colon = &pt.colon_token;
let arg_ty = &pt.ty; // Type
let arg_attrs = &pt.attrs; // Attributes
quote! {
#(#arg_attrs)
*
#vis #arg_pat #arg_colon #arg_ty
}
}
}
});
let struct_ident = Ident::new(&format!("{fn_ident}Props"), fn_ident.span());
let first_lifetime = if let Some(GenericParam::Lifetime(lt)) = generics.params.first() {
Some(lt)
} else {
None
};
let struct_attrs = if first_lifetime.is_some() {
quote! { #[derive(Props)] }
} else {
quote! { #[derive(Props, PartialEq)] }
};
let struct_generics = if first_lifetime.is_some() {
let struct_generics: Punctuated<GenericParam, Comma> = component_body
.item_fn
.sig
.generics
.params
.iter()
.map(|it| match it {
GenericParam::Type(tp) => {
let mut tp = tp.clone();
tp.bounds.push(parse_quote!( 'a ));
GenericParam::Type(tp)
}
_ => it.clone(),
})
.collect();
quote! { <#struct_generics> }
} else {
quote! { #generics }
};
parse_quote! {
#struct_attrs
#[allow(non_camel_case_types)]
#vis struct #struct_ident #struct_generics
{
#(#struct_fields),*
}
}
}
fn get_props_docs(fn_ident: &Ident, inputs: Vec<&FnArg>) -> Vec<Attribute> {
if inputs.len() <= 1 {
return Vec::new();
}
let arg_docs = inputs
.iter()
.filter_map(|f| match f {
FnArg::Receiver(_) => unreachable!(), // ComponentBody prohibits receiver parameters.
FnArg::Typed(pt) => {
let arg_doc = pt
.attrs
.iter()
.filter_map(|attr| {
// TODO: Error reporting
// Check if the path of the attribute is "doc"
if !is_attr_doc(attr) {
return None;
};
let Meta::NameValue(meta_name_value) = &attr.meta else {
return None;
};
let Expr::Lit(doc_lit) = &meta_name_value.value else {
return None;
};
let Lit::Str(doc_lit_str) = &doc_lit.lit else {
return None;
};
Some(doc_lit_str.value())
})
.fold(String::new(), |mut doc, next_doc_line| {
doc.push('\n');
doc.push_str(&next_doc_line);
doc
});
Some((
&pt.pat,
&pt.ty,
pt.attrs.iter().find_map(|attr| {
if attr.path() != &parse_quote!(deprecated) {
return None;
}
let res = crate::utils::DeprecatedAttribute::from_meta(&attr.meta);
match res {
Err(e) => panic!("{}", e.to_string()),
Ok(v) => Some(v),
}
}),
arg_doc,
))
}
})
.collect::<Vec<_>>();
let mut props_docs = Vec::with_capacity(5);
let props_def_link = fn_ident.to_string() + "Props";
let header =
format!("# Props\n*For details, see the [props struct definition]({props_def_link}).*");
props_docs.push(parse_quote! {
#[doc = #header]
});
for (arg_name, arg_type, deprecation, input_arg_doc) in arg_docs {
let arg_name = arg_name.into_token_stream().to_string();
let arg_type = crate::utils::format_type_string(arg_type);
let input_arg_doc = keep_up_to_n_consecutive_chars(input_arg_doc.trim(), 2, '\n')
.replace("\n\n", "</p><p>");
let prop_def_link = format!("{props_def_link}::{arg_name}");
let mut arg_doc = format!("- [`{arg_name}`]({prop_def_link}) : `{arg_type}`");
if let Some(deprecation) = deprecation {
arg_doc.push_str("<p>👎 Deprecated");
if let Some(since) = deprecation.since {
arg_doc.push_str(&format!(" since {since}"));
}
if let Some(note) = deprecation.note {
let note = keep_up_to_n_consecutive_chars(&note, 1, '\n').replace('\n', " ");
let note = keep_up_to_n_consecutive_chars(&note, 1, '\t').replace('\t', " ");
arg_doc.push_str(&format!(": {note}"));
}
arg_doc.push_str("</p>");
if !input_arg_doc.is_empty() {
arg_doc.push_str("<hr/>");
}
}
if !input_arg_doc.is_empty() {
arg_doc.push_str(&format!("<p>{input_arg_doc}</p>"));
}
props_docs.push(parse_quote! {
#[doc = #arg_doc]
});
}
props_docs
}
fn get_function(component_body: &ComponentBody) -> ItemFn {
let ComponentBody {
item_fn,
cx_pat_type,
..
} = component_body;
let ItemFn {
attrs: fn_attrs,
vis,
sig,
block: fn_block,
} = item_fn;
let Signature {
inputs,
ident: fn_ident,
generics,
output: fn_output,
asyncness,
..
} = sig;
let Generics { where_clause, .. } = generics;
let cx_pat = &cx_pat_type.pat;
let struct_ident = Ident::new(&format!("{fn_ident}Props"), fn_ident.span());
// Skip first arg since that's the context
let struct_field_names = inputs.iter().skip(1).filter_map(|f| match f {
FnArg::Receiver(_) => unreachable!(), // ComponentBody prohibits receiver parameters.
FnArg::Typed(pt) => Some(&pt.pat),
});
let first_lifetime = if let Some(GenericParam::Lifetime(lt)) = generics.params.first() {
Some(lt)
} else {
None
};
let (scope_lifetime, fn_generics) = if let Some(lt) = first_lifetime {
(quote! { #lt, }, generics.clone())
} else {
let lifetime: LifetimeParam = parse_quote! { 'a };
let mut fn_generics = generics.clone();
fn_generics
.params
.insert(0, GenericParam::Lifetime(lifetime.clone()));
(quote! { #lifetime, }, fn_generics)
};
let generics_no_bounds = {
let mut generics = generics.clone();
generics.params = generics
.params
.iter()
.map(|it| match it {
GenericParam::Type(tp) => {
let mut tp = tp.clone();
tp.bounds.clear();
GenericParam::Type(tp)
}
_ => it.clone(),
})
.collect();
generics
};
let props_docs = get_props_docs(fn_ident, inputs.iter().skip(1).collect());
parse_quote! {
#(#fn_attrs)*
#(#props_docs)*
#asyncness #vis fn #fn_ident #fn_generics (#cx_pat: Scope<#scope_lifetime #struct_ident #generics_no_bounds>) #fn_output
#where_clause
{
let #struct_ident { #(#struct_field_names),* } = &#cx_pat.props;
#fn_block
}
}
}
/// Checks if the attribute is a `#[doc]` attribute.
fn is_attr_doc(attr: &Attribute) -> bool {
attr.path() == &parse_quote!(doc)
}
fn keep_up_to_n_consecutive_chars(
input: &str,
n_of_consecutive_chars_allowed: usize,
target_char: char,
) -> String {
let mut output = String::new();
let mut prev_char: Option<char> = None;
let mut consecutive_count = 0;
for c in input.chars() {
match prev_char {
Some(prev) if c == target_char && prev == target_char => {
if consecutive_count < n_of_consecutive_chars_allowed {
output.push(c);
consecutive_count += 1;
}
}
_ => {
output.push(c);
prev_char = Some(c);
consecutive_count = 1;
}
}
}
output
}
@@ -0,0 +1,4 @@
//! This module contains all [`ComponentBody`](crate::component_body::ComponentBody) deserializers.
pub mod component;
pub mod inline_props;
-185
View File
@@ -1,185 +0,0 @@
use proc_macro2::{Span, TokenStream as TokenStream2};
use quote::{quote, ToTokens, TokenStreamExt};
use syn::{
parse::{Parse, ParseStream},
punctuated::Punctuated,
*,
};
pub struct InlinePropsBody {
pub attrs: Vec<Attribute>,
pub vis: syn::Visibility,
pub maybe_async: Option<Token![async]>,
pub fn_token: Token![fn],
pub ident: Ident,
pub cx_token: Box<Pat>,
pub generics: Generics,
pub paren_token: token::Paren,
pub inputs: Punctuated<FnArg, Token![,]>,
// pub fields: FieldsNamed,
pub output: ReturnType,
pub where_clause: Option<WhereClause>,
pub block: Box<Block>,
}
/// The custom rusty variant of parsing rsx!
impl Parse for InlinePropsBody {
fn parse(input: ParseStream) -> Result<Self> {
let attrs: Vec<Attribute> = input.call(Attribute::parse_outer)?;
let maybe_async: Option<Token![async]> = input.parse().ok();
let vis: Visibility = input.parse()?;
let fn_token = input.parse()?;
let ident = input.parse()?;
let generics: Generics = input.parse()?;
let content;
let paren_token = syn::parenthesized!(content in input);
let first_arg: FnArg = content.parse()?;
let cx_token = {
match first_arg {
FnArg::Receiver(_) => panic!("first argument must not be a receiver argument"),
FnArg::Typed(f) => f.pat,
}
};
let _: Result<Token![,]> = content.parse();
let inputs = syn::punctuated::Punctuated::parse_terminated(&content)?;
let output = input.parse()?;
let where_clause = input
.peek(syn::token::Where)
.then(|| input.parse())
.transpose()?;
let block = input.parse()?;
Ok(Self {
vis,
maybe_async,
fn_token,
ident,
generics,
paren_token,
inputs,
output,
where_clause,
block,
cx_token,
attrs,
})
}
}
/// Serialize the same way, regardless of flavor
impl ToTokens for InlinePropsBody {
fn to_tokens(&self, out_tokens: &mut TokenStream2) {
let Self {
vis,
ident,
generics,
inputs,
output,
where_clause,
block,
cx_token,
attrs,
maybe_async,
..
} = self;
let fields = inputs.iter().map(|f| {
quote! { #vis #f }
});
let struct_name = Ident::new(&format!("{ident}Props"), Span::call_site());
let field_names = inputs.iter().filter_map(|f| match f {
FnArg::Receiver(_) => todo!(),
FnArg::Typed(t) => Some(&t.pat),
});
let first_lifetime = if let Some(GenericParam::Lifetime(lt)) = generics.params.first() {
Some(lt)
} else {
None
};
let modifiers = if first_lifetime.is_some() {
quote! { #[derive(Props)] }
} else {
quote! { #[derive(Props, PartialEq)] }
};
let (scope_lifetime, fn_generics, struct_generics) = if let Some(lt) = first_lifetime {
let struct_generics: Punctuated<_, token::Comma> = generics
.params
.iter()
.map(|it| match it {
GenericParam::Type(tp) => {
let mut tp = tp.clone();
tp.bounds.push(parse_quote!( 'a ));
GenericParam::Type(tp)
}
_ => it.clone(),
})
.collect();
(
quote! { #lt, },
generics.clone(),
quote! { <#struct_generics> },
)
} else {
let lifetime: LifetimeParam = parse_quote! { 'a };
let mut fn_generics = generics.clone();
fn_generics
.params
.insert(0, GenericParam::Lifetime(lifetime.clone()));
(quote! { #lifetime, }, fn_generics, quote! { #generics })
};
let generics_no_bounds = {
let mut generics = generics.clone();
generics.params = generics
.params
.iter()
.map(|it| match it {
GenericParam::Type(tp) => {
let mut tp = tp.clone();
tp.bounds.clear();
GenericParam::Type(tp)
}
_ => it.clone(),
})
.collect();
generics
};
out_tokens.append_all(quote! {
#modifiers
#[allow(non_camel_case_types)]
#vis struct #struct_name #struct_generics
#where_clause
{
#(#fields),*
}
#(#attrs)*
#maybe_async #vis fn #ident #fn_generics (#cx_token: Scope<#scope_lifetime #struct_name #generics_no_bounds>) #output
#where_clause
{
let #struct_name { #(#field_names),* } = &#cx_token.props;
#block
}
});
}
}
+128 -8
View File
@@ -1,12 +1,23 @@
#![doc = include_str!("../README.md")]
#![doc(html_logo_url = "https://avatars.githubusercontent.com/u/79236386")]
#![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")]
use proc_macro::TokenStream;
use quote::ToTokens;
use rsx::RenderCallBody;
use syn::parse_macro_input;
use syn::parse::Parser;
use syn::punctuated::Punctuated;
use syn::{parse_macro_input, Path, Token};
mod inlineprops;
mod component_body;
mod component_body_deserializers;
mod props;
mod utils;
// mod rsx;
use crate::component_body::ComponentBody;
use crate::component_body_deserializers::component::ComponentDeserializerArgs;
use crate::component_body_deserializers::inline_props::InlinePropsDeserializerArgs;
use dioxus_rsx as rsx;
#[proc_macro]
@@ -18,7 +29,7 @@ pub fn format_args_f(input: TokenStream) -> TokenStream {
}
#[proc_macro_derive(Props, attributes(props))]
pub fn derive_typed_builder(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
pub fn derive_typed_builder(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as syn::DeriveInput);
match props::impl_my_derive(&input) {
Ok(output) => output.into(),
@@ -37,7 +48,7 @@ pub fn rsx(s: TokenStream) -> TokenStream {
/// The render! macro makes it easy for developers to write jsx-style markup in their components.
///
/// The render macro automatically renders rsx - making it unhygenic.
/// The render macro automatically renders rsx - making it unhygienic.
#[proc_macro]
pub fn render(s: TokenStream) -> TokenStream {
match syn::parse::<rsx::CallBody>(s) {
@@ -55,7 +66,7 @@ pub fn render(s: TokenStream) -> TokenStream {
/// you would be repeating a lot of the usual Rust boilerplate.
///
/// # Example
/// ```ignore
/// ```rust,ignore
/// #[inline_props]
/// fn app(cx: Scope, bob: String) -> Element {
/// cx.render(rsx!("hello, {bob}"))
@@ -73,9 +84,118 @@ pub fn render(s: TokenStream) -> TokenStream {
/// }
/// ```
#[proc_macro_attribute]
pub fn inline_props(_args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
match syn::parse::<inlineprops::InlinePropsBody>(s) {
#[deprecated(note = "Use `#[component]` instead.")]
pub fn inline_props(_args: TokenStream, s: TokenStream) -> TokenStream {
let comp_body = parse_macro_input!(s as ComponentBody);
match comp_body.deserialize(InlinePropsDeserializerArgs {}) {
Err(e) => e.to_compile_error().into(),
Ok(s) => s.to_token_stream().into(),
Ok(output) => output.to_token_stream().into(),
}
}
pub(crate) const COMPONENT_ARG_CASE_CHECK_OFF: &str = "no_case_check";
/// Streamlines component creation.
/// This is the recommended way of creating components,
/// though you might want lower-level control with more advanced uses.
///
/// # Arguments
/// * `no_case_check` - Doesn't enforce `PascalCase` on your component names.
/// **This will be removed/deprecated in a future update in favor of a more complete Clippy-backed linting system.**
/// The reasoning behind this is that Clippy allows more robust and powerful lints, whereas
/// macros are extremely limited.
///
/// # Features
/// This attribute:
/// * Enforces that your component uses `PascalCase`.
/// No warnings are generated for the `PascalCase`
/// function name, but everything else will still raise a warning if it's incorrectly `PascalCase`.
/// Does not disable warnings anywhere else, so if you, for example,
/// accidentally don't use `snake_case`
/// for a variable name in the function, the compiler will still warn you.
/// * Automatically uses `#[inline_props]` if there's more than 1 parameter in the function.
/// * Verifies the validity of your component.
/// E.g. if it has a [`Scope`](dioxus_core::Scope) argument.
/// Notes:
/// * This doesn't work 100% of the time, because of macro limitations.
/// * Provides helpful messages if your component is not correct.
/// Possible bugs (please, report these!):
/// * There might be bugs where it incorrectly *denies* validity.
/// This is bad as it means that you can't use the attribute or you have to change the component.
/// * There might be bugs where it incorrectly *confirms* validity.
/// You will still know if the component is invalid once you use it,
/// but the error might be less helpful.
///
/// # Examples
/// * Without props:
/// ```rust,ignore
/// #[component]
/// fn GreetBob(cx: Scope) -> Element {
/// render! { "hello, bob" }
/// }
///
/// // is equivalent to
///
/// #[allow(non_snake_case)]
/// fn GreetBob(cx: Scope) -> Element {
/// #[warn(non_snake_case)]
/// #[inline(always)]
/// fn __dx_inner_comp(cx: Scope) -> Element {
/// render! { "hello, bob" }
/// }
/// // There's no function call overhead since __dx_inner_comp has the #[inline(always)] attribute,
/// // so don't worry about performance.
/// __dx_inner_comp(cx)
/// }
/// ```
/// * With props:
/// ```rust,ignore
/// #[component(no_case_check)]
/// fn GreetPerson(cx: Scope, person: String) -> Element {
/// render! { "hello, {person}" }
/// }
///
/// // is equivalent to
///
/// #[derive(Props, PartialEq)]
/// #[allow(non_camel_case_types)]
/// struct GreetPersonProps {
/// person: String,
/// }
///
/// #[allow(non_snake_case)]
/// fn GreetPerson<'a>(cx: Scope<'a, GreetPersonProps>) -> Element {
/// #[warn(non_snake_case)]
/// #[inline(always)]
/// fn __dx_inner_comp<'a>(cx: Scope<'a, GreetPersonProps>e) -> Element {
/// let GreetPersonProps { person } = &cx.props;
/// {
/// render! { "hello, {person}" }
/// }
/// }
///
/// __dx_inner_comp(cx)
/// }
/// ```
// TODO: Maybe add an option to input a custom component name through the args.
// I think that's unnecessary, but there might be some scenario where it could be useful.
#[proc_macro_attribute]
pub fn component(args: TokenStream, input: TokenStream) -> TokenStream {
let component_body = parse_macro_input!(input as ComponentBody);
let case_check = match Punctuated::<Path, Token![,]>::parse_terminated.parse(args) {
Err(e) => return e.to_compile_error().into(),
Ok(args) => {
if let Some(first) = args.first() {
!first.is_ident(COMPONENT_ARG_CASE_CHECK_OFF)
} else {
true
}
}
};
match component_body.deserialize(ComponentDeserializerArgs { case_check }) {
Err(e) => e.to_compile_error().into(),
Ok(output) => output.to_token_stream().into(),
}
}
+37 -31
View File
@@ -2,9 +2,9 @@
//!
//! However, it has been adopted to fit the Dioxus Props builder pattern.
//!
//! For dioxus, we make a few changes:
//! - [ ] automatically implement Into<Option> on the setters (IE the strip setter option)
//! - [ ] automatically implement a default of none for optional fields (those explicitly wrapped with Option<T>)
//! For Dioxus, we make a few changes:
//! - [ ] Automatically implement Into<Option> on the setters (IE the strip setter option)
//! - [ ] Automatically implement a default of none for optional fields (those explicitly wrapped with Option<T>)
use proc_macro2::TokenStream;
@@ -194,8 +194,9 @@ mod field_info {
// children field is automatically defaulted to None
if name == "children" {
builder_attr.default =
Some(syn::parse(quote!(Default::default()).into()).unwrap());
builder_attr.default = Some(
syn::parse(quote!(::core::default::Default::default()).into()).unwrap(),
);
}
// auto detect optional
@@ -204,8 +205,9 @@ mod field_info {
&& type_from_inside_option(&field.ty, true).is_some();
if !builder_attr.strip_option && strip_option_auto {
builder_attr.strip_option = true;
builder_attr.default =
Some(syn::parse(quote!(Default::default()).into()).unwrap());
builder_attr.default = Some(
syn::parse(quote!(::core::default::Default::default()).into()).unwrap(),
);
}
Ok(FieldInfo {
@@ -213,7 +215,7 @@ mod field_info {
name,
generic_ident: syn::Ident::new(
&format!("__{}", strip_raw_ident_prefix(name.to_string())),
proc_macro2::Span::call_site(),
name.span(),
),
ty: &field.ty,
builder_attr,
@@ -345,14 +347,18 @@ mod field_info {
.ok_or_else(|| Error::new_spanned(&path, "Expected identifier"))?;
match name.as_str() {
"default" => {
self.default =
Some(syn::parse(quote!(Default::default()).into()).unwrap());
self.default = Some(
syn::parse(quote!(::core::default::Default::default()).into())
.unwrap(),
);
Ok(())
}
"optional" => {
self.default =
Some(syn::parse(quote!(Default::default()).into()).unwrap());
self.default = Some(
syn::parse(quote!(::core::default::Default::default()).into())
.unwrap(),
);
self.strip_option = true;
Ok(())
}
@@ -505,15 +511,12 @@ mod struct_info {
.map(|(i, f)| FieldInfo::new(i, f, builder_attr.field_defaults.clone()))
.collect::<Result<_, _>>()?,
builder_attr,
builder_name: syn::Ident::new(&builder_name, proc_macro2::Span::call_site()),
builder_name: syn::Ident::new(&builder_name, ast.ident.span()),
conversion_helper_trait_name: syn::Ident::new(
&format!("{builder_name}_Optional"),
proc_macro2::Span::call_site(),
),
core: syn::Ident::new(
&format!("{builder_name}_core"),
proc_macro2::Span::call_site(),
ast.ident.span(),
),
core: syn::Ident::new(&format!("{builder_name}_core"), ast.ident.span()),
})
}
@@ -551,11 +554,11 @@ mod struct_info {
let phantom_generics = self.generics.params.iter().map(|param| match param {
syn::GenericParam::Lifetime(lifetime) => {
let lifetime = &lifetime.lifetime;
quote!(core::marker::PhantomData<&#lifetime ()>)
quote!(::core::marker::PhantomData<&#lifetime ()>)
}
syn::GenericParam::Type(ty) => {
let ty = &ty.ident;
quote!(core::marker::PhantomData<#ty>)
quote!(::core::marker::PhantomData<#ty>)
}
syn::GenericParam::Const(_cnst) => {
quote!()
@@ -634,7 +637,7 @@ Finally, call `.build()` to create the instance of `{name}`.
#vis fn builder() -> #builder_name #generics_with_empty {
#builder_name {
fields: #empties_tuple,
_phantom: core::default::Default::default(),
_phantom: ::core::default::Default::default(),
}
}
}
@@ -651,12 +654,12 @@ Finally, call `.build()` to create the instance of `{name}`.
fn clone(&self) -> Self {
Self {
fields: self.fields.clone(),
_phantom: Default::default(),
_phantom: ::core::default::Default::default(),
}
}
}
impl #impl_generics dioxus::prelude::Properties for #name #ty_generics
impl #impl_generics ::dioxus::prelude::Properties for #name #ty_generics
#b_generics_where_extras_predicates
{
type Builder = #builder_name #generics_with_empty;
@@ -698,6 +701,14 @@ Finally, call `.build()` to create the instance of `{name}`.
}
pub fn field_impl(&self, field: &FieldInfo) -> Result<TokenStream, Error> {
let FieldInfo {
name: field_name,
ty: field_type,
..
} = field;
if *field_name == "key" {
return Err(Error::new_spanned(field_name, "Naming a prop `key` is not allowed because the name can conflict with the built in key attribute. See https://dioxuslabs.com/learn/0.4/reference/dynamic_rendering#rendering-lists for more information about keys"));
}
let StructInfo {
ref builder_name, ..
} = *self;
@@ -712,11 +723,6 @@ Finally, call `.build()` to create the instance of `{name}`.
});
let reconstructing = self.included_fields().map(|f| f.name);
let FieldInfo {
name: field_name,
ty: field_type,
..
} = field;
let mut ty_generics: Vec<syn::GenericArgument> = self
.generics
.params
@@ -793,7 +799,7 @@ Finally, call `.build()` to create the instance of `{name}`.
};
let (arg_type, arg_expr) = if field.builder_attr.auto_into {
(
quote!(impl core::convert::Into<#arg_type>),
quote!(impl ::core::convert::Into<#arg_type>),
quote!(#field_name.into()),
)
} else {
@@ -811,7 +817,7 @@ Finally, call `.build()` to create the instance of `{name}`.
builder_name,
strip_raw_ident_prefix(field_name.to_string())
),
proc_macro2::Span::call_site(),
builder_name.span(),
);
let repeated_fields_error_message = format!("Repeated field {field_name}");
@@ -930,7 +936,7 @@ Finally, call `.build()` to create the instance of `{name}`.
builder_name,
strip_raw_ident_prefix(field_name.to_string())
),
proc_macro2::Span::call_site(),
builder_name.span(),
);
let early_build_error_message = format!("Missing required field {field_name}");
+129
View File
@@ -0,0 +1,129 @@
use quote::ToTokens;
use syn::parse::{Parse, ParseStream};
use syn::spanned::Spanned;
use syn::{parse_quote, Expr, Lit, Meta, Token, Type};
const FORMATTED_TYPE_START: &str = "static TY_AFTER_HERE:";
const FORMATTED_TYPE_END: &str = "= todo!();";
/// Attempts to convert the given literal to a string.
/// Converts ints and floats to their base 10 counterparts.
///
/// Returns `None` if the literal is [`Lit::Verbatim`] or if the literal is [`Lit::ByteStr`]
/// and the byte string could not be converted to UTF-8.
pub fn lit_to_string(lit: Lit) -> Option<String> {
match lit {
Lit::Str(l) => Some(l.value()),
Lit::ByteStr(l) => String::from_utf8(l.value()).ok(),
Lit::Byte(l) => Some(String::from(l.value() as char)),
Lit::Char(l) => Some(l.value().to_string()),
Lit::Int(l) => Some(l.base10_digits().to_string()),
Lit::Float(l) => Some(l.base10_digits().to_string()),
Lit::Bool(l) => Some(l.value().to_string()),
Lit::Verbatim(_) => None,
_ => None,
}
}
pub fn format_type_string(ty: &Type) -> String {
let ty_unformatted = ty.into_token_stream().to_string();
let ty_unformatted = ty_unformatted.trim();
// This should always be valid syntax.
// Not Rust code, but syntax, which is the only thing that `syn` cares about.
let Ok(file_unformatted) = syn::parse_file(&format!(
"{FORMATTED_TYPE_START}{ty_unformatted}{FORMATTED_TYPE_END}"
)) else {
return ty_unformatted.to_string();
};
let file_formatted = prettyplease::unparse(&file_unformatted);
let file_trimmed = file_formatted.trim();
let start_removed = file_trimmed.trim_start_matches(FORMATTED_TYPE_START);
let end_removed = start_removed.trim_end_matches(FORMATTED_TYPE_END);
let ty_formatted = end_removed.trim();
ty_formatted.to_string()
}
/// Represents the `#[deprecated]` attribute.
///
/// You can use the [`DeprecatedAttribute::from_meta`] function to try to parse an attribute to this struct.
#[derive(Default)]
pub struct DeprecatedAttribute {
pub since: Option<String>,
pub note: Option<String>,
}
impl DeprecatedAttribute {
/// Returns `None` if the given attribute was not a valid form of the `#[deprecated]` attribute.
pub fn from_meta(meta: &Meta) -> syn::Result<Self> {
if meta.path() != &parse_quote!(deprecated) {
return Err(syn::Error::new(
meta.span(),
"attribute path is not `deprecated`",
));
}
match &meta {
Meta::Path(_) => Ok(Self::default()),
Meta::NameValue(name_value) => {
let Expr::Lit(expr_lit) = &name_value.value else {
return Err(syn::Error::new(
name_value.span(),
"literal in `deprecated` value must be a string",
));
};
Ok(Self {
since: None,
note: lit_to_string(expr_lit.lit.clone()).map(|s| s.trim().to_string()),
})
}
Meta::List(list) => {
let parsed = list.parse_args::<DeprecatedAttributeArgsParser>()?;
Ok(Self {
since: parsed.since.map(|s| s.trim().to_string()),
note: parsed.note.map(|s| s.trim().to_string()),
})
}
}
}
}
mod kw {
use syn::custom_keyword;
custom_keyword!(since);
custom_keyword!(note);
}
struct DeprecatedAttributeArgsParser {
since: Option<String>,
note: Option<String>,
}
impl Parse for DeprecatedAttributeArgsParser {
fn parse(input: ParseStream) -> syn::Result<Self> {
let mut since: Option<String> = None;
let mut note: Option<String> = None;
if input.peek(kw::since) {
input.parse::<kw::since>()?;
input.parse::<Token![=]>()?;
since = lit_to_string(input.parse()?);
}
if input.peek(Token![,]) && input.peek2(kw::note) {
input.parse::<Token![,]>()?;
input.parse::<kw::note>()?;
input.parse::<Token![=]>()?;
note = lit_to_string(input.parse()?);
}
Ok(Self { since, note })
}
}
+12 -15
View File
@@ -49,17 +49,16 @@ impl ErrorBoundary {
/// A trait to allow results to be thrown upwards to the nearest Error Boundary
///
/// The canonical way of using this trait is to throw results from hooks, aborting rendering
/// through question mark synax. The throw method returns an option that evalutes to None
/// through question mark syntax. The throw method returns an option that evaluates to None
/// if there is an error, injecting the error to the nearest error boundary.
///
/// If the value is `Ok`, then throw returns the value, not aborting the rendering preocess.
/// If the value is `Ok`, then throw returns the value, not aborting the rendering process.
///
/// The call stack is saved for this component and provided to the error boundary
///
/// ```rust, ignore
///
/// #[inline_props]
/// fn app(cx: Scope, count: String) -> Element {
/// #[component]
/// fn App(cx: Scope, count: String) -> Element {
/// let id: i32 = count.parse().throw(cx)?;
///
/// cx.render(rsx! {
@@ -71,9 +70,9 @@ pub trait Throw<S = ()>: Sized {
/// The value that will be returned in if the given value is `Ok`.
type Out;
/// Returns an option that evalutes to None if there is an error, injecting the error to the nearest error boundary.
/// Returns an option that evaluates to None if there is an error, injecting the error to the nearest error boundary.
///
/// If the value is `Ok`, then throw returns the value, not aborting the rendering preocess.
/// If the value is `Ok`, then throw returns the value, not aborting the rendering process.
///
/// The call stack is saved for this component and provided to the error boundary
///
@@ -83,9 +82,8 @@ pub trait Throw<S = ()>: Sized {
///
///
/// ```rust, ignore
///
/// #[inline_props]
/// fn app(cx: Scope, count: String) -> Element {
/// #[component]
/// fn App(cx: Scope, count: String) -> Element {
/// let id: i32 = count.parse().throw(cx)?;
///
/// cx.render(rsx! {
@@ -95,9 +93,9 @@ pub trait Throw<S = ()>: Sized {
/// ```
fn throw(self, cx: &ScopeState) -> Option<Self::Out>;
/// Returns an option that evalutes to None if there is an error, injecting the error to the nearest error boundary.
/// Returns an option that evaluates to None if there is an error, injecting the error to the nearest error boundary.
///
/// If the value is `Ok`, then throw returns the value, not aborting the rendering preocess.
/// If the value is `Ok`, then throw returns the value, not aborting the rendering process.
///
/// The call stack is saved for this component and provided to the error boundary
///
@@ -107,9 +105,8 @@ pub trait Throw<S = ()>: Sized {
///
///
/// ```rust, ignore
///
/// #[inline_props]
/// fn app(cx: Scope, count: String) -> Element {
/// #[component]
/// fn App(cx: Scope, count: String) -> Element {
/// let id: i32 = count.parse().throw(cx)?;
///
/// cx.render(rsx! {
+6 -3
View File
@@ -1,4 +1,6 @@
#![doc = include_str!("../README.md")]
#![doc(html_logo_url = "https://avatars.githubusercontent.com/u/79236386")]
#![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")]
#![warn(missing_docs)]
mod any_props;
@@ -32,6 +34,7 @@ pub(crate) mod innerlude {
pub use crate::nodes::RenderReturn;
pub use crate::nodes::*;
pub use crate::properties::*;
pub use crate::runtime::{Runtime, RuntimeGuard};
pub use crate::scheduler::*;
pub use crate::scope_context::*;
pub use crate::scopes::*;
@@ -88,9 +91,9 @@ pub mod prelude {
consume_context, consume_context_from_scope, current_scope_id, fc_to_builder, has_context,
provide_context, provide_context_to_scope, provide_root_context, push_future,
remove_future, schedule_update_any, spawn, spawn_forever, suspend, throw, AnyValue,
Component, Element, Event, EventHandler, Fragment, IntoAttributeValue, LazyNodes,
Properties, Scope, ScopeId, ScopeState, Scoped, TaskId, Template, TemplateAttribute,
TemplateNode, Throw, VNode, VirtualDom,
Component, Element, Event, EventHandler, Fragment, IntoAttributeValue, IntoDynNode,
LazyNodes, Properties, Runtime, RuntimeGuard, Scope, ScopeId, ScopeState, Scoped, TaskId,
Template, TemplateAttribute, TemplateNode, Throw, VNode, VirtualDom,
};
}
+43 -5
View File
@@ -41,7 +41,8 @@ where
.flatten()
}
pub(crate) struct Runtime {
/// A global runtime that is shared across all scopes that provides the async runtime and context API
pub struct Runtime {
pub(crate) scope_contexts: RefCell<Vec<Option<ScopeContext>>>,
pub(crate) scheduler: Rc<Scheduler>,
@@ -63,6 +64,11 @@ impl Runtime {
})
}
/// Get the current runtime
pub fn current() -> Option<Rc<Self>> {
RUNTIMES.with(|stack| stack.borrow().last().cloned())
}
/// Create a scope context. This slab is synchronized with the scope slab.
pub(crate) fn create_context_at(&self, id: ScopeId, context: ScopeContext) {
let mut contexts = self.scope_contexts.borrow_mut();
@@ -77,14 +83,14 @@ impl Runtime {
}
/// Get the current scope id
pub fn current_scope_id(&self) -> Option<ScopeId> {
pub(crate) fn current_scope_id(&self) -> Option<ScopeId> {
self.scope_stack.borrow().last().copied()
}
/// Get the context for any scope given its ID
///
/// This is useful for inserting or removing contexts from a scope, or rendering out its root node
pub fn get_context(&self, id: ScopeId) -> Option<Ref<'_, ScopeContext>> {
pub(crate) fn get_context(&self, id: ScopeId) -> Option<Ref<'_, ScopeContext>> {
Ref::filter_map(self.scope_contexts.borrow(), |contexts| {
contexts.get(id.0).and_then(|f| f.as_ref())
})
@@ -92,10 +98,42 @@ impl Runtime {
}
}
pub(crate) struct RuntimeGuard(Rc<Runtime>);
/// A gaurd for a new runtime. This must be used to override the current runtime when importing components from a dynamic library that has it's own runtime.
///
/// ```rust
/// use dioxus::prelude::*;
///
/// fn main() {
/// let virtual_dom = VirtualDom::new(app);
/// }
///
/// fn app(cx: Scope) -> Element {
/// render!{ Component { runtime: Runtime::current().unwrap() } }
/// }
///
/// // In a dynamic library
/// #[derive(Props)]
/// struct ComponentProps {
/// runtime: std::rc::Rc<Runtime>,
/// }
///
/// impl PartialEq for ComponentProps {
/// fn eq(&self, _other: &Self) -> bool {
/// true
/// }
/// }
///
/// fn Component(cx: Scope<ComponentProps>) -> Element {
/// cx.use_hook(|| RuntimeGuard::new(cx.props.runtime.clone()));
///
/// render! { div {} }
/// }
/// ```
pub struct RuntimeGuard(Rc<Runtime>);
impl RuntimeGuard {
pub(crate) fn new(runtime: Rc<Runtime>) -> Self {
/// Create a new runtime guard that sets the current Dioxus runtime. The runtime will be reset when the guard is dropped
pub fn new(runtime: Rc<Runtime>) -> Self {
push_runtime(runtime.clone());
Self(runtime)
}
+1 -1
View File
@@ -128,7 +128,7 @@ impl ScopeContext {
parent.name
);
if let Some(shared) = parent.shared_contexts.borrow().iter().find_map(|any| {
tracing::trace!("found context {:?}", any.type_id());
tracing::trace!("found context {:?}", (**any).type_id());
any.downcast_ref::<T>()
}) {
return Some(shared.clone());
+12 -4
View File
@@ -52,6 +52,7 @@ use std::{any::Any, cell::Cell, collections::BTreeSet, future::Future, rc::Rc};
///
/// static ROUTES: &str = "";
///
/// #[component]
/// fn App(cx: Scope<AppProps>) -> Element {
/// cx.render(rsx!(
/// NavBar { routes: ROUTES }
@@ -60,18 +61,19 @@ use std::{any::Any, cell::Cell, collections::BTreeSet, future::Future, rc::Rc};
/// ))
/// }
///
/// #[inline_props]
/// #[component]
/// fn NavBar(cx: Scope, routes: &'static str) -> Element {
/// cx.render(rsx! {
/// div { "Routes: {routes}" }
/// })
/// }
///
/// #[component]
/// fn Footer(cx: Scope) -> Element {
/// cx.render(rsx! { div { "Footer" } })
/// }
///
/// #[inline_props]
/// #[component]
/// fn Title<'a>(cx: Scope<'a>, children: Element<'a>) -> Element {
/// cx.render(rsx! {
/// div { id: "title", children }
@@ -122,13 +124,14 @@ use std::{any::Any, cell::Cell, collections::BTreeSet, future::Future, rc::Rc};
///
/// Putting everything together, you can build an event loop around Dioxus by using the methods outlined above.
/// ```rust, ignore
/// fn app(cx: Scope) -> Element {
/// #[component]
/// fn App(cx: Scope) -> Element {
/// cx.render(rsx! {
/// div { "Hello World" }
/// })
/// }
///
/// let dom = VirtualDom::new(app);
/// let dom = VirtualDom::new(App);
///
/// real_dom.apply(dom.rebuild());
///
@@ -659,6 +662,11 @@ impl VirtualDom {
fn finalize(&mut self) -> Mutations {
std::mem::take(&mut self.mutations)
}
/// Get the current runtime
pub fn runtime(&self) -> Rc<Runtime> {
self.runtime.clone()
}
}
impl Drop for VirtualDom {
+19 -16
View File
@@ -5,11 +5,12 @@ use dioxus_core::ElementId;
/// Should push the text node onto the stack and modify it
#[test]
fn nested_passthru_creates() {
fn app(cx: Scope) -> Element {
#[component]
fn App(cx: Scope) -> Element {
cx.render(rsx! {
pass_thru {
pass_thru {
pass_thru {
PassThru {
PassThru {
PassThru {
div { "hi" }
}
}
@@ -17,12 +18,12 @@ fn nested_passthru_creates() {
})
}
#[inline_props]
fn pass_thru<'a>(cx: Scope<'a>, children: Element<'a>) -> Element {
#[component]
fn PassThru<'a>(cx: Scope<'a>, children: Element<'a>) -> Element {
cx.render(rsx!(children))
}
let mut dom = VirtualDom::new(app);
let mut dom = VirtualDom::new(App);
let edits = dom.rebuild().santize();
assert_eq!(
@@ -39,13 +40,14 @@ fn nested_passthru_creates() {
/// Take note on how we don't spit out the template for child_comp since it's entirely dynamic
#[test]
fn nested_passthru_creates_add() {
fn app(cx: Scope) -> Element {
#[component]
fn App(cx: Scope) -> Element {
cx.render(rsx! {
child_comp {
ChildComp {
"1"
child_comp {
ChildComp {
"2"
child_comp {
ChildComp {
"3"
div {
"hi"
@@ -56,12 +58,12 @@ fn nested_passthru_creates_add() {
})
}
#[inline_props]
fn child_comp<'a>(cx: Scope, children: Element<'a>) -> Element {
#[component]
fn ChildComp<'a>(cx: Scope, children: Element<'a>) -> Element {
cx.render(rsx! { children })
}
let mut dom = VirtualDom::new(app);
let mut dom = VirtualDom::new(App);
assert_eq!(
dom.rebuild().santize().edits,
@@ -82,13 +84,14 @@ fn nested_passthru_creates_add() {
/// note that the template is all dynamic roots - so it doesn't actually get cached as a template
#[test]
fn dynamic_node_as_root() {
fn app(cx: Scope) -> Element {
#[component]
fn App(cx: Scope) -> Element {
let a = 123;
let b = 456;
cx.render(rsx! { "{a}" "{b}" })
}
let mut dom = VirtualDom::new(app);
let mut dom = VirtualDom::new(App);
let edits = dom.rebuild().santize();
// Since the roots were all dynamic, they should not cause any template muations
+6 -5
View File
@@ -4,7 +4,7 @@ use std::rc::Rc;
#[test]
fn miri_rollover() {
let mut dom = VirtualDom::new(app);
let mut dom = VirtualDom::new(App);
_ = dom.rebuild();
@@ -15,7 +15,8 @@ fn miri_rollover() {
}
}
fn app(cx: Scope) -> Element {
#[component]
fn App(cx: Scope) -> Element {
let mut idx = use_state(cx, || 0);
let onhover = |_| println!("go!");
@@ -31,15 +32,15 @@ fn app(cx: Scope) -> Element {
button { onclick: move |_| idx -= 1, "-" }
ul {
(0..**idx).map(|i| rsx! {
child_example { i: i, onhover: onhover }
ChildExample { i: i, onhover: onhover }
})
}
}
})
}
#[inline_props]
fn child_example<'a>(cx: Scope<'a>, i: i32, onhover: EventHandler<'a, MouseEvent>) -> Element {
#[component]
fn ChildExample<'a>(cx: Scope<'a>, i: i32, onhover: EventHandler<'a, MouseEvent>) -> Element {
cx.render(rsx! {
li {
onmouseover: move |e| onhover.call(e),
+24 -18
View File
@@ -2,13 +2,14 @@ use dioxus::prelude::*;
#[test]
fn app_drops() {
fn app(cx: Scope) -> Element {
#[component]
fn App(cx: Scope) -> Element {
cx.render(rsx! {
div {}
})
}
let mut dom = VirtualDom::new(app);
let mut dom = VirtualDom::new(App);
_ = dom.rebuild();
dom.mark_dirty(ScopeId::ROOT);
@@ -17,7 +18,8 @@ fn app_drops() {
#[test]
fn hooks_drop() {
fn app(cx: Scope) -> Element {
#[component]
fn App(cx: Scope) -> Element {
cx.use_hook(|| String::from("asd"));
cx.use_hook(|| String::from("asd"));
cx.use_hook(|| String::from("asd"));
@@ -28,7 +30,7 @@ fn hooks_drop() {
})
}
let mut dom = VirtualDom::new(app);
let mut dom = VirtualDom::new(App);
_ = dom.rebuild();
dom.mark_dirty(ScopeId::ROOT);
@@ -37,17 +39,19 @@ fn hooks_drop() {
#[test]
fn contexts_drop() {
fn app(cx: Scope) -> Element {
#[component]
fn App(cx: Scope) -> Element {
cx.provide_context(String::from("asd"));
cx.render(rsx! {
div {
child_comp {}
ChildComp {}
}
})
}
fn child_comp(cx: Scope) -> Element {
#[component]
fn ChildComp(cx: Scope) -> Element {
let el = cx.consume_context::<String>().unwrap();
cx.render(rsx! {
@@ -55,7 +59,7 @@ fn contexts_drop() {
})
}
let mut dom = VirtualDom::new(app);
let mut dom = VirtualDom::new(App);
_ = dom.rebuild();
dom.mark_dirty(ScopeId::ROOT);
@@ -64,7 +68,8 @@ fn contexts_drop() {
#[test]
fn tasks_drop() {
fn app(cx: Scope) -> Element {
#[component]
fn App(cx: Scope) -> Element {
cx.spawn(async {
// tokio::time::sleep(std::time::Duration::from_millis(100000)).await;
});
@@ -74,7 +79,7 @@ fn tasks_drop() {
})
}
let mut dom = VirtualDom::new(app);
let mut dom = VirtualDom::new(App);
_ = dom.rebuild();
dom.mark_dirty(ScopeId::ROOT);
@@ -97,29 +102,30 @@ fn root_props_drop() {
#[test]
fn diffing_drops_old() {
fn app(cx: Scope) -> Element {
#[component]
fn App(cx: Scope) -> Element {
cx.render(rsx! {
div {
match cx.generation() % 2 {
0 => rsx!( child_comp1 { name: "asdasd".to_string() }),
1 => rsx!( child_comp2 { name: "asdasd".to_string() }),
0 => rsx!( ChildComp1 { name: "asdasd".to_string() }),
1 => rsx!( ChildComp2 { name: "asdasd".to_string() }),
_ => todo!()
}
}
})
}
#[inline_props]
fn child_comp1(cx: Scope, name: String) -> Element {
#[component]
fn ChildComp1(cx: Scope, name: String) -> Element {
cx.render(rsx! { "Hello {name}" })
}
#[inline_props]
fn child_comp2(cx: Scope, name: String) -> Element {
#[component]
fn ChildComp2(cx: Scope, name: String) -> Element {
cx.render(rsx! { "Goodbye {name}" })
}
let mut dom = VirtualDom::new(app);
let mut dom = VirtualDom::new(App);
_ = dom.rebuild();
dom.mark_dirty(ScopeId::ROOT);
+1 -1
View File
@@ -36,7 +36,7 @@ fn suspended_child(cx: Scope) -> Element {
cx.spawn(async move {
val += 1;
});
return cx.suspend()?;
cx.suspend()?;
}
render!("child")
+8 -3
View File
@@ -6,7 +6,7 @@ edition = "2018"
description = "WebView renderer for Dioxus"
license = "MIT OR Apache-2.0"
repository = "https://github.com/DioxusLabs/dioxus/"
homepage = "https://dioxuslabs.com/docs/0.3/guide/en/getting_started/desktop.html"
homepage = "https://dioxuslabs.com/learn/0.4/getting_started/desktop"
keywords = ["dom", "ui", "gui", "react"]
[dependencies]
@@ -19,7 +19,7 @@ serde = "1.0.136"
serde_json = "1.0.79"
thiserror = { workspace = true }
tracing = { workspace = true }
wry = { version = "0.31.0" }
wry = { version = "0.31.0", default-features = false, features = ["protocol", "file-drop"] }
futures-channel = { workspace = true }
tokio = { workspace = true, features = [
"sync",
@@ -53,14 +53,19 @@ core-foundation = "0.9.3"
objc = "0.2.7"
[features]
default = ["tokio_runtime", "hot-reload"]
default = ["tokio_runtime", "hot-reload", "wry/objc-exception"]
tokio_runtime = ["tokio"]
fullscreen = ["wry/fullscreen"]
transparent = ["wry/transparent"]
devtools = ["wry/devtools"]
tray = ["wry/tray"]
dox = ["wry/dox"]
hot-reload = ["dioxus-hot-reload"]
[package.metadata.docs.rs]
default-features = false
features = [ "dox", "tokio_runtime", "hot-reload" ]
[dev-dependencies]
dioxus-core-macro = { workspace = true }
dioxus-hooks = { workspace = true }
+3 -9
View File
@@ -1,6 +1,5 @@
# Dioxus Desktop (webview)
[![Crates.io][crates-badge]][crates-url]
[![MIT licensed][mit-badge]][mit-url]
[![Build Status][actions-badge]][actions-url]
@@ -8,31 +7,25 @@
[crates-badge]: https://img.shields.io/crates/v/dioxus-desktop.svg
[crates-url]: https://crates.io/crates/dioxus-desktop
[mit-badge]: https://img.shields.io/badge/license-MIT-blue.svg
[mit-url]: https://github.com/dioxuslabs/dioxus/blob/master/LICENSE
[actions-badge]: https://github.com/dioxuslabs/dioxus/actions/workflows/main.yml/badge.svg
[actions-url]: https://github.com/dioxuslabs/dioxus/actions?query=workflow%3ACI+branch%3Amaster
[discord-badge]: https://img.shields.io/discord/899851952891002890.svg?logo=discord&style=flat-square
[discord-url]: https://discord.gg/XgGxMSkvUM
[Website](https://dioxuslabs.com) |
[Guides](https://dioxuslabs.com/docs/0.3/guide/en/) |
[Guides](https://dioxuslabs.com/learn/0.4/) |
[API Docs](https://docs.rs/dioxus-desktop/latest/dioxus_desktop) |
[Chat](https://discord.gg/XgGxMSkvUM)
## Overview
`dioxus-desktop` provides a webview-based desktop renderer for the Dioxus VirtualDom.
This requires that webview is installed on the target system. WebView is installed by default on macOS and iOS devices, but might not come preinstalled on Windows or Linux devices. To fix these issues, follow the [instructions in the guide](guide-url).
[guide-url]: https://dioxuslabs.com/docs/0.3/guide/en/getting_started/desktop.html
[guide-url]: https://dioxuslabs.com/learn/0.4/getting_started/desktop
## Features
@@ -48,6 +41,7 @@ This requires that webview is installed on the target system. WebView is install
- Join the discord and ask questions!
## License
This project is licensed under the [MIT license].
[mit license]: https://github.com/DioxusLabs/dioxus/blob/master/LICENSE-MIT
+7
View File
@@ -163,6 +163,7 @@ pub fn launch_with_props<P: 'static>(root: Component<P>, props: P, cfg: Config)
// iOS panics if we create a window before the event loop is started
let props = Rc::new(Cell::new(Some(props)));
let cfg = Rc::new(Cell::new(Some(cfg)));
let mut is_visible_before_start = true;
event_loop.run(move |window_event, event_loop, control_flow| {
*control_flow = ControlFlow::Poll;
@@ -216,6 +217,8 @@ pub fn launch_with_props<P: 'static>(root: Component<P>, props: P, cfg: Config)
// Create a dom
let dom = VirtualDom::new_with_props(root, props);
is_visible_before_start = cfg.window.window.visible;
let handler = create_new_window(
cfg,
event_loop,
@@ -329,6 +332,10 @@ pub fn launch_with_props<P: 'static>(root: Component<P>, props: P, cfg: Config)
EventData::Ipc(msg) if msg.method() == "initialize" => {
let view = webviews.get_mut(&event.1).unwrap();
send_edits(view.dom.rebuild(), &view.desktop_context.webview);
view.desktop_context
.webview
.window()
.set_visible(is_visible_before_start);
}
EventData::Ipc(msg) if msg.method() == "browser_open" => {
+1 -1
View File
@@ -153,7 +153,7 @@ fn get_asset_root() -> Option<PathBuf> {
/// Get the mime type from a path-like string
fn get_mime_from_path(trimmed: &Path) -> Result<&'static str> {
if trimmed.ends_with(".svg") {
if trimmed.extension().is_some_and(|ext| ext == "svg") {
return Ok("image/svg+xml");
}
+1 -3
View File
@@ -8,7 +8,6 @@ One of Dioxus' flagship features is the ability to quickly build a native deskto
Dioxus Desktop is built off Tauri. Right now there aren't any Dioxus abstractions over the menubar, handling, etc, so you'll want to leverage Tauri - mostly [Wry](http://github.com/tauri-apps/wry/) and [Tao](http://github.com/tauri-apps/tao) directly. An upcoming release of Dioxus-Desktop will include components and hooks for notifications, global shortcuts, menubar, etc.
## Getting Set up
Getting Set up with Dioxus-Desktop is quite easy. Make sure you have Rust and Cargo installed, and then create a new project:
@@ -44,9 +43,8 @@ fn app(cx: Scope) -> Element {
}
```
To configure the webview, menubar, and other important desktop-specific features, checkout out some of the launch configuration in the [API reference](https://docs.rs/dioxus-desktop/).
## Future Steps
Make sure to read the [Dioxus Guide](https://dioxuslabs.com/docs/0.3/guide/en) if you already haven't!
Make sure to read the [Dioxus Guide](https://dioxuslabs.com/learn/0.4/) if you already haven't!
+1 -1
View File
@@ -13,7 +13,7 @@ pub fn build(
proxy: EventLoopProxy<UserWindowEvent>,
) -> (WebView, WebContext) {
let builder = cfg.window.clone();
let window = builder.build(event_loop).unwrap();
let window = builder.with_visible(false).build(event_loop).unwrap();
let file_handler = cfg.file_drop_handler.take();
let custom_head = cfg.custom_head.clone();
let index_file = cfg.custom_index.clone();
+1 -2
View File
@@ -5,12 +5,11 @@ authors = ["Jonathan Kelley, Evan Almloff"]
edition = "2021"
description = "TUI-based renderer for Dioxus"
repository = "https://github.com/DioxusLabs/dioxus/"
homepage = "https://dioxuslabs.com/docs/0.3/guide/en/getting_started/tui.html"
homepage = "https://dioxuslabs.com/learn/0.4/getting_started/tui"
keywords = ["dom", "ui", "gui", "react", "terminal"]
license = "MIT OR Apache-2.0"
[dependencies]
dioxus = { workspace = true }
dioxus-core = { workspace = true, features = ["serialize"] }
dioxus-html = { workspace = true }
dioxus-native-core = { workspace = true, features = ["dioxus"] }
+4
View File
@@ -1,3 +1,7 @@
#![doc = include_str!("../README.md")]
#![doc(html_logo_url = "https://avatars.githubusercontent.com/u/79236386")]
#![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")]
mod element;
use std::{
+1 -1
View File
@@ -6,7 +6,7 @@ edition = "2021"
description = "Portable, performant, and ergonomic framework for building cross-platform user interfaces in Rust"
license = "MIT OR Apache-2.0"
repository = "https://github.com/DioxusLabs/dioxus/"
homepage = "https://dioxuslabs.com/docs/0.3/guide/en/index.html"
homepage = "https://dioxuslabs.com/learn/0.4/"
keywords = ["dom", "ui", "gui", "react", "wasm"]
rust-version = "1.65.0"
+41 -24
View File
@@ -1,4 +1,4 @@
<div align="center">
<div style="text-align: center">
<h1>🌗🚀 Dioxus</h1>
<p>
<strong>A concurrent, functional, virtual DOM for Rust</strong>
@@ -9,8 +9,8 @@
This overview provides a brief introduction to Dioxus. For a more in-depth guide, make sure to check out:
- [Getting Started](https://dioxuslabs.com/docs/0.3/guide/en/getting_started/index.html)
- [Book](https://dioxuslabs.com/docs/0.3/guide/en/)
- [Getting Started](https://dioxuslabs.com/learn/0.4/getting_started)
- [Book (0.4)](https://dioxuslabs.com/learn/0.4/)
- [Examples](https://github.com/DioxusLabs/example-projects)
# Overview and Goals
@@ -27,15 +27,15 @@ Dioxus is heavily inspired by React, supporting many of the same concepts:
If you know React, then you know Dioxus.
Dioxus is *substantially* more performant than many of the other Rust UI libraries (Yew/Percy) and is *significantly* more performant
than React - roughly competitive with InfernoJS.
Dioxus is _substantially_ more performant than many of the other Rust UI libraries (Yew/Percy) and is _significantly_ more performant
than React—roughly competitive with InfernoJS.
Remember: Dioxus is a library for declaring interactive user interfaces - it is not a dedicated renderer. Most 1st party renderers for Dioxus currently only support web technologies.
Remember: Dioxus is a library for declaring interactive user interfaces—it is not a dedicated renderer. Most 1st party renderers for Dioxus currently only support web technologies.
## Brief Overview
All Dioxus apps are built by composing functions that take in a `Scope` which is generic over some `Properties` and return an `Element`.
A `Scope` holds relevant state data for the currently-rendered component.
A `Scope` holds relevant state data for the currently rendered component.
To launch an app, we use the `launch` method for the specific renderer we want to use. In the launch function, we pass the app's `Component`.
@@ -43,10 +43,13 @@ To launch an app, we use the `launch` method for the specific renderer we want t
use dioxus::prelude::*;
fn main() {
dioxus_desktop::launch(app);
dioxus_desktop::launch(App);
}
fn app(cx: Scope) -> Element {
// The #[component] attribute streamlines component creation.
// It's not required, but highly recommended. For example, UpperCamelCase components will not generate a warning.
#[component]
fn App(cx: Scope) -> Element {
cx.render(rsx!("hello world!"))
}
```
@@ -59,7 +62,7 @@ macro or the NodeFactory API. For the most part, you want to use the `rsx!`
macro.
Any element in `rsx!` can have attributes, listeners, and children. For
consistency, we force all attributes and listeners to be listed *before*
consistency, we force all attributes and listeners to be listed _before_
children.
```rust, ignore
@@ -84,7 +87,6 @@ rsx!(
(0..10).map(|_| rsx!(span { "hello world" }))
}
)
```
Used within components, the `rsx!` macro must be rendered into an `Element` with
@@ -95,7 +97,8 @@ If we want to omit the boilerplate of `cx.render`, we can simply pass in
render nodes in match statements.
```rust, ignore
fn example(cx: Scope) -> Element {
#[component[
fn Example(cx: Scope) -> Element {
// both of these are equivalent
cx.render(rsx!("hello world"))
@@ -108,7 +111,8 @@ Putting everything together, we can write a simple component that renders a list
elements:
```rust, ignore
fn app(cx: Scope) -> Element {
#[component]
fn App(cx: Scope) -> Element {
let name = "dave";
cx.render(rsx!(
h1 { "Hello, {name}!" }
@@ -136,6 +140,7 @@ properties, we can use the `()` type or simply omit the type altogether.
In Dioxus, all properties are memoized by default!
```rust, ignore
#[component]
fn App(cx: Scope) -> Element {
cx.render(rsx!(
Header {
@@ -157,6 +162,7 @@ struct HeaderProps {
color: String,
}
#[component]
fn Header(cx: Scope<HeaderProps>) -> Element {
cx.render(rsx!(
div {
@@ -167,11 +173,11 @@ fn Header(cx: Scope<HeaderProps>) -> Element {
}
```
Components may use the `inline_props` macro to completely inline the props
definition into the function arguments.
The `#[component]` macro also allows you to derive the props
struct from function arguments:
```rust, ignore
#[inline_props]
#[component]
fn Header(cx: Scope, title: String, color: String) -> Element {
cx.render(rsx!(
div {
@@ -184,6 +190,7 @@ fn Header(cx: Scope, title: String, color: String) -> Element {
Components may also borrow data from their parent component. We just need to
attach some lifetimes to the props struct.
> Note: we don't need to derive `PartialEq` for borrowed props since they cannot be memoized.
```rust, ignore
@@ -193,6 +200,7 @@ struct HeaderProps<'a> {
color: &'a str,
}
#[component]
fn Header<'a>(cx: Scope<'a, HeaderProps<'a>>) -> Element {
cx.render(rsx!(
div {
@@ -221,6 +229,9 @@ rsx!(
)
```
However, the convention is to use UpperCamelCase. The `#[component]` attribute will enforce this,
but you can turn it off if you wish.
## Hooks
While components are reusable forms of UI elements, hooks are reusable forms
@@ -231,7 +242,8 @@ By convention, all hooks are functions that should start with `use_`. We can
use hooks to define the state and modify it from within listeners.
```rust, ignore
fn app(cx: Scope) -> Element {
#[component]
fn App(cx: Scope) -> Element {
let name = use_state(cx, || "world");
render!("hello {name}!")
@@ -239,10 +251,11 @@ fn app(cx: Scope) -> Element {
```
Hooks are sensitive to how they are used. To use hooks, you must abide by the
["rules of hooks" (borrowed from react)](https://reactjs.org/docs/hooks-rules.html):
- Functions with "use_" should not be called in callbacks
- Functions with "use_" should not be called out of order
- Functions with "use_" should not be called in loops or conditionals
["rules of hooks" (borrowed from React)](https://reactjs.org/docs/hooks-rules.html):
- Functions with "use\_" should not be called in callbacks
- Functions with "use\_" should not be called out of order
- Functions with "use\_" should not be called in loops or conditionals
In a sense, hooks let us add a field of state to our component without declaring
an explicit state struct. However, this means we need to "load" the struct in the right
@@ -278,6 +291,7 @@ fn main() {
dioxus_desktop::launch(App);
}
#[component]
fn App(cx: Scope) -> Element {
let count = use_state(cx, || 0);
@@ -295,6 +309,7 @@ This overview doesn't cover everything. Make sure to check out the tutorial and
website for more details.
Beyond this overview, Dioxus supports:
- Server-side rendering
- Concurrent rendering (with async support)
- Web/Desktop/Mobile support
@@ -311,14 +326,16 @@ Good luck!
## Inspiration, Resources, Alternatives, and Credits
Dioxus is inspired by:
- React: for its hooks, concurrency, suspense
- Dodrio: for its research in bump allocation, double buffering, and diffing architecture
Alternatives to Dioxus include:
- Yew: supports function components and web, but no SSR, borrowed data, or bump allocation. Rather slow at times.
- Percy: supports function components, web, ssr, but lacks state management
- Sycamore: supports function components, web, ssr, but is closer to SolidJS than React
- MoonZoom/Seed: opinionated frameworks based on the Elm model (message, update) - no hooks
- MoonZoom/Seed: opinionated frameworks based on the Elm model (message, update)—no hooks
We've put a lot of work into making Dioxus ergonomic and *familiar*.
Our target audience is TypeScript developers looking to switch to Rust for the web - so we need to be comparable to React.
We've put a lot of work into making Dioxus ergonomic and _familiar_.
Our target audience is TypeScript developers looking to switch to Rust for the web—so we need to be comparable to React.
+6 -1
View File
@@ -1,3 +1,7 @@
#![doc = include_str!("../README.md")]
#![doc(html_logo_url = "https://avatars.githubusercontent.com/u/79236386")]
#![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")]
pub use dioxus_core as core;
#[cfg(feature = "hooks")]
@@ -24,7 +28,8 @@ pub mod prelude {
pub use dioxus_core::prelude::*;
#[cfg(feature = "macro")]
pub use dioxus_core_macro::{format_args_f, inline_props, render, rsx, Props};
#[allow(deprecated)]
pub use dioxus_core_macro::{component, format_args_f, inline_props, render, rsx, Props};
#[cfg(feature = "html")]
pub use dioxus_html as dioxus_elements;
+1
View File
@@ -28,6 +28,7 @@ impl CallbackApi {
}
}
#[must_use]
pub fn use_atom_context(cx: &ScopeState) -> &CallbackApi {
todo!()
}
+1
View File
@@ -13,6 +13,7 @@ use std::{
///
///
///
#[must_use]
pub fn use_atom_ref<'a, T: 'static>(
cx: &'a ScopeState,
atom: &'static AtomRef<T>,
+2
View File
@@ -2,10 +2,12 @@ use crate::{use_atom_root, AtomId, AtomRoot, Readable};
use dioxus_core::{ScopeId, ScopeState};
use std::rc::Rc;
#[must_use]
pub fn use_read<V: 'static>(cx: &ScopeState, f: impl Readable<V>) -> &V {
use_read_rc(cx, f).as_ref()
}
#[must_use]
pub fn use_read_rc<V: 'static>(cx: &ScopeState, f: impl Readable<V>) -> &Rc<V> {
let root = use_atom_root(cx);
+1
View File
@@ -2,6 +2,7 @@ use crate::{use_atom_root, Writable};
use dioxus_core::ScopeState;
use std::rc::Rc;
#[must_use]
pub fn use_set<T: 'static>(cx: &ScopeState, f: impl Writable<T>) -> &Rc<dyn Fn(T)> {
let root = use_atom_root(cx);
cx.use_hook(|| {
+1
View File
@@ -30,6 +30,7 @@ use std::{
/// ))
/// }
/// ```
#[must_use]
pub fn use_atom_state<T: 'static>(cx: &ScopeState, f: impl Writable<T>) -> &AtomState<T> {
let root = crate::use_atom_root(cx);
+2
View File
@@ -1,4 +1,6 @@
#![doc = include_str!("../README.md")]
#![doc(html_logo_url = "https://avatars.githubusercontent.com/u/79236386")]
#![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")]
pub mod prelude {
pub use crate::*;
+1 -1
View File
@@ -15,7 +15,7 @@
[discord-url]: https://discord.gg/XgGxMSkvUM
[Website](https://dioxuslabs.com) |
[Guides](https://dioxuslabs.com/docs/0.3/guide/en/) |
[Guides](https://dioxuslabs.com/learn/0.4/) |
[API Docs](https://docs.rs/dioxus-fullstack/latest/dioxus_sever) |
[Chat](https://discord.gg/XgGxMSkvUM)
@@ -5,8 +5,6 @@
//! cargo run --features ssr --release
//! ```
#![allow(non_snake_case)]
use dioxus::prelude::*;
use dioxus_fullstack::prelude::*;
use dioxus_router::prelude::*;
@@ -33,7 +31,7 @@ enum Route {
Blog { id: i32 },
}
#[inline_props]
#[component]
fn Blog(cx: Scope, id: i32) -> Element {
render! {
Link { to: Route::Home {}, "Go to counter" }
@@ -51,7 +49,7 @@ fn Blog(cx: Scope, id: i32) -> Element {
}
}
#[inline_props]
#[component]
fn Home(cx: Scope) -> Element {
let mut count = use_state(cx, || 0);
let text = use_state(cx, || "...".to_string());
@@ -5,7 +5,7 @@
//! cargo run --features ssr
//! ```
#![allow(non_snake_case, unused)]
#![allow(unused)]
use dioxus::prelude::*;
use dioxus_fullstack::{launch, prelude::*};
use dioxus_router::prelude::*;
@@ -49,7 +49,7 @@ enum Route {
Blog,
}
#[inline_props]
#[component]
fn Blog(cx: Scope) -> Element {
render! {
Link { to: Route::Home {}, "Go to counter" }
@@ -67,7 +67,7 @@ fn Blog(cx: Scope) -> Element {
}
}
#[inline_props]
#[component]
fn Home(cx: Scope) -> Element {
let mut count = use_state(cx, || 0);
let text = use_state(cx, || "...".to_string());
@@ -263,8 +263,8 @@ where
let res = service.run(req);
match res.await {
Ok(res) => Ok::<_, std::convert::Infallible>(res.map(|b| b.into())),
Err(_e) => {
let mut res = Response::new(Body::empty());
Err(e) => {
let mut res = Response::new(Body::from(e.to_string()));
*res.status_mut() = StatusCode::INTERNAL_SERVER_ERROR;
Ok(res)
}
@@ -143,7 +143,17 @@ pub fn register_server_fns(server_fn_route: &'static str) -> BoxedFilter<(impl R
let req = warp::hyper::Request::from_parts(parts, bytes.into());
service.run(req).await.map_err(|err| {
tracing::error!("Server function error: {}", err);
warp::reject::reject()
struct WarpServerFnError(String);
impl std::fmt::Debug for WarpServerFnError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl warp::reject::Reject for WarpServerFnError {}
warp::reject::custom(WarpServerFnError(err.to_string()))
})
}
})
@@ -22,6 +22,7 @@ use std::sync::Arc;
/// will be allowed to continue
///
/// - dependencies: a tuple of references to values that are PartialEq + Clone
#[must_use = "Consider using `cx.spawn` to run a future without reading its value"]
pub fn use_server_future<T, F, D>(
cx: &ScopeState,
dependencies: D,
+9 -2
View File
@@ -121,8 +121,15 @@ impl<Props: Clone + serde::Serialize + serde::de::DeserializeOwned + Send + Sync
#[cfg(feature = "web")]
/// Launch the web application
pub fn launch_web(self) {
let cfg = self.web_cfg.hydrate(true);
dioxus_web::launch_with_props(self.component, get_root_props_from_document().unwrap(), cfg);
#[cfg(not(feature = "ssr"))]
{
let cfg = self.web_cfg.hydrate(true);
dioxus_web::launch_with_props(
self.component,
get_root_props_from_document().unwrap(),
cfg,
);
}
}
#[cfg(feature = "desktop")]
+7
View File
@@ -64,3 +64,10 @@ pub mod prelude {
pub use hooks::{server_cached::server_cached, server_future::use_server_future};
}
// Warn users about overlapping features
#[cfg(all(feature = "ssr", feature = "web"))]
compile_error!("The `ssr` feature (enabled by `warp`, `axum`, or `salvo`) and `web` feature are overlapping. Please choose one or the other.");
#[cfg(all(feature = "ssr", feature = "desktop"))]
compile_error!("The `ssr` feature (enabled by `warp`, `axum`, or `salvo`) and `desktop` feature are overlapping. Please choose one or the other.");
+1 -1
View File
@@ -31,4 +31,4 @@ let store = Store::default();
## How it works
Internally
Internally, `generational-box` creates an arena of generational RefCell's that are recyled when the owner is dropped. You can think of the cells as something like `&'static RefCell<Box<dyn Any>>` with a generational check to make recyling a cell easier to debug. Then GenerationalBox's are `Copy` because the `&'static` pointer is `Copy`
+4 -4
View File
@@ -184,7 +184,7 @@ impl<T: 'static> GenerationalBox<T> {
}
/// Try to read the value. Returns None if the value is no longer valid.
pub fn try_read(&self) -> Option<Ref<'_, T>> {
pub fn try_read(&self) -> Option<Ref<'static, T>> {
self.validate()
.then(|| {
Ref::filter_map(self.raw.data.borrow(), |any| {
@@ -196,12 +196,12 @@ impl<T: 'static> GenerationalBox<T> {
}
/// Read the value. Panics if the value is no longer valid.
pub fn read(&self) -> Ref<'_, T> {
pub fn read(&self) -> Ref<'static, T> {
self.try_read().unwrap()
}
/// Try to write the value. Returns None if the value is no longer valid.
pub fn try_write(&self) -> Option<RefMut<'_, T>> {
pub fn try_write(&self) -> Option<RefMut<'static, T>> {
self.validate()
.then(|| {
RefMut::filter_map(self.raw.data.borrow_mut(), |any| {
@@ -213,7 +213,7 @@ impl<T: 'static> GenerationalBox<T> {
}
/// Write the value. Panics if the value is no longer valid.
pub fn write(&self) -> RefMut<'_, T> {
pub fn write(&self) -> RefMut<'static, T> {
self.try_write().unwrap()
}
+2 -7
View File
@@ -1,6 +1,5 @@
# Dioxus Hooks
[![Crates.io][crates-badge]][crates-url]
[![MIT licensed][mit-badge]][mit-url]
[![Build Status][actions-badge]][actions-url]
@@ -8,22 +7,18 @@
[crates-badge]: https://img.shields.io/crates/v/dioxus-hooks.svg
[crates-url]: https://crates.io/crates/dioxus-hooks
[mit-badge]: https://img.shields.io/badge/license-MIT-blue.svg
[mit-url]: https://github.com/dioxuslabs/dioxus/blob/master/LICENSE
[actions-badge]: https://github.com/dioxuslabs/dioxus/actions/workflows/main.yml/badge.svg
[actions-url]: https://github.com/dioxuslabs/dioxus/actions?query=workflow%3ACI+branch%3Amaster
[discord-badge]: https://img.shields.io/discord/899851952891002890.svg?logo=discord&style=flat-square
[discord-url]: https://discord.gg/XgGxMSkvUM
[Website](https://dioxuslabs.com) |
[Guides](https://dioxuslabs.com/docs/0.3/guide/en/) |
[Guides](https://dioxuslabs.com/learn/0.4/) |
[API Docs](https://docs.rs/dioxus-hooks/latest/dioxus_hooks) |
[Chat](https://discord.gg/XgGxMSkvUM)
## Overview
`dioxus-hooks` includes some basic useful hooks for dioxus:
@@ -42,13 +37,13 @@ This crate also provides a few helpful macros to get around some Rust lifetime m
- `use_future!()`
- `use_callback!()`
## Contributing
- Report issues on our [issue tracker](https://github.com/dioxuslabs/dioxus/issues).
- Join the discord and ask questions!
## License
This project is licensed under the [MIT license].
[mit license]: https://github.com/DioxusLabs/dioxus/blob/master/LICENSE-MIT
+4 -1
View File
@@ -17,6 +17,7 @@ use std::{
/// ```rust
/// use dioxus::prelude::*;
///
/// #[component]
/// fn Parent(cx: Scope) -> Element {
/// let count = use_tracked_state(cx, || 0);
///
@@ -27,7 +28,7 @@ use std::{
/// }
/// }
///
/// #[inline_props]
/// #[component]
/// fn Child(cx: Scope, count: Tracked<usize>) -> Element {
/// let less_than_five = use_selector(cx, count, |count| *count < 5);
///
@@ -36,6 +37,7 @@ use std::{
/// }
/// }
/// ```
#[must_use]
pub fn use_tracked_state<T: 'static>(cx: &ScopeState, init: impl FnOnce() -> T) -> &Tracked<T> {
cx.use_hook(|| {
let init = init();
@@ -159,6 +161,7 @@ impl<I> Drop for Tracker<I> {
}
}
#[must_use = "Consider using the `use_effect` hook to rerun an effect whenever the tracked state changes if you don't need the result of the computation"]
pub fn use_selector<I: 'static, O: Clone + PartialEq + 'static>(
cx: &ScopeState,
tracked: &Tracked<I>,
+26 -21
View File
@@ -1,7 +1,10 @@
#![doc = include_str!("../README.md")]
#![doc(html_logo_url = "https://avatars.githubusercontent.com/u/79236386")]
#![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")]
#![cfg_attr(feature = "nightly-features", feature(debug_refcell))]
#[macro_export]
/// A helper macro for using hooks and properties in async environements.
/// A helper macro for using hooks and properties in async environments.
///
/// # Usage
///
@@ -54,35 +57,37 @@ macro_rules! to_owned {
pub mod computed;
mod use_on_unmount;
pub use use_on_unmount::*;
mod use_on_destroy;
pub use use_on_destroy::*;
mod usecontext;
pub use usecontext::*;
mod use_context;
pub use use_context::*;
mod usestate;
pub use usestate::{use_state, UseState};
mod use_state;
pub use use_state::{use_state, UseState};
mod useref;
pub use useref::*;
mod use_ref;
pub use use_ref::*;
mod use_shared_state;
pub use use_shared_state::*;
mod usecoroutine;
pub use usecoroutine::*;
mod use_coroutine;
pub use use_coroutine::*;
mod usefuture;
pub use usefuture::*;
mod use_future;
pub use use_future::*;
mod useeffect;
pub use useeffect::*;
mod use_effect;
pub use use_effect::*;
mod usecallback;
pub use usecallback::*;
mod use_callback;
pub use use_callback::*;
mod usememo;
pub use usememo::*;
mod use_memo;
pub use use_memo::*;
mod userootcontext;
pub use userootcontext::*;
mod use_on_create;
pub use use_on_create::*;
mod use_root_context;
pub use use_root_context::*;
@@ -24,6 +24,7 @@ macro_rules! use_callback {
)
};
}
pub fn use_callback<T, R, F>(cx: &ScopeState, make: impl FnOnce() -> R) -> impl FnMut(T) + '_
where
R: FnMut(T) -> F + 'static,
File renamed without changes.
@@ -3,6 +3,7 @@ use dioxus_core::ScopeState;
/// Consume some context in the tree, providing a sharable handle to the value
///
/// Does not regenerate the value if the value is changed at the parent.
#[must_use]
pub fn use_context<T: 'static + Clone>(cx: &ScopeState) -> Option<&T> {
cx.use_hook(|| cx.consume_context::<T>()).as_ref()
}
@@ -79,6 +79,7 @@ where
/// Get a handle to a coroutine higher in the tree
///
/// See the docs for [`use_coroutine`] for more details.
#[must_use]
pub fn use_coroutine_handle<M: 'static>(cx: &ScopeState) -> Option<&Coroutine<M>> {
cx.use_hook(|| cx.consume_context::<Coroutine<M>>())
.as_ref()
@@ -3,18 +3,24 @@ use std::{any::Any, cell::Cell, future::Future};
use crate::UseFutureDep;
/// A hook that provides a future that executes after the hooks have been applied
/// A hook that provides a future that executes after the hooks have been applied.
///
/// Whenever the hooks dependencies change, the future will be re-evaluated.
/// If a future is pending when the dependencies change, the previous future
/// will be allowed to continue
/// will be allowed to continue.
///
/// - dependencies: a tuple of references to values that are `PartialEq` + `Clone`
/// **Note:** If your dependency list is always empty, use [`use_on_create`](crate::use_on_create).
///
/// ## Arguments
///
/// - `dependencies`: a tuple of references to values that are `PartialEq` + `Clone`.
/// - `future`: a closure that takes the `dependencies` as arguments and returns a `'static` future.
///
/// ## Examples
///
/// ```rust, no_run
/// #[inline_props]
/// # use dioxus::prelude::*;
/// #[component]
/// fn Profile(cx: Scope, id: usize) -> Element {
/// let name = use_state(cx, || None);
///
@@ -34,7 +40,8 @@ use crate::UseFutureDep;
/// )
/// }
///
/// fn app(cx: Scope) -> Element {
/// #[component]
/// fn App(cx: Scope) -> Element {
/// render!(Profile { id: 0 })
/// }
/// ```
@@ -14,7 +14,7 @@ use crate::{use_state, UseState};
///
/// Whenever the hooks dependencies change, the future will be re-evaluated.
/// If a future is pending when the dependencies change, the previous future
/// will be allowed to continue
/// will be canceled before the new one is started.
///
/// - dependencies: a tuple of references to values that are PartialEq + Clone
pub fn use_future<T, F, D>(
@@ -10,8 +10,9 @@ use crate::UseFutureDep;
/// ## Examples
///
/// ```rust, no_run
/// # use dioxus::prelude::*;
///
/// #[inline_props]
/// #[component]
/// fn Calculator(cx: Scope, number: usize) -> Element {
/// let bigger_number = use_memo(cx, (number,), |(number,)| {
/// // This will only be calculated when `number` has changed.
@@ -21,10 +22,13 @@ use crate::UseFutureDep;
/// p { "{bigger_number}" }
/// )
/// }
/// fn app(cx: Scope) -> Element {
///
/// #[component]
/// fn App(cx: Scope) -> Element {
/// render!(Calculator { number: 0 })
/// }
/// ```
#[must_use = "Consider using `use_effect` to run rerun a callback when dependencies change"]
pub fn use_memo<T, D>(cx: &ScopeState, dependencies: D, callback: impl FnOnce(D::Out) -> T) -> &T
where
T: 'static,
File renamed without changes.
+27
View File
@@ -0,0 +1,27 @@
use dioxus_core::ScopeState;
use std::cell::Cell;
use std::future::Future;
/// A hook that runs a future when the component is mounted.
///
/// This is just [`use_effect`](crate::use_effect), but with no dependencies.
/// If you have no dependencies, it's recommended to use this, not just because it's more readable,
/// but also because it's a tiny bit more efficient.
pub fn use_on_create<T, F>(cx: &ScopeState, future: impl FnOnce() -> F)
where
T: 'static,
F: Future<Output = T> + 'static,
{
let needs_regen = cx.use_hook(|| Cell::new(true));
if needs_regen.get() {
// We don't need regen anymore
needs_regen.set(false);
let fut = future();
cx.push_future(async move {
fut.await;
});
}
}
@@ -1,9 +1,20 @@
/// Creates a callback that will be run before the component is removed. This can be used to clean up side effects from the component (created with use_effect)
#[deprecated(
note = "Use `use_on_destroy` instead, which has the same functionality. \
This is deprecated because of the introduction of `use_on_create` which is better mirrored by `use_on_destroy`. \
The reason why `use_on_create` is not `use_on_mount` is because of potential confusion with `dioxus::events::onmounted`."
)]
pub fn use_on_unmount<D: FnOnce() + 'static>(cx: &dioxus_core::ScopeState, destroy: D) {
use_on_destroy(cx, destroy);
}
/// Creates a callback that will be run before the component is removed.
/// This can be used to clean up side effects from the component
/// (created with [`use_effect`](crate::use_effect)).
///
/// Example:
/// ```rust
/// use dioxus::prelude::*;
///
/// fn app(cx: Scope) -> Element {
/// let state = use_state(cx, || true);
/// render! {
@@ -25,7 +36,7 @@
/// }
/// }
/// }
///
/// fn child_component(cx: Scope) -> Element {
/// let original_scroll_position = use_state(cx, || 0.0);
/// use_effect(cx, (), move |_| {
@@ -38,8 +49,8 @@
/// original_scroll_position.set(window.scroll_y().unwrap());
/// }
/// });
/// use_on_unmount(cx, {
///
/// use_on_destroy(cx, {
/// to_owned![original_scroll_position];
/// /// restore scroll to the top of the page
/// move || {
@@ -47,7 +58,7 @@
/// window.scroll_with_x_and_y(*original_scroll_position.current(), 0.0);
/// }
/// });
///
/// render!{
/// div {
/// id: "my_element",
@@ -56,7 +67,7 @@
/// }
/// }
/// ```
pub fn use_on_unmount<D: FnOnce() + 'static>(cx: &dioxus_core::ScopeState, destroy: D) {
pub fn use_on_destroy<D: FnOnce() + 'static>(cx: &dioxus_core::ScopeState, destroy: D) {
cx.use_hook(|| LifeCycle {
ondestroy: Some(destroy),
});
Loaded 100 of 448 files, more files were not shown because too many files have changed in this diff. Show more