Merge branch 'master' into many_optional_attributes
This commit is contained in:
569 files changed
+8815
-21321
No files matched your search
@@ -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
|
||||
|
||||
@@ -8,13 +8,14 @@ use std::fmt::{Result, Write};
|
||||
|
||||
use dioxus_rsx::IfmtInput;
|
||||
|
||||
use crate::write_ifmt;
|
||||
use crate::{indent::IndentOptions, write_ifmt};
|
||||
|
||||
/// The output buffer that tracks indent and string
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Buffer {
|
||||
pub buf: String,
|
||||
pub indent: usize,
|
||||
pub indent_level: usize,
|
||||
pub indent: IndentOptions,
|
||||
}
|
||||
|
||||
impl Buffer {
|
||||
@@ -31,16 +32,16 @@ impl Buffer {
|
||||
}
|
||||
|
||||
pub fn tab(&mut self) -> Result {
|
||||
self.write_tabs(self.indent)
|
||||
self.write_tabs(self.indent_level)
|
||||
}
|
||||
|
||||
pub fn indented_tab(&mut self) -> Result {
|
||||
self.write_tabs(self.indent + 1)
|
||||
self.write_tabs(self.indent_level + 1)
|
||||
}
|
||||
|
||||
pub fn write_tabs(&mut self, num: usize) -> std::fmt::Result {
|
||||
for _ in 0..num {
|
||||
write!(self.buf, " ")?
|
||||
write!(self.buf, "{}", self.indent.indent_str())?
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ impl Writer<'_> {
|
||||
|
||||
// check if we have a lot of attributes
|
||||
let attr_len = self.is_short_attrs(attributes);
|
||||
let is_short_attr_list = (attr_len + self.out.indent * 4) < 80;
|
||||
let is_short_attr_list = (attr_len + self.out.indent_level * 4) < 80;
|
||||
let children_len = self.is_short_children(children);
|
||||
let is_small_children = children_len.is_some();
|
||||
|
||||
@@ -87,7 +87,7 @@ impl Writer<'_> {
|
||||
|
||||
// if we have few children and few attributes, make it a one-liner
|
||||
if is_short_attr_list && is_small_children {
|
||||
if children_len.unwrap() + attr_len + self.out.indent * 4 < 100 {
|
||||
if children_len.unwrap() + attr_len + self.out.indent_level * 4 < 100 {
|
||||
opt_level = ShortOptimization::Oneliner;
|
||||
} else {
|
||||
opt_level = ShortOptimization::PropsOnTop;
|
||||
@@ -186,11 +186,11 @@ impl Writer<'_> {
|
||||
}
|
||||
|
||||
while let Some(attr) = attr_iter.next() {
|
||||
self.out.indent += 1;
|
||||
self.out.indent_level += 1;
|
||||
if !sameline {
|
||||
self.write_comments(attr.attr.start())?;
|
||||
}
|
||||
self.out.indent -= 1;
|
||||
self.out.indent_level -= 1;
|
||||
|
||||
if !sameline {
|
||||
self.out.indented_tabbed_line()?;
|
||||
@@ -414,14 +414,14 @@ impl Writer<'_> {
|
||||
for idx in start.line..end.line {
|
||||
let line = &self.src[idx];
|
||||
if line.trim().starts_with("//") {
|
||||
for _ in 0..self.out.indent + 1 {
|
||||
for _ in 0..self.out.indent_level + 1 {
|
||||
write!(self.out, " ")?
|
||||
}
|
||||
writeln!(self.out, "{}", line.trim()).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
for _ in 0..self.out.indent {
|
||||
for _ in 0..self.out.indent_level {
|
||||
write!(self.out, " ")?
|
||||
}
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ impl Writer<'_> {
|
||||
let first_line = &self.src[start.line - 1];
|
||||
write!(self.out, "{}", &first_line[start.column - 1..].trim_start())?;
|
||||
|
||||
let prev_block_indent_level = crate::leading_whitespaces(first_line) / 4;
|
||||
let prev_block_indent_level = self.out.indent.count_indents(first_line);
|
||||
|
||||
for (id, line) in self.src[start.line..end.line].iter().enumerate() {
|
||||
writeln!(self.out)?;
|
||||
@@ -43,9 +43,9 @@ impl Writer<'_> {
|
||||
};
|
||||
|
||||
// trim the leading whitespace
|
||||
let previous_indent = crate::leading_whitespaces(line) / 4;
|
||||
let previous_indent = self.out.indent.count_indents(line);
|
||||
let offset = previous_indent.saturating_sub(prev_block_indent_level);
|
||||
let required_indent = self.out.indent + offset;
|
||||
let required_indent = self.out.indent_level + offset;
|
||||
self.out.write_tabs(required_indent)?;
|
||||
|
||||
let line = line.trim_start();
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
pub enum IndentType {
|
||||
Spaces,
|
||||
Tabs,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct IndentOptions {
|
||||
width: usize,
|
||||
indent_string: String,
|
||||
}
|
||||
|
||||
impl IndentOptions {
|
||||
pub fn new(typ: IndentType, width: usize) -> Self {
|
||||
assert_ne!(width, 0, "Cannot have an indent width of 0");
|
||||
Self {
|
||||
width,
|
||||
indent_string: match typ {
|
||||
IndentType::Tabs => "\t".into(),
|
||||
IndentType::Spaces => " ".repeat(width),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets a string containing one indent worth of whitespace
|
||||
pub fn indent_str(&self) -> &str {
|
||||
&self.indent_string
|
||||
}
|
||||
|
||||
/// Computes the line length in characters, counting tabs as the indent width.
|
||||
pub fn line_length(&self, line: &str) -> usize {
|
||||
line.chars()
|
||||
.map(|ch| if ch == '\t' { self.width } else { 1 })
|
||||
.sum()
|
||||
}
|
||||
|
||||
/// Estimates how many times the line has been indented.
|
||||
pub fn count_indents(&self, mut line: &str) -> usize {
|
||||
let mut indent = 0;
|
||||
while !line.is_empty() {
|
||||
// Try to count tabs
|
||||
let num_tabs = line.chars().take_while(|ch| *ch == '\t').count();
|
||||
if num_tabs > 0 {
|
||||
indent += num_tabs;
|
||||
line = &line[num_tabs..];
|
||||
continue;
|
||||
}
|
||||
|
||||
// Try to count spaces
|
||||
let num_spaces = line.chars().take_while(|ch| *ch == ' ').count();
|
||||
if num_spaces >= self.width {
|
||||
// Intentionally floor here to take only the amount of space that matches an indent
|
||||
let num_space_indents = num_spaces / self.width;
|
||||
indent += num_space_indents;
|
||||
line = &line[num_space_indents * self.width..];
|
||||
continue;
|
||||
}
|
||||
|
||||
// Line starts with either non-indent characters or an unevent amount of spaces,
|
||||
// so no more indent remains.
|
||||
break;
|
||||
}
|
||||
indent
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for IndentOptions {
|
||||
fn default() -> Self {
|
||||
Self::new(IndentType::Spaces, 4)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn count_indents() {
|
||||
assert_eq!(
|
||||
IndentOptions::new(IndentType::Spaces, 4).count_indents("no indentation here!"),
|
||||
0
|
||||
);
|
||||
assert_eq!(
|
||||
IndentOptions::new(IndentType::Spaces, 4).count_indents(" v += 2"),
|
||||
1
|
||||
);
|
||||
assert_eq!(
|
||||
IndentOptions::new(IndentType::Spaces, 4).count_indents(" v += 2"),
|
||||
2
|
||||
);
|
||||
assert_eq!(
|
||||
IndentOptions::new(IndentType::Spaces, 4).count_indents(" v += 2"),
|
||||
2
|
||||
);
|
||||
assert_eq!(
|
||||
IndentOptions::new(IndentType::Spaces, 4).count_indents("\t\tv += 2"),
|
||||
2
|
||||
);
|
||||
assert_eq!(
|
||||
IndentOptions::new(IndentType::Spaces, 4).count_indents("\t\t v += 2"),
|
||||
2
|
||||
);
|
||||
assert_eq!(
|
||||
IndentOptions::new(IndentType::Spaces, 2).count_indents(" v += 2"),
|
||||
2
|
||||
);
|
||||
}
|
||||
}
|
||||
+16
-15
@@ -1,3 +1,7 @@
|
||||
#![doc = include_str!("../README.md")]
|
||||
#![doc(html_logo_url = "https://avatars.githubusercontent.com/u/79236386")]
|
||||
#![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")]
|
||||
|
||||
use std::fmt::{Display, Write};
|
||||
|
||||
use crate::writer::*;
|
||||
@@ -12,8 +16,11 @@ mod collect_macros;
|
||||
mod component;
|
||||
mod element;
|
||||
mod expr;
|
||||
mod indent;
|
||||
mod writer;
|
||||
|
||||
pub use indent::{IndentOptions, IndentType};
|
||||
|
||||
/// A modification to the original file to be applied by an IDE
|
||||
///
|
||||
/// Right now this re-writes entire rsx! blocks at a time, instead of precise line-by-line changes.
|
||||
@@ -43,7 +50,7 @@ pub struct FormattedBlock {
|
||||
/// back to the file precisely.
|
||||
///
|
||||
/// Nested blocks of RSX will be handled automatically
|
||||
pub fn fmt_file(contents: &str) -> Vec<FormattedBlock> {
|
||||
pub fn fmt_file(contents: &str, indent: IndentOptions) -> Vec<FormattedBlock> {
|
||||
let mut formatted_blocks = Vec::new();
|
||||
|
||||
let parsed = syn::parse_file(contents).unwrap();
|
||||
@@ -57,6 +64,7 @@ pub fn fmt_file(contents: &str) -> Vec<FormattedBlock> {
|
||||
}
|
||||
|
||||
let mut writer = Writer::new(contents);
|
||||
writer.out.indent = indent;
|
||||
|
||||
// Don't parse nested macros
|
||||
let mut end_span = LineColumn { column: 0, line: 0 };
|
||||
@@ -72,7 +80,10 @@ pub fn fmt_file(contents: &str) -> Vec<FormattedBlock> {
|
||||
|
||||
let rsx_start = macro_path.span().start();
|
||||
|
||||
writer.out.indent = leading_whitespaces(writer.src[rsx_start.line - 1]) / 4;
|
||||
writer.out.indent_level = writer
|
||||
.out
|
||||
.indent
|
||||
.count_indents(writer.src[rsx_start.line - 1]);
|
||||
|
||||
write_body(&mut writer, &body);
|
||||
|
||||
@@ -155,12 +166,13 @@ pub fn fmt_block_from_expr(raw: &str, expr: ExprMacro) -> Option<String> {
|
||||
buf.consume()
|
||||
}
|
||||
|
||||
pub fn fmt_block(block: &str, indent_level: usize) -> Option<String> {
|
||||
pub fn fmt_block(block: &str, indent_level: usize, indent: IndentOptions) -> Option<String> {
|
||||
let body = syn::parse_str::<dioxus_rsx::CallBody>(block).unwrap();
|
||||
|
||||
let mut buf = Writer::new(block);
|
||||
|
||||
buf.out.indent = indent_level;
|
||||
buf.out.indent = indent;
|
||||
buf.out.indent_level = indent_level;
|
||||
|
||||
write_body(&mut buf, &body);
|
||||
|
||||
@@ -226,14 +238,3 @@ 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()
|
||||
}
|
||||
@@ -96,11 +96,11 @@ impl<'a> Writer<'a> {
|
||||
|
||||
// Push out the indent level and write each component, line by line
|
||||
pub fn write_body_indented(&mut self, children: &[BodyNode]) -> Result {
|
||||
self.out.indent += 1;
|
||||
self.out.indent_level += 1;
|
||||
|
||||
self.write_body_no_indent(children)?;
|
||||
|
||||
self.out.indent -= 1;
|
||||
self.out.indent_level -= 1;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ macro_rules! twoway {
|
||||
#[test]
|
||||
fn $name() {
|
||||
let src = include_str!(concat!("./samples/", stringify!($name), ".rsx"));
|
||||
let formatted = dioxus_autofmt::fmt_file(src);
|
||||
let formatted = dioxus_autofmt::fmt_file(src, Default::default());
|
||||
let out = dioxus_autofmt::apply_formats(src, formatted);
|
||||
// normalize line endings
|
||||
let out = out.replace("\r", "");
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use dioxus::prelude::*;
|
||||
|
||||
#[inline_props]
|
||||
#[component]
|
||||
pub fn Explainer<'a>(
|
||||
cx: Scope<'a>,
|
||||
invert: bool,
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
use dioxus_autofmt::{IndentOptions, IndentType};
|
||||
|
||||
macro_rules! twoway {
|
||||
($val:literal => $name:ident) => {
|
||||
($val:literal => $name:ident ($indent:expr)) => {
|
||||
#[test]
|
||||
fn $name() {
|
||||
let src_right = include_str!(concat!("./wrong/", $val, ".rsx"));
|
||||
let src_wrong = include_str!(concat!("./wrong/", $val, ".wrong.rsx"));
|
||||
let formatted = dioxus_autofmt::fmt_file(src_wrong);
|
||||
let formatted = dioxus_autofmt::fmt_file(src_wrong, $indent);
|
||||
let out = dioxus_autofmt::apply_formats(src_wrong, formatted);
|
||||
|
||||
// normalize line endings
|
||||
@@ -16,8 +18,11 @@ macro_rules! twoway {
|
||||
};
|
||||
}
|
||||
|
||||
twoway!("comments" => comments);
|
||||
twoway!("comments-4sp" => comments_4sp (IndentOptions::new(IndentType::Spaces, 4)));
|
||||
twoway!("comments-tab" => comments_tab (IndentOptions::new(IndentType::Tabs, 4)));
|
||||
|
||||
twoway!("multi" => multi);
|
||||
twoway!("multi-4sp" => multi_4sp (IndentOptions::new(IndentType::Spaces, 4)));
|
||||
twoway!("multi-tab" => multi_tab (IndentOptions::new(IndentType::Tabs, 4)));
|
||||
|
||||
twoway!("multiexpr" => multiexpr);
|
||||
twoway!("multiexpr-4sp" => multiexpr_4sp (IndentOptions::new(IndentType::Spaces, 4)));
|
||||
twoway!("multiexpr-tab" => multiexpr_tab (IndentOptions::new(IndentType::Tabs, 4)));
|
||||
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,7 @@
|
||||
rsx! {
|
||||
div {
|
||||
// Comments
|
||||
class: "asdasd",
|
||||
"hello world"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
rsx! {
|
||||
div {
|
||||
// Comments
|
||||
class: "asdasd", "hello world" }
|
||||
}
|
||||
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,3 @@
|
||||
fn app(cx: Scope) -> Element {
|
||||
cx.render(rsx! { div { "hello world" } })
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
fn app(cx: Scope) -> Element {
|
||||
cx.render(rsx! {
|
||||
div {"hello world" }
|
||||
})
|
||||
}
|
||||
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,8 @@
|
||||
fn ItWroks() {
|
||||
cx.render(rsx! {
|
||||
div { class: "flex flex-wrap items-center dark:text-white py-16 border-t font-light",
|
||||
left,
|
||||
right
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
fn ItWroks() {
|
||||
cx.render(rsx! {
|
||||
div { class: "flex flex-wrap items-center dark:text-white py-16 border-t font-light", left, right }
|
||||
})
|
||||
}
|
||||
@@ -11,13 +11,10 @@ keywords = ["dom", "ui", "gui", "react"]
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
dioxus-rsx = { workspace = true }
|
||||
proc-macro2 = { version = "1.0.6", features = ["span-locations"] }
|
||||
quote = "1.0"
|
||||
syn = { version = "1.0.11", features = ["full", "extra-traits", "visit"] }
|
||||
serde = { version = "1.0.136", features = ["derive"] }
|
||||
owo-colors = { version = "3.5.0", features = ["supports-colors"] }
|
||||
prettyplease = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
indoc = "2.0.3"
|
||||
|
||||
@@ -1,32 +1,27 @@
|
||||
# dioxus-check
|
||||
|
||||
|
||||
[![Crates.io][crates-badge]][crates-url]
|
||||
[![MIT licensed][mit-badge]][mit-url]
|
||||
[![Build Status][actions-badge]][actions-url]
|
||||
[![Discord chat][discord-badge]][discord-url]
|
||||
|
||||
[crates-badge]: https://img.shields.io/crates/v/dioxus-autofmt.svg
|
||||
[crates-url]: https://crates.io/crates/dioxus-autofmt
|
||||
|
||||
[crates-url]: https://crates.io/crates/dioxus-check
|
||||
[mit-badge]: https://img.shields.io/badge/license-MIT-blue.svg
|
||||
[mit-url]: https://github.com/dioxuslabs/dioxus/blob/master/LICENSE
|
||||
|
||||
[actions-badge]: https://github.com/dioxuslabs/dioxus/actions/workflows/main.yml/badge.svg
|
||||
[actions-url]: https://github.com/dioxuslabs/dioxus/actions?query=workflow%3ACI+branch%3Amaster
|
||||
|
||||
[discord-badge]: https://img.shields.io/discord/899851952891002890.svg?logo=discord&style=flat-square
|
||||
[discord-url]: https://discord.gg/XgGxMSkvUM
|
||||
|
||||
[Website](https://dioxuslabs.com) |
|
||||
[Guides](https://dioxuslabs.com/docs/0.3/guide/en/) |
|
||||
[API Docs](https://docs.rs/dioxus-autofmt/latest/dioxus_autofmt) |
|
||||
[Guides](https://dioxuslabs.com/learn/0.4/) |
|
||||
[API Docs](https://docs.rs/dioxus-check) |
|
||||
[Chat](https://discord.gg/XgGxMSkvUM)
|
||||
|
||||
|
||||
## Overview
|
||||
|
||||
`dioxus-check` analyzes Dioxus source code and reports errors and warnings. Primarily, it enforces the [Rules of Hooks](https://dioxuslabs.com/docs/0.3/guide/en/interactivity/hooks.html#no-hooks-in-conditionals).
|
||||
`dioxus-check` analyzes Dioxus source code and reports errors and warnings. Primarily, it enforces the [Rules of Hooks](https://dioxuslabs.com/learn/0.4/reference/hooks#rules-of-hooks).
|
||||
|
||||
## Contributing
|
||||
|
||||
@@ -34,6 +29,7 @@
|
||||
- Join the discord and ask questions!
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the [MIT license].
|
||||
|
||||
[mit license]: https://github.com/DioxusLabs/dioxus/blob/master/LICENSE-MIT
|
||||
|
||||
@@ -158,11 +158,11 @@ impl Display for IssueReport {
|
||||
#[allow(clippy::enum_variant_names)] // we'll add non-hook ones in the future
|
||||
/// Issues that might be found via static analysis of a Dioxus file.
|
||||
pub enum Issue {
|
||||
/// https://dioxuslabs.com/docs/0.3/guide/en/interactivity/hooks.html#no-hooks-in-conditionals
|
||||
/// https://dioxuslabs.com/learn/0.4/reference/hooks#no-hooks-in-conditionals
|
||||
HookInsideConditional(HookInfo, ConditionalInfo),
|
||||
/// https://dioxuslabs.com/docs/0.3/guide/en/interactivity/hooks.html#no-hooks-in-loops
|
||||
/// https://dioxuslabs.com/learn/0.4/reference/hooks#no-hooks-in-loops
|
||||
HookInsideLoop(HookInfo, AnyLoopInfo),
|
||||
/// https://dioxuslabs.com/docs/0.3/guide/en/interactivity/hooks.html#no-hooks-in-closures
|
||||
/// https://dioxuslabs.com/learn/0.4/reference/hooks#no-hooks-in-closures
|
||||
HookInsideClosure(HookInfo, ClosureInfo),
|
||||
HookOutsideComponent(HookInfo),
|
||||
}
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
#![doc = include_str!("../README.md")]
|
||||
#![doc(html_logo_url = "https://avatars.githubusercontent.com/u/79236386")]
|
||||
#![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")]
|
||||
|
||||
mod check;
|
||||
mod issues;
|
||||
mod metadata;
|
||||
|
||||
-31
@@ -1,31 +0,0 @@
|
||||
# .github/workflows/build.yml
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: release ${{ matrix.target }}
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- target: x86_64-unknown-linux-gnu
|
||||
archive: tar.gz tar.xz
|
||||
- target: x86_64-unknown-linux-musl
|
||||
archive: tar.gz tar.xz
|
||||
- target: x86_64-apple-darwin
|
||||
archive: tar.gz tar.xz
|
||||
- target: x86_64-pc-windows-gnu
|
||||
archive: zip
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- name: Compile and release
|
||||
uses: rust-build/rust-build.action@latest
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
RUSTTARGET: ${{ matrix.target }}
|
||||
ARCHIVE_TYPES: ${{ matrix.archive }}
|
||||
-34
@@ -1,34 +0,0 @@
|
||||
name: github pages
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- docs/**
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-20.04
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Setup mdBook
|
||||
uses: peaceiris/actions-mdbook@v1
|
||||
with:
|
||||
mdbook-version: '0.4.10'
|
||||
# mdbook-version: 'latest'
|
||||
|
||||
- run: cd docs && mdbook build
|
||||
|
||||
- name: Deploy 🚀
|
||||
uses: JamesIves/github-pages-deploy-action@v4.2.3
|
||||
with:
|
||||
branch: gh-pages # The branch the action should deploy to.
|
||||
folder: docs/book # The folder the action should deploy.
|
||||
target-folder: docs/nightly/cli
|
||||
repository-name: dioxuslabs/docsite
|
||||
clean: false
|
||||
token: ${{ secrets.DEPLOY_KEY }} # let's pretend I don't need it for now
|
||||
-52
@@ -1,52 +0,0 @@
|
||||
on: [push, pull_request]
|
||||
|
||||
name: Rust CI
|
||||
|
||||
jobs:
|
||||
check:
|
||||
name: Check
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
- uses: Swatinem/rust-cache@v1
|
||||
- run: cargo check
|
||||
|
||||
test:
|
||||
name: Test Suite
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
- uses: Swatinem/rust-cache@v1
|
||||
- run: cargo test
|
||||
|
||||
fmt:
|
||||
name: Rustfmt
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Install Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
- uses: Swatinem/rust-cache@v1
|
||||
- run: rustup component add rustfmt
|
||||
- run: cargo fmt --all -- --check
|
||||
|
||||
# clippy:
|
||||
# name: Clippy
|
||||
# runs-on: ubuntu-latest
|
||||
# steps:
|
||||
# - uses: actions/checkout@v2
|
||||
# - uses: actions-rs/toolchain@v1
|
||||
# with:
|
||||
# profile: minimal
|
||||
# toolchain: stable
|
||||
# override: true
|
||||
# - uses: Swatinem/rust-cache@v1
|
||||
# - run: rustup component add clippy
|
||||
# - uses: actions-rs/cargo@v1
|
||||
# with:
|
||||
# command: clippy
|
||||
# args: -- -D warnings
|
||||
Generated
-4778
File diff suppressed because it is too large.
Load diff
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "dioxus-cli"
|
||||
version = "0.4.1"
|
||||
version = "0.4.3"
|
||||
authors = ["Jonathan Kelley"]
|
||||
edition = "2021"
|
||||
description = "CLI tool for developing, testing, and publishing Dioxus apps"
|
||||
@@ -22,16 +22,13 @@ serde = { version = "1.0.136", features = ["derive"] }
|
||||
serde_json = "1.0.79"
|
||||
toml = "0.5.8"
|
||||
fs_extra = "1.2.0"
|
||||
cargo_toml = "0.11.4"
|
||||
cargo_toml = "0.16.0"
|
||||
futures = "0.3.21"
|
||||
notify = { version = "5.0.0-pre.16", features = ["serde"] }
|
||||
html_parser = { workspace = true }
|
||||
binary-install = "0.0.2"
|
||||
convert_case = "0.5.0"
|
||||
cargo_metadata = "0.15.0"
|
||||
tokio = { version = "1.16.1", features = ["full"] }
|
||||
tokio = { version = "1.16.1", features = ["fs", "sync", "rt", "macros"] }
|
||||
atty = "0.2.14"
|
||||
regex = "1.5.4"
|
||||
chrono = "0.4.19"
|
||||
anyhow = "1.0.53"
|
||||
hyper = "0.14.17"
|
||||
@@ -59,7 +56,6 @@ tar = "0.4.38"
|
||||
zip = "0.6.2"
|
||||
tower = "0.4.12"
|
||||
syn = { version = "2.0", features = ["full", "extra-traits"] }
|
||||
proc-macro2 = { version = "1.0", features = ["span-locations"] }
|
||||
lazy_static = "1.4.0"
|
||||
|
||||
# plugin packages
|
||||
@@ -71,7 +67,6 @@ mlua = { version = "0.8.1", features = [
|
||||
"macros",
|
||||
], optional = true }
|
||||
ctrlc = "3.2.3"
|
||||
gitignore = "1.0.7"
|
||||
open = "4.1.0"
|
||||
cargo-generate = "0.18"
|
||||
toml_edit = "0.19.11"
|
||||
@@ -88,6 +83,7 @@ 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" }
|
||||
gitignore = "1.0.8"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
|
||||
@@ -4,14 +4,14 @@
|
||||
</div>
|
||||
|
||||
The **dioxus-cli** (inspired by wasm-pack and webpack) is a tool for getting Dioxus projects up and running.
|
||||
It handles all building, bundling, development and publishing to simplify development.
|
||||
It handles building, bundling, development and publishing to simplify development.
|
||||
|
||||
## Installation
|
||||
|
||||
### Install the stable version (recommended)
|
||||
|
||||
```
|
||||
cargo install dioxus-cli --locked
|
||||
```shell
|
||||
cargo install dioxus-cli
|
||||
```
|
||||
|
||||
### Install the latest development build through git
|
||||
@@ -20,7 +20,7 @@ To get the latest bug fixes and features, you can install the development versio
|
||||
However, this is not fully tested.
|
||||
That means you're probably going to have more bugs despite having the latest bug fixes.
|
||||
|
||||
```
|
||||
```shell
|
||||
cargo install --git https://github.com/DioxusLabs/dioxus dioxus-cli
|
||||
```
|
||||
|
||||
@@ -29,7 +29,7 @@ and install it in Cargo's global binary directory (`~/.cargo/bin/` by default).
|
||||
|
||||
### Install from local folder
|
||||
|
||||
```
|
||||
```shell
|
||||
cargo install --path . --debug
|
||||
```
|
||||
|
||||
@@ -40,7 +40,7 @@ It will be cloned from the [dioxus-template](https://github.com/DioxusLabs/dioxu
|
||||
|
||||
Alternatively, you can specify the template path:
|
||||
|
||||
```
|
||||
```shell
|
||||
dx create hello --template gh:dioxuslabs/dioxus-template
|
||||
```
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
book
|
||||
@@ -1,6 +0,0 @@
|
||||
[book]
|
||||
authors = ["YuKun Liu"]
|
||||
language = "en"
|
||||
multilingual = false
|
||||
src = "src"
|
||||
title = "Dioxus CLI"
|
||||
@@ -1,13 +0,0 @@
|
||||
# Summary
|
||||
|
||||
- [Introduction](./introduction.md)
|
||||
- [Installation](./installation.md)
|
||||
- [Create a project](./creating.md)
|
||||
- [Configure a project](./configure.md)
|
||||
- [Plugin development](./plugin/README.md)
|
||||
- [API.Log](plugin/interface/log.md)
|
||||
- [API.Command](plugin/interface/command.md)
|
||||
- [API.OS](plugin/interface/os.md)
|
||||
- [API.Directories](plugin/interface/dirs.md)
|
||||
- [API.Network](plugin/interface/network.md)
|
||||
- [API.Path](plugin/interface/path.md)
|
||||
@@ -1,197 +0,0 @@
|
||||
# Configure Project
|
||||
|
||||
This chapter will teach you how to configure the CLI with the `Dioxus.toml` file.
|
||||
There's an [example](#config-example) which has comments to describe individual keys.
|
||||
You can copy that or view this documentation for a more complete learning experience.
|
||||
|
||||
"🔒" indicates a mandatory item. Some headers are mandatory, but none of the keys inside them are. It might look weird, but it's normal. Simply don't include any keys.
|
||||
|
||||
## Structure
|
||||
|
||||
Each header has it's TOML form directly under it.
|
||||
|
||||
### Application 🔒
|
||||
|
||||
```toml
|
||||
[application]
|
||||
```
|
||||
|
||||
Application-wide configuration. Applies to both web and desktop.
|
||||
|
||||
1. **name** 🔒 - Project name & title.
|
||||
```toml
|
||||
name = "my_project"
|
||||
```
|
||||
|
||||
2. **default_platform** 🔒 - The platform this project targets
|
||||
```toml
|
||||
# Currently supported platforms: web, desktop
|
||||
default_platform = "web"
|
||||
```
|
||||
|
||||
3. **out_dir** - The directory to place the build artifacts from `dx build` or `dx serve` into. This is also where the `assets` directory will be copied into.
|
||||
```toml
|
||||
out_dir = "dist"
|
||||
```
|
||||
|
||||
4. **asset_dir** - The directory with your static assets. The CLI will automatically copy these assets into the **out_dir** after a build/serve.
|
||||
```toml
|
||||
asset_dir = "public"
|
||||
```
|
||||
|
||||
5. **sub_package** - The sub package in the workspace to build by default.
|
||||
```toml
|
||||
sub_package = "my-crate"
|
||||
```
|
||||
|
||||
### Web.App 🔒
|
||||
|
||||
```toml
|
||||
[web.app]
|
||||
```
|
||||
|
||||
Web-specific configuration.
|
||||
|
||||
1. **title** - The title of the web page.
|
||||
```toml
|
||||
# HTML title tag content
|
||||
title = "project_name"
|
||||
```
|
||||
|
||||
2. **base_path** - The base path to build the application for serving at. This can be useful when serving your application in a subdirectory under a domain. For example when building a site to be served on GitHub Pages.
|
||||
```toml
|
||||
# The application will be served at domain.com/my_application/, so we need to modify the base_path to the path where the application will be served
|
||||
base_path = "my_application"
|
||||
```
|
||||
|
||||
### Web.Watcher ✍
|
||||
|
||||
```toml
|
||||
[web.watcher]
|
||||
```
|
||||
|
||||
Development server configuration.
|
||||
|
||||
1. **reload_html** - If this is true, the cli will rebuild the index.html file every time the application is rebuilt
|
||||
```toml
|
||||
reload_html = true
|
||||
```
|
||||
|
||||
2. **watch_path** - The files & directories to monitor for changes
|
||||
```toml
|
||||
watch_path = ["src", "public"]
|
||||
```
|
||||
|
||||
3. **index_on_404** - If enabled, Dioxus will serve the root page when a route is not found.
|
||||
*This is needed when serving an application that uses the router*.
|
||||
However, when serving your app using something else than Dioxus (e.g. GitHub Pages), you will have to check how to configure it on that platform.
|
||||
In GitHub Pages, you can make a copy of `index.html` named `404.html` in the same directory.
|
||||
```toml
|
||||
index_on_404 = true
|
||||
```
|
||||
|
||||
### Web.Resource 🔒
|
||||
|
||||
```toml
|
||||
[web.resource]
|
||||
```
|
||||
|
||||
Static resource configuration.
|
||||
|
||||
1. **style** - CSS files to include in your application.
|
||||
```toml
|
||||
style = [
|
||||
# Include from public_dir.
|
||||
"./assets/style.css",
|
||||
# Or some asset from online cdn.
|
||||
"https://cdn.jsdelivr.net/npm/bootstrap/dist/css/bootstrap.css"
|
||||
]
|
||||
```
|
||||
|
||||
2. **script** - JavaScript files to include in your application.
|
||||
```toml
|
||||
script = [
|
||||
# Include from asset_dir.
|
||||
"./public/index.js",
|
||||
# Or from an online CDN.
|
||||
"https://cdn.jsdelivr.net/npm/bootstrap/dist/js/bootstrap.js"
|
||||
]
|
||||
```
|
||||
|
||||
### Web.Resource.Dev 🔒
|
||||
|
||||
```toml
|
||||
[web.resource.dev]
|
||||
```
|
||||
|
||||
This is the same as [`[web.resource]`](#webresource-), but it only works in development servers.
|
||||
For example, if you want to include a file in a `dx serve` server, but not a `dx serve --release` server, put it here.
|
||||
|
||||
### Web.Proxy
|
||||
|
||||
```toml
|
||||
[[web.proxy]]
|
||||
```
|
||||
|
||||
Configuration related to any proxies your application requires during development. Proxies will forward requests to a new service.
|
||||
|
||||
1. **backend** - The URL to the server to proxy. The CLI will forward any requests under the backend relative route to the backend instead of returning 404
|
||||
```toml
|
||||
backend = "http://localhost:8000/api/"
|
||||
```
|
||||
This will cause any requests made to the dev server with prefix /api/ to be redirected to the backend server at http://localhost:8000. The path and query parameters will be passed on as-is (path rewriting is currently not supported).
|
||||
|
||||
## Config example
|
||||
|
||||
This includes all fields, mandatory or not.
|
||||
|
||||
```toml
|
||||
[application]
|
||||
|
||||
# App name
|
||||
name = "project_name"
|
||||
|
||||
# The Dioxus platform to default to
|
||||
default_platform = "web"
|
||||
|
||||
# `build` & `serve` output path
|
||||
out_dir = "dist"
|
||||
|
||||
# The static resource path
|
||||
asset_dir = "public"
|
||||
|
||||
[web.app]
|
||||
|
||||
# HTML title tag content
|
||||
title = "project_name"
|
||||
|
||||
[web.watcher]
|
||||
|
||||
# When watcher is triggered, regenerate the `index.html`
|
||||
reload_html = true
|
||||
|
||||
# Which files or dirs will be monitored
|
||||
watch_path = ["src", "public"]
|
||||
|
||||
# Include style or script assets
|
||||
[web.resource]
|
||||
|
||||
# CSS style file
|
||||
style = []
|
||||
|
||||
# Javascript code file
|
||||
script = []
|
||||
|
||||
[web.resource.dev]
|
||||
|
||||
# Same as [web.resource], but for development servers
|
||||
|
||||
# CSS style file
|
||||
style = []
|
||||
|
||||
# JavaScript files
|
||||
script = []
|
||||
|
||||
[[web.proxy]]
|
||||
backend = "http://localhost:8000/api/"
|
||||
```
|
||||
@@ -1,37 +0,0 @@
|
||||
# Create a Project
|
||||
|
||||
Once you have the Dioxus CLI installed, you can use it to create your own project!
|
||||
|
||||
## Initializing a default project
|
||||
|
||||
First, run the `dx create` command to create a new project:
|
||||
```
|
||||
dx create hello-dioxus
|
||||
```
|
||||
|
||||
> It will clone this [template](https://github.com/DioxusLabs/dioxus-template).
|
||||
> This default template is used for `web` platform application.
|
||||
>
|
||||
> You can choose to create your project from a different template by passing the `template` argument:
|
||||
> ```
|
||||
> dx init hello-dioxus --template=gh:dioxuslabs/dioxus-template
|
||||
> ```
|
||||
|
||||
Next, navigate into your new project:
|
||||
|
||||
```
|
||||
cd hello-dioxus
|
||||
```
|
||||
|
||||
> Make sure the WASM target is installed before running the projects.
|
||||
> You can install the WASM target for rust using rustup:
|
||||
> ```
|
||||
> rustup target add wasm32-unknown-unknown
|
||||
> ```
|
||||
|
||||
Finally, serve your project:
|
||||
```
|
||||
dx serve
|
||||
```
|
||||
|
||||
By default, the CLI serves your website at [`http://127.0.0.1:8080/`](http://127.0.0.1:8080/).
|
||||
@@ -1,23 +0,0 @@
|
||||
# Installation
|
||||
|
||||
## Install the latest development build through git
|
||||
|
||||
To get the latest bug fixes and features, you can install the development version from git.
|
||||
|
||||
```
|
||||
cargo install --git https://github.com/Dioxuslabs/cli
|
||||
```
|
||||
|
||||
This will download `Dioxus-CLI` source from GitHub master branch,
|
||||
and install it in Cargo's global binary directory (`~/.cargo/bin/` by default).
|
||||
|
||||
## Install stable through `crates.io`
|
||||
|
||||
The published version of the Dioxus CLI is updated less often, but is more stable than the git version.
|
||||
|
||||
```
|
||||
cargo install dioxus-cli --locked
|
||||
```
|
||||
|
||||
Run `dx --help` for a list of all the available commands.
|
||||
Furthermore, you can run `dx <COMMAND> --help` to get help with a specific command.
|
||||
@@ -1,18 +0,0 @@
|
||||
# Introduction
|
||||
|
||||
The 📦✨ **Dioxus CLI** is a tool to help get Dioxus projects off the ground.
|
||||
|
||||
## Features
|
||||
|
||||
* Build and pack a Dioxus project
|
||||
* `html` to `rsx` conversion tool
|
||||
* Hot Reload for `web` platform
|
||||
* Create a Dioxus project from `git` repo
|
||||
* And more!
|
||||
<!-- Checkmarks don't render on the website, so I've just made a normal list. You can uncomment this if the website rendering is fixed.
|
||||
- [x] `html` to `rsx` conversion tool
|
||||
- [x] Hot Reload for `web` platform
|
||||
- [x] Create a Dioxus project from `git` repo
|
||||
- [x] Build & pack Dioxus project
|
||||
- [ ] Automatically format Dioxus `rsx` code
|
||||
-->
|
||||
@@ -1,140 +0,0 @@
|
||||
# CLI Plugin development
|
||||
|
||||
**IMPORTANT: Ignore this documentation. Plugins are yet to be released and chances are it won't work for you. This is just what plugins *could* look like.**
|
||||
|
||||
In the past we used `dx tool` to use and install tools, but it was a flawed system.
|
||||
Tools were hard-coded by us, but people want more tools than we could code, so this plugin system was made to let
|
||||
anyone develop plugins and use them in Dioxus projects.
|
||||
|
||||
Plugin resources:
|
||||
* [Source code](https://github.com/DioxusLabs/dioxus/tree/master/packages/cli/src/plugin)
|
||||
* [Unofficial Dioxus plugin community](https://github.com/DioxusPluginCommunity). Contains certain plugins you can use right now.
|
||||
|
||||
### Why Lua?
|
||||
|
||||
We chose Lua `5.4` to be the plugin developing language,
|
||||
because it's extremely lightweight, embeddable and easy to learn.
|
||||
We installed Lua into the CLI, so you don't need to do it yourself.
|
||||
|
||||
Lua resources:
|
||||
* [Official website](https://www.lua.org/). You can basically find everything here.
|
||||
* [Awesome Lua](https://github.com/LewisJEllis/awesome-lua). Additional resources (such as Lua plugins for your favorite IDE), and other *awesome* tools!
|
||||
|
||||
|
||||
## Creating a plugin
|
||||
|
||||
A plugin is just an `init.lua` file.
|
||||
You can include other files using `dofile(path)`.
|
||||
You need to have a plugin and a manager instance, which you can get using `require`:
|
||||
```lua
|
||||
local plugin = require("plugin")
|
||||
local manager = require("manager")
|
||||
```
|
||||
|
||||
You need to set some `manager` fields and then initialize the plugin:
|
||||
```lua
|
||||
manager.name = "My first plugin"
|
||||
manager.repository = "https://github.com/john-doe/my-first-plugin" -- The repository URL.
|
||||
manager.author = "John Doe <john.doe@example.com>"
|
||||
manager.version = "0.1.0"
|
||||
plugin.init(manager)
|
||||
```
|
||||
|
||||
You also need to return the `manager`, which basically represents your plugin:
|
||||
```lua
|
||||
-- Your code here.
|
||||
-- End of file.
|
||||
|
||||
manager.serve.interval = 1000
|
||||
return manager
|
||||
```
|
||||
|
||||
And you're ready to go. Now, go and have a look at the stuff below and the API documentation.
|
||||
|
||||
### Plugin info
|
||||
|
||||
You will encounter this type in the events below. The keys are as follows:
|
||||
* `name: string` - The name of the plugin.
|
||||
* `repository: string` - The plugin repository URL.
|
||||
* `author: string` - The author of the plugin.
|
||||
* `version: string` - The plugin version.
|
||||
|
||||
### Event management
|
||||
|
||||
The plugin library has certain events that you can subscribe to.
|
||||
|
||||
* `manager.on_init` - Triggers the first time the plugin is loaded.
|
||||
* `manager.build.on_start(info)` - Triggers before the build process. E.g., before `dx build`.
|
||||
* `manager.build.on_finish(info)` - Triggers after the build process. E.g., after `dx build`.
|
||||
* `manager.serve.on_start(info)` - Triggers before the serving process. E.g., before `dx serve`.
|
||||
* `manager.serve.on_rebuild_start(info)` - Triggers before the server rebuilds the web with hot reload.
|
||||
* `manager.serve.on_rebuild_end(info)` - Triggers after the server rebuilds the web with hot reload.
|
||||
* `manager.serve.on_shutdown` - Triggers when the server is shutdown. E.g., when the `dx serve` process is terminated.
|
||||
|
||||
To subscribe to an event, you simply need to assign it to a function:
|
||||
|
||||
```lua
|
||||
manager.build.on_start = function (info)
|
||||
log.info("[plugin] Build starting: " .. info.name)
|
||||
end
|
||||
```
|
||||
|
||||
### Plugin template
|
||||
|
||||
```lua
|
||||
package.path = library_dir .. "/?.lua"
|
||||
|
||||
local plugin = require("plugin")
|
||||
local manager = require("manager")
|
||||
|
||||
-- deconstruct api functions
|
||||
local log = plugin.log
|
||||
|
||||
-- plugin information
|
||||
manager.name = "Hello Dixous Plugin"
|
||||
manager.repository = "https://github.com/mrxiaozhuox/hello-dioxus-plugin"
|
||||
manager.author = "YuKun Liu <mrxzx.info@gmail.com>"
|
||||
manager.version = "0.0.1"
|
||||
|
||||
-- init manager info to plugin api
|
||||
plugin.init(manager)
|
||||
|
||||
manager.on_init = function ()
|
||||
-- when the first time plugin been load, this function will be execute.
|
||||
-- system will create a `dcp.json` file to verify init state.
|
||||
log.info("[plugin] Start to init plugin: " .. manager.name)
|
||||
end
|
||||
|
||||
---@param info BuildInfo
|
||||
manager.build.on_start = function (info)
|
||||
-- before the build work start, system will execute this function.
|
||||
log.info("[plugin] Build starting: " .. info.name)
|
||||
end
|
||||
|
||||
---@param info BuildInfo
|
||||
manager.build.on_finish = function (info)
|
||||
-- when the build work is done, system will execute this function.
|
||||
log.info("[plugin] Build finished: " .. info.name)
|
||||
end
|
||||
|
||||
---@param info ServeStartInfo
|
||||
manager.serve.on_start = function (info)
|
||||
-- this function will after clean & print to run, so you can print some thing.
|
||||
log.info("[plugin] Serve start: " .. info.name)
|
||||
end
|
||||
|
||||
---@param info ServeRebuildInfo
|
||||
manager.serve.on_rebuild = function (info)
|
||||
-- this function will after clean & print to run, so you can print some thing.
|
||||
local files = plugin.tool.dump(info.changed_files)
|
||||
log.info("[plugin] Serve rebuild: '" .. files .. "'")
|
||||
end
|
||||
|
||||
manager.serve.on_shutdown = function ()
|
||||
log.info("[plugin] Serve shutdown")
|
||||
end
|
||||
|
||||
manager.serve.interval = 1000
|
||||
|
||||
return manager
|
||||
```
|
||||
@@ -1,21 +0,0 @@
|
||||
# Command Functions
|
||||
|
||||
You can use command functions to execute code and scripts.
|
||||
|
||||
Type definition:
|
||||
```
|
||||
Stdio: "Inherit" | "Piped" | "Null"
|
||||
```
|
||||
|
||||
### `exec(commands: [string], stdout: Stdio, stderr: Stdio)`
|
||||
|
||||
You can use this function to run some commands on the current system.
|
||||
|
||||
```lua
|
||||
local cmd = plugin.command
|
||||
|
||||
manager.test = function ()
|
||||
cmd.exec({"git", "clone", "https://github.com/DioxusLabs/cli-plugin-library"})
|
||||
end
|
||||
```
|
||||
> Warning: This function doesn't catch exceptions.
|
||||
@@ -1,30 +0,0 @@
|
||||
# Dirs Functions
|
||||
|
||||
Dirs functions are for getting various directory paths. Not to be confused with `plugin.path`.
|
||||
|
||||
### `plugin_dir() -> string`
|
||||
|
||||
Get the plugin's root directory path.
|
||||
|
||||
```lua
|
||||
local path = plugin.dirs.plugin_dir()
|
||||
-- example: ~/Development/DioxusCli/plugin/test-plugin/
|
||||
```
|
||||
|
||||
### `bin_dir() -> string`
|
||||
|
||||
Get the plugin's binary directory path. Put binary files like `tailwind-cli` or `sass-cli` in this directory.
|
||||
|
||||
```lua
|
||||
local path = plugin.dirs.bin_dir()
|
||||
-- example: ~/Development/DioxusCli/plugin/test-plugin/bin/
|
||||
```
|
||||
|
||||
### `temp_dir() -> string`
|
||||
|
||||
Get the plugin's temporary directory path. Put any temporary files here.
|
||||
|
||||
```lua
|
||||
local path = plugin.dirs.bin_dir()
|
||||
-- example: ~/Development/DioxusCli/plugin/test-plugin/temp/
|
||||
```
|
||||
@@ -1,48 +0,0 @@
|
||||
# Log Functions
|
||||
|
||||
You can use log functions to print various logging information.
|
||||
|
||||
### `trace(info: string)`
|
||||
|
||||
Print trace log info.
|
||||
|
||||
```lua
|
||||
local log = plugin.log
|
||||
log.trace("trace information")
|
||||
```
|
||||
|
||||
### `debug(info: string)`
|
||||
|
||||
Print debug log info.
|
||||
|
||||
```lua
|
||||
local log = plugin.log
|
||||
log.debug("debug information")
|
||||
```
|
||||
|
||||
### `info(info: string)`
|
||||
|
||||
Print info log info.
|
||||
|
||||
```lua
|
||||
local log = plugin.log
|
||||
log.info("info information")
|
||||
```
|
||||
|
||||
### `warn(info: string)`
|
||||
|
||||
Print warning log info.
|
||||
|
||||
```lua
|
||||
local log = plugin.log
|
||||
log.warn("warn information")
|
||||
```
|
||||
|
||||
### `error(info: string)`
|
||||
|
||||
Print error log info.
|
||||
|
||||
```lua
|
||||
local log = plugin.log
|
||||
log.error("error information")
|
||||
```
|
||||
@@ -1,37 +0,0 @@
|
||||
# Network Functions
|
||||
|
||||
You can use Network functions to download & read some data from the internet.
|
||||
|
||||
### `download_file(url: string, path: string) -> boolean`
|
||||
|
||||
Downloads a file from the specified URL,
|
||||
and returns a `boolean` that represents the download status (true: success, false: failure).
|
||||
|
||||
You need to pass a target URL and a local path (where you want to save this file).
|
||||
|
||||
```lua
|
||||
-- this file will download to plugin temp directory
|
||||
local status = plugin.network.download_file(
|
||||
"http://xxx.com/xxx.zip",
|
||||
plugin.dirs.temp_dir()
|
||||
)
|
||||
if status != true then
|
||||
log.error("Download Failed")
|
||||
end
|
||||
```
|
||||
|
||||
### `clone_repo(url: string, path: string) -> boolean`
|
||||
|
||||
Clone a repository from the given URL into the given path.
|
||||
Returns a `boolean` that represents the clone status (true: success, false: failure).
|
||||
The system executing this function must have git installed.
|
||||
|
||||
```lua
|
||||
local status = plugin.network.clone_repo(
|
||||
"http://github.com/mrxiaozhuox/dioxus-starter",
|
||||
plugin.dirs.bin_dir()
|
||||
)
|
||||
if status != true then
|
||||
log.error("Clone Failed")
|
||||
end
|
||||
```
|
||||
@@ -1,11 +0,0 @@
|
||||
# OS Functions
|
||||
|
||||
OS functions are for getting system information.
|
||||
|
||||
### `current_platform() -> string ("windows" | "macos" | "linux")`
|
||||
|
||||
Get the current OS platform.
|
||||
|
||||
```lua
|
||||
local platform = plugin.os.current_platform()
|
||||
```
|
||||
@@ -1,38 +0,0 @@
|
||||
# Path Functions
|
||||
|
||||
You can use path functions to perform operations on valid path strings.
|
||||
|
||||
### `join(path: string, extra: string) -> string`
|
||||
|
||||
<!-- TODO: Add specifics.
|
||||
From the example given, it seems like it just creates a subdirectory path.
|
||||
What would it do when "extending" file paths? -->
|
||||
Extend a path; you can extend both directory and file paths.
|
||||
|
||||
```lua
|
||||
local current_path = "~/hello/dioxus"
|
||||
local new_path = plugin.path.join(current_path, "world")
|
||||
-- new_path = "~/hello/dioxus/world"
|
||||
```
|
||||
|
||||
### `parent(path: string) -> string`
|
||||
|
||||
Return the parent path of the specified path. The parent path is always a directory.
|
||||
|
||||
```lua
|
||||
local current_path = "~/hello/dioxus"
|
||||
local new_path = plugin.path.parent(current_path)
|
||||
-- new_path = "~/hello/"
|
||||
```
|
||||
|
||||
### `exists(path: string) -> boolean`
|
||||
|
||||
Check if the specified path exists, as either a file or a directory.
|
||||
|
||||
### `is_file(path: string) -> boolean`
|
||||
|
||||
Check if the specified path is a file.
|
||||
|
||||
### `is_dir(path: string) -> boolean`
|
||||
|
||||
Check if the specified path is a directory.
|
||||
@@ -1,18 +0,0 @@
|
||||
local Api = require("./interface")
|
||||
local log = Api.log;
|
||||
|
||||
local manager = {
|
||||
name = "Dioxus-CLI Plugin Demo",
|
||||
repository = "http://github.com/DioxusLabs/cli",
|
||||
author = "YuKun Liu <mrxzx.info@gmail.com>",
|
||||
}
|
||||
|
||||
manager.onLoad = function ()
|
||||
log.info("plugin loaded.")
|
||||
end
|
||||
|
||||
manager.onStartBuild = function ()
|
||||
log.warn("system start to build")
|
||||
end
|
||||
|
||||
return manager
|
||||
@@ -1,25 +0,0 @@
|
||||
local interface = {}
|
||||
|
||||
if plugin_logger ~= nil then
|
||||
interface.log = plugin_logger
|
||||
else
|
||||
interface.log = {
|
||||
trace = function (info)
|
||||
print("trace: " .. info)
|
||||
end,
|
||||
debug = function (info)
|
||||
print("debug: " .. info)
|
||||
end,
|
||||
info = function (info)
|
||||
print("info: " .. info)
|
||||
end,
|
||||
warn = function (info)
|
||||
print("warn: " .. info)
|
||||
end,
|
||||
error = function (info)
|
||||
print("error: " .. info)
|
||||
end,
|
||||
}
|
||||
end
|
||||
|
||||
return interface
|
||||
@@ -23,7 +23,7 @@ title = "Dioxus | An elegant GUI library for Rust"
|
||||
|
||||
index_on_404 = true
|
||||
|
||||
watch_path = ["src"]
|
||||
watch_path = ["src", "examples"]
|
||||
|
||||
# include `assets` in web platform
|
||||
[web.resource]
|
||||
|
||||
+39
-43
@@ -48,14 +48,25 @@ pub fn build(config: &CrateConfig, quiet: bool) -> Result<BuildResult> {
|
||||
|
||||
// [1] Build the .wasm module
|
||||
log::info!("🚅 Running build command...");
|
||||
|
||||
let wasm_check_command = std::process::Command::new("rustup")
|
||||
.args(["show"])
|
||||
.output()?;
|
||||
let wasm_check_output = String::from_utf8(wasm_check_command.stdout).unwrap();
|
||||
if !wasm_check_output.contains("wasm32-unknown-unknown") {
|
||||
log::info!("wasm32-unknown-unknown target not detected, installing..");
|
||||
let _ = std::process::Command::new("rustup")
|
||||
.args(["target", "add", "wasm32-unknown-unknown"])
|
||||
.output()?;
|
||||
}
|
||||
|
||||
let cmd = subprocess::Exec::cmd("cargo");
|
||||
let cmd = cmd
|
||||
.cwd(crate_dir)
|
||||
.arg("build")
|
||||
.arg("--target")
|
||||
.arg("wasm32-unknown-unknown")
|
||||
.arg("--message-format=json")
|
||||
.arg("--quiet");
|
||||
.arg("--message-format=json");
|
||||
|
||||
let cmd = if config.release {
|
||||
cmd.arg("--release")
|
||||
@@ -65,7 +76,7 @@ pub fn build(config: &CrateConfig, quiet: bool) -> Result<BuildResult> {
|
||||
let cmd = if config.verbose {
|
||||
cmd.arg("--verbose")
|
||||
} else {
|
||||
cmd
|
||||
cmd.arg("--quiet")
|
||||
};
|
||||
|
||||
let cmd = if config.custom_profile.is_some() {
|
||||
@@ -82,6 +93,8 @@ pub fn build(config: &CrateConfig, quiet: bool) -> Result<BuildResult> {
|
||||
cmd
|
||||
};
|
||||
|
||||
let cmd = cmd.args(&config.cargo_args);
|
||||
|
||||
let cmd = match executable {
|
||||
ExecutableType::Binary(name) => cmd.arg("--bin").arg(name),
|
||||
ExecutableType::Lib(name) => cmd.arg("--lib").arg(name),
|
||||
@@ -261,6 +274,8 @@ pub fn build_desktop(config: &CrateConfig, _is_serve: bool) -> Result<BuildResul
|
||||
}
|
||||
if config.verbose {
|
||||
cmd = cmd.arg("--verbose");
|
||||
} else {
|
||||
cmd = cmd.arg("--quiet");
|
||||
}
|
||||
|
||||
if config.custom_profile.is_some() {
|
||||
@@ -273,6 +288,14 @@ pub fn build_desktop(config: &CrateConfig, _is_serve: bool) -> Result<BuildResul
|
||||
cmd = cmd.arg("--features").arg(features_str);
|
||||
}
|
||||
|
||||
if let Some(target) = &config.target {
|
||||
cmd = cmd.arg("--target").arg(target);
|
||||
}
|
||||
|
||||
let target_platform = config.target.as_deref().unwrap_or("");
|
||||
|
||||
cmd = cmd.args(&config.cargo_args);
|
||||
|
||||
let cmd = match &config.executable {
|
||||
crate::ExecutableType::Binary(name) => cmd.arg("--bin").arg(name),
|
||||
crate::ExecutableType::Lib(name) => cmd.arg("--lib").arg(name),
|
||||
@@ -290,12 +313,17 @@ pub fn build_desktop(config: &CrateConfig, _is_serve: bool) -> Result<BuildResul
|
||||
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)
|
||||
config
|
||||
.target_dir
|
||||
.join(target_platform)
|
||||
.join(release_type)
|
||||
.join(name)
|
||||
}
|
||||
crate::ExecutableType::Example(name) => {
|
||||
file_name = name.clone();
|
||||
config
|
||||
.target_dir
|
||||
.join(target_platform)
|
||||
.join(release_type)
|
||||
.join("examples")
|
||||
.join(name)
|
||||
@@ -312,7 +340,7 @@ pub fn build_desktop(config: &CrateConfig, _is_serve: bool) -> Result<BuildResul
|
||||
if !config.out_dir.is_dir() {
|
||||
create_dir_all(&config.out_dir)?;
|
||||
}
|
||||
copy(res_path, &config.out_dir.join(target_file))?;
|
||||
copy(res_path, config.out_dir.join(target_file))?;
|
||||
|
||||
// this code will copy all public file to the output dir
|
||||
if config.asset_dir.is_dir() {
|
||||
@@ -442,14 +470,14 @@ pub fn gen_page(config: &DioxusConfig, serve: bool) -> String {
|
||||
String::from(include_str!("./assets/index.html"))
|
||||
};
|
||||
|
||||
let resouces = config.web.resource.clone();
|
||||
let resources = config.web.resource.clone();
|
||||
|
||||
let mut style_list = resouces.style.unwrap_or_default();
|
||||
let mut script_list = resouces.script.unwrap_or_default();
|
||||
let mut style_list = resources.style.unwrap_or_default();
|
||||
let mut script_list = resources.script.unwrap_or_default();
|
||||
|
||||
if serve {
|
||||
let mut dev_style = resouces.dev.style.clone().unwrap_or_default();
|
||||
let mut dev_script = resouces.dev.script.unwrap_or_default();
|
||||
let mut dev_style = resources.dev.style.clone().unwrap_or_default();
|
||||
let mut dev_script = resources.dev.script.unwrap_or_default();
|
||||
style_list.append(&mut dev_style);
|
||||
script_list.append(&mut dev_script);
|
||||
}
|
||||
@@ -468,7 +496,7 @@ pub fn gen_page(config: &DioxusConfig, serve: bool) -> String {
|
||||
.unwrap_or_default()
|
||||
.contains_key("tailwindcss")
|
||||
{
|
||||
style_str.push_str("<link rel=\"stylesheet\" href=\"tailwind.css\">\n");
|
||||
style_str.push_str("<link rel=\"stylesheet\" href=\"/{base_path}/tailwind.css\">\n");
|
||||
}
|
||||
|
||||
replace_or_insert_before("{style_include}", &style_str, "</head", &mut html);
|
||||
@@ -688,35 +716,3 @@ fn build_assets(config: &CrateConfig) -> Result<Vec<PathBuf>> {
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
// use binary_install::{Cache, Download};
|
||||
|
||||
// /// Attempts to find `wasm-opt` in `PATH` locally, or failing that downloads a
|
||||
// /// precompiled binary.
|
||||
// ///
|
||||
// /// Returns `Some` if a binary was found or it was successfully downloaded.
|
||||
// /// Returns `None` if a binary wasn't found in `PATH` and this platform doesn't
|
||||
// /// have precompiled binaries. Returns an error if we failed to download the
|
||||
// /// binary.
|
||||
// pub fn find_wasm_opt(
|
||||
// cache: &Cache,
|
||||
// install_permitted: bool,
|
||||
// ) -> Result<install::Status, failure::Error> {
|
||||
// // First attempt to look up in PATH. If found assume it works.
|
||||
// if let Ok(path) = which::which("wasm-opt") {
|
||||
// PBAR.info(&format!("found wasm-opt at {:?}", path));
|
||||
|
||||
// match path.as_path().parent() {
|
||||
// Some(path) => return Ok(install::Status::Found(Download::at(path))),
|
||||
// None => {}
|
||||
// }
|
||||
// }
|
||||
|
||||
// let version = "version_78";
|
||||
// Ok(install::download_prebuilt(
|
||||
// &install::Tool::WasmOpt,
|
||||
// cache,
|
||||
// version,
|
||||
// install_permitted,
|
||||
// )?)
|
||||
// }
|
||||
@@ -1,3 +1,4 @@
|
||||
use dioxus_autofmt::{IndentOptions, IndentType};
|
||||
use futures::{stream::FuturesUnordered, StreamExt};
|
||||
use std::{fs, path::Path, process::exit};
|
||||
|
||||
@@ -26,16 +27,19 @@ pub struct Autoformat {
|
||||
impl Autoformat {
|
||||
// Todo: autoformat the entire crate
|
||||
pub async fn autoformat(self) -> Result<()> {
|
||||
let Autoformat { check, raw, file } = self;
|
||||
|
||||
// Default to formatting the project
|
||||
if self.raw.is_none() && self.file.is_none() {
|
||||
if let Err(e) = autoformat_project(self.check).await {
|
||||
if raw.is_none() && file.is_none() {
|
||||
if let Err(e) = autoformat_project(check).await {
|
||||
eprintln!("error formatting project: {}", e);
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(raw) = self.raw {
|
||||
if let Some(inner) = dioxus_autofmt::fmt_block(&raw, 0) {
|
||||
if let Some(raw) = raw {
|
||||
let indent = indentation_for(".")?;
|
||||
if let Some(inner) = dioxus_autofmt::fmt_block(&raw, 0, indent) {
|
||||
println!("{}", inner);
|
||||
} else {
|
||||
// exit process with error
|
||||
@@ -45,33 +49,90 @@ impl Autoformat {
|
||||
}
|
||||
|
||||
// Format single file
|
||||
if let Some(file) = self.file {
|
||||
let file_content = fs::read_to_string(&file);
|
||||
|
||||
match file_content {
|
||||
Ok(s) => {
|
||||
let edits = dioxus_autofmt::fmt_file(&s);
|
||||
let out = dioxus_autofmt::apply_formats(&s, edits);
|
||||
match fs::write(&file, out) {
|
||||
Ok(_) => {
|
||||
println!("formatted {}", file);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("failed to write formatted content to file: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("failed to open file: {}", e);
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
if let Some(file) = file {
|
||||
refactor_file(file)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn refactor_file(file: String) -> Result<(), Error> {
|
||||
let indent = indentation_for(".")?;
|
||||
let file_content = if file == "-" {
|
||||
let mut contents = String::new();
|
||||
std::io::stdin().read_to_string(&mut contents)?;
|
||||
Ok(contents)
|
||||
} else {
|
||||
fs::read_to_string(&file)
|
||||
};
|
||||
let Ok(s) = file_content else {
|
||||
eprintln!("failed to open file: {}", file_content.unwrap_err());
|
||||
exit(1);
|
||||
};
|
||||
let edits = dioxus_autofmt::fmt_file(&s, indent);
|
||||
let out = dioxus_autofmt::apply_formats(&s, edits);
|
||||
|
||||
if file == "-" {
|
||||
print!("{}", out);
|
||||
} else if let Err(e) = fs::write(&file, out) {
|
||||
eprintln!("failed to write formatted content to file: {e}",);
|
||||
} else {
|
||||
println!("formatted {}", file);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_project_files(config: &CrateConfig) -> Vec<PathBuf> {
|
||||
let mut files = vec![];
|
||||
|
||||
let gitignore_path = config.crate_dir.join(".gitignore");
|
||||
if gitignore_path.is_file() {
|
||||
let gitigno = gitignore::File::new(gitignore_path.as_path()).unwrap();
|
||||
if let Ok(git_files) = gitigno.included_files() {
|
||||
let git_files = git_files
|
||||
.into_iter()
|
||||
.filter(|f| f.ends_with(".rs") && !is_target_dir(f));
|
||||
files.extend(git_files)
|
||||
};
|
||||
} else {
|
||||
collect_rs_files(&config.crate_dir, &mut files);
|
||||
}
|
||||
|
||||
files
|
||||
}
|
||||
|
||||
fn is_target_dir(file: &Path) -> bool {
|
||||
let stripped = if let Ok(cwd) = std::env::current_dir() {
|
||||
file.strip_prefix(cwd).unwrap_or(file)
|
||||
} else {
|
||||
file
|
||||
};
|
||||
if let Some(first) = stripped.components().next() {
|
||||
first.as_os_str() == "target"
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
async fn format_file(
|
||||
path: impl AsRef<Path>,
|
||||
indent: IndentOptions,
|
||||
) -> Result<usize, tokio::io::Error> {
|
||||
let contents = tokio::fs::read_to_string(&path).await?;
|
||||
|
||||
let edits = dioxus_autofmt::fmt_file(&contents, indent);
|
||||
let len = edits.len();
|
||||
|
||||
if !edits.is_empty() {
|
||||
let out = dioxus_autofmt::apply_formats(&contents, edits);
|
||||
tokio::fs::write(path, out).await?;
|
||||
}
|
||||
|
||||
Ok(len)
|
||||
}
|
||||
|
||||
/// Read every .rs file accessible when considering the .gitignore and try to format it
|
||||
///
|
||||
/// Runs using Tokio for multithreading, so it should be really really fast
|
||||
@@ -80,42 +141,27 @@ impl Autoformat {
|
||||
async fn autoformat_project(check: bool) -> Result<()> {
|
||||
let crate_config = crate::CrateConfig::new(None)?;
|
||||
|
||||
let mut files_to_format = vec![];
|
||||
collect_rs_files(&crate_config.crate_dir, &mut files_to_format);
|
||||
let files_to_format = get_project_files(&crate_config);
|
||||
|
||||
if files_to_format.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let indent = indentation_for(&files_to_format[0])?;
|
||||
|
||||
let counts = files_to_format
|
||||
.into_iter()
|
||||
.filter(|file| {
|
||||
if file.components().any(|f| f.as_os_str() == "target") {
|
||||
return false;
|
||||
}
|
||||
|
||||
true
|
||||
})
|
||||
.map(|path| async {
|
||||
let _path = path.clone();
|
||||
let res = tokio::spawn(async move {
|
||||
let contents = tokio::fs::read_to_string(&path).await?;
|
||||
|
||||
let edits = dioxus_autofmt::fmt_file(&contents);
|
||||
let len = edits.len();
|
||||
|
||||
if !edits.is_empty() {
|
||||
let out = dioxus_autofmt::apply_formats(&contents, edits);
|
||||
tokio::fs::write(&path, out).await?;
|
||||
}
|
||||
|
||||
Ok(len) as Result<usize, tokio::io::Error>
|
||||
})
|
||||
.await;
|
||||
let path_clone = path.clone();
|
||||
let res = tokio::spawn(format_file(path, indent.clone())).await;
|
||||
|
||||
match res {
|
||||
Err(err) => {
|
||||
eprintln!("error formatting file: {}\n{err}", _path.display());
|
||||
eprintln!("error formatting file: {}\n{err}", path_clone.display());
|
||||
None
|
||||
}
|
||||
Ok(Err(err)) => {
|
||||
eprintln!("error formatting file: {}\n{err}", _path.display());
|
||||
eprintln!("error formatting file: {}\n{err}", path_clone.display());
|
||||
None
|
||||
}
|
||||
Ok(Ok(res)) => Some(res),
|
||||
@@ -125,13 +171,7 @@ async fn autoformat_project(check: bool) -> Result<()> {
|
||||
.collect::<Vec<_>>()
|
||||
.await;
|
||||
|
||||
let files_formatted: usize = counts
|
||||
.into_iter()
|
||||
.map(|f| match f {
|
||||
Some(res) => res,
|
||||
_ => 0,
|
||||
})
|
||||
.sum();
|
||||
let files_formatted: usize = counts.into_iter().flatten().sum();
|
||||
|
||||
if files_formatted > 0 && check {
|
||||
eprintln!("{} files needed formatting", files_formatted);
|
||||
@@ -141,26 +181,67 @@ async fn autoformat_project(check: bool) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn collect_rs_files(folder: &Path, files: &mut Vec<PathBuf>) {
|
||||
let Ok(folder) = folder.read_dir() else {
|
||||
fn indentation_for(file_or_dir: impl AsRef<Path>) -> Result<IndentOptions> {
|
||||
let out = std::process::Command::new("cargo")
|
||||
.args(["fmt", "--", "--print-config", "current"])
|
||||
.arg(file_or_dir.as_ref())
|
||||
.stdout(std::process::Stdio::piped())
|
||||
.stderr(std::process::Stdio::inherit())
|
||||
.output()?;
|
||||
if !out.status.success() {
|
||||
return Err(Error::CargoError("cargo fmt failed".into()));
|
||||
}
|
||||
|
||||
let config = String::from_utf8_lossy(&out.stdout);
|
||||
|
||||
let hard_tabs = config
|
||||
.lines()
|
||||
.find(|line| line.starts_with("hard_tabs "))
|
||||
.and_then(|line| line.split_once('='))
|
||||
.map(|(_, value)| value.trim() == "true")
|
||||
.ok_or_else(|| {
|
||||
Error::RuntimeError("Could not find hard_tabs option in rustfmt config".into())
|
||||
})?;
|
||||
let tab_spaces = config
|
||||
.lines()
|
||||
.find(|line| line.starts_with("tab_spaces "))
|
||||
.and_then(|line| line.split_once('='))
|
||||
.map(|(_, value)| value.trim().parse::<usize>())
|
||||
.ok_or_else(|| {
|
||||
Error::RuntimeError("Could not find tab_spaces option in rustfmt config".into())
|
||||
})?
|
||||
.map_err(|_| {
|
||||
Error::RuntimeError("Could not parse tab_spaces option in rustfmt config".into())
|
||||
})?;
|
||||
|
||||
Ok(IndentOptions::new(
|
||||
if hard_tabs {
|
||||
IndentType::Tabs
|
||||
} else {
|
||||
IndentType::Spaces
|
||||
},
|
||||
tab_spaces,
|
||||
))
|
||||
}
|
||||
|
||||
fn collect_rs_files(folder: &impl AsRef<Path>, files: &mut Vec<PathBuf>) {
|
||||
if is_target_dir(folder.as_ref()) {
|
||||
return;
|
||||
}
|
||||
let Ok(folder) = folder.as_ref().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" {
|
||||
if ext == "rs" && !is_target_dir(&path) {
|
||||
files.push(path);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,8 +37,14 @@ impl Build {
|
||||
.platform
|
||||
.unwrap_or(crate_config.dioxus_config.application.default_platform);
|
||||
|
||||
#[cfg(feature = "plugin")]
|
||||
let _ = PluginManager::on_build_start(&crate_config, &platform);
|
||||
if let Some(target) = self.build.target {
|
||||
crate_config.set_target(target);
|
||||
}
|
||||
|
||||
crate_config.set_cargo_args(self.build.cargo_args);
|
||||
|
||||
// #[cfg(feature = "plugin")]
|
||||
// let _ = PluginManager::on_build_start(&crate_config, &platform);
|
||||
|
||||
match platform {
|
||||
Platform::Web => {
|
||||
@@ -66,8 +72,8 @@ impl Build {
|
||||
)?;
|
||||
file.write_all(temp.as_bytes())?;
|
||||
|
||||
#[cfg(feature = "plugin")]
|
||||
let _ = PluginManager::on_build_finish(&crate_config, &platform);
|
||||
// #[cfg(feature = "plugin")]
|
||||
// let _ = PluginManager::on_build_finish(&crate_config, &platform);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -76,6 +76,12 @@ impl Bundle {
|
||||
crate_config.set_profile(self.build.profile.unwrap());
|
||||
}
|
||||
|
||||
if let Some(target) = &self.build.target {
|
||||
crate_config.set_target(target.to_string());
|
||||
}
|
||||
|
||||
crate_config.set_cargo_args(self.build.cargo_args);
|
||||
|
||||
// build the desktop app
|
||||
build_desktop(&crate_config, false)?;
|
||||
|
||||
@@ -132,10 +138,10 @@ impl Bundle {
|
||||
.project_out_directory(crate_config.out_dir)
|
||||
.package_settings(PackageSettings {
|
||||
product_name: crate_config.dioxus_config.application.name.clone(),
|
||||
version: package.version,
|
||||
description: package.description.unwrap_or_default(),
|
||||
homepage: package.homepage,
|
||||
authors: Some(package.authors),
|
||||
version: package.version().to_string(),
|
||||
description: package.description().unwrap_or_default().to_string(),
|
||||
homepage: Some(package.homepage().unwrap_or_default().to_string()),
|
||||
authors: Some(Vec::from(package.authors())),
|
||||
default_run: Some(crate_config.dioxus_config.application.name.clone()),
|
||||
})
|
||||
.binaries(binaries)
|
||||
@@ -148,6 +154,11 @@ impl Bundle {
|
||||
.collect(),
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(target) = &self.build.target {
|
||||
settings = settings.target(target.to_string());
|
||||
}
|
||||
|
||||
let settings = settings.build();
|
||||
|
||||
// on macos we need to set CI=true (https://github.com/tauri-apps/tauri/issues/2567)
|
||||
@@ -156,9 +167,9 @@ impl Bundle {
|
||||
|
||||
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);
|
||||
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);
|
||||
panic!("Failed to bundle project: {:#?}", err);
|
||||
});
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -6,10 +6,6 @@ use super::*;
|
||||
/// Config options for the build system.
|
||||
#[derive(Clone, Debug, Default, Deserialize, Parser)]
|
||||
pub struct ConfigOptsBuild {
|
||||
/// The index HTML file to drive the bundling process [default: index.html]
|
||||
#[arg(long)]
|
||||
pub target: Option<PathBuf>,
|
||||
|
||||
/// Build in release mode [default: false]
|
||||
#[clap(long)]
|
||||
#[serde(default)]
|
||||
@@ -35,14 +31,18 @@ pub struct ConfigOptsBuild {
|
||||
/// Space separated list of features to activate
|
||||
#[clap(long)]
|
||||
pub features: Option<Vec<String>>,
|
||||
|
||||
/// Rustc platform triple
|
||||
#[clap(long)]
|
||||
pub target: Option<String>,
|
||||
|
||||
/// Extra arguments passed to cargo build
|
||||
#[clap(last = true)]
|
||||
pub cargo_args: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize, Parser)]
|
||||
pub struct ConfigOptsServe {
|
||||
/// The index HTML file to drive the bundling process [default: index.html]
|
||||
#[arg(short, long)]
|
||||
pub target: Option<PathBuf>,
|
||||
|
||||
/// Port of dev server
|
||||
#[clap(long)]
|
||||
#[clap(default_value_t = 8080)]
|
||||
@@ -89,6 +89,14 @@ pub struct ConfigOptsServe {
|
||||
/// Space separated list of features to activate
|
||||
#[clap(long)]
|
||||
pub features: Option<Vec<String>>,
|
||||
|
||||
/// Rustc platform triple
|
||||
#[clap(long)]
|
||||
pub target: Option<String>,
|
||||
|
||||
/// Extra arguments passed to cargo build
|
||||
#[clap(last = true)]
|
||||
pub cargo_args: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Serialize, Deserialize, Debug)]
|
||||
@@ -129,4 +137,12 @@ pub struct ConfigOptsBundle {
|
||||
/// Space separated list of features to activate
|
||||
#[clap(long)]
|
||||
pub features: Option<Vec<String>>,
|
||||
|
||||
/// Rustc platform triple
|
||||
#[clap(long)]
|
||||
pub target: Option<String>,
|
||||
|
||||
/// Extra arguments passed to cargo build
|
||||
#[clap(last = true)]
|
||||
pub cargo_args: Vec<String>,
|
||||
}
|
||||
@@ -34,6 +34,12 @@ impl Serve {
|
||||
// Subdirectories don't work with the server
|
||||
crate_config.dioxus_config.web.app.base_path = None;
|
||||
|
||||
if let Some(target) = self.serve.target {
|
||||
crate_config.set_target(target);
|
||||
}
|
||||
|
||||
crate_config.set_cargo_args(self.serve.cargo_args);
|
||||
|
||||
let platform = self
|
||||
.serve
|
||||
.platform
|
||||
|
||||
@@ -105,7 +105,7 @@ impl Default for DioxusConfig {
|
||||
},
|
||||
proxy: Some(vec![]),
|
||||
watcher: WebWatcherConfig {
|
||||
watch_path: Some(vec![PathBuf::from("src")]),
|
||||
watch_path: Some(vec![PathBuf::from("src"), PathBuf::from("examples")]),
|
||||
reload_html: Some(false),
|
||||
index_on_404: Some(true),
|
||||
},
|
||||
@@ -211,6 +211,8 @@ pub struct CrateConfig {
|
||||
pub verbose: bool,
|
||||
pub custom_profile: Option<String>,
|
||||
pub features: Option<Vec<String>>,
|
||||
pub target: Option<String>,
|
||||
pub cargo_args: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -278,6 +280,8 @@ impl CrateConfig {
|
||||
let verbose = false;
|
||||
let custom_profile = None;
|
||||
let features = None;
|
||||
let target = None;
|
||||
let cargo_args = vec![];
|
||||
|
||||
Ok(Self {
|
||||
out_dir,
|
||||
@@ -294,6 +298,8 @@ impl CrateConfig {
|
||||
custom_profile,
|
||||
features,
|
||||
verbose,
|
||||
target,
|
||||
cargo_args,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -331,6 +337,16 @@ impl CrateConfig {
|
||||
self.features = Some(features);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_target(&mut self, target: String) -> &mut Self {
|
||||
self.target = Some(target);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_cargo_args(&mut self, cargo_args: Vec<String>) -> &mut Self {
|
||||
self.cargo_args = cargo_args;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
|
||||
@@ -29,6 +29,9 @@ pub enum Error {
|
||||
#[error("Cargo Error: {0}")]
|
||||
CargoError(String),
|
||||
|
||||
#[error("Couldn't retrieve cargo metadata")]
|
||||
CargoMetadata(#[source] cargo_metadata::Error),
|
||||
|
||||
#[error("{0}")]
|
||||
CustomError(String),
|
||||
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
#![doc = include_str!("../README.md")]
|
||||
#![doc(html_logo_url = "https://avatars.githubusercontent.com/u/79236386")]
|
||||
#![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")]
|
||||
|
||||
pub const DIOXUS_CLI_VERSION: &str = "0.4.1";
|
||||
|
||||
pub mod builder;
|
||||
|
||||
@@ -28,7 +28,19 @@ pub fn set_up_logging() {
|
||||
message = message,
|
||||
));
|
||||
})
|
||||
.level(log::LevelFilter::Info)
|
||||
.level(match std::env::var("DIOXUS_LOG") {
|
||||
Ok(level) => match level.to_lowercase().as_str() {
|
||||
"error" => log::LevelFilter::Error,
|
||||
"warn" => log::LevelFilter::Warn,
|
||||
"info" => log::LevelFilter::Info,
|
||||
"debug" => log::LevelFilter::Debug,
|
||||
"trace" => log::LevelFilter::Trace,
|
||||
_ => {
|
||||
panic!("Invalid log level: {}", level)
|
||||
}
|
||||
},
|
||||
Err(_) => log::LevelFilter::Info,
|
||||
})
|
||||
.chain(std::io::stdout())
|
||||
.apply()
|
||||
.unwrap();
|
||||
|
||||
+39
-47
@@ -9,42 +9,31 @@ use dioxus_cli::plugin::PluginManager;
|
||||
|
||||
use Commands::*;
|
||||
|
||||
fn get_bin(bin: Option<String>) -> Result<Option<PathBuf>> {
|
||||
const ERR_MESSAGE: &str = "The `--bin` flag has to be ran in a Cargo workspace.";
|
||||
fn get_bin(bin: Option<String>) -> Result<PathBuf> {
|
||||
let metadata = cargo_metadata::MetadataCommand::new()
|
||||
.exec()
|
||||
.map_err(Error::CargoMetadata)?;
|
||||
let package = if let Some(bin) = bin {
|
||||
metadata
|
||||
.workspace_packages()
|
||||
.into_iter()
|
||||
.find(|p| p.name == bin)
|
||||
.ok_or(format!("no such package: {}", bin))
|
||||
.map_err(Error::CargoError)?
|
||||
} else {
|
||||
metadata
|
||||
.root_package()
|
||||
.ok_or("no root package?".into())
|
||||
.map_err(Error::CargoError)?
|
||||
};
|
||||
|
||||
if let Some(ref bin) = bin {
|
||||
let manifest = cargo_toml::Manifest::from_path("./Cargo.toml")
|
||||
.map_err(|_| Error::CargoError(ERR_MESSAGE.to_string()))?;
|
||||
let crate_dir = package
|
||||
.manifest_path
|
||||
.parent()
|
||||
.ok_or("couldn't take parent dir".into())
|
||||
.map_err(Error::CargoError)?;
|
||||
|
||||
if let Some(workspace) = manifest.workspace {
|
||||
for item in workspace.members.iter() {
|
||||
let path = PathBuf::from(item);
|
||||
|
||||
if !path.exists() {
|
||||
continue;
|
||||
}
|
||||
|
||||
if !path.is_dir() {
|
||||
continue;
|
||||
}
|
||||
|
||||
if path.ends_with(bin.clone()) {
|
||||
return Ok(Some(path));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return Err(Error::CargoError(ERR_MESSAGE.to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
// If the bin exists but we couldn't find it
|
||||
if bin.is_some() {
|
||||
return Err(Error::CargoError(
|
||||
"The specified bin does not exist.".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
Ok(crate_dir.into())
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
@@ -53,34 +42,36 @@ async fn main() -> anyhow::Result<()> {
|
||||
|
||||
set_up_logging();
|
||||
|
||||
let bin = get_bin(args.bin)?;
|
||||
let bin = get_bin(args.bin);
|
||||
|
||||
let _dioxus_config = DioxusConfig::load(bin.clone())
|
||||
if let Ok(bin) = &bin {
|
||||
let _dioxus_config = DioxusConfig::load(Some(bin.clone()))
|
||||
.map_err(|e| anyhow!("Failed to load Dioxus config because: {e}"))?
|
||||
.unwrap_or_else(|| {
|
||||
log::warn!("You appear to be creating a Dioxus project from scratch; we will use the default config");
|
||||
DioxusConfig::default()
|
||||
});
|
||||
|
||||
#[cfg(feature = "plugin")]
|
||||
PluginManager::init(_dioxus_config.plugin)
|
||||
.map_err(|e| anyhow!("🚫 Plugin system initialization failed: {e}"))?;
|
||||
#[cfg(feature = "plugin")]
|
||||
PluginManager::init(_dioxus_config.plugin)
|
||||
.map_err(|e| anyhow!("🚫 Plugin system initialization failed: {e}"))?;
|
||||
}
|
||||
|
||||
match args.action {
|
||||
Translate(opts) => opts
|
||||
.translate()
|
||||
.map_err(|e| anyhow!("🚫 Translation of HTML into RSX failed: {}", e)),
|
||||
|
||||
Build(opts) => opts
|
||||
.build(bin.clone())
|
||||
Build(opts) if bin.is_ok() => opts
|
||||
.build(Some(bin.unwrap().clone()))
|
||||
.map_err(|e| anyhow!("🚫 Building project failed: {}", e)),
|
||||
|
||||
Clean(opts) => opts
|
||||
.clean(bin.clone())
|
||||
Clean(opts) if bin.is_ok() => opts
|
||||
.clean(Some(bin.unwrap().clone()))
|
||||
.map_err(|e| anyhow!("🚫 Cleaning project failed: {}", e)),
|
||||
|
||||
Serve(opts) => opts
|
||||
.serve(bin.clone())
|
||||
Serve(opts) if bin.is_ok() => opts
|
||||
.serve(Some(bin.unwrap().clone()))
|
||||
.await
|
||||
.map_err(|e| anyhow!("🚫 Serving project failed: {}", e)),
|
||||
|
||||
@@ -92,8 +83,8 @@ async fn main() -> anyhow::Result<()> {
|
||||
.config()
|
||||
.map_err(|e| anyhow!("🚫 Configuring new project failed: {}", e)),
|
||||
|
||||
Bundle(opts) => opts
|
||||
.bundle(bin.clone())
|
||||
Bundle(opts) if bin.is_ok() => opts
|
||||
.bundle(Some(bin.unwrap().clone()))
|
||||
.map_err(|e| anyhow!("🚫 Bundling project failed: {}", e)),
|
||||
|
||||
#[cfg(feature = "plugin")]
|
||||
@@ -118,5 +109,6 @@ async fn main() -> anyhow::Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
_ => Err(anyhow::anyhow!(bin.unwrap_err())),
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::{
|
||||
server::{
|
||||
output::{print_console_info, PrettierOptions},
|
||||
setup_file_watcher, setup_file_watcher_hot_reload,
|
||||
setup_file_watcher,
|
||||
},
|
||||
BuildResult, CrateConfig, Result,
|
||||
};
|
||||
@@ -19,6 +19,8 @@ use tokio::sync::broadcast::{self};
|
||||
#[cfg(feature = "plugin")]
|
||||
use plugin::PluginManager;
|
||||
|
||||
use super::HotReloadState;
|
||||
|
||||
pub async fn startup(config: CrateConfig) -> Result<()> {
|
||||
// ctrl-c shutdown checker
|
||||
let _crate_config = config.clone();
|
||||
@@ -28,16 +30,34 @@ pub async fn startup(config: CrateConfig) -> Result<()> {
|
||||
std::process::exit(0);
|
||||
});
|
||||
|
||||
match config.hot_reload {
|
||||
true => serve_hot_reload(config).await?,
|
||||
false => serve_default(config).await?,
|
||||
}
|
||||
let hot_reload_state = match config.hot_reload {
|
||||
true => {
|
||||
let FileMapBuildResult { map, errors } =
|
||||
FileMap::<HtmlCtx>::create(config.crate_dir.clone()).unwrap();
|
||||
|
||||
for err in errors {
|
||||
log::error!("{}", err);
|
||||
}
|
||||
|
||||
let file_map = Arc::new(Mutex::new(map));
|
||||
|
||||
let hot_reload_tx = broadcast::channel(100).0;
|
||||
|
||||
Some(HotReloadState {
|
||||
messages: hot_reload_tx.clone(),
|
||||
file_map: file_map.clone(),
|
||||
})
|
||||
}
|
||||
false => None,
|
||||
};
|
||||
|
||||
serve(config, hot_reload_state).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Start the server without hot reload
|
||||
pub async fn serve_default(config: CrateConfig) -> Result<()> {
|
||||
pub async fn serve(config: CrateConfig, hot_reload_state: Option<HotReloadState>) -> Result<()> {
|
||||
let (child, first_build_result) = start_desktop(&config)?;
|
||||
let currently_running_child: RwLock<Child> = RwLock::new(child);
|
||||
|
||||
@@ -51,6 +71,7 @@ pub async fn serve_default(config: CrateConfig) -> Result<()> {
|
||||
|
||||
move || {
|
||||
let mut current_child = currently_running_child.write().unwrap();
|
||||
log::trace!("Killing old process");
|
||||
current_child.kill()?;
|
||||
let (child, result) = start_desktop(&config)?;
|
||||
*current_child = child;
|
||||
@@ -59,6 +80,7 @@ pub async fn serve_default(config: CrateConfig) -> Result<()> {
|
||||
},
|
||||
&config,
|
||||
None,
|
||||
hot_reload_state.clone(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -73,105 +95,73 @@ pub async fn serve_default(config: CrateConfig) -> Result<()> {
|
||||
None,
|
||||
);
|
||||
|
||||
std::future::pending::<()>().await;
|
||||
match hot_reload_state {
|
||||
Some(hot_reload_state) => {
|
||||
start_desktop_hot_reload(hot_reload_state).await?;
|
||||
}
|
||||
None => {
|
||||
std::future::pending::<()>().await;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Start the server without hot reload
|
||||
|
||||
/// Start dx serve with hot reload
|
||||
pub async fn serve_hot_reload(config: CrateConfig) -> Result<()> {
|
||||
let (_, first_build_result) = start_desktop(&config)?;
|
||||
|
||||
println!("🚀 Starting development server...");
|
||||
|
||||
// Setup hot reload
|
||||
let FileMapBuildResult { map, errors } =
|
||||
FileMap::<HtmlCtx>::create(config.crate_dir.clone()).unwrap();
|
||||
|
||||
println!("🚀 Starting development server...");
|
||||
|
||||
for err in errors {
|
||||
log::error!("{}", err);
|
||||
}
|
||||
|
||||
let file_map = Arc::new(Mutex::new(map));
|
||||
|
||||
let (hot_reload_tx, mut hot_reload_rx) = broadcast::channel(100);
|
||||
|
||||
// States
|
||||
// The open interprocess sockets
|
||||
let channels = Arc::new(Mutex::new(Vec::new()));
|
||||
|
||||
// Setup file watcher
|
||||
// We got to own watcher so that it exists for the duration of serve
|
||||
// Otherwise hot reload won't work.
|
||||
let _watcher = setup_file_watcher_hot_reload(
|
||||
&config,
|
||||
hot_reload_tx,
|
||||
file_map.clone(),
|
||||
{
|
||||
let config = config.clone();
|
||||
|
||||
let channels = channels.clone();
|
||||
move || {
|
||||
for channel in &mut *channels.lock().unwrap() {
|
||||
send_msg(HotReloadMsg::Shutdown, channel);
|
||||
}
|
||||
Ok(start_desktop(&config)?.1)
|
||||
}
|
||||
},
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Print serve info
|
||||
print_console_info(
|
||||
&config,
|
||||
PrettierOptions {
|
||||
changed: vec![],
|
||||
warnings: first_build_result.warnings,
|
||||
elapsed_time: first_build_result.elapsed_time,
|
||||
},
|
||||
None,
|
||||
);
|
||||
|
||||
clear_paths();
|
||||
|
||||
match LocalSocketListener::bind("@dioxusin") {
|
||||
async fn start_desktop_hot_reload(hot_reload_state: HotReloadState) -> Result<()> {
|
||||
let metadata = cargo_metadata::MetadataCommand::new()
|
||||
.no_deps()
|
||||
.exec()
|
||||
.unwrap();
|
||||
let target_dir = metadata.target_directory.as_std_path();
|
||||
let path = target_dir.join("dioxusin");
|
||||
clear_paths(&path);
|
||||
match LocalSocketListener::bind(path) {
|
||||
Ok(local_socket_stream) => {
|
||||
let aborted = Arc::new(Mutex::new(false));
|
||||
// States
|
||||
// The open interprocess sockets
|
||||
let channels = Arc::new(Mutex::new(Vec::new()));
|
||||
|
||||
// listen for connections
|
||||
std::thread::spawn({
|
||||
let file_map = file_map.clone();
|
||||
let file_map = hot_reload_state.file_map.clone();
|
||||
let channels = channels.clone();
|
||||
let aborted = aborted.clone();
|
||||
let _ = local_socket_stream.set_nonblocking(true);
|
||||
move || {
|
||||
loop {
|
||||
if let Ok(mut connection) = local_socket_stream.accept() {
|
||||
// send any templates than have changed before the socket connected
|
||||
let templates: Vec<_> = {
|
||||
file_map
|
||||
.lock()
|
||||
.unwrap()
|
||||
.map
|
||||
.values()
|
||||
.filter_map(|(_, template_slot)| *template_slot)
|
||||
.collect()
|
||||
};
|
||||
for template in templates {
|
||||
if !send_msg(
|
||||
HotReloadMsg::UpdateTemplate(template),
|
||||
&mut connection,
|
||||
) {
|
||||
continue;
|
||||
//accept() will block the thread when local_socket_stream is in blocking mode (default)
|
||||
match local_socket_stream.accept() {
|
||||
Ok(mut connection) => {
|
||||
// send any templates than have changed before the socket connected
|
||||
let templates: Vec<_> = {
|
||||
file_map
|
||||
.lock()
|
||||
.unwrap()
|
||||
.map
|
||||
.values()
|
||||
.filter_map(|(_, template_slot)| *template_slot)
|
||||
.collect()
|
||||
};
|
||||
for template in templates {
|
||||
if !send_msg(
|
||||
HotReloadMsg::UpdateTemplate(template),
|
||||
&mut connection,
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
channels.lock().unwrap().push(connection);
|
||||
println!("Connected to hot reloading 🚀");
|
||||
}
|
||||
Err(err) => {
|
||||
let error_string = err.to_string();
|
||||
// Filter out any error messages about a operation that may block and an error message that triggers on some operating systems that says "Waiting for a process to open the other end of the pipe" without WouldBlock being set
|
||||
let display_error = err.kind() != std::io::ErrorKind::WouldBlock
|
||||
&& !error_string.contains("Waiting for a process");
|
||||
if display_error {
|
||||
println!("Error connecting to hot reloading: {} (Hot reloading is a feature of the dioxus-cli. If you are not using the CLI, this error can be ignored)", err);
|
||||
}
|
||||
}
|
||||
channels.lock().unwrap().push(connection);
|
||||
println!("Connected to hot reloading 🚀");
|
||||
}
|
||||
if *aborted.lock().unwrap() {
|
||||
break;
|
||||
@@ -180,6 +170,8 @@ pub async fn serve_hot_reload(config: CrateConfig) -> Result<()> {
|
||||
}
|
||||
});
|
||||
|
||||
let mut hot_reload_rx = hot_reload_state.messages.subscribe();
|
||||
|
||||
while let Ok(template) = hot_reload_rx.recv().await {
|
||||
let channels = &mut *channels.lock().unwrap();
|
||||
let mut i = 0;
|
||||
@@ -199,17 +191,14 @@ pub async fn serve_hot_reload(config: CrateConfig) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn clear_paths() {
|
||||
fn clear_paths(file_socket_path: &std::path::Path) {
|
||||
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);
|
||||
}
|
||||
|
||||
if file_socket_path.exists() {
|
||||
let _ = std::fs::remove_file(file_socket_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -230,6 +219,7 @@ fn send_msg(msg: HotReloadMsg, channel: &mut impl std::io::Write) -> bool {
|
||||
|
||||
pub fn start_desktop(config: &CrateConfig) -> Result<(Child, BuildResult)> {
|
||||
// Run the desktop application
|
||||
log::trace!("Building application");
|
||||
let result = crate::builder::build_desktop(config, true)?;
|
||||
|
||||
match &config.executable {
|
||||
@@ -240,6 +230,7 @@ pub fn start_desktop(config: &CrateConfig) -> Result<(Child, BuildResult)> {
|
||||
if cfg!(windows) {
|
||||
file.set_extension("exe");
|
||||
}
|
||||
log::trace!("Running application from {:?}", file);
|
||||
let child = Command::new(file.to_str().unwrap()).spawn()?;
|
||||
|
||||
Ok((child, result))
|
||||
|
||||
+93
-127
@@ -9,7 +9,7 @@ use std::{
|
||||
path::PathBuf,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
use tokio::sync::broadcast::Sender;
|
||||
use tokio::sync::broadcast::{self};
|
||||
|
||||
mod output;
|
||||
use output::*;
|
||||
@@ -21,6 +21,7 @@ async fn setup_file_watcher<F: Fn() -> Result<BuildResult> + Send + 'static>(
|
||||
build_with: F,
|
||||
config: &CrateConfig,
|
||||
web_info: Option<WebServerInfo>,
|
||||
hot_reload: Option<HotReloadState>,
|
||||
) -> Result<RecommendedWatcher> {
|
||||
let mut last_update_time = chrono::Local::now().timestamp();
|
||||
|
||||
@@ -31,152 +32,117 @@ async fn setup_file_watcher<F: Fn() -> Result<BuildResult> + Send + 'static>(
|
||||
.watcher
|
||||
.watch_path
|
||||
.clone()
|
||||
.unwrap_or_else(|| vec![PathBuf::from("src")]);
|
||||
.unwrap_or_else(|| vec![PathBuf::from("src"), PathBuf::from("examples")]);
|
||||
|
||||
let watcher_config = config.clone();
|
||||
let mut watcher = notify::recommended_watcher(move |info: notify::Result<notify::Event>| {
|
||||
let config = watcher_config.clone();
|
||||
if let Ok(e) = info {
|
||||
if chrono::Local::now().timestamp() > last_update_time {
|
||||
match build_with() {
|
||||
Ok(res) => {
|
||||
last_update_time = chrono::Local::now().timestamp();
|
||||
let mut needs_full_rebuild;
|
||||
if let Some(hot_reload) = &hot_reload {
|
||||
// find changes to the rsx in the file
|
||||
let mut rsx_file_map = hot_reload.file_map.lock().unwrap();
|
||||
let mut messages: Vec<Template<'static>> = Vec::new();
|
||||
|
||||
#[allow(clippy::redundant_clone)]
|
||||
print_console_info(
|
||||
&config,
|
||||
PrettierOptions {
|
||||
changed: e.paths.clone(),
|
||||
warnings: res.warnings,
|
||||
elapsed_time: res.elapsed_time,
|
||||
},
|
||||
web_info.clone(),
|
||||
);
|
||||
// In hot reload mode, we only need to rebuild if non-rsx code is changed
|
||||
needs_full_rebuild = false;
|
||||
|
||||
#[cfg(feature = "plugin")]
|
||||
let _ = PluginManager::on_serve_rebuild(
|
||||
chrono::Local::now().timestamp(),
|
||||
e.paths,
|
||||
);
|
||||
for path in &e.paths {
|
||||
// if this is not a rust file, rebuild the whole project
|
||||
if path.extension().and_then(|p| p.to_str()) != Some("rs") {
|
||||
needs_full_rebuild = true;
|
||||
break;
|
||||
}
|
||||
|
||||
// Workaround for notify and vscode-like editor:
|
||||
// when edit & save a file in vscode, there will be two notifications,
|
||||
// the first one is a file with empty content.
|
||||
// filter the empty file notification to avoid false rebuild during hot-reload
|
||||
if let Ok(metadata) = fs::metadata(path) {
|
||||
if metadata.len() == 0 {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
match rsx_file_map.update_rsx(path, &config.crate_dir) {
|
||||
Ok(UpdateResult::UpdatedRsx(msgs)) => {
|
||||
messages.extend(msgs);
|
||||
needs_full_rebuild = false;
|
||||
}
|
||||
Ok(UpdateResult::NeedsRebuild) => {
|
||||
needs_full_rebuild = true;
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!("{}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if needs_full_rebuild {
|
||||
// Reset the file map to the new state of the project
|
||||
let FileMapBuildResult {
|
||||
map: new_file_map,
|
||||
errors,
|
||||
} = FileMap::<HtmlCtx>::create(config.crate_dir.clone()).unwrap();
|
||||
|
||||
for err in errors {
|
||||
log::error!("{}", err);
|
||||
}
|
||||
|
||||
*rsx_file_map = new_file_map;
|
||||
} else {
|
||||
for msg in messages {
|
||||
let _ = hot_reload.messages.send(msg);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
needs_full_rebuild = true;
|
||||
}
|
||||
|
||||
if needs_full_rebuild {
|
||||
match build_with() {
|
||||
Ok(res) => {
|
||||
last_update_time = chrono::Local::now().timestamp();
|
||||
|
||||
#[allow(clippy::redundant_clone)]
|
||||
print_console_info(
|
||||
&config,
|
||||
PrettierOptions {
|
||||
changed: e.paths.clone(),
|
||||
warnings: res.warnings,
|
||||
elapsed_time: res.elapsed_time,
|
||||
},
|
||||
web_info.clone(),
|
||||
);
|
||||
|
||||
#[cfg(feature = "plugin")]
|
||||
let _ = PluginManager::on_serve_rebuild(
|
||||
chrono::Local::now().timestamp(),
|
||||
e.paths,
|
||||
);
|
||||
}
|
||||
Err(e) => log::error!("{}", e),
|
||||
}
|
||||
Err(e) => log::error!("{}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
for sub_path in allow_watch_path {
|
||||
watcher
|
||||
.watch(
|
||||
&config.crate_dir.join(sub_path),
|
||||
notify::RecursiveMode::Recursive,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
Ok(watcher)
|
||||
}
|
||||
|
||||
// Todo: reduce duplication and merge with setup_file_watcher()
|
||||
/// Sets up a file watcher with hot reload
|
||||
async fn setup_file_watcher_hot_reload<F: Fn() -> Result<BuildResult> + Send + 'static>(
|
||||
config: &CrateConfig,
|
||||
hot_reload_tx: Sender<Template<'static>>,
|
||||
file_map: Arc<Mutex<FileMap<HtmlCtx>>>,
|
||||
build_with: F,
|
||||
web_info: Option<WebServerInfo>,
|
||||
) -> Result<RecommendedWatcher> {
|
||||
// file watcher: check file change
|
||||
let allow_watch_path = config
|
||||
.dioxus_config
|
||||
.web
|
||||
.watcher
|
||||
.watch_path
|
||||
.clone()
|
||||
.unwrap_or_else(|| vec![PathBuf::from("src")]);
|
||||
|
||||
let watcher_config = config.clone();
|
||||
let mut last_update_time = chrono::Local::now().timestamp();
|
||||
|
||||
let mut watcher = RecommendedWatcher::new(
|
||||
move |evt: notify::Result<notify::Event>| {
|
||||
let config = watcher_config.clone();
|
||||
// Give time for the change to take effect before reading the file
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
if chrono::Local::now().timestamp() > last_update_time {
|
||||
if let Ok(evt) = evt {
|
||||
let mut messages: Vec<Template<'static>> = Vec::new();
|
||||
for path in evt.paths.clone() {
|
||||
// if this is not a rust file, rebuild the whole project
|
||||
if path.extension().and_then(|p| p.to_str()) != Some("rs") {
|
||||
match build_with() {
|
||||
Ok(res) => {
|
||||
print_console_info(
|
||||
&config,
|
||||
PrettierOptions {
|
||||
changed: evt.paths,
|
||||
warnings: res.warnings,
|
||||
elapsed_time: res.elapsed_time,
|
||||
},
|
||||
web_info.clone(),
|
||||
);
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!("{}", err);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
// find changes to the rsx in the file
|
||||
let mut map = file_map.lock().unwrap();
|
||||
|
||||
match map.update_rsx(&path, &config.crate_dir) {
|
||||
Ok(UpdateResult::UpdatedRsx(msgs)) => {
|
||||
messages.extend(msgs);
|
||||
}
|
||||
Ok(UpdateResult::NeedsRebuild) => {
|
||||
match build_with() {
|
||||
Ok(res) => {
|
||||
print_console_info(
|
||||
&config,
|
||||
PrettierOptions {
|
||||
changed: evt.paths,
|
||||
warnings: res.warnings,
|
||||
elapsed_time: res.elapsed_time,
|
||||
},
|
||||
web_info.clone(),
|
||||
);
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!("{}", err);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!("{}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
for msg in messages {
|
||||
let _ = hot_reload_tx.send(msg);
|
||||
}
|
||||
}
|
||||
last_update_time = chrono::Local::now().timestamp();
|
||||
}
|
||||
},
|
||||
notify::Config::default(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
for sub_path in allow_watch_path {
|
||||
if let Err(err) = watcher.watch(
|
||||
&config.crate_dir.join(&sub_path),
|
||||
&config.crate_dir.join(sub_path),
|
||||
notify::RecursiveMode::Recursive,
|
||||
) {
|
||||
log::error!("error watching {sub_path:?}: \n{}", err);
|
||||
log::error!("Failed to watch path: {}", err);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(watcher)
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct HotReloadState {
|
||||
pub messages: broadcast::Sender<Template<'static>>,
|
||||
pub file_map: Arc<Mutex<FileMap<HtmlCtx>>>,
|
||||
}
|
||||
@@ -22,17 +22,20 @@ pub fn print_console_info(
|
||||
options: PrettierOptions,
|
||||
web_info: Option<WebServerInfo>,
|
||||
) {
|
||||
if let Ok(native_clearseq) = Command::new(if cfg!(target_os = "windows") {
|
||||
"cls"
|
||||
} else {
|
||||
"clear"
|
||||
})
|
||||
.output()
|
||||
{
|
||||
print!("{}", String::from_utf8_lossy(&native_clearseq.stdout));
|
||||
} else {
|
||||
// Try ANSI-Escape characters
|
||||
print!("\x1b[2J\x1b[H");
|
||||
// Don't clear the screen if the user has set the DIOXUS_LOG environment variable to "trace" so that we can see the logs
|
||||
if Some("trace") != std::env::var("DIOXUS_LOG").ok().as_deref() {
|
||||
if let Ok(native_clearseq) = Command::new(if cfg!(target_os = "windows") {
|
||||
"cls"
|
||||
} else {
|
||||
"clear"
|
||||
})
|
||||
.output()
|
||||
{
|
||||
print!("{}", String::from_utf8_lossy(&native_clearseq.stdout));
|
||||
} else {
|
||||
// Try ANSI-Escape characters
|
||||
print!("\x1b[2J\x1b[H");
|
||||
}
|
||||
}
|
||||
|
||||
let mut profile = if config.release { "Release" } else { "Debug" }.to_string();
|
||||
@@ -129,7 +132,7 @@ pub fn print_console_info(
|
||||
log::warn!(
|
||||
"{}",
|
||||
format!(
|
||||
"There were {} warning messages during the build.",
|
||||
"There were {} warning messages during the build. Run `cargo check` to see them.",
|
||||
options.warnings.len() - 1
|
||||
)
|
||||
.yellow()
|
||||
|
||||
@@ -1,27 +1,15 @@
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use axum::{
|
||||
extract::{ws::Message, WebSocketUpgrade},
|
||||
response::IntoResponse,
|
||||
Extension, TypedHeader,
|
||||
};
|
||||
use dioxus_core::Template;
|
||||
use dioxus_html::HtmlCtx;
|
||||
use dioxus_rsx::hot_reload::FileMap;
|
||||
use tokio::sync::broadcast;
|
||||
|
||||
use crate::CrateConfig;
|
||||
|
||||
pub struct HotReloadState {
|
||||
pub messages: broadcast::Sender<Template<'static>>,
|
||||
pub file_map: Arc<Mutex<FileMap<HtmlCtx>>>,
|
||||
pub watcher_config: CrateConfig,
|
||||
}
|
||||
use crate::server::HotReloadState;
|
||||
|
||||
pub async fn hot_reload_handler(
|
||||
ws: WebSocketUpgrade,
|
||||
_: Option<TypedHeader<headers::UserAgent>>,
|
||||
Extension(state): Extension<Arc<HotReloadState>>,
|
||||
Extension(state): Extension<HotReloadState>,
|
||||
) -> impl IntoResponse {
|
||||
ws.on_upgrade(|mut socket| async move {
|
||||
log::info!("🔥 Hot Reload WebSocket connected");
|
||||
|
||||
@@ -3,7 +3,7 @@ use crate::{
|
||||
serve::Serve,
|
||||
server::{
|
||||
output::{print_console_info, PrettierOptions, WebServerInfo},
|
||||
setup_file_watcher, setup_file_watcher_hot_reload,
|
||||
setup_file_watcher, HotReloadState,
|
||||
},
|
||||
BuildResult, CrateConfig, Result, WebHttpsConfig,
|
||||
};
|
||||
@@ -11,6 +11,7 @@ use axum::{
|
||||
body::{Full, HttpBody},
|
||||
extract::{ws::Message, Extension, TypedHeader, WebSocketUpgrade},
|
||||
http::{
|
||||
self,
|
||||
header::{HeaderName, HeaderValue},
|
||||
Method, Response, StatusCode,
|
||||
},
|
||||
@@ -58,20 +59,39 @@ pub async fn startup(port: u16, config: CrateConfig, start_browser: bool) -> Res
|
||||
|
||||
let ip = get_ip().unwrap_or(String::from("0.0.0.0"));
|
||||
|
||||
match config.hot_reload {
|
||||
true => serve_hot_reload(ip, port, config, start_browser).await?,
|
||||
false => serve_default(ip, port, config, start_browser).await?,
|
||||
}
|
||||
let hot_reload_state = match config.hot_reload {
|
||||
true => {
|
||||
let FileMapBuildResult { map, errors } =
|
||||
FileMap::<HtmlCtx>::create(config.crate_dir.clone()).unwrap();
|
||||
|
||||
for err in errors {
|
||||
log::error!("{}", err);
|
||||
}
|
||||
|
||||
let file_map = Arc::new(Mutex::new(map));
|
||||
|
||||
let hot_reload_tx = broadcast::channel(100).0;
|
||||
|
||||
Some(HotReloadState {
|
||||
messages: hot_reload_tx.clone(),
|
||||
file_map: file_map.clone(),
|
||||
})
|
||||
}
|
||||
false => None,
|
||||
};
|
||||
|
||||
serve(ip, port, config, start_browser, hot_reload_state).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Start the server without hot reload
|
||||
pub async fn serve_default(
|
||||
pub async fn serve(
|
||||
ip: String,
|
||||
port: u16,
|
||||
config: CrateConfig,
|
||||
start_browser: bool,
|
||||
hot_reload_state: Option<HotReloadState>,
|
||||
) -> Result<()> {
|
||||
let first_build_result = crate::builder::build(&config, true)?;
|
||||
|
||||
@@ -93,6 +113,7 @@ pub async fn serve_default(
|
||||
ip: ip.clone(),
|
||||
port,
|
||||
}),
|
||||
hot_reload_state.clone(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -119,88 +140,7 @@ pub async fn serve_default(
|
||||
);
|
||||
|
||||
// Router
|
||||
let router = setup_router(config, ws_reload_state, None).await?;
|
||||
|
||||
// Start server
|
||||
start_server(port, router, start_browser, rustls_config).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Start dx serve with hot reload
|
||||
pub async fn serve_hot_reload(
|
||||
ip: String,
|
||||
port: u16,
|
||||
config: CrateConfig,
|
||||
start_browser: bool,
|
||||
) -> Result<()> {
|
||||
let first_build_result = crate::builder::build(&config, true)?;
|
||||
|
||||
log::info!("🚀 Starting development server...");
|
||||
|
||||
// Setup hot reload
|
||||
let (reload_tx, _) = broadcast::channel(100);
|
||||
let FileMapBuildResult { map, errors } =
|
||||
FileMap::<HtmlCtx>::create(config.crate_dir.clone()).unwrap();
|
||||
|
||||
for err in errors {
|
||||
log::error!("{}", err);
|
||||
}
|
||||
|
||||
let file_map = Arc::new(Mutex::new(map));
|
||||
|
||||
let hot_reload_tx = broadcast::channel(100).0;
|
||||
|
||||
// States
|
||||
let hot_reload_state = Arc::new(HotReloadState {
|
||||
messages: hot_reload_tx.clone(),
|
||||
file_map: file_map.clone(),
|
||||
watcher_config: config.clone(),
|
||||
});
|
||||
|
||||
let ws_reload_state = Arc::new(WsReloadState {
|
||||
update: reload_tx.clone(),
|
||||
});
|
||||
|
||||
// Setup file watcher
|
||||
// We got to own watcher so that it exists for the duration of serve
|
||||
// Otherwise hot reload won't work.
|
||||
let _watcher = setup_file_watcher_hot_reload(
|
||||
&config,
|
||||
hot_reload_tx,
|
||||
file_map,
|
||||
{
|
||||
let config = config.clone();
|
||||
let reload_tx = reload_tx.clone();
|
||||
move || build(&config, &reload_tx)
|
||||
},
|
||||
Some(WebServerInfo {
|
||||
ip: ip.clone(),
|
||||
port,
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// HTTPS
|
||||
// Before console info so it can stop if mkcert isn't installed or fails
|
||||
let rustls_config = get_rustls(&config).await?;
|
||||
|
||||
// Print serve info
|
||||
print_console_info(
|
||||
&config,
|
||||
PrettierOptions {
|
||||
changed: vec![],
|
||||
warnings: first_build_result.warnings,
|
||||
elapsed_time: first_build_result.elapsed_time,
|
||||
},
|
||||
Some(WebServerInfo {
|
||||
ip: ip.clone(),
|
||||
port,
|
||||
}),
|
||||
);
|
||||
|
||||
// Router
|
||||
let router = setup_router(config, ws_reload_state, Some(hot_reload_state)).await?;
|
||||
let router = setup_router(config, ws_reload_state, hot_reload_state).await?;
|
||||
|
||||
// Start server
|
||||
start_server(port, router, start_browser, rustls_config).await?;
|
||||
@@ -291,7 +231,7 @@ fn get_rustls_without_mkcert(web_config: &WebHttpsConfig) -> Result<(String, Str
|
||||
async fn setup_router(
|
||||
config: CrateConfig,
|
||||
ws_reload: Arc<WsReloadState>,
|
||||
hot_reload: Option<Arc<HotReloadState>>,
|
||||
hot_reload: Option<HotReloadState>,
|
||||
) -> Result<Router> {
|
||||
// Setup cors
|
||||
let cors = CorsLayer::new()
|
||||
@@ -323,7 +263,7 @@ async fn setup_router(
|
||||
.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
|
||||
let mut response = if file_service_config
|
||||
.dioxus_config
|
||||
.web
|
||||
.watcher
|
||||
@@ -351,6 +291,13 @@ async fn setup_router(
|
||||
} else {
|
||||
response.map(|body| body.boxed())
|
||||
};
|
||||
let headers = response.headers_mut();
|
||||
headers.insert(
|
||||
http::header::CACHE_CONTROL,
|
||||
HeaderValue::from_static("no-cache"),
|
||||
);
|
||||
headers.insert(http::header::PRAGMA, HeaderValue::from_static("no-cache"));
|
||||
headers.insert(http::header::EXPIRES, HeaderValue::from_static("0"));
|
||||
Ok(response)
|
||||
},
|
||||
)
|
||||
|
||||
@@ -17,6 +17,9 @@ proc-macro2 = { version = "1.0" }
|
||||
quote = "1.0"
|
||||
syn = { version = "2.0", features = ["full", "extra-traits"] }
|
||||
dioxus-rsx = { workspace = true }
|
||||
dioxus-core = { workspace = true }
|
||||
constcat = "0.3.0"
|
||||
prettyplease = "0.2.15"
|
||||
|
||||
# testing
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
# dioxus-core-macro
|
||||
|
||||
|
||||
[![Crates.io][crates-badge]][crates-url]
|
||||
[![MIT licensed][mit-badge]][mit-url]
|
||||
[![Build Status][actions-badge]][actions-url]
|
||||
@@ -8,30 +7,28 @@
|
||||
|
||||
[crates-badge]: https://img.shields.io/crates/v/dioxus-core-macro.svg
|
||||
[crates-url]: https://crates.io/crates/dioxus-core-macro
|
||||
|
||||
[mit-badge]: https://img.shields.io/badge/license-MIT-blue.svg
|
||||
[mit-url]: https://github.com/dioxuslabs/dioxus/blob/master/LICENSE
|
||||
|
||||
[actions-badge]: https://github.com/dioxuslabs/dioxus/actions/workflows/main.yml/badge.svg
|
||||
[actions-url]: https://github.com/dioxuslabs/dioxus/actions?query=workflow%3ACI+branch%3Amaster
|
||||
|
||||
[discord-badge]: https://img.shields.io/discord/899851952891002890.svg?logo=discord&style=flat-square
|
||||
[discord-url]: https://discord.gg/XgGxMSkvUM
|
||||
|
||||
[Website](https://dioxuslabs.com) |
|
||||
[Guides](https://dioxuslabs.com/docs/0.3/guide/en/) |
|
||||
[Guides](https://dioxuslabs.com/learn/0.4/) |
|
||||
[API Docs](https://docs.rs/dioxus-core-macro/latest/dioxus_core_macro) |
|
||||
[Chat](https://discord.gg/XgGxMSkvUM)
|
||||
|
||||
|
||||
## Overview
|
||||
|
||||
`dioxus-core-macro` provides a handful of helpful macros used by the `dioxus` crate. These include:
|
||||
|
||||
- The `rsx!` macro that underpins templates and node creation
|
||||
- The `inline_props` macro transforms function arguments into an auto-derived struct
|
||||
- The `format_args_f` macro which allows f-string formatting with support for expressions
|
||||
|
||||
- The `rsx!` macro that underpins templates and node creation.
|
||||
- The `component` attribute macro denotes a function as a Dioxus component. Currently, this:
|
||||
- Transforms function arguments into an auto-derived struct.
|
||||
- Ensures that your component name uses PascalCase.
|
||||
- Probably more stuff in the future. This macro allows us to have a way of distinguishing functions and components, which can be quite handy.
|
||||
- The `format_args_f` macro which allows f-string formatting with support for expressions.
|
||||
|
||||
## Contributing
|
||||
|
||||
@@ -39,6 +36,7 @@
|
||||
- Join the discord and ask questions!
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the [MIT license].
|
||||
|
||||
[mit license]: https://github.com/DioxusLabs/dioxus/blob/master/LICENSE-MIT
|
||||
|
||||
@@ -0,0 +1,237 @@
|
||||
//! This module is used for parsing a component function into a struct that is subsequently
|
||||
//! deserialized into something useful using deserializer arguments.
|
||||
//!
|
||||
//! Let's break that down with a term glossary and examples which show usage and implementing.
|
||||
//!
|
||||
//! # Glossary
|
||||
//! * `component body` - The [`ComponentBody`] struct. It's used to parse a component function [`proc_macro::TokenStream`]
|
||||
//! to a reusable struct that deserializers use to modify the token stream.
|
||||
//! * `deserializer` - A struct that deserializes the [`ComponentBody`] into a [`DeserializerOutput`].
|
||||
//! It implements the [`DeserializerArgs`] trait, but as you can see, it's called "DeserializerArgs",
|
||||
//! not "Deserializer". Why?
|
||||
//! Because "args" makes more sense to the caller of [`ComponentBody::deserialize`], which
|
||||
//! takes an [`DeserializerArgs`] argument. However, you can think of "DeserializerArgs" as the deserializer.
|
||||
//! * `deserializer output` - A struct that implements the [`DeserializerOutput`] trait.
|
||||
//! This struct is what enables deserializers to use each other, since it contains the fields that
|
||||
//! a deserializer needs to turn a token stream to a different token stream.
|
||||
//! This means a deserializer can get the output of another deserializer, and use that output,
|
||||
//! thereby using the functionality of a different deserializer.
|
||||
//! This struct also implements [`ToTokens`], which means that this is the final stage of the whole process.
|
||||
//!
|
||||
//! # Examples
|
||||
//! *Not all imports might be included.*
|
||||
//!
|
||||
//! ## Usage in a procedural macro attribute
|
||||
//! ```rs,ignore
|
||||
//! use proc_macro::TokenStream;
|
||||
//!
|
||||
//! // Some documentation. You can reuse this in your deserializer structs.
|
||||
//! /// This attribute changes the name of a component function to whatever the first argument is.
|
||||
//! #[proc_macro_attribute]
|
||||
//! pub fn name_changer(args: TokenStream, input: TokenStream) -> TokenStream {
|
||||
//! // Parse the component body.
|
||||
//! let component_body = parse_macro_input!(input as ComponentBody);
|
||||
//!
|
||||
//! // Parse the first argument, which is going to be the components new name.
|
||||
//! let new_name: String = match Punctuated::<Path, Token![,]>::parse_terminated.parse(args) {
|
||||
//! Err(e) => return e.to_compile_error().into(), // Convert to a compile error and return
|
||||
//! Ok(args) => {
|
||||
//! // If the argument exists, then convert it to a string
|
||||
//! if let Some(first) = args.first() {
|
||||
//! first.to_token_stream().to_string()
|
||||
//! } else {
|
||||
//! // If the argument doesn't exist, return an error with the appropriate message.
|
||||
//! // The "span" is the location of some code.
|
||||
//! // The error occurred in the "args" token stream, so point the error there.
|
||||
//! return Error::new(args.span(), "No new name provided").to_compile_error().into();
|
||||
//! }
|
||||
//! }
|
||||
//! };
|
||||
//!
|
||||
//! let new_name = &*new_name;
|
||||
//!
|
||||
//! // Deserialize the component body to an output with the given args.
|
||||
//! let output = component_body.deserialize(NameChangerDeserializerArgs { new_name });
|
||||
//!
|
||||
//! // Error handling like before, except now you're ready to return the final value.
|
||||
//! match output {
|
||||
//! Err(e) => e.to_compile_error().into(),
|
||||
//! Ok(output) => output.to_token_stream().into(),
|
||||
//! }
|
||||
//! }
|
||||
//! ```
|
||||
//! ## Using the macro in Dioxus code:
|
||||
//! ```rs
|
||||
//! use your_proc_macro_library::name_changer;
|
||||
//! use dioxus::prelude::*;
|
||||
//!
|
||||
//! #[name_changer(CoolName)]
|
||||
//! pub fn LameName(cx: Scope) -> Element {
|
||||
//! render! { "I want a cool name!" }
|
||||
//! }
|
||||
//!
|
||||
//! pub fn App(cx: Scope) -> Element {
|
||||
//! render! { CoolName {} } // Renders: "I want a cool name!"
|
||||
//! }
|
||||
//! ```
|
||||
//! ## Implementing a component body deserializer
|
||||
//! ```rs
|
||||
//! use syn::{Result, ItemFn, Signature, Ident};
|
||||
//! use quote::quote;
|
||||
//!
|
||||
//! // Create a list of arguments.
|
||||
//! // If there was no args, just make it empty. The "args" struct is also the deserializer struct.
|
||||
//! // For the docs, you can basically copy paste this text and replace "name_changer" with your macro path.
|
||||
//! // Although unfortunately, the link does not work
|
||||
//! // Just make sure that your macro is well documented.
|
||||
//! /// The args and deserializing implementation for the [`name_changer`] macro.
|
||||
//! #[derive(Clone)]
|
||||
//! pub struct NameChangerDeserializerArgs<'a> {
|
||||
//! pub new_name: &'a str,
|
||||
//! }
|
||||
//!
|
||||
//! // Create an output struct.
|
||||
//! // The ItemFn represents a modified component function.
|
||||
//! // To read what fields should be here, check out the `DeserializerOutput` struct docs.
|
||||
//! // For the docs, you can basically copy paste this text and replace "name_changer" with your macro path.
|
||||
//! // Just make sure that your macro is well documented.
|
||||
//! /// The output fields and [`ToTokens`] implementation for the [`name_changer`] macro.
|
||||
//! #[derive(Clone)]
|
||||
//! pub struct NameChangerDeserializerOutput {
|
||||
//! pub comp_fn: ItemFn,
|
||||
//! }
|
||||
//!
|
||||
//! // Implement `ToTokens`, which is forced by `DeserializerOutput`.
|
||||
//! // This will usually be very simple like this, even for complex deserializers.
|
||||
//! // That's because of the way the `DeserializerOutput` is designed.
|
||||
//! impl ToTokens for NameChangerDeserializerOutput {
|
||||
//! fn to_tokens(&self, tokens: &mut TokenStream) {
|
||||
//! let comp_fn = &self.comp_fn;
|
||||
//!
|
||||
//! tokens.append_all(quote! {
|
||||
//! #comp_fn
|
||||
//! });
|
||||
//! }
|
||||
//! }
|
||||
//!
|
||||
//! impl DeserializerOutput for NameChangerDeserializerOutput {}
|
||||
//!
|
||||
//! // Implement `DeserializerArgs`. This is the core part of deserializers.
|
||||
//! impl<'a> DeserializerArgs<NameChangerDeserializerOutput> for NameChangerDeserializerArgs<'a> {
|
||||
//! fn to_output(&self, component_body: &ComponentBody) -> Result<NameChangerDeserializerOutput> {
|
||||
//! let old_fn = &component_body.item_fn;
|
||||
//! let old_sig = &old_fn.sig;
|
||||
//!
|
||||
//! // For more complex uses, you will probably use `quote::parse_quote!` in combination with
|
||||
//! // creating the structs manually.
|
||||
//! // However, create the structs manually if you can.
|
||||
//! // It's more reliable, because you only modify a certain struct field
|
||||
//! // and set the others to be the clone of the original component body.
|
||||
//! // That ensures that no information will be accidentally removed.
|
||||
//! let new_sig = Signature {
|
||||
//! ident: Ident::new(self.new_name, old_sig.ident.span()),
|
||||
//! ..old_sig.clone()
|
||||
//! };
|
||||
//! let new_fn = ItemFn {
|
||||
//! sig: new_sig,
|
||||
//! ..old_fn.clone()
|
||||
//! };
|
||||
//!
|
||||
//! Ok(NameChangerDeserializerOutput {
|
||||
//! comp_fn: new_fn
|
||||
//! })
|
||||
//! }
|
||||
//! ```
|
||||
|
||||
pub mod utils;
|
||||
|
||||
pub use utils::DeserializerArgs;
|
||||
pub use utils::DeserializerOutput;
|
||||
pub use utils::TypeHelper;
|
||||
|
||||
use dioxus_core::{Element, Scope};
|
||||
use syn::parse::{Parse, ParseStream};
|
||||
use syn::spanned::Spanned;
|
||||
use syn::*;
|
||||
|
||||
/// General struct for parsing a component body.
|
||||
/// However, because it's ambiguous, it does not implement [`ToTokens`](quote::to_tokens::ToTokens).
|
||||
///
|
||||
/// Refer to the [module documentation](crate::component_body) for more.
|
||||
pub struct ComponentBody {
|
||||
/// The component function definition. You can parse this back into a [`ComponentBody`].
|
||||
/// For example, you might modify it, parse it into a [`ComponentBody`], and deserialize that
|
||||
/// using some deserializer. This is how deserializers use other deserializers.
|
||||
///
|
||||
/// **`item_fn.sig.inputs` includes the context argument!**
|
||||
/// Keep this in mind when creating deserializers, because you often might want to ignore it.
|
||||
/// That might be annoying, but it would be bad design for this kind of struct to not be parsable from itself.
|
||||
pub item_fn: ItemFn,
|
||||
/// Parsing tries to ensure that this argument will be a [`Scope`].
|
||||
/// **However, macros have limitations that prevent this from always working,
|
||||
/// so don't take this for granted!**
|
||||
pub cx_arg: FnArg,
|
||||
/// The pattern (name) and type of the context argument.
|
||||
pub cx_pat_type: PatType,
|
||||
/// If the function has any arguments other than the context.
|
||||
pub has_extra_args: bool,
|
||||
}
|
||||
|
||||
impl ComponentBody {
|
||||
/// Deserializes the body into the [`TOutput`] with the specific [`TArgs`].
|
||||
/// Even if the args are empty, the [`TArg`] type still determines what [`TOutput`] will be generated.
|
||||
pub fn deserialize<TOutput, TArgs>(&self, args: TArgs) -> Result<TOutput>
|
||||
where
|
||||
TOutput: DeserializerOutput,
|
||||
TArgs: DeserializerArgs<TOutput>,
|
||||
{
|
||||
args.to_output(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl Parse for ComponentBody {
|
||||
fn parse(input: ParseStream) -> Result<Self> {
|
||||
let item_fn: ItemFn = input.parse()?;
|
||||
let scope_type_path = Scope::get_path_string();
|
||||
|
||||
let (cx_arg, cx_pat_type) = if let Some(first_arg) = item_fn.sig.inputs.first() {
|
||||
let incorrect_first_arg_err = Err(Error::new(
|
||||
first_arg.span(),
|
||||
format!("First argument must be a <{}>", scope_type_path),
|
||||
));
|
||||
|
||||
match first_arg.to_owned() {
|
||||
FnArg::Receiver(_) => {
|
||||
return incorrect_first_arg_err;
|
||||
}
|
||||
FnArg::Typed(f) => (first_arg.to_owned(), f),
|
||||
}
|
||||
} else {
|
||||
return Err(Error::new(
|
||||
item_fn.sig.ident.span(), // Or maybe just item_f.sig.span()?
|
||||
format!(
|
||||
"Must have at least one argument that's a <{}>",
|
||||
scope_type_path
|
||||
),
|
||||
));
|
||||
};
|
||||
|
||||
let element_type_path = Element::get_path_string();
|
||||
|
||||
if item_fn.sig.output == ReturnType::Default {
|
||||
return Err(Error::new(
|
||||
item_fn.sig.output.span(),
|
||||
format!("Must return a <{}>", element_type_path),
|
||||
));
|
||||
}
|
||||
|
||||
let has_extra_args = item_fn.sig.inputs.len() > 1;
|
||||
|
||||
Ok(Self {
|
||||
item_fn,
|
||||
cx_arg,
|
||||
cx_pat_type,
|
||||
has_extra_args,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
use crate::component_body::ComponentBody;
|
||||
use dioxus_core::{Element, Scope};
|
||||
use quote::ToTokens;
|
||||
use syn::{parse_quote, Path};
|
||||
|
||||
/// The output produced by a deserializer.
|
||||
///
|
||||
/// # For implementors
|
||||
/// Struct field guidelines:
|
||||
/// * Must be public, so that other deserializers can utilize them.
|
||||
/// * Should usually be [`Item`]s that you then simply combine in a [`quote!`]
|
||||
/// in the [`ComponentBodyDeserializer::output_to_token_stream2`] function.
|
||||
/// * If an [`Item`] might not be included, wrap it in an [`Option`].
|
||||
/// * Must be at the component function "level"/"context".
|
||||
/// For example, the [`InlinePropsDeserializer`](crate::component_body_deserializers::inline_props::InlinePropsDeserializer)
|
||||
/// produces two [`Item`]s; the function but with arguments turned into props, and the props struct.
|
||||
/// It does not return any [`Item`]s inside the struct or function.
|
||||
pub trait DeserializerOutput: ToTokens {}
|
||||
|
||||
impl<T: ToTokens> DeserializerOutput for T {}
|
||||
|
||||
/// The args passed to a [`ComponentBody`] when deserializing it.
|
||||
///
|
||||
/// It's also the struct that does the deserializing.
|
||||
/// It's called "DeserializerArgs", not "Deserializer". Why?
|
||||
/// Because "args" makes more sense to the caller of [`ComponentBody::deserialize`], which
|
||||
/// takes an [`DeserializerArgs`] argument. However, you can think of "DeserializerArgs" as the deserializer.
|
||||
pub trait DeserializerArgs<TOutput>: Clone
|
||||
where
|
||||
TOutput: ToTokens,
|
||||
{
|
||||
// There's a lot of Results out there... let's make sure that this is a syn::Result.
|
||||
// Let's also make sure there's not a warning.
|
||||
/// Creates a [`ToTokens`] struct from the `self` args and a [`ComponentBody`].
|
||||
/// The [`ComponentBody::deserialize`] provides a cleaner way of calling this function.
|
||||
#[allow(unused_qualifications)]
|
||||
fn to_output(&self, component_body: &ComponentBody) -> syn::Result<TOutput>;
|
||||
}
|
||||
|
||||
pub trait TypeHelper {
|
||||
fn get_path() -> Path;
|
||||
fn get_path_string() -> String {
|
||||
Self::get_path().to_token_stream().to_string()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TypeHelper for Scope<'a> {
|
||||
fn get_path() -> Path {
|
||||
parse_quote!(::dioxus::core::Scope)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TypeHelper for Element<'a> {
|
||||
fn get_path() -> Path {
|
||||
parse_quote!(::dioxus::core::Element)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
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)]
|
||||
#[allow(clippy::inline_always)]
|
||||
#[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;
|
||||
let fn_ident = &comp_fn.sig.ident;
|
||||
|
||||
let doc = format!("Properties for the [`{fn_ident}`] component.");
|
||||
tokens.append_all(quote! {
|
||||
#[doc = #doc]
|
||||
#props_struct
|
||||
#[allow(non_snake_case)]
|
||||
#comp_fn
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl DeserializerArgs<ComponentDeserializerOutput> for ComponentDeserializerArgs {
|
||||
fn to_output(&self, component_body: &ComponentBody) -> Result<ComponentDeserializerOutput> {
|
||||
let Signature { ident, .. } = &component_body.item_fn.sig;
|
||||
|
||||
if self.case_check && !is_pascal_case(&ident.to_string()) {
|
||||
return Err(Error::new(ident.span(), COMPONENT_ARG_CASE_CHECK_ERROR));
|
||||
}
|
||||
|
||||
if component_body.has_extra_args {
|
||||
Self::deserialize_with_props(component_body)
|
||||
} else {
|
||||
Ok(Self::deserialize_no_props(component_body))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ComponentDeserializerArgs {
|
||||
fn deserialize_no_props(component_body: &ComponentBody) -> ComponentDeserializerOutput {
|
||||
let ComponentBody {
|
||||
item_fn,
|
||||
cx_pat_type,
|
||||
..
|
||||
} = component_body;
|
||||
let cx_pat = &cx_pat_type.pat;
|
||||
|
||||
let comp_fn = get_out_comp_fn(item_fn, cx_pat);
|
||||
|
||||
ComponentDeserializerOutput {
|
||||
comp_fn,
|
||||
props_struct: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn deserialize_with_props(
|
||||
component_body: &ComponentBody,
|
||||
) -> Result<ComponentDeserializerOutput> {
|
||||
let ComponentBody {
|
||||
item_fn,
|
||||
cx_pat_type,
|
||||
..
|
||||
} = component_body;
|
||||
let cx_pat = &cx_pat_type.pat;
|
||||
|
||||
let comp_parsed = match parse2::<ComponentBody>(quote!(#item_fn)) {
|
||||
Ok(comp_body) => comp_body,
|
||||
Err(e) => {
|
||||
return Err(Error::new(
|
||||
e.span(),
|
||||
format!(
|
||||
"This is probably a bug in our code, please report it! Error: {}",
|
||||
e
|
||||
),
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
let inlined_props_output = comp_parsed.deserialize(InlinePropsDeserializerArgs {})?;
|
||||
let props_struct = inlined_props_output.props_struct;
|
||||
let props_fn = inlined_props_output.comp_fn;
|
||||
|
||||
let comp_fn = get_out_comp_fn(&props_fn, cx_pat);
|
||||
|
||||
Ok(ComponentDeserializerOutput {
|
||||
comp_fn,
|
||||
props_struct: Some(props_struct),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn is_pascal_case(input: &str) -> bool {
|
||||
let mut is_next_lowercase = false;
|
||||
|
||||
for c in input.chars() {
|
||||
let is_upper = c.is_ascii_uppercase();
|
||||
|
||||
if (c == '_') || (is_upper && is_next_lowercase) {
|
||||
return false;
|
||||
}
|
||||
|
||||
is_next_lowercase = is_upper;
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
@@ -0,0 +1,341 @@
|
||||
use crate::component_body::{ComponentBody, DeserializerArgs};
|
||||
use proc_macro2::TokenStream as TokenStream2;
|
||||
use quote::{quote, ToTokens, TokenStreamExt};
|
||||
use syn::token::Comma;
|
||||
use syn::{punctuated::Punctuated, *};
|
||||
|
||||
/// The args and deserializing implementation for the [`crate::inline_props`] macro.
|
||||
#[derive(Clone)]
|
||||
pub struct InlinePropsDeserializerArgs;
|
||||
|
||||
/// The output fields and [`ToTokens`] implementation for the [`crate::inline_props`] macro.
|
||||
#[derive(Clone)]
|
||||
pub struct InlinePropsDeserializerOutput {
|
||||
pub comp_fn: ItemFn,
|
||||
pub props_struct: ItemStruct,
|
||||
}
|
||||
|
||||
impl ToTokens for InlinePropsDeserializerOutput {
|
||||
fn to_tokens(&self, tokens: &mut TokenStream2) {
|
||||
let function = &self.comp_fn;
|
||||
let props_struct = &self.props_struct;
|
||||
|
||||
tokens.append_all(quote! {
|
||||
#function
|
||||
#props_struct
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl DeserializerArgs<InlinePropsDeserializerOutput> for InlinePropsDeserializerArgs {
|
||||
fn to_output(&self, component_body: &ComponentBody) -> Result<InlinePropsDeserializerOutput> {
|
||||
Ok(InlinePropsDeserializerOutput {
|
||||
comp_fn: get_function(component_body),
|
||||
props_struct: get_props_struct(component_body),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn get_props_struct(component_body: &ComponentBody) -> ItemStruct {
|
||||
let ComponentBody { item_fn, .. } = component_body;
|
||||
let ItemFn { vis, sig, .. } = item_fn;
|
||||
let Signature {
|
||||
inputs,
|
||||
ident: fn_ident,
|
||||
generics,
|
||||
..
|
||||
} = sig;
|
||||
|
||||
// Skip first arg since that's the context
|
||||
let struct_fields = inputs.iter().skip(1).map(move |f| {
|
||||
match f {
|
||||
FnArg::Receiver(_) => unreachable!(), // Unreachable because of ComponentBody parsing
|
||||
FnArg::Typed(pt) => {
|
||||
let arg_pat = &pt.pat; // Pattern (identifier)
|
||||
let arg_colon = &pt.colon_token;
|
||||
let arg_ty = &pt.ty; // Type
|
||||
let arg_attrs = &pt.attrs; // Attributes
|
||||
|
||||
quote! {
|
||||
#(#arg_attrs)
|
||||
*
|
||||
#vis #arg_pat #arg_colon #arg_ty
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let struct_ident = Ident::new(&format!("{fn_ident}Props"), fn_ident.span());
|
||||
|
||||
let first_lifetime = if let Some(GenericParam::Lifetime(lt)) = generics.params.first() {
|
||||
Some(lt)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let struct_attrs = if first_lifetime.is_some() {
|
||||
quote! { #[derive(Props)] }
|
||||
} else {
|
||||
quote! { #[derive(Props, PartialEq)] }
|
||||
};
|
||||
|
||||
let struct_generics = if first_lifetime.is_some() {
|
||||
let struct_generics: Punctuated<GenericParam, Comma> = component_body
|
||||
.item_fn
|
||||
.sig
|
||||
.generics
|
||||
.params
|
||||
.iter()
|
||||
.map(|it| match it {
|
||||
GenericParam::Type(tp) => {
|
||||
let mut tp = tp.clone();
|
||||
tp.bounds.push(parse_quote!( 'a ));
|
||||
|
||||
GenericParam::Type(tp)
|
||||
}
|
||||
_ => it.clone(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
quote! { <#struct_generics> }
|
||||
} else {
|
||||
quote! { #generics }
|
||||
};
|
||||
|
||||
parse_quote! {
|
||||
#struct_attrs
|
||||
#[allow(non_camel_case_types)]
|
||||
#vis struct #struct_ident #struct_generics
|
||||
{
|
||||
#(#struct_fields),*
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_props_docs(fn_ident: &Ident, inputs: Vec<&FnArg>) -> Vec<Attribute> {
|
||||
if inputs.len() <= 1 {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let arg_docs = inputs
|
||||
.iter()
|
||||
.filter_map(|f| match f {
|
||||
FnArg::Receiver(_) => unreachable!(), // ComponentBody prohibits receiver parameters.
|
||||
FnArg::Typed(pt) => {
|
||||
let arg_doc = pt
|
||||
.attrs
|
||||
.iter()
|
||||
.filter_map(|attr| {
|
||||
// TODO: Error reporting
|
||||
// Check if the path of the attribute is "doc"
|
||||
if !is_attr_doc(attr) {
|
||||
return None;
|
||||
};
|
||||
|
||||
let Meta::NameValue(meta_name_value) = &attr.meta else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let Expr::Lit(doc_lit) = &meta_name_value.value else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let Lit::Str(doc_lit_str) = &doc_lit.lit else {
|
||||
return None;
|
||||
};
|
||||
|
||||
Some(doc_lit_str.value())
|
||||
})
|
||||
.fold(String::new(), |mut doc, next_doc_line| {
|
||||
doc.push('\n');
|
||||
doc.push_str(&next_doc_line);
|
||||
doc
|
||||
});
|
||||
|
||||
Some((
|
||||
&pt.pat,
|
||||
&pt.ty,
|
||||
pt.attrs.iter().find_map(|attr| {
|
||||
if attr.path() != &parse_quote!(deprecated) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let res = crate::utils::DeprecatedAttribute::from_meta(&attr.meta);
|
||||
|
||||
match res {
|
||||
Err(e) => panic!("{}", e.to_string()),
|
||||
Ok(v) => Some(v),
|
||||
}
|
||||
}),
|
||||
arg_doc,
|
||||
))
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut props_docs = Vec::with_capacity(5);
|
||||
let props_def_link = fn_ident.to_string() + "Props";
|
||||
let header =
|
||||
format!("# Props\n*For details, see the [props struct definition]({props_def_link}).*");
|
||||
|
||||
props_docs.push(parse_quote! {
|
||||
#[doc = #header]
|
||||
});
|
||||
|
||||
for (arg_name, arg_type, deprecation, input_arg_doc) in arg_docs {
|
||||
let arg_name = arg_name.into_token_stream().to_string();
|
||||
let arg_type = crate::utils::format_type_string(arg_type);
|
||||
|
||||
let input_arg_doc = keep_up_to_n_consecutive_chars(input_arg_doc.trim(), 2, '\n')
|
||||
.replace("\n\n", "</p><p>");
|
||||
let prop_def_link = format!("{props_def_link}::{arg_name}");
|
||||
let mut arg_doc = format!("- [`{arg_name}`]({prop_def_link}) : `{arg_type}`");
|
||||
|
||||
if let Some(deprecation) = deprecation {
|
||||
arg_doc.push_str("<p>👎 Deprecated");
|
||||
|
||||
if let Some(since) = deprecation.since {
|
||||
arg_doc.push_str(&format!(" since {since}"));
|
||||
}
|
||||
|
||||
if let Some(note) = deprecation.note {
|
||||
let note = keep_up_to_n_consecutive_chars(¬e, 1, '\n').replace('\n', " ");
|
||||
let note = keep_up_to_n_consecutive_chars(¬e, 1, '\t').replace('\t', " ");
|
||||
|
||||
arg_doc.push_str(&format!(": {note}"));
|
||||
}
|
||||
|
||||
arg_doc.push_str("</p>");
|
||||
|
||||
if !input_arg_doc.is_empty() {
|
||||
arg_doc.push_str("<hr/>");
|
||||
}
|
||||
}
|
||||
|
||||
if !input_arg_doc.is_empty() {
|
||||
arg_doc.push_str(&format!("<p>{input_arg_doc}</p>"));
|
||||
}
|
||||
|
||||
props_docs.push(parse_quote! {
|
||||
#[doc = #arg_doc]
|
||||
});
|
||||
}
|
||||
|
||||
props_docs
|
||||
}
|
||||
|
||||
fn get_function(component_body: &ComponentBody) -> ItemFn {
|
||||
let ComponentBody {
|
||||
item_fn,
|
||||
cx_pat_type,
|
||||
..
|
||||
} = component_body;
|
||||
let ItemFn {
|
||||
attrs: fn_attrs,
|
||||
vis,
|
||||
sig,
|
||||
block: fn_block,
|
||||
} = item_fn;
|
||||
let Signature {
|
||||
inputs,
|
||||
ident: fn_ident,
|
||||
generics,
|
||||
output: fn_output,
|
||||
asyncness,
|
||||
..
|
||||
} = sig;
|
||||
let Generics { where_clause, .. } = generics;
|
||||
|
||||
let cx_pat = &cx_pat_type.pat;
|
||||
let struct_ident = Ident::new(&format!("{fn_ident}Props"), fn_ident.span());
|
||||
|
||||
// Skip first arg since that's the context
|
||||
let struct_field_names = inputs.iter().skip(1).filter_map(|f| match f {
|
||||
FnArg::Receiver(_) => unreachable!(), // ComponentBody prohibits receiver parameters.
|
||||
FnArg::Typed(pt) => Some(&pt.pat),
|
||||
});
|
||||
|
||||
let first_lifetime = if let Some(GenericParam::Lifetime(lt)) = generics.params.first() {
|
||||
Some(lt)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let (scope_lifetime, fn_generics) = if let Some(lt) = first_lifetime {
|
||||
(quote! { #lt, }, generics.clone())
|
||||
} else {
|
||||
let lifetime: LifetimeParam = parse_quote! { 'a };
|
||||
|
||||
let mut fn_generics = generics.clone();
|
||||
fn_generics
|
||||
.params
|
||||
.insert(0, GenericParam::Lifetime(lifetime.clone()));
|
||||
|
||||
(quote! { #lifetime, }, fn_generics)
|
||||
};
|
||||
|
||||
let generics_no_bounds = {
|
||||
let mut generics = generics.clone();
|
||||
generics.params = generics
|
||||
.params
|
||||
.iter()
|
||||
.map(|it| match it {
|
||||
GenericParam::Type(tp) => {
|
||||
let mut tp = tp.clone();
|
||||
tp.bounds.clear();
|
||||
|
||||
GenericParam::Type(tp)
|
||||
}
|
||||
_ => it.clone(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
generics
|
||||
};
|
||||
|
||||
let props_docs = get_props_docs(fn_ident, inputs.iter().skip(1).collect());
|
||||
|
||||
parse_quote! {
|
||||
#(#fn_attrs)*
|
||||
#(#props_docs)*
|
||||
#asyncness #vis fn #fn_ident #fn_generics (#cx_pat: Scope<#scope_lifetime #struct_ident #generics_no_bounds>) #fn_output
|
||||
#where_clause
|
||||
{
|
||||
let #struct_ident { #(#struct_field_names),* } = &#cx_pat.props;
|
||||
#fn_block
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if the attribute is a `#[doc]` attribute.
|
||||
fn is_attr_doc(attr: &Attribute) -> bool {
|
||||
attr.path() == &parse_quote!(doc)
|
||||
}
|
||||
|
||||
fn keep_up_to_n_consecutive_chars(
|
||||
input: &str,
|
||||
n_of_consecutive_chars_allowed: usize,
|
||||
target_char: char,
|
||||
) -> String {
|
||||
let mut output = String::new();
|
||||
let mut prev_char: Option<char> = None;
|
||||
let mut consecutive_count = 0;
|
||||
|
||||
for c in input.chars() {
|
||||
match prev_char {
|
||||
Some(prev) if c == target_char && prev == target_char => {
|
||||
if consecutive_count < n_of_consecutive_chars_allowed {
|
||||
output.push(c);
|
||||
consecutive_count += 1;
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
output.push(c);
|
||||
prev_char = Some(c);
|
||||
consecutive_count = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
output
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
//! This module contains all [`ComponentBody`](crate::component_body::ComponentBody) deserializers.
|
||||
|
||||
pub mod component;
|
||||
pub mod inline_props;
|
||||
@@ -1,185 +0,0 @@
|
||||
use proc_macro2::{Span, TokenStream as TokenStream2};
|
||||
use quote::{quote, ToTokens, TokenStreamExt};
|
||||
use syn::{
|
||||
parse::{Parse, ParseStream},
|
||||
punctuated::Punctuated,
|
||||
*,
|
||||
};
|
||||
|
||||
pub struct InlinePropsBody {
|
||||
pub attrs: Vec<Attribute>,
|
||||
pub vis: syn::Visibility,
|
||||
pub maybe_async: Option<Token![async]>,
|
||||
pub fn_token: Token![fn],
|
||||
pub ident: Ident,
|
||||
pub cx_token: Box<Pat>,
|
||||
pub generics: Generics,
|
||||
pub paren_token: token::Paren,
|
||||
pub inputs: Punctuated<FnArg, Token![,]>,
|
||||
// pub fields: FieldsNamed,
|
||||
pub output: ReturnType,
|
||||
pub where_clause: Option<WhereClause>,
|
||||
pub block: Box<Block>,
|
||||
}
|
||||
|
||||
/// The custom rusty variant of parsing rsx!
|
||||
impl Parse for InlinePropsBody {
|
||||
fn parse(input: ParseStream) -> Result<Self> {
|
||||
let attrs: Vec<Attribute> = input.call(Attribute::parse_outer)?;
|
||||
let maybe_async: Option<Token![async]> = input.parse().ok();
|
||||
let vis: Visibility = input.parse()?;
|
||||
|
||||
let fn_token = input.parse()?;
|
||||
let ident = input.parse()?;
|
||||
let generics: Generics = input.parse()?;
|
||||
|
||||
let content;
|
||||
let paren_token = syn::parenthesized!(content in input);
|
||||
|
||||
let first_arg: FnArg = content.parse()?;
|
||||
let cx_token = {
|
||||
match first_arg {
|
||||
FnArg::Receiver(_) => panic!("first argument must not be a receiver argument"),
|
||||
FnArg::Typed(f) => f.pat,
|
||||
}
|
||||
};
|
||||
|
||||
let _: Result<Token![,]> = content.parse();
|
||||
|
||||
let inputs = syn::punctuated::Punctuated::parse_terminated(&content)?;
|
||||
|
||||
let output = input.parse()?;
|
||||
|
||||
let where_clause = input
|
||||
.peek(syn::token::Where)
|
||||
.then(|| input.parse())
|
||||
.transpose()?;
|
||||
|
||||
let block = input.parse()?;
|
||||
|
||||
Ok(Self {
|
||||
vis,
|
||||
maybe_async,
|
||||
fn_token,
|
||||
ident,
|
||||
generics,
|
||||
paren_token,
|
||||
inputs,
|
||||
output,
|
||||
where_clause,
|
||||
block,
|
||||
cx_token,
|
||||
attrs,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Serialize the same way, regardless of flavor
|
||||
impl ToTokens for InlinePropsBody {
|
||||
fn to_tokens(&self, out_tokens: &mut TokenStream2) {
|
||||
let Self {
|
||||
vis,
|
||||
ident,
|
||||
generics,
|
||||
inputs,
|
||||
output,
|
||||
where_clause,
|
||||
block,
|
||||
cx_token,
|
||||
attrs,
|
||||
maybe_async,
|
||||
..
|
||||
} = self;
|
||||
|
||||
let fields = inputs.iter().map(|f| {
|
||||
quote! { #vis #f }
|
||||
});
|
||||
|
||||
let struct_name = Ident::new(&format!("{ident}Props"), Span::call_site());
|
||||
|
||||
let field_names = inputs.iter().filter_map(|f| match f {
|
||||
FnArg::Receiver(_) => todo!(),
|
||||
FnArg::Typed(t) => Some(&t.pat),
|
||||
});
|
||||
|
||||
let first_lifetime = if let Some(GenericParam::Lifetime(lt)) = generics.params.first() {
|
||||
Some(lt)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let modifiers = if first_lifetime.is_some() {
|
||||
quote! { #[derive(Props)] }
|
||||
} else {
|
||||
quote! { #[derive(Props, PartialEq)] }
|
||||
};
|
||||
|
||||
let (scope_lifetime, fn_generics, struct_generics) = if let Some(lt) = first_lifetime {
|
||||
let struct_generics: Punctuated<_, token::Comma> = generics
|
||||
.params
|
||||
.iter()
|
||||
.map(|it| match it {
|
||||
GenericParam::Type(tp) => {
|
||||
let mut tp = tp.clone();
|
||||
tp.bounds.push(parse_quote!( 'a ));
|
||||
|
||||
GenericParam::Type(tp)
|
||||
}
|
||||
_ => it.clone(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
(
|
||||
quote! { #lt, },
|
||||
generics.clone(),
|
||||
quote! { <#struct_generics> },
|
||||
)
|
||||
} else {
|
||||
let lifetime: LifetimeParam = parse_quote! { 'a };
|
||||
|
||||
let mut fn_generics = generics.clone();
|
||||
fn_generics
|
||||
.params
|
||||
.insert(0, GenericParam::Lifetime(lifetime.clone()));
|
||||
|
||||
(quote! { #lifetime, }, fn_generics, quote! { #generics })
|
||||
};
|
||||
|
||||
let generics_no_bounds = {
|
||||
let mut generics = generics.clone();
|
||||
generics.params = generics
|
||||
.params
|
||||
.iter()
|
||||
.map(|it| match it {
|
||||
GenericParam::Type(tp) => {
|
||||
let mut tp = tp.clone();
|
||||
tp.bounds.clear();
|
||||
|
||||
GenericParam::Type(tp)
|
||||
}
|
||||
_ => it.clone(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
generics
|
||||
};
|
||||
|
||||
out_tokens.append_all(quote! {
|
||||
#modifiers
|
||||
#[allow(non_camel_case_types)]
|
||||
#vis struct #struct_name #struct_generics
|
||||
#where_clause
|
||||
{
|
||||
#(#fields),*
|
||||
}
|
||||
|
||||
#(#attrs)*
|
||||
#maybe_async #vis fn #ident #fn_generics (#cx_token: Scope<#scope_lifetime #struct_name #generics_no_bounds>) #output
|
||||
#where_clause
|
||||
{
|
||||
let #struct_name { #(#field_names),* } = &#cx_token.props;
|
||||
#block
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,23 @@
|
||||
#![doc = include_str!("../README.md")]
|
||||
#![doc(html_logo_url = "https://avatars.githubusercontent.com/u/79236386")]
|
||||
#![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")]
|
||||
|
||||
use proc_macro::TokenStream;
|
||||
use quote::ToTokens;
|
||||
use rsx::RenderCallBody;
|
||||
use syn::parse_macro_input;
|
||||
use syn::parse::Parser;
|
||||
use syn::punctuated::Punctuated;
|
||||
use syn::{parse_macro_input, Path, Token};
|
||||
|
||||
mod inlineprops;
|
||||
mod component_body;
|
||||
mod component_body_deserializers;
|
||||
mod props;
|
||||
mod utils;
|
||||
|
||||
// mod rsx;
|
||||
use crate::component_body::ComponentBody;
|
||||
use crate::component_body_deserializers::component::ComponentDeserializerArgs;
|
||||
use crate::component_body_deserializers::inline_props::InlinePropsDeserializerArgs;
|
||||
use dioxus_rsx as rsx;
|
||||
|
||||
#[proc_macro]
|
||||
@@ -18,7 +29,7 @@ pub fn format_args_f(input: TokenStream) -> TokenStream {
|
||||
}
|
||||
|
||||
#[proc_macro_derive(Props, attributes(props))]
|
||||
pub fn derive_typed_builder(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
|
||||
pub fn derive_typed_builder(input: TokenStream) -> TokenStream {
|
||||
let input = parse_macro_input!(input as syn::DeriveInput);
|
||||
match props::impl_my_derive(&input) {
|
||||
Ok(output) => output.into(),
|
||||
@@ -37,7 +48,7 @@ pub fn rsx(s: TokenStream) -> TokenStream {
|
||||
|
||||
/// The render! macro makes it easy for developers to write jsx-style markup in their components.
|
||||
///
|
||||
/// The render macro automatically renders rsx - making it unhygenic.
|
||||
/// The render macro automatically renders rsx - making it unhygienic.
|
||||
#[proc_macro]
|
||||
pub fn render(s: TokenStream) -> TokenStream {
|
||||
match syn::parse::<rsx::CallBody>(s) {
|
||||
@@ -55,7 +66,7 @@ pub fn render(s: TokenStream) -> TokenStream {
|
||||
/// you would be repeating a lot of the usual Rust boilerplate.
|
||||
///
|
||||
/// # Example
|
||||
/// ```ignore
|
||||
/// ```rust,ignore
|
||||
/// #[inline_props]
|
||||
/// fn app(cx: Scope, bob: String) -> Element {
|
||||
/// cx.render(rsx!("hello, {bob}"))
|
||||
@@ -73,9 +84,118 @@ pub fn render(s: TokenStream) -> TokenStream {
|
||||
/// }
|
||||
/// ```
|
||||
#[proc_macro_attribute]
|
||||
pub fn inline_props(_args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
|
||||
match syn::parse::<inlineprops::InlinePropsBody>(s) {
|
||||
#[deprecated(note = "Use `#[component]` instead.")]
|
||||
pub fn inline_props(_args: TokenStream, s: TokenStream) -> TokenStream {
|
||||
let comp_body = parse_macro_input!(s as ComponentBody);
|
||||
|
||||
match comp_body.deserialize(InlinePropsDeserializerArgs {}) {
|
||||
Err(e) => e.to_compile_error().into(),
|
||||
Ok(s) => s.to_token_stream().into(),
|
||||
Ok(output) => output.to_token_stream().into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) const COMPONENT_ARG_CASE_CHECK_OFF: &str = "no_case_check";
|
||||
|
||||
/// Streamlines component creation.
|
||||
/// This is the recommended way of creating components,
|
||||
/// though you might want lower-level control with more advanced uses.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `no_case_check` - Doesn't enforce `PascalCase` on your component names.
|
||||
/// **This will be removed/deprecated in a future update in favor of a more complete Clippy-backed linting system.**
|
||||
/// The reasoning behind this is that Clippy allows more robust and powerful lints, whereas
|
||||
/// macros are extremely limited.
|
||||
///
|
||||
/// # Features
|
||||
/// This attribute:
|
||||
/// * Enforces that your component uses `PascalCase`.
|
||||
/// No warnings are generated for the `PascalCase`
|
||||
/// function name, but everything else will still raise a warning if it's incorrectly `PascalCase`.
|
||||
/// Does not disable warnings anywhere else, so if you, for example,
|
||||
/// accidentally don't use `snake_case`
|
||||
/// for a variable name in the function, the compiler will still warn you.
|
||||
/// * Automatically uses `#[inline_props]` if there's more than 1 parameter in the function.
|
||||
/// * Verifies the validity of your component.
|
||||
/// E.g. if it has a [`Scope`](dioxus_core::Scope) argument.
|
||||
/// Notes:
|
||||
/// * This doesn't work 100% of the time, because of macro limitations.
|
||||
/// * Provides helpful messages if your component is not correct.
|
||||
/// Possible bugs (please, report these!):
|
||||
/// * There might be bugs where it incorrectly *denies* validity.
|
||||
/// This is bad as it means that you can't use the attribute or you have to change the component.
|
||||
/// * There might be bugs where it incorrectly *confirms* validity.
|
||||
/// You will still know if the component is invalid once you use it,
|
||||
/// but the error might be less helpful.
|
||||
///
|
||||
/// # Examples
|
||||
/// * Without props:
|
||||
/// ```rust,ignore
|
||||
/// #[component]
|
||||
/// fn GreetBob(cx: Scope) -> Element {
|
||||
/// render! { "hello, bob" }
|
||||
/// }
|
||||
///
|
||||
/// // is equivalent to
|
||||
///
|
||||
/// #[allow(non_snake_case)]
|
||||
/// fn GreetBob(cx: Scope) -> Element {
|
||||
/// #[warn(non_snake_case)]
|
||||
/// #[inline(always)]
|
||||
/// fn __dx_inner_comp(cx: Scope) -> Element {
|
||||
/// render! { "hello, bob" }
|
||||
/// }
|
||||
/// // There's no function call overhead since __dx_inner_comp has the #[inline(always)] attribute,
|
||||
/// // so don't worry about performance.
|
||||
/// __dx_inner_comp(cx)
|
||||
/// }
|
||||
/// ```
|
||||
/// * With props:
|
||||
/// ```rust,ignore
|
||||
/// #[component(no_case_check)]
|
||||
/// fn GreetPerson(cx: Scope, person: String) -> Element {
|
||||
/// render! { "hello, {person}" }
|
||||
/// }
|
||||
///
|
||||
/// // is equivalent to
|
||||
///
|
||||
/// #[derive(Props, PartialEq)]
|
||||
/// #[allow(non_camel_case_types)]
|
||||
/// struct GreetPersonProps {
|
||||
/// person: String,
|
||||
/// }
|
||||
///
|
||||
/// #[allow(non_snake_case)]
|
||||
/// fn GreetPerson<'a>(cx: Scope<'a, GreetPersonProps>) -> Element {
|
||||
/// #[warn(non_snake_case)]
|
||||
/// #[inline(always)]
|
||||
/// fn __dx_inner_comp<'a>(cx: Scope<'a, GreetPersonProps>e) -> Element {
|
||||
/// let GreetPersonProps { person } = &cx.props;
|
||||
/// {
|
||||
/// render! { "hello, {person}" }
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// __dx_inner_comp(cx)
|
||||
/// }
|
||||
/// ```
|
||||
// TODO: Maybe add an option to input a custom component name through the args.
|
||||
// I think that's unnecessary, but there might be some scenario where it could be useful.
|
||||
#[proc_macro_attribute]
|
||||
pub fn component(args: TokenStream, input: TokenStream) -> TokenStream {
|
||||
let component_body = parse_macro_input!(input as ComponentBody);
|
||||
let case_check = match Punctuated::<Path, Token![,]>::parse_terminated.parse(args) {
|
||||
Err(e) => return e.to_compile_error().into(),
|
||||
Ok(args) => {
|
||||
if let Some(first) = args.first() {
|
||||
!first.is_ident(COMPONENT_ARG_CASE_CHECK_OFF)
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
match component_body.deserialize(ComponentDeserializerArgs { case_check }) {
|
||||
Err(e) => e.to_compile_error().into(),
|
||||
Ok(output) => output.to_token_stream().into(),
|
||||
}
|
||||
}
|
||||
@@ -2,9 +2,9 @@
|
||||
//!
|
||||
//! However, it has been adopted to fit the Dioxus Props builder pattern.
|
||||
//!
|
||||
//! For dioxus, we make a few changes:
|
||||
//! - [ ] automatically implement Into<Option> on the setters (IE the strip setter option)
|
||||
//! - [ ] automatically implement a default of none for optional fields (those explicitly wrapped with Option<T>)
|
||||
//! For Dioxus, we make a few changes:
|
||||
//! - [ ] Automatically implement Into<Option> on the setters (IE the strip setter option)
|
||||
//! - [ ] Automatically implement a default of none for optional fields (those explicitly wrapped with Option<T>)
|
||||
|
||||
use proc_macro2::TokenStream;
|
||||
|
||||
@@ -194,8 +194,9 @@ mod field_info {
|
||||
|
||||
// children field is automatically defaulted to None
|
||||
if name == "children" {
|
||||
builder_attr.default =
|
||||
Some(syn::parse(quote!(Default::default()).into()).unwrap());
|
||||
builder_attr.default = Some(
|
||||
syn::parse(quote!(::core::default::Default::default()).into()).unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
// auto detect optional
|
||||
@@ -204,8 +205,9 @@ mod field_info {
|
||||
&& type_from_inside_option(&field.ty, true).is_some();
|
||||
if !builder_attr.strip_option && strip_option_auto {
|
||||
builder_attr.strip_option = true;
|
||||
builder_attr.default =
|
||||
Some(syn::parse(quote!(Default::default()).into()).unwrap());
|
||||
builder_attr.default = Some(
|
||||
syn::parse(quote!(::core::default::Default::default()).into()).unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
Ok(FieldInfo {
|
||||
@@ -213,7 +215,7 @@ mod field_info {
|
||||
name,
|
||||
generic_ident: syn::Ident::new(
|
||||
&format!("__{}", strip_raw_ident_prefix(name.to_string())),
|
||||
proc_macro2::Span::call_site(),
|
||||
name.span(),
|
||||
),
|
||||
ty: &field.ty,
|
||||
builder_attr,
|
||||
@@ -241,10 +243,6 @@ mod field_info {
|
||||
}
|
||||
.into()
|
||||
}
|
||||
|
||||
pub fn type_from_inside_option(&self, check_option_name: bool) -> Option<&syn::Type> {
|
||||
type_from_inside_option(self.ty, check_option_name)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
@@ -345,14 +343,18 @@ mod field_info {
|
||||
.ok_or_else(|| Error::new_spanned(&path, "Expected identifier"))?;
|
||||
match name.as_str() {
|
||||
"default" => {
|
||||
self.default =
|
||||
Some(syn::parse(quote!(Default::default()).into()).unwrap());
|
||||
self.default = Some(
|
||||
syn::parse(quote!(::core::default::Default::default()).into())
|
||||
.unwrap(),
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
"optional" => {
|
||||
self.default =
|
||||
Some(syn::parse(quote!(Default::default()).into()).unwrap());
|
||||
self.default = Some(
|
||||
syn::parse(quote!(::core::default::Default::default()).into())
|
||||
.unwrap(),
|
||||
);
|
||||
self.strip_option = true;
|
||||
Ok(())
|
||||
}
|
||||
@@ -505,15 +507,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()),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -548,18 +547,16 @@ mod struct_info {
|
||||
let generics_with_empty = modify_types_generics_hack(&ty_generics, |args| {
|
||||
args.insert(0, syn::GenericArgument::Type(empties_tuple.clone().into()));
|
||||
});
|
||||
let phantom_generics = self.generics.params.iter().map(|param| match param {
|
||||
let phantom_generics = self.generics.params.iter().filter_map(|param| match param {
|
||||
syn::GenericParam::Lifetime(lifetime) => {
|
||||
let lifetime = &lifetime.lifetime;
|
||||
quote!(core::marker::PhantomData<&#lifetime ()>)
|
||||
Some(quote!(::core::marker::PhantomData<&#lifetime ()>))
|
||||
}
|
||||
syn::GenericParam::Type(ty) => {
|
||||
let ty = &ty.ident;
|
||||
quote!(core::marker::PhantomData<#ty>)
|
||||
}
|
||||
syn::GenericParam::Const(_cnst) => {
|
||||
quote!()
|
||||
Some(quote!(::core::marker::PhantomData<#ty>))
|
||||
}
|
||||
syn::GenericParam::Const(_cnst) => None,
|
||||
});
|
||||
let builder_method_doc = match self.builder_attr.builder_method_doc {
|
||||
Some(ref doc) => quote!(#doc),
|
||||
@@ -630,11 +627,11 @@ Finally, call `.build()` to create the instance of `{name}`.
|
||||
Ok(quote! {
|
||||
impl #impl_generics #name #ty_generics #where_clause {
|
||||
#[doc = #builder_method_doc]
|
||||
#[allow(dead_code)]
|
||||
#[allow(dead_code, clippy::type_complexity)]
|
||||
#vis fn builder() -> #builder_name #generics_with_empty {
|
||||
#builder_name {
|
||||
fields: #empties_tuple,
|
||||
_phantom: core::default::Default::default(),
|
||||
_phantom: ::core::default::Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -651,12 +648,12 @@ Finally, call `.build()` to create the instance of `{name}`.
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
fields: self.fields.clone(),
|
||||
_phantom: Default::default(),
|
||||
_phantom: ::core::default::Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl #impl_generics dioxus::prelude::Properties for #name #ty_generics
|
||||
impl #impl_generics ::dioxus::prelude::Properties for #name #ty_generics
|
||||
#b_generics_where_extras_predicates
|
||||
{
|
||||
type Builder = #builder_name #generics_with_empty;
|
||||
@@ -698,6 +695,14 @@ Finally, call `.build()` to create the instance of `{name}`.
|
||||
}
|
||||
|
||||
pub fn field_impl(&self, field: &FieldInfo) -> Result<TokenStream, Error> {
|
||||
let FieldInfo {
|
||||
name: field_name,
|
||||
ty: field_type,
|
||||
..
|
||||
} = field;
|
||||
if *field_name == "key" {
|
||||
return Err(Error::new_spanned(field_name, "Naming a prop `key` is not allowed because the name can conflict with the built in key attribute. See https://dioxuslabs.com/learn/0.4/reference/dynamic_rendering#rendering-lists for more information about keys"));
|
||||
}
|
||||
let StructInfo {
|
||||
ref builder_name, ..
|
||||
} = *self;
|
||||
@@ -712,11 +717,6 @@ Finally, call `.build()` to create the instance of `{name}`.
|
||||
});
|
||||
let reconstructing = self.included_fields().map(|f| f.name);
|
||||
|
||||
let FieldInfo {
|
||||
name: field_name,
|
||||
ty: field_type,
|
||||
..
|
||||
} = field;
|
||||
let mut ty_generics: Vec<syn::GenericArgument> = self
|
||||
.generics
|
||||
.params
|
||||
@@ -779,31 +779,16 @@ Finally, call `.build()` to create the instance of `{name}`.
|
||||
None => quote!(),
|
||||
};
|
||||
|
||||
// NOTE: both auto_into and strip_option affect `arg_type` and `arg_expr`, but the order of
|
||||
// nesting is different so we have to do this little dance.
|
||||
let arg_type = if field.builder_attr.strip_option {
|
||||
field.type_from_inside_option(false).ok_or_else(|| {
|
||||
Error::new_spanned(
|
||||
field_type,
|
||||
"can't `strip_option` - field is not `Option<...>`",
|
||||
let arg_type = field_type;
|
||||
let (arg_type, arg_expr) =
|
||||
if field.builder_attr.auto_into || field.builder_attr.strip_option {
|
||||
(
|
||||
quote!(impl ::core::convert::Into<#arg_type>),
|
||||
quote!(#field_name.into()),
|
||||
)
|
||||
})?
|
||||
} else {
|
||||
field_type
|
||||
};
|
||||
let (arg_type, arg_expr) = if field.builder_attr.auto_into {
|
||||
(
|
||||
quote!(impl core::convert::Into<#arg_type>),
|
||||
quote!(#field_name.into()),
|
||||
)
|
||||
} else {
|
||||
(quote!(#arg_type), quote!(#field_name))
|
||||
};
|
||||
let arg_expr = if field.builder_attr.strip_option {
|
||||
quote!(Some(#arg_expr))
|
||||
} else {
|
||||
arg_expr
|
||||
};
|
||||
} else {
|
||||
(quote!(#arg_type), quote!(#field_name))
|
||||
};
|
||||
|
||||
let repeated_fields_error_type_name = syn::Ident::new(
|
||||
&format!(
|
||||
@@ -811,7 +796,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}");
|
||||
|
||||
@@ -819,6 +804,7 @@ Finally, call `.build()` to create the instance of `{name}`.
|
||||
#[allow(dead_code, non_camel_case_types, missing_docs)]
|
||||
impl #impl_generics #builder_name < #( #ty_generics ),* > #where_clause {
|
||||
#doc
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub fn #field_name (self, #field_name: #arg_type) -> #builder_name < #( #target_generics ),* > {
|
||||
let #field_name = (#arg_expr,);
|
||||
let ( #(#descructuring,)* ) = self.fields;
|
||||
@@ -837,6 +823,7 @@ Finally, call `.build()` to create the instance of `{name}`.
|
||||
#[deprecated(
|
||||
note = #repeated_fields_error_message
|
||||
)]
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub fn #field_name (self, _: #repeated_fields_error_type_name) -> #builder_name < #( #target_generics ),* > {
|
||||
self
|
||||
}
|
||||
@@ -930,7 +917,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}");
|
||||
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
use quote::ToTokens;
|
||||
use syn::parse::{Parse, ParseStream};
|
||||
use syn::spanned::Spanned;
|
||||
use syn::{parse_quote, Expr, Lit, Meta, Token, Type};
|
||||
|
||||
const FORMATTED_TYPE_START: &str = "static TY_AFTER_HERE:";
|
||||
const FORMATTED_TYPE_END: &str = "= todo!();";
|
||||
|
||||
/// Attempts to convert the given literal to a string.
|
||||
/// Converts ints and floats to their base 10 counterparts.
|
||||
///
|
||||
/// Returns `None` if the literal is [`Lit::Verbatim`] or if the literal is [`Lit::ByteStr`]
|
||||
/// and the byte string could not be converted to UTF-8.
|
||||
pub fn lit_to_string(lit: Lit) -> Option<String> {
|
||||
match lit {
|
||||
Lit::Str(l) => Some(l.value()),
|
||||
Lit::ByteStr(l) => String::from_utf8(l.value()).ok(),
|
||||
Lit::Byte(l) => Some(String::from(l.value() as char)),
|
||||
Lit::Char(l) => Some(l.value().to_string()),
|
||||
Lit::Int(l) => Some(l.base10_digits().to_string()),
|
||||
Lit::Float(l) => Some(l.base10_digits().to_string()),
|
||||
Lit::Bool(l) => Some(l.value().to_string()),
|
||||
Lit::Verbatim(_) => None,
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn format_type_string(ty: &Type) -> String {
|
||||
let ty_unformatted = ty.into_token_stream().to_string();
|
||||
let ty_unformatted = ty_unformatted.trim();
|
||||
|
||||
// This should always be valid syntax.
|
||||
// Not Rust code, but syntax, which is the only thing that `syn` cares about.
|
||||
let Ok(file_unformatted) = syn::parse_file(&format!(
|
||||
"{FORMATTED_TYPE_START}{ty_unformatted}{FORMATTED_TYPE_END}"
|
||||
)) else {
|
||||
return ty_unformatted.to_string();
|
||||
};
|
||||
|
||||
let file_formatted = prettyplease::unparse(&file_unformatted);
|
||||
|
||||
let file_trimmed = file_formatted.trim();
|
||||
let start_removed = file_trimmed.trim_start_matches(FORMATTED_TYPE_START);
|
||||
let end_removed = start_removed.trim_end_matches(FORMATTED_TYPE_END);
|
||||
let ty_formatted = end_removed.trim();
|
||||
|
||||
ty_formatted.to_string()
|
||||
}
|
||||
|
||||
/// Represents the `#[deprecated]` attribute.
|
||||
///
|
||||
/// You can use the [`DeprecatedAttribute::from_meta`] function to try to parse an attribute to this struct.
|
||||
#[derive(Default)]
|
||||
pub struct DeprecatedAttribute {
|
||||
pub since: Option<String>,
|
||||
pub note: Option<String>,
|
||||
}
|
||||
|
||||
impl DeprecatedAttribute {
|
||||
/// Returns `None` if the given attribute was not a valid form of the `#[deprecated]` attribute.
|
||||
pub fn from_meta(meta: &Meta) -> syn::Result<Self> {
|
||||
if meta.path() != &parse_quote!(deprecated) {
|
||||
return Err(syn::Error::new(
|
||||
meta.span(),
|
||||
"attribute path is not `deprecated`",
|
||||
));
|
||||
}
|
||||
|
||||
match &meta {
|
||||
Meta::Path(_) => Ok(Self::default()),
|
||||
Meta::NameValue(name_value) => {
|
||||
let Expr::Lit(expr_lit) = &name_value.value else {
|
||||
return Err(syn::Error::new(
|
||||
name_value.span(),
|
||||
"literal in `deprecated` value must be a string",
|
||||
));
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
since: None,
|
||||
note: lit_to_string(expr_lit.lit.clone()).map(|s| s.trim().to_string()),
|
||||
})
|
||||
}
|
||||
Meta::List(list) => {
|
||||
let parsed = list.parse_args::<DeprecatedAttributeArgsParser>()?;
|
||||
|
||||
Ok(Self {
|
||||
since: parsed.since.map(|s| s.trim().to_string()),
|
||||
note: parsed.note.map(|s| s.trim().to_string()),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod kw {
|
||||
use syn::custom_keyword;
|
||||
custom_keyword!(since);
|
||||
custom_keyword!(note);
|
||||
}
|
||||
|
||||
struct DeprecatedAttributeArgsParser {
|
||||
since: Option<String>,
|
||||
note: Option<String>,
|
||||
}
|
||||
|
||||
impl Parse for DeprecatedAttributeArgsParser {
|
||||
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||
let mut since: Option<String> = None;
|
||||
let mut note: Option<String> = None;
|
||||
|
||||
if input.peek(kw::since) {
|
||||
input.parse::<kw::since>()?;
|
||||
input.parse::<Token![=]>()?;
|
||||
|
||||
since = lit_to_string(input.parse()?);
|
||||
}
|
||||
|
||||
if input.peek(Token![,]) && input.peek2(kw::note) {
|
||||
input.parse::<Token![,]>()?;
|
||||
input.parse::<kw::note>()?;
|
||||
input.parse::<Token![=]>()?;
|
||||
|
||||
note = lit_to_string(input.parse()?);
|
||||
}
|
||||
|
||||
Ok(Self { since, note })
|
||||
}
|
||||
}
|
||||
+65
-67
@@ -13,62 +13,50 @@ use crate::{
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
|
||||
pub struct ElementId(pub usize);
|
||||
|
||||
pub(crate) struct ElementRef {
|
||||
/// An Element that can be bubbled to's unique identifier.
|
||||
///
|
||||
/// `BubbleId` is a `usize` that is unique across the entire VirtualDOM - but not unique across time. If a component is
|
||||
/// unmounted, then the `BubbleId` will be reused for a new component.
|
||||
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
|
||||
pub struct VNodeId(pub usize);
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct ElementRef {
|
||||
// the pathway of the real element inside the template
|
||||
pub path: ElementPath,
|
||||
pub(crate) path: ElementPath,
|
||||
|
||||
// The actual template
|
||||
pub template: Option<NonNull<VNode<'static>>>,
|
||||
pub(crate) template: VNodeId,
|
||||
|
||||
// The scope the element belongs to
|
||||
pub scope: ScopeId,
|
||||
pub(crate) scope: ScopeId,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum ElementPath {
|
||||
Deep(&'static [u8]),
|
||||
Root(usize),
|
||||
}
|
||||
|
||||
impl ElementRef {
|
||||
pub(crate) fn none() -> Self {
|
||||
Self {
|
||||
template: None,
|
||||
path: ElementPath::Root(0),
|
||||
scope: ScopeId::ROOT,
|
||||
}
|
||||
}
|
||||
pub struct ElementPath {
|
||||
pub(crate) path: &'static [u8],
|
||||
}
|
||||
|
||||
impl VirtualDom {
|
||||
pub(crate) fn next_element(&mut self, template: &VNode, path: &'static [u8]) -> ElementId {
|
||||
self.next_reference(template, ElementPath::Deep(path))
|
||||
pub(crate) fn next_element(&mut self) -> ElementId {
|
||||
ElementId(self.elements.insert(None))
|
||||
}
|
||||
|
||||
pub(crate) fn next_root(&mut self, template: &VNode, path: usize) -> ElementId {
|
||||
self.next_reference(template, ElementPath::Root(path))
|
||||
}
|
||||
pub(crate) fn next_vnode_ref(&mut self, vnode: &VNode) -> VNodeId {
|
||||
let new_id = VNodeId(self.element_refs.insert(Some(unsafe {
|
||||
std::mem::transmute::<NonNull<VNode>, _>(vnode.into())
|
||||
})));
|
||||
|
||||
pub(crate) fn next_null(&mut self) -> ElementId {
|
||||
let entry = self.elements.vacant_entry();
|
||||
let id = entry.key();
|
||||
// Set this id to be dropped when the scope is rerun
|
||||
if let Some(scope) = self.runtime.current_scope_id() {
|
||||
self.scopes[scope.0]
|
||||
.element_refs_to_drop
|
||||
.borrow_mut()
|
||||
.push(new_id);
|
||||
}
|
||||
|
||||
entry.insert(ElementRef::none());
|
||||
ElementId(id)
|
||||
}
|
||||
|
||||
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)
|
||||
new_id
|
||||
}
|
||||
|
||||
pub(crate) fn reclaim(&mut self, el: ElementId) {
|
||||
@@ -76,7 +64,7 @@ impl VirtualDom {
|
||||
.unwrap_or_else(|| panic!("cannot reclaim {:?}", el));
|
||||
}
|
||||
|
||||
pub(crate) fn try_reclaim(&mut self, el: ElementId) -> Option<ElementRef> {
|
||||
pub(crate) fn try_reclaim(&mut self, el: ElementId) -> Option<()> {
|
||||
if el.0 == 0 {
|
||||
panic!(
|
||||
"Cannot reclaim the root element - {:#?}",
|
||||
@@ -84,12 +72,12 @@ impl VirtualDom {
|
||||
);
|
||||
}
|
||||
|
||||
self.elements.try_remove(el.0)
|
||||
self.elements.try_remove(el.0).map(|_| ())
|
||||
}
|
||||
|
||||
pub(crate) fn update_template(&mut self, el: ElementId, node: &VNode) {
|
||||
let node: *const VNode = node as *const _;
|
||||
self.elements[el.0].template = unsafe { std::mem::transmute(node) };
|
||||
pub(crate) fn set_template(&mut self, id: VNodeId, vnode: &VNode) {
|
||||
self.element_refs[id.0] =
|
||||
Some(unsafe { std::mem::transmute::<NonNull<VNode>, _>(vnode.into()) });
|
||||
}
|
||||
|
||||
// Drop a scope and all its children
|
||||
@@ -101,6 +89,15 @@ impl VirtualDom {
|
||||
id,
|
||||
});
|
||||
|
||||
// Remove all VNode ids from the scope
|
||||
for id in self.scopes[id.0]
|
||||
.element_refs_to_drop
|
||||
.borrow_mut()
|
||||
.drain(..)
|
||||
{
|
||||
self.element_refs.try_remove(id.0);
|
||||
}
|
||||
|
||||
self.ensure_drop_safety(id);
|
||||
|
||||
if recursive {
|
||||
@@ -145,14 +142,25 @@ impl VirtualDom {
|
||||
}
|
||||
|
||||
/// Descend through the tree, removing any borrowed props and listeners
|
||||
pub(crate) fn ensure_drop_safety(&self, scope_id: ScopeId) {
|
||||
pub(crate) fn ensure_drop_safety(&mut self, scope_id: ScopeId) {
|
||||
let scope = &self.scopes[scope_id.0];
|
||||
|
||||
{
|
||||
// Drop all element refs that could be invalidated when the component was rerun
|
||||
let mut element_refs = self.scopes[scope_id.0].element_refs_to_drop.borrow_mut();
|
||||
let element_refs_slab = &mut self.element_refs;
|
||||
for element_ref in element_refs.drain(..) {
|
||||
if let Some(element_ref) = element_refs_slab.get_mut(element_ref.0) {
|
||||
*element_ref = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
// recursively call ensure_drop_safety on all children
|
||||
let mut props = scope.borrowed_props.borrow_mut();
|
||||
props.drain(..).for_each(|comp| {
|
||||
let props = { scope.borrowed_props.borrow_mut().clone() };
|
||||
for comp in props {
|
||||
let comp = unsafe { &*comp };
|
||||
match comp.scope.get() {
|
||||
Some(child) if child != scope_id => self.ensure_drop_safety(child),
|
||||
@@ -161,20 +169,16 @@ impl VirtualDom {
|
||||
if let Ok(mut props) = comp.props.try_borrow_mut() {
|
||||
*props = None;
|
||||
}
|
||||
});
|
||||
}
|
||||
let scope = &self.scopes[scope_id.0];
|
||||
scope.borrowed_props.borrow_mut().clear();
|
||||
|
||||
// Now that all the references are gone, we can safely drop our own references in our listeners.
|
||||
let mut listeners = scope.attributes_to_drop.borrow_mut();
|
||||
let mut listeners = scope.attributes_to_drop_before_render.borrow_mut();
|
||||
listeners.drain(..).for_each(|listener| {
|
||||
let listener = unsafe { &*listener };
|
||||
match &listener.value {
|
||||
AttributeValue::Listener(l) => {
|
||||
_ = l.take();
|
||||
}
|
||||
AttributeValue::Any(a) => {
|
||||
_ = a.take();
|
||||
}
|
||||
_ => (),
|
||||
if let AttributeValue::Listener(l) = &listener.value {
|
||||
_ = l.take();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -182,18 +186,12 @@ impl VirtualDom {
|
||||
|
||||
impl ElementPath {
|
||||
pub(crate) fn is_decendant(&self, small: &&[u8]) -> bool {
|
||||
match *self {
|
||||
ElementPath::Deep(big) => small.len() <= big.len() && *small == &big[..small.len()],
|
||||
ElementPath::Root(r) => small.len() == 1 && small[0] == r as u8,
|
||||
}
|
||||
small.len() <= self.path.len() && *small == &self.path[..small.len()]
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<&[u8]> for ElementPath {
|
||||
fn eq(&self, other: &&[u8]) -> bool {
|
||||
match *self {
|
||||
ElementPath::Deep(deep) => deep.eq(*other),
|
||||
ElementPath::Root(r) => other.len() == 1 && other[0] == r as u8,
|
||||
}
|
||||
self.path.eq(*other)
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,16 @@
|
||||
use crate::nodes::RenderReturn;
|
||||
use crate::{Attribute, AttributeValue, VComponent};
|
||||
use bumpalo::Bump;
|
||||
use std::cell::RefCell;
|
||||
use std::cell::{Cell, UnsafeCell};
|
||||
|
||||
pub(crate) struct BumpFrame {
|
||||
pub bump: UnsafeCell<Bump>,
|
||||
pub node: Cell<*const RenderReturn<'static>>,
|
||||
|
||||
// The bump allocator will not call the destructor of the objects it allocated. Attributes and props need to have there destructor called, so we keep a list of them to drop before the bump allocator is reset.
|
||||
pub(crate) attributes_to_drop_before_reset: RefCell<Vec<*const Attribute<'static>>>,
|
||||
pub(crate) props_to_drop_before_reset: RefCell<Vec<*const VComponent<'static>>>,
|
||||
}
|
||||
|
||||
impl BumpFrame {
|
||||
@@ -13,6 +19,8 @@ impl BumpFrame {
|
||||
Self {
|
||||
bump: UnsafeCell::new(bump),
|
||||
node: Cell::new(std::ptr::null()),
|
||||
attributes_to_drop_before_reset: Default::default(),
|
||||
props_to_drop_before_reset: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,8 +39,38 @@ impl BumpFrame {
|
||||
unsafe { &*self.bump.get() }
|
||||
}
|
||||
|
||||
#[allow(clippy::mut_from_ref)]
|
||||
pub(crate) unsafe fn bump_mut(&self) -> &mut Bump {
|
||||
unsafe { &mut *self.bump.get() }
|
||||
pub(crate) fn add_attribute_to_drop(&self, attribute: *const Attribute<'static>) {
|
||||
self.attributes_to_drop_before_reset
|
||||
.borrow_mut()
|
||||
.push(attribute);
|
||||
}
|
||||
|
||||
/// Reset the bump allocator and drop all the attributes and props that were allocated in it.
|
||||
///
|
||||
/// # Safety
|
||||
/// The caller must insure that no reference to anything allocated in the bump allocator is available after this function is called.
|
||||
pub(crate) unsafe fn reset(&self) {
|
||||
let mut attributes = self.attributes_to_drop_before_reset.borrow_mut();
|
||||
attributes.drain(..).for_each(|attribute| {
|
||||
let attribute = unsafe { &*attribute };
|
||||
if let AttributeValue::Any(l) = &attribute.value {
|
||||
_ = l.take();
|
||||
}
|
||||
});
|
||||
let mut props = self.props_to_drop_before_reset.borrow_mut();
|
||||
props.drain(..).for_each(|prop| {
|
||||
let prop = unsafe { &*prop };
|
||||
_ = prop.props.borrow_mut().take();
|
||||
});
|
||||
unsafe {
|
||||
let bump = &mut *self.bump.get();
|
||||
bump.reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for BumpFrame {
|
||||
fn drop(&mut self) {
|
||||
unsafe { self.reset() }
|
||||
}
|
||||
}
|
||||
+80
-45
@@ -1,5 +1,7 @@
|
||||
use crate::any_props::AnyProps;
|
||||
use crate::innerlude::{BorrowedAttributeValue, VComponent, VPlaceholder, VText};
|
||||
use crate::innerlude::{
|
||||
BorrowedAttributeValue, ElementPath, ElementRef, VComponent, VPlaceholder, VText,
|
||||
};
|
||||
use crate::mutations::Mutation;
|
||||
use crate::mutations::Mutation::*;
|
||||
use crate::nodes::VNode;
|
||||
@@ -94,6 +96,9 @@ impl<'b> VirtualDom {
|
||||
nodes_mut.resize(len, ElementId::default());
|
||||
};
|
||||
|
||||
// Set this node id
|
||||
node.stable_id.set(Some(self.next_vnode_ref(node)));
|
||||
|
||||
// The best renderers will have templates prehydrated and registered
|
||||
// Just in case, let's create the template using instructions anyways
|
||||
self.register_template(node.template.get());
|
||||
@@ -181,15 +186,30 @@ impl<'b> VirtualDom {
|
||||
use DynamicNode::*;
|
||||
match &template.dynamic_nodes[idx] {
|
||||
node @ Component { .. } | node @ Fragment(_) => {
|
||||
self.create_dynamic_node(template, node, idx)
|
||||
let template_ref = ElementRef {
|
||||
path: ElementPath {
|
||||
path: template.template.get().node_paths[idx],
|
||||
},
|
||||
template: template.stable_id().unwrap(),
|
||||
scope: self.runtime.current_scope_id().unwrap_or(ScopeId(0)),
|
||||
};
|
||||
self.create_dynamic_node(template_ref, node)
|
||||
}
|
||||
Placeholder(VPlaceholder { id }) => {
|
||||
let id = self.set_slot(template, id, idx);
|
||||
Placeholder(VPlaceholder { id, parent }) => {
|
||||
let template_ref = ElementRef {
|
||||
path: ElementPath {
|
||||
path: template.template.get().node_paths[idx],
|
||||
},
|
||||
template: template.stable_id().unwrap(),
|
||||
scope: self.runtime.current_scope_id().unwrap_or(ScopeId(0)),
|
||||
};
|
||||
parent.set(Some(template_ref));
|
||||
let id = self.set_slot(id);
|
||||
self.mutations.push(CreatePlaceholder { id });
|
||||
1
|
||||
}
|
||||
Text(VText { id, value }) => {
|
||||
let id = self.set_slot(template, id, idx);
|
||||
let id = self.set_slot(id);
|
||||
self.create_static_text(value, id);
|
||||
1
|
||||
}
|
||||
@@ -205,7 +225,7 @@ impl<'b> VirtualDom {
|
||||
});
|
||||
}
|
||||
|
||||
/// We write all the descndent data for this element
|
||||
/// We write all the descendent data for this element
|
||||
///
|
||||
/// Elements can contain other nodes - and those nodes can be dynamic or static
|
||||
///
|
||||
@@ -265,7 +285,14 @@ impl<'b> VirtualDom {
|
||||
.map(|sorted_index| dynamic_nodes[sorted_index].0);
|
||||
|
||||
for idx in reversed_iter {
|
||||
let m = self.create_dynamic_node(template, &template.dynamic_nodes[idx], idx);
|
||||
let boundary_ref = ElementRef {
|
||||
path: ElementPath {
|
||||
path: template.template.get().node_paths[idx],
|
||||
},
|
||||
template: template.stable_id().unwrap(),
|
||||
scope: self.runtime.current_scope_id().unwrap_or(ScopeId(0)),
|
||||
};
|
||||
let m = self.create_dynamic_node(boundary_ref, &template.dynamic_nodes[idx]);
|
||||
if m > 0 {
|
||||
// The path is one shorter because the top node is the root
|
||||
let path = &template.template.get().node_paths[idx][1..];
|
||||
@@ -279,15 +306,15 @@ impl<'b> VirtualDom {
|
||||
attrs: &mut Peekable<impl Iterator<Item = (usize, &'static [u8])>>,
|
||||
root_idx: u8,
|
||||
root: ElementId,
|
||||
node: &VNode,
|
||||
node: &'b VNode<'b>,
|
||||
) {
|
||||
while let Some((mut attr_id, path)) =
|
||||
attrs.next_if(|(_, p)| p.first().copied() == Some(root_idx))
|
||||
{
|
||||
let id = self.assign_static_node_as_dynamic(path, root, node, attr_id);
|
||||
let id = self.assign_static_node_as_dynamic(path, root);
|
||||
|
||||
loop {
|
||||
self.write_attribute(&node.dynamic_attrs[attr_id], id);
|
||||
self.write_attribute(node, attr_id, &node.dynamic_attrs[attr_id], id);
|
||||
|
||||
// Only push the dynamic attributes forward if they match the current path (same element)
|
||||
match attrs.next_if(|(_, p)| *p == path) {
|
||||
@@ -298,7 +325,13 @@ impl<'b> VirtualDom {
|
||||
}
|
||||
}
|
||||
|
||||
fn write_attribute(&mut self, attribute: &'b crate::Attribute<'b>, id: ElementId) {
|
||||
fn write_attribute(
|
||||
&mut self,
|
||||
template: &'b VNode<'b>,
|
||||
idx: usize,
|
||||
attribute: &'b crate::Attribute<'b>,
|
||||
id: ElementId,
|
||||
) {
|
||||
// Make sure we set the attribute's associated id
|
||||
attribute.mounted_element.set(id);
|
||||
|
||||
@@ -307,6 +340,13 @@ impl<'b> VirtualDom {
|
||||
|
||||
match &attribute.value {
|
||||
AttributeValue::Listener(_) => {
|
||||
let path = &template.template.get().attr_paths[idx];
|
||||
let element_ref = ElementRef {
|
||||
path: ElementPath { path },
|
||||
template: template.stable_id().unwrap(),
|
||||
scope: self.runtime.current_scope_id().unwrap_or(ScopeId(0)),
|
||||
};
|
||||
self.elements[id.0] = Some(element_ref);
|
||||
self.mutations.push(NewEventListener {
|
||||
// all listeners start with "on"
|
||||
name: &unbounded_name[2..],
|
||||
@@ -330,7 +370,7 @@ impl<'b> VirtualDom {
|
||||
|
||||
fn load_template_root(&mut self, template: &VNode, root_idx: usize) -> ElementId {
|
||||
// Get an ID for this root since it's a real root
|
||||
let this_id = self.next_root(template, root_idx);
|
||||
let this_id = self.next_element();
|
||||
template.root_ids.borrow_mut()[root_idx] = this_id;
|
||||
|
||||
self.mutations.push(LoadTemplate {
|
||||
@@ -353,8 +393,6 @@ impl<'b> VirtualDom {
|
||||
&mut self,
|
||||
path: &'static [u8],
|
||||
this_id: ElementId,
|
||||
template: &VNode,
|
||||
attr_id: usize,
|
||||
) -> ElementId {
|
||||
if path.len() == 1 {
|
||||
return this_id;
|
||||
@@ -362,7 +400,7 @@ impl<'b> VirtualDom {
|
||||
|
||||
// if attribute is on a root node, then we've already created the element
|
||||
// Else, it's deep in the template and we should create a new id for it
|
||||
let id = self.next_element(template, template.template.get().attr_paths[attr_id]);
|
||||
let id = self.next_element();
|
||||
|
||||
self.mutations.push(Mutation::AssignId {
|
||||
path: &path[1..],
|
||||
@@ -405,6 +443,7 @@ impl<'b> VirtualDom {
|
||||
#[allow(unused_mut)]
|
||||
pub(crate) fn register_template(&mut self, mut template: Template<'static>) {
|
||||
let (path, byte_index) = template.name.rsplit_once(':').unwrap();
|
||||
|
||||
let byte_index = byte_index.parse::<usize>().unwrap();
|
||||
// First, check if we've already seen this template
|
||||
if self
|
||||
@@ -439,27 +478,21 @@ impl<'b> VirtualDom {
|
||||
|
||||
pub(crate) fn create_dynamic_node(
|
||||
&mut self,
|
||||
template: &'b VNode<'b>,
|
||||
parent: ElementRef,
|
||||
node: &'b DynamicNode<'b>,
|
||||
idx: usize,
|
||||
) -> usize {
|
||||
use DynamicNode::*;
|
||||
match node {
|
||||
Text(text) => self.create_dynamic_text(template, text, idx),
|
||||
Placeholder(place) => self.create_placeholder(place, template, idx),
|
||||
Component(component) => self.create_component_node(template, component),
|
||||
Fragment(frag) => frag.iter().map(|child| self.create(child)).sum(),
|
||||
Text(text) => self.create_dynamic_text(parent, text),
|
||||
Placeholder(place) => self.create_placeholder(place, parent),
|
||||
Component(component) => self.create_component_node(Some(parent), component),
|
||||
Fragment(frag) => self.create_children(*frag, Some(parent)),
|
||||
}
|
||||
}
|
||||
|
||||
fn create_dynamic_text(
|
||||
&mut self,
|
||||
template: &'b VNode<'b>,
|
||||
text: &'b VText<'b>,
|
||||
idx: usize,
|
||||
) -> usize {
|
||||
fn create_dynamic_text(&mut self, parent: ElementRef, text: &'b VText<'b>) -> usize {
|
||||
// Allocate a dynamic element reference for this text node
|
||||
let new_id = self.next_element(template, template.template.get().node_paths[idx]);
|
||||
let new_id = self.next_element();
|
||||
|
||||
// Make sure the text node is assigned to the correct element
|
||||
text.id.set(Some(new_id));
|
||||
@@ -470,7 +503,7 @@ impl<'b> VirtualDom {
|
||||
// Add the mutation to the list
|
||||
self.mutations.push(HydrateText {
|
||||
id: new_id,
|
||||
path: &template.template.get().node_paths[idx][1..],
|
||||
path: &parent.path.path[1..],
|
||||
value,
|
||||
});
|
||||
|
||||
@@ -481,18 +514,20 @@ impl<'b> VirtualDom {
|
||||
pub(crate) fn create_placeholder(
|
||||
&mut self,
|
||||
placeholder: &VPlaceholder,
|
||||
template: &'b VNode<'b>,
|
||||
idx: usize,
|
||||
parent: ElementRef,
|
||||
) -> usize {
|
||||
// Allocate a dynamic element reference for this text node
|
||||
let id = self.next_element(template, template.template.get().node_paths[idx]);
|
||||
let id = self.next_element();
|
||||
|
||||
// Make sure the text node is assigned to the correct element
|
||||
placeholder.id.set(Some(id));
|
||||
|
||||
// Assign the placeholder's parent
|
||||
placeholder.parent.set(Some(parent));
|
||||
|
||||
// Assign the ID to the existing node in the template
|
||||
self.mutations.push(AssignId {
|
||||
path: &template.template.get().node_paths[idx][1..],
|
||||
path: &parent.path.path[1..],
|
||||
id,
|
||||
});
|
||||
|
||||
@@ -502,7 +537,7 @@ impl<'b> VirtualDom {
|
||||
|
||||
pub(super) fn create_component_node(
|
||||
&mut self,
|
||||
template: &'b VNode<'b>,
|
||||
parent: Option<ElementRef>,
|
||||
component: &'b VComponent<'b>,
|
||||
) -> usize {
|
||||
use RenderReturn::*;
|
||||
@@ -514,8 +549,11 @@ impl<'b> VirtualDom {
|
||||
|
||||
match unsafe { self.run_scope(scope).extend_lifetime_ref() } {
|
||||
// Create the component's root element
|
||||
Ready(t) => self.create_scope(scope, t),
|
||||
Aborted(t) => self.mount_aborted(template, t),
|
||||
Ready(t) => {
|
||||
self.assign_boundary_ref(parent, t);
|
||||
self.create_scope(scope, t)
|
||||
}
|
||||
Aborted(t) => self.mount_aborted(t, parent),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -531,20 +569,17 @@ impl<'b> VirtualDom {
|
||||
.unwrap_or_else(|| component.scope.get().unwrap())
|
||||
}
|
||||
|
||||
fn mount_aborted(&mut self, parent: &'b VNode<'b>, placeholder: &VPlaceholder) -> usize {
|
||||
let id = self.next_element(parent, &[]);
|
||||
fn mount_aborted(&mut self, placeholder: &VPlaceholder, parent: Option<ElementRef>) -> usize {
|
||||
let id = self.next_element();
|
||||
self.mutations.push(Mutation::CreatePlaceholder { id });
|
||||
placeholder.id.set(Some(id));
|
||||
placeholder.parent.set(parent);
|
||||
|
||||
1
|
||||
}
|
||||
|
||||
fn set_slot(
|
||||
&mut self,
|
||||
template: &'b VNode<'b>,
|
||||
slot: &'b Cell<Option<ElementId>>,
|
||||
id: usize,
|
||||
) -> ElementId {
|
||||
let id = self.next_element(template, template.template.get().node_paths[id]);
|
||||
fn set_slot(&mut self, slot: &'b Cell<Option<ElementId>>) -> ElementId {
|
||||
let id = self.next_element();
|
||||
slot.set(Some(id));
|
||||
id
|
||||
}
|
||||
|
||||
+151
-68
@@ -1,7 +1,10 @@
|
||||
use crate::{
|
||||
any_props::AnyProps,
|
||||
arena::ElementId,
|
||||
innerlude::{BorrowedAttributeValue, DirtyScope, VComponent, VPlaceholder, VText},
|
||||
innerlude::{
|
||||
BorrowedAttributeValue, DirtyScope, ElementPath, ElementRef, VComponent, VPlaceholder,
|
||||
VText,
|
||||
},
|
||||
mutations::Mutation,
|
||||
nodes::RenderReturn,
|
||||
nodes::{DynamicNode, VNode},
|
||||
@@ -39,19 +42,27 @@ impl<'b> VirtualDom {
|
||||
(Ready(l), Aborted(p)) => self.diff_ok_to_err(l, p),
|
||||
|
||||
// Just move over the placeholder
|
||||
(Aborted(l), Aborted(r)) => r.id.set(l.id.get()),
|
||||
(Aborted(l), Aborted(r)) => {
|
||||
r.id.set(l.id.get());
|
||||
r.parent.set(l.parent.get())
|
||||
}
|
||||
|
||||
// Placeholder becomes something
|
||||
// We should also clear the error now
|
||||
(Aborted(l), Ready(r)) => self.replace_placeholder(l, [r]),
|
||||
(Aborted(l), Ready(r)) => self.replace_placeholder(
|
||||
l,
|
||||
[r],
|
||||
l.parent.get().expect("root node should not be none"),
|
||||
),
|
||||
};
|
||||
}
|
||||
self.runtime.scope_stack.borrow_mut().pop();
|
||||
}
|
||||
|
||||
fn diff_ok_to_err(&mut self, l: &'b VNode<'b>, p: &'b VPlaceholder) {
|
||||
let id = self.next_null();
|
||||
let id = self.next_element();
|
||||
p.id.set(Some(id));
|
||||
p.parent.set(l.parent.get());
|
||||
self.mutations.push(Mutation::CreatePlaceholder { id });
|
||||
|
||||
let pre_edits = self.mutations.edits.len();
|
||||
@@ -81,12 +92,24 @@ impl<'b> VirtualDom {
|
||||
if let Some(&template) = map.get(&byte_index) {
|
||||
right_template.template.set(template);
|
||||
if template != left_template.template.get() {
|
||||
return self.replace(left_template, [right_template]);
|
||||
let parent = left_template.parent.take();
|
||||
return self.replace(left_template, [right_template], parent);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Copy over the parent
|
||||
{
|
||||
right_template.parent.set(left_template.parent.get());
|
||||
}
|
||||
|
||||
// Update the bubble id pointer
|
||||
right_template.stable_id.set(left_template.stable_id.get());
|
||||
if let Some(bubble_id) = right_template.stable_id.get() {
|
||||
self.set_template(bubble_id, right_template);
|
||||
}
|
||||
|
||||
// If the templates are the same, we don't need to do anything, nor do we want to
|
||||
if templates_are_the_same(left_template, right_template) {
|
||||
return;
|
||||
@@ -105,12 +128,8 @@ impl<'b> VirtualDom {
|
||||
.zip(right_template.dynamic_attrs.iter())
|
||||
.for_each(|(left_attr, right_attr)| {
|
||||
// Move over the ID from the old to the new
|
||||
right_attr
|
||||
.mounted_element
|
||||
.set(left_attr.mounted_element.get());
|
||||
|
||||
// We want to make sure anything that gets pulled is valid
|
||||
self.update_template(left_attr.mounted_element.get(), right_template);
|
||||
let mounted_element = left_attr.mounted_element.get();
|
||||
right_attr.mounted_element.set(mounted_element);
|
||||
|
||||
// If the attributes are different (or volatile), we need to update them
|
||||
if left_attr.value != right_attr.value || left_attr.volatile {
|
||||
@@ -123,8 +142,16 @@ impl<'b> VirtualDom {
|
||||
.dynamic_nodes
|
||||
.iter()
|
||||
.zip(right_template.dynamic_nodes.iter())
|
||||
.for_each(|(left_node, right_node)| {
|
||||
self.diff_dynamic_node(left_node, right_node, right_template);
|
||||
.enumerate()
|
||||
.for_each(|(dyn_node_idx, (left_node, right_node))| {
|
||||
let current_ref = ElementRef {
|
||||
template: right_template.stable_id().unwrap(),
|
||||
path: ElementPath {
|
||||
path: left_template.template.get().node_paths[dyn_node_idx],
|
||||
},
|
||||
scope: self.runtime.scope_stack.borrow().last().copied().unwrap(),
|
||||
};
|
||||
self.diff_dynamic_node(left_node, right_node, current_ref);
|
||||
});
|
||||
|
||||
// Make sure the roots get transferred over while we're here
|
||||
@@ -135,30 +162,24 @@ impl<'b> VirtualDom {
|
||||
right.push(element);
|
||||
}
|
||||
}
|
||||
|
||||
let root_ids = right_template.root_ids.borrow();
|
||||
|
||||
// Update the node refs
|
||||
for i in 0..root_ids.len() {
|
||||
if let Some(root_id) = root_ids.get(i) {
|
||||
self.update_template(*root_id, right_template);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn diff_dynamic_node(
|
||||
&mut self,
|
||||
left_node: &'b DynamicNode<'b>,
|
||||
right_node: &'b DynamicNode<'b>,
|
||||
node: &'b VNode<'b>,
|
||||
parent: ElementRef,
|
||||
) {
|
||||
match (left_node, right_node) {
|
||||
(Text(left), Text(right)) => self.diff_vtext(left, right, node),
|
||||
(Fragment(left), Fragment(right)) => self.diff_non_empty_fragment(left, right),
|
||||
(Placeholder(left), Placeholder(right)) => right.id.set(left.id.get()),
|
||||
(Component(left), Component(right)) => self.diff_vcomponent(left, right, node),
|
||||
(Placeholder(left), Fragment(right)) => self.replace_placeholder(left, *right),
|
||||
(Fragment(left), Placeholder(right)) => self.node_to_placeholder(left, right),
|
||||
(Text(left), Text(right)) => self.diff_vtext(left, right),
|
||||
(Fragment(left), Fragment(right)) => self.diff_non_empty_fragment(left, right, parent),
|
||||
(Placeholder(left), Placeholder(right)) => {
|
||||
right.id.set(left.id.get());
|
||||
right.parent.set(left.parent.get());
|
||||
},
|
||||
(Component(left), Component(right)) => self.diff_vcomponent(left, right, Some(parent)),
|
||||
(Placeholder(left), Fragment(right)) => self.replace_placeholder(left, *right, parent),
|
||||
(Fragment(left), Placeholder(right)) => self.node_to_placeholder(left, right, parent),
|
||||
_ => todo!("This is an usual custom case for dynamic nodes. We don't know how to handle it yet."),
|
||||
};
|
||||
}
|
||||
@@ -179,7 +200,7 @@ impl<'b> VirtualDom {
|
||||
&mut self,
|
||||
left: &'b VComponent<'b>,
|
||||
right: &'b VComponent<'b>,
|
||||
right_template: &'b VNode<'b>,
|
||||
parent: Option<ElementRef>,
|
||||
) {
|
||||
if std::ptr::eq(left, right) {
|
||||
return;
|
||||
@@ -187,7 +208,7 @@ impl<'b> VirtualDom {
|
||||
|
||||
// Replace components that have different render fns
|
||||
if left.render_fn != right.render_fn {
|
||||
return self.replace_vcomponent(right_template, right, left);
|
||||
return self.replace_vcomponent(right, left, parent);
|
||||
}
|
||||
|
||||
// Make sure the new vcomponent has the right scopeid associated to it
|
||||
@@ -228,11 +249,11 @@ impl<'b> VirtualDom {
|
||||
|
||||
fn replace_vcomponent(
|
||||
&mut self,
|
||||
right_template: &'b VNode<'b>,
|
||||
right: &'b VComponent<'b>,
|
||||
left: &'b VComponent<'b>,
|
||||
parent: Option<ElementRef>,
|
||||
) {
|
||||
let m = self.create_component_node(right_template, right);
|
||||
let m = self.create_component_node(parent, right);
|
||||
|
||||
let pre_edits = self.mutations.edits.len();
|
||||
|
||||
@@ -287,11 +308,12 @@ impl<'b> VirtualDom {
|
||||
/// }
|
||||
/// ```
|
||||
fn light_diff_templates(&mut self, left: &'b VNode<'b>, right: &'b VNode<'b>) {
|
||||
let parent = left.parent.take();
|
||||
match matching_components(left, right) {
|
||||
None => self.replace(left, [right]),
|
||||
None => self.replace(left, [right], parent),
|
||||
Some(components) => components
|
||||
.into_iter()
|
||||
.for_each(|(l, r)| self.diff_vcomponent(l, r, right)),
|
||||
.for_each(|(l, r)| self.diff_vcomponent(l, r, parent)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -299,11 +321,8 @@ impl<'b> VirtualDom {
|
||||
///
|
||||
/// This just moves the ID of the old node over to the new node, and then sets the text of the new node if it's
|
||||
/// different.
|
||||
fn diff_vtext(&mut self, left: &'b VText<'b>, right: &'b VText<'b>, node: &'b VNode<'b>) {
|
||||
let id = left
|
||||
.id
|
||||
.get()
|
||||
.unwrap_or_else(|| self.next_element(node, &[0]));
|
||||
fn diff_vtext(&mut self, left: &'b VText<'b>, right: &'b VText<'b>) {
|
||||
let id = left.id.get().unwrap_or_else(|| self.next_element());
|
||||
|
||||
right.id.set(Some(id));
|
||||
if left.value != right.value {
|
||||
@@ -312,7 +331,12 @@ impl<'b> VirtualDom {
|
||||
}
|
||||
}
|
||||
|
||||
fn diff_non_empty_fragment(&mut self, old: &'b [VNode<'b>], new: &'b [VNode<'b>]) {
|
||||
fn diff_non_empty_fragment(
|
||||
&mut self,
|
||||
old: &'b [VNode<'b>],
|
||||
new: &'b [VNode<'b>],
|
||||
parent: ElementRef,
|
||||
) {
|
||||
let new_is_keyed = new[0].key.is_some();
|
||||
let old_is_keyed = old[0].key.is_some();
|
||||
debug_assert!(
|
||||
@@ -325,9 +349,9 @@ impl<'b> VirtualDom {
|
||||
);
|
||||
|
||||
if new_is_keyed && old_is_keyed {
|
||||
self.diff_keyed_children(old, new);
|
||||
self.diff_keyed_children(old, new, parent);
|
||||
} else {
|
||||
self.diff_non_keyed_children(old, new);
|
||||
self.diff_non_keyed_children(old, new, parent);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -339,7 +363,12 @@ impl<'b> VirtualDom {
|
||||
// [... parent]
|
||||
//
|
||||
// the change list stack is in the same state when this function returns.
|
||||
fn diff_non_keyed_children(&mut self, old: &'b [VNode<'b>], new: &'b [VNode<'b>]) {
|
||||
fn diff_non_keyed_children(
|
||||
&mut self,
|
||||
old: &'b [VNode<'b>],
|
||||
new: &'b [VNode<'b>],
|
||||
parent: ElementRef,
|
||||
) {
|
||||
use std::cmp::Ordering;
|
||||
|
||||
// Handled these cases in `diff_children` before calling this function.
|
||||
@@ -348,7 +377,9 @@ impl<'b> VirtualDom {
|
||||
|
||||
match old.len().cmp(&new.len()) {
|
||||
Ordering::Greater => self.remove_nodes(&old[new.len()..]),
|
||||
Ordering::Less => self.create_and_insert_after(&new[old.len()..], old.last().unwrap()),
|
||||
Ordering::Less => {
|
||||
self.create_and_insert_after(&new[old.len()..], old.last().unwrap(), parent)
|
||||
}
|
||||
Ordering::Equal => {}
|
||||
}
|
||||
|
||||
@@ -373,7 +404,12 @@ impl<'b> VirtualDom {
|
||||
// https://github.com/infernojs/inferno/blob/36fd96/packages/inferno/src/DOM/patching.ts#L530-L739
|
||||
//
|
||||
// The stack is empty upon entry.
|
||||
fn diff_keyed_children(&mut self, old: &'b [VNode<'b>], new: &'b [VNode<'b>]) {
|
||||
fn diff_keyed_children(
|
||||
&mut self,
|
||||
old: &'b [VNode<'b>],
|
||||
new: &'b [VNode<'b>],
|
||||
parent: ElementRef,
|
||||
) {
|
||||
if cfg!(debug_assertions) {
|
||||
let mut keys = rustc_hash::FxHashSet::default();
|
||||
let mut assert_unique_keys = |children: &'b [VNode<'b>]| {
|
||||
@@ -401,7 +437,7 @@ impl<'b> VirtualDom {
|
||||
//
|
||||
// `shared_prefix_count` is the count of how many nodes at the start of
|
||||
// `new` and `old` share the same keys.
|
||||
let (left_offset, right_offset) = match self.diff_keyed_ends(old, new) {
|
||||
let (left_offset, right_offset) = match self.diff_keyed_ends(old, new, parent) {
|
||||
Some(count) => count,
|
||||
None => return,
|
||||
};
|
||||
@@ -427,18 +463,18 @@ impl<'b> VirtualDom {
|
||||
if left_offset == 0 {
|
||||
// insert at the beginning of the old list
|
||||
let foothold = &old[old.len() - right_offset];
|
||||
self.create_and_insert_before(new_middle, foothold);
|
||||
self.create_and_insert_before(new_middle, foothold, parent);
|
||||
} else if right_offset == 0 {
|
||||
// insert at the end the old list
|
||||
let foothold = old.last().unwrap();
|
||||
self.create_and_insert_after(new_middle, foothold);
|
||||
self.create_and_insert_after(new_middle, foothold, parent);
|
||||
} else {
|
||||
// inserting in the middle
|
||||
let foothold = &old[left_offset - 1];
|
||||
self.create_and_insert_after(new_middle, foothold);
|
||||
self.create_and_insert_after(new_middle, foothold, parent);
|
||||
}
|
||||
} else {
|
||||
self.diff_keyed_middle(old_middle, new_middle);
|
||||
self.diff_keyed_middle(old_middle, new_middle, parent);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -451,6 +487,7 @@ impl<'b> VirtualDom {
|
||||
&mut self,
|
||||
old: &'b [VNode<'b>],
|
||||
new: &'b [VNode<'b>],
|
||||
parent: ElementRef,
|
||||
) -> Option<(usize, usize)> {
|
||||
let mut left_offset = 0;
|
||||
|
||||
@@ -466,7 +503,7 @@ impl<'b> VirtualDom {
|
||||
// If that was all of the old children, then create and append the remaining
|
||||
// new children and we're finished.
|
||||
if left_offset == old.len() {
|
||||
self.create_and_insert_after(&new[left_offset..], old.last().unwrap());
|
||||
self.create_and_insert_after(&new[left_offset..], old.last().unwrap(), parent);
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -505,7 +542,12 @@ impl<'b> VirtualDom {
|
||||
//
|
||||
// Upon exit from this function, it will be restored to that same self.
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn diff_keyed_middle(&mut self, old: &'b [VNode<'b>], new: &'b [VNode<'b>]) {
|
||||
fn diff_keyed_middle(
|
||||
&mut self,
|
||||
old: &'b [VNode<'b>],
|
||||
new: &'b [VNode<'b>],
|
||||
parent: ElementRef,
|
||||
) {
|
||||
/*
|
||||
1. Map the old keys into a numerical ordering based on indices.
|
||||
2. Create a map of old key to its index
|
||||
@@ -560,9 +602,9 @@ impl<'b> VirtualDom {
|
||||
// If none of the old keys are reused by the new children, then we remove all the remaining old children and
|
||||
// create the new children afresh.
|
||||
if shared_keys.is_empty() {
|
||||
if old.get(0).is_some() {
|
||||
if old.first().is_some() {
|
||||
self.remove_nodes(&old[1..]);
|
||||
self.replace(&old[0], new);
|
||||
self.replace(&old[0], new, Some(parent));
|
||||
} else {
|
||||
// I think this is wrong - why are we appending?
|
||||
// only valid of the if there are no trailing elements
|
||||
@@ -583,8 +625,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()];
|
||||
@@ -740,20 +781,38 @@ impl<'b> VirtualDom {
|
||||
.sum()
|
||||
}
|
||||
|
||||
fn create_children(&mut self, nodes: impl IntoIterator<Item = &'b VNode<'b>>) -> usize {
|
||||
pub(crate) fn create_children(
|
||||
&mut self,
|
||||
nodes: impl IntoIterator<Item = &'b VNode<'b>>,
|
||||
parent: Option<ElementRef>,
|
||||
) -> usize {
|
||||
nodes
|
||||
.into_iter()
|
||||
.fold(0, |acc, child| acc + self.create(child))
|
||||
.map(|child| {
|
||||
self.assign_boundary_ref(parent, child);
|
||||
self.create(child)
|
||||
})
|
||||
.sum()
|
||||
}
|
||||
|
||||
fn create_and_insert_before(&mut self, new: &'b [VNode<'b>], before: &'b VNode<'b>) {
|
||||
let m = self.create_children(new);
|
||||
fn create_and_insert_before(
|
||||
&mut self,
|
||||
new: &'b [VNode<'b>],
|
||||
before: &'b VNode<'b>,
|
||||
parent: ElementRef,
|
||||
) {
|
||||
let m = self.create_children(new, Some(parent));
|
||||
let id = self.find_first_element(before);
|
||||
self.mutations.push(Mutation::InsertBefore { id, m })
|
||||
}
|
||||
|
||||
fn create_and_insert_after(&mut self, new: &'b [VNode<'b>], after: &'b VNode<'b>) {
|
||||
let m = self.create_children(new);
|
||||
fn create_and_insert_after(
|
||||
&mut self,
|
||||
new: &'b [VNode<'b>],
|
||||
after: &'b VNode<'b>,
|
||||
parent: ElementRef,
|
||||
) {
|
||||
let m = self.create_children(new, Some(parent));
|
||||
let id = self.find_last_element(after);
|
||||
self.mutations.push(Mutation::InsertAfter { id, m })
|
||||
}
|
||||
@@ -763,15 +822,21 @@ impl<'b> VirtualDom {
|
||||
&mut self,
|
||||
l: &'b VPlaceholder,
|
||||
r: impl IntoIterator<Item = &'b VNode<'b>>,
|
||||
parent: ElementRef,
|
||||
) {
|
||||
let m = self.create_children(r);
|
||||
let m = self.create_children(r, Some(parent));
|
||||
let id = l.id.get().unwrap();
|
||||
self.mutations.push(Mutation::ReplaceWith { id, m });
|
||||
self.reclaim(id);
|
||||
}
|
||||
|
||||
fn replace(&mut self, left: &'b VNode<'b>, right: impl IntoIterator<Item = &'b VNode<'b>>) {
|
||||
let m = self.create_children(right);
|
||||
fn replace(
|
||||
&mut self,
|
||||
left: &'b VNode<'b>,
|
||||
right: impl IntoIterator<Item = &'b VNode<'b>>,
|
||||
parent: Option<ElementRef>,
|
||||
) {
|
||||
let m = self.create_children(right, parent);
|
||||
|
||||
let pre_edits = self.mutations.edits.len();
|
||||
|
||||
@@ -790,11 +855,12 @@ impl<'b> VirtualDom {
|
||||
};
|
||||
}
|
||||
|
||||
fn node_to_placeholder(&mut self, l: &'b [VNode<'b>], r: &'b VPlaceholder) {
|
||||
fn node_to_placeholder(&mut self, l: &'b [VNode<'b>], r: &'b VPlaceholder, parent: ElementRef) {
|
||||
// Create the placeholder first, ensuring we get a dedicated ID for the placeholder
|
||||
let placeholder = self.next_element(&l[0], &[]);
|
||||
let placeholder = self.next_element();
|
||||
|
||||
r.id.set(Some(placeholder));
|
||||
r.parent.set(Some(parent));
|
||||
|
||||
self.mutations
|
||||
.push(Mutation::CreatePlaceholder { id: placeholder });
|
||||
@@ -832,6 +898,16 @@ impl<'b> VirtualDom {
|
||||
// Clean up the roots, assuming we need to generate mutations for these
|
||||
// This is done last in order to preserve Node ID reclaim order (reclaim in reverse order of claim)
|
||||
self.reclaim_roots(node, gen_muts);
|
||||
|
||||
// Clean up the vnode id
|
||||
self.reclaim_vnode_id(node);
|
||||
}
|
||||
|
||||
fn reclaim_vnode_id(&mut self, node: &'b VNode<'b>) {
|
||||
// Clean up the vnode id
|
||||
if let Some(id) = node.stable_id() {
|
||||
self.element_refs.remove(id.0);
|
||||
}
|
||||
}
|
||||
|
||||
fn reclaim_roots(&mut self, node: &VNode, gen_muts: bool) {
|
||||
@@ -990,6 +1066,13 @@ impl<'b> VirtualDom {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn assign_boundary_ref(&mut self, parent: Option<ElementRef>, child: &'b VNode<'b>) {
|
||||
if let Some(parent) = parent {
|
||||
// assign the parent of the child
|
||||
child.parent.set(Some(parent));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Are the templates the same?
|
||||
|
||||
@@ -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! {
|
||||
|
||||
@@ -107,8 +107,6 @@ impl<T: std::fmt::Debug> std::fmt::Debug for Event<T> {
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
|
||||
/// The callback type generated by the `rsx!` macro when an `on` field is specified for components.
|
||||
///
|
||||
/// This makes it possible to pass `move |evt| {}` style closures into components as property fields.
|
||||
|
||||
@@ -30,7 +30,8 @@ pub fn Fragment<'a>(cx: Scope<'a, FragmentProps<'a>>) -> Element {
|
||||
let children = cx.props.0.as_ref()?;
|
||||
Some(VNode {
|
||||
key: children.key,
|
||||
parent: children.parent,
|
||||
parent: children.parent.clone(),
|
||||
stable_id: children.stable_id.clone(),
|
||||
template: children.template.clone(),
|
||||
root_ids: children.root_ids.clone(),
|
||||
dynamic_nodes: children.dynamic_nodes,
|
||||
|
||||
@@ -18,13 +18,42 @@ use crate::{innerlude::VNode, ScopeState};
|
||||
|
||||
/// A concrete type provider for closures that build [`VNode`] structures.
|
||||
///
|
||||
/// This struct wraps lazy structs that build [`VNode`] trees Normally, we cannot perform a blanket implementation over
|
||||
/// This struct wraps lazy structs that build [`VNode`] trees. Normally, we cannot perform a blanket implementation over
|
||||
/// closures, but if we wrap the closure in a concrete type, we can use it for different branches in matching.
|
||||
///
|
||||
///
|
||||
/// ```rust, ignore
|
||||
/// LazyNodes::new(|f| f.element("div", [], [], [] None))
|
||||
/// LazyNodes::new(|f| {
|
||||
/// static TEMPLATE: dioxus::core::Template = dioxus::core::Template {
|
||||
/// name: "main.rs:5:5:20", // Source location of the template for hot reloading
|
||||
/// roots: &[
|
||||
/// dioxus::core::TemplateNode::Element {
|
||||
/// tag: dioxus_elements::div::TAG_NAME,
|
||||
/// namespace: dioxus_elements::div::NAME_SPACE,
|
||||
/// attrs: &[],
|
||||
/// children: &[],
|
||||
/// },
|
||||
/// ],
|
||||
/// node_paths: &[],
|
||||
/// attr_paths: &[],
|
||||
/// };
|
||||
/// dioxus::core::VNode {
|
||||
/// parent: None,
|
||||
/// key: None,
|
||||
/// template: std::cell::Cell::new(TEMPLATE),
|
||||
/// root_ids: dioxus::core::exports::bumpalo::collections::Vec::with_capacity_in(
|
||||
/// 1usize,
|
||||
/// f.bump(),
|
||||
/// )
|
||||
/// .into(),
|
||||
/// dynamic_nodes: f.bump().alloc([]),
|
||||
/// dynamic_attrs: f.bump().alloc([]),
|
||||
/// })
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Find more information about how to construct [`VNode`] at <https://dioxuslabs.com/learn/0.4/contributing/walkthrough_readme#the-rsx-macro>
|
||||
|
||||
pub struct LazyNodes<'a, 'b> {
|
||||
#[cfg(not(miri))]
|
||||
inner: SmallBox<dyn FnMut(&'a ScopeState) -> VNode<'a> + 'b, S16>,
|
||||
@@ -61,7 +90,7 @@ impl<'a, 'b> LazyNodes<'a, 'b> {
|
||||
/// Call the closure with the given factory to produce real [`VNode`].
|
||||
///
|
||||
/// ```rust, ignore
|
||||
/// let f = LazyNodes::new(move |f| f.element("div", [], [], [] None));
|
||||
/// let f = LazyNodes::new(/* Closure for creating VNodes */);
|
||||
///
|
||||
/// let node = f.call(cac);
|
||||
/// ```
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
#![doc = include_str!("../README.md")]
|
||||
#![doc(html_logo_url = "https://avatars.githubusercontent.com/u/79236386")]
|
||||
#![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")]
|
||||
#![warn(missing_docs)]
|
||||
|
||||
mod any_props;
|
||||
@@ -32,6 +34,7 @@ pub(crate) mod innerlude {
|
||||
pub use crate::nodes::RenderReturn;
|
||||
pub use crate::nodes::*;
|
||||
pub use crate::properties::*;
|
||||
pub use crate::runtime::{Runtime, RuntimeGuard};
|
||||
pub use crate::scheduler::*;
|
||||
pub use crate::scope_context::*;
|
||||
pub use crate::scopes::*;
|
||||
@@ -88,9 +91,9 @@ pub mod prelude {
|
||||
consume_context, consume_context_from_scope, current_scope_id, fc_to_builder, has_context,
|
||||
provide_context, provide_context_to_scope, provide_root_context, push_future,
|
||||
remove_future, schedule_update_any, spawn, spawn_forever, suspend, throw, AnyValue,
|
||||
Component, Element, Event, EventHandler, Fragment, IntoAttributeValue, LazyNodes,
|
||||
Properties, Scope, ScopeId, ScopeState, Scoped, TaskId, Template, TemplateAttribute,
|
||||
TemplateNode, Throw, VNode, VirtualDom,
|
||||
Component, Element, Event, EventHandler, Fragment, IntoAttributeValue, IntoDynNode,
|
||||
LazyNodes, Properties, Runtime, RuntimeGuard, Scope, ScopeId, ScopeState, Scoped, TaskId,
|
||||
Template, TemplateAttribute, TemplateNode, Throw, VNode, VirtualDom,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ pub struct Mutations<'a> {
|
||||
/// Any templates encountered while diffing the DOM.
|
||||
///
|
||||
/// These must be loaded into a cache before applying the edits
|
||||
pub templates: Vec<Template<'a>>,
|
||||
pub templates: Vec<Template<'static>>,
|
||||
|
||||
/// Any mutations required to patch the renderer to match the layout of the VirtualDom
|
||||
pub edits: Vec<Mutation<'a>>,
|
||||
@@ -91,7 +91,7 @@ pub enum Mutation<'a> {
|
||||
id: ElementId,
|
||||
},
|
||||
|
||||
/// Create an placeholder int he DOM that we will use later.
|
||||
/// Create a placeholder in the DOM that we will use later.
|
||||
///
|
||||
/// Dioxus currently requires the use of placeholders to maintain a re-entrance point for things like list diffing
|
||||
CreatePlaceholder {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::innerlude::{ElementRef, VNodeId};
|
||||
use crate::{
|
||||
any_props::AnyProps, arena::ElementId, Element, Event, LazyNodes, ScopeId, ScopeState,
|
||||
};
|
||||
@@ -47,7 +48,10 @@ pub struct VNode<'a> {
|
||||
pub key: Option<&'a str>,
|
||||
|
||||
/// When rendered, this template will be linked to its parent manually
|
||||
pub parent: Option<ElementId>,
|
||||
pub(crate) parent: Cell<Option<ElementRef>>,
|
||||
|
||||
/// The bubble id assigned to the child that we need to update and drop when diffing happens
|
||||
pub(crate) stable_id: Cell<Option<VNodeId>>,
|
||||
|
||||
/// The static nodes and static descriptor of the template
|
||||
pub template: Cell<Template<'static>>,
|
||||
@@ -68,7 +72,8 @@ impl<'a> VNode<'a> {
|
||||
pub fn empty(cx: &'a ScopeState) -> Element<'a> {
|
||||
Some(VNode {
|
||||
key: None,
|
||||
parent: None,
|
||||
parent: Default::default(),
|
||||
stable_id: Default::default(),
|
||||
root_ids: RefCell::new(bumpalo::collections::Vec::new_in(cx.bump())),
|
||||
dynamic_nodes: &[],
|
||||
dynamic_attrs: &[],
|
||||
@@ -81,6 +86,30 @@ impl<'a> VNode<'a> {
|
||||
})
|
||||
}
|
||||
|
||||
/// Create a new VNode
|
||||
pub fn new(
|
||||
key: Option<&'a str>,
|
||||
template: Template<'static>,
|
||||
root_ids: bumpalo::collections::Vec<'a, ElementId>,
|
||||
dynamic_nodes: &'a [DynamicNode<'a>],
|
||||
dynamic_attrs: &'a [Attribute<'a>],
|
||||
) -> Self {
|
||||
Self {
|
||||
key,
|
||||
parent: Cell::new(None),
|
||||
stable_id: Cell::new(None),
|
||||
template: Cell::new(template),
|
||||
root_ids: RefCell::new(root_ids),
|
||||
dynamic_nodes,
|
||||
dynamic_attrs,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the stable id of this node used for bubbling events
|
||||
pub(crate) fn stable_id(&self) -> Option<VNodeId> {
|
||||
self.stable_id.get()
|
||||
}
|
||||
|
||||
/// Load a dynamic root at the given index
|
||||
///
|
||||
/// Returns [`None`] if the root is actually a static node (Element/Text)
|
||||
@@ -319,7 +348,7 @@ pub struct VComponent<'a> {
|
||||
|
||||
/// 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
|
||||
/// It is possible that components get folded at compile time, so these shouldn't be really used as a key
|
||||
pub(crate) render_fn: *const (),
|
||||
|
||||
pub(crate) props: RefCell<Option<Box<dyn AnyProps<'a> + 'a>>>,
|
||||
@@ -372,6 +401,8 @@ impl<'a> VText<'a> {
|
||||
pub struct VPlaceholder {
|
||||
/// The ID of this node in the real DOM
|
||||
pub(crate) id: Cell<Option<ElementId>>,
|
||||
/// The parent of this node
|
||||
pub(crate) parent: Cell<Option<ElementRef>>,
|
||||
}
|
||||
|
||||
impl VPlaceholder {
|
||||
@@ -707,7 +738,7 @@ impl<'a, 'b> IntoDynNode<'b> for &'a str {
|
||||
impl IntoDynNode<'_> for String {
|
||||
fn into_vnode(self, cx: &ScopeState) -> DynamicNode {
|
||||
DynamicNode::Text(VText {
|
||||
value: cx.bump().alloc(self),
|
||||
value: cx.bump().alloc_str(&self),
|
||||
id: Default::default(),
|
||||
})
|
||||
}
|
||||
@@ -722,7 +753,8 @@ impl<'b> IntoDynNode<'b> for Arguments<'_> {
|
||||
impl<'a> IntoDynNode<'a> for &'a VNode<'a> {
|
||||
fn into_vnode(self, _cx: &'a ScopeState) -> DynamicNode<'a> {
|
||||
DynamicNode::Fragment(_cx.bump().alloc([VNode {
|
||||
parent: self.parent,
|
||||
parent: self.parent.clone(),
|
||||
stable_id: self.stable_id.clone(),
|
||||
template: self.template.clone(),
|
||||
root_ids: self.root_ids.clone(),
|
||||
key: self.key,
|
||||
@@ -792,8 +824,8 @@ impl<'a> IntoAttributeValue<'a> for &'a str {
|
||||
}
|
||||
|
||||
impl<'a> IntoAttributeValue<'a> for String {
|
||||
fn into_value(self, bump: &'a Bump) -> AttributeValue<'a> {
|
||||
AttributeValue::Text(bump.alloc(self))
|
||||
fn into_value(self, cx: &'a Bump) -> AttributeValue<'a> {
|
||||
AttributeValue::Text(cx.alloc_str(&self))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -41,7 +41,8 @@ where
|
||||
.flatten()
|
||||
}
|
||||
|
||||
pub(crate) struct Runtime {
|
||||
/// A global runtime that is shared across all scopes that provides the async runtime and context API
|
||||
pub struct Runtime {
|
||||
pub(crate) scope_contexts: RefCell<Vec<Option<ScopeContext>>>,
|
||||
pub(crate) scheduler: Rc<Scheduler>,
|
||||
|
||||
@@ -63,6 +64,11 @@ impl Runtime {
|
||||
})
|
||||
}
|
||||
|
||||
/// Get the current runtime
|
||||
pub fn current() -> Option<Rc<Self>> {
|
||||
RUNTIMES.with(|stack| stack.borrow().last().cloned())
|
||||
}
|
||||
|
||||
/// Create a scope context. This slab is synchronized with the scope slab.
|
||||
pub(crate) fn create_context_at(&self, id: ScopeId, context: ScopeContext) {
|
||||
let mut contexts = self.scope_contexts.borrow_mut();
|
||||
@@ -77,14 +83,14 @@ impl Runtime {
|
||||
}
|
||||
|
||||
/// Get the current scope id
|
||||
pub fn current_scope_id(&self) -> Option<ScopeId> {
|
||||
pub(crate) fn current_scope_id(&self) -> Option<ScopeId> {
|
||||
self.scope_stack.borrow().last().copied()
|
||||
}
|
||||
|
||||
/// Get the context for any scope given its ID
|
||||
///
|
||||
/// This is useful for inserting or removing contexts from a scope, or rendering out its root node
|
||||
pub fn get_context(&self, id: ScopeId) -> Option<Ref<'_, ScopeContext>> {
|
||||
pub(crate) fn get_context(&self, id: ScopeId) -> Option<Ref<'_, ScopeContext>> {
|
||||
Ref::filter_map(self.scope_contexts.borrow(), |contexts| {
|
||||
contexts.get(id.0).and_then(|f| f.as_ref())
|
||||
})
|
||||
@@ -92,10 +98,42 @@ impl Runtime {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct RuntimeGuard(Rc<Runtime>);
|
||||
/// A gaurd for a new runtime. This must be used to override the current runtime when importing components from a dynamic library that has it's own runtime.
|
||||
///
|
||||
/// ```rust
|
||||
/// use dioxus::prelude::*;
|
||||
///
|
||||
/// fn main() {
|
||||
/// let virtual_dom = VirtualDom::new(app);
|
||||
/// }
|
||||
///
|
||||
/// fn app(cx: Scope) -> Element {
|
||||
/// render!{ Component { runtime: Runtime::current().unwrap() } }
|
||||
/// }
|
||||
///
|
||||
/// // In a dynamic library
|
||||
/// #[derive(Props)]
|
||||
/// struct ComponentProps {
|
||||
/// runtime: std::rc::Rc<Runtime>,
|
||||
/// }
|
||||
///
|
||||
/// impl PartialEq for ComponentProps {
|
||||
/// fn eq(&self, _other: &Self) -> bool {
|
||||
/// true
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// fn Component(cx: Scope<ComponentProps>) -> Element {
|
||||
/// cx.use_hook(|| RuntimeGuard::new(cx.props.runtime.clone()));
|
||||
///
|
||||
/// render! { div {} }
|
||||
/// }
|
||||
/// ```
|
||||
pub struct RuntimeGuard(Rc<Runtime>);
|
||||
|
||||
impl RuntimeGuard {
|
||||
pub(crate) fn new(runtime: Rc<Runtime>) -> Self {
|
||||
/// Create a new runtime guard that sets the current Dioxus runtime. The runtime will be reset when the guard is dropped
|
||||
pub fn new(runtime: Rc<Runtime>) -> Self {
|
||||
push_runtime(runtime.clone());
|
||||
Self(runtime)
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ pub struct TaskId(pub usize);
|
||||
/// the task itself is the waker
|
||||
pub(crate) struct LocalTask {
|
||||
pub scope: ScopeId,
|
||||
pub(super) task: RefCell<Pin<Box<dyn Future<Output = ()> + 'static>>>,
|
||||
pub task: RefCell<Pin<Box<dyn Future<Output = ()> + 'static>>>,
|
||||
pub waker: Waker,
|
||||
}
|
||||
|
||||
@@ -48,11 +48,15 @@ impl Scheduler {
|
||||
})),
|
||||
};
|
||||
|
||||
entry.insert(task);
|
||||
let mut cx = std::task::Context::from_waker(&task.waker);
|
||||
|
||||
self.sender
|
||||
.unbounded_send(SchedulerMsg::TaskNotified(task_id))
|
||||
.expect("Scheduler should exist");
|
||||
if !task.task.borrow_mut().as_mut().poll(&mut cx).is_ready() {
|
||||
self.sender
|
||||
.unbounded_send(SchedulerMsg::TaskNotified(task_id))
|
||||
.expect("Scheduler should exist");
|
||||
}
|
||||
|
||||
entry.insert(task);
|
||||
|
||||
task_id
|
||||
}
|
||||
@@ -60,8 +64,8 @@ impl Scheduler {
|
||||
/// Drop the future with the given TaskId
|
||||
///
|
||||
/// This does not abort the task, so you'll want to wrap it in an aborthandle if that's important to you
|
||||
pub fn remove(&self, id: TaskId) {
|
||||
self.tasks.borrow_mut().try_remove(id.0);
|
||||
pub fn remove(&self, id: TaskId) -> Option<LocalTask> {
|
||||
self.tasks.borrow_mut().try_remove(id.0)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -35,7 +35,8 @@ impl VirtualDom {
|
||||
hook_idx: Default::default(),
|
||||
|
||||
borrowed_props: Default::default(),
|
||||
attributes_to_drop: Default::default(),
|
||||
attributes_to_drop_before_render: Default::default(),
|
||||
element_refs_to_drop: Default::default(),
|
||||
}));
|
||||
|
||||
let context =
|
||||
@@ -54,7 +55,7 @@ impl VirtualDom {
|
||||
|
||||
let new_nodes = unsafe {
|
||||
let scope = &self.scopes[scope_id.0];
|
||||
scope.previous_frame().bump_mut().reset();
|
||||
scope.previous_frame().reset();
|
||||
|
||||
scope.context().suspended.set(false);
|
||||
|
||||
|
||||
@@ -128,7 +128,7 @@ impl ScopeContext {
|
||||
parent.name
|
||||
);
|
||||
if let Some(shared) = parent.shared_contexts.borrow().iter().find_map(|any| {
|
||||
tracing::trace!("found context {:?}", any.type_id());
|
||||
tracing::trace!("found context {:?}", (**any).type_id());
|
||||
any.downcast_ref::<T>()
|
||||
}) {
|
||||
return Some(shared.clone());
|
||||
@@ -230,17 +230,7 @@ impl ScopeContext {
|
||||
/// 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
|
||||
self.tasks.spawn(ScopeId::ROOT, fut)
|
||||
}
|
||||
|
||||
/// Informs the scheduler that this task is no longer needed and should be removed.
|
||||
|
||||
@@ -3,7 +3,7 @@ use crate::{
|
||||
any_props::VProps,
|
||||
bump_frame::BumpFrame,
|
||||
innerlude::ErrorBoundary,
|
||||
innerlude::{DynamicNode, EventHandler, VComponent, VText},
|
||||
innerlude::{DynamicNode, EventHandler, VComponent, VNodeId, VText},
|
||||
lazynodes::LazyNodes,
|
||||
nodes::{IntoAttributeValue, IntoDynNode, RenderReturn},
|
||||
runtime::Runtime,
|
||||
@@ -94,7 +94,8 @@ pub struct ScopeState {
|
||||
pub(crate) hook_idx: Cell<usize>,
|
||||
|
||||
pub(crate) borrowed_props: RefCell<Vec<*const VComponent<'static>>>,
|
||||
pub(crate) attributes_to_drop: RefCell<Vec<*const Attribute<'static>>>,
|
||||
pub(crate) element_refs_to_drop: RefCell<Vec<VNodeId>>,
|
||||
pub(crate) attributes_to_drop_before_render: RefCell<Vec<*const Attribute<'static>>>,
|
||||
|
||||
pub(crate) props: Option<Box<dyn AnyProps<'static>>>,
|
||||
}
|
||||
@@ -348,25 +349,36 @@ impl<'src> ScopeState {
|
||||
pub fn render(&'src self, rsx: LazyNodes<'src, '_>) -> Element<'src> {
|
||||
let element = rsx.call(self);
|
||||
|
||||
let mut listeners = self.attributes_to_drop.borrow_mut();
|
||||
let mut listeners = self.attributes_to_drop_before_render.borrow_mut();
|
||||
for attr in element.dynamic_attrs {
|
||||
match attr.value {
|
||||
AttributeValue::Any(_) | AttributeValue::Listener(_) => {
|
||||
// We need to drop listeners before the next render because they may borrow data from the borrowed props which will be dropped
|
||||
AttributeValue::Listener(_) => {
|
||||
let unbounded = unsafe { std::mem::transmute(attr as *const Attribute) };
|
||||
listeners.push(unbounded);
|
||||
}
|
||||
// We need to drop any values manually to make sure that their drop implementation is called before the next render
|
||||
AttributeValue::Any(_) => {
|
||||
let unbounded = unsafe { std::mem::transmute(attr as *const Attribute) };
|
||||
self.previous_frame().add_attribute_to_drop(unbounded);
|
||||
}
|
||||
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
let mut props = self.borrowed_props.borrow_mut();
|
||||
let mut drop_props = self
|
||||
.previous_frame()
|
||||
.props_to_drop_before_reset
|
||||
.borrow_mut();
|
||||
for node in element.dynamic_nodes {
|
||||
if let DynamicNode::Component(comp) = node {
|
||||
let unbounded = unsafe { std::mem::transmute(comp as *const VComponent) };
|
||||
if !comp.static_props {
|
||||
let unbounded = unsafe { std::mem::transmute(comp as *const VComponent) };
|
||||
props.push(unbounded);
|
||||
}
|
||||
drop_props.push(unbounded);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -455,7 +467,7 @@ impl<'src> ScopeState {
|
||||
render_fn: component as *const (),
|
||||
static_props: P::IS_STATIC,
|
||||
props: RefCell::new(Some(extended)),
|
||||
scope: Cell::new(None),
|
||||
scope: Default::default(),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -4,19 +4,19 @@
|
||||
|
||||
use crate::{
|
||||
any_props::VProps,
|
||||
arena::{ElementId, ElementRef},
|
||||
innerlude::{DirtyScope, ErrorBoundary, Mutations, Scheduler, SchedulerMsg},
|
||||
arena::ElementId,
|
||||
innerlude::{DirtyScope, ElementRef, ErrorBoundary, Mutations, Scheduler, SchedulerMsg},
|
||||
mutations::Mutation,
|
||||
nodes::RenderReturn,
|
||||
nodes::{Template, TemplateId},
|
||||
runtime::{Runtime, RuntimeGuard},
|
||||
scopes::{ScopeId, ScopeState},
|
||||
AttributeValue, Element, Event, Scope,
|
||||
AttributeValue, Element, Event, Scope, VNode,
|
||||
};
|
||||
use futures_util::{pin_mut, StreamExt};
|
||||
use rustc_hash::{FxHashMap, FxHashSet};
|
||||
use slab::Slab;
|
||||
use std::{any::Any, cell::Cell, collections::BTreeSet, future::Future, rc::Rc};
|
||||
use std::{any::Any, cell::Cell, collections::BTreeSet, future::Future, ptr::NonNull, rc::Rc};
|
||||
|
||||
/// A virtual node system that progresses user events and diffs UI trees.
|
||||
///
|
||||
@@ -52,6 +52,7 @@ use std::{any::Any, cell::Cell, collections::BTreeSet, future::Future, rc::Rc};
|
||||
///
|
||||
/// static ROUTES: &str = "";
|
||||
///
|
||||
/// #[component]
|
||||
/// fn App(cx: Scope<AppProps>) -> Element {
|
||||
/// cx.render(rsx!(
|
||||
/// NavBar { routes: ROUTES }
|
||||
@@ -60,18 +61,19 @@ use std::{any::Any, cell::Cell, collections::BTreeSet, future::Future, rc::Rc};
|
||||
/// ))
|
||||
/// }
|
||||
///
|
||||
/// #[inline_props]
|
||||
/// #[component]
|
||||
/// fn NavBar(cx: Scope, routes: &'static str) -> Element {
|
||||
/// cx.render(rsx! {
|
||||
/// div { "Routes: {routes}" }
|
||||
/// })
|
||||
/// }
|
||||
///
|
||||
/// #[component]
|
||||
/// fn Footer(cx: Scope) -> Element {
|
||||
/// cx.render(rsx! { div { "Footer" } })
|
||||
/// }
|
||||
///
|
||||
/// #[inline_props]
|
||||
/// #[component]
|
||||
/// fn Title<'a>(cx: Scope<'a>, children: Element<'a>) -> Element {
|
||||
/// cx.render(rsx! {
|
||||
/// div { id: "title", children }
|
||||
@@ -122,13 +124,14 @@ use std::{any::Any, cell::Cell, collections::BTreeSet, future::Future, rc::Rc};
|
||||
///
|
||||
/// Putting everything together, you can build an event loop around Dioxus by using the methods outlined above.
|
||||
/// ```rust, ignore
|
||||
/// fn app(cx: Scope) -> Element {
|
||||
/// #[component]
|
||||
/// fn App(cx: Scope) -> Element {
|
||||
/// cx.render(rsx! {
|
||||
/// div { "Hello World" }
|
||||
/// })
|
||||
/// }
|
||||
///
|
||||
/// let dom = VirtualDom::new(app);
|
||||
/// let dom = VirtualDom::new(App);
|
||||
///
|
||||
/// real_dom.apply(dom.rebuild());
|
||||
///
|
||||
@@ -183,7 +186,10 @@ pub struct VirtualDom {
|
||||
pub(crate) templates: FxHashMap<TemplateId, FxHashMap<usize, Template<'static>>>,
|
||||
|
||||
// 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>,
|
||||
pub(crate) element_refs: Slab<Option<NonNull<VNode<'static>>>>,
|
||||
|
||||
// The element ids that are used in the renderer
|
||||
pub(crate) elements: Slab<Option<ElementRef>>,
|
||||
|
||||
pub(crate) mutations: Mutations<'static>,
|
||||
|
||||
@@ -260,6 +266,7 @@ impl VirtualDom {
|
||||
dirty_scopes: Default::default(),
|
||||
templates: Default::default(),
|
||||
elements: Default::default(),
|
||||
element_refs: Default::default(),
|
||||
mutations: Mutations::default(),
|
||||
suspended_scopes: Default::default(),
|
||||
};
|
||||
@@ -273,7 +280,7 @@ impl VirtualDom {
|
||||
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());
|
||||
dom.elements.insert(None);
|
||||
|
||||
dom
|
||||
}
|
||||
@@ -311,9 +318,9 @@ impl VirtualDom {
|
||||
}
|
||||
}
|
||||
|
||||
/// Call a listener inside the VirtualDom with data from outside the VirtualDom.
|
||||
/// Call a listener inside the VirtualDom with data from outside the VirtualDom. **The ElementId passed in must be the id of an dynamic element, not a static node or a text node.**
|
||||
///
|
||||
/// This method will identify the appropriate element. The data must match up with the listener delcared. Note that
|
||||
/// This method will identify the appropriate element. The data must match up with the listener declared. Note that
|
||||
/// this method does not give any indication as to the success of the listener call. If the listener is not found,
|
||||
/// nothing will happen.
|
||||
///
|
||||
@@ -350,7 +357,15 @@ impl VirtualDom {
|
||||
| | | <-- no, broke early
|
||||
| <-- no, broke early
|
||||
*/
|
||||
let mut parent_path = self.elements.get(element.0);
|
||||
let parent_path = match self.elements.get(element.0) {
|
||||
Some(Some(el)) => el,
|
||||
_ => return,
|
||||
};
|
||||
let mut parent_node = self
|
||||
.element_refs
|
||||
.get(parent_path.template.0)
|
||||
.cloned()
|
||||
.map(|el| (*parent_path, el));
|
||||
let mut listeners = vec![];
|
||||
|
||||
// We will clone this later. The data itself is wrapped in RC to be used in callbacks if required
|
||||
@@ -362,36 +377,72 @@ impl VirtualDom {
|
||||
// If the event bubbles, we traverse through the tree until we find the target element.
|
||||
if bubbles {
|
||||
// Loop through each dynamic attribute (in a depth first order) in this template before moving up to the template's parent.
|
||||
while let Some(el_ref) = parent_path {
|
||||
while let Some((path, el_ref)) = parent_node {
|
||||
// safety: we maintain references of all vnodes in the element slab
|
||||
if let Some(template) = el_ref.template {
|
||||
let template = unsafe { template.as_ref() };
|
||||
let node_template = template.template.get();
|
||||
let target_path = el_ref.path;
|
||||
let template = unsafe { el_ref.unwrap().as_ref() };
|
||||
let node_template = template.template.get();
|
||||
let target_path = path.path;
|
||||
|
||||
for (idx, attr) in template.dynamic_attrs.iter().enumerate() {
|
||||
let this_path = node_template.attr_paths[idx];
|
||||
for (idx, attr) in template.dynamic_attrs.iter().enumerate() {
|
||||
let this_path = node_template.attr_paths[idx];
|
||||
|
||||
// Remove the "on" prefix if it exists, TODO, we should remove this and settle on one
|
||||
if attr.name.trim_start_matches("on") == name
|
||||
&& target_path.is_decendant(&this_path)
|
||||
{
|
||||
listeners.push(&attr.value);
|
||||
// Remove the "on" prefix if it exists, TODO, we should remove this and settle on one
|
||||
if attr.name.trim_start_matches("on") == name
|
||||
&& target_path.is_decendant(&this_path)
|
||||
{
|
||||
listeners.push(&attr.value);
|
||||
|
||||
// Break if this is the exact target element.
|
||||
// This means we won't call two listeners with the same name on the same element. This should be
|
||||
// documented, or be rejected from the rsx! macro outright
|
||||
if target_path == this_path {
|
||||
break;
|
||||
}
|
||||
// Break if this is the exact target element.
|
||||
// This means we won't call two listeners with the same name on the same element. This should be
|
||||
// documented, or be rejected from the rsx! macro outright
|
||||
if target_path == this_path {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Now that we've accumulated all the parent attributes for the target element, call them in reverse order
|
||||
// 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;
|
||||
// Now that we've accumulated all the parent attributes for the target element, call them in reverse order
|
||||
// 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 = path.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
parent_node = template.parent.get().and_then(|element_ref| {
|
||||
self.element_refs
|
||||
.get(element_ref.template.0)
|
||||
.cloned()
|
||||
.map(|el| (element_ref, el))
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Otherwise, we just call the listener on the target element
|
||||
if let Some((path, el_ref)) = parent_node {
|
||||
// safety: we maintain references of all vnodes in the element slab
|
||||
let template = unsafe { el_ref.unwrap().as_ref() };
|
||||
let node_template = template.template.get();
|
||||
let target_path = path.path;
|
||||
|
||||
for (idx, attr) in template.dynamic_attrs.iter().enumerate() {
|
||||
let this_path = node_template.attr_paths[idx];
|
||||
|
||||
// Remove the "on" prefix if it exists, TODO, we should remove this and settle on one
|
||||
// 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 = path.scope;
|
||||
self.runtime.scope_stack.borrow_mut().push(origin);
|
||||
self.runtime.rendering.set(false);
|
||||
if let Some(cb) = listener.borrow_mut().as_deref_mut() {
|
||||
@@ -400,44 +451,7 @@ impl VirtualDom {
|
||||
self.runtime.scope_stack.borrow_mut().pop();
|
||||
self.runtime.rendering.set(true);
|
||||
|
||||
if !uievent.propagates.get() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
parent_path = template.parent.and_then(|id| self.elements.get(id.0));
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Otherwise, we just call the listener on the target element
|
||||
if let Some(el_ref) = parent_path {
|
||||
// safety: we maintain references of all vnodes in the element slab
|
||||
if let Some(template) = el_ref.template {
|
||||
let template = unsafe { template.as_ref() };
|
||||
let node_template = template.template.get();
|
||||
let target_path = el_ref.path;
|
||||
|
||||
for (idx, attr) in template.dynamic_attrs.iter().enumerate() {
|
||||
let this_path = node_template.attr_paths[idx];
|
||||
|
||||
// Remove the "on" prefix if it exists, TODO, we should remove this and settle on one
|
||||
// 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;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -560,7 +574,7 @@ impl VirtualDom {
|
||||
// If an error occurs, we should try to render the default error component and context where the error occured
|
||||
RenderReturn::Aborted(placeholder) => {
|
||||
tracing::debug!("Ran into suspended or aborted scope during rebuild");
|
||||
let id = self.next_null();
|
||||
let id = self.next_element();
|
||||
placeholder.id.set(Some(id));
|
||||
self.mutations.push(Mutation::CreatePlaceholder { id });
|
||||
}
|
||||
@@ -592,15 +606,12 @@ impl VirtualDom {
|
||||
/// The mutations will be thrown out, so it's best to use this method for things like SSR that have async content
|
||||
pub async fn wait_for_suspense(&mut self) {
|
||||
loop {
|
||||
// println!("waiting for suspense {:?}", self.suspended_scopes);
|
||||
if self.suspended_scopes.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
// println!("waiting for suspense");
|
||||
self.wait_for_work().await;
|
||||
|
||||
// println!("Rendered immediately");
|
||||
_ = self.render_immediate();
|
||||
}
|
||||
}
|
||||
@@ -659,6 +670,11 @@ impl VirtualDom {
|
||||
fn finalize(&mut self) -> Mutations {
|
||||
std::mem::take(&mut self.mutations)
|
||||
}
|
||||
|
||||
/// Get the current runtime
|
||||
pub fn runtime(&self) -> Rc<Runtime> {
|
||||
self.runtime.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for VirtualDom {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_core::ElementId;
|
||||
use std::{rc::Rc, sync::Mutex};
|
||||
|
||||
static CLICKS: Mutex<usize> = Mutex::new(0);
|
||||
|
||||
#[test]
|
||||
fn events_propagate() {
|
||||
let mut dom = VirtualDom::new(app);
|
||||
_ = dom.rebuild();
|
||||
|
||||
// Top-level click is registered
|
||||
dom.handle_event("click", Rc::new(MouseData::default()), ElementId(1), true);
|
||||
assert_eq!(*CLICKS.lock().unwrap(), 1);
|
||||
|
||||
// break reference....
|
||||
for _ in 0..5 {
|
||||
dom.mark_dirty(ScopeId(0));
|
||||
_ = dom.render_immediate();
|
||||
}
|
||||
|
||||
// Lower click is registered
|
||||
dom.handle_event("click", Rc::new(MouseData::default()), ElementId(2), true);
|
||||
assert_eq!(*CLICKS.lock().unwrap(), 3);
|
||||
|
||||
// break reference....
|
||||
for _ in 0..5 {
|
||||
dom.mark_dirty(ScopeId(0));
|
||||
_ = dom.render_immediate();
|
||||
}
|
||||
|
||||
// Stop propagation occurs
|
||||
dom.handle_event("click", Rc::new(MouseData::default()), ElementId(2), true);
|
||||
assert_eq!(*CLICKS.lock().unwrap(), 3);
|
||||
}
|
||||
|
||||
fn app(cx: Scope) -> Element {
|
||||
render! {
|
||||
div {
|
||||
onclick: move |_| {
|
||||
println!("top clicked");
|
||||
*CLICKS.lock().unwrap() += 1;
|
||||
},
|
||||
|
||||
vec![
|
||||
render! {
|
||||
problematic_child {}
|
||||
}
|
||||
].into_iter()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn problematic_child(cx: Scope) -> Element {
|
||||
render! {
|
||||
button {
|
||||
onclick: move |evt| {
|
||||
println!("bottom clicked");
|
||||
let mut clicks = CLICKS.lock().unwrap();
|
||||
|
||||
if *clicks == 3 {
|
||||
evt.stop_propagation();
|
||||
} else {
|
||||
*clicks += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
use dioxus::prelude::Props;
|
||||
use dioxus_core::*;
|
||||
use std::{cell::Cell, collections::HashSet};
|
||||
use std::{cfg, collections::HashSet};
|
||||
|
||||
fn random_ns() -> Option<&'static str> {
|
||||
let namespace = rand::random::<u8>() % 2;
|
||||
@@ -170,22 +170,23 @@ fn create_random_dynamic_node(cx: &ScopeState, depth: usize) -> DynamicNode {
|
||||
let range = if depth > 5 { 1 } else { 4 };
|
||||
match rand::random::<u8>() % range {
|
||||
0 => DynamicNode::Placeholder(Default::default()),
|
||||
1 => cx.make_node((0..(rand::random::<u8>() % 5)).map(|_| VNode {
|
||||
key: None,
|
||||
parent: Default::default(),
|
||||
template: Cell::new(Template {
|
||||
name: concat!(file!(), ":", line!(), ":", column!(), ":0"),
|
||||
roots: &[TemplateNode::Dynamic { id: 0 }],
|
||||
node_paths: &[&[0]],
|
||||
attr_paths: &[],
|
||||
}),
|
||||
root_ids: bumpalo::collections::Vec::new_in(cx.bump()).into(),
|
||||
dynamic_nodes: cx.bump().alloc([cx.component(
|
||||
create_random_element,
|
||||
DepthProps { depth, root: false },
|
||||
"create_random_element",
|
||||
)]),
|
||||
dynamic_attrs: &[],
|
||||
1 => cx.make_node((0..(rand::random::<u8>() % 5)).map(|_| {
|
||||
VNode::new(
|
||||
None,
|
||||
Template {
|
||||
name: concat!(file!(), ":", line!(), ":", column!(), ":0"),
|
||||
roots: &[TemplateNode::Dynamic { id: 0 }],
|
||||
node_paths: &[&[0]],
|
||||
attr_paths: &[],
|
||||
},
|
||||
bumpalo::collections::Vec::new_in(cx.bump()),
|
||||
cx.bump().alloc([cx.component(
|
||||
create_random_element,
|
||||
DepthProps { depth, root: false },
|
||||
"create_random_element",
|
||||
)]),
|
||||
&[],
|
||||
)
|
||||
})),
|
||||
2 => cx.component(
|
||||
create_random_element,
|
||||
@@ -271,13 +272,11 @@ fn create_random_element(cx: Scope<DepthProps>) -> Element {
|
||||
)
|
||||
.into_boxed_str(),
|
||||
));
|
||||
// println!("{template:#?}");
|
||||
let node = VNode {
|
||||
key: None,
|
||||
parent: None,
|
||||
template: Cell::new(template),
|
||||
root_ids: bumpalo::collections::Vec::new_in(cx.bump()).into(),
|
||||
dynamic_nodes: {
|
||||
let node = VNode::new(
|
||||
None,
|
||||
template,
|
||||
bumpalo::collections::Vec::new_in(cx.bump()),
|
||||
{
|
||||
let dynamic_nodes: Vec<_> = dynamic_node_types
|
||||
.iter()
|
||||
.map(|ty| match ty {
|
||||
@@ -291,12 +290,12 @@ fn create_random_element(cx: Scope<DepthProps>) -> Element {
|
||||
.collect();
|
||||
cx.bump().alloc(dynamic_nodes)
|
||||
},
|
||||
dynamic_attrs: cx.bump().alloc(
|
||||
cx.bump().alloc(
|
||||
(0..template.attr_paths.len())
|
||||
.map(|_| create_random_dynamic_attr(cx))
|
||||
.collect::<Vec<_>>(),
|
||||
),
|
||||
};
|
||||
);
|
||||
Some(node)
|
||||
}
|
||||
_ => None,
|
||||
@@ -306,10 +305,10 @@ fn create_random_element(cx: Scope<DepthProps>) -> Element {
|
||||
}
|
||||
|
||||
// test for panics when creating random nodes and templates
|
||||
#[cfg(not(miri))]
|
||||
#[test]
|
||||
fn create() {
|
||||
for _ in 0..1000 {
|
||||
let repeat_count = if cfg!(miri) { 100 } else { 1000 };
|
||||
for _ in 0..repeat_count {
|
||||
let mut vdom =
|
||||
VirtualDom::new_with_props(create_random_element, DepthProps { depth: 0, root: true });
|
||||
let _ = vdom.rebuild();
|
||||
@@ -318,10 +317,10 @@ fn create() {
|
||||
|
||||
// test for panics when diffing random nodes
|
||||
// This test will change the template every render which is not very realistic, but it helps stress the system
|
||||
#[cfg(not(miri))]
|
||||
#[test]
|
||||
fn diff() {
|
||||
for _ in 0..100000 {
|
||||
let repeat_count = if cfg!(miri) { 100 } else { 1000 };
|
||||
for _ in 0..repeat_count {
|
||||
let mut vdom =
|
||||
VirtualDom::new_with_props(create_random_element, DepthProps { depth: 0, root: true });
|
||||
let _ = vdom.rebuild();
|
||||
|
||||
@@ -4,7 +4,7 @@ use std::rc::Rc;
|
||||
|
||||
#[test]
|
||||
fn miri_rollover() {
|
||||
let mut dom = VirtualDom::new(app);
|
||||
let mut dom = VirtualDom::new(App);
|
||||
|
||||
_ = dom.rebuild();
|
||||
|
||||
@@ -15,7 +15,8 @@ fn miri_rollover() {
|
||||
}
|
||||
}
|
||||
|
||||
fn app(cx: Scope) -> Element {
|
||||
#[component]
|
||||
fn App(cx: Scope) -> Element {
|
||||
let mut idx = use_state(cx, || 0);
|
||||
let onhover = |_| println!("go!");
|
||||
|
||||
@@ -31,15 +32,15 @@ fn app(cx: Scope) -> Element {
|
||||
button { onclick: move |_| idx -= 1, "-" }
|
||||
ul {
|
||||
(0..**idx).map(|i| rsx! {
|
||||
child_example { i: i, onhover: onhover }
|
||||
ChildExample { i: i, onhover: onhover }
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[inline_props]
|
||||
fn child_example<'a>(cx: Scope<'a>, i: i32, onhover: EventHandler<'a, MouseEvent>) -> Element {
|
||||
#[component]
|
||||
fn ChildExample<'a>(cx: Scope<'a>, i: i32, onhover: EventHandler<'a, MouseEvent>) -> Element {
|
||||
cx.render(rsx! {
|
||||
li {
|
||||
onmouseover: move |e| onhover.call(e),
|
||||
|
||||
@@ -2,13 +2,14 @@ use dioxus::prelude::*;
|
||||
|
||||
#[test]
|
||||
fn app_drops() {
|
||||
fn app(cx: Scope) -> Element {
|
||||
#[component]
|
||||
fn App(cx: Scope) -> Element {
|
||||
cx.render(rsx! {
|
||||
div {}
|
||||
})
|
||||
}
|
||||
|
||||
let mut dom = VirtualDom::new(app);
|
||||
let mut dom = VirtualDom::new(App);
|
||||
|
||||
_ = dom.rebuild();
|
||||
dom.mark_dirty(ScopeId::ROOT);
|
||||
@@ -17,7 +18,8 @@ fn app_drops() {
|
||||
|
||||
#[test]
|
||||
fn hooks_drop() {
|
||||
fn app(cx: Scope) -> Element {
|
||||
#[component]
|
||||
fn App(cx: Scope) -> Element {
|
||||
cx.use_hook(|| String::from("asd"));
|
||||
cx.use_hook(|| String::from("asd"));
|
||||
cx.use_hook(|| String::from("asd"));
|
||||
@@ -28,7 +30,7 @@ fn hooks_drop() {
|
||||
})
|
||||
}
|
||||
|
||||
let mut dom = VirtualDom::new(app);
|
||||
let mut dom = VirtualDom::new(App);
|
||||
|
||||
_ = dom.rebuild();
|
||||
dom.mark_dirty(ScopeId::ROOT);
|
||||
@@ -37,17 +39,19 @@ fn hooks_drop() {
|
||||
|
||||
#[test]
|
||||
fn contexts_drop() {
|
||||
fn app(cx: Scope) -> Element {
|
||||
#[component]
|
||||
fn App(cx: Scope) -> Element {
|
||||
cx.provide_context(String::from("asd"));
|
||||
|
||||
cx.render(rsx! {
|
||||
div {
|
||||
child_comp {}
|
||||
ChildComp {}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn child_comp(cx: Scope) -> Element {
|
||||
#[component]
|
||||
fn ChildComp(cx: Scope) -> Element {
|
||||
let el = cx.consume_context::<String>().unwrap();
|
||||
|
||||
cx.render(rsx! {
|
||||
@@ -55,7 +59,7 @@ fn contexts_drop() {
|
||||
})
|
||||
}
|
||||
|
||||
let mut dom = VirtualDom::new(app);
|
||||
let mut dom = VirtualDom::new(App);
|
||||
|
||||
_ = dom.rebuild();
|
||||
dom.mark_dirty(ScopeId::ROOT);
|
||||
@@ -64,7 +68,8 @@ fn contexts_drop() {
|
||||
|
||||
#[test]
|
||||
fn tasks_drop() {
|
||||
fn app(cx: Scope) -> Element {
|
||||
#[component]
|
||||
fn App(cx: Scope) -> Element {
|
||||
cx.spawn(async {
|
||||
// tokio::time::sleep(std::time::Duration::from_millis(100000)).await;
|
||||
});
|
||||
@@ -74,7 +79,7 @@ fn tasks_drop() {
|
||||
})
|
||||
}
|
||||
|
||||
let mut dom = VirtualDom::new(app);
|
||||
let mut dom = VirtualDom::new(App);
|
||||
|
||||
_ = dom.rebuild();
|
||||
dom.mark_dirty(ScopeId::ROOT);
|
||||
@@ -97,29 +102,30 @@ fn root_props_drop() {
|
||||
|
||||
#[test]
|
||||
fn diffing_drops_old() {
|
||||
fn app(cx: Scope) -> Element {
|
||||
#[component]
|
||||
fn App(cx: Scope) -> Element {
|
||||
cx.render(rsx! {
|
||||
div {
|
||||
match cx.generation() % 2 {
|
||||
0 => rsx!( child_comp1 { name: "asdasd".to_string() }),
|
||||
1 => rsx!( child_comp2 { name: "asdasd".to_string() }),
|
||||
0 => rsx!( ChildComp1 { name: "asdasd".to_string() }),
|
||||
1 => rsx!( ChildComp2 { name: "asdasd".to_string() }),
|
||||
_ => todo!()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[inline_props]
|
||||
fn child_comp1(cx: Scope, name: String) -> Element {
|
||||
#[component]
|
||||
fn ChildComp1(cx: Scope, name: String) -> Element {
|
||||
cx.render(rsx! { "Hello {name}" })
|
||||
}
|
||||
|
||||
#[inline_props]
|
||||
fn child_comp2(cx: Scope, name: String) -> Element {
|
||||
#[component]
|
||||
fn ChildComp2(cx: Scope, name: String) -> Element {
|
||||
cx.render(rsx! { "Goodbye {name}" })
|
||||
}
|
||||
|
||||
let mut dom = VirtualDom::new(app);
|
||||
let mut dom = VirtualDom::new(App);
|
||||
_ = dom.rebuild();
|
||||
dom.mark_dirty(ScopeId::ROOT);
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ fn suspended_child(cx: Scope) -> Element {
|
||||
cx.spawn(async move {
|
||||
val += 1;
|
||||
});
|
||||
return cx.suspend()?;
|
||||
cx.suspend()?;
|
||||
}
|
||||
|
||||
render!("child")
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
/src/minified.js
|
||||
Loaded 100 of 569 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user