Merge branch 'master' into add-file-data-drag-event
This commit is contained in:
commit
847801908f
575 files changed
+12880
-17751
No files matched your search
@@ -1,10 +1,10 @@
|
||||
[package]
|
||||
name = "dioxus-autofmt"
|
||||
version = "0.3.0"
|
||||
version = { workspace = true }
|
||||
edition = "2021"
|
||||
authors = ["Jonathan Kelley"]
|
||||
description = "Autofomatter for Dioxus RSX"
|
||||
license = "MIT/Apache-2.0"
|
||||
license = "MIT OR Apache-2.0"
|
||||
repository = "https://github.com/DioxusLabs/dioxus/"
|
||||
homepage = "https://dioxuslabs.com"
|
||||
keywords = ["dom", "ui", "gui", "react"]
|
||||
@@ -14,11 +14,9 @@ keywords = ["dom", "ui", "gui", "react"]
|
||||
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"] }
|
||||
syn = { version = "2.0", features = ["full", "extra-traits", "visit"] }
|
||||
serde = { version = "1.0.136", features = ["derive"] }
|
||||
prettyplease = { package = "prettier-please", version = "0.1.16", features = [
|
||||
"verbatim",
|
||||
] }
|
||||
prettyplease = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions = "1.2.1"
|
||||
@@ -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
|
||||
|
||||
@@ -3,178 +3,32 @@
|
||||
//! Returns all macros that match a pattern. You can use this information to autoformat them later
|
||||
|
||||
use proc_macro2::LineColumn;
|
||||
use syn::{Block, Expr, File, Item, Macro, Stmt};
|
||||
use syn::{visit::Visit, File, Macro};
|
||||
|
||||
type CollectedMacro<'a> = &'a Macro;
|
||||
|
||||
pub fn collect_from_file<'a>(file: &'a File, macros: &mut Vec<CollectedMacro<'a>>) {
|
||||
for item in file.items.iter() {
|
||||
collect_from_item(item, macros);
|
||||
}
|
||||
MacroCollector::visit_file(&mut MacroCollector { macros }, file);
|
||||
}
|
||||
|
||||
pub fn collect_from_item<'a>(item: &'a Item, macros: &mut Vec<CollectedMacro<'a>>) {
|
||||
match item {
|
||||
Item::Fn(f) => collect_from_block(&f.block, macros),
|
||||
|
||||
// Ignore macros if they're not rsx or render
|
||||
Item::Macro(macro_) => {
|
||||
if macro_.mac.path.segments[0].ident == "rsx"
|
||||
|| macro_.mac.path.segments[0].ident == "render"
|
||||
{
|
||||
macros.push(¯o_.mac);
|
||||
}
|
||||
}
|
||||
|
||||
// Currently disabled since we're not focused on autoformatting these
|
||||
Item::Impl(_imp) => {}
|
||||
Item::Trait(_) => {}
|
||||
|
||||
// Global-ish things
|
||||
Item::Static(f) => collect_from_expr(&f.expr, macros),
|
||||
Item::Const(f) => collect_from_expr(&f.expr, macros),
|
||||
Item::Mod(s) => {
|
||||
if let Some((_, block)) = &s.content {
|
||||
for item in block {
|
||||
collect_from_item(item, macros);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// None of these we can really do anything with at the item level
|
||||
Item::Macro2(_)
|
||||
| Item::Enum(_)
|
||||
| Item::ExternCrate(_)
|
||||
| Item::ForeignMod(_)
|
||||
| Item::TraitAlias(_)
|
||||
| Item::Type(_)
|
||||
| Item::Struct(_)
|
||||
| Item::Union(_)
|
||||
| Item::Use(_)
|
||||
| Item::Verbatim(_) => {}
|
||||
_ => {}
|
||||
}
|
||||
struct MacroCollector<'a, 'b> {
|
||||
macros: &'a mut Vec<CollectedMacro<'b>>,
|
||||
}
|
||||
|
||||
pub fn collect_from_block<'a>(block: &'a Block, macros: &mut Vec<CollectedMacro<'a>>) {
|
||||
for stmt in &block.stmts {
|
||||
match stmt {
|
||||
Stmt::Item(item) => collect_from_item(item, macros),
|
||||
Stmt::Local(local) => {
|
||||
if let Some((_eq, init)) = &local.init {
|
||||
collect_from_expr(init, macros);
|
||||
}
|
||||
}
|
||||
Stmt::Expr(exp) | Stmt::Semi(exp, _) => collect_from_expr(exp, macros),
|
||||
impl<'a, 'b> Visit<'b> for MacroCollector<'a, 'b> {
|
||||
fn visit_macro(&mut self, i: &'b Macro) {
|
||||
if let Some("rsx" | "render") = i
|
||||
.path
|
||||
.segments
|
||||
.last()
|
||||
.map(|i| i.ident.to_string())
|
||||
.as_deref()
|
||||
{
|
||||
self.macros.push(i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn collect_from_expr<'a>(expr: &'a Expr, macros: &mut Vec<CollectedMacro<'a>>) {
|
||||
// collect an expr from the exprs, descending into blocks
|
||||
match expr {
|
||||
Expr::Macro(macro_) => {
|
||||
if macro_.mac.path.segments[0].ident == "rsx"
|
||||
|| macro_.mac.path.segments[0].ident == "render"
|
||||
{
|
||||
macros.push(¯o_.mac);
|
||||
}
|
||||
}
|
||||
|
||||
Expr::MethodCall(e) => {
|
||||
collect_from_expr(&e.receiver, macros);
|
||||
for expr in e.args.iter() {
|
||||
collect_from_expr(expr, macros);
|
||||
}
|
||||
}
|
||||
Expr::Assign(exp) => {
|
||||
collect_from_expr(&exp.left, macros);
|
||||
collect_from_expr(&exp.right, macros);
|
||||
}
|
||||
|
||||
Expr::Async(b) => collect_from_block(&b.block, macros),
|
||||
Expr::Block(b) => collect_from_block(&b.block, macros),
|
||||
Expr::Closure(c) => collect_from_expr(&c.body, macros),
|
||||
Expr::Let(l) => collect_from_expr(&l.expr, macros),
|
||||
Expr::Unsafe(u) => collect_from_block(&u.block, macros),
|
||||
Expr::Loop(l) => collect_from_block(&l.body, macros),
|
||||
|
||||
Expr::Call(c) => {
|
||||
collect_from_expr(&c.func, macros);
|
||||
for expr in c.args.iter() {
|
||||
collect_from_expr(expr, macros);
|
||||
}
|
||||
}
|
||||
|
||||
Expr::ForLoop(b) => {
|
||||
collect_from_expr(&b.expr, macros);
|
||||
collect_from_block(&b.body, macros);
|
||||
}
|
||||
Expr::If(f) => {
|
||||
collect_from_expr(&f.cond, macros);
|
||||
collect_from_block(&f.then_branch, macros);
|
||||
if let Some((_, else_branch)) = &f.else_branch {
|
||||
collect_from_expr(else_branch, macros);
|
||||
}
|
||||
}
|
||||
Expr::Yield(y) => {
|
||||
if let Some(expr) = &y.expr {
|
||||
collect_from_expr(expr, macros);
|
||||
}
|
||||
}
|
||||
|
||||
Expr::Return(r) => {
|
||||
if let Some(expr) = &r.expr {
|
||||
collect_from_expr(expr, macros);
|
||||
}
|
||||
}
|
||||
|
||||
Expr::Match(l) => {
|
||||
collect_from_expr(&l.expr, macros);
|
||||
for arm in l.arms.iter() {
|
||||
if let Some((_, expr)) = &arm.guard {
|
||||
collect_from_expr(expr, macros);
|
||||
}
|
||||
|
||||
collect_from_expr(&arm.body, macros);
|
||||
}
|
||||
}
|
||||
|
||||
Expr::While(w) => {
|
||||
collect_from_expr(&w.cond, macros);
|
||||
collect_from_block(&w.body, macros);
|
||||
}
|
||||
|
||||
// don't both formatting these for now
|
||||
Expr::Array(_)
|
||||
| Expr::AssignOp(_)
|
||||
| Expr::Await(_)
|
||||
| Expr::Binary(_)
|
||||
| Expr::Box(_)
|
||||
| Expr::Break(_)
|
||||
| Expr::Cast(_)
|
||||
| Expr::Continue(_)
|
||||
| Expr::Field(_)
|
||||
| Expr::Group(_)
|
||||
| Expr::Index(_)
|
||||
| Expr::Lit(_)
|
||||
| Expr::Paren(_)
|
||||
| Expr::Path(_)
|
||||
| Expr::Range(_)
|
||||
| Expr::Reference(_)
|
||||
| Expr::Repeat(_)
|
||||
| Expr::Struct(_)
|
||||
| Expr::Try(_)
|
||||
| Expr::TryBlock(_)
|
||||
| Expr::Tuple(_)
|
||||
| Expr::Type(_)
|
||||
| Expr::Unary(_)
|
||||
| Expr::Verbatim(_) => {}
|
||||
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
|
||||
pub fn byte_offset(input: &str, location: LineColumn) -> usize {
|
||||
let mut offset = 0;
|
||||
for _ in 1..location.line {
|
||||
|
||||
@@ -165,7 +165,14 @@ impl Writer<'_> {
|
||||
match &field.content {
|
||||
ContentField::ManExpr(exp) => {
|
||||
let out = prettyplease::unparse_expr(exp);
|
||||
write!(self.out, "{name}: {out}")?;
|
||||
let mut lines = out.split('\n').peekable();
|
||||
let first = lines.next().unwrap();
|
||||
write!(self.out, "{name}: {first}")?;
|
||||
for line in lines {
|
||||
self.out.new_line()?;
|
||||
self.out.indented_tab()?;
|
||||
write!(self.out, "{line}")?;
|
||||
}
|
||||
}
|
||||
ContentField::Formatted(s) => {
|
||||
write!(
|
||||
|
||||
@@ -216,7 +216,26 @@ impl Writer<'_> {
|
||||
}
|
||||
ElementAttr::AttrExpression { name, value } => {
|
||||
let out = prettyplease::unparse_expr(value);
|
||||
write!(self.out, "{name}: {out}")?;
|
||||
let mut lines = out.split('\n').peekable();
|
||||
let first = lines.next().unwrap();
|
||||
|
||||
// a one-liner for whatever reason
|
||||
// Does not need a new line
|
||||
if lines.peek().is_none() {
|
||||
write!(self.out, "{name}: {first}")?;
|
||||
} else {
|
||||
writeln!(self.out, "{name}: {first}")?;
|
||||
|
||||
while let Some(line) = lines.next() {
|
||||
self.out.indented_tab()?;
|
||||
write!(self.out, "{line}")?;
|
||||
if lines.peek().is_none() {
|
||||
write!(self.out, "")?;
|
||||
} else {
|
||||
writeln!(self.out)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ElementAttr::CustomAttrText { name, value } => {
|
||||
|
||||
@@ -27,41 +27,29 @@ impl Writer<'_> {
|
||||
// If the expr is multiline, we want to collect all of its lines together and write them out properly
|
||||
// This involves unshifting the first line if it's aligned
|
||||
let first_line = &self.src[start.line - 1];
|
||||
write!(
|
||||
self.out,
|
||||
"{}",
|
||||
&first_line[start.column - 1..first_line.len()].trim()
|
||||
)?;
|
||||
write!(self.out, "{}", &first_line[start.column - 1..].trim_start())?;
|
||||
|
||||
let first_prefix = &self.src[start.line - 1][..start.column];
|
||||
let offset = match first_prefix.trim() {
|
||||
"" => 0,
|
||||
_ => first_prefix
|
||||
.chars()
|
||||
.rev()
|
||||
.take_while(|c| c.is_whitespace())
|
||||
.count() as isize,
|
||||
};
|
||||
let prev_block_indent_level = crate::leading_whitespaces(first_line) / 4;
|
||||
|
||||
for (id, line) in self.src[start.line..end.line].iter().enumerate() {
|
||||
writeln!(self.out)?;
|
||||
// trim the leading whitespace
|
||||
let line = match id {
|
||||
x if x == (end.line - start.line) - 1 => &line[..end.column],
|
||||
_ => line,
|
||||
// check if this is the last line
|
||||
let line = {
|
||||
if id == (end.line - start.line) - 1 {
|
||||
&line[..end.column]
|
||||
} else {
|
||||
line
|
||||
}
|
||||
};
|
||||
|
||||
if offset < 0 {
|
||||
for _ in 0..-offset {
|
||||
write!(self.out, " ")?;
|
||||
}
|
||||
// trim the leading whitespace
|
||||
let previous_indent = crate::leading_whitespaces(line) / 4;
|
||||
let offset = previous_indent.saturating_sub(prev_block_indent_level);
|
||||
let required_indent = self.out.indent + offset;
|
||||
self.out.write_tabs(required_indent)?;
|
||||
|
||||
write!(self.out, "{line}")?;
|
||||
} else {
|
||||
let offset = offset as usize;
|
||||
let right = &line[offset..];
|
||||
write!(self.out, "{right}")?;
|
||||
}
|
||||
let line = line.trim_start();
|
||||
write!(self.out, "{line}")?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -58,7 +58,7 @@ pub fn fmt_file(contents: &str) -> Vec<FormattedBlock> {
|
||||
|
||||
let mut writer = Writer::new(contents);
|
||||
|
||||
// Dont parse nested macros
|
||||
// Don't parse nested macros
|
||||
let mut end_span = LineColumn { column: 0, line: 0 };
|
||||
for item in macros {
|
||||
let macro_path = &item.path.segments[0].ident;
|
||||
@@ -68,16 +68,11 @@ pub fn fmt_file(contents: &str) -> Vec<FormattedBlock> {
|
||||
continue;
|
||||
}
|
||||
|
||||
// item.parse_body::<CallBody>();
|
||||
let body = item.parse_body::<CallBody>().unwrap();
|
||||
|
||||
let rsx_start = macro_path.span().start();
|
||||
|
||||
writer.out.indent = &writer.src[rsx_start.line - 1]
|
||||
.chars()
|
||||
.take_while(|c| *c == ' ')
|
||||
.count()
|
||||
/ 4;
|
||||
writer.out.indent = leading_whitespaces(writer.src[rsx_start.line - 1]) / 4;
|
||||
|
||||
write_body(&mut writer, &body);
|
||||
|
||||
@@ -91,7 +86,8 @@ pub fn fmt_file(contents: &str) -> Vec<FormattedBlock> {
|
||||
MacroDelimiter::Paren(b) => b.span,
|
||||
MacroDelimiter::Brace(b) => b.span,
|
||||
MacroDelimiter::Bracket(b) => b.span,
|
||||
};
|
||||
}
|
||||
.join();
|
||||
|
||||
let mut formatted = String::new();
|
||||
|
||||
@@ -230,3 +226,14 @@ pub(crate) fn write_ifmt(input: &IfmtInput, writable: &mut impl Write) -> std::f
|
||||
let display = DisplayIfmt(input);
|
||||
write!(writable, "{}", display)
|
||||
}
|
||||
|
||||
pub fn leading_whitespaces(input: &str) -> usize {
|
||||
input
|
||||
.chars()
|
||||
.map_while(|c| match c {
|
||||
' ' => Some(1),
|
||||
'\t' => Some(4),
|
||||
_ => None,
|
||||
})
|
||||
.sum()
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
use dioxus::prelude::*;
|
||||
|
||||
#[inline_props]
|
||||
#[component]
|
||||
pub fn Explainer<'a>(
|
||||
cx: Scope<'a>,
|
||||
invert: bool,
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
[package]
|
||||
name = "dioxus-check"
|
||||
version = { workspace = true }
|
||||
edition = "2021"
|
||||
authors = ["Dioxus Labs"]
|
||||
description = "Checks Dioxus RSX files for issues"
|
||||
license = "MIT/Apache-2.0"
|
||||
repository = "https://github.com/DioxusLabs/dioxus/"
|
||||
homepage = "https://dioxuslabs.com"
|
||||
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"
|
||||
pretty_assertions = "1.2.1"
|
||||
@@ -0,0 +1,39 @@
|
||||
# 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
|
||||
[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/learn/0.4/) |
|
||||
[API Docs](https://docs.rs/dioxus-autofmt/latest/dioxus_autofmt) |
|
||||
[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/learn/0.4/reference/hooks#rules-of-hooks).
|
||||
|
||||
## 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
|
||||
|
||||
Unless you explicitly state otherwise, any contribution intentionally submitted
|
||||
for inclusion in Dioxus by you shall be licensed as MIT without any additional
|
||||
terms or conditions.
|
||||
@@ -0,0 +1,639 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use syn::{spanned::Spanned, visit::Visit, Pat};
|
||||
|
||||
use crate::{
|
||||
issues::{Issue, IssueReport},
|
||||
metadata::{
|
||||
AnyLoopInfo, ClosureInfo, ComponentInfo, ConditionalInfo, FnInfo, ForInfo, HookInfo,
|
||||
IfInfo, LoopInfo, MatchInfo, Span, WhileInfo,
|
||||
},
|
||||
};
|
||||
|
||||
struct VisitHooks {
|
||||
issues: Vec<Issue>,
|
||||
context: Vec<Node>,
|
||||
}
|
||||
|
||||
impl VisitHooks {
|
||||
const fn new() -> Self {
|
||||
Self {
|
||||
issues: vec![],
|
||||
context: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks a Dioxus file for issues.
|
||||
pub fn check_file(path: PathBuf, file_content: &str) -> IssueReport {
|
||||
let file = syn::parse_file(file_content).unwrap();
|
||||
let mut visit_hooks = VisitHooks::new();
|
||||
visit_hooks.visit_file(&file);
|
||||
IssueReport::new(
|
||||
path,
|
||||
std::env::current_dir().unwrap_or_default(),
|
||||
file_content.to_string(),
|
||||
visit_hooks.issues,
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum Node {
|
||||
If(IfInfo),
|
||||
Match(MatchInfo),
|
||||
For(ForInfo),
|
||||
While(WhileInfo),
|
||||
Loop(LoopInfo),
|
||||
Closure(ClosureInfo),
|
||||
ComponentFn(ComponentInfo),
|
||||
HookFn(HookInfo),
|
||||
OtherFn(FnInfo),
|
||||
}
|
||||
|
||||
fn returns_element(ty: &syn::ReturnType) -> bool {
|
||||
match ty {
|
||||
syn::ReturnType::Default => false,
|
||||
syn::ReturnType::Type(_, ref ty) => {
|
||||
if let syn::Type::Path(ref path) = **ty {
|
||||
if let Some(segment) = path.path.segments.last() {
|
||||
if segment.ident == "Element" {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn is_hook_ident(ident: &syn::Ident) -> bool {
|
||||
ident.to_string().starts_with("use_")
|
||||
}
|
||||
|
||||
fn is_component_fn(item_fn: &syn::ItemFn) -> bool {
|
||||
returns_element(&item_fn.sig.output)
|
||||
}
|
||||
|
||||
fn get_closure_hook_body(local: &syn::Local) -> Option<&syn::Expr> {
|
||||
if let Pat::Ident(ident) = &local.pat {
|
||||
if is_hook_ident(&ident.ident) {
|
||||
if let Some((_, expr)) = &local.init {
|
||||
if let syn::Expr::Closure(closure) = &**expr {
|
||||
return Some(&closure.body);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn fn_name_and_name_span(item_fn: &syn::ItemFn) -> (String, Span) {
|
||||
let name = item_fn.sig.ident.to_string();
|
||||
let name_span = item_fn.sig.ident.span().into();
|
||||
(name, name_span)
|
||||
}
|
||||
|
||||
impl<'ast> syn::visit::Visit<'ast> for VisitHooks {
|
||||
fn visit_expr_call(&mut self, i: &'ast syn::ExprCall) {
|
||||
if let syn::Expr::Path(ref path) = *i.func {
|
||||
if let Some(segment) = path.path.segments.last() {
|
||||
if is_hook_ident(&segment.ident) {
|
||||
let hook_info = HookInfo::new(
|
||||
i.span().into(),
|
||||
segment.ident.span().into(),
|
||||
segment.ident.to_string(),
|
||||
);
|
||||
let mut container_fn: Option<Node> = None;
|
||||
for node in self.context.iter().rev() {
|
||||
match node {
|
||||
Node::If(if_info) => {
|
||||
let issue = Issue::HookInsideConditional(
|
||||
hook_info.clone(),
|
||||
ConditionalInfo::If(if_info.clone()),
|
||||
);
|
||||
self.issues.push(issue);
|
||||
}
|
||||
Node::Match(match_info) => {
|
||||
let issue = Issue::HookInsideConditional(
|
||||
hook_info.clone(),
|
||||
ConditionalInfo::Match(match_info.clone()),
|
||||
);
|
||||
self.issues.push(issue);
|
||||
}
|
||||
Node::For(for_info) => {
|
||||
let issue = Issue::HookInsideLoop(
|
||||
hook_info.clone(),
|
||||
AnyLoopInfo::For(for_info.clone()),
|
||||
);
|
||||
self.issues.push(issue);
|
||||
}
|
||||
Node::While(while_info) => {
|
||||
let issue = Issue::HookInsideLoop(
|
||||
hook_info.clone(),
|
||||
AnyLoopInfo::While(while_info.clone()),
|
||||
);
|
||||
self.issues.push(issue);
|
||||
}
|
||||
Node::Loop(loop_info) => {
|
||||
let issue = Issue::HookInsideLoop(
|
||||
hook_info.clone(),
|
||||
AnyLoopInfo::Loop(loop_info.clone()),
|
||||
);
|
||||
self.issues.push(issue);
|
||||
}
|
||||
Node::Closure(closure_info) => {
|
||||
let issue = Issue::HookInsideClosure(
|
||||
hook_info.clone(),
|
||||
closure_info.clone(),
|
||||
);
|
||||
self.issues.push(issue);
|
||||
}
|
||||
Node::ComponentFn(_) | Node::HookFn(_) | Node::OtherFn(_) => {
|
||||
container_fn = Some(node.clone());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(Node::OtherFn(_)) = container_fn {
|
||||
let issue = Issue::HookOutsideComponent(hook_info);
|
||||
self.issues.push(issue);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_item_fn(&mut self, i: &'ast syn::ItemFn) {
|
||||
let (name, name_span) = fn_name_and_name_span(i);
|
||||
if is_component_fn(i) {
|
||||
self.context.push(Node::ComponentFn(ComponentInfo::new(
|
||||
i.span().into(),
|
||||
name,
|
||||
name_span,
|
||||
)));
|
||||
} else if is_hook_ident(&i.sig.ident) {
|
||||
self.context.push(Node::HookFn(HookInfo::new(
|
||||
i.span().into(),
|
||||
i.sig.ident.span().into(),
|
||||
name,
|
||||
)));
|
||||
} else {
|
||||
self.context
|
||||
.push(Node::OtherFn(FnInfo::new(i.span().into(), name, name_span)));
|
||||
}
|
||||
syn::visit::visit_item_fn(self, i);
|
||||
self.context.pop();
|
||||
}
|
||||
|
||||
fn visit_local(&mut self, i: &'ast syn::Local) {
|
||||
if let Some(body) = get_closure_hook_body(i) {
|
||||
// if the closure is a hook, we only visit the body of the closure.
|
||||
// this prevents adding a ClosureInfo node to the context
|
||||
syn::visit::visit_expr(self, body);
|
||||
} else {
|
||||
// otherwise visit the whole local
|
||||
syn::visit::visit_local(self, i);
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_expr_if(&mut self, i: &'ast syn::ExprIf) {
|
||||
self.context.push(Node::If(IfInfo::new(
|
||||
i.span().into(),
|
||||
i.if_token
|
||||
.span()
|
||||
.join(i.cond.span())
|
||||
.unwrap_or_else(|| i.span())
|
||||
.into(),
|
||||
)));
|
||||
syn::visit::visit_expr_if(self, i);
|
||||
self.context.pop();
|
||||
}
|
||||
|
||||
fn visit_expr_match(&mut self, i: &'ast syn::ExprMatch) {
|
||||
self.context.push(Node::Match(MatchInfo::new(
|
||||
i.span().into(),
|
||||
i.match_token
|
||||
.span()
|
||||
.join(i.expr.span())
|
||||
.unwrap_or_else(|| i.span())
|
||||
.into(),
|
||||
)));
|
||||
syn::visit::visit_expr_match(self, i);
|
||||
self.context.pop();
|
||||
}
|
||||
|
||||
fn visit_expr_for_loop(&mut self, i: &'ast syn::ExprForLoop) {
|
||||
self.context.push(Node::For(ForInfo::new(
|
||||
i.span().into(),
|
||||
i.for_token
|
||||
.span()
|
||||
.join(i.expr.span())
|
||||
.unwrap_or_else(|| i.span())
|
||||
.into(),
|
||||
)));
|
||||
syn::visit::visit_expr_for_loop(self, i);
|
||||
self.context.pop();
|
||||
}
|
||||
|
||||
fn visit_expr_while(&mut self, i: &'ast syn::ExprWhile) {
|
||||
self.context.push(Node::While(WhileInfo::new(
|
||||
i.span().into(),
|
||||
i.while_token
|
||||
.span()
|
||||
.join(i.cond.span())
|
||||
.unwrap_or_else(|| i.span())
|
||||
.into(),
|
||||
)));
|
||||
syn::visit::visit_expr_while(self, i);
|
||||
self.context.pop();
|
||||
}
|
||||
|
||||
fn visit_expr_loop(&mut self, i: &'ast syn::ExprLoop) {
|
||||
self.context
|
||||
.push(Node::Loop(LoopInfo::new(i.span().into())));
|
||||
syn::visit::visit_expr_loop(self, i);
|
||||
self.context.pop();
|
||||
}
|
||||
|
||||
fn visit_expr_closure(&mut self, i: &'ast syn::ExprClosure) {
|
||||
self.context
|
||||
.push(Node::Closure(ClosureInfo::new(i.span().into())));
|
||||
syn::visit::visit_expr_closure(self, i);
|
||||
self.context.pop();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::metadata::{
|
||||
AnyLoopInfo, ClosureInfo, ConditionalInfo, ForInfo, HookInfo, IfInfo, LineColumn, LoopInfo,
|
||||
MatchInfo, Span, WhileInfo,
|
||||
};
|
||||
use indoc::indoc;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_no_hooks() {
|
||||
let contents = indoc! {r#"
|
||||
fn App(cx: Scope) -> Element {
|
||||
rsx! {
|
||||
p { "Hello World" }
|
||||
}
|
||||
}
|
||||
"#};
|
||||
|
||||
let report = check_file("app.rs".into(), contents);
|
||||
|
||||
assert_eq!(report.issues, vec![]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hook_correctly_used_inside_component() {
|
||||
let contents = indoc! {r#"
|
||||
fn App(cx: Scope) -> Element {
|
||||
let count = use_state(cx, || 0);
|
||||
rsx! {
|
||||
p { "Hello World: {count}" }
|
||||
}
|
||||
}
|
||||
"#};
|
||||
|
||||
let report = check_file("app.rs".into(), contents);
|
||||
|
||||
assert_eq!(report.issues, vec![]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hook_correctly_used_inside_hook_fn() {
|
||||
let contents = indoc! {r#"
|
||||
fn use_thing(cx: Scope) -> UseState<i32> {
|
||||
use_state(cx, || 0)
|
||||
}
|
||||
"#};
|
||||
|
||||
let report = check_file("use_thing.rs".into(), contents);
|
||||
|
||||
assert_eq!(report.issues, vec![]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hook_correctly_used_inside_hook_closure() {
|
||||
let contents = indoc! {r#"
|
||||
fn App(cx: Scope) -> Element {
|
||||
let use_thing = || {
|
||||
use_state(cx, || 0)
|
||||
};
|
||||
let count = use_thing();
|
||||
rsx! {
|
||||
p { "Hello World: {count}" }
|
||||
}
|
||||
}
|
||||
"#};
|
||||
|
||||
let report = check_file("app.rs".into(), contents);
|
||||
|
||||
assert_eq!(report.issues, vec![]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_conditional_hook_if() {
|
||||
let contents = indoc! {r#"
|
||||
fn App(cx: Scope) -> Element {
|
||||
if you_are_happy && you_know_it {
|
||||
let something = use_state(cx, || "hands");
|
||||
println!("clap your {something}")
|
||||
}
|
||||
}
|
||||
"#};
|
||||
|
||||
let report = check_file("app.rs".into(), contents);
|
||||
|
||||
assert_eq!(
|
||||
report.issues,
|
||||
vec![Issue::HookInsideConditional(
|
||||
HookInfo::new(
|
||||
Span::new_from_str(
|
||||
r#"use_state(cx, || "hands")"#,
|
||||
LineColumn { line: 3, column: 24 },
|
||||
),
|
||||
Span::new_from_str(
|
||||
r#"use_state"#,
|
||||
LineColumn { line: 3, column: 24 },
|
||||
),
|
||||
"use_state".to_string()
|
||||
),
|
||||
ConditionalInfo::If(IfInfo::new(
|
||||
Span::new_from_str(
|
||||
"if you_are_happy && you_know_it {\n let something = use_state(cx, || \"hands\");\n println!(\"clap your {something}\")\n }",
|
||||
LineColumn { line: 2, column: 4 },
|
||||
),
|
||||
Span::new_from_str(
|
||||
"if you_are_happy && you_know_it",
|
||||
LineColumn { line: 2, column: 4 }
|
||||
)
|
||||
))
|
||||
)],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_conditional_hook_match() {
|
||||
let contents = indoc! {r#"
|
||||
fn App(cx: Scope) -> Element {
|
||||
match you_are_happy && you_know_it {
|
||||
true => {
|
||||
let something = use_state(cx, || "hands");
|
||||
println!("clap your {something}")
|
||||
}
|
||||
false => {}
|
||||
}
|
||||
}
|
||||
"#};
|
||||
|
||||
let report = check_file("app.rs".into(), contents);
|
||||
|
||||
assert_eq!(
|
||||
report.issues,
|
||||
vec![Issue::HookInsideConditional(
|
||||
HookInfo::new(
|
||||
Span::new_from_str(r#"use_state(cx, || "hands")"#, LineColumn { line: 4, column: 28 }),
|
||||
Span::new_from_str(r#"use_state"#, LineColumn { line: 4, column: 28 }),
|
||||
"use_state".to_string()
|
||||
),
|
||||
ConditionalInfo::Match(MatchInfo::new(
|
||||
Span::new_from_str(
|
||||
"match you_are_happy && you_know_it {\n true => {\n let something = use_state(cx, || \"hands\");\n println!(\"clap your {something}\")\n }\n false => {}\n }",
|
||||
LineColumn { line: 2, column: 4 },
|
||||
),
|
||||
Span::new_from_str("match you_are_happy && you_know_it", LineColumn { line: 2, column: 4 })
|
||||
))
|
||||
)]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_for_loop_hook() {
|
||||
let contents = indoc! {r#"
|
||||
fn App(cx: Scope) -> Element {
|
||||
for _name in &names {
|
||||
let is_selected = use_state(cx, || false);
|
||||
println!("selected: {is_selected}");
|
||||
}
|
||||
}
|
||||
"#};
|
||||
|
||||
let report = check_file("app.rs".into(), contents);
|
||||
|
||||
assert_eq!(
|
||||
report.issues,
|
||||
vec![Issue::HookInsideLoop(
|
||||
HookInfo::new(
|
||||
Span::new_from_str(
|
||||
"use_state(cx, || false)",
|
||||
LineColumn { line: 3, column: 26 },
|
||||
),
|
||||
Span::new_from_str(
|
||||
"use_state",
|
||||
LineColumn { line: 3, column: 26 },
|
||||
),
|
||||
"use_state".to_string()
|
||||
),
|
||||
AnyLoopInfo::For(ForInfo::new(
|
||||
Span::new_from_str(
|
||||
"for _name in &names {\n let is_selected = use_state(cx, || false);\n println!(\"selected: {is_selected}\");\n }",
|
||||
LineColumn { line: 2, column: 4 },
|
||||
),
|
||||
Span::new_from_str(
|
||||
"for _name in &names",
|
||||
LineColumn { line: 2, column: 4 },
|
||||
)
|
||||
))
|
||||
)]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_while_loop_hook() {
|
||||
let contents = indoc! {r#"
|
||||
fn App(cx: Scope) -> Element {
|
||||
while true {
|
||||
let something = use_state(cx, || "hands");
|
||||
println!("clap your {something}")
|
||||
}
|
||||
}
|
||||
"#};
|
||||
|
||||
let report = check_file("app.rs".into(), contents);
|
||||
|
||||
assert_eq!(
|
||||
report.issues,
|
||||
vec![Issue::HookInsideLoop(
|
||||
HookInfo::new(
|
||||
Span::new_from_str(
|
||||
r#"use_state(cx, || "hands")"#,
|
||||
LineColumn { line: 3, column: 24 },
|
||||
),
|
||||
Span::new_from_str(
|
||||
"use_state",
|
||||
LineColumn { line: 3, column: 24 },
|
||||
),
|
||||
"use_state".to_string()
|
||||
),
|
||||
AnyLoopInfo::While(WhileInfo::new(
|
||||
Span::new_from_str(
|
||||
"while true {\n let something = use_state(cx, || \"hands\");\n println!(\"clap your {something}\")\n }",
|
||||
LineColumn { line: 2, column: 4 },
|
||||
),
|
||||
Span::new_from_str(
|
||||
"while true",
|
||||
LineColumn { line: 2, column: 4 },
|
||||
)
|
||||
))
|
||||
)],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_loop_hook() {
|
||||
let contents = indoc! {r#"
|
||||
fn App(cx: Scope) -> Element {
|
||||
loop {
|
||||
let something = use_state(cx, || "hands");
|
||||
println!("clap your {something}")
|
||||
}
|
||||
}
|
||||
"#};
|
||||
|
||||
let report = check_file("app.rs".into(), contents);
|
||||
|
||||
assert_eq!(
|
||||
report.issues,
|
||||
vec![Issue::HookInsideLoop(
|
||||
HookInfo::new(
|
||||
Span::new_from_str(
|
||||
r#"use_state(cx, || "hands")"#,
|
||||
LineColumn { line: 3, column: 24 },
|
||||
),
|
||||
Span::new_from_str(
|
||||
"use_state",
|
||||
LineColumn { line: 3, column: 24 },
|
||||
),
|
||||
"use_state".to_string()
|
||||
),
|
||||
AnyLoopInfo::Loop(LoopInfo::new(Span::new_from_str(
|
||||
"loop {\n let something = use_state(cx, || \"hands\");\n println!(\"clap your {something}\")\n }",
|
||||
LineColumn { line: 2, column: 4 },
|
||||
)))
|
||||
)],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_conditional_okay() {
|
||||
let contents = indoc! {r#"
|
||||
fn App(cx: Scope) -> Element {
|
||||
let something = use_state(cx, || "hands");
|
||||
if you_are_happy && you_know_it {
|
||||
println!("clap your {something}")
|
||||
}
|
||||
}
|
||||
"#};
|
||||
|
||||
let report = check_file("app.rs".into(), contents);
|
||||
|
||||
assert_eq!(report.issues, vec![]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_closure_hook() {
|
||||
let contents = indoc! {r#"
|
||||
fn App(cx: Scope) -> Element {
|
||||
let _a = || {
|
||||
let b = use_state(cx, || 0);
|
||||
b.get()
|
||||
};
|
||||
}
|
||||
"#};
|
||||
|
||||
let report = check_file("app.rs".into(), contents);
|
||||
|
||||
assert_eq!(
|
||||
report.issues,
|
||||
vec![Issue::HookInsideClosure(
|
||||
HookInfo::new(
|
||||
Span::new_from_str(
|
||||
"use_state(cx, || 0)",
|
||||
LineColumn {
|
||||
line: 3,
|
||||
column: 16
|
||||
},
|
||||
),
|
||||
Span::new_from_str(
|
||||
"use_state",
|
||||
LineColumn {
|
||||
line: 3,
|
||||
column: 16
|
||||
},
|
||||
),
|
||||
"use_state".to_string()
|
||||
),
|
||||
ClosureInfo::new(Span::new_from_str(
|
||||
"|| {\n let b = use_state(cx, || 0);\n b.get()\n }",
|
||||
LineColumn {
|
||||
line: 2,
|
||||
column: 13
|
||||
},
|
||||
))
|
||||
)]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hook_outside_component() {
|
||||
let contents = indoc! {r#"
|
||||
fn not_component_or_hook(cx: Scope) {
|
||||
let _a = use_state(cx, || 0);
|
||||
}
|
||||
"#};
|
||||
|
||||
let report = check_file("app.rs".into(), contents);
|
||||
|
||||
assert_eq!(
|
||||
report.issues,
|
||||
vec![Issue::HookOutsideComponent(HookInfo::new(
|
||||
Span::new_from_str(
|
||||
"use_state(cx, || 0)",
|
||||
LineColumn {
|
||||
line: 2,
|
||||
column: 13
|
||||
}
|
||||
),
|
||||
Span::new_from_str(
|
||||
"use_state",
|
||||
LineColumn {
|
||||
line: 2,
|
||||
column: 13
|
||||
},
|
||||
),
|
||||
"use_state".to_string()
|
||||
))]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hook_inside_hook() {
|
||||
let contents = indoc! {r#"
|
||||
fn use_thing(cx: Scope) {
|
||||
let _a = use_state(cx, || 0);
|
||||
}
|
||||
"#};
|
||||
|
||||
let report = check_file("app.rs".into(), contents);
|
||||
|
||||
assert_eq!(report.issues, vec![]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,427 @@
|
||||
use owo_colors::{
|
||||
colors::{css::LightBlue, BrightRed},
|
||||
OwoColorize, Stream,
|
||||
};
|
||||
use std::{
|
||||
fmt::Display,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use crate::metadata::{
|
||||
AnyLoopInfo, ClosureInfo, ConditionalInfo, ForInfo, HookInfo, IfInfo, MatchInfo, WhileInfo,
|
||||
};
|
||||
|
||||
/// The result of checking a Dioxus file for issues.
|
||||
pub struct IssueReport {
|
||||
pub path: PathBuf,
|
||||
pub crate_root: PathBuf,
|
||||
pub file_content: String,
|
||||
pub issues: Vec<Issue>,
|
||||
}
|
||||
|
||||
impl IssueReport {
|
||||
pub fn new<S: ToString>(
|
||||
path: PathBuf,
|
||||
crate_root: PathBuf,
|
||||
file_content: S,
|
||||
issues: Vec<Issue>,
|
||||
) -> Self {
|
||||
Self {
|
||||
path,
|
||||
crate_root,
|
||||
file_content: file_content.to_string(),
|
||||
issues,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn lightblue(text: &str) -> String {
|
||||
text.if_supports_color(Stream::Stderr, |text| text.fg::<LightBlue>())
|
||||
.to_string()
|
||||
}
|
||||
|
||||
fn brightred(text: &str) -> String {
|
||||
text.if_supports_color(Stream::Stderr, |text| text.fg::<BrightRed>())
|
||||
.to_string()
|
||||
}
|
||||
|
||||
fn bold(text: &str) -> String {
|
||||
text.if_supports_color(Stream::Stderr, |text| text.bold())
|
||||
.to_string()
|
||||
}
|
||||
|
||||
impl Display for IssueReport {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let relative_file = Path::new(&self.path)
|
||||
.strip_prefix(&self.crate_root)
|
||||
.unwrap_or(Path::new(&self.path))
|
||||
.display();
|
||||
|
||||
let pipe_char = lightblue("|");
|
||||
|
||||
for (i, issue) in self.issues.iter().enumerate() {
|
||||
let hook_info = issue.hook_info();
|
||||
let hook_span = hook_info.span;
|
||||
let hook_name_span = hook_info.name_span;
|
||||
let error_line = format!("{}: {}", brightred("error"), issue);
|
||||
writeln!(f, "{}", bold(&error_line))?;
|
||||
writeln!(
|
||||
f,
|
||||
" {} {}:{}:{}",
|
||||
lightblue("-->"),
|
||||
relative_file,
|
||||
hook_span.start.line,
|
||||
hook_span.start.column + 1
|
||||
)?;
|
||||
let max_line_num_len = hook_span.end.line.to_string().len();
|
||||
writeln!(f, "{:>max_line_num_len$} {}", "", pipe_char)?;
|
||||
for (i, line) in self.file_content.lines().enumerate() {
|
||||
let line_num = i + 1;
|
||||
if line_num >= hook_span.start.line && line_num <= hook_span.end.line {
|
||||
writeln!(
|
||||
f,
|
||||
"{:>max_line_num_len$} {} {}",
|
||||
lightblue(&line_num.to_string()),
|
||||
pipe_char,
|
||||
line,
|
||||
)?;
|
||||
if line_num == hook_span.start.line {
|
||||
let mut caret = String::new();
|
||||
for _ in 0..hook_name_span.start.column {
|
||||
caret.push(' ');
|
||||
}
|
||||
for _ in hook_name_span.start.column..hook_name_span.end.column {
|
||||
caret.push('^');
|
||||
}
|
||||
writeln!(
|
||||
f,
|
||||
"{:>max_line_num_len$} {} {}",
|
||||
"",
|
||||
pipe_char,
|
||||
brightred(&caret),
|
||||
)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let note_text_prefix = format!(
|
||||
"{:>max_line_num_len$} {}\n{:>max_line_num_len$} {} note:",
|
||||
"",
|
||||
pipe_char,
|
||||
"",
|
||||
lightblue("=")
|
||||
);
|
||||
|
||||
match issue {
|
||||
Issue::HookInsideConditional(
|
||||
_,
|
||||
ConditionalInfo::If(IfInfo { span: _, head_span }),
|
||||
)
|
||||
| Issue::HookInsideConditional(
|
||||
_,
|
||||
ConditionalInfo::Match(MatchInfo { span: _, head_span }),
|
||||
) => {
|
||||
if let Some(source_text) = &head_span.source_text {
|
||||
writeln!(
|
||||
f,
|
||||
"{} `{} {{ … }}` is the conditional",
|
||||
note_text_prefix, source_text,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
Issue::HookInsideLoop(_, AnyLoopInfo::For(ForInfo { span: _, head_span }))
|
||||
| Issue::HookInsideLoop(_, AnyLoopInfo::While(WhileInfo { span: _, head_span })) => {
|
||||
if let Some(source_text) = &head_span.source_text {
|
||||
writeln!(
|
||||
f,
|
||||
"{} `{} {{ … }}` is the loop",
|
||||
note_text_prefix, source_text,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
Issue::HookInsideLoop(_, AnyLoopInfo::Loop(_)) => {
|
||||
writeln!(f, "{} `loop {{ … }}` is the loop", note_text_prefix,)?;
|
||||
}
|
||||
Issue::HookOutsideComponent(_) | Issue::HookInsideClosure(_, _) => {}
|
||||
}
|
||||
|
||||
if i < self.issues.len() - 1 {
|
||||
writeln!(f)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[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/learn/0.4/reference/hooks#no-hooks-in-conditionals
|
||||
HookInsideConditional(HookInfo, ConditionalInfo),
|
||||
/// https://dioxuslabs.com/learn/0.4/reference/hooks#no-hooks-in-loops
|
||||
HookInsideLoop(HookInfo, AnyLoopInfo),
|
||||
/// https://dioxuslabs.com/learn/0.4/reference/hooks#no-hooks-in-closures
|
||||
HookInsideClosure(HookInfo, ClosureInfo),
|
||||
HookOutsideComponent(HookInfo),
|
||||
}
|
||||
|
||||
impl Issue {
|
||||
pub fn hook_info(&self) -> HookInfo {
|
||||
match self {
|
||||
Issue::HookInsideConditional(hook_info, _)
|
||||
| Issue::HookInsideLoop(hook_info, _)
|
||||
| Issue::HookInsideClosure(hook_info, _)
|
||||
| Issue::HookOutsideComponent(hook_info) => hook_info.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Issue {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Issue::HookInsideConditional(hook_info, conditional_info) => {
|
||||
write!(
|
||||
f,
|
||||
"hook called conditionally: `{}` (inside `{}`)",
|
||||
hook_info.name,
|
||||
match conditional_info {
|
||||
ConditionalInfo::If(_) => "if",
|
||||
ConditionalInfo::Match(_) => "match",
|
||||
}
|
||||
)
|
||||
}
|
||||
Issue::HookInsideLoop(hook_info, loop_info) => {
|
||||
write!(
|
||||
f,
|
||||
"hook called in a loop: `{}` (inside {})",
|
||||
hook_info.name,
|
||||
match loop_info {
|
||||
AnyLoopInfo::For(_) => "`for` loop",
|
||||
AnyLoopInfo::While(_) => "`while` loop",
|
||||
AnyLoopInfo::Loop(_) => "`loop`",
|
||||
}
|
||||
)
|
||||
}
|
||||
Issue::HookInsideClosure(hook_info, _) => {
|
||||
write!(f, "hook called in a closure: `{}`", hook_info.name)
|
||||
}
|
||||
Issue::HookOutsideComponent(hook_info) => {
|
||||
write!(
|
||||
f,
|
||||
"hook called outside component or hook: `{}`",
|
||||
hook_info.name
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::check_file;
|
||||
use indoc::indoc;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_issue_report_display_conditional_if() {
|
||||
owo_colors::set_override(false);
|
||||
let issue_report = check_file(
|
||||
"src/main.rs".into(),
|
||||
indoc! {r#"
|
||||
fn App(cx: Scope) -> Element {
|
||||
if you_are_happy && you_know_it {
|
||||
let something = use_state(cx, || "hands");
|
||||
println!("clap your {something}")
|
||||
}
|
||||
}
|
||||
"#},
|
||||
);
|
||||
|
||||
let expected = indoc! {r#"
|
||||
error: hook called conditionally: `use_state` (inside `if`)
|
||||
--> src/main.rs:3:25
|
||||
|
|
||||
3 | let something = use_state(cx, || "hands");
|
||||
| ^^^^^^^^^
|
||||
|
|
||||
= note: `if you_are_happy && you_know_it { … }` is the conditional
|
||||
"#};
|
||||
|
||||
assert_eq!(expected, issue_report.to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_issue_report_display_conditional_match() {
|
||||
owo_colors::set_override(false);
|
||||
let issue_report = check_file(
|
||||
"src/main.rs".into(),
|
||||
indoc! {r#"
|
||||
fn App(cx: Scope) -> Element {
|
||||
match you_are_happy && you_know_it {
|
||||
true => {
|
||||
let something = use_state(cx, || "hands");
|
||||
println!("clap your {something}")
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
"#},
|
||||
);
|
||||
|
||||
let expected = indoc! {r#"
|
||||
error: hook called conditionally: `use_state` (inside `match`)
|
||||
--> src/main.rs:4:29
|
||||
|
|
||||
4 | let something = use_state(cx, || "hands");
|
||||
| ^^^^^^^^^
|
||||
|
|
||||
= note: `match you_are_happy && you_know_it { … }` is the conditional
|
||||
"#};
|
||||
|
||||
assert_eq!(expected, issue_report.to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_issue_report_display_for_loop() {
|
||||
owo_colors::set_override(false);
|
||||
let issue_report = check_file(
|
||||
"src/main.rs".into(),
|
||||
indoc! {r#"
|
||||
fn App(cx: Scope) -> Element {
|
||||
for i in 0..10 {
|
||||
let something = use_state(cx, || "hands");
|
||||
println!("clap your {something}")
|
||||
}
|
||||
}
|
||||
"#},
|
||||
);
|
||||
|
||||
let expected = indoc! {r#"
|
||||
error: hook called in a loop: `use_state` (inside `for` loop)
|
||||
--> src/main.rs:3:25
|
||||
|
|
||||
3 | let something = use_state(cx, || "hands");
|
||||
| ^^^^^^^^^
|
||||
|
|
||||
= note: `for i in 0..10 { … }` is the loop
|
||||
"#};
|
||||
|
||||
assert_eq!(expected, issue_report.to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_issue_report_display_while_loop() {
|
||||
owo_colors::set_override(false);
|
||||
let issue_report = check_file(
|
||||
"src/main.rs".into(),
|
||||
indoc! {r#"
|
||||
fn App(cx: Scope) -> Element {
|
||||
while check_thing() {
|
||||
let something = use_state(cx, || "hands");
|
||||
println!("clap your {something}")
|
||||
}
|
||||
}
|
||||
"#},
|
||||
);
|
||||
|
||||
let expected = indoc! {r#"
|
||||
error: hook called in a loop: `use_state` (inside `while` loop)
|
||||
--> src/main.rs:3:25
|
||||
|
|
||||
3 | let something = use_state(cx, || "hands");
|
||||
| ^^^^^^^^^
|
||||
|
|
||||
= note: `while check_thing() { … }` is the loop
|
||||
"#};
|
||||
|
||||
assert_eq!(expected, issue_report.to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_issue_report_display_loop() {
|
||||
owo_colors::set_override(false);
|
||||
let issue_report = check_file(
|
||||
"src/main.rs".into(),
|
||||
indoc! {r#"
|
||||
fn App(cx: Scope) -> Element {
|
||||
loop {
|
||||
let something = use_state(cx, || "hands");
|
||||
println!("clap your {something}")
|
||||
}
|
||||
}
|
||||
"#},
|
||||
);
|
||||
|
||||
let expected = indoc! {r#"
|
||||
error: hook called in a loop: `use_state` (inside `loop`)
|
||||
--> src/main.rs:3:25
|
||||
|
|
||||
3 | let something = use_state(cx, || "hands");
|
||||
| ^^^^^^^^^
|
||||
|
|
||||
= note: `loop { … }` is the loop
|
||||
"#};
|
||||
|
||||
assert_eq!(expected, issue_report.to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_issue_report_display_closure() {
|
||||
owo_colors::set_override(false);
|
||||
let issue_report = check_file(
|
||||
"src/main.rs".into(),
|
||||
indoc! {r#"
|
||||
fn App(cx: Scope) -> Element {
|
||||
let something = || {
|
||||
let something = use_state(cx, || "hands");
|
||||
println!("clap your {something}")
|
||||
};
|
||||
}
|
||||
"#},
|
||||
);
|
||||
|
||||
let expected = indoc! {r#"
|
||||
error: hook called in a closure: `use_state`
|
||||
--> src/main.rs:3:25
|
||||
|
|
||||
3 | let something = use_state(cx, || "hands");
|
||||
| ^^^^^^^^^
|
||||
"#};
|
||||
|
||||
assert_eq!(expected, issue_report.to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_issue_report_display_multiline_hook() {
|
||||
owo_colors::set_override(false);
|
||||
let issue_report = check_file(
|
||||
"src/main.rs".into(),
|
||||
indoc! {r#"
|
||||
fn App(cx: Scope) -> Element {
|
||||
if you_are_happy && you_know_it {
|
||||
let something = use_state(cx, || {
|
||||
"hands"
|
||||
});
|
||||
println!("clap your {something}")
|
||||
}
|
||||
}
|
||||
"#},
|
||||
);
|
||||
|
||||
let expected = indoc! {r#"
|
||||
error: hook called conditionally: `use_state` (inside `if`)
|
||||
--> src/main.rs:3:25
|
||||
|
|
||||
3 | let something = use_state(cx, || {
|
||||
| ^^^^^^^^^
|
||||
4 | "hands"
|
||||
5 | });
|
||||
|
|
||||
= note: `if you_are_happy && you_know_it { … }` is the conditional
|
||||
"#};
|
||||
|
||||
assert_eq!(expected, issue_report.to_string());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
mod check;
|
||||
mod issues;
|
||||
mod metadata;
|
||||
|
||||
pub use check::check_file;
|
||||
pub use issues::{Issue, IssueReport};
|
||||
@@ -0,0 +1,202 @@
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
/// Information about a hook call or function.
|
||||
pub struct HookInfo {
|
||||
/// The name of the hook, e.g. `use_state`.
|
||||
pub name: String,
|
||||
/// The span of the hook, e.g. `use_state(cx, || 0)`.
|
||||
pub span: Span,
|
||||
/// The span of the name, e.g. `use_state`.
|
||||
pub name_span: Span,
|
||||
}
|
||||
|
||||
impl HookInfo {
|
||||
pub const fn new(span: Span, name_span: Span, name: String) -> Self {
|
||||
Self {
|
||||
span,
|
||||
name_span,
|
||||
name,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum ConditionalInfo {
|
||||
If(IfInfo),
|
||||
Match(MatchInfo),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct IfInfo {
|
||||
/// The span of the `if` statement, e.g. `if true { ... }`.
|
||||
pub span: Span,
|
||||
/// The span of the `if true` part only.
|
||||
pub head_span: Span,
|
||||
}
|
||||
|
||||
impl IfInfo {
|
||||
pub const fn new(span: Span, head_span: Span) -> Self {
|
||||
Self { span, head_span }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct MatchInfo {
|
||||
/// The span of the `match` statement, e.g. `match true { ... }`.
|
||||
pub span: Span,
|
||||
/// The span of the `match true` part only.
|
||||
pub head_span: Span,
|
||||
}
|
||||
|
||||
impl MatchInfo {
|
||||
pub const fn new(span: Span, head_span: Span) -> Self {
|
||||
Self { span, head_span }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
/// Information about one of the possible loop types.
|
||||
pub enum AnyLoopInfo {
|
||||
For(ForInfo),
|
||||
While(WhileInfo),
|
||||
Loop(LoopInfo),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
/// Information about a `for` loop.
|
||||
pub struct ForInfo {
|
||||
pub span: Span,
|
||||
pub head_span: Span,
|
||||
}
|
||||
|
||||
impl ForInfo {
|
||||
pub const fn new(span: Span, head_span: Span) -> Self {
|
||||
Self { span, head_span }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
/// Information about a `while` loop.
|
||||
pub struct WhileInfo {
|
||||
pub span: Span,
|
||||
pub head_span: Span,
|
||||
}
|
||||
|
||||
impl WhileInfo {
|
||||
pub const fn new(span: Span, head_span: Span) -> Self {
|
||||
Self { span, head_span }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
/// Information about a `loop` loop.
|
||||
pub struct LoopInfo {
|
||||
pub span: Span,
|
||||
}
|
||||
|
||||
impl LoopInfo {
|
||||
pub const fn new(span: Span) -> Self {
|
||||
Self { span }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
/// Information about a closure.
|
||||
pub struct ClosureInfo {
|
||||
pub span: Span,
|
||||
}
|
||||
|
||||
impl ClosureInfo {
|
||||
pub const fn new(span: Span) -> Self {
|
||||
Self { span }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
/// Information about a component function.
|
||||
pub struct ComponentInfo {
|
||||
pub span: Span,
|
||||
pub name: String,
|
||||
pub name_span: Span,
|
||||
}
|
||||
|
||||
impl ComponentInfo {
|
||||
pub const fn new(span: Span, name: String, name_span: Span) -> Self {
|
||||
Self {
|
||||
span,
|
||||
name,
|
||||
name_span,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
/// Information about a non-component, non-hook function.
|
||||
pub struct FnInfo {
|
||||
pub span: Span,
|
||||
pub name: String,
|
||||
pub name_span: Span,
|
||||
}
|
||||
|
||||
impl FnInfo {
|
||||
pub const fn new(span: Span, name: String, name_span: Span) -> Self {
|
||||
Self {
|
||||
span,
|
||||
name,
|
||||
name_span,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
/// A span of text in a source code file.
|
||||
pub struct Span {
|
||||
pub source_text: Option<String>,
|
||||
pub start: LineColumn,
|
||||
pub end: LineColumn,
|
||||
}
|
||||
|
||||
impl Span {
|
||||
pub fn new_from_str(source_text: &str, start: LineColumn) -> Self {
|
||||
let mut lines = source_text.lines();
|
||||
let first_line = lines.next().unwrap_or_default();
|
||||
let mut end = LineColumn {
|
||||
line: start.line,
|
||||
column: start.column + first_line.len(),
|
||||
};
|
||||
for line in lines {
|
||||
end.line += 1;
|
||||
end.column = line.len();
|
||||
}
|
||||
Self {
|
||||
source_text: Some(source_text.to_string()),
|
||||
start,
|
||||
end,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<proc_macro2::Span> for Span {
|
||||
fn from(span: proc_macro2::Span) -> Self {
|
||||
Self {
|
||||
source_text: span.source_text(),
|
||||
start: span.start().into(),
|
||||
end: span.end().into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
/// A location in a source code file.
|
||||
pub struct LineColumn {
|
||||
pub line: usize,
|
||||
pub column: usize,
|
||||
}
|
||||
|
||||
impl From<proc_macro2::LineColumn> for LineColumn {
|
||||
fn from(lc: proc_macro2::LineColumn) -> Self {
|
||||
Self {
|
||||
line: lc.line,
|
||||
column: lc.column,
|
||||
}
|
||||
}
|
||||
}
|
||||
+9
-25
@@ -8,47 +8,31 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
- uses: Swatinem/rust-cache@v1
|
||||
- uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: check
|
||||
- run: cargo check
|
||||
|
||||
test:
|
||||
name: Test Suite
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
- uses: Swatinem/rust-cache@v1
|
||||
- uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: test
|
||||
- run: cargo test
|
||||
|
||||
fmt:
|
||||
name: Rustfmt
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
- uses: Swatinem/rust-cache@v1
|
||||
- run: rustup component add rustfmt
|
||||
- uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: fmt
|
||||
args: --all -- --check
|
||||
- run: cargo fmt --all -- --check
|
||||
|
||||
# clippy:
|
||||
# name: Clippy
|
||||
|
||||
+24
-8
@@ -1,16 +1,17 @@
|
||||
[package]
|
||||
name = "dioxus-cli"
|
||||
version = "0.3.1"
|
||||
version = "0.4.1"
|
||||
authors = ["Jonathan Kelley"]
|
||||
edition = "2021"
|
||||
description = "CLI tool for developing, testing, and publishing Dioxus apps"
|
||||
repository = "https://github.com/DioxusLabs/dioxus/"
|
||||
license = "MIT/Apache-2.0"
|
||||
license = "MIT OR Apache-2.0"
|
||||
keywords = ["react", "gui", "cli", "dioxus", "wasm"]
|
||||
|
||||
[dependencies]
|
||||
# cli core
|
||||
clap = { version = "4.2", features = ["derive"] }
|
||||
thiserror = "1.0.30"
|
||||
thiserror = { workspace = true }
|
||||
wasm-bindgen-cli-support = "0.2"
|
||||
colored = "2.0.0"
|
||||
|
||||
@@ -24,7 +25,7 @@ fs_extra = "1.2.0"
|
||||
cargo_toml = "0.11.4"
|
||||
futures = "0.3.21"
|
||||
notify = { version = "5.0.0-pre.16", features = ["serde"] }
|
||||
html_parser.workspace = true
|
||||
html_parser = { workspace = true }
|
||||
binary-install = "0.0.2"
|
||||
convert_case = "0.5.0"
|
||||
cargo_metadata = "0.15.0"
|
||||
@@ -35,7 +36,7 @@ chrono = "0.4.19"
|
||||
anyhow = "1.0.53"
|
||||
hyper = "0.14.17"
|
||||
hyper-rustls = "0.23.2"
|
||||
indicatif = "0.17.0-rc.11"
|
||||
indicatif = "0.17.5"
|
||||
subprocess = "0.2.9"
|
||||
|
||||
axum = { version = "0.5.1", features = ["ws", "headers"] }
|
||||
@@ -57,7 +58,7 @@ flate2 = "1.0.22"
|
||||
tar = "0.4.38"
|
||||
zip = "0.6.2"
|
||||
tower = "0.4.12"
|
||||
syn = { version = "1.0", features = ["full", "extra-traits"] }
|
||||
syn = { version = "2.0", features = ["full", "extra-traits"] }
|
||||
proc-macro2 = { version = "1.0", features = ["span-locations"] }
|
||||
lazy_static = "1.4.0"
|
||||
|
||||
@@ -72,15 +73,21 @@ mlua = { version = "0.8.1", features = [
|
||||
ctrlc = "3.2.3"
|
||||
gitignore = "1.0.7"
|
||||
open = "4.1.0"
|
||||
cargo-generate = "0.18.3"
|
||||
cargo-generate = "0.18"
|
||||
toml_edit = "0.19.11"
|
||||
# dioxus-rsx = "0.0.1"
|
||||
|
||||
# bundling
|
||||
tauri-bundler = { version = "=1.3.0", features = ["native-tls-vendored"] }
|
||||
tauri-utils = "=1.4.*"
|
||||
|
||||
dioxus-autofmt = { workspace = true }
|
||||
dioxus-check = { workspace = true }
|
||||
rsx-rosetta = { workspace = true }
|
||||
dioxus-rsx = { workspace = true }
|
||||
dioxus-html = { workspace = true, features = ["hot-reload-context"] }
|
||||
dioxus-core = { workspace = true, features = ["serialize"] }
|
||||
dioxus-hot-reload = { workspace = true }
|
||||
interprocess-docfix = { version = "1.2.2" }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
@@ -89,3 +96,12 @@ plugin = ["mlua"]
|
||||
[[bin]]
|
||||
path = "src/main.rs"
|
||||
name = "dx"
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.3"
|
||||
|
||||
[package.metadata.binstall]
|
||||
pkg-url = "{ repo }/releases/download/v{ version }/dx-{ target }{ archive-suffix }"
|
||||
|
||||
[package.metadata.binstall.overrides.x86_64-pc-windows-msvc]
|
||||
pkg-fmt = "zip"
|
||||
+21
-18
@@ -1,29 +1,31 @@
|
||||
[application]
|
||||
|
||||
# dioxus project name
|
||||
name = "dioxus-cli"
|
||||
# App name
|
||||
name = "project_name"
|
||||
|
||||
# default platfrom
|
||||
# you can also use `dx serve/build --platform XXX` to use other platform
|
||||
# value: web | desktop
|
||||
default_platform = "desktop"
|
||||
# The Dioxus platform to default to
|
||||
default_platform = "web"
|
||||
|
||||
# Web `build` & `serve` dist path
|
||||
# `build` & `serve` output path
|
||||
out_dir = "dist"
|
||||
|
||||
# resource (static) file folder
|
||||
# The static resource path
|
||||
asset_dir = "public"
|
||||
|
||||
[web.app]
|
||||
|
||||
# HTML title tag content
|
||||
title = "dioxus | ⛺"
|
||||
title = "project_name"
|
||||
|
||||
[web.watcher]
|
||||
|
||||
watch_path = ["src"]
|
||||
# When watcher is triggered, regenerate the `index.html`
|
||||
reload_html = true
|
||||
|
||||
# include `assets` in web platform
|
||||
# Which files or dirs will be monitored
|
||||
watch_path = ["src", "public"]
|
||||
|
||||
# Include style or script assets
|
||||
[web.resource]
|
||||
|
||||
# CSS style file
|
||||
@@ -34,12 +36,13 @@ script = []
|
||||
|
||||
[web.resource.dev]
|
||||
|
||||
# Javascript code file
|
||||
# serve: [dev-server] only
|
||||
# Same as [web.resource], but for development servers
|
||||
|
||||
# CSS style file
|
||||
style = []
|
||||
|
||||
# JavaScript files
|
||||
script = []
|
||||
|
||||
[application.tools]
|
||||
|
||||
# use binaryen.wasm-opt for output Wasm file
|
||||
# binaryen just will trigger in `web` platform
|
||||
binaryen = { wasm_opt = true }
|
||||
[[web.proxy]]
|
||||
backend = "http://localhost:8000/api/"
|
||||
+44
-16
@@ -1,43 +1,71 @@
|
||||
<div align="center">
|
||||
<h1>📦✨ Dioxus CLI </h1>
|
||||
<div style="text-align: center">
|
||||
<h1>📦✨ Dioxus CLI</h1>
|
||||
<p><strong>Tooling to supercharge Dioxus projects</strong></p>
|
||||
</div>
|
||||
**dioxus-cli** (inspired by wasm-pack and webpack) is a tool for getting Dioxus projects up and running.
|
||||
It handles all build, bundling, development and publishing to simplify web development.
|
||||
|
||||
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.
|
||||
|
||||
## Installation
|
||||
|
||||
### Install stable version
|
||||
### Install the stable version (recommended)
|
||||
|
||||
```
|
||||
cargo install dioxus-cli
|
||||
cargo install dioxus-cli --locked
|
||||
```
|
||||
### Install from git repository
|
||||
|
||||
### Install the latest development build through git
|
||||
|
||||
To get the latest bug fixes and features, you can install the development version from git.
|
||||
However, this is not fully tested.
|
||||
That means you're probably going to have more bugs despite having the latest bug fixes.
|
||||
|
||||
```
|
||||
cargo install --git https://github.com/DioxusLabs/dioxus dioxus-cli
|
||||
```
|
||||
|
||||
This will download the CLI from the master branch,
|
||||
and install it in Cargo's global binary directory (`~/.cargo/bin/` by default).
|
||||
|
||||
### Install from local folder
|
||||
|
||||
```
|
||||
cargo install --path . --debug
|
||||
```
|
||||
|
||||
## Get started
|
||||
|
||||
## Get Started
|
||||
|
||||
Use `dx create project-name` to initialize a new Dioxus project. <br>
|
||||
|
||||
Use `dx create project-name` to initialize a new Dioxus project.
|
||||
It will be cloned from the [dioxus-template](https://github.com/DioxusLabs/dioxus-template) repository.
|
||||
|
||||
<br>
|
||||
|
||||
Alternatively, you can specify the template path:
|
||||
|
||||
```
|
||||
dx create hello --template gh:dioxuslabs/dioxus-template
|
||||
```
|
||||
|
||||
## Dioxus Config File
|
||||
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.
|
||||
|
||||
Dioxus CLI will use `Dioxus.toml` file to Identify some project info and switch some cli feature.
|
||||
## Dioxus config file
|
||||
|
||||
You can get more configure information from [Dioxus CLI Document](https://dioxuslabs.com/cli/configure.html).
|
||||
You can use the `Dioxus.toml` file for further configuration.
|
||||
Some fields are mandatory, but the CLI tool will tell you which ones are missing.
|
||||
You can create a `Dioxus.toml` with all fields already set using `dx config init project-name`,
|
||||
or you can use this bare-bones template (only mandatory fields) to get started:
|
||||
|
||||
```toml
|
||||
[application]
|
||||
name = "project-name"
|
||||
# Currently supported platforms: web, desktop
|
||||
default_platform = "web"
|
||||
|
||||
[web.app]
|
||||
title = "Hello"
|
||||
|
||||
[web.watcher]
|
||||
|
||||
[web.resource.dev]
|
||||
```
|
||||
|
||||
The full anatomy of `Dioxus.toml` is shown on the [Dioxus website](https://dioxuslabs.com/learn/0.4/CLI/configure).
|
||||
@@ -3,4 +3,4 @@ authors = ["YuKun Liu"]
|
||||
language = "en"
|
||||
multilingual = false
|
||||
src = "src"
|
||||
title = "Dioxus Cli"
|
||||
title = "Dioxus CLI"
|
||||
@@ -2,17 +2,12 @@
|
||||
|
||||
- [Introduction](./introduction.md)
|
||||
- [Installation](./installation.md)
|
||||
- [Create a Project](./creating.md)
|
||||
- [Configure Project](./configure.md)
|
||||
- [Commands](./cmd/README.md)
|
||||
- [Build](./cmd/build.md)
|
||||
- [Serve](./cmd/serve.md)
|
||||
- [Clean](./cmd/clean.md)
|
||||
- [Translate](./cmd/translate.md)
|
||||
- [Plugin Development](./plugin/README.md)
|
||||
- [API.Log](./plugin/interface/log.md)
|
||||
- [API.Command](./plugin/interface/command.md)
|
||||
- [API.OS](./plugin/interface/os.md)
|
||||
- [API.Directories](./plugin/interface/dirs.md)
|
||||
- [API.Network](./plugin/interface/network.md)
|
||||
- [API.Path](./plugin/interface/path.md)
|
||||
- [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)
|
||||
@@ -1,26 +0,0 @@
|
||||
# Commands
|
||||
|
||||
In this chapter we will introduce all `dioxus-cli` commands.
|
||||
|
||||
> You can also use `dx --help` to get cli help info.
|
||||
|
||||
```
|
||||
dx
|
||||
Build, bundle, & ship your Dioxus app
|
||||
|
||||
USAGE:
|
||||
dx [OPTIONS] <SUBCOMMAND>
|
||||
|
||||
OPTIONS:
|
||||
-h, --help Print help information
|
||||
-v Enable verbose logging
|
||||
|
||||
SUBCOMMANDS:
|
||||
build Build the Dioxus application and all of its assets
|
||||
clean Clean output artifacts
|
||||
config Dioxus config file controls
|
||||
create Init a new project for Dioxus
|
||||
help Print this message or the help of the given subcommand(s)
|
||||
serve Build, watch & serve the Rust WASM app and all of its assets
|
||||
translate Translate some html file into a Dioxus component
|
||||
```
|
||||
@@ -1,56 +0,0 @@
|
||||
# Build
|
||||
|
||||
The `dx build` command can help you `pack & build` a dioxus project.
|
||||
|
||||
```
|
||||
dioxus-build
|
||||
Build the Rust WASM app and all of its assets
|
||||
|
||||
USAGE:
|
||||
dx build [OPTIONS]
|
||||
|
||||
OPTIONS:
|
||||
--example <EXAMPLE> [default: ""]
|
||||
--platform <PLATFORM> [default: "default_platform"]
|
||||
--release [default: false]
|
||||
--bin [default: None]
|
||||
```
|
||||
|
||||
You can use this command to build a project:
|
||||
|
||||
```
|
||||
dx build --release
|
||||
```
|
||||
|
||||
## Target platform
|
||||
|
||||
Use the `platform` option to choose your target platform:
|
||||
|
||||
```
|
||||
# for desktop project
|
||||
dx build --platform desktop
|
||||
```
|
||||
|
||||
`platform` currently only supports `desktop` & `web`.
|
||||
|
||||
```
|
||||
# for web project
|
||||
dx build --platform web
|
||||
```
|
||||
|
||||
## Specify workspace bin
|
||||
|
||||
You can add the `--bin` option to select which crate you want Dioxus to build:
|
||||
|
||||
```
|
||||
dx build --bin app
|
||||
```
|
||||
|
||||
## Build Example
|
||||
|
||||
You can use the `example` option to select a example to build:
|
||||
|
||||
```
|
||||
# build the `test` example
|
||||
dx build --exmaple test
|
||||
```
|
||||
@@ -1,27 +0,0 @@
|
||||
# Clean
|
||||
|
||||
`dx clean` will clear the build artifacts (the out_dir and the cargo cache)
|
||||
|
||||
```
|
||||
dioxus-clean
|
||||
Clean build artifacts
|
||||
|
||||
USAGE:
|
||||
dx clean [OPTIONS]
|
||||
|
||||
OPTIONS:
|
||||
--bin [default: None]
|
||||
```
|
||||
|
||||
# Example
|
||||
|
||||
```
|
||||
dx clean
|
||||
```
|
||||
|
||||
# Specify workspace bin
|
||||
You can add the `--bin` option to select which crate you want Dioxus to clean artifacts from:
|
||||
|
||||
```
|
||||
dx clean --bin app
|
||||
```
|
||||
@@ -1,70 +0,0 @@
|
||||
# Serve
|
||||
|
||||
The `dx serve` can start a dev server with hot-reloading
|
||||
|
||||
```
|
||||
dioxus-serve
|
||||
Build, watch & serve the Rust WASM app and all of its assets
|
||||
|
||||
USAGE:
|
||||
dx serve [OPTIONS]
|
||||
|
||||
OPTIONS:
|
||||
--example <EXAMPLE> [default: ""]
|
||||
--platform <PLATFORM> [default: "default_platform"]
|
||||
--release [default: false]
|
||||
--hot-reload [default: false]
|
||||
--bin [default: None]
|
||||
```
|
||||
|
||||
You can use this command to build project and start a dev server:
|
||||
|
||||
```
|
||||
dx serve
|
||||
```
|
||||
|
||||
## Serve Example
|
||||
|
||||
You can use the `example` option to serve a example:
|
||||
|
||||
```
|
||||
# serve the `test` example
|
||||
dx serve --exmaple test
|
||||
```
|
||||
|
||||
## Specify workspace bin
|
||||
|
||||
You can add the `--bin` option to select which crate you want Dioxus to build and serve:
|
||||
|
||||
```
|
||||
dx serve --bin app
|
||||
```
|
||||
|
||||
## Open Browser
|
||||
|
||||
You can add the `--open` option to open system default browser when server startup:
|
||||
|
||||
```
|
||||
dx serve --open
|
||||
```
|
||||
|
||||
## RSX Hot Reloading
|
||||
|
||||
You can add the `--hot-reload` flag to enable [rsx hot reloading](https://dioxuslabs.com/docs/0.3/guide/en/getting_started/hot_reload.html). This will allow you to reload some rsx changes without a full recompile:
|
||||
|
||||
```
|
||||
dx serve --open
|
||||
```
|
||||
|
||||
## Cross Origin Policy
|
||||
|
||||
You can add the `cross-origin-policy` option to change cross-origin header to:
|
||||
|
||||
```
|
||||
Cross-Origin-Opener-Policy: same-origin
|
||||
Cross-Origin-Embedder-Policy: require-corp
|
||||
```
|
||||
|
||||
```
|
||||
dx serve --corss-origin-policy
|
||||
```
|
||||
@@ -1,68 +0,0 @@
|
||||
# Translate
|
||||
|
||||
`dx translate` can translate some `html` file into a Dioxus compoent
|
||||
|
||||
```
|
||||
dioxus-translate
|
||||
Translate some source file into a Dioxus component
|
||||
|
||||
USAGE:
|
||||
dx translate [OPTIONS] [OUTPUT]
|
||||
|
||||
ARGS:
|
||||
<OUTPUT> Output file, defaults to stdout if not present
|
||||
|
||||
OPTIONS:
|
||||
-c, --component Activate debug mode
|
||||
-f, --file <FILE> Input file
|
||||
```
|
||||
|
||||
## Translate HTML to stdout
|
||||
|
||||
You can use the `file` option to set path to the `html` file to translate:
|
||||
|
||||
```
|
||||
dx transtale --file ./index.html
|
||||
```
|
||||
|
||||
## Output rsx to a file
|
||||
|
||||
You can pass a file to the traslate command to set the path to write the output of the command to:
|
||||
|
||||
```
|
||||
dx translate --file ./index.html component.rsx
|
||||
```
|
||||
|
||||
## Output rsx to a file
|
||||
|
||||
Setting the `component` option will create a compoent from the HTML:
|
||||
|
||||
```
|
||||
dx translate --file ./index.html --component
|
||||
```
|
||||
|
||||
## Example
|
||||
|
||||
This HTML:
|
||||
```html
|
||||
<div>
|
||||
<h1> Hello World </h1>
|
||||
<a href="https://dioxuslabs.com/">Link</a>
|
||||
</div>
|
||||
```
|
||||
|
||||
Translates into this Dioxus component:
|
||||
|
||||
```rust
|
||||
fn component(cx: Scope) -> Element {
|
||||
cx.render(rsx! {
|
||||
div {
|
||||
h1 { "Hello World" },
|
||||
a {
|
||||
href: "https://dioxuslabs.com/",
|
||||
"Link"
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
@@ -1,166 +1,155 @@
|
||||
# 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.
|
||||
|
||||
This chapter will introduce you to how to configure the CLI with your `Dioxus.toml` file
|
||||
|
||||
Be aware that if the config file is present in the folder, some fields must be filled out, or the CLI tool will abort. The mandatory [table headers](https://toml.io/en/v1.0.0#table) and keys will have a '✍' sign beside it.
|
||||
"🔒" 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
|
||||
|
||||
The CLI uses a `Dioxus.toml` file in the root of your crate to define some configuration for your `dioxus` project.
|
||||
Each header has it's TOML form directly under it.
|
||||
|
||||
### Application ✍
|
||||
### Application 🔒
|
||||
|
||||
General application confiration:
|
||||
|
||||
```
|
||||
```toml
|
||||
[application]
|
||||
# configuration
|
||||
```
|
||||
1. ***name*** ✍ - project name & title
|
||||
2. ***default_platform*** ✍ - which platform target for this project.
|
||||
|
||||
Application-wide configuration. Applies to both web and desktop.
|
||||
|
||||
1. **name** 🔒 - Project name & title.
|
||||
```toml
|
||||
name = "my_project"
|
||||
```
|
||||
name = "my-project"
|
||||
```
|
||||
2. ***default_platform*** - The platform this project targets
|
||||
```ß
|
||||
# current supported platforms: web, desktop
|
||||
# default: web
|
||||
|
||||
2. **default_platform** 🔒 - The platform this project targets
|
||||
```toml
|
||||
# Currently supported platforms: web, desktop
|
||||
default_platform = "web"
|
||||
```
|
||||
if you change this to `desktop`, the `dx build` will default building a desktop app
|
||||
3. ***out_dir*** - The directory to place the build artifacts from `dx build` or `dx service` into. This is also where the `assets` directory will be copied to
|
||||
```
|
||||
|
||||
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.
|
||||
```
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
5. **sub_package** - The sub package in the workspace to build by default.
|
||||
```toml
|
||||
sub_package = "my-crate"
|
||||
```
|
||||
|
||||
### Web.App ✍
|
||||
### Web.App 🔒
|
||||
|
||||
Configeration specific to web applications:
|
||||
|
||||
```
|
||||
```toml
|
||||
[web.app]
|
||||
# configuration
|
||||
```
|
||||
|
||||
1. ***title*** - The title of the web page
|
||||
```
|
||||
Web-specific configuration.
|
||||
|
||||
1. **title** - The title of the web page.
|
||||
```toml
|
||||
# HTML title tag content
|
||||
title = "dioxus app | ⛺"
|
||||
```
|
||||
2. ***base_path*** - The base path to build the appliation for serving at. This can be useful when serving your application in a subdirectory under a domain. For example when building a site to be served on github pages.
|
||||
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 ✍
|
||||
|
||||
Configeration related to the development server:
|
||||
|
||||
```
|
||||
```toml
|
||||
[web.watcher]
|
||||
# configuration
|
||||
```
|
||||
|
||||
1. ***reload_html*** - If this is true, the cli will rebuild the index.html file every time the application is rebuilt
|
||||
```
|
||||
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 moniter for changes
|
||||
```
|
||||
|
||||
2. **watch_path** - The files & directories to monitor for changes
|
||||
```toml
|
||||
watch_path = ["src", "public"]
|
||||
```
|
||||
3. ***index_on_404*** - If enabled, Dioxus CLI will serve the root page when a route is not found. *This is needed when serving an application that uses the router*
|
||||
```
|
||||
|
||||
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 ✍
|
||||
### Web.Resource 🔒
|
||||
|
||||
Configeration related to static resources your application uses:
|
||||
```
|
||||
```toml
|
||||
[web.resource]
|
||||
# configuration
|
||||
```
|
||||
|
||||
1. ***style*** - The styles (`.css` files) to include in your application
|
||||
```
|
||||
Static resource configuration.
|
||||
|
||||
1. **style** - CSS files to include in your application.
|
||||
```toml
|
||||
style = [
|
||||
# include from public_dir.
|
||||
# Include from public_dir.
|
||||
"./assets/style.css",
|
||||
# or some asset from online cdn.
|
||||
# Or some asset from online cdn.
|
||||
"https://cdn.jsdelivr.net/npm/bootstrap/dist/css/bootstrap.css"
|
||||
]
|
||||
```
|
||||
2. ***script*** - The additional scripts (`.js` files) to include in your application
|
||||
```
|
||||
style = [
|
||||
# include from public_dir.
|
||||
"./assets/index.js",
|
||||
# or some asset from online cdn.
|
||||
|
||||
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 ✍
|
||||
### Web.Resource.Dev 🔒
|
||||
|
||||
Configeration related to static resources your application uses in development:
|
||||
```
|
||||
```toml
|
||||
[web.resource.dev]
|
||||
# configuration
|
||||
```
|
||||
|
||||
1. ***style*** - The styles (`.css` files) to include in your application
|
||||
```
|
||||
style = [
|
||||
# include from public_dir.
|
||||
"./assets/style.css",
|
||||
# or some asset from online cdn.
|
||||
"https://cdn.jsdelivr.net/npm/bootstrap/dist/css/bootstrap.css"
|
||||
]
|
||||
```
|
||||
2. ***script*** - The additional scripts (`.js` files) to include in your application
|
||||
```
|
||||
style = [
|
||||
# include from public_dir.
|
||||
"./assets/index.js",
|
||||
# or some asset from online cdn.
|
||||
"https://cdn.jsdelivr.net/npm/bootstrap/dist/js/bootstrap.js"
|
||||
]
|
||||
```
|
||||
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
|
||||
|
||||
Configeration related to any proxies your application requires durring development. Proxies will forward requests to a new service
|
||||
|
||||
```
|
||||
[web.proxy]
|
||||
# configuration
|
||||
```toml
|
||||
[[web.proxy]]
|
||||
```
|
||||
|
||||
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
|
||||
```
|
||||
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 not currently supported).
|
||||
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 (Project) Name
|
||||
name = "{{project-name}}"
|
||||
# App name
|
||||
name = "project_name"
|
||||
|
||||
# The Dioxus platform to default to
|
||||
default_platform = "web"
|
||||
@@ -168,23 +157,23 @@ default_platform = "web"
|
||||
# `build` & `serve` output path
|
||||
out_dir = "dist"
|
||||
|
||||
# the static resource path
|
||||
# The static resource path
|
||||
asset_dir = "public"
|
||||
|
||||
[web.app]
|
||||
|
||||
# HTML title tag content
|
||||
title = "dioxus | ⛺"
|
||||
title = "project_name"
|
||||
|
||||
[web.watcher]
|
||||
|
||||
# when watcher is triggered, regenerate the `index.html`
|
||||
# When watcher is triggered, regenerate the `index.html`
|
||||
reload_html = true
|
||||
|
||||
# which files or dirs will be monitored
|
||||
# Which files or dirs will be monitored
|
||||
watch_path = ["src", "public"]
|
||||
|
||||
# include `assets` in web platform
|
||||
# Include style or script assets
|
||||
[web.resource]
|
||||
|
||||
# CSS style file
|
||||
@@ -195,12 +184,12 @@ script = []
|
||||
|
||||
[web.resource.dev]
|
||||
|
||||
# serve: [dev-server] only
|
||||
# Same as [web.resource], but for development servers
|
||||
|
||||
# CSS style file
|
||||
style = []
|
||||
|
||||
# Javascript code file
|
||||
# JavaScript files
|
||||
script = []
|
||||
|
||||
[[web.proxy]]
|
||||
|
||||
@@ -1,39 +1,37 @@
|
||||
# Create a Project
|
||||
|
||||
Once you have the Dioxus CLI tool installed, you can use it to create dioxus 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 ready to be used with Dioxus and the Dioxus CLI:
|
||||
|
||||
First, run the `dx create` command to create a new project:
|
||||
```
|
||||
dx create hello-dioxus
|
||||
```
|
||||
|
||||
> It will clone a default template from github template: [DioxusLabs/dioxus-template](https://github.com/DioxusLabs/dioxus-template)
|
||||
> This default template is use for `web` platform application.
|
||||
> 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, move the current directory into your new project:
|
||||
Next, navigate into your new project:
|
||||
|
||||
```
|
||||
cd hello-dioxus
|
||||
```
|
||||
|
||||
> Make sure `wasm32 target` is installed before running the Web project.
|
||||
> You can install the wasm target for rust using rustup:
|
||||
> 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, create serve your project with the Dioxus CLI:
|
||||
|
||||
Finally, serve your project:
|
||||
```
|
||||
dx serve
|
||||
```
|
||||
|
||||
By default, the CLI serve your site at: [`http://127.0.0.1:8080/`](http://127.0.0.1:8080/)
|
||||
By default, the CLI serves your website at [`http://127.0.0.1:8080/`](http://127.0.0.1:8080/).
|
||||
@@ -1,22 +1,23 @@
|
||||
# Installation
|
||||
|
||||
Choose any one of the methods below to install the Dioxus CLI:
|
||||
## Install the latest development build through git
|
||||
|
||||
## Install from latest git version
|
||||
|
||||
To get the most up to date bug fixes and features of the Dioxus CLI, you can install the development version from git.
|
||||
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 automatically download `Dioxus-CLI` source from github master branch,
|
||||
This will download `Dioxus-CLI` source from GitHub master branch,
|
||||
and install it in Cargo's global binary directory (`~/.cargo/bin/` by default).
|
||||
|
||||
## Install from `crates.io` version
|
||||
## Install stable through `crates.io`
|
||||
|
||||
The published version of the Dioxus CLI is updated less often, but should be more stable than the git version of the Dioxus CLI.
|
||||
The published version of the Dioxus CLI is updated less often, but is more stable than the git version.
|
||||
|
||||
```
|
||||
cargo install dioxus-cli
|
||||
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.
|
||||
@@ -1,21 +1,18 @@
|
||||
# Introduction
|
||||
|
||||
📦✨ **Dioxus-Cli** is a tool to help get dioxus projects off the ground.
|
||||
|
||||

|
||||
|
||||
It includes `dev server`, `hot reload` and some `quick command` to help you use dioxus.
|
||||
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 dioxus project from `git` repo
|
||||
- [x] build & pack dioxus project
|
||||
- [ ] autoformat dioxus `rsx` code
|
||||
|
||||
## Contributors
|
||||
|
||||
Contributors to this guide:
|
||||
|
||||
- [mrxiaozhuox](https://github.com/mrxiaozhuox)
|
||||
- [x] Hot Reload for `web` platform
|
||||
- [x] Create a Dioxus project from `git` repo
|
||||
- [x] Build & pack Dioxus project
|
||||
- [ ] Automatically format Dioxus `rsx` code
|
||||
-->
|
||||
@@ -1,24 +1,85 @@
|
||||
# CLI Plugin Development
|
||||
# CLI Plugin development
|
||||
|
||||
> For Cli 0.2.0 we will add `plugin-develop` support.
|
||||
**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.**
|
||||
|
||||
Before the 0.2.0 we use `dx tool` to use & install some plugin, but we think that is not good for extend cli program, some people want tailwind support, some people want sass support, we can't add all this thing in to the cli source code and we don't have time to maintain a lot of tools that user request, so maybe user make plugin by themself is a good choice.
|
||||
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.
|
||||
|
||||
### Why Lua ?
|
||||
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.
|
||||
|
||||
We choose `Lua: 5.4` to be the plugin develop language, because cli plugin is not complex, just like a workflow, and user & developer can write some easy code for their plugin. We have **vendored** lua in cli program, and user don't need install lua runtime in their computer, and the lua parser & runtime doesn't take up much disk memory.
|
||||
### Why Lua?
|
||||
|
||||
### Event Management
|
||||
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.
|
||||
|
||||
The plugin library have pre-define some important event you can control:
|
||||
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!
|
||||
|
||||
- `build.on_start`
|
||||
- `build.on_finished`
|
||||
- `serve.on_start`
|
||||
- `serve.on_rebuild`
|
||||
- `serve.on_shutdown`
|
||||
|
||||
### Plugin Template
|
||||
## 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"
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
# Command Functions
|
||||
|
||||
> you can use command functions to execute some code & script
|
||||
You can use command functions to execute code and scripts.
|
||||
|
||||
Type Define:
|
||||
Type definition:
|
||||
```
|
||||
Stdio: "Inherit" | "Piped" | "Null"
|
||||
```
|
||||
|
||||
### `exec(commands: [string], stdout: Stdio, stderr: Stdio)`
|
||||
|
||||
you can use this function to run some command on the current system.
|
||||
You can use this function to run some commands on the current system.
|
||||
|
||||
```lua
|
||||
local cmd = plugin.command
|
||||
@@ -18,4 +18,4 @@ manager.test = function ()
|
||||
cmd.exec({"git", "clone", "https://github.com/DioxusLabs/cli-plugin-library"})
|
||||
end
|
||||
```
|
||||
> Warning: This function don't have exception catch.
|
||||
> Warning: This function doesn't catch exceptions.
|
||||
@@ -1,33 +1,28 @@
|
||||
# Dirs Functions
|
||||
|
||||
> you can use Dirs functions to get some directory path
|
||||
Dirs functions are for getting various directory paths. Not to be confused with `plugin.path`.
|
||||
|
||||
### `plugin_dir() -> string`
|
||||
|
||||
### plugin_dir() -> string
|
||||
|
||||
You can get current plugin **root** directory path
|
||||
Get the plugin's root directory path.
|
||||
|
||||
```lua
|
||||
local path = plugin.dirs.plugin_dir()
|
||||
-- example: ~/Development/DioxusCli/plugin/test-plugin/
|
||||
```
|
||||
|
||||
### bin_dir() -> string
|
||||
### `bin_dir() -> string`
|
||||
|
||||
You can get plugin **bin** direcotry path
|
||||
|
||||
Sometime you need install some binary file like `tailwind-cli` & `sass-cli` to help your plugin work, then you should put binary file in this directory.
|
||||
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
|
||||
### `temp_dir() -> string`
|
||||
|
||||
You can get plugin **temp** direcotry path
|
||||
|
||||
Just put some temporary file in this directory.
|
||||
Get the plugin's temporary directory path. Put any temporary files here.
|
||||
|
||||
```lua
|
||||
local path = plugin.dirs.bin_dir()
|
||||
|
||||
@@ -1,46 +1,46 @@
|
||||
# Log Functions
|
||||
|
||||
> You can use log function to print some useful log info
|
||||
You can use log functions to print various logging information.
|
||||
|
||||
### Trace(info: string)
|
||||
### `trace(info: string)`
|
||||
|
||||
Print trace log info
|
||||
Print trace log info.
|
||||
|
||||
```lua
|
||||
local log = plugin.log
|
||||
log.trace("trace information")
|
||||
```
|
||||
|
||||
### Debug(info: string)
|
||||
### `debug(info: string)`
|
||||
|
||||
Print debug log info
|
||||
Print debug log info.
|
||||
|
||||
```lua
|
||||
local log = plugin.log
|
||||
log.debug("debug information")
|
||||
```
|
||||
|
||||
### Info(info: string)
|
||||
### `info(info: string)`
|
||||
|
||||
Print info log info
|
||||
Print info log info.
|
||||
|
||||
```lua
|
||||
local log = plugin.log
|
||||
log.info("info information")
|
||||
```
|
||||
|
||||
### Warn(info: string)
|
||||
### `warn(info: string)`
|
||||
|
||||
Print warning log info
|
||||
Print warning log info.
|
||||
|
||||
```lua
|
||||
local log = plugin.log
|
||||
log.warn("warn information")
|
||||
```
|
||||
|
||||
### Error(info: string)
|
||||
### `error(info: string)`
|
||||
|
||||
Print error log info
|
||||
Print error log info.
|
||||
|
||||
```lua
|
||||
local log = plugin.log
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
# Network Functions
|
||||
|
||||
> you can use Network functions to download & read some data from internet
|
||||
You can use Network functions to download & read some data from the internet.
|
||||
|
||||
### download_file(url: string, path: string) -> boolean
|
||||
### `download_file(url: string, path: string) -> boolean`
|
||||
|
||||
This function can help you download some file from url, and it will return a *boolean* value to check the download status. (true: success | false: fail)
|
||||
Downloads a file from the specified URL,
|
||||
and returns a `boolean` that represents the download status (true: success, false: failure).
|
||||
|
||||
You need pass a target url and a local path (where you want to save this file)
|
||||
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
|
||||
@@ -19,9 +20,11 @@ if status != true then
|
||||
end
|
||||
```
|
||||
|
||||
### clone_repo(url: string, path: string) -> boolean
|
||||
### `clone_repo(url: string, path: string) -> boolean`
|
||||
|
||||
This function can help you use `git clone` command (this system must have been installed git)
|
||||
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(
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# OS Functions
|
||||
|
||||
> you can use OS functions to get some system information
|
||||
OS functions are for getting system information.
|
||||
|
||||
### current_platform() -> string ("windows" | "macos" | "linux")
|
||||
### `current_platform() -> string ("windows" | "macos" | "linux")`
|
||||
|
||||
This function can help you get system & platform type:
|
||||
Get the current OS platform.
|
||||
|
||||
```lua
|
||||
local platform = plugin.os.current_platform()
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
# Path Functions
|
||||
|
||||
> you can use path functions to operate valid path string
|
||||
You can use path functions to perform operations on valid path strings.
|
||||
|
||||
### join(path: string, extra: string) -> string
|
||||
### `join(path: string, extra: string) -> string`
|
||||
|
||||
This function can help you extend a path, you can extend any path, dirname or filename.
|
||||
<!-- 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"
|
||||
@@ -12,9 +15,9 @@ local new_path = plugin.path.join(current_path, "world")
|
||||
-- new_path = "~/hello/dioxus/world"
|
||||
```
|
||||
|
||||
### parent(path: string) -> string
|
||||
### `parent(path: string) -> string`
|
||||
|
||||
This function will return `path` parent-path string, back to the parent.
|
||||
Return the parent path of the specified path. The parent path is always a directory.
|
||||
|
||||
```lua
|
||||
local current_path = "~/hello/dioxus"
|
||||
@@ -22,14 +25,14 @@ local new_path = plugin.path.parent(current_path)
|
||||
-- new_path = "~/hello/"
|
||||
```
|
||||
|
||||
### exists(path: string) -> boolean
|
||||
### `exists(path: string) -> boolean`
|
||||
|
||||
This function can check some path (dir & file) is exists.
|
||||
Check if the specified path exists, as either a file or a directory.
|
||||
|
||||
### is_file(path: string) -> boolean
|
||||
### `is_file(path: string) -> boolean`
|
||||
|
||||
This function can check some path is a exist file.
|
||||
Check if the specified path is a file.
|
||||
|
||||
### is_dir(path: string) -> boolean
|
||||
### `is_dir(path: string) -> boolean`
|
||||
|
||||
This function can check some path is a exist dir.
|
||||
Check if the specified path is a directory.
|
||||
@@ -45,3 +45,30 @@ script = []
|
||||
available = true
|
||||
|
||||
required = []
|
||||
|
||||
[bundler]
|
||||
# Bundle identifier
|
||||
identifier = "io.github.{{project-name}}"
|
||||
|
||||
# Bundle publisher
|
||||
publisher = "{{project-name}}"
|
||||
|
||||
# Bundle icon
|
||||
icon = ["icons/icon.png"]
|
||||
|
||||
# Bundle resources
|
||||
resources = ["public/*"]
|
||||
|
||||
# Bundle copyright
|
||||
copyright = ""
|
||||
|
||||
# Bundle category
|
||||
category = "Utility"
|
||||
|
||||
# Bundle short description
|
||||
short_description = "An amazing dioxus application."
|
||||
|
||||
# Bundle long description
|
||||
long_description = """
|
||||
An amazing dioxus application.
|
||||
"""
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 130 KiB |
+97
-100
@@ -12,7 +12,6 @@ use std::{
|
||||
io::Read,
|
||||
panic,
|
||||
path::PathBuf,
|
||||
process::Command,
|
||||
time::Duration,
|
||||
};
|
||||
use wasm_bindgen_cli_support::Bindgen;
|
||||
@@ -23,6 +22,7 @@ pub struct BuildResult {
|
||||
pub elapsed_time: u128,
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub fn build(config: &CrateConfig, quiet: bool) -> Result<BuildResult> {
|
||||
// [1] Build the project with cargo, generating a wasm32-unknown-unknown target (is there a more specific, better target to leverage?)
|
||||
// [2] Generate the appropriate build folders
|
||||
@@ -54,7 +54,8 @@ pub fn build(config: &CrateConfig, quiet: bool) -> Result<BuildResult> {
|
||||
.arg("build")
|
||||
.arg("--target")
|
||||
.arg("wasm32-unknown-unknown")
|
||||
.arg("--message-format=json");
|
||||
.arg("--message-format=json")
|
||||
.arg("--quiet");
|
||||
|
||||
let cmd = if config.release {
|
||||
cmd.arg("--release")
|
||||
@@ -67,8 +68,6 @@ pub fn build(config: &CrateConfig, quiet: bool) -> Result<BuildResult> {
|
||||
cmd
|
||||
};
|
||||
|
||||
let cmd = if quiet { cmd.arg("--quiet") } else { cmd };
|
||||
|
||||
let cmd = if config.custom_profile.is_some() {
|
||||
let custom_profile = config.custom_profile.as_ref().unwrap();
|
||||
cmd.arg("--profile").arg(custom_profile)
|
||||
@@ -94,18 +93,21 @@ pub fn build(config: &CrateConfig, quiet: bool) -> Result<BuildResult> {
|
||||
// [2] Establish the output directory structure
|
||||
let bindgen_outdir = out_dir.join("assets").join("dioxus");
|
||||
|
||||
let release_type = match config.release {
|
||||
true => "release",
|
||||
false => "debug",
|
||||
let build_profile = if config.custom_profile.is_some() {
|
||||
config.custom_profile.as_ref().unwrap()
|
||||
} else if config.release {
|
||||
"release"
|
||||
} else {
|
||||
"debug"
|
||||
};
|
||||
|
||||
let input_path = match executable {
|
||||
ExecutableType::Binary(name) | ExecutableType::Lib(name) => target_dir
|
||||
.join(format!("wasm32-unknown-unknown/{}", release_type))
|
||||
.join(format!("wasm32-unknown-unknown/{}", build_profile))
|
||||
.join(format!("{}.wasm", name)),
|
||||
|
||||
ExecutableType::Example(name) => target_dir
|
||||
.join(format!("wasm32-unknown-unknown/{}/examples", release_type))
|
||||
.join(format!("wasm32-unknown-unknown/{}/examples", build_profile))
|
||||
.join(format!("{}.wasm", name)),
|
||||
};
|
||||
|
||||
@@ -237,135 +239,131 @@ pub fn build(config: &CrateConfig, quiet: bool) -> Result<BuildResult> {
|
||||
}
|
||||
}
|
||||
|
||||
let t_end = std::time::Instant::now();
|
||||
Ok(BuildResult {
|
||||
warnings: warning_messages,
|
||||
elapsed_time: (t_end - t_start).as_millis(),
|
||||
elapsed_time: t_start.elapsed().as_millis(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn build_desktop(config: &CrateConfig, _is_serve: bool) -> Result<()> {
|
||||
pub fn build_desktop(config: &CrateConfig, _is_serve: bool) -> Result<BuildResult> {
|
||||
log::info!("🚅 Running build [Desktop] command...");
|
||||
|
||||
let t_start = std::time::Instant::now();
|
||||
let ignore_files = build_assets(config)?;
|
||||
|
||||
let mut cmd = Command::new("cargo");
|
||||
cmd.current_dir(&config.crate_dir)
|
||||
let mut cmd = subprocess::Exec::cmd("cargo")
|
||||
.cwd(&config.crate_dir)
|
||||
.arg("build")
|
||||
.stdout(std::process::Stdio::inherit())
|
||||
.stderr(std::process::Stdio::inherit());
|
||||
.arg("--message-format=json");
|
||||
|
||||
if config.release {
|
||||
cmd.arg("--release");
|
||||
cmd = cmd.arg("--release");
|
||||
}
|
||||
if config.verbose {
|
||||
cmd.arg("--verbose");
|
||||
cmd = cmd.arg("--verbose");
|
||||
}
|
||||
|
||||
if config.custom_profile.is_some() {
|
||||
let custom_profile = config.custom_profile.as_ref().unwrap();
|
||||
cmd.arg("--profile");
|
||||
cmd.arg(custom_profile);
|
||||
cmd = cmd.arg("--profile").arg(custom_profile);
|
||||
}
|
||||
|
||||
if config.features.is_some() {
|
||||
let features_str = config.features.as_ref().unwrap().join(" ");
|
||||
cmd.arg("--features");
|
||||
cmd.arg(features_str);
|
||||
cmd = cmd.arg("--features").arg(features_str);
|
||||
}
|
||||
|
||||
match &config.executable {
|
||||
let cmd = match &config.executable {
|
||||
crate::ExecutableType::Binary(name) => cmd.arg("--bin").arg(name),
|
||||
crate::ExecutableType::Lib(name) => cmd.arg("--lib").arg(name),
|
||||
crate::ExecutableType::Example(name) => cmd.arg("--example").arg(name),
|
||||
};
|
||||
|
||||
let output = cmd.output()?;
|
||||
let warning_messages = prettier_build(cmd)?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(Error::BuildFailed("Program build failed.".into()));
|
||||
}
|
||||
let release_type = match config.release {
|
||||
true => "release",
|
||||
false => "debug",
|
||||
};
|
||||
|
||||
if output.status.success() {
|
||||
let release_type = match config.release {
|
||||
true => "release",
|
||||
false => "debug",
|
||||
};
|
||||
|
||||
let file_name: String;
|
||||
let mut res_path = match &config.executable {
|
||||
crate::ExecutableType::Binary(name) | crate::ExecutableType::Lib(name) => {
|
||||
file_name = name.clone();
|
||||
config.target_dir.join(release_type).join(name)
|
||||
}
|
||||
crate::ExecutableType::Example(name) => {
|
||||
file_name = name.clone();
|
||||
config
|
||||
.target_dir
|
||||
.join(release_type)
|
||||
.join("examples")
|
||||
.join(name)
|
||||
}
|
||||
};
|
||||
|
||||
let target_file = if cfg!(windows) {
|
||||
res_path.set_extension("exe");
|
||||
format!("{}.exe", &file_name)
|
||||
} else {
|
||||
file_name
|
||||
};
|
||||
|
||||
if !config.out_dir.is_dir() {
|
||||
create_dir_all(&config.out_dir)?;
|
||||
let file_name: String;
|
||||
let mut res_path = match &config.executable {
|
||||
crate::ExecutableType::Binary(name) | crate::ExecutableType::Lib(name) => {
|
||||
file_name = name.clone();
|
||||
config.target_dir.join(release_type).join(name)
|
||||
}
|
||||
copy(res_path, &config.out_dir.join(target_file))?;
|
||||
crate::ExecutableType::Example(name) => {
|
||||
file_name = name.clone();
|
||||
config
|
||||
.target_dir
|
||||
.join(release_type)
|
||||
.join("examples")
|
||||
.join(name)
|
||||
}
|
||||
};
|
||||
|
||||
// this code will copy all public file to the output dir
|
||||
if config.asset_dir.is_dir() {
|
||||
let copy_options = fs_extra::dir::CopyOptions {
|
||||
overwrite: true,
|
||||
skip_exist: false,
|
||||
buffer_size: 64000,
|
||||
copy_inside: false,
|
||||
content_only: false,
|
||||
depth: 0,
|
||||
};
|
||||
let target_file = if cfg!(windows) {
|
||||
res_path.set_extension("exe");
|
||||
format!("{}.exe", &file_name)
|
||||
} else {
|
||||
file_name
|
||||
};
|
||||
|
||||
for entry in std::fs::read_dir(&config.asset_dir)? {
|
||||
let path = entry?.path();
|
||||
if path.is_file() {
|
||||
std::fs::copy(&path, &config.out_dir.join(path.file_name().unwrap()))?;
|
||||
} else {
|
||||
match fs_extra::dir::copy(&path, &config.out_dir, ©_options) {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
log::warn!("Error copying dir: {}", e);
|
||||
}
|
||||
if !config.out_dir.is_dir() {
|
||||
create_dir_all(&config.out_dir)?;
|
||||
}
|
||||
copy(res_path, &config.out_dir.join(target_file))?;
|
||||
|
||||
// this code will copy all public file to the output dir
|
||||
if config.asset_dir.is_dir() {
|
||||
let copy_options = fs_extra::dir::CopyOptions {
|
||||
overwrite: true,
|
||||
skip_exist: false,
|
||||
buffer_size: 64000,
|
||||
copy_inside: false,
|
||||
content_only: false,
|
||||
depth: 0,
|
||||
};
|
||||
|
||||
for entry in std::fs::read_dir(&config.asset_dir)? {
|
||||
let path = entry?.path();
|
||||
if path.is_file() {
|
||||
std::fs::copy(&path, &config.out_dir.join(path.file_name().unwrap()))?;
|
||||
} else {
|
||||
match fs_extra::dir::copy(&path, &config.out_dir, ©_options) {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
log::warn!("Error copying dir: {}", e);
|
||||
}
|
||||
for ignore in &ignore_files {
|
||||
let ignore = ignore.strip_prefix(&config.asset_dir).unwrap();
|
||||
let ignore = config.out_dir.join(ignore);
|
||||
if ignore.is_file() {
|
||||
std::fs::remove_file(ignore)?;
|
||||
}
|
||||
}
|
||||
for ignore in &ignore_files {
|
||||
let ignore = ignore.strip_prefix(&config.asset_dir).unwrap();
|
||||
let ignore = config.out_dir.join(ignore);
|
||||
if ignore.is_file() {
|
||||
std::fs::remove_file(ignore)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log::info!(
|
||||
"🚩 Build completed: [./{}]",
|
||||
config
|
||||
.dioxus_config
|
||||
.application
|
||||
.out_dir
|
||||
.clone()
|
||||
.unwrap_or_else(|| PathBuf::from("dist"))
|
||||
.display()
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
log::info!(
|
||||
"🚩 Build completed: [./{}]",
|
||||
config
|
||||
.dioxus_config
|
||||
.application
|
||||
.out_dir
|
||||
.clone()
|
||||
.unwrap_or_else(|| PathBuf::from("dist"))
|
||||
.display()
|
||||
);
|
||||
|
||||
println!("build desktop done");
|
||||
|
||||
Ok(BuildResult {
|
||||
warnings: warning_messages,
|
||||
elapsed_time: t_start.elapsed().as_millis(),
|
||||
})
|
||||
}
|
||||
|
||||
fn prettier_build(cmd: subprocess::Exec) -> anyhow::Result<Vec<Diagnostic>> {
|
||||
@@ -388,10 +386,9 @@ fn prettier_build(cmd: subprocess::Exec) -> anyhow::Result<Vec<Diagnostic>> {
|
||||
}
|
||||
}
|
||||
|
||||
StopSpinOnDrop(pb.clone());
|
||||
|
||||
let stdout = cmd.detached().stream_stdout()?;
|
||||
let reader = std::io::BufReader::new(stdout);
|
||||
|
||||
for message in cargo_metadata::Message::parse_stream(reader) {
|
||||
match message.unwrap() {
|
||||
Message::CompilerMessage(msg) => {
|
||||
@@ -411,7 +408,7 @@ fn prettier_build(cmd: subprocess::Exec) -> anyhow::Result<Vec<Diagnostic>> {
|
||||
}
|
||||
}
|
||||
Message::CompilerArtifact(artifact) => {
|
||||
pb.set_message(format!("Compiling {} ", artifact.package_id));
|
||||
pb.set_message(format!("⚙️ Compiling {} ", artifact.package_id));
|
||||
pb.tick();
|
||||
}
|
||||
Message::BuildScriptExecuted(script) => {
|
||||
|
||||
@@ -6,7 +6,7 @@ use super::*;
|
||||
// For reference, the rustfmt main.rs file
|
||||
// https://github.com/rust-lang/rustfmt/blob/master/src/bin/main.rs
|
||||
|
||||
/// Build the Rust WASM app and all of its assets.
|
||||
/// Format some rsx
|
||||
#[derive(Clone, Debug, Parser)]
|
||||
pub struct Autoformat {
|
||||
/// Run in 'check' mode. Exits with 0 if input is formatted correctly. Exits
|
||||
@@ -109,11 +109,17 @@ async fn autoformat_project(check: bool) -> Result<()> {
|
||||
})
|
||||
.await;
|
||||
|
||||
if res.is_err() {
|
||||
eprintln!("error formatting file: {}", _path.display());
|
||||
match res {
|
||||
Err(err) => {
|
||||
eprintln!("error formatting file: {}\n{err}", _path.display());
|
||||
None
|
||||
}
|
||||
Ok(Err(err)) => {
|
||||
eprintln!("error formatting file: {}\n{err}", _path.display());
|
||||
None
|
||||
}
|
||||
Ok(Ok(res)) => Some(res),
|
||||
}
|
||||
|
||||
res
|
||||
})
|
||||
.collect::<FuturesUnordered<_>>()
|
||||
.collect::<Vec<_>>()
|
||||
@@ -122,7 +128,7 @@ async fn autoformat_project(check: bool) -> Result<()> {
|
||||
let files_formatted: usize = counts
|
||||
.into_iter()
|
||||
.map(|f| match f {
|
||||
Ok(Ok(res)) => res,
|
||||
Some(res) => res,
|
||||
_ => 0,
|
||||
})
|
||||
.sum();
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::cfg::Platform;
|
||||
#[cfg(feature = "plugin")]
|
||||
use crate::plugin::PluginManager;
|
||||
|
||||
@@ -31,27 +32,21 @@ impl Build {
|
||||
crate_config.set_features(self.build.features.unwrap());
|
||||
}
|
||||
|
||||
let platform = self.build.platform.unwrap_or_else(|| {
|
||||
crate_config
|
||||
.dioxus_config
|
||||
.application
|
||||
.default_platform
|
||||
.clone()
|
||||
});
|
||||
let platform = self
|
||||
.build
|
||||
.platform
|
||||
.unwrap_or(crate_config.dioxus_config.application.default_platform);
|
||||
|
||||
#[cfg(feature = "plugin")]
|
||||
let _ = PluginManager::on_build_start(&crate_config, &platform);
|
||||
|
||||
match platform.as_str() {
|
||||
"web" => {
|
||||
crate::builder::build(&crate_config, false)?;
|
||||
match platform {
|
||||
Platform::Web => {
|
||||
crate::builder::build(&crate_config, true)?;
|
||||
}
|
||||
"desktop" => {
|
||||
Platform::Desktop => {
|
||||
crate::builder::build_desktop(&crate_config, false)?;
|
||||
}
|
||||
_ => {
|
||||
return custom_error!("Unsupported platform target.");
|
||||
}
|
||||
}
|
||||
|
||||
let temp = gen_page(&crate_config.dioxus_config, false);
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
use core::panic;
|
||||
use std::{fs::create_dir_all, str::FromStr};
|
||||
|
||||
use tauri_bundler::{BundleSettings, PackageSettings, SettingsBuilder};
|
||||
|
||||
use super::*;
|
||||
use crate::{build_desktop, cfg::ConfigOptsBundle};
|
||||
|
||||
/// Bundle the Rust desktop app and all of its assets
|
||||
#[derive(Clone, Debug, Parser)]
|
||||
#[clap(name = "bundle")]
|
||||
pub struct Bundle {
|
||||
#[clap(long)]
|
||||
pub package: Option<Vec<String>>,
|
||||
#[clap(flatten)]
|
||||
pub build: ConfigOptsBundle,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum PackageType {
|
||||
MacOsBundle,
|
||||
IosBundle,
|
||||
WindowsMsi,
|
||||
Deb,
|
||||
Rpm,
|
||||
AppImage,
|
||||
Dmg,
|
||||
Updater,
|
||||
}
|
||||
|
||||
impl FromStr for PackageType {
|
||||
type Err = String;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"macos" => Ok(PackageType::MacOsBundle),
|
||||
"ios" => Ok(PackageType::IosBundle),
|
||||
"msi" => Ok(PackageType::WindowsMsi),
|
||||
"deb" => Ok(PackageType::Deb),
|
||||
"rpm" => Ok(PackageType::Rpm),
|
||||
"appimage" => Ok(PackageType::AppImage),
|
||||
"dmg" => Ok(PackageType::Dmg),
|
||||
_ => Err(format!("{} is not a valid package type", s)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PackageType> for tauri_bundler::PackageType {
|
||||
fn from(val: PackageType) -> Self {
|
||||
match val {
|
||||
PackageType::MacOsBundle => tauri_bundler::PackageType::MacOsBundle,
|
||||
PackageType::IosBundle => tauri_bundler::PackageType::IosBundle,
|
||||
PackageType::WindowsMsi => tauri_bundler::PackageType::WindowsMsi,
|
||||
PackageType::Deb => tauri_bundler::PackageType::Deb,
|
||||
PackageType::Rpm => tauri_bundler::PackageType::Rpm,
|
||||
PackageType::AppImage => tauri_bundler::PackageType::AppImage,
|
||||
PackageType::Dmg => tauri_bundler::PackageType::Dmg,
|
||||
PackageType::Updater => tauri_bundler::PackageType::Updater,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Bundle {
|
||||
pub fn bundle(self, bin: Option<PathBuf>) -> Result<()> {
|
||||
let mut crate_config = crate::CrateConfig::new(bin)?;
|
||||
|
||||
// change the release state.
|
||||
crate_config.with_release(self.build.release);
|
||||
crate_config.with_verbose(self.build.verbose);
|
||||
|
||||
if self.build.example.is_some() {
|
||||
crate_config.as_example(self.build.example.unwrap());
|
||||
}
|
||||
|
||||
if self.build.profile.is_some() {
|
||||
crate_config.set_profile(self.build.profile.unwrap());
|
||||
}
|
||||
|
||||
// build the desktop app
|
||||
build_desktop(&crate_config, false)?;
|
||||
|
||||
// copy the binary to the out dir
|
||||
let package = crate_config.manifest.package.unwrap();
|
||||
|
||||
let mut name: PathBuf = match &crate_config.executable {
|
||||
crate::ExecutableType::Binary(name)
|
||||
| crate::ExecutableType::Lib(name)
|
||||
| crate::ExecutableType::Example(name) => name,
|
||||
}
|
||||
.into();
|
||||
if cfg!(windows) {
|
||||
name.set_extension("exe");
|
||||
}
|
||||
|
||||
// bundle the app
|
||||
let binaries = vec![
|
||||
tauri_bundler::BundleBinary::new(name.display().to_string(), true)
|
||||
.set_src_path(Some(crate_config.crate_dir.display().to_string())),
|
||||
];
|
||||
|
||||
let mut bundle_settings: BundleSettings = crate_config.dioxus_config.bundle.clone().into();
|
||||
if cfg!(windows) {
|
||||
let windows_icon_override = crate_config
|
||||
.dioxus_config
|
||||
.bundle
|
||||
.windows
|
||||
.as_ref()
|
||||
.map(|w| &w.icon_path);
|
||||
if windows_icon_override.is_none() {
|
||||
let icon_path = bundle_settings
|
||||
.icon
|
||||
.as_ref()
|
||||
.and_then(|icons| icons.first());
|
||||
let icon_path = if let Some(icon_path) = icon_path {
|
||||
icon_path.into()
|
||||
} else {
|
||||
let path = PathBuf::from("./icons/icon.ico");
|
||||
// create the icon if it doesn't exist
|
||||
if !path.exists() {
|
||||
create_dir_all(path.parent().unwrap()).unwrap();
|
||||
let mut file = File::create(&path).unwrap();
|
||||
file.write_all(include_bytes!("../assets/icon.ico"))
|
||||
.unwrap();
|
||||
}
|
||||
path
|
||||
};
|
||||
bundle_settings.windows.icon_path = icon_path;
|
||||
}
|
||||
}
|
||||
|
||||
let mut settings = SettingsBuilder::new()
|
||||
.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),
|
||||
default_run: Some(crate_config.dioxus_config.application.name.clone()),
|
||||
})
|
||||
.binaries(binaries)
|
||||
.bundle_settings(bundle_settings);
|
||||
if let Some(packages) = self.package {
|
||||
settings = settings.package_types(
|
||||
packages
|
||||
.into_iter()
|
||||
.map(|p| p.parse::<PackageType>().unwrap().into())
|
||||
.collect(),
|
||||
);
|
||||
}
|
||||
let settings = settings.build();
|
||||
|
||||
// on macos we need to set CI=true (https://github.com/tauri-apps/tauri/issues/2567)
|
||||
#[cfg(target_os = "macos")]
|
||||
std::env::set_var("CI", "true");
|
||||
|
||||
tauri_bundler::bundle::bundle_project(settings.unwrap()).unwrap_or_else(|err|{
|
||||
#[cfg(target_os = "macos")]
|
||||
panic!("Failed to bundle project: {}\nMake sure you have automation enabled in your terminal (https://github.com/tauri-apps/tauri/issues/3055#issuecomment-1624389208) and full disk access enabled for your terminal (https://github.com/tauri-apps/tauri/issues/3055#issuecomment-1624389208)", err);
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
panic!("Failed to bundle project: {}", err);
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,6 @@
|
||||
use clap::ValueEnum;
|
||||
use serde::Serialize;
|
||||
|
||||
use super::*;
|
||||
|
||||
/// Config options for the build system.
|
||||
@@ -26,8 +29,8 @@ pub struct ConfigOptsBuild {
|
||||
pub profile: Option<String>,
|
||||
|
||||
/// Build platform: support Web & Desktop [default: "default_platform"]
|
||||
#[clap(long)]
|
||||
pub platform: Option<String>,
|
||||
#[clap(long, value_enum)]
|
||||
pub platform: Option<Platform>,
|
||||
|
||||
/// Space separated list of features to activate
|
||||
#[clap(long)]
|
||||
@@ -69,8 +72,8 @@ pub struct ConfigOptsServe {
|
||||
pub profile: Option<String>,
|
||||
|
||||
/// Build platform: support Web & Desktop [default: "default_platform"]
|
||||
#[clap(long)]
|
||||
pub platform: Option<String>,
|
||||
#[clap(long, value_enum)]
|
||||
pub platform: Option<Platform>,
|
||||
|
||||
/// Build with hot reloading rsx [default: false]
|
||||
#[clap(long)]
|
||||
@@ -88,9 +91,42 @@ pub struct ConfigOptsServe {
|
||||
pub features: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
/// Ensure the given value for `--public-url` is formatted correctly.
|
||||
pub fn parse_public_url(val: &str) -> String {
|
||||
let prefix = if !val.starts_with('/') { "/" } else { "" };
|
||||
let suffix = if !val.ends_with('/') { "/" } else { "" };
|
||||
format!("{}{}{}", prefix, val, suffix)
|
||||
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Serialize, Deserialize, Debug)]
|
||||
pub enum Platform {
|
||||
#[clap(name = "web")]
|
||||
#[serde(rename = "web")]
|
||||
Web,
|
||||
#[clap(name = "desktop")]
|
||||
#[serde(rename = "desktop")]
|
||||
Desktop,
|
||||
}
|
||||
|
||||
/// Config options for the bundling system.
|
||||
#[derive(Clone, Debug, Default, Deserialize, Parser)]
|
||||
pub struct ConfigOptsBundle {
|
||||
/// Build in release mode [default: false]
|
||||
#[clap(long)]
|
||||
#[serde(default)]
|
||||
pub release: bool,
|
||||
|
||||
// Use verbose output [default: false]
|
||||
#[clap(long)]
|
||||
#[serde(default)]
|
||||
pub verbose: bool,
|
||||
|
||||
/// Build a example [default: ""]
|
||||
#[clap(long)]
|
||||
pub example: Option<String>,
|
||||
|
||||
/// Build with custom profile
|
||||
#[clap(long)]
|
||||
pub profile: Option<String>,
|
||||
|
||||
/// Build platform: support Web & Desktop [default: "default_platform"]
|
||||
#[clap(long)]
|
||||
pub platform: Option<String>,
|
||||
|
||||
/// Space separated list of features to activate
|
||||
#[clap(long)]
|
||||
pub features: Option<Vec<String>>,
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
use futures::{stream::FuturesUnordered, StreamExt};
|
||||
use std::{path::Path, process::exit};
|
||||
|
||||
use super::*;
|
||||
|
||||
// For reference, the rustfmt main.rs file
|
||||
// https://github.com/rust-lang/rustfmt/blob/master/src/bin/main.rs
|
||||
|
||||
/// Check the Rust files in the project for issues.
|
||||
#[derive(Clone, Debug, Parser)]
|
||||
pub struct Check {
|
||||
/// Input file
|
||||
#[clap(short, long)]
|
||||
pub file: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl Check {
|
||||
// Todo: check the entire crate
|
||||
pub async fn check(self) -> Result<()> {
|
||||
match self.file {
|
||||
// Default to checking the project
|
||||
None => {
|
||||
if let Err(e) = check_project_and_report().await {
|
||||
eprintln!("error checking project: {}", e);
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
Some(file) => {
|
||||
if let Err(e) = check_file_and_report(file).await {
|
||||
eprintln!("failed to check file: {}", e);
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
async fn check_file_and_report(path: PathBuf) -> Result<()> {
|
||||
check_files_and_report(vec![path]).await
|
||||
}
|
||||
|
||||
/// Read every .rs file accessible when considering the .gitignore and check it
|
||||
///
|
||||
/// Runs using Tokio for multithreading, so it should be really really fast
|
||||
///
|
||||
/// Doesn't do mod-descending, so it will still try to check unreachable files. TODO.
|
||||
async fn check_project_and_report() -> Result<()> {
|
||||
let crate_config = crate::CrateConfig::new(None)?;
|
||||
|
||||
let mut files_to_check = vec![];
|
||||
collect_rs_files(&crate_config.crate_dir, &mut files_to_check);
|
||||
check_files_and_report(files_to_check).await
|
||||
}
|
||||
|
||||
/// Check a list of files and report the issues.
|
||||
async fn check_files_and_report(files_to_check: Vec<PathBuf>) -> Result<()> {
|
||||
let issue_reports = files_to_check
|
||||
.into_iter()
|
||||
.filter(|file| file.components().all(|f| f.as_os_str() != "target"))
|
||||
.map(|path| async move {
|
||||
let _path = path.clone();
|
||||
let res = tokio::spawn(async move {
|
||||
tokio::fs::read_to_string(&_path)
|
||||
.await
|
||||
.map(|contents| dioxus_check::check_file(_path, &contents))
|
||||
})
|
||||
.await;
|
||||
|
||||
if res.is_err() {
|
||||
eprintln!("error checking file: {}", path.display());
|
||||
}
|
||||
|
||||
res
|
||||
})
|
||||
.collect::<FuturesUnordered<_>>()
|
||||
.collect::<Vec<_>>()
|
||||
.await;
|
||||
|
||||
// remove error results which we've already printed
|
||||
let issue_reports = issue_reports
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.flatten()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let total_issues = issue_reports.iter().map(|r| r.issues.len()).sum::<usize>();
|
||||
|
||||
for report in issue_reports.into_iter() {
|
||||
if !report.issues.is_empty() {
|
||||
println!("{}", report);
|
||||
}
|
||||
}
|
||||
|
||||
match total_issues {
|
||||
0 => println!("No issues found."),
|
||||
1 => println!("1 issue found."),
|
||||
_ => println!("{} issues found.", total_issues),
|
||||
}
|
||||
|
||||
match total_issues {
|
||||
0 => exit(0),
|
||||
_ => exit(1),
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_rs_files(folder: &Path, files: &mut Vec<PathBuf>) {
|
||||
let Ok(folder) = folder.read_dir() else {
|
||||
return;
|
||||
};
|
||||
|
||||
// load the gitignore
|
||||
|
||||
for entry in folder {
|
||||
let Ok(entry) = entry else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let path = entry.path();
|
||||
|
||||
if path.is_dir() {
|
||||
collect_rs_files(&path, files);
|
||||
}
|
||||
|
||||
if let Some(ext) = path.extension() {
|
||||
if ext == "rs" {
|
||||
files.push(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
use super::*;
|
||||
|
||||
/// Build the Rust WASM app and all of its assets.
|
||||
/// Clean build artifacts.
|
||||
#[derive(Clone, Debug, Parser)]
|
||||
#[clap(name = "clean")]
|
||||
pub struct Clean {}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use super::*;
|
||||
|
||||
/// Build the Rust WASM app and all of its assets.
|
||||
/// Dioxus config file controls
|
||||
#[derive(Clone, Debug, Deserialize, Subcommand)]
|
||||
#[clap(name = "config")]
|
||||
pub enum Config {
|
||||
|
||||
@@ -46,15 +46,15 @@ impl Create {
|
||||
toml.as_table_mut().fmt();
|
||||
|
||||
let as_string = toml.to_string();
|
||||
let new_string = remove_tripple_newlines(&as_string);
|
||||
let new_string = remove_triple_newlines(&as_string);
|
||||
let mut file = std::fs::File::create(toml_path)?;
|
||||
file.write_all(new_string.as_bytes())?;
|
||||
}
|
||||
|
||||
// remove any tripple newlines from the readme
|
||||
// remove any triple newlines from the readme
|
||||
let readme_path = path.join("README.md");
|
||||
let readme = std::fs::read_to_string(&readme_path)?;
|
||||
let new_readme = remove_tripple_newlines(&readme);
|
||||
let new_readme = remove_triple_newlines(&readme);
|
||||
let mut file = std::fs::File::create(readme_path)?;
|
||||
file.write_all(new_readme.as_bytes())?;
|
||||
|
||||
@@ -64,7 +64,7 @@ impl Create {
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_tripple_newlines(string: &str) -> String {
|
||||
fn remove_triple_newlines(string: &str) -> String {
|
||||
let mut new_string = String::new();
|
||||
for char in string.chars() {
|
||||
if char == '\n' && new_string.ends_with("\n\n") {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
pub mod autoformat;
|
||||
pub mod build;
|
||||
pub mod bundle;
|
||||
pub mod cfg;
|
||||
pub mod check;
|
||||
pub mod clean;
|
||||
pub mod config;
|
||||
pub mod create;
|
||||
@@ -59,6 +61,9 @@ pub enum Commands {
|
||||
/// Clean output artifacts.
|
||||
Clean(clean::Clean),
|
||||
|
||||
/// Bundle the Rust desktop app and all of its assets.
|
||||
Bundle(bundle::Bundle),
|
||||
|
||||
/// Print the version of this extension
|
||||
#[clap(name = "version")]
|
||||
Version(version::Version),
|
||||
@@ -67,6 +72,10 @@ pub enum Commands {
|
||||
#[clap(name = "fmt")]
|
||||
Autoformat(autoformat::Autoformat),
|
||||
|
||||
/// Check the Rust files in the project for issues.
|
||||
#[clap(name = "check")]
|
||||
Check(check::Check),
|
||||
|
||||
/// Dioxus config file controls.
|
||||
#[clap(subcommand)]
|
||||
Config(config::Config),
|
||||
@@ -88,6 +97,8 @@ impl Display for Commands {
|
||||
Commands::Config(_) => write!(f, "config"),
|
||||
Commands::Version(_) => write!(f, "version"),
|
||||
Commands::Autoformat(_) => write!(f, "fmt"),
|
||||
Commands::Check(_) => write!(f, "check"),
|
||||
Commands::Bundle(_) => write!(f, "bundle"),
|
||||
|
||||
#[cfg(feature = "plugin")]
|
||||
Commands::Plugin(_) => write!(f, "plugin"),
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
use super::*;
|
||||
|
||||
/// Build the Rust WASM app and all of its assets.
|
||||
/// Manage plugins for dioxus cli
|
||||
#[derive(Clone, Debug, Deserialize, Subcommand)]
|
||||
#[clap(name = "plugin")]
|
||||
pub enum Plugin {
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
use super::*;
|
||||
use std::{
|
||||
fs::create_dir_all,
|
||||
io::Write,
|
||||
path::PathBuf,
|
||||
process::{Command, Stdio},
|
||||
};
|
||||
use std::{fs::create_dir_all, io::Write, path::PathBuf};
|
||||
|
||||
/// Run the WASM project on dev-server
|
||||
#[derive(Clone, Debug, Parser)]
|
||||
@@ -39,41 +34,24 @@ impl Serve {
|
||||
// Subdirectories don't work with the server
|
||||
crate_config.dioxus_config.web.app.base_path = None;
|
||||
|
||||
let platform = self.serve.platform.unwrap_or_else(|| {
|
||||
crate_config
|
||||
.dioxus_config
|
||||
.application
|
||||
.default_platform
|
||||
.clone()
|
||||
});
|
||||
let platform = self
|
||||
.serve
|
||||
.platform
|
||||
.unwrap_or(crate_config.dioxus_config.application.default_platform);
|
||||
|
||||
if platform.as_str() == "desktop" {
|
||||
crate::builder::build_desktop(&crate_config, true)?;
|
||||
match platform {
|
||||
cfg::Platform::Web => {
|
||||
// generate dev-index page
|
||||
Serve::regen_dev_page(&crate_config)?;
|
||||
|
||||
match &crate_config.executable {
|
||||
crate::ExecutableType::Binary(name)
|
||||
| crate::ExecutableType::Lib(name)
|
||||
| crate::ExecutableType::Example(name) => {
|
||||
let mut file = crate_config.out_dir.join(name);
|
||||
if cfg!(windows) {
|
||||
file.set_extension("exe");
|
||||
}
|
||||
Command::new(file.to_str().unwrap())
|
||||
.stdout(Stdio::inherit())
|
||||
.output()?;
|
||||
}
|
||||
// start the develop server
|
||||
server::web::startup(self.serve.port, crate_config.clone(), self.serve.open)
|
||||
.await?;
|
||||
}
|
||||
cfg::Platform::Desktop => {
|
||||
server::desktop::startup(crate_config.clone()).await?;
|
||||
}
|
||||
return Ok(());
|
||||
} else if platform != "web" {
|
||||
return custom_error!("Unsupported platform target.");
|
||||
}
|
||||
|
||||
// generate dev-index page
|
||||
Serve::regen_dev_page(&crate_config)?;
|
||||
|
||||
// start the develop server
|
||||
server::startup(self.serve.port, crate_config.clone(), self.serve.open).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
use crate::tools;
|
||||
|
||||
use super::*;
|
||||
|
||||
/// Build the Rust WASM app and all of its assets.
|
||||
#[derive(Clone, Debug, Deserialize, Subcommand)]
|
||||
#[clap(name = "tool")]
|
||||
pub enum Tool {
|
||||
/// Return all dioxus-cli support tools.
|
||||
List {},
|
||||
/// Get default app install path.
|
||||
AppPath {},
|
||||
/// Install a new tool.
|
||||
Add { name: String },
|
||||
}
|
||||
|
||||
impl Tool {
|
||||
pub async fn tool(self) -> Result<()> {
|
||||
match self {
|
||||
Tool::List {} => {
|
||||
for item in tools::tool_list() {
|
||||
if tools::Tool::from_str(item).unwrap().is_installed() {
|
||||
println!("- {item} [installed]");
|
||||
} else {
|
||||
println!("- {item}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Tool::AppPath {} => {
|
||||
if let Some(v) = tools::tools_path().to_str() {
|
||||
println!("{}", v);
|
||||
} else {
|
||||
return custom_error!("Tools path get failed.");
|
||||
}
|
||||
}
|
||||
Tool::Add { name } => {
|
||||
let tool_list = tools::tool_list();
|
||||
|
||||
if !tool_list.contains(&name.as_str()) {
|
||||
return custom_error!("Tool {name} not found.");
|
||||
}
|
||||
let target_tool = tools::Tool::from_str(&name).unwrap();
|
||||
|
||||
if target_tool.is_installed() {
|
||||
log::warn!("Tool {name} is installed.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
log::info!("Start to download tool package...");
|
||||
if let Err(e) = target_tool.download_package().await {
|
||||
return custom_error!("Tool download failed: {e}");
|
||||
}
|
||||
|
||||
log::info!("Start to install tool package...");
|
||||
if let Err(e) = target_tool.install_package().await {
|
||||
return custom_error!("Tool install failed: {e}");
|
||||
}
|
||||
|
||||
log::info!("Tool {name} installed successfully!");
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ use dioxus_rsx::{BodyNode, CallBody};
|
||||
|
||||
use super::*;
|
||||
|
||||
/// Build the Rust WASM app and all of its assets.
|
||||
/// Translate some source file into Dioxus code
|
||||
#[derive(Clone, Debug, Parser)]
|
||||
#[clap(name = "translate")]
|
||||
pub struct Translate {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use super::*;
|
||||
|
||||
/// Build the Rust WASM app and all of its assets.
|
||||
/// Print the version of this extension
|
||||
#[derive(Clone, Debug, Parser)]
|
||||
#[clap(name = "version")]
|
||||
pub struct Version {}
|
||||
|
||||
+276
-6
@@ -1,4 +1,4 @@
|
||||
use crate::error::Result;
|
||||
use crate::{cfg::Platform, error::Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
@@ -11,6 +11,9 @@ pub struct DioxusConfig {
|
||||
|
||||
pub web: WebConfig,
|
||||
|
||||
#[serde(default)]
|
||||
pub bundle: BundleConfig,
|
||||
|
||||
#[serde(default = "default_plugin")]
|
||||
pub plugin: toml::Value,
|
||||
}
|
||||
@@ -40,7 +43,7 @@ impl DioxusConfig {
|
||||
};
|
||||
|
||||
let dioxus_conf_file = dioxus_conf_file.as_path();
|
||||
toml::from_str::<DioxusConfig>(&std::fs::read_to_string(dioxus_conf_file)?)
|
||||
let cfg = toml::from_str::<DioxusConfig>(&std::fs::read_to_string(dioxus_conf_file)?)
|
||||
.map_err(|err| {
|
||||
let error_location = dioxus_conf_file
|
||||
.strip_prefix(crate_dir)
|
||||
@@ -48,7 +51,20 @@ impl DioxusConfig {
|
||||
.display();
|
||||
crate::Error::Unique(format!("{error_location} {err}"))
|
||||
})
|
||||
.map(Some)
|
||||
.map(Some);
|
||||
match cfg {
|
||||
Ok(Some(mut cfg)) => {
|
||||
let name = cfg.application.name.clone();
|
||||
if cfg.bundle.identifier.is_none() {
|
||||
cfg.bundle.identifier = Some(format!("io.github.{name}"));
|
||||
}
|
||||
if cfg.bundle.publisher.is_none() {
|
||||
cfg.bundle.publisher = Some(name);
|
||||
}
|
||||
Ok(Some(cfg))
|
||||
}
|
||||
cfg => cfg,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,10 +86,11 @@ fn acquire_dioxus_toml(dir: &Path) -> Option<PathBuf> {
|
||||
|
||||
impl Default for DioxusConfig {
|
||||
fn default() -> Self {
|
||||
let name = "name";
|
||||
Self {
|
||||
application: ApplicationConfig {
|
||||
name: "dioxus".into(),
|
||||
default_platform: "web".to_string(),
|
||||
name: name.into(),
|
||||
default_platform: Platform::Web,
|
||||
out_dir: Some(PathBuf::from("dist")),
|
||||
asset_dir: Some(PathBuf::from("public")),
|
||||
|
||||
@@ -107,6 +124,11 @@ impl Default for DioxusConfig {
|
||||
cert_path: None,
|
||||
},
|
||||
},
|
||||
bundle: BundleConfig {
|
||||
identifier: Some(format!("io.github.{name}")),
|
||||
publisher: Some(name.into()),
|
||||
..Default::default()
|
||||
},
|
||||
plugin: toml::Value::Table(toml::map::Map::new()),
|
||||
}
|
||||
}
|
||||
@@ -115,7 +137,7 @@ impl Default for DioxusConfig {
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ApplicationConfig {
|
||||
pub name: String,
|
||||
pub default_platform: String,
|
||||
pub default_platform: Platform,
|
||||
pub out_dir: Option<PathBuf>,
|
||||
pub asset_dir: Option<PathBuf>,
|
||||
|
||||
@@ -310,3 +332,251 @@ impl CrateConfig {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct BundleConfig {
|
||||
pub identifier: Option<String>,
|
||||
pub publisher: Option<String>,
|
||||
pub icon: Option<Vec<String>>,
|
||||
pub resources: Option<Vec<String>>,
|
||||
pub copyright: Option<String>,
|
||||
pub category: Option<String>,
|
||||
pub short_description: Option<String>,
|
||||
pub long_description: Option<String>,
|
||||
pub external_bin: Option<Vec<String>>,
|
||||
pub deb: Option<DebianSettings>,
|
||||
pub macos: Option<MacOsSettings>,
|
||||
pub windows: Option<WindowsSettings>,
|
||||
}
|
||||
|
||||
impl From<BundleConfig> for tauri_bundler::BundleSettings {
|
||||
fn from(val: BundleConfig) -> Self {
|
||||
tauri_bundler::BundleSettings {
|
||||
identifier: val.identifier,
|
||||
publisher: val.publisher,
|
||||
icon: val.icon,
|
||||
resources: val.resources,
|
||||
copyright: val.copyright,
|
||||
category: val.category.and_then(|c| c.parse().ok()),
|
||||
short_description: val.short_description,
|
||||
long_description: val.long_description,
|
||||
external_bin: val.external_bin,
|
||||
deb: val.deb.map(Into::into).unwrap_or_default(),
|
||||
macos: val.macos.map(Into::into).unwrap_or_default(),
|
||||
windows: val.windows.map(Into::into).unwrap_or_default(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct DebianSettings {
|
||||
pub depends: Option<Vec<String>>,
|
||||
pub files: HashMap<PathBuf, PathBuf>,
|
||||
pub nsis: Option<NsisSettings>,
|
||||
}
|
||||
|
||||
impl From<DebianSettings> for tauri_bundler::DebianSettings {
|
||||
fn from(val: DebianSettings) -> Self {
|
||||
tauri_bundler::DebianSettings {
|
||||
depends: val.depends,
|
||||
files: val.files,
|
||||
desktop_template: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct WixSettings {
|
||||
pub language: Vec<(String, Option<PathBuf>)>,
|
||||
pub template: Option<PathBuf>,
|
||||
pub fragment_paths: Vec<PathBuf>,
|
||||
pub component_group_refs: Vec<String>,
|
||||
pub component_refs: Vec<String>,
|
||||
pub feature_group_refs: Vec<String>,
|
||||
pub feature_refs: Vec<String>,
|
||||
pub merge_refs: Vec<String>,
|
||||
pub skip_webview_install: bool,
|
||||
pub license: Option<PathBuf>,
|
||||
pub enable_elevated_update_task: bool,
|
||||
pub banner_path: Option<PathBuf>,
|
||||
pub dialog_image_path: Option<PathBuf>,
|
||||
pub fips_compliant: bool,
|
||||
}
|
||||
|
||||
impl From<WixSettings> for tauri_bundler::WixSettings {
|
||||
fn from(val: WixSettings) -> Self {
|
||||
tauri_bundler::WixSettings {
|
||||
language: tauri_bundler::bundle::WixLanguage({
|
||||
let mut languages: Vec<_> = val
|
||||
.language
|
||||
.iter()
|
||||
.map(|l| {
|
||||
(
|
||||
l.0.clone(),
|
||||
tauri_bundler::bundle::WixLanguageConfig {
|
||||
locale_path: l.1.clone(),
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
if languages.is_empty() {
|
||||
languages.push(("en-US".into(), Default::default()));
|
||||
}
|
||||
languages
|
||||
}),
|
||||
template: val.template,
|
||||
fragment_paths: val.fragment_paths,
|
||||
component_group_refs: val.component_group_refs,
|
||||
component_refs: val.component_refs,
|
||||
feature_group_refs: val.feature_group_refs,
|
||||
feature_refs: val.feature_refs,
|
||||
merge_refs: val.merge_refs,
|
||||
skip_webview_install: val.skip_webview_install,
|
||||
license: val.license,
|
||||
enable_elevated_update_task: val.enable_elevated_update_task,
|
||||
banner_path: val.banner_path,
|
||||
dialog_image_path: val.dialog_image_path,
|
||||
fips_compliant: val.fips_compliant,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct MacOsSettings {
|
||||
pub frameworks: Option<Vec<String>>,
|
||||
pub minimum_system_version: Option<String>,
|
||||
pub license: Option<String>,
|
||||
pub exception_domain: Option<String>,
|
||||
pub signing_identity: Option<String>,
|
||||
pub provider_short_name: Option<String>,
|
||||
pub entitlements: Option<String>,
|
||||
pub info_plist_path: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl From<MacOsSettings> for tauri_bundler::MacOsSettings {
|
||||
fn from(val: MacOsSettings) -> Self {
|
||||
tauri_bundler::MacOsSettings {
|
||||
frameworks: val.frameworks,
|
||||
minimum_system_version: val.minimum_system_version,
|
||||
license: val.license,
|
||||
exception_domain: val.exception_domain,
|
||||
signing_identity: val.signing_identity,
|
||||
provider_short_name: val.provider_short_name,
|
||||
entitlements: val.entitlements,
|
||||
info_plist_path: val.info_plist_path,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct WindowsSettings {
|
||||
pub digest_algorithm: Option<String>,
|
||||
pub certificate_thumbprint: Option<String>,
|
||||
pub timestamp_url: Option<String>,
|
||||
pub tsp: bool,
|
||||
pub wix: Option<WixSettings>,
|
||||
pub icon_path: Option<PathBuf>,
|
||||
pub webview_install_mode: WebviewInstallMode,
|
||||
pub webview_fixed_runtime_path: Option<PathBuf>,
|
||||
pub allow_downgrades: bool,
|
||||
pub nsis: Option<NsisSettings>,
|
||||
}
|
||||
|
||||
impl From<WindowsSettings> for tauri_bundler::WindowsSettings {
|
||||
fn from(val: WindowsSettings) -> Self {
|
||||
tauri_bundler::WindowsSettings {
|
||||
digest_algorithm: val.digest_algorithm,
|
||||
certificate_thumbprint: val.certificate_thumbprint,
|
||||
timestamp_url: val.timestamp_url,
|
||||
tsp: val.tsp,
|
||||
wix: val.wix.map(Into::into),
|
||||
icon_path: val.icon_path.unwrap_or("icons/icon.ico".into()),
|
||||
webview_install_mode: val.webview_install_mode.into(),
|
||||
webview_fixed_runtime_path: val.webview_fixed_runtime_path,
|
||||
allow_downgrades: val.allow_downgrades,
|
||||
nsis: val.nsis.map(Into::into),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct NsisSettings {
|
||||
pub template: Option<PathBuf>,
|
||||
pub license: Option<PathBuf>,
|
||||
pub header_image: Option<PathBuf>,
|
||||
pub sidebar_image: Option<PathBuf>,
|
||||
pub installer_icon: Option<PathBuf>,
|
||||
pub install_mode: NSISInstallerMode,
|
||||
pub languages: Option<Vec<String>>,
|
||||
pub custom_language_files: Option<HashMap<String, PathBuf>>,
|
||||
pub display_language_selector: bool,
|
||||
}
|
||||
|
||||
impl From<NsisSettings> for tauri_bundler::NsisSettings {
|
||||
fn from(val: NsisSettings) -> Self {
|
||||
tauri_bundler::NsisSettings {
|
||||
license: val.license,
|
||||
header_image: val.header_image,
|
||||
sidebar_image: val.sidebar_image,
|
||||
installer_icon: val.installer_icon,
|
||||
install_mode: val.install_mode.into(),
|
||||
languages: val.languages,
|
||||
display_language_selector: val.display_language_selector,
|
||||
custom_language_files: None,
|
||||
template: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum NSISInstallerMode {
|
||||
CurrentUser,
|
||||
PerMachine,
|
||||
Both,
|
||||
}
|
||||
|
||||
impl From<NSISInstallerMode> for tauri_utils::config::NSISInstallerMode {
|
||||
fn from(val: NSISInstallerMode) -> Self {
|
||||
match val {
|
||||
NSISInstallerMode::CurrentUser => tauri_utils::config::NSISInstallerMode::CurrentUser,
|
||||
NSISInstallerMode::PerMachine => tauri_utils::config::NSISInstallerMode::PerMachine,
|
||||
NSISInstallerMode::Both => tauri_utils::config::NSISInstallerMode::Both,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum WebviewInstallMode {
|
||||
Skip,
|
||||
DownloadBootstrapper { silent: bool },
|
||||
EmbedBootstrapper { silent: bool },
|
||||
OfflineInstaller { silent: bool },
|
||||
FixedRuntime { path: PathBuf },
|
||||
}
|
||||
|
||||
impl WebviewInstallMode {
|
||||
fn into(self) -> tauri_utils::config::WebviewInstallMode {
|
||||
match self {
|
||||
Self::Skip => tauri_utils::config::WebviewInstallMode::Skip,
|
||||
Self::DownloadBootstrapper { silent } => {
|
||||
tauri_utils::config::WebviewInstallMode::DownloadBootstrapper { silent }
|
||||
}
|
||||
Self::EmbedBootstrapper { silent } => {
|
||||
tauri_utils::config::WebviewInstallMode::EmbedBootstrapper { silent }
|
||||
}
|
||||
Self::OfflineInstaller { silent } => {
|
||||
tauri_utils::config::WebviewInstallMode::OfflineInstaller { silent }
|
||||
}
|
||||
Self::FixedRuntime { path } => {
|
||||
tauri_utils::config::WebviewInstallMode::FixedRuntime { path }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for WebviewInstallMode {
|
||||
fn default() -> Self {
|
||||
Self::OfflineInstaller { silent: false }
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,9 @@ pub enum Error {
|
||||
#[error("Invalid proxy URL: {0}")]
|
||||
InvalidProxy(#[from] hyper::http::uri::InvalidUri),
|
||||
|
||||
#[error("Failed to establish proxy: {0}")]
|
||||
ProxySetupError(String),
|
||||
|
||||
#[error("Error proxying request: {0}")]
|
||||
ProxyRequestError(hyper::Error),
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
pub const DIOXUS_CLI_VERSION: &str = "0.1.5";
|
||||
pub const DIOXUS_CLI_VERSION: &str = "0.4.1";
|
||||
|
||||
pub mod builder;
|
||||
pub mod server;
|
||||
|
||||
@@ -92,6 +92,10 @@ async fn main() -> anyhow::Result<()> {
|
||||
.config()
|
||||
.map_err(|e| anyhow!("🚫 Configuring new project failed: {}", e)),
|
||||
|
||||
Bundle(opts) => opts
|
||||
.bundle(bin.clone())
|
||||
.map_err(|e| anyhow!("🚫 Bundling project failed: {}", e)),
|
||||
|
||||
#[cfg(feature = "plugin")]
|
||||
Plugin(opts) => opts
|
||||
.plugin()
|
||||
@@ -103,6 +107,11 @@ async fn main() -> anyhow::Result<()> {
|
||||
.await
|
||||
.map_err(|e| anyhow!("🚫 Error autoformatting RSX: {}", e)),
|
||||
|
||||
Check(opts) => opts
|
||||
.check()
|
||||
.await
|
||||
.map_err(|e| anyhow!("🚫 Error checking RSX: {}", e)),
|
||||
|
||||
Version(opt) => {
|
||||
let version = opt.version();
|
||||
println!("{}", version);
|
||||
|
||||
@@ -0,0 +1,223 @@
|
||||
use crate::{
|
||||
server::{
|
||||
output::{print_console_info, PrettierOptions},
|
||||
setup_file_watcher,
|
||||
},
|
||||
BuildResult, CrateConfig, Result,
|
||||
};
|
||||
|
||||
use dioxus_hot_reload::HotReloadMsg;
|
||||
use dioxus_html::HtmlCtx;
|
||||
use dioxus_rsx::hot_reload::*;
|
||||
use interprocess_docfix::local_socket::LocalSocketListener;
|
||||
use std::{
|
||||
process::{Child, Command},
|
||||
sync::{Arc, Mutex, RwLock},
|
||||
};
|
||||
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();
|
||||
let _ = ctrlc::set_handler(move || {
|
||||
#[cfg(feature = "plugin")]
|
||||
let _ = PluginManager::on_serve_shutdown(&_crate_config);
|
||||
std::process::exit(0);
|
||||
});
|
||||
|
||||
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(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);
|
||||
|
||||
log::info!("🚀 Starting development server...");
|
||||
|
||||
// We got to own watcher so that it exists for the duration of serve
|
||||
// Otherwise full reload won't work.
|
||||
let _watcher = setup_file_watcher(
|
||||
{
|
||||
let config = config.clone();
|
||||
|
||||
move || {
|
||||
let mut current_child = currently_running_child.write().unwrap();
|
||||
current_child.kill()?;
|
||||
let (child, result) = start_desktop(&config)?;
|
||||
*current_child = child;
|
||||
Ok(result)
|
||||
}
|
||||
},
|
||||
&config,
|
||||
None,
|
||||
hot_reload_state.clone(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Print serve info
|
||||
print_console_info(
|
||||
&config,
|
||||
PrettierOptions {
|
||||
changed: vec![],
|
||||
warnings: first_build_result.warnings,
|
||||
elapsed_time: first_build_result.elapsed_time,
|
||||
},
|
||||
None,
|
||||
);
|
||||
|
||||
match hot_reload_state {
|
||||
Some(hot_reload_state) => {
|
||||
start_desktop_hot_reload(hot_reload_state).await?;
|
||||
}
|
||||
None => {
|
||||
std::future::pending::<()>().await;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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 = 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;
|
||||
}
|
||||
}
|
||||
channels.lock().unwrap().push(connection);
|
||||
println!("Connected to hot reloading 🚀");
|
||||
}
|
||||
if *aborted.lock().unwrap() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
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;
|
||||
while i < channels.len() {
|
||||
let channel = &mut channels[i];
|
||||
if send_msg(HotReloadMsg::UpdateTemplate(template), channel) {
|
||||
i += 1;
|
||||
} else {
|
||||
channels.remove(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(error) => println!("failed to connect to hot reloading\n{error}"),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn clear_paths() {
|
||||
if cfg!(target_os = "macos") {
|
||||
// On unix, if you force quit the application, it can leave the file socket open
|
||||
// This will cause the local socket listener to fail to open
|
||||
// We check if the file socket is already open from an old session and then delete it
|
||||
let paths = ["./dioxusin", "./@dioxusin"];
|
||||
for path in paths {
|
||||
let path = std::path::PathBuf::from(path);
|
||||
if path.exists() {
|
||||
let _ = std::fs::remove_file(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn send_msg(msg: HotReloadMsg, channel: &mut impl std::io::Write) -> bool {
|
||||
if let Ok(msg) = serde_json::to_string(&msg) {
|
||||
if channel.write_all(msg.as_bytes()).is_err() {
|
||||
return false;
|
||||
}
|
||||
if channel.write_all(&[b'\n']).is_err() {
|
||||
return false;
|
||||
}
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start_desktop(config: &CrateConfig) -> Result<(Child, BuildResult)> {
|
||||
// Run the desktop application
|
||||
let result = crate::builder::build_desktop(config, true)?;
|
||||
|
||||
match &config.executable {
|
||||
crate::ExecutableType::Binary(name)
|
||||
| crate::ExecutableType::Lib(name)
|
||||
| crate::ExecutableType::Example(name) => {
|
||||
let mut file = config.out_dir.join(name);
|
||||
if cfg!(windows) {
|
||||
file.set_extension("exe");
|
||||
}
|
||||
let child = Command::new(file.to_str().unwrap()).spawn()?;
|
||||
|
||||
Ok((child, result))
|
||||
}
|
||||
}
|
||||
}
|
||||
+85
-602
@@ -1,449 +1,28 @@
|
||||
use crate::{builder, serve::Serve, BuildResult, CrateConfig, Result};
|
||||
use axum::{
|
||||
body::{Full, HttpBody},
|
||||
extract::{ws::Message, Extension, TypedHeader, WebSocketUpgrade},
|
||||
http::{
|
||||
header::{HeaderName, HeaderValue},
|
||||
Method, Response, StatusCode,
|
||||
},
|
||||
response::IntoResponse,
|
||||
routing::{get, get_service},
|
||||
Router,
|
||||
};
|
||||
use axum_server::tls_rustls::RustlsConfig;
|
||||
use crate::{BuildResult, CrateConfig, Result};
|
||||
|
||||
use cargo_metadata::diagnostic::Diagnostic;
|
||||
use dioxus_core::Template;
|
||||
use dioxus_html::HtmlCtx;
|
||||
use dioxus_rsx::hot_reload::*;
|
||||
use notify::{RecommendedWatcher, Watcher};
|
||||
use std::{
|
||||
net::UdpSocket,
|
||||
path::PathBuf,
|
||||
process::Command,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
use tokio::sync::broadcast::{self, Sender};
|
||||
use tower::ServiceBuilder;
|
||||
use tower_http::services::fs::{ServeDir, ServeFileSystemResponseBody};
|
||||
use tower_http::{
|
||||
cors::{Any, CorsLayer},
|
||||
ServiceBuilderExt,
|
||||
};
|
||||
|
||||
#[cfg(feature = "plugin")]
|
||||
use plugin::PluginManager;
|
||||
|
||||
mod proxy;
|
||||
|
||||
mod hot_reload;
|
||||
use hot_reload::*;
|
||||
use tokio::sync::broadcast::{self};
|
||||
|
||||
mod output;
|
||||
use output::*;
|
||||
|
||||
pub struct BuildManager {
|
||||
config: CrateConfig,
|
||||
reload_tx: broadcast::Sender<()>,
|
||||
}
|
||||
|
||||
impl BuildManager {
|
||||
fn rebuild(&self) -> Result<BuildResult> {
|
||||
log::info!("🪁 Rebuild project");
|
||||
let result = builder::build(&self.config, true)?;
|
||||
// change the websocket reload state to true;
|
||||
// the page will auto-reload.
|
||||
if self
|
||||
.config
|
||||
.dioxus_config
|
||||
.web
|
||||
.watcher
|
||||
.reload_html
|
||||
.unwrap_or(false)
|
||||
{
|
||||
let _ = Serve::regen_dev_page(&self.config);
|
||||
}
|
||||
let _ = self.reload_tx.send(());
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
struct WsReloadState {
|
||||
update: broadcast::Sender<()>,
|
||||
}
|
||||
|
||||
pub async fn startup(port: u16, config: CrateConfig, start_browser: bool) -> Result<()> {
|
||||
// ctrl-c shutdown checker
|
||||
let _crate_config = config.clone();
|
||||
let _ = ctrlc::set_handler(move || {
|
||||
#[cfg(feature = "plugin")]
|
||||
let _ = PluginManager::on_serve_shutdown(&_crate_config);
|
||||
std::process::exit(0);
|
||||
});
|
||||
|
||||
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?,
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Start the server without hot reload
|
||||
pub async fn serve_default(
|
||||
ip: String,
|
||||
port: u16,
|
||||
config: CrateConfig,
|
||||
start_browser: bool,
|
||||
) -> Result<()> {
|
||||
let first_build_result = crate::builder::build(&config, false)?;
|
||||
|
||||
log::info!("🚀 Starting development server...");
|
||||
|
||||
// WS Reload Watching
|
||||
let (reload_tx, _) = broadcast::channel(100);
|
||||
|
||||
// We got to own watcher so that it exists for the duration of serve
|
||||
// Otherwise full reload won't work.
|
||||
let _watcher = setup_file_watcher(&config, port, ip.clone(), reload_tx.clone()).await?;
|
||||
|
||||
let ws_reload_state = Arc::new(WsReloadState {
|
||||
update: reload_tx.clone(),
|
||||
});
|
||||
|
||||
// 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(
|
||||
&ip,
|
||||
port,
|
||||
&config,
|
||||
PrettierOptions {
|
||||
changed: vec![],
|
||||
warnings: first_build_result.warnings,
|
||||
elapsed_time: first_build_result.elapsed_time,
|
||||
},
|
||||
);
|
||||
|
||||
// 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, false)?;
|
||||
|
||||
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 build_manager = Arc::new(BuildManager {
|
||||
config: config.clone(),
|
||||
reload_tx: reload_tx.clone(),
|
||||
});
|
||||
|
||||
let hot_reload_tx = broadcast::channel(100).0;
|
||||
|
||||
// States
|
||||
let hot_reload_state = Arc::new(HotReloadState {
|
||||
messages: hot_reload_tx.clone(),
|
||||
build_manager: build_manager.clone(),
|
||||
file_map: file_map.clone(),
|
||||
watcher_config: config.clone(),
|
||||
});
|
||||
|
||||
let ws_reload_state = Arc::new(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,
|
||||
port,
|
||||
ip.clone(),
|
||||
hot_reload_tx,
|
||||
file_map,
|
||||
build_manager,
|
||||
)
|
||||
.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(
|
||||
&ip,
|
||||
port,
|
||||
&config,
|
||||
PrettierOptions {
|
||||
changed: vec![],
|
||||
warnings: first_build_result.warnings,
|
||||
elapsed_time: first_build_result.elapsed_time,
|
||||
},
|
||||
);
|
||||
|
||||
// Router
|
||||
let router = setup_router(config, ws_reload_state, Some(hot_reload_state)).await?;
|
||||
|
||||
// Start server
|
||||
start_server(port, router, start_browser, rustls_config).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
const DEFAULT_KEY_PATH: &str = "ssl/key.pem";
|
||||
const DEFAULT_CERT_PATH: &str = "ssl/cert.pem";
|
||||
|
||||
/// Returns an enum of rustls config and a bool if mkcert isn't installed
|
||||
async fn get_rustls(config: &CrateConfig) -> Result<Option<RustlsConfig>> {
|
||||
let web_config = &config.dioxus_config.web.https;
|
||||
if web_config.enabled != Some(true) {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let (cert_path, key_path) = match web_config.mkcert {
|
||||
// mkcert, use it
|
||||
Some(true) => {
|
||||
// Get paths to store certs, otherwise use ssl/item.pem
|
||||
let key_path = web_config
|
||||
.key_path
|
||||
.clone()
|
||||
.unwrap_or(DEFAULT_KEY_PATH.to_string());
|
||||
|
||||
let cert_path = web_config
|
||||
.cert_path
|
||||
.clone()
|
||||
.unwrap_or(DEFAULT_CERT_PATH.to_string());
|
||||
|
||||
// Create ssl directory if using defaults
|
||||
if key_path == DEFAULT_KEY_PATH && cert_path == DEFAULT_CERT_PATH {
|
||||
_ = fs::create_dir("ssl");
|
||||
}
|
||||
|
||||
let cmd = Command::new("mkcert")
|
||||
.args([
|
||||
"-install",
|
||||
"-key-file",
|
||||
&key_path,
|
||||
"-cert-file",
|
||||
&cert_path,
|
||||
"localhost",
|
||||
"::1",
|
||||
"127.0.0.1",
|
||||
])
|
||||
.spawn();
|
||||
|
||||
match cmd {
|
||||
Err(e) => {
|
||||
match e.kind() {
|
||||
io::ErrorKind::NotFound => log::error!("mkcert is not installed. See https://github.com/FiloSottile/mkcert#installation for installation instructions."),
|
||||
e => log::error!("an error occured while generating mkcert certificates: {}", e.to_string()),
|
||||
};
|
||||
return Err("failed to generate mkcert certificates".into());
|
||||
}
|
||||
Ok(mut cmd) => {
|
||||
cmd.wait()?;
|
||||
}
|
||||
}
|
||||
|
||||
(cert_path, key_path)
|
||||
}
|
||||
// not mkcert
|
||||
Some(false) => {
|
||||
// get paths to cert & key
|
||||
if let (Some(key), Some(cert)) =
|
||||
(web_config.key_path.clone(), web_config.cert_path.clone())
|
||||
{
|
||||
(cert, key)
|
||||
} else {
|
||||
// missing cert or key
|
||||
return Err("https is enabled but cert or key path is missing".into());
|
||||
}
|
||||
}
|
||||
// other
|
||||
_ => return Ok(None),
|
||||
};
|
||||
|
||||
Ok(Some(
|
||||
RustlsConfig::from_pem_file(cert_path, key_path).await?,
|
||||
))
|
||||
}
|
||||
|
||||
/// Sets up and returns a router
|
||||
async fn setup_router(
|
||||
config: CrateConfig,
|
||||
ws_reload: Arc<WsReloadState>,
|
||||
hot_reload: Option<Arc<HotReloadState>>,
|
||||
) -> Result<Router> {
|
||||
// Setup cors
|
||||
let cors = CorsLayer::new()
|
||||
// allow `GET` and `POST` when accessing the resource
|
||||
.allow_methods([Method::GET, Method::POST])
|
||||
// allow requests from any origin
|
||||
.allow_origin(Any)
|
||||
.allow_headers(Any);
|
||||
|
||||
let (coep, coop) = if config.cross_origin_policy {
|
||||
(
|
||||
HeaderValue::from_static("require-corp"),
|
||||
HeaderValue::from_static("same-origin"),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
HeaderValue::from_static("unsafe-none"),
|
||||
HeaderValue::from_static("unsafe-none"),
|
||||
)
|
||||
};
|
||||
|
||||
// Create file service
|
||||
let file_service_config = config.clone();
|
||||
let file_service = ServiceBuilder::new()
|
||||
.override_response_header(
|
||||
HeaderName::from_static("cross-origin-embedder-policy"),
|
||||
coep,
|
||||
)
|
||||
.override_response_header(HeaderName::from_static("cross-origin-opener-policy"), coop)
|
||||
.and_then(
|
||||
move |response: Response<ServeFileSystemResponseBody>| async move {
|
||||
let response = if file_service_config
|
||||
.dioxus_config
|
||||
.web
|
||||
.watcher
|
||||
.index_on_404
|
||||
.unwrap_or(false)
|
||||
&& response.status() == StatusCode::NOT_FOUND
|
||||
{
|
||||
let body = Full::from(
|
||||
// TODO: Cache/memoize this.
|
||||
std::fs::read_to_string(
|
||||
file_service_config
|
||||
.crate_dir
|
||||
.join(file_service_config.out_dir)
|
||||
.join("index.html"),
|
||||
)
|
||||
.ok()
|
||||
.unwrap(),
|
||||
)
|
||||
.map_err(|err| match err {})
|
||||
.boxed();
|
||||
Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
.body(body)
|
||||
.unwrap()
|
||||
} else {
|
||||
response.map(|body| body.boxed())
|
||||
};
|
||||
Ok(response)
|
||||
},
|
||||
)
|
||||
.service(ServeDir::new(config.crate_dir.join(&config.out_dir)));
|
||||
|
||||
// Setup websocket
|
||||
let mut router = Router::new().route("/_dioxus/ws", get(ws_handler));
|
||||
|
||||
// Setup proxy
|
||||
for proxy_config in config.dioxus_config.web.proxy.unwrap_or_default() {
|
||||
router = proxy::add_proxy(router, &proxy_config)?;
|
||||
}
|
||||
|
||||
// Route file service
|
||||
router = router.fallback(get_service(file_service).handle_error(
|
||||
|error: std::io::Error| async move {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Unhandled internal error: {}", error),
|
||||
)
|
||||
},
|
||||
));
|
||||
|
||||
// Setup routes
|
||||
router = router
|
||||
.route("/_dioxus/hot_reload", get(hot_reload_handler))
|
||||
.layer(cors)
|
||||
.layer(Extension(ws_reload));
|
||||
|
||||
if let Some(hot_reload) = hot_reload {
|
||||
router = router.layer(Extension(hot_reload))
|
||||
}
|
||||
|
||||
Ok(router)
|
||||
}
|
||||
|
||||
/// Starts dx serve with no hot reload
|
||||
async fn start_server(
|
||||
port: u16,
|
||||
router: Router,
|
||||
start_browser: bool,
|
||||
rustls: Option<RustlsConfig>,
|
||||
) -> Result<()> {
|
||||
// If plugins, call on_serve_start event
|
||||
#[cfg(feature = "plugin")]
|
||||
PluginManager::on_serve_start(&config)?;
|
||||
|
||||
// Parse address
|
||||
let addr = format!("0.0.0.0:{}", port).parse().unwrap();
|
||||
|
||||
// Open the browser
|
||||
if start_browser {
|
||||
match rustls {
|
||||
Some(_) => _ = open::that(format!("https://{}", addr)),
|
||||
None => _ = open::that(format!("http://{}", addr)),
|
||||
}
|
||||
}
|
||||
|
||||
// Start the server with or without rustls
|
||||
match rustls {
|
||||
Some(rustls) => {
|
||||
axum_server::bind_rustls(addr, rustls)
|
||||
.serve(router.into_make_service())
|
||||
.await?
|
||||
}
|
||||
None => {
|
||||
axum::Server::bind(&addr)
|
||||
.serve(router.into_make_service())
|
||||
.await?
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
pub mod desktop;
|
||||
pub mod web;
|
||||
|
||||
/// Sets up a file watcher
|
||||
async fn setup_file_watcher(
|
||||
async fn setup_file_watcher<F: Fn() -> Result<BuildResult> + Send + 'static>(
|
||||
build_with: F,
|
||||
config: &CrateConfig,
|
||||
port: u16,
|
||||
watcher_ip: String,
|
||||
reload_tx: Sender<()>,
|
||||
web_info: Option<WebServerInfo>,
|
||||
hot_reload: Option<HotReloadState>,
|
||||
) -> Result<RecommendedWatcher> {
|
||||
let build_manager = BuildManager {
|
||||
config: config.clone(),
|
||||
reload_tx,
|
||||
};
|
||||
|
||||
let mut last_update_time = chrono::Local::now().timestamp();
|
||||
|
||||
// file watcher: check file change
|
||||
@@ -460,29 +39,81 @@ async fn setup_file_watcher(
|
||||
let config = watcher_config.clone();
|
||||
if let Ok(e) = info {
|
||||
if chrono::Local::now().timestamp() > last_update_time {
|
||||
match build_manager.rebuild() {
|
||||
Ok(res) => {
|
||||
last_update_time = chrono::Local::now().timestamp();
|
||||
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(
|
||||
&watcher_ip,
|
||||
port,
|
||||
&config,
|
||||
PrettierOptions {
|
||||
changed: e.paths.clone(),
|
||||
warnings: res.warnings,
|
||||
elapsed_time: res.elapsed_time,
|
||||
},
|
||||
);
|
||||
// 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),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -500,156 +131,8 @@ async fn setup_file_watcher(
|
||||
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(
|
||||
config: &CrateConfig,
|
||||
port: u16,
|
||||
watcher_ip: String,
|
||||
hot_reload_tx: Sender<Template<'static>>,
|
||||
file_map: Arc<Mutex<FileMap<HtmlCtx>>>,
|
||||
build_manager: Arc<BuildManager>,
|
||||
) -> 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_manager.rebuild() {
|
||||
Ok(res) => {
|
||||
print_console_info(
|
||||
&watcher_ip,
|
||||
port,
|
||||
&config,
|
||||
PrettierOptions {
|
||||
changed: evt.paths,
|
||||
warnings: res.warnings,
|
||||
elapsed_time: res.elapsed_time,
|
||||
},
|
||||
);
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!("{}", err);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
// find changes to the rsx in the file
|
||||
let mut map = file_map.lock().unwrap();
|
||||
|
||||
match map.update_rsx(&path, &config.crate_dir) {
|
||||
Ok(UpdateResult::UpdatedRsx(msgs)) => {
|
||||
messages.extend(msgs);
|
||||
}
|
||||
Ok(UpdateResult::NeedsRebuild) => {
|
||||
match build_manager.rebuild() {
|
||||
Ok(res) => {
|
||||
print_console_info(
|
||||
&watcher_ip,
|
||||
port,
|
||||
&config,
|
||||
PrettierOptions {
|
||||
changed: evt.paths,
|
||||
warnings: res.warnings,
|
||||
elapsed_time: res.elapsed_time,
|
||||
},
|
||||
);
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!("{}", err);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!("{}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
for msg in messages {
|
||||
let _ = hot_reload_tx.send(msg);
|
||||
}
|
||||
}
|
||||
last_update_time = chrono::Local::now().timestamp();
|
||||
}
|
||||
},
|
||||
notify::Config::default(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
for sub_path in allow_watch_path {
|
||||
if let Err(err) = watcher.watch(
|
||||
&config.crate_dir.join(&sub_path),
|
||||
notify::RecursiveMode::Recursive,
|
||||
) {
|
||||
log::error!("error watching {sub_path:?}: \n{}", err);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(watcher)
|
||||
}
|
||||
|
||||
/// Get the network ip
|
||||
fn get_ip() -> Option<String> {
|
||||
let socket = match UdpSocket::bind("0.0.0.0:0") {
|
||||
Ok(s) => s,
|
||||
Err(_) => return None,
|
||||
};
|
||||
|
||||
match socket.connect("8.8.8.8:80") {
|
||||
Ok(()) => (),
|
||||
Err(_) => return None,
|
||||
};
|
||||
|
||||
match socket.local_addr() {
|
||||
Ok(addr) => Some(addr.ip().to_string()),
|
||||
Err(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle websockets
|
||||
async fn ws_handler(
|
||||
ws: WebSocketUpgrade,
|
||||
_: Option<TypedHeader<headers::UserAgent>>,
|
||||
Extension(state): Extension<Arc<WsReloadState>>,
|
||||
) -> impl IntoResponse {
|
||||
ws.on_upgrade(|mut socket| async move {
|
||||
let mut rx = state.update.subscribe();
|
||||
let reload_watcher = tokio::spawn(async move {
|
||||
loop {
|
||||
rx.recv().await.unwrap();
|
||||
// ignore the error
|
||||
if socket
|
||||
.send(Message::Text(String::from("reload")))
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
// flush the errors after recompling
|
||||
rx = rx.resubscribe();
|
||||
}
|
||||
});
|
||||
|
||||
reload_watcher.await.unwrap();
|
||||
})
|
||||
#[derive(Clone)]
|
||||
pub struct HotReloadState {
|
||||
pub messages: broadcast::Sender<Template<'static>>,
|
||||
pub file_map: Arc<Mutex<FileMap<HtmlCtx>>>,
|
||||
}
|
||||
@@ -11,7 +11,17 @@ pub struct PrettierOptions {
|
||||
pub elapsed_time: u128,
|
||||
}
|
||||
|
||||
pub fn print_console_info(ip: &String, port: u16, config: &CrateConfig, options: PrettierOptions) {
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct WebServerInfo {
|
||||
pub ip: String,
|
||||
pub port: u16,
|
||||
}
|
||||
|
||||
pub fn print_console_info(
|
||||
config: &CrateConfig,
|
||||
options: PrettierOptions,
|
||||
web_info: Option<WebServerInfo>,
|
||||
) {
|
||||
if let Ok(native_clearseq) = Command::new(if cfg!(target_os = "windows") {
|
||||
"cls"
|
||||
} else {
|
||||
@@ -70,26 +80,28 @@ pub fn print_console_info(ip: &String, port: u16, config: &CrateConfig, options:
|
||||
);
|
||||
}
|
||||
|
||||
if config.dioxus_config.web.https.enabled == Some(true) {
|
||||
println!(
|
||||
"\t> Local : {}",
|
||||
format!("https://localhost:{}/", port).blue()
|
||||
);
|
||||
println!(
|
||||
"\t> Network : {}",
|
||||
format!("https://{}:{}/", ip, port).blue()
|
||||
);
|
||||
println!("\t> HTTPS : {}", "Enabled".to_string().green());
|
||||
} else {
|
||||
println!(
|
||||
"\t> Local : {}",
|
||||
format!("http://localhost:{}/", port).blue()
|
||||
);
|
||||
println!(
|
||||
"\t> Network : {}",
|
||||
format!("http://{}:{}/", ip, port).blue()
|
||||
);
|
||||
println!("\t> HTTPS : {}", "Disabled".to_string().red());
|
||||
if let Some(WebServerInfo { ip, port }) = web_info {
|
||||
if config.dioxus_config.web.https.enabled == Some(true) {
|
||||
println!(
|
||||
"\t> Local : {}",
|
||||
format!("https://localhost:{}/", port).blue()
|
||||
);
|
||||
println!(
|
||||
"\t> Network : {}",
|
||||
format!("https://{}:{}/", ip, port).blue()
|
||||
);
|
||||
println!("\t> HTTPS : {}", "Enabled".to_string().green());
|
||||
} else {
|
||||
println!(
|
||||
"\t> Local : {}",
|
||||
format!("http://localhost:{}/", port).blue()
|
||||
);
|
||||
println!(
|
||||
"\t> Network : {}",
|
||||
format!("http://{}:{}/", ip, port).blue()
|
||||
);
|
||||
println!("\t> HTTPS : {}", "Disabled".to_string().red());
|
||||
}
|
||||
}
|
||||
println!();
|
||||
println!("\t> Profile : {}", profile.green());
|
||||
|
||||
@@ -1,29 +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 super::BuildManager;
|
||||
use crate::CrateConfig;
|
||||
|
||||
pub struct HotReloadState {
|
||||
pub messages: broadcast::Sender<Template<'static>>,
|
||||
pub build_manager: Arc<BuildManager>,
|
||||
pub file_map: Arc<Mutex<FileMap<HtmlCtx>>>,
|
||||
pub watcher_config: CrateConfig,
|
||||
}
|
||||
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");
|
||||
@@ -0,0 +1,430 @@
|
||||
use crate::{
|
||||
builder,
|
||||
serve::Serve,
|
||||
server::{
|
||||
output::{print_console_info, PrettierOptions, WebServerInfo},
|
||||
setup_file_watcher, HotReloadState,
|
||||
},
|
||||
BuildResult, CrateConfig, Result, WebHttpsConfig,
|
||||
};
|
||||
use axum::{
|
||||
body::{Full, HttpBody},
|
||||
extract::{ws::Message, Extension, TypedHeader, WebSocketUpgrade},
|
||||
http::{
|
||||
header::{HeaderName, HeaderValue},
|
||||
Method, Response, StatusCode,
|
||||
},
|
||||
response::IntoResponse,
|
||||
routing::{get, get_service},
|
||||
Router,
|
||||
};
|
||||
use axum_server::tls_rustls::RustlsConfig;
|
||||
|
||||
use dioxus_html::HtmlCtx;
|
||||
use dioxus_rsx::hot_reload::*;
|
||||
use std::{
|
||||
net::UdpSocket,
|
||||
process::Command,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
use tokio::sync::broadcast::{self, Sender};
|
||||
use tower::ServiceBuilder;
|
||||
use tower_http::services::fs::{ServeDir, ServeFileSystemResponseBody};
|
||||
use tower_http::{
|
||||
cors::{Any, CorsLayer},
|
||||
ServiceBuilderExt,
|
||||
};
|
||||
|
||||
#[cfg(feature = "plugin")]
|
||||
use plugin::PluginManager;
|
||||
|
||||
mod proxy;
|
||||
|
||||
mod hot_reload;
|
||||
use hot_reload::*;
|
||||
|
||||
struct WsReloadState {
|
||||
update: broadcast::Sender<()>,
|
||||
}
|
||||
|
||||
pub async fn startup(port: u16, config: CrateConfig, start_browser: bool) -> Result<()> {
|
||||
// ctrl-c shutdown checker
|
||||
let _crate_config = config.clone();
|
||||
let _ = ctrlc::set_handler(move || {
|
||||
#[cfg(feature = "plugin")]
|
||||
let _ = PluginManager::on_serve_shutdown(&_crate_config);
|
||||
std::process::exit(0);
|
||||
});
|
||||
|
||||
let ip = get_ip().unwrap_or(String::from("0.0.0.0"));
|
||||
|
||||
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(
|
||||
ip: String,
|
||||
port: u16,
|
||||
config: CrateConfig,
|
||||
start_browser: bool,
|
||||
hot_reload_state: Option<HotReloadState>,
|
||||
) -> Result<()> {
|
||||
let first_build_result = crate::builder::build(&config, true)?;
|
||||
|
||||
log::info!("🚀 Starting development server...");
|
||||
|
||||
// WS Reload Watching
|
||||
let (reload_tx, _) = broadcast::channel(100);
|
||||
|
||||
// We got to own watcher so that it exists for the duration of serve
|
||||
// Otherwise full reload won't work.
|
||||
let _watcher = setup_file_watcher(
|
||||
{
|
||||
let config = config.clone();
|
||||
let reload_tx = reload_tx.clone();
|
||||
move || build(&config, &reload_tx)
|
||||
},
|
||||
&config,
|
||||
Some(WebServerInfo {
|
||||
ip: ip.clone(),
|
||||
port,
|
||||
}),
|
||||
hot_reload_state.clone(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let ws_reload_state = Arc::new(WsReloadState {
|
||||
update: reload_tx.clone(),
|
||||
});
|
||||
|
||||
// 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(crate::server::output::WebServerInfo {
|
||||
ip: ip.clone(),
|
||||
port,
|
||||
}),
|
||||
);
|
||||
|
||||
// Router
|
||||
let router = setup_router(config, ws_reload_state, hot_reload_state).await?;
|
||||
|
||||
// Start server
|
||||
start_server(port, router, start_browser, rustls_config).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
const DEFAULT_KEY_PATH: &str = "ssl/key.pem";
|
||||
const DEFAULT_CERT_PATH: &str = "ssl/cert.pem";
|
||||
|
||||
/// Returns an enum of rustls config and a bool if mkcert isn't installed
|
||||
async fn get_rustls(config: &CrateConfig) -> Result<Option<RustlsConfig>> {
|
||||
let web_config = &config.dioxus_config.web.https;
|
||||
if web_config.enabled != Some(true) {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let (cert_path, key_path) = if let Some(true) = web_config.mkcert {
|
||||
// mkcert, use it
|
||||
get_rustls_with_mkcert(web_config)?
|
||||
} else {
|
||||
// if mkcert not specified or false, don't use it
|
||||
get_rustls_without_mkcert(web_config)?
|
||||
};
|
||||
|
||||
Ok(Some(
|
||||
RustlsConfig::from_pem_file(cert_path, key_path).await?,
|
||||
))
|
||||
}
|
||||
|
||||
fn get_rustls_with_mkcert(web_config: &WebHttpsConfig) -> Result<(String, String)> {
|
||||
// Get paths to store certs, otherwise use ssl/item.pem
|
||||
let key_path = web_config
|
||||
.key_path
|
||||
.clone()
|
||||
.unwrap_or(DEFAULT_KEY_PATH.to_string());
|
||||
|
||||
let cert_path = web_config
|
||||
.cert_path
|
||||
.clone()
|
||||
.unwrap_or(DEFAULT_CERT_PATH.to_string());
|
||||
|
||||
// Create ssl directory if using defaults
|
||||
if key_path == DEFAULT_KEY_PATH && cert_path == DEFAULT_CERT_PATH {
|
||||
_ = fs::create_dir("ssl");
|
||||
}
|
||||
|
||||
let cmd = Command::new("mkcert")
|
||||
.args([
|
||||
"-install",
|
||||
"-key-file",
|
||||
&key_path,
|
||||
"-cert-file",
|
||||
&cert_path,
|
||||
"localhost",
|
||||
"::1",
|
||||
"127.0.0.1",
|
||||
])
|
||||
.spawn();
|
||||
|
||||
match cmd {
|
||||
Err(e) => {
|
||||
match e.kind() {
|
||||
io::ErrorKind::NotFound => log::error!("mkcert is not installed. See https://github.com/FiloSottile/mkcert#installation for installation instructions."),
|
||||
e => log::error!("an error occured while generating mkcert certificates: {}", e.to_string()),
|
||||
};
|
||||
return Err("failed to generate mkcert certificates".into());
|
||||
}
|
||||
Ok(mut cmd) => {
|
||||
cmd.wait()?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok((cert_path, key_path))
|
||||
}
|
||||
|
||||
fn get_rustls_without_mkcert(web_config: &WebHttpsConfig) -> Result<(String, String)> {
|
||||
// get paths to cert & key
|
||||
if let (Some(key), Some(cert)) = (web_config.key_path.clone(), web_config.cert_path.clone()) {
|
||||
Ok((cert, key))
|
||||
} else {
|
||||
// missing cert or key
|
||||
Err("https is enabled but cert or key path is missing".into())
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets up and returns a router
|
||||
async fn setup_router(
|
||||
config: CrateConfig,
|
||||
ws_reload: Arc<WsReloadState>,
|
||||
hot_reload: Option<HotReloadState>,
|
||||
) -> Result<Router> {
|
||||
// Setup cors
|
||||
let cors = CorsLayer::new()
|
||||
// allow `GET` and `POST` when accessing the resource
|
||||
.allow_methods([Method::GET, Method::POST])
|
||||
// allow requests from any origin
|
||||
.allow_origin(Any)
|
||||
.allow_headers(Any);
|
||||
|
||||
let (coep, coop) = if config.cross_origin_policy {
|
||||
(
|
||||
HeaderValue::from_static("require-corp"),
|
||||
HeaderValue::from_static("same-origin"),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
HeaderValue::from_static("unsafe-none"),
|
||||
HeaderValue::from_static("unsafe-none"),
|
||||
)
|
||||
};
|
||||
|
||||
// Create file service
|
||||
let file_service_config = config.clone();
|
||||
let file_service = ServiceBuilder::new()
|
||||
.override_response_header(
|
||||
HeaderName::from_static("cross-origin-embedder-policy"),
|
||||
coep,
|
||||
)
|
||||
.override_response_header(HeaderName::from_static("cross-origin-opener-policy"), coop)
|
||||
.and_then(
|
||||
move |response: Response<ServeFileSystemResponseBody>| async move {
|
||||
let response = if file_service_config
|
||||
.dioxus_config
|
||||
.web
|
||||
.watcher
|
||||
.index_on_404
|
||||
.unwrap_or(false)
|
||||
&& response.status() == StatusCode::NOT_FOUND
|
||||
{
|
||||
let body = Full::from(
|
||||
// TODO: Cache/memoize this.
|
||||
std::fs::read_to_string(
|
||||
file_service_config
|
||||
.crate_dir
|
||||
.join(file_service_config.out_dir)
|
||||
.join("index.html"),
|
||||
)
|
||||
.ok()
|
||||
.unwrap(),
|
||||
)
|
||||
.map_err(|err| match err {})
|
||||
.boxed();
|
||||
Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
.body(body)
|
||||
.unwrap()
|
||||
} else {
|
||||
response.map(|body| body.boxed())
|
||||
};
|
||||
Ok(response)
|
||||
},
|
||||
)
|
||||
.service(ServeDir::new(config.crate_dir.join(&config.out_dir)));
|
||||
|
||||
// Setup websocket
|
||||
let mut router = Router::new().route("/_dioxus/ws", get(ws_handler));
|
||||
|
||||
// Setup proxy
|
||||
for proxy_config in config.dioxus_config.web.proxy.unwrap_or_default() {
|
||||
router = proxy::add_proxy(router, &proxy_config)?;
|
||||
}
|
||||
|
||||
// Route file service
|
||||
router = router.fallback(get_service(file_service).handle_error(
|
||||
|error: std::io::Error| async move {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Unhandled internal error: {}", error),
|
||||
)
|
||||
},
|
||||
));
|
||||
|
||||
// Setup routes
|
||||
router = router
|
||||
.route("/_dioxus/hot_reload", get(hot_reload_handler))
|
||||
.layer(cors)
|
||||
.layer(Extension(ws_reload));
|
||||
|
||||
if let Some(hot_reload) = hot_reload {
|
||||
router = router.layer(Extension(hot_reload))
|
||||
}
|
||||
|
||||
Ok(router)
|
||||
}
|
||||
|
||||
/// Starts dx serve with no hot reload
|
||||
async fn start_server(
|
||||
port: u16,
|
||||
router: Router,
|
||||
start_browser: bool,
|
||||
rustls: Option<RustlsConfig>,
|
||||
) -> Result<()> {
|
||||
// If plugins, call on_serve_start event
|
||||
#[cfg(feature = "plugin")]
|
||||
PluginManager::on_serve_start(&config)?;
|
||||
|
||||
// Parse address
|
||||
let addr = format!("0.0.0.0:{}", port).parse().unwrap();
|
||||
|
||||
// Open the browser
|
||||
if start_browser {
|
||||
match rustls {
|
||||
Some(_) => _ = open::that(format!("https://{}", addr)),
|
||||
None => _ = open::that(format!("http://{}", addr)),
|
||||
}
|
||||
}
|
||||
|
||||
// Start the server with or without rustls
|
||||
match rustls {
|
||||
Some(rustls) => {
|
||||
axum_server::bind_rustls(addr, rustls)
|
||||
.serve(router.into_make_service())
|
||||
.await?
|
||||
}
|
||||
None => {
|
||||
axum::Server::bind(&addr)
|
||||
.serve(router.into_make_service())
|
||||
.await?
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the network ip
|
||||
fn get_ip() -> Option<String> {
|
||||
let socket = match UdpSocket::bind("0.0.0.0:0") {
|
||||
Ok(s) => s,
|
||||
Err(_) => return None,
|
||||
};
|
||||
|
||||
match socket.connect("8.8.8.8:80") {
|
||||
Ok(()) => (),
|
||||
Err(_) => return None,
|
||||
};
|
||||
|
||||
match socket.local_addr() {
|
||||
Ok(addr) => Some(addr.ip().to_string()),
|
||||
Err(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle websockets
|
||||
async fn ws_handler(
|
||||
ws: WebSocketUpgrade,
|
||||
_: Option<TypedHeader<headers::UserAgent>>,
|
||||
Extension(state): Extension<Arc<WsReloadState>>,
|
||||
) -> impl IntoResponse {
|
||||
ws.on_upgrade(|mut socket| async move {
|
||||
let mut rx = state.update.subscribe();
|
||||
let reload_watcher = tokio::spawn(async move {
|
||||
loop {
|
||||
rx.recv().await.unwrap();
|
||||
// ignore the error
|
||||
if socket
|
||||
.send(Message::Text(String::from("reload")))
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
// flush the errors after recompling
|
||||
rx = rx.resubscribe();
|
||||
}
|
||||
});
|
||||
|
||||
reload_watcher.await.unwrap();
|
||||
})
|
||||
}
|
||||
|
||||
fn build(config: &CrateConfig, reload_tx: &Sender<()>) -> Result<BuildResult> {
|
||||
let result = builder::build(config, true)?;
|
||||
// change the websocket reload state to true;
|
||||
// the page will auto-reload.
|
||||
if config
|
||||
.dioxus_config
|
||||
.web
|
||||
.watcher
|
||||
.reload_html
|
||||
.unwrap_or(false)
|
||||
{
|
||||
let _ = Serve::regen_dev_page(config);
|
||||
}
|
||||
let _ = reload_tx.send(());
|
||||
Ok(result)
|
||||
}
|
||||
@@ -48,6 +48,16 @@ impl ProxyClient {
|
||||
pub fn add_proxy(mut router: Router, proxy: &WebProxyConfig) -> Result<Router> {
|
||||
let url: Uri = proxy.backend.parse()?;
|
||||
let path = url.path().to_string();
|
||||
let trimmed_path = path.trim_end_matches('/');
|
||||
|
||||
if trimmed_path.is_empty() {
|
||||
return Err(crate::Error::ProxySetupError(format!(
|
||||
"Proxy backend URL must have a non-empty path, e.g. {}/api instead of {}",
|
||||
proxy.backend.trim_end_matches('/'),
|
||||
proxy.backend
|
||||
)));
|
||||
}
|
||||
|
||||
let client = ProxyClient::new(url);
|
||||
|
||||
// We also match everything after the path using a wildcard matcher.
|
||||
@@ -56,7 +66,7 @@ pub fn add_proxy(mut router: Router, proxy: &WebProxyConfig) -> Result<Router> {
|
||||
router = router.route(
|
||||
// Always remove trailing /'s so that the exact route
|
||||
// matches.
|
||||
path.trim_end_matches('/'),
|
||||
trimmed_path,
|
||||
any(move |req| async move {
|
||||
client
|
||||
.send(req)
|
||||
@@ -68,7 +78,7 @@ pub fn add_proxy(mut router: Router, proxy: &WebProxyConfig) -> Result<Router> {
|
||||
// Wildcard match anything else _after_ the backend URL's path.
|
||||
// Note that we know `path` ends with a trailing `/` in this branch,
|
||||
// so `wildcard` will look like `http://localhost/api/*proxywildcard`.
|
||||
let wildcard = format!("{}/*proxywildcard", path.trim_end_matches('/'));
|
||||
let wildcard = format!("{}/*proxywildcard", trimmed_path);
|
||||
router = router.route(
|
||||
&wildcard,
|
||||
any(move |req| async move {
|
||||
@@ -168,4 +178,21 @@ mod test {
|
||||
async fn add_proxy_trailing_slash() {
|
||||
test_proxy_requests("/api/".to_string()).await;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_proxy_empty_path() {
|
||||
let config = WebProxyConfig {
|
||||
backend: "http://localhost:8000".to_string(),
|
||||
};
|
||||
let router = super::add_proxy(Router::new(), &config);
|
||||
match router.unwrap_err() {
|
||||
crate::Error::ProxySetupError(e) => {
|
||||
assert_eq!(
|
||||
e,
|
||||
"Proxy backend URL must have a non-empty path, e.g. http://localhost:8000/api instead of http://localhost:8000"
|
||||
);
|
||||
}
|
||||
e => panic!("Unexpected error type: {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
+68
-11
@@ -328,22 +328,79 @@ pub fn extract_zip(file: &Path, target: &Path) -> anyhow::Result<()> {
|
||||
}
|
||||
|
||||
for i in 0..zip.len() {
|
||||
let mut file = zip.by_index(i)?;
|
||||
if file.is_dir() {
|
||||
// dir
|
||||
let target = target.join(Path::new(&file.name().replace('\\', "")));
|
||||
std::fs::create_dir_all(target)?;
|
||||
let mut zip_entry = zip.by_index(i)?;
|
||||
|
||||
// check for dangerous paths
|
||||
// see https://docs.rs/zip/latest/zip/read/struct.ZipFile.html#warnings
|
||||
let Some(enclosed_name) = zip_entry.enclosed_name() else {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Refusing to unpack zip entry with potentially dangerous path: zip={} entry={:?}",
|
||||
file.display(),
|
||||
zip_entry.name()
|
||||
));
|
||||
};
|
||||
|
||||
let output_path = target.join(enclosed_name);
|
||||
if zip_entry.is_dir() {
|
||||
std::fs::create_dir_all(output_path)?;
|
||||
} else {
|
||||
// file
|
||||
let file_path = target.join(Path::new(file.name()));
|
||||
let mut target_file = if !file_path.exists() {
|
||||
std::fs::File::create(file_path)?
|
||||
// create parent dirs if needed
|
||||
if let Some(parent) = output_path.parent() {
|
||||
std::fs::create_dir_all(parent)?;
|
||||
}
|
||||
|
||||
// extract file
|
||||
let mut target_file = if !output_path.exists() {
|
||||
std::fs::File::create(output_path)?
|
||||
} else {
|
||||
std::fs::File::open(file_path)?
|
||||
std::fs::File::open(output_path)?
|
||||
};
|
||||
let _num = std::io::copy(&mut file, &mut target_file)?;
|
||||
let _num = std::io::copy(&mut zip_entry, &mut target_file)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use std::path::PathBuf;
|
||||
use tempfile::tempdir;
|
||||
|
||||
#[test]
|
||||
fn test_extract_zip() -> anyhow::Result<()> {
|
||||
let path = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap())
|
||||
.join("tests/fixtures/test.zip");
|
||||
let temp_dir = tempdir()?;
|
||||
let temp_path = temp_dir.path();
|
||||
|
||||
extract_zip(path.as_path(), temp_path)?;
|
||||
|
||||
let expected_files = vec!["file1.txt", "file2.txt", "dir/file3.txt"];
|
||||
for file in expected_files {
|
||||
let path = temp_path.join(file);
|
||||
assert!(path.exists(), "File not found: {:?}", path);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_zip_dangerous_path() -> anyhow::Result<()> {
|
||||
let path = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap())
|
||||
.join("tests/fixtures/dangerous.zip");
|
||||
let temp_dir = tempdir()?;
|
||||
let temp_path = temp_dir.path();
|
||||
|
||||
let result = extract_zip(path.as_path(), temp_path);
|
||||
|
||||
let err = result.unwrap_err();
|
||||
assert!(err
|
||||
.to_string()
|
||||
.contains("Refusing to unpack zip entry with potentially dangerous path: zip="));
|
||||
assert!(err.to_string().contains("entry=\"/etc/passwd\""));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
@@ -1,10 +1,10 @@
|
||||
[package]
|
||||
name = "dioxus-core-macro"
|
||||
version = "0.3.0"
|
||||
version = { workspace = true }
|
||||
authors = ["Jonathan Kelley"]
|
||||
edition = "2021"
|
||||
description = "Core macro for Dioxus Virtual DOM"
|
||||
license = "MIT/Apache-2.0"
|
||||
license = "MIT OR Apache-2.0"
|
||||
repository = "https://github.com/DioxusLabs/dioxus/"
|
||||
homepage = "https://dioxuslabs.com"
|
||||
keywords = ["dom", "ui", "gui", "react"]
|
||||
@@ -15,8 +15,10 @@ proc-macro = true
|
||||
[dependencies]
|
||||
proc-macro2 = { version = "1.0" }
|
||||
quote = "1.0"
|
||||
syn = { version = "1.0", features = ["full", "extra-traits"] }
|
||||
syn = { version = "2.0", features = ["full", "extra-traits"] }
|
||||
dioxus-rsx = { workspace = true }
|
||||
dioxus-core = { workspace = true }
|
||||
constcat = "0.3.0"
|
||||
|
||||
# testing
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -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,22 +7,18 @@
|
||||
|
||||
[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:
|
||||
@@ -32,13 +27,13 @@
|
||||
- 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
|
||||
|
||||
|
||||
## 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
|
||||
|
||||
@@ -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,195 @@
|
||||
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: Self::get_function(component_body),
|
||||
props_struct: Self::get_props_struct(component_body),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl InlinePropsDeserializerArgs {
|
||||
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_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(t) => Some(&t.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
|
||||
};
|
||||
|
||||
parse_quote! {
|
||||
#(#fn_attrs)*
|
||||
#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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
//! This module contains all [`ComponentBody`](crate::component_body::ComponentBody) deserializers.
|
||||
|
||||
pub mod component;
|
||||
pub mod inline_props;
|
||||
@@ -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: LifetimeDef = 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
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,18 @@
|
||||
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 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 +24,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 +43,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 +61,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 +79,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(),
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -166,8 +166,9 @@ mod field_info {
|
||||
use crate::props::type_from_inside_option;
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::quote;
|
||||
use syn::parse::Error;
|
||||
use syn::spanned::Spanned;
|
||||
use syn::Expr;
|
||||
use syn::{parse::Error, punctuated::Punctuated};
|
||||
|
||||
use super::util::{
|
||||
expr_to_single_string, ident_to_type, path_to_single_string, strip_raw_ident_prefix,
|
||||
@@ -193,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
|
||||
@@ -203,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 {
|
||||
@@ -212,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,
|
||||
@@ -260,31 +263,32 @@ mod field_info {
|
||||
pub fn with(mut self, attrs: &[syn::Attribute]) -> Result<Self, Error> {
|
||||
let mut skip_tokens = None;
|
||||
for attr in attrs {
|
||||
if path_to_single_string(&attr.path).as_deref() != Some("props") {
|
||||
if path_to_single_string(attr.path()).as_deref() != Some("props") {
|
||||
continue;
|
||||
}
|
||||
|
||||
if attr.tokens.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let as_expr: syn::Expr = syn::parse2(attr.tokens.clone())?;
|
||||
match as_expr {
|
||||
syn::Expr::Paren(body) => {
|
||||
self.apply_meta(*body.expr)?;
|
||||
}
|
||||
syn::Expr::Tuple(body) => {
|
||||
for expr in body.elems.into_iter() {
|
||||
self.apply_meta(expr)?;
|
||||
match &attr.meta {
|
||||
syn::Meta::List(list) => {
|
||||
if list.tokens.is_empty() {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
return Err(Error::new_spanned(attr.tokens.clone(), "Expected (<...>)"));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
let as_expr = attr.parse_args_with(
|
||||
Punctuated::<Expr, syn::Token![,]>::parse_separated_nonempty,
|
||||
)?;
|
||||
|
||||
for expr in as_expr.into_iter() {
|
||||
self.apply_meta(expr)?;
|
||||
}
|
||||
|
||||
// Stash its span for later (we don’t yet know if it’ll be an error)
|
||||
if self.skip && skip_tokens.is_none() {
|
||||
skip_tokens = Some(attr.tokens.clone());
|
||||
skip_tokens = Some(attr.meta.clone());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -343,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(())
|
||||
}
|
||||
@@ -461,6 +469,8 @@ mod struct_info {
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::quote;
|
||||
use syn::parse::Error;
|
||||
use syn::punctuated::Punctuated;
|
||||
use syn::Expr;
|
||||
|
||||
use super::field_info::{FieldBuilderAttr, FieldInfo};
|
||||
use super::util::{
|
||||
@@ -501,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()),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -547,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!()
|
||||
@@ -630,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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -647,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;
|
||||
@@ -789,7 +796,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 {
|
||||
@@ -807,7 +814,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}");
|
||||
|
||||
@@ -926,7 +933,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}");
|
||||
|
||||
@@ -1082,28 +1089,28 @@ Finally, call `.build()` to create the instance of `{name}`.
|
||||
pub fn new(attrs: &[syn::Attribute]) -> Result<TypeBuilderAttr, Error> {
|
||||
let mut result = TypeBuilderAttr::default();
|
||||
for attr in attrs {
|
||||
if path_to_single_string(&attr.path).as_deref() != Some("builder") {
|
||||
if path_to_single_string(attr.path()).as_deref() != Some("builder") {
|
||||
continue;
|
||||
}
|
||||
|
||||
if attr.tokens.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let as_expr: syn::Expr = syn::parse2(attr.tokens.clone())?;
|
||||
|
||||
match as_expr {
|
||||
syn::Expr::Paren(body) => {
|
||||
result.apply_meta(*body.expr)?;
|
||||
}
|
||||
syn::Expr::Tuple(body) => {
|
||||
for expr in body.elems.into_iter() {
|
||||
result.apply_meta(expr)?;
|
||||
match &attr.meta {
|
||||
syn::Meta::List(list) => {
|
||||
if list.tokens.is_empty() {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
return Err(Error::new_spanned(attr.tokens.clone(), "Expected (<...>)"));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
let as_expr = attr.parse_args_with(
|
||||
Punctuated::<Expr, syn::Token![,]>::parse_separated_nonempty,
|
||||
)?;
|
||||
|
||||
for expr in as_expr.into_iter() {
|
||||
result.apply_meta(expr)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
[package]
|
||||
name = "dioxus-core"
|
||||
version = "0.3.3"
|
||||
version = { workspace = true }
|
||||
authors = ["Jonathan Kelley"]
|
||||
edition = "2018"
|
||||
description = "Core functionality for Dioxus - a concurrent renderer-agnostic Virtual DOM for interactive user experiences"
|
||||
license = "MIT/Apache-2.0"
|
||||
license = "MIT OR Apache-2.0"
|
||||
repository = "https://github.com/DioxusLabs/dioxus/"
|
||||
homepage = "https://dioxuslabs.com"
|
||||
keywords = ["dom", "ui", "gui", "react"]
|
||||
@@ -28,19 +28,18 @@ slab = { workspace = true }
|
||||
futures-channel = { workspace = true }
|
||||
|
||||
smallbox = "0.8.1"
|
||||
log = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
|
||||
# Serialize the Edits for use in Webview/Liveview instances
|
||||
serde = { version = "1", features = ["derive"], optional = true }
|
||||
|
||||
bumpslab = { version = "0.2.0" }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { workspace = true, features = ["full"] }
|
||||
dioxus = { workspace = true }
|
||||
pretty_assertions = "1.3.0"
|
||||
rand = "0.8.5"
|
||||
dioxus-ssr = { workspace = true }
|
||||
trybuild = "1.0"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
use dioxus::prelude::*;
|
||||
|
||||
fn main() {}
|
||||
|
||||
fn app(cx: Scope) -> Element {
|
||||
let count: &RefCell<Vec<Element>> = cx.use_hook(|| RefCell::new(Vec::new()));
|
||||
|
||||
render! {
|
||||
unsafe_child_component {
|
||||
borrowed: count
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Props)]
|
||||
struct Testing<'a> {
|
||||
borrowed: &'a RefCell<Vec<Element<'a>>>,
|
||||
}
|
||||
|
||||
fn unsafe_child_component<'a>(cx: Scope<'a, Testing<'a>>) -> Element<'a> {
|
||||
let Testing { borrowed } = cx.props;
|
||||
let borrowed_temporary_data =
|
||||
cx.use_hook(|| String::from("This data is only valid for the lifetime of the child"));
|
||||
|
||||
borrowed
|
||||
.borrow_mut()
|
||||
.push(render! {"{borrowed_temporary_data}"});
|
||||
|
||||
cx.render(rsx! {
|
||||
div { "Hello, world!" }
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
error[E0521]: borrowed data escapes outside of function
|
||||
--> compile_tests/props_safety.rs:8:5
|
||||
|
|
||||
5 | fn app(cx: Scope) -> Element {
|
||||
| --
|
||||
| |
|
||||
| `cx` is a reference that is only valid in the function body
|
||||
| has type `&'1 Scoped<'1>`
|
||||
...
|
||||
8 | / render! {
|
||||
9 | | unsafe_child_component {
|
||||
10 | | borrowed: count
|
||||
11 | | }
|
||||
12 | | }
|
||||
| | ^
|
||||
| | |
|
||||
| |_____`cx` escapes the function body here
|
||||
| argument requires that `'1` must outlive `'static`
|
||||
|
|
||||
= note: this error originates in the macro `render` (in Nightly builds, run with -Z macro-backtrace for more info)
|
||||
@@ -0,0 +1,24 @@
|
||||
use dioxus::prelude::*;
|
||||
|
||||
fn main() {}
|
||||
|
||||
fn app(cx: Scope) -> Element {
|
||||
let count = vec![1, 2, 3];
|
||||
|
||||
render! {
|
||||
unsafe_child_component {
|
||||
borrowed: &count
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Props)]
|
||||
struct Testing<'a> {
|
||||
borrowed: &'a Vec<u32>,
|
||||
}
|
||||
|
||||
fn unsafe_child_component<'a>(cx: Scope<'a, Testing<'a>>) -> Element<'a> {
|
||||
cx.render(rsx! {
|
||||
div { "{cx.props.borrowed:?}" }
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
error[E0515]: cannot return value referencing local variable `count`
|
||||
--> compile_tests/props_safety_temporary_values.rs:8:5
|
||||
|
|
||||
8 | / render! {
|
||||
9 | | unsafe_child_component {
|
||||
10 | | borrowed: &count
|
||||
| | ------ `count` is borrowed here
|
||||
11 | | }
|
||||
12 | | }
|
||||
| |_____^ returns a value referencing data owned by the current function
|
||||
|
|
||||
= note: this error originates in the macro `render` (in Nightly builds, run with -Z macro-backtrace for more info)
|
||||
@@ -65,7 +65,12 @@ unsafe impl<'a, P> AnyProps<'a> for VProps<'a, P> {
|
||||
|
||||
match res {
|
||||
Ok(Some(e)) => RenderReturn::Ready(e),
|
||||
_ => RenderReturn::default(),
|
||||
Ok(None) => RenderReturn::default(),
|
||||
Err(err) => {
|
||||
let component_name = cx.name();
|
||||
tracing::error!("Error while rendering component `{component_name}`: {err:?}");
|
||||
RenderReturn::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,9 @@ pub(crate) struct ElementRef {
|
||||
|
||||
// The actual template
|
||||
pub template: Option<NonNull<VNode<'static>>>,
|
||||
|
||||
// The scope the element belongs to
|
||||
pub scope: ScopeId,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
@@ -32,6 +35,7 @@ impl ElementRef {
|
||||
Self {
|
||||
template: None,
|
||||
path: ElementPath::Root(0),
|
||||
scope: ScopeId::ROOT,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -56,11 +60,13 @@ impl VirtualDom {
|
||||
fn next_reference(&mut self, template: &VNode, path: ElementPath) -> ElementId {
|
||||
let entry = self.elements.vacant_entry();
|
||||
let id = entry.key();
|
||||
let scope = self.runtime.current_scope_id().unwrap_or(ScopeId::ROOT);
|
||||
|
||||
entry.insert(ElementRef {
|
||||
// We know this is non-null because it comes from a reference
|
||||
template: Some(unsafe { NonNull::new_unchecked(template as *const _ as *mut _) }),
|
||||
path,
|
||||
scope,
|
||||
});
|
||||
ElementId(id)
|
||||
}
|
||||
@@ -91,32 +97,35 @@ impl VirtualDom {
|
||||
// Note: This will not remove any ids from the arena
|
||||
pub(crate) fn drop_scope(&mut self, id: ScopeId, recursive: bool) {
|
||||
self.dirty_scopes.remove(&DirtyScope {
|
||||
height: self.scopes[id].height,
|
||||
height: self.scopes[id.0].height(),
|
||||
id,
|
||||
});
|
||||
|
||||
self.ensure_drop_safety(id);
|
||||
|
||||
if recursive {
|
||||
if let Some(root) = self.scopes[id].try_root_node() {
|
||||
if let Some(root) = self.scopes[id.0].try_root_node() {
|
||||
if let RenderReturn::Ready(node) = unsafe { root.extend_lifetime_ref() } {
|
||||
self.drop_scope_inner(node)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let scope = &mut self.scopes[id];
|
||||
let scope = &mut self.scopes[id.0];
|
||||
|
||||
// Drop all the hooks once the children are dropped
|
||||
// this means we'll drop hooks bottom-up
|
||||
scope.hooks.get_mut().clear();
|
||||
{
|
||||
let context = scope.context();
|
||||
|
||||
// Drop all the futures once the hooks are dropped
|
||||
for task_id in scope.spawned_tasks.borrow_mut().drain() {
|
||||
scope.tasks.remove(task_id);
|
||||
// Drop all the futures once the hooks are dropped
|
||||
for task_id in context.spawned_tasks.borrow_mut().drain() {
|
||||
context.tasks.remove(task_id);
|
||||
}
|
||||
}
|
||||
|
||||
self.scopes.remove(id);
|
||||
self.scopes.remove(id.0);
|
||||
}
|
||||
|
||||
fn drop_scope_inner(&mut self, node: &VNode) {
|
||||
@@ -137,7 +146,7 @@ impl VirtualDom {
|
||||
|
||||
/// Descend through the tree, removing any borrowed props and listeners
|
||||
pub(crate) fn ensure_drop_safety(&self, scope_id: ScopeId) {
|
||||
let scope = &self.scopes[scope_id];
|
||||
let scope = &self.scopes[scope_id.0];
|
||||
|
||||
// make sure we drop all borrowed props manually to guarantee that their drop implementation is called before we
|
||||
// run the hooks (which hold an &mut Reference)
|
||||
|
||||
@@ -66,10 +66,10 @@ impl<'b> VirtualDom {
|
||||
///
|
||||
/// This method pushes the ScopeID to the internal scopestack and returns the number of nodes created.
|
||||
pub(crate) fn create_scope(&mut self, scope: ScopeId, template: &'b VNode<'b>) -> usize {
|
||||
self.scope_stack.push(scope);
|
||||
let out = self.create(template);
|
||||
self.scope_stack.pop();
|
||||
out
|
||||
self.runtime.scope_stack.borrow_mut().push(scope);
|
||||
let nodes = self.create(template);
|
||||
self.runtime.scope_stack.borrow_mut().pop();
|
||||
nodes
|
||||
}
|
||||
|
||||
/// Create this template and write its mutations
|
||||
@@ -87,8 +87,12 @@ impl<'b> VirtualDom {
|
||||
}
|
||||
}
|
||||
|
||||
// Intialize the root nodes slice
|
||||
*node.root_ids.borrow_mut() = vec![ElementId(0); node.template.get().roots.len()];
|
||||
// Initialize the root nodes slice
|
||||
{
|
||||
let mut nodes_mut = node.root_ids.borrow_mut();
|
||||
let len = node.template.get().roots.len();
|
||||
nodes_mut.resize(len, ElementId::default());
|
||||
};
|
||||
|
||||
// The best renderers will have templates prehydrated and registered
|
||||
// Just in case, let's create the template using instructions anyways
|
||||
@@ -522,7 +526,7 @@ impl<'b> VirtualDom {
|
||||
.take()
|
||||
.map(|props| {
|
||||
let unbounded_props: Box<dyn AnyProps> = unsafe { std::mem::transmute(props) };
|
||||
self.new_scope(unbounded_props, component.name).id
|
||||
self.new_scope(unbounded_props, component.name).context().id
|
||||
})
|
||||
.unwrap_or_else(|| component.scope.get().unwrap())
|
||||
}
|
||||
|
||||
+45
-15
@@ -15,9 +15,8 @@ use DynamicNode::*;
|
||||
|
||||
impl<'b> VirtualDom {
|
||||
pub(super) fn diff_scope(&mut self, scope: ScopeId) {
|
||||
let scope_state = &mut self.scopes[scope];
|
||||
|
||||
self.scope_stack.push(scope);
|
||||
self.runtime.scope_stack.borrow_mut().push(scope);
|
||||
let scope_state = &mut self.get_scope(scope).unwrap();
|
||||
unsafe {
|
||||
// Load the old and new bump arenas
|
||||
let old = scope_state
|
||||
@@ -47,7 +46,7 @@ impl<'b> VirtualDom {
|
||||
(Aborted(l), Ready(r)) => self.replace_placeholder(l, [r]),
|
||||
};
|
||||
}
|
||||
self.scope_stack.pop();
|
||||
self.runtime.scope_stack.borrow_mut().pop();
|
||||
}
|
||||
|
||||
fn diff_ok_to_err(&mut self, l: &'b VNode<'b>, p: &'b VPlaceholder) {
|
||||
@@ -129,7 +128,13 @@ impl<'b> VirtualDom {
|
||||
});
|
||||
|
||||
// Make sure the roots get transferred over while we're here
|
||||
*right_template.root_ids.borrow_mut() = left_template.root_ids.borrow().clone();
|
||||
{
|
||||
let mut right = right_template.root_ids.borrow_mut();
|
||||
right.clear();
|
||||
for &element in left_template.root_ids.borrow().iter() {
|
||||
right.push(element);
|
||||
}
|
||||
}
|
||||
|
||||
let root_ids = right_template.root_ids.borrow();
|
||||
|
||||
@@ -191,7 +196,8 @@ impl<'b> VirtualDom {
|
||||
right.scope.set(Some(scope_id));
|
||||
|
||||
// copy out the box for both
|
||||
let old = self.scopes[scope_id].props.as_ref();
|
||||
let old_scope = &self.scopes[scope_id.0];
|
||||
let old = old_scope.props.as_ref();
|
||||
let new: Box<dyn AnyProps> = right.props.take().unwrap();
|
||||
let new: Box<dyn AnyProps> = unsafe { std::mem::transmute(new) };
|
||||
|
||||
@@ -199,18 +205,23 @@ impl<'b> VirtualDom {
|
||||
// The target scopestate still has the reference to the old props, so there's no need to update anything
|
||||
// This also implicitly drops the new props since they're not used
|
||||
if left.static_props && unsafe { old.as_ref().unwrap().memoize(new.as_ref()) } {
|
||||
tracing::trace!(
|
||||
"Memoized props for component {:#?} ({})",
|
||||
scope_id,
|
||||
old_scope.context().name
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// First, move over the props from the old to the new, dropping old props in the process
|
||||
self.scopes[scope_id].props = Some(new);
|
||||
self.scopes[scope_id.0].props = Some(new);
|
||||
|
||||
// Now run the component and diff it
|
||||
self.run_scope(scope_id);
|
||||
self.diff_scope(scope_id);
|
||||
|
||||
self.dirty_scopes.remove(&DirtyScope {
|
||||
height: self.scopes[scope_id].height,
|
||||
height: self.runtime.get_context(scope_id).unwrap().height,
|
||||
id: scope_id,
|
||||
});
|
||||
}
|
||||
@@ -572,8 +583,7 @@ impl<'b> VirtualDom {
|
||||
}
|
||||
|
||||
// 4. Compute the LIS of this list
|
||||
let mut lis_sequence = Vec::default();
|
||||
lis_sequence.reserve(new_index_to_old_index.len());
|
||||
let mut lis_sequence = Vec::with_capacity(new_index_to_old_index.len());
|
||||
|
||||
let mut predecessors = vec![0; new_index_to_old_index.len()];
|
||||
let mut starts = vec![0; new_index_to_old_index.len()];
|
||||
@@ -714,7 +724,12 @@ impl<'b> VirtualDom {
|
||||
|
||||
Component(comp) => {
|
||||
let scope = comp.scope.get().unwrap();
|
||||
match unsafe { self.scopes[scope].root_node().extend_lifetime_ref() } {
|
||||
match unsafe {
|
||||
self.get_scope(scope)
|
||||
.unwrap()
|
||||
.root_node()
|
||||
.extend_lifetime_ref()
|
||||
} {
|
||||
RenderReturn::Ready(node) => self.push_all_real_nodes(node),
|
||||
RenderReturn::Aborted(_node) => todo!(),
|
||||
}
|
||||
@@ -915,13 +930,18 @@ impl<'b> VirtualDom {
|
||||
.expect("VComponents to always have a scope");
|
||||
|
||||
// Remove the component from the dom
|
||||
match unsafe { self.scopes[scope].root_node().extend_lifetime_ref() } {
|
||||
match unsafe {
|
||||
self.get_scope(scope)
|
||||
.unwrap()
|
||||
.root_node()
|
||||
.extend_lifetime_ref()
|
||||
} {
|
||||
RenderReturn::Ready(t) => self.remove_node(t, gen_muts),
|
||||
RenderReturn::Aborted(placeholder) => self.remove_placeholder(placeholder, gen_muts),
|
||||
};
|
||||
|
||||
// Restore the props back to the vcomponent in case it gets rendered again
|
||||
let props = self.scopes[scope].props.take();
|
||||
let props = self.scopes[scope.0].props.take();
|
||||
*comp.props.borrow_mut() = unsafe { std::mem::transmute(props) };
|
||||
|
||||
// Now drop all the resouces
|
||||
@@ -936,7 +956,12 @@ impl<'b> VirtualDom {
|
||||
Some(Placeholder(t)) => t.id.get().unwrap(),
|
||||
Some(Component(comp)) => {
|
||||
let scope = comp.scope.get().unwrap();
|
||||
match unsafe { self.scopes[scope].root_node().extend_lifetime_ref() } {
|
||||
match unsafe {
|
||||
self.get_scope(scope)
|
||||
.unwrap()
|
||||
.root_node()
|
||||
.extend_lifetime_ref()
|
||||
} {
|
||||
RenderReturn::Ready(t) => self.find_first_element(t),
|
||||
_ => todo!("cannot handle nonstandard nodes"),
|
||||
}
|
||||
@@ -952,7 +977,12 @@ impl<'b> VirtualDom {
|
||||
Some(Placeholder(t)) => t.id.get().unwrap(),
|
||||
Some(Component(comp)) => {
|
||||
let scope = comp.scope.get().unwrap();
|
||||
match unsafe { self.scopes[scope].root_node().extend_lifetime_ref() } {
|
||||
match unsafe {
|
||||
self.get_scope(scope)
|
||||
.unwrap()
|
||||
.root_node()
|
||||
.extend_lifetime_ref()
|
||||
} {
|
||||
RenderReturn::Ready(t) => self.find_last_element(t),
|
||||
_ => todo!("cannot handle nonstandard nodes"),
|
||||
}
|
||||
|
||||
@@ -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! {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::{runtime::with_runtime, ScopeId};
|
||||
use std::{
|
||||
cell::{Cell, RefCell},
|
||||
rc::Rc,
|
||||
@@ -117,7 +118,7 @@ impl<T: std::fmt::Debug> std::fmt::Debug for Event<T> {
|
||||
///
|
||||
/// ```rust, ignore
|
||||
/// rsx!{
|
||||
/// MyComponent { onclick: move |evt| log::info!("clicked") }
|
||||
/// MyComponent { onclick: move |evt| tracing::debug!("clicked") }
|
||||
/// }
|
||||
///
|
||||
/// #[derive(Props)]
|
||||
@@ -135,12 +136,14 @@ impl<T: std::fmt::Debug> std::fmt::Debug for Event<T> {
|
||||
///
|
||||
/// ```
|
||||
pub struct EventHandler<'bump, T = ()> {
|
||||
pub(crate) origin: ScopeId,
|
||||
pub(super) callback: RefCell<Option<ExternalListenerCallback<'bump, T>>>,
|
||||
}
|
||||
|
||||
impl<T> Default for EventHandler<'_, T> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
origin: ScopeId::ROOT,
|
||||
callback: Default::default(),
|
||||
}
|
||||
}
|
||||
@@ -154,7 +157,13 @@ impl<T> EventHandler<'_, T> {
|
||||
/// This borrows the event using a RefCell. Recursively calling a listener will cause a panic.
|
||||
pub fn call(&self, event: T) {
|
||||
if let Some(callback) = self.callback.borrow_mut().as_mut() {
|
||||
with_runtime(|rt| {
|
||||
rt.scope_stack.borrow_mut().push(self.origin);
|
||||
});
|
||||
callback(event);
|
||||
with_runtime(|rt| {
|
||||
rt.scope_stack.borrow_mut().pop();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,8 +14,10 @@ mod lazynodes;
|
||||
mod mutations;
|
||||
mod nodes;
|
||||
mod properties;
|
||||
mod runtime;
|
||||
mod scheduler;
|
||||
mod scope_arena;
|
||||
mod scope_context;
|
||||
mod scopes;
|
||||
mod virtual_dom;
|
||||
|
||||
@@ -30,7 +32,9 @@ pub(crate) mod innerlude {
|
||||
pub use crate::nodes::RenderReturn;
|
||||
pub use crate::nodes::*;
|
||||
pub use crate::properties::*;
|
||||
pub use crate::runtime::{in_runtime, override_runtime, Runtime};
|
||||
pub use crate::scheduler::*;
|
||||
pub use crate::scope_context::*;
|
||||
pub use crate::scopes::*;
|
||||
pub use crate::virtual_dom::*;
|
||||
|
||||
@@ -70,10 +74,11 @@ pub(crate) mod innerlude {
|
||||
}
|
||||
|
||||
pub use crate::innerlude::{
|
||||
fc_to_builder, AnyValue, Attribute, AttributeValue, BorrowedAttributeValue, CapturedError,
|
||||
Component, DynamicNode, Element, ElementId, Event, Fragment, IntoDynNode, LazyNodes, Mutation,
|
||||
Mutations, Properties, RenderReturn, Scope, ScopeId, ScopeState, Scoped, TaskId, Template,
|
||||
TemplateAttribute, TemplateNode, VComponent, VNode, VPlaceholder, VText, VirtualDom,
|
||||
fc_to_builder, vdom_is_rendering, AnyValue, Attribute, AttributeValue, BorrowedAttributeValue,
|
||||
CapturedError, Component, DynamicNode, Element, ElementId, Event, Fragment, IntoDynNode,
|
||||
LazyNodes, Mutation, Mutations, Properties, RenderReturn, Scope, ScopeId, ScopeState, Scoped,
|
||||
TaskId, Template, TemplateAttribute, TemplateNode, VComponent, VNode, VPlaceholder, VText,
|
||||
VirtualDom,
|
||||
};
|
||||
|
||||
/// The purpose of this module is to alleviate imports of many common types
|
||||
@@ -81,9 +86,12 @@ pub use crate::innerlude::{
|
||||
/// This includes types like [`Scope`], [`Element`], and [`Component`].
|
||||
pub mod prelude {
|
||||
pub use crate::innerlude::{
|
||||
fc_to_builder, AnyValue, Component, Element, Event, EventHandler, Fragment,
|
||||
IntoAttributeValue, LazyNodes, Properties, Scope, ScopeId, ScopeState, Scoped, TaskId,
|
||||
Template, TemplateAttribute, TemplateNode, Throw, VNode, VirtualDom,
|
||||
consume_context, consume_context_from_scope, current_scope_id, fc_to_builder, has_context,
|
||||
in_runtime, override_runtime, 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, Runtime, Scope, ScopeId, ScopeState, Scoped,
|
||||
TaskId, Template, TemplateAttribute, TemplateNode, Throw, VNode, VirtualDom,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+69
-17
@@ -54,7 +54,7 @@ pub struct VNode<'a> {
|
||||
|
||||
/// The IDs for the roots of this template - to be used when moving the template around and removing it from
|
||||
/// the actual Dom
|
||||
pub root_ids: RefCell<Vec<ElementId>>,
|
||||
pub root_ids: RefCell<bumpalo::collections::Vec<'a, ElementId>>,
|
||||
|
||||
/// The dynamic parts of the template
|
||||
pub dynamic_nodes: &'a [DynamicNode<'a>],
|
||||
@@ -65,11 +65,11 @@ pub struct VNode<'a> {
|
||||
|
||||
impl<'a> VNode<'a> {
|
||||
/// Create a template with no nodes that will be skipped over during diffing
|
||||
pub fn empty() -> Element<'a> {
|
||||
pub fn empty(cx: &'a ScopeState) -> Element<'a> {
|
||||
Some(VNode {
|
||||
key: None,
|
||||
parent: None,
|
||||
root_ids: Default::default(),
|
||||
root_ids: RefCell::new(bumpalo::collections::Vec::new_in(cx.bump())),
|
||||
dynamic_nodes: &[],
|
||||
dynamic_attrs: &[],
|
||||
template: Cell::new(Template {
|
||||
@@ -312,19 +312,26 @@ pub struct VComponent<'a> {
|
||||
/// Internally, this is used as a guarantee. Externally, this might be incorrect, so don't count on it.
|
||||
///
|
||||
/// This flag is assumed by the [`crate::Properties`] trait which is unsafe to implement
|
||||
pub static_props: bool,
|
||||
pub(crate) static_props: bool,
|
||||
|
||||
/// The assigned Scope for this component
|
||||
pub scope: Cell<Option<ScopeId>>,
|
||||
pub(crate) scope: Cell<Option<ScopeId>>,
|
||||
|
||||
/// The function pointer of the component, known at compile time
|
||||
///
|
||||
/// It is possible that components get folded at comppile time, so these shouldn't be really used as a key
|
||||
pub render_fn: *const (),
|
||||
pub(crate) render_fn: *const (),
|
||||
|
||||
pub(crate) props: RefCell<Option<Box<dyn AnyProps<'a> + 'a>>>,
|
||||
}
|
||||
|
||||
impl<'a> VComponent<'a> {
|
||||
/// Get the scope that this component is mounted to
|
||||
pub fn mounted_scope(&self) -> Option<ScopeId> {
|
||||
self.scope.get()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> std::fmt::Debug for VComponent<'a> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("VComponent")
|
||||
@@ -342,14 +349,36 @@ pub struct VText<'a> {
|
||||
pub value: &'a str,
|
||||
|
||||
/// The ID of this node in the real DOM
|
||||
pub id: Cell<Option<ElementId>>,
|
||||
pub(crate) id: Cell<Option<ElementId>>,
|
||||
}
|
||||
|
||||
impl<'a> VText<'a> {
|
||||
/// Create a new VText
|
||||
pub fn new(value: &'a str) -> Self {
|
||||
Self {
|
||||
value,
|
||||
id: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the mounted ID of this node
|
||||
pub fn mounted_element(&self) -> Option<ElementId> {
|
||||
self.id.get()
|
||||
}
|
||||
}
|
||||
|
||||
/// A placeholder node, used by suspense and fragments
|
||||
#[derive(Debug, Default)]
|
||||
pub struct VPlaceholder {
|
||||
/// The ID of this node in the real DOM
|
||||
pub id: Cell<Option<ElementId>>,
|
||||
pub(crate) id: Cell<Option<ElementId>>,
|
||||
}
|
||||
|
||||
impl VPlaceholder {
|
||||
/// Get the mounted ID of this node
|
||||
pub fn mounted_element(&self) -> Option<ElementId> {
|
||||
self.id.get()
|
||||
}
|
||||
}
|
||||
|
||||
/// An attribute of the TemplateNode, created at compile time
|
||||
@@ -399,11 +428,34 @@ pub struct Attribute<'a> {
|
||||
/// Doesn’t exist in the html spec. Used in Dioxus to denote “style” tags and other attribute groups.
|
||||
pub namespace: Option<&'static str>,
|
||||
|
||||
/// The element in the DOM that this attribute belongs to
|
||||
pub mounted_element: Cell<ElementId>,
|
||||
|
||||
/// An indication of we should always try and set the attribute. Used in controlled components to ensure changes are propagated
|
||||
pub volatile: bool,
|
||||
|
||||
/// The element in the DOM that this attribute belongs to
|
||||
pub(crate) mounted_element: Cell<ElementId>,
|
||||
}
|
||||
|
||||
impl<'a> Attribute<'a> {
|
||||
/// Create a new attribute
|
||||
pub fn new(
|
||||
name: &'a str,
|
||||
value: AttributeValue<'a>,
|
||||
namespace: Option<&'static str>,
|
||||
volatile: bool,
|
||||
) -> Self {
|
||||
Self {
|
||||
name,
|
||||
value,
|
||||
namespace,
|
||||
volatile,
|
||||
mounted_element: Cell::new(ElementId::default()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the element that this attribute is mounted to
|
||||
pub fn mounted_element(&self) -> ElementId {
|
||||
self.mounted_element.get()
|
||||
}
|
||||
}
|
||||
|
||||
/// Any of the built-in values that the Dioxus VirtualDom supports as dynamic attributes on elements
|
||||
@@ -639,14 +691,14 @@ impl<'a> IntoDynNode<'a> for &Element<'a> {
|
||||
|
||||
impl<'a, 'b> IntoDynNode<'a> for LazyNodes<'a, 'b> {
|
||||
fn into_vnode(self, cx: &'a ScopeState) -> DynamicNode<'a> {
|
||||
DynamicNode::Fragment(cx.bump().alloc([self.call(cx)]))
|
||||
DynamicNode::Fragment(cx.bump().alloc([cx.render(self).unwrap()]))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b> IntoDynNode<'b> for &'a str {
|
||||
fn into_vnode(self, cx: &'b ScopeState) -> DynamicNode<'b> {
|
||||
DynamicNode::Text(VText {
|
||||
value: bumpalo::collections::String::from_str_in(self, cx.bump()).into_bump_str(),
|
||||
value: cx.bump().alloc_str(self),
|
||||
id: Default::default(),
|
||||
})
|
||||
}
|
||||
@@ -689,16 +741,16 @@ impl<'a> IntoTemplate<'a> for VNode<'a> {
|
||||
}
|
||||
}
|
||||
impl<'a> IntoTemplate<'a> for Element<'a> {
|
||||
fn into_template(self, _cx: &'a ScopeState) -> VNode<'a> {
|
||||
fn into_template(self, cx: &'a ScopeState) -> VNode<'a> {
|
||||
match self {
|
||||
Some(val) => val.into_template(_cx),
|
||||
_ => VNode::empty().unwrap(),
|
||||
Some(val) => val.into_template(cx),
|
||||
_ => VNode::empty(cx).unwrap(),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<'a, 'b> IntoTemplate<'a> for LazyNodes<'a, 'b> {
|
||||
fn into_template(self, cx: &'a ScopeState) -> VNode<'a> {
|
||||
self.call(cx)
|
||||
cx.render(self).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -73,3 +73,11 @@ impl EmptyBuilder {
|
||||
pub fn fc_to_builder<'a, T: Properties + 'a>(_: fn(Scope<'a, T>) -> Element<'a>) -> T::Builder {
|
||||
T::builder()
|
||||
}
|
||||
|
||||
#[cfg(not(miri))]
|
||||
#[test]
|
||||
fn unsafe_props_fail() {
|
||||
let t = trybuild::TestCases::new();
|
||||
t.compile_fail("compile_tests/props_safety.rs");
|
||||
t.compile_fail("compile_tests/props_safety_temporary_values.rs");
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
use std::cell::{Cell, Ref, RefCell};
|
||||
|
||||
use crate::{innerlude::Scheduler, scope_context::ScopeContext, scopes::ScopeId};
|
||||
use std::rc::Rc;
|
||||
|
||||
thread_local! {
|
||||
static RUNTIMES: RefCell<Vec<Rc<Runtime>>> = RefCell::new(vec![]);
|
||||
}
|
||||
|
||||
/// Run some code within a runtime
|
||||
pub fn in_runtime<R>(runtime: Rc<Runtime>, f: impl FnOnce() -> R) -> R {
|
||||
let _guard = RuntimeGuard::new(runtime);
|
||||
f()
|
||||
}
|
||||
|
||||
/// Override the current 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(|| override_runtime(cx.props.runtime.clone()));
|
||||
///
|
||||
/// render! { div {} }
|
||||
/// }
|
||||
/// ```
|
||||
pub fn override_runtime(runtime: Rc<Runtime>) {
|
||||
RUNTIMES.with(|stack| {
|
||||
let mut stack = stack.borrow_mut();
|
||||
stack.pop();
|
||||
stack.push(runtime);
|
||||
});
|
||||
}
|
||||
|
||||
/// Pushes a new scope onto the stack
|
||||
pub(crate) fn push_runtime(runtime: Rc<Runtime>) {
|
||||
RUNTIMES.with(|stack| stack.borrow_mut().push(runtime));
|
||||
}
|
||||
|
||||
/// Pops a scope off the stack
|
||||
pub(crate) fn pop_runtime() {
|
||||
RUNTIMES.with(|stack| stack.borrow_mut().pop());
|
||||
}
|
||||
|
||||
/// Runs a function with the current runtime
|
||||
pub(crate) fn with_runtime<F, R>(f: F) -> Option<R>
|
||||
where
|
||||
F: FnOnce(&Runtime) -> R,
|
||||
{
|
||||
RUNTIMES.with(|stack| {
|
||||
let stack = stack.borrow();
|
||||
stack.last().map(|r| f(r))
|
||||
})
|
||||
}
|
||||
|
||||
/// Runs a function with the current scope
|
||||
pub(crate) fn with_current_scope<F, R>(f: F) -> Option<R>
|
||||
where
|
||||
F: FnOnce(&ScopeContext) -> R,
|
||||
{
|
||||
with_runtime(|runtime| {
|
||||
runtime
|
||||
.current_scope_id()
|
||||
.and_then(|scope| runtime.get_context(scope).map(|sc| f(&sc)))
|
||||
})
|
||||
.flatten()
|
||||
}
|
||||
|
||||
/// 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>,
|
||||
|
||||
// We use this to track the current scope
|
||||
pub(crate) scope_stack: RefCell<Vec<ScopeId>>,
|
||||
pub(crate) rendering: Cell<bool>,
|
||||
}
|
||||
|
||||
impl Runtime {
|
||||
pub(crate) fn new(scheduler: Rc<Scheduler>) -> Rc<Self> {
|
||||
Rc::new(Self {
|
||||
scheduler,
|
||||
|
||||
scope_contexts: Default::default(),
|
||||
|
||||
scope_stack: Default::default(),
|
||||
|
||||
rendering: Cell::new(true),
|
||||
})
|
||||
}
|
||||
|
||||
/// 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();
|
||||
if contexts.len() <= id.0 {
|
||||
contexts.resize_with(id.0 + 1, Default::default);
|
||||
}
|
||||
contexts[id.0] = Some(context);
|
||||
}
|
||||
|
||||
pub(crate) fn remove_context(&self, id: ScopeId) {
|
||||
self.scope_contexts.borrow_mut()[id.0] = None;
|
||||
}
|
||||
|
||||
/// Get the current scope id
|
||||
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(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())
|
||||
})
|
||||
.ok()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct RuntimeGuard(Rc<Runtime>);
|
||||
|
||||
impl RuntimeGuard {
|
||||
pub(crate) fn new(runtime: Rc<Runtime>) -> Self {
|
||||
push_runtime(runtime.clone());
|
||||
Self(runtime)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for RuntimeGuard {
|
||||
fn drop(&mut self) {
|
||||
pop_runtime();
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::{TaskId, VirtualDom};
|
||||
use crate::{runtime::RuntimeGuard, TaskId, VirtualDom};
|
||||
use std::task::Context;
|
||||
|
||||
impl VirtualDom {
|
||||
@@ -7,7 +7,8 @@ impl VirtualDom {
|
||||
/// This is precise, meaning we won't poll every task, just tasks that have woken up as notified to use by the
|
||||
/// queue
|
||||
pub(crate) fn handle_task_wakeup(&mut self, id: TaskId) {
|
||||
let mut tasks = self.scheduler.tasks.borrow_mut();
|
||||
let _runtime = RuntimeGuard::new(self.runtime.clone());
|
||||
let mut tasks = self.runtime.scheduler.tasks.borrow_mut();
|
||||
|
||||
let task = match tasks.get(id.0) {
|
||||
Some(task) => task,
|
||||
@@ -17,14 +18,22 @@ impl VirtualDom {
|
||||
|
||||
let mut cx = Context::from_waker(&task.waker);
|
||||
|
||||
// update the scope stack
|
||||
self.runtime.scope_stack.borrow_mut().push(task.scope);
|
||||
self.runtime.rendering.set(false);
|
||||
|
||||
// If the task completes...
|
||||
if task.task.borrow_mut().as_mut().poll(&mut cx).is_ready() {
|
||||
// Remove it from the scope so we dont try to double drop it when the scope dropes
|
||||
let scope = &self.scopes[task.scope];
|
||||
scope.spawned_tasks.borrow_mut().remove(&id);
|
||||
let scope = &self.get_scope(task.scope).unwrap();
|
||||
scope.context().spawned_tasks.borrow_mut().remove(&id);
|
||||
|
||||
// Remove it from the scheduler
|
||||
tasks.try_remove(id.0);
|
||||
}
|
||||
|
||||
// Remove the scope from the stack
|
||||
self.runtime.scope_stack.borrow_mut().pop();
|
||||
self.runtime.rendering.set(true);
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ use crate::{
|
||||
bump_frame::BumpFrame,
|
||||
innerlude::DirtyScope,
|
||||
nodes::RenderReturn,
|
||||
scope_context::ScopeContext,
|
||||
scopes::{ScopeId, ScopeState},
|
||||
virtual_dom::VirtualDom,
|
||||
};
|
||||
@@ -13,48 +14,49 @@ impl VirtualDom {
|
||||
props: Box<dyn AnyProps<'static>>,
|
||||
name: &'static str,
|
||||
) -> &ScopeState {
|
||||
let parent = self.acquire_current_scope_raw();
|
||||
let parent_id = self.runtime.current_scope_id();
|
||||
let height = parent_id
|
||||
.and_then(|parent_id| self.get_scope(parent_id).map(|f| f.context().height + 1))
|
||||
.unwrap_or(0);
|
||||
let entry = self.scopes.vacant_entry();
|
||||
let height = unsafe { parent.map(|f| (*f).height + 1).unwrap_or(0) };
|
||||
let id = entry.key();
|
||||
let id = ScopeId(entry.key());
|
||||
|
||||
let scope = entry.insert(Box::new(ScopeState {
|
||||
runtime: self.runtime.clone(),
|
||||
context_id: id,
|
||||
|
||||
entry.insert(ScopeState {
|
||||
parent,
|
||||
id,
|
||||
height,
|
||||
name,
|
||||
props: Some(props),
|
||||
tasks: self.scheduler.clone(),
|
||||
|
||||
node_arena_1: BumpFrame::new(0),
|
||||
node_arena_2: BumpFrame::new(0),
|
||||
spawned_tasks: Default::default(),
|
||||
suspended: Default::default(),
|
||||
|
||||
render_cnt: Default::default(),
|
||||
hooks: Default::default(),
|
||||
hook_idx: Default::default(),
|
||||
shared_contexts: Default::default(),
|
||||
|
||||
borrowed_props: Default::default(),
|
||||
attributes_to_drop: Default::default(),
|
||||
})
|
||||
}
|
||||
}));
|
||||
|
||||
fn acquire_current_scope_raw(&self) -> Option<*const ScopeState> {
|
||||
let id = self.scope_stack.last().copied()?;
|
||||
let scope = self.scopes.get(id)?;
|
||||
Some(scope)
|
||||
let context =
|
||||
ScopeContext::new(name, id, parent_id, height, self.runtime.scheduler.clone());
|
||||
self.runtime.create_context_at(id, context);
|
||||
|
||||
scope
|
||||
}
|
||||
|
||||
pub(crate) fn run_scope(&mut self, scope_id: ScopeId) -> &RenderReturn {
|
||||
self.runtime.scope_stack.borrow_mut().push(scope_id);
|
||||
// Cycle to the next frame and then reset it
|
||||
// This breaks any latent references, invalidating every pointer referencing into it.
|
||||
// Remove all the outdated listeners
|
||||
self.ensure_drop_safety(scope_id);
|
||||
|
||||
let new_nodes = unsafe {
|
||||
self.scopes[scope_id].previous_frame().bump_mut().reset();
|
||||
let scope = &self.scopes[scope_id.0];
|
||||
scope.previous_frame().bump_mut().reset();
|
||||
|
||||
let scope = &self.scopes[scope_id];
|
||||
scope.suspended.set(false);
|
||||
scope.context().suspended.set(false);
|
||||
|
||||
scope.hook_idx.set(0);
|
||||
|
||||
@@ -62,10 +64,11 @@ impl VirtualDom {
|
||||
let props: &dyn AnyProps = scope.props.as_ref().unwrap().as_ref();
|
||||
let props: &dyn AnyProps = std::mem::transmute(props);
|
||||
|
||||
let _span = tracing::trace_span!("render", scope = %scope.context().name);
|
||||
props.render(scope).extend_lifetime()
|
||||
};
|
||||
|
||||
let scope = &self.scopes[scope_id];
|
||||
let scope = &self.scopes[scope_id.0];
|
||||
|
||||
// We write on top of the previous frame and then make it the current by pushing the generation forward
|
||||
let frame = scope.previous_frame();
|
||||
@@ -77,21 +80,26 @@ impl VirtualDom {
|
||||
// And move the render generation forward by one
|
||||
scope.render_cnt.set(scope.render_cnt.get() + 1);
|
||||
|
||||
let context = scope.context();
|
||||
// remove this scope from dirty scopes
|
||||
self.dirty_scopes.remove(&DirtyScope {
|
||||
height: scope.height,
|
||||
id: scope.id,
|
||||
height: context.height,
|
||||
id: context.id,
|
||||
});
|
||||
|
||||
if scope.suspended.get() {
|
||||
if context.suspended.get() {
|
||||
if matches!(allocated, RenderReturn::Aborted(_)) {
|
||||
self.suspended_scopes.insert(scope.id);
|
||||
self.suspended_scopes.insert(context.id);
|
||||
}
|
||||
} else if !self.suspended_scopes.is_empty() {
|
||||
_ = self.suspended_scopes.remove(&scope.id);
|
||||
_ = self.suspended_scopes.remove(&context.id);
|
||||
}
|
||||
|
||||
// rebind the lifetime now that its stored internally
|
||||
unsafe { allocated.extend_lifetime_ref() }
|
||||
let result = unsafe { allocated.extend_lifetime_ref() };
|
||||
|
||||
self.runtime.scope_stack.borrow_mut().pop();
|
||||
|
||||
result
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,362 @@
|
||||
use crate::{
|
||||
innerlude::{ErrorBoundary, Scheduler, SchedulerMsg},
|
||||
runtime::{with_current_scope, with_runtime},
|
||||
Element, ScopeId, TaskId,
|
||||
};
|
||||
use rustc_hash::FxHashSet;
|
||||
use std::{
|
||||
any::Any,
|
||||
cell::{Cell, RefCell},
|
||||
fmt::Debug,
|
||||
future::Future,
|
||||
rc::Rc,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
/// A component's state separate from its props.
|
||||
///
|
||||
/// This struct exists to provide a common interface for all scopes without relying on generics.
|
||||
pub(crate) struct ScopeContext {
|
||||
pub(crate) name: &'static str,
|
||||
|
||||
pub(crate) id: ScopeId,
|
||||
pub(crate) parent_id: Option<ScopeId>,
|
||||
|
||||
pub(crate) height: u32,
|
||||
pub(crate) suspended: Cell<bool>,
|
||||
|
||||
pub(crate) shared_contexts: RefCell<Vec<Box<dyn Any>>>,
|
||||
|
||||
pub(crate) tasks: Rc<Scheduler>,
|
||||
pub(crate) spawned_tasks: RefCell<FxHashSet<TaskId>>,
|
||||
}
|
||||
|
||||
impl ScopeContext {
|
||||
pub(crate) fn new(
|
||||
name: &'static str,
|
||||
id: ScopeId,
|
||||
parent_id: Option<ScopeId>,
|
||||
height: u32,
|
||||
tasks: Rc<Scheduler>,
|
||||
) -> Self {
|
||||
Self {
|
||||
name,
|
||||
id,
|
||||
parent_id,
|
||||
height,
|
||||
suspended: Cell::new(false),
|
||||
shared_contexts: RefCell::new(vec![]),
|
||||
tasks,
|
||||
spawned_tasks: RefCell::new(FxHashSet::default()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parent_id(&self) -> Option<ScopeId> {
|
||||
self.parent_id
|
||||
}
|
||||
|
||||
pub fn scope_id(&self) -> ScopeId {
|
||||
self.id
|
||||
}
|
||||
|
||||
/// Create a subscription that schedules a future render for the reference component
|
||||
///
|
||||
/// ## Notice: you should prefer using [`Self::schedule_update_any`] and [`Self::scope_id`]
|
||||
pub fn schedule_update(&self) -> Arc<dyn Fn() + Send + Sync + 'static> {
|
||||
let (chan, id) = (self.tasks.sender.clone(), self.scope_id());
|
||||
Arc::new(move || drop(chan.unbounded_send(SchedulerMsg::Immediate(id))))
|
||||
}
|
||||
|
||||
/// Schedule an update for any component given its [`ScopeId`].
|
||||
///
|
||||
/// A component's [`ScopeId`] can be obtained from `use_hook` or the [`ScopeState::scope_id`] method.
|
||||
///
|
||||
/// This method should be used when you want to schedule an update for a component
|
||||
pub fn schedule_update_any(&self) -> Arc<dyn Fn(ScopeId) + Send + Sync> {
|
||||
let chan = self.tasks.sender.clone();
|
||||
Arc::new(move |id| {
|
||||
chan.unbounded_send(SchedulerMsg::Immediate(id)).unwrap();
|
||||
})
|
||||
}
|
||||
|
||||
/// Mark this scope as dirty, and schedule a render for it.
|
||||
pub fn needs_update(&self) {
|
||||
self.needs_update_any(self.scope_id());
|
||||
}
|
||||
|
||||
/// Get the [`ScopeId`] of a mounted component.
|
||||
///
|
||||
/// `ScopeId` is not unique for the lifetime of the [`crate::VirtualDom`] - a [`ScopeId`] will be reused if a component is unmounted.
|
||||
pub fn needs_update_any(&self, id: ScopeId) {
|
||||
self.tasks
|
||||
.sender
|
||||
.unbounded_send(SchedulerMsg::Immediate(id))
|
||||
.expect("Scheduler to exist if scope exists");
|
||||
}
|
||||
|
||||
/// Return any context of type T if it exists on this scope
|
||||
pub fn has_context<T: 'static + Clone>(&self) -> Option<T> {
|
||||
self.shared_contexts
|
||||
.borrow()
|
||||
.iter()
|
||||
.find_map(|any| any.downcast_ref::<T>())
|
||||
.cloned()
|
||||
}
|
||||
|
||||
/// Try to retrieve a shared state with type `T` from any parent scope.
|
||||
///
|
||||
/// Clones the state if it exists.
|
||||
pub fn consume_context<T: 'static + Clone>(&self) -> Option<T> {
|
||||
tracing::trace!(
|
||||
"looking for context {} ({:?}) in {}",
|
||||
std::any::type_name::<T>(),
|
||||
std::any::TypeId::of::<T>(),
|
||||
self.name
|
||||
);
|
||||
if let Some(this_ctx) = self.has_context() {
|
||||
return Some(this_ctx);
|
||||
}
|
||||
|
||||
let mut search_parent = self.parent_id;
|
||||
match with_runtime(|runtime: &crate::runtime::Runtime| {
|
||||
while let Some(parent_id) = search_parent {
|
||||
let parent = runtime.get_context(parent_id).unwrap();
|
||||
tracing::trace!(
|
||||
"looking for context {} ({:?}) in {}",
|
||||
std::any::type_name::<T>(),
|
||||
std::any::TypeId::of::<T>(),
|
||||
parent.name
|
||||
);
|
||||
if let Some(shared) = parent.shared_contexts.borrow().iter().find_map(|any| {
|
||||
tracing::trace!("found context {:?}", any.type_id());
|
||||
any.downcast_ref::<T>()
|
||||
}) {
|
||||
return Some(shared.clone());
|
||||
}
|
||||
search_parent = parent.parent_id;
|
||||
}
|
||||
None
|
||||
})
|
||||
.flatten()
|
||||
{
|
||||
Some(ctx) => Some(ctx),
|
||||
None => {
|
||||
tracing::trace!(
|
||||
"context {} ({:?}) not found",
|
||||
std::any::type_name::<T>(),
|
||||
std::any::TypeId::of::<T>()
|
||||
);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Expose state to children further down the [`crate::VirtualDom`] Tree. Requires `Clone` on the context to allow getting values down the tree.
|
||||
///
|
||||
/// This is a "fundamental" operation and should only be called during initialization of a hook.
|
||||
///
|
||||
/// For a hook that provides the same functionality, use `use_provide_context` and `use_context` instead.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust, ignore
|
||||
/// struct SharedState(&'static str);
|
||||
///
|
||||
/// static App: Component = |cx| {
|
||||
/// cx.use_hook(|| cx.provide_context(SharedState("world")));
|
||||
/// render!(Child {})
|
||||
/// }
|
||||
///
|
||||
/// static Child: Component = |cx| {
|
||||
/// let state = cx.consume_state::<SharedState>();
|
||||
/// render!(div { "hello {state.0}" })
|
||||
/// }
|
||||
/// ```
|
||||
pub fn provide_context<T: 'static + Clone>(&self, value: T) -> T {
|
||||
tracing::trace!(
|
||||
"providing context {} ({:?}) in {}",
|
||||
std::any::type_name::<T>(),
|
||||
std::any::TypeId::of::<T>(),
|
||||
self.name
|
||||
);
|
||||
let mut contexts = self.shared_contexts.borrow_mut();
|
||||
|
||||
// If the context exists, swap it out for the new value
|
||||
for ctx in contexts.iter_mut() {
|
||||
// Swap the ptr directly
|
||||
if let Some(ctx) = ctx.downcast_mut::<T>() {
|
||||
std::mem::swap(ctx, &mut value.clone());
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
// Else, just push it
|
||||
contexts.push(Box::new(value.clone()));
|
||||
|
||||
value
|
||||
}
|
||||
|
||||
/// Provide a context to the root and then consume it
|
||||
///
|
||||
/// This is intended for "global" state management solutions that would rather be implicit for the entire app.
|
||||
/// Things like signal runtimes and routers are examples of "singletons" that would benefit from lazy initialization.
|
||||
///
|
||||
/// Note that you should be checking if the context existed before trying to provide a new one. Providing a context
|
||||
/// when a context already exists will swap the context out for the new one, which may not be what you want.
|
||||
pub fn provide_root_context<T: 'static + Clone>(&self, context: T) -> T {
|
||||
with_runtime(|runtime| {
|
||||
runtime
|
||||
.get_context(ScopeId::ROOT)
|
||||
.unwrap()
|
||||
.provide_context(context)
|
||||
})
|
||||
.expect("Runtime to exist")
|
||||
}
|
||||
|
||||
/// Pushes the future onto the poll queue to be polled after the component renders.
|
||||
pub fn push_future(&self, fut: impl Future<Output = ()> + 'static) -> TaskId {
|
||||
let id = self.tasks.spawn(self.id, fut);
|
||||
self.spawned_tasks.borrow_mut().insert(id);
|
||||
id
|
||||
}
|
||||
|
||||
/// Spawns the future but does not return the [`TaskId`]
|
||||
pub fn spawn(&self, fut: impl Future<Output = ()> + 'static) {
|
||||
self.push_future(fut);
|
||||
}
|
||||
|
||||
/// Spawn a future that Dioxus won't clean up when this component is unmounted
|
||||
///
|
||||
/// This is good for tasks that need to be run after the component has been dropped.
|
||||
pub fn spawn_forever(&self, fut: impl Future<Output = ()> + 'static) -> TaskId {
|
||||
// The root scope will never be unmounted so we can just add the task at the top of the app
|
||||
let id = self.tasks.spawn(ScopeId::ROOT, fut);
|
||||
|
||||
// wake up the scheduler if it is sleeping
|
||||
self.tasks
|
||||
.sender
|
||||
.unbounded_send(SchedulerMsg::TaskNotified(id))
|
||||
.expect("Scheduler should exist");
|
||||
|
||||
self.spawned_tasks.borrow_mut().insert(id);
|
||||
|
||||
id
|
||||
}
|
||||
|
||||
/// Informs the scheduler that this task is no longer needed and should be removed.
|
||||
///
|
||||
/// This drops the task immediately.
|
||||
pub fn remove_future(&self, id: TaskId) {
|
||||
self.tasks.remove(id);
|
||||
}
|
||||
|
||||
/// Inject an error into the nearest error boundary and quit rendering
|
||||
///
|
||||
/// The error doesn't need to implement Error or any specific traits since the boundary
|
||||
/// itself will downcast the error into a trait object.
|
||||
pub fn throw(&self, error: impl Debug + 'static) -> Option<()> {
|
||||
if let Some(cx) = self.consume_context::<Rc<ErrorBoundary>>() {
|
||||
cx.insert_error(self.scope_id(), Box::new(error));
|
||||
}
|
||||
|
||||
// Always return none during a throw
|
||||
None
|
||||
}
|
||||
|
||||
/// Mark this component as suspended and then return None
|
||||
pub fn suspend(&self) -> Option<Element> {
|
||||
self.suspended.set(true);
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Schedule an update for any component given its [`ScopeId`].
|
||||
///
|
||||
/// A component's [`ScopeId`] can be obtained from `use_hook` or the [`crate::scopes::ScopeState::scope_id`] method.
|
||||
///
|
||||
/// This method should be used when you want to schedule an update for a component
|
||||
pub fn schedule_update_any() -> Option<Arc<dyn Fn(ScopeId) + Send + Sync>> {
|
||||
with_current_scope(|cx| cx.schedule_update_any())
|
||||
}
|
||||
|
||||
/// Get the current scope id
|
||||
pub fn current_scope_id() -> Option<ScopeId> {
|
||||
with_runtime(|rt| rt.current_scope_id()).flatten()
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
/// Check if the virtual dom is currently inside of the body of a component
|
||||
pub fn vdom_is_rendering() -> bool {
|
||||
with_runtime(|rt| rt.rendering.get()).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Consume context from the current scope
|
||||
pub fn consume_context<T: 'static + Clone>() -> Option<T> {
|
||||
with_current_scope(|cx| cx.consume_context::<T>()).flatten()
|
||||
}
|
||||
|
||||
/// Consume context from the current scope
|
||||
pub fn consume_context_from_scope<T: 'static + Clone>(scope_id: ScopeId) -> Option<T> {
|
||||
with_runtime(|rt| {
|
||||
rt.get_context(scope_id)
|
||||
.and_then(|cx| cx.consume_context::<T>())
|
||||
})
|
||||
.flatten()
|
||||
}
|
||||
|
||||
/// Check if the current scope has a context
|
||||
pub fn has_context<T: 'static + Clone>() -> Option<T> {
|
||||
with_current_scope(|cx| cx.has_context::<T>()).flatten()
|
||||
}
|
||||
|
||||
/// Provide context to the current scope
|
||||
pub fn provide_context<T: 'static + Clone>(value: T) -> Option<T> {
|
||||
with_current_scope(|cx| cx.provide_context(value))
|
||||
}
|
||||
|
||||
/// Provide context to the the given scope
|
||||
pub fn provide_context_to_scope<T: 'static + Clone>(scope_id: ScopeId, value: T) -> Option<T> {
|
||||
with_runtime(|rt| rt.get_context(scope_id).map(|cx| cx.provide_context(value))).flatten()
|
||||
}
|
||||
|
||||
/// Provide a context to the root scope
|
||||
pub fn provide_root_context<T: 'static + Clone>(value: T) -> Option<T> {
|
||||
with_current_scope(|cx| cx.provide_root_context(value))
|
||||
}
|
||||
|
||||
/// Suspends the current component
|
||||
pub fn suspend() -> Option<Element<'static>> {
|
||||
with_current_scope(|cx| {
|
||||
cx.suspend();
|
||||
});
|
||||
None
|
||||
}
|
||||
|
||||
/// Throw an error into the nearest error boundary
|
||||
pub fn throw(error: impl Debug + 'static) -> Option<()> {
|
||||
with_current_scope(|cx| cx.throw(error)).flatten()
|
||||
}
|
||||
|
||||
/// Pushes the future onto the poll queue to be polled after the component renders.
|
||||
pub fn push_future(fut: impl Future<Output = ()> + 'static) -> Option<TaskId> {
|
||||
with_current_scope(|cx| cx.push_future(fut))
|
||||
}
|
||||
|
||||
/// Spawns the future but does not return the [`TaskId`]
|
||||
pub fn spawn(fut: impl Future<Output = ()> + 'static) {
|
||||
with_current_scope(|cx| cx.spawn(fut));
|
||||
}
|
||||
|
||||
/// Spawn a future that Dioxus won't clean up when this component is unmounted
|
||||
///
|
||||
/// This is good for tasks that need to be run after the component has been dropped.
|
||||
pub fn spawn_forever(fut: impl Future<Output = ()> + 'static) -> Option<TaskId> {
|
||||
with_current_scope(|cx| cx.spawn_forever(fut))
|
||||
}
|
||||
|
||||
/// Informs the scheduler that this task is no longer needed and should be removed.
|
||||
///
|
||||
/// This drops the task immediately.
|
||||
pub fn remove_future(id: TaskId) {
|
||||
with_current_scope(|cx| cx.remove_future(id));
|
||||
}
|
||||
+59
-193
@@ -2,22 +2,20 @@ use crate::{
|
||||
any_props::AnyProps,
|
||||
any_props::VProps,
|
||||
bump_frame::BumpFrame,
|
||||
innerlude::ErrorBoundary,
|
||||
innerlude::{DynamicNode, EventHandler, VComponent, VText},
|
||||
innerlude::{ErrorBoundary, Scheduler, SchedulerMsg},
|
||||
lazynodes::LazyNodes,
|
||||
nodes::{IntoAttributeValue, IntoDynNode, RenderReturn},
|
||||
runtime::Runtime,
|
||||
scope_context::ScopeContext,
|
||||
AnyValue, Attribute, AttributeValue, Element, Event, Properties, TaskId,
|
||||
};
|
||||
use bumpalo::{boxed::Box as BumpBox, Bump};
|
||||
use bumpslab::{BumpSlab, Slot};
|
||||
use rustc_hash::FxHashSet;
|
||||
use slab::{Slab, VacantEntry};
|
||||
use std::{
|
||||
any::{Any, TypeId},
|
||||
cell::{Cell, RefCell, UnsafeCell},
|
||||
any::Any,
|
||||
cell::{Cell, Ref, RefCell, UnsafeCell},
|
||||
fmt::{Arguments, Debug},
|
||||
future::Future,
|
||||
ops::{Index, IndexMut},
|
||||
rc::Rc,
|
||||
sync::Arc,
|
||||
};
|
||||
@@ -65,126 +63,53 @@ impl<'a, T> std::ops::Deref for Scoped<'a, T> {
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug, PartialOrd, Ord)]
|
||||
pub struct ScopeId(pub usize);
|
||||
|
||||
/// A thin wrapper around a BumpSlab that uses ids to index into the slab.
|
||||
pub(crate) struct ScopeSlab {
|
||||
slab: BumpSlab<ScopeState>,
|
||||
// a slab of slots of stable pointers to the ScopeState in the bump slab
|
||||
entries: Slab<Slot<'static, ScopeState>>,
|
||||
}
|
||||
|
||||
impl Drop for ScopeSlab {
|
||||
fn drop(&mut self) {
|
||||
// Bump slab doesn't drop its contents, so we need to do it manually
|
||||
for slot in self.entries.drain() {
|
||||
self.slab.remove(slot);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ScopeSlab {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
slab: BumpSlab::new(),
|
||||
entries: Slab::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ScopeSlab {
|
||||
pub(crate) fn get(&self, id: ScopeId) -> Option<&ScopeState> {
|
||||
self.entries.get(id.0).map(|slot| unsafe { &*slot.ptr() })
|
||||
}
|
||||
|
||||
pub(crate) fn get_mut(&mut self, id: ScopeId) -> Option<&mut ScopeState> {
|
||||
self.entries
|
||||
.get(id.0)
|
||||
.map(|slot| unsafe { &mut *slot.ptr_mut() })
|
||||
}
|
||||
|
||||
pub(crate) fn vacant_entry(&mut self) -> ScopeSlabEntry {
|
||||
let entry = self.entries.vacant_entry();
|
||||
ScopeSlabEntry {
|
||||
slab: &mut self.slab,
|
||||
entry,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn remove(&mut self, id: ScopeId) {
|
||||
self.slab.remove(self.entries.remove(id.0));
|
||||
}
|
||||
|
||||
pub(crate) fn contains(&self, id: ScopeId) -> bool {
|
||||
self.entries.contains(id.0)
|
||||
}
|
||||
|
||||
pub(crate) fn iter(&self) -> impl Iterator<Item = &ScopeState> {
|
||||
self.entries.iter().map(|(_, slot)| unsafe { &*slot.ptr() })
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct ScopeSlabEntry<'a> {
|
||||
slab: &'a mut BumpSlab<ScopeState>,
|
||||
entry: VacantEntry<'a, Slot<'static, ScopeState>>,
|
||||
}
|
||||
|
||||
impl<'a> ScopeSlabEntry<'a> {
|
||||
pub(crate) fn key(&self) -> ScopeId {
|
||||
ScopeId(self.entry.key())
|
||||
}
|
||||
|
||||
pub(crate) fn insert(self, scope: ScopeState) -> &'a ScopeState {
|
||||
let slot = self.slab.push(scope);
|
||||
// this is safe because the slot is only ever accessed with the lifetime of the borrow of the slab
|
||||
let slot = unsafe { std::mem::transmute(slot) };
|
||||
let entry = self.entry.insert(slot);
|
||||
unsafe { &*entry.ptr() }
|
||||
}
|
||||
}
|
||||
|
||||
impl Index<ScopeId> for ScopeSlab {
|
||||
type Output = ScopeState;
|
||||
fn index(&self, id: ScopeId) -> &Self::Output {
|
||||
self.get(id).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl IndexMut<ScopeId> for ScopeSlab {
|
||||
fn index_mut(&mut self, id: ScopeId) -> &mut Self::Output {
|
||||
self.get_mut(id).unwrap()
|
||||
}
|
||||
impl ScopeId {
|
||||
/// The root ScopeId.
|
||||
///
|
||||
/// This scope will last for the entire duration of your app, making it convenient for long-lived state
|
||||
/// that is created dynamically somewhere down the component tree.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust, ignore
|
||||
/// use dioxus_signals::*;
|
||||
/// let my_persistent_state = Signal::new_in_scope(ScopeId::ROOT, String::new());
|
||||
/// ```
|
||||
pub const ROOT: ScopeId = ScopeId(0);
|
||||
}
|
||||
|
||||
/// A component's state separate from its props.
|
||||
///
|
||||
/// This struct exists to provide a common interface for all scopes without relying on generics.
|
||||
pub struct ScopeState {
|
||||
pub(crate) runtime: Rc<Runtime>,
|
||||
pub(crate) context_id: ScopeId,
|
||||
|
||||
pub(crate) render_cnt: Cell<usize>,
|
||||
pub(crate) name: &'static str,
|
||||
|
||||
pub(crate) node_arena_1: BumpFrame,
|
||||
pub(crate) node_arena_2: BumpFrame,
|
||||
|
||||
pub(crate) parent: Option<*const ScopeState>,
|
||||
pub(crate) id: ScopeId,
|
||||
|
||||
pub(crate) height: u32,
|
||||
pub(crate) suspended: Cell<bool>,
|
||||
|
||||
pub(crate) hooks: RefCell<Vec<Box<UnsafeCell<dyn Any>>>>,
|
||||
pub(crate) hook_idx: Cell<usize>,
|
||||
|
||||
pub(crate) shared_contexts: RefCell<Vec<(TypeId, Box<dyn Any>)>>,
|
||||
|
||||
pub(crate) tasks: Rc<Scheduler>,
|
||||
pub(crate) spawned_tasks: RefCell<FxHashSet<TaskId>>,
|
||||
|
||||
pub(crate) borrowed_props: RefCell<Vec<*const VComponent<'static>>>,
|
||||
pub(crate) attributes_to_drop: RefCell<Vec<*const Attribute<'static>>>,
|
||||
|
||||
pub(crate) props: Option<Box<dyn AnyProps<'static>>>,
|
||||
}
|
||||
|
||||
impl Drop for ScopeState {
|
||||
fn drop(&mut self) {
|
||||
self.runtime.remove_context(self.context_id);
|
||||
}
|
||||
}
|
||||
|
||||
impl<'src> ScopeState {
|
||||
pub(crate) fn context(&self) -> Ref<'_, ScopeContext> {
|
||||
self.runtime.get_context(self.context_id).unwrap()
|
||||
}
|
||||
|
||||
pub(crate) fn current_frame(&self) -> &BumpFrame {
|
||||
match self.render_cnt.get() % 2 {
|
||||
0 => &self.node_arena_1,
|
||||
@@ -203,7 +128,7 @@ impl<'src> ScopeState {
|
||||
|
||||
/// Get the name of this component
|
||||
pub fn name(&self) -> &str {
|
||||
self.name
|
||||
self.context().name
|
||||
}
|
||||
|
||||
/// Get the current render since the inception of this component
|
||||
@@ -266,7 +191,7 @@ impl<'src> ScopeState {
|
||||
/// assert_eq!(base.height(), 0);
|
||||
/// ```
|
||||
pub fn height(&self) -> u32 {
|
||||
self.height
|
||||
self.context().height
|
||||
}
|
||||
|
||||
/// Get the Parent of this [`Scope`] within this Dioxus [`crate::VirtualDom`].
|
||||
@@ -287,7 +212,7 @@ impl<'src> ScopeState {
|
||||
/// ```
|
||||
pub fn parent(&self) -> Option<ScopeId> {
|
||||
// safety: the pointer to our parent is *always* valid thanks to the bump arena
|
||||
self.parent.map(|p| unsafe { &*p }.id)
|
||||
self.context().parent_id()
|
||||
}
|
||||
|
||||
/// Get the ID of this Scope within this Dioxus [`crate::VirtualDom`].
|
||||
@@ -304,15 +229,14 @@ impl<'src> ScopeState {
|
||||
/// assert_eq!(base.scope_id(), 0);
|
||||
/// ```
|
||||
pub fn scope_id(&self) -> ScopeId {
|
||||
self.id
|
||||
self.context().scope_id()
|
||||
}
|
||||
|
||||
/// Create a subscription that schedules a future render for the reference component
|
||||
///
|
||||
/// ## Notice: you should prefer using [`Self::schedule_update_any`] and [`Self::scope_id`]
|
||||
pub fn schedule_update(&self) -> Arc<dyn Fn() + Send + Sync + 'static> {
|
||||
let (chan, id) = (self.tasks.sender.clone(), self.scope_id());
|
||||
Arc::new(move || drop(chan.unbounded_send(SchedulerMsg::Immediate(id))))
|
||||
self.context().schedule_update()
|
||||
}
|
||||
|
||||
/// Schedule an update for any component given its [`ScopeId`].
|
||||
@@ -321,61 +245,31 @@ impl<'src> ScopeState {
|
||||
///
|
||||
/// This method should be used when you want to schedule an update for a component
|
||||
pub fn schedule_update_any(&self) -> Arc<dyn Fn(ScopeId) + Send + Sync> {
|
||||
let chan = self.tasks.sender.clone();
|
||||
Arc::new(move |id| {
|
||||
chan.unbounded_send(SchedulerMsg::Immediate(id)).unwrap();
|
||||
})
|
||||
self.context().schedule_update_any()
|
||||
}
|
||||
|
||||
/// Mark this scope as dirty, and schedule a render for it.
|
||||
pub fn needs_update(&self) {
|
||||
self.needs_update_any(self.scope_id());
|
||||
self.context().needs_update()
|
||||
}
|
||||
|
||||
/// Get the [`ScopeId`] of a mounted component.
|
||||
///
|
||||
/// `ScopeId` is not unique for the lifetime of the [`crate::VirtualDom`] - a [`ScopeId`] will be reused if a component is unmounted.
|
||||
pub fn needs_update_any(&self, id: ScopeId) {
|
||||
self.tasks
|
||||
.sender
|
||||
.unbounded_send(SchedulerMsg::Immediate(id))
|
||||
.expect("Scheduler to exist if scope exists");
|
||||
self.context().needs_update_any(id)
|
||||
}
|
||||
|
||||
/// Return any context of type T if it exists on this scope
|
||||
pub fn has_context<T: 'static + Clone>(&self) -> Option<T> {
|
||||
self.shared_contexts
|
||||
.borrow()
|
||||
.iter()
|
||||
.find(|(k, _)| *k == TypeId::of::<T>())
|
||||
.map(|(_, v)| v)?
|
||||
.downcast_ref::<T>()
|
||||
.cloned()
|
||||
self.context().has_context()
|
||||
}
|
||||
|
||||
/// Try to retrieve a shared state with type `T` from any parent scope.
|
||||
///
|
||||
/// Clones the state if it exists.
|
||||
pub fn consume_context<T: 'static + Clone>(&self) -> Option<T> {
|
||||
if let Some(this_ctx) = self.has_context() {
|
||||
return Some(this_ctx);
|
||||
}
|
||||
|
||||
let mut search_parent = self.parent;
|
||||
while let Some(parent_ptr) = search_parent {
|
||||
// safety: all parent pointers are valid thanks to the bump arena
|
||||
let parent = unsafe { &*parent_ptr };
|
||||
if let Some(shared) = parent
|
||||
.shared_contexts
|
||||
.borrow()
|
||||
.iter()
|
||||
.find(|(k, _)| *k == TypeId::of::<T>())
|
||||
{
|
||||
return shared.1.downcast_ref::<T>().cloned();
|
||||
}
|
||||
search_parent = parent.parent;
|
||||
}
|
||||
None
|
||||
self.context().consume_context()
|
||||
}
|
||||
|
||||
/// Expose state to children further down the [`crate::VirtualDom`] Tree. Requires `Clone` on the context to allow getting values down the tree.
|
||||
@@ -400,21 +294,7 @@ impl<'src> ScopeState {
|
||||
/// }
|
||||
/// ```
|
||||
pub fn provide_context<T: 'static + Clone>(&self, value: T) -> T {
|
||||
let mut contexts = self.shared_contexts.borrow_mut();
|
||||
|
||||
// If the context exists, swap it out for the new value
|
||||
for ctx in contexts.iter_mut() {
|
||||
// Swap the ptr directly
|
||||
if let Some(ctx) = ctx.1.downcast_mut::<T>() {
|
||||
std::mem::swap(ctx, &mut value.clone());
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
// Else, just push it
|
||||
contexts.push((TypeId::of::<T>(), Box::new(value.clone())));
|
||||
|
||||
value
|
||||
self.context().provide_context(value)
|
||||
}
|
||||
|
||||
/// Provide a context to the root and then consume it
|
||||
@@ -425,52 +305,31 @@ impl<'src> ScopeState {
|
||||
/// Note that you should be checking if the context existed before trying to provide a new one. Providing a context
|
||||
/// when a context already exists will swap the context out for the new one, which may not be what you want.
|
||||
pub fn provide_root_context<T: 'static + Clone>(&self, context: T) -> T {
|
||||
let mut parent = self;
|
||||
|
||||
// Walk upwards until there is no more parent - and tada we have the root
|
||||
while let Some(next_parent) = parent.parent {
|
||||
parent = unsafe { &*next_parent };
|
||||
debug_assert_eq!(parent.scope_id(), ScopeId(0));
|
||||
}
|
||||
|
||||
parent.provide_context(context)
|
||||
self.context().provide_root_context(context)
|
||||
}
|
||||
|
||||
/// Pushes the future onto the poll queue to be polled after the component renders.
|
||||
pub fn push_future(&self, fut: impl Future<Output = ()> + 'static) -> TaskId {
|
||||
let id = self.tasks.spawn(self.id, fut);
|
||||
self.spawned_tasks.borrow_mut().insert(id);
|
||||
id
|
||||
self.context().push_future(fut)
|
||||
}
|
||||
|
||||
/// Spawns the future but does not return the [`TaskId`]
|
||||
pub fn spawn(&self, fut: impl Future<Output = ()> + 'static) {
|
||||
self.push_future(fut);
|
||||
self.context().spawn(fut);
|
||||
}
|
||||
|
||||
/// Spawn a future that Dioxus won't clean up when this component is unmounted
|
||||
///
|
||||
/// This is good for tasks that need to be run after the component has been dropped.
|
||||
pub fn spawn_forever(&self, fut: impl Future<Output = ()> + 'static) -> TaskId {
|
||||
// The root scope will never be unmounted so we can just add the task at the top of the app
|
||||
let id = self.tasks.spawn(ScopeId(0), fut);
|
||||
|
||||
// wake up the scheduler if it is sleeping
|
||||
self.tasks
|
||||
.sender
|
||||
.unbounded_send(SchedulerMsg::TaskNotified(id))
|
||||
.expect("Scheduler should exist");
|
||||
|
||||
self.spawned_tasks.borrow_mut().insert(id);
|
||||
|
||||
id
|
||||
self.context().spawn_forever(fut)
|
||||
}
|
||||
|
||||
/// Informs the scheduler that this task is no longer needed and should be removed.
|
||||
///
|
||||
/// This drops the task immediately.
|
||||
pub fn remove_future(&self, id: TaskId) {
|
||||
self.tasks.remove(id);
|
||||
self.context().remove_future(id);
|
||||
}
|
||||
|
||||
/// Take a lazy [`crate::VNode`] structure and actually build it with the context of the efficient [`bumpalo::Bump`] allocator.
|
||||
@@ -573,19 +432,22 @@ impl<'src> ScopeState {
|
||||
/// fn(Scope<Props>) -> Element;
|
||||
/// async fn(Scope<Props<'_>>) -> Element;
|
||||
/// ```
|
||||
pub fn component<P>(
|
||||
pub fn component<'child, P>(
|
||||
&'src self,
|
||||
component: fn(Scope<'src, P>) -> Element<'src>,
|
||||
component: fn(Scope<'child, P>) -> Element<'child>,
|
||||
props: P,
|
||||
fn_name: &'static str,
|
||||
) -> DynamicNode<'src>
|
||||
where
|
||||
// The properties must be valid until the next bump frame
|
||||
P: Properties + 'src,
|
||||
// The current bump allocator frame must outlive the child's borrowed props
|
||||
'src: 'child,
|
||||
{
|
||||
let vcomp = VProps::new(component, P::memoize, props);
|
||||
|
||||
// cast off the lifetime of the render return
|
||||
let as_dyn: Box<dyn AnyProps<'src> + '_> = Box::new(vcomp);
|
||||
let as_dyn: Box<dyn AnyProps<'child> + '_> = Box::new(vcomp);
|
||||
let extended: Box<dyn AnyProps<'src> + 'src> = unsafe { std::mem::transmute(as_dyn) };
|
||||
|
||||
DynamicNode::Component(VComponent {
|
||||
@@ -602,7 +464,10 @@ impl<'src> ScopeState {
|
||||
let handler: &mut dyn FnMut(T) = self.bump().alloc(f);
|
||||
let caller = unsafe { BumpBox::from_raw(handler as *mut dyn FnMut(T)) };
|
||||
let callback = RefCell::new(Some(caller));
|
||||
EventHandler { callback }
|
||||
EventHandler {
|
||||
callback,
|
||||
origin: self.context().id,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new [`AttributeValue`] with the listener variant from a callback
|
||||
@@ -656,7 +521,8 @@ impl<'src> ScopeState {
|
||||
|
||||
/// Mark this component as suspended and then return None
|
||||
pub fn suspend(&self) -> Option<Element> {
|
||||
self.suspended.set(true);
|
||||
let cx = self.context();
|
||||
cx.suspend();
|
||||
None
|
||||
}
|
||||
|
||||
|
||||
@@ -5,10 +5,11 @@
|
||||
use crate::{
|
||||
any_props::VProps,
|
||||
arena::{ElementId, ElementRef},
|
||||
innerlude::{DirtyScope, ErrorBoundary, Mutations, Scheduler, SchedulerMsg, ScopeSlab},
|
||||
innerlude::{DirtyScope, ErrorBoundary, Mutations, Scheduler, SchedulerMsg},
|
||||
mutations::Mutation,
|
||||
nodes::RenderReturn,
|
||||
nodes::{Template, TemplateId},
|
||||
runtime::{Runtime, RuntimeGuard},
|
||||
scopes::{ScopeId, ScopeState},
|
||||
AttributeValue, Element, Event, Scope,
|
||||
};
|
||||
@@ -51,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 }
|
||||
@@ -59,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 }
|
||||
@@ -121,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());
|
||||
///
|
||||
@@ -174,24 +178,24 @@ use std::{any::Any, cell::Cell, collections::BTreeSet, future::Future, rc::Rc};
|
||||
/// }
|
||||
/// ```
|
||||
pub struct VirtualDom {
|
||||
pub(crate) scopes: Slab<Box<ScopeState>>,
|
||||
|
||||
pub(crate) dirty_scopes: BTreeSet<DirtyScope>,
|
||||
|
||||
// Maps a template path to a map of byteindexes to templates
|
||||
pub(crate) templates: FxHashMap<TemplateId, FxHashMap<usize, Template<'static>>>,
|
||||
pub(crate) scopes: ScopeSlab,
|
||||
pub(crate) dirty_scopes: BTreeSet<DirtyScope>,
|
||||
pub(crate) scheduler: Rc<Scheduler>,
|
||||
|
||||
// Every element is actually a dual reference - one to the template and the other to the dynamic node in that template
|
||||
pub(crate) elements: Slab<ElementRef>,
|
||||
|
||||
// While diffing we need some sort of way of breaking off a stream of suspended mutations.
|
||||
pub(crate) scope_stack: Vec<ScopeId>,
|
||||
pub(crate) mutations: Mutations<'static>,
|
||||
|
||||
pub(crate) runtime: Rc<Runtime>,
|
||||
|
||||
// Currently suspended scopes
|
||||
pub(crate) suspended_scopes: FxHashSet<ScopeId>,
|
||||
|
||||
pub(crate) rx: futures_channel::mpsc::UnboundedReceiver<SchedulerMsg>,
|
||||
|
||||
pub(crate) mutations: Mutations<'static>,
|
||||
}
|
||||
|
||||
impl VirtualDom {
|
||||
@@ -251,16 +255,16 @@ impl VirtualDom {
|
||||
/// ```
|
||||
pub fn new_with_props<P: 'static>(root: fn(Scope<P>) -> Element, root_props: P) -> Self {
|
||||
let (tx, rx) = futures_channel::mpsc::unbounded();
|
||||
let scheduler = Scheduler::new(tx);
|
||||
let mut dom = Self {
|
||||
rx,
|
||||
scheduler: Scheduler::new(tx),
|
||||
templates: Default::default(),
|
||||
runtime: Runtime::new(scheduler),
|
||||
scopes: Default::default(),
|
||||
dirty_scopes: Default::default(),
|
||||
templates: Default::default(),
|
||||
elements: Default::default(),
|
||||
scope_stack: Vec::new(),
|
||||
dirty_scopes: BTreeSet::new(),
|
||||
suspended_scopes: FxHashSet::default(),
|
||||
mutations: Mutations::default(),
|
||||
suspended_scopes: Default::default(),
|
||||
};
|
||||
|
||||
let root = dom.new_scope(
|
||||
@@ -269,7 +273,7 @@ impl VirtualDom {
|
||||
);
|
||||
|
||||
// Unlike react, we provide a default error boundary that just renders the error as a string
|
||||
root.provide_context(Rc::new(ErrorBoundary::new(ScopeId(0))));
|
||||
root.provide_context(Rc::new(ErrorBoundary::new(ScopeId::ROOT)));
|
||||
|
||||
// the root element is always given element ID 0 since it's the container for the entire tree
|
||||
dom.elements.insert(ElementRef::none());
|
||||
@@ -281,14 +285,14 @@ impl VirtualDom {
|
||||
///
|
||||
/// This is useful for inserting or removing contexts from a scope, or rendering out its root node
|
||||
pub fn get_scope(&self, id: ScopeId) -> Option<&ScopeState> {
|
||||
self.scopes.get(id)
|
||||
self.scopes.get(id.0).map(|s| &**s)
|
||||
}
|
||||
|
||||
/// Get the single scope at the top of the VirtualDom tree that will always be around
|
||||
///
|
||||
/// This scope has a ScopeId of 0 and is the root of the tree
|
||||
pub fn base_scope(&self) -> &ScopeState {
|
||||
self.scopes.get(ScopeId(0)).unwrap()
|
||||
self.get_scope(ScopeId::ROOT).unwrap()
|
||||
}
|
||||
|
||||
/// Build the virtualdom with a global context inserted into the base scope
|
||||
@@ -301,10 +305,11 @@ impl VirtualDom {
|
||||
|
||||
/// Manually mark a scope as requiring a re-render
|
||||
///
|
||||
/// Whenever the VirtualDom "works", it will re-render this scope
|
||||
/// Whenever the Runtime "works", it will re-render this scope
|
||||
pub fn mark_dirty(&mut self, id: ScopeId) {
|
||||
if let Some(scope) = self.scopes.get(id) {
|
||||
let height = scope.height;
|
||||
if let Some(scope) = self.get_scope(id) {
|
||||
let height = scope.height();
|
||||
tracing::trace!("Marking scope {:?} ({}) as dirty", id, scope.context().name);
|
||||
self.dirty_scopes.insert(DirtyScope { height, id });
|
||||
}
|
||||
}
|
||||
@@ -325,6 +330,8 @@ impl VirtualDom {
|
||||
element: ElementId,
|
||||
bubbles: bool,
|
||||
) {
|
||||
let _runtime = RuntimeGuard::new(self.runtime.clone());
|
||||
|
||||
/*
|
||||
------------------------
|
||||
The algorithm works by walking through the list of dynamic attributes, checking their paths, and breaking when
|
||||
@@ -387,9 +394,14 @@ impl VirtualDom {
|
||||
// We check the bubble state between each call to see if the event has been stopped from bubbling
|
||||
for listener in listeners.drain(..).rev() {
|
||||
if let AttributeValue::Listener(listener) = listener {
|
||||
let origin = el_ref.scope;
|
||||
self.runtime.scope_stack.borrow_mut().push(origin);
|
||||
self.runtime.rendering.set(false);
|
||||
if let Some(cb) = listener.borrow_mut().as_deref_mut() {
|
||||
cb(uievent.clone());
|
||||
}
|
||||
self.runtime.scope_stack.borrow_mut().pop();
|
||||
self.runtime.rendering.set(true);
|
||||
|
||||
if !uievent.propagates.get() {
|
||||
return;
|
||||
@@ -418,9 +430,14 @@ impl VirtualDom {
|
||||
// Only call the listener if this is the exact target element.
|
||||
if attr.name.trim_start_matches("on") == name && target_path == this_path {
|
||||
if let AttributeValue::Listener(listener) = &attr.value {
|
||||
let origin = el_ref.scope;
|
||||
self.runtime.scope_stack.borrow_mut().push(origin);
|
||||
self.runtime.rendering.set(false);
|
||||
if let Some(cb) = listener.borrow_mut().as_deref_mut() {
|
||||
cb(uievent.clone());
|
||||
}
|
||||
self.runtime.scope_stack.borrow_mut().pop();
|
||||
self.runtime.rendering.set(true);
|
||||
|
||||
break;
|
||||
}
|
||||
@@ -496,15 +513,16 @@ impl VirtualDom {
|
||||
pub fn replace_template(&mut self, template: Template<'static>) {
|
||||
self.register_template_first_byte_index(template);
|
||||
// iterating a slab is very inefficient, but this is a rare operation that will only happen during development so it's fine
|
||||
for scope in self.scopes.iter() {
|
||||
for (_, scope) in self.scopes.iter() {
|
||||
if let Some(RenderReturn::Ready(sync)) = scope.try_root_node() {
|
||||
if sync.template.get().name.rsplit_once(':').unwrap().0
|
||||
== template.name.rsplit_once(':').unwrap().0
|
||||
{
|
||||
let height = scope.height;
|
||||
let context = scope.context();
|
||||
let height = context.height;
|
||||
self.dirty_scopes.insert(DirtyScope {
|
||||
height,
|
||||
id: scope.id,
|
||||
id: context.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -532,17 +550,23 @@ impl VirtualDom {
|
||||
/// apply_edits(edits);
|
||||
/// ```
|
||||
pub fn rebuild(&mut self) -> Mutations {
|
||||
match unsafe { self.run_scope(ScopeId(0)).extend_lifetime_ref() } {
|
||||
let _runtime = RuntimeGuard::new(self.runtime.clone());
|
||||
match unsafe { self.run_scope(ScopeId::ROOT).extend_lifetime_ref() } {
|
||||
// Rebuilding implies we append the created elements to the root
|
||||
RenderReturn::Ready(node) => {
|
||||
let m = self.create_scope(ScopeId(0), node);
|
||||
let m = self.create_scope(ScopeId::ROOT, node);
|
||||
self.mutations.edits.push(Mutation::AppendChildren {
|
||||
id: ElementId(0),
|
||||
m,
|
||||
});
|
||||
}
|
||||
// If an error occurs, we should try to render the default error component and context where the error occured
|
||||
RenderReturn::Aborted(_placeholder) => panic!("Cannot catch errors during rebuild"),
|
||||
RenderReturn::Aborted(placeholder) => {
|
||||
tracing::debug!("Ran into suspended or aborted scope during rebuild");
|
||||
let id = self.next_null();
|
||||
placeholder.id.set(Some(id));
|
||||
self.mutations.push(Mutation::CreatePlaceholder { id });
|
||||
}
|
||||
}
|
||||
|
||||
self.finalize()
|
||||
@@ -601,13 +625,16 @@ impl VirtualDom {
|
||||
self.dirty_scopes.remove(&dirty);
|
||||
|
||||
// If the scope doesn't exist for whatever reason, then we should skip it
|
||||
if !self.scopes.contains(dirty.id) {
|
||||
if !self.scopes.contains(dirty.id.0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Run the scope and get the mutations
|
||||
self.run_scope(dirty.id);
|
||||
self.diff_scope(dirty.id);
|
||||
{
|
||||
let _runtime = RuntimeGuard::new(self.runtime.clone());
|
||||
// Run the scope and get the mutations
|
||||
self.run_scope(dirty.id);
|
||||
self.diff_scope(dirty.id);
|
||||
}
|
||||
}
|
||||
|
||||
// If there's more work, then just continue, plenty of work to do
|
||||
@@ -635,11 +662,16 @@ 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 {
|
||||
fn drop(&mut self) {
|
||||
// Simply drop this scope which drops all of its children
|
||||
self.drop_scope(ScopeId(0), true);
|
||||
self.drop_scope(ScopeId::ROOT, true);
|
||||
}
|
||||
}
|
||||
@@ -34,7 +34,7 @@ fn attrs_cycle() {
|
||||
]
|
||||
);
|
||||
|
||||
dom.mark_dirty(ScopeId(0));
|
||||
dom.mark_dirty(ScopeId::ROOT);
|
||||
assert_eq!(
|
||||
dom.render_immediate().santize().edits,
|
||||
[
|
||||
@@ -56,7 +56,7 @@ fn attrs_cycle() {
|
||||
]
|
||||
);
|
||||
|
||||
dom.mark_dirty(ScopeId(0));
|
||||
dom.mark_dirty(ScopeId::ROOT);
|
||||
assert_eq!(
|
||||
dom.render_immediate().santize().edits,
|
||||
[
|
||||
@@ -65,7 +65,7 @@ fn attrs_cycle() {
|
||||
]
|
||||
);
|
||||
|
||||
dom.mark_dirty(ScopeId(0));
|
||||
dom.mark_dirty(ScopeId::ROOT);
|
||||
assert_eq!(
|
||||
dom.render_immediate().santize().edits,
|
||||
[
|
||||
@@ -88,7 +88,7 @@ fn attrs_cycle() {
|
||||
);
|
||||
|
||||
// we take the node taken by attributes since we reused it
|
||||
dom.mark_dirty(ScopeId(0));
|
||||
dom.mark_dirty(ScopeId::ROOT);
|
||||
assert_eq!(
|
||||
dom.render_immediate().santize().edits,
|
||||
[
|
||||
|
||||
@@ -24,7 +24,7 @@ fn bubbles_error() {
|
||||
let _edits = dom.rebuild().santize();
|
||||
}
|
||||
|
||||
dom.mark_dirty(ScopeId(0));
|
||||
dom.mark_dirty(ScopeId::ROOT);
|
||||
|
||||
_ = dom.render_immediate();
|
||||
}
|
||||
@@ -27,11 +27,11 @@ fn state_shares() {
|
||||
]
|
||||
);
|
||||
|
||||
dom.mark_dirty(ScopeId(0));
|
||||
dom.mark_dirty(ScopeId::ROOT);
|
||||
_ = dom.render_immediate();
|
||||
assert_eq!(dom.base_scope().consume_context::<i32>().unwrap(), 1);
|
||||
|
||||
dom.mark_dirty(ScopeId(0));
|
||||
dom.mark_dirty(ScopeId::ROOT);
|
||||
_ = dom.render_immediate();
|
||||
assert_eq!(dom.base_scope().consume_context::<i32>().unwrap(), 2);
|
||||
|
||||
@@ -41,7 +41,7 @@ fn state_shares() {
|
||||
[SetText { value: "Value is 2", id: ElementId(1,) },]
|
||||
);
|
||||
|
||||
dom.mark_dirty(ScopeId(0));
|
||||
dom.mark_dirty(ScopeId::ROOT);
|
||||
dom.mark_dirty(ScopeId(2));
|
||||
let edits = dom.render_immediate();
|
||||
assert_eq!(
|
||||
|
||||
@@ -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
|
||||
|
||||
Loaded 100 of 575 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user