(
+ config: CrateConfig,
+ serve_cfg: &ConfigOptsServe,
+) -> Result<()> {
// ctrl-c shutdown checker
let _crate_config = config.clone();
let _ = ctrlc::set_handler(move || {
@@ -43,8 +51,6 @@ pub async fn startup(config: CrateConfig) -> Result<()> {
let hot_reload_tx = broadcast::channel(100).0;
- clear_paths();
-
Some(HotReloadState {
messages: hot_reload_tx.clone(),
file_map: file_map.clone(),
@@ -53,15 +59,18 @@ pub async fn startup(config: CrateConfig) -> Result<()> {
false => None,
};
- serve(config, hot_reload_state).await?;
+ serve::(config, serve_cfg, hot_reload_state).await?;
Ok(())
}
/// Start the server without hot reload
-pub async fn serve(config: CrateConfig, hot_reload_state: Option) -> Result<()> {
- let (child, first_build_result) = start_desktop(&config)?;
- let currently_running_child: RwLock = RwLock::new(child);
+async fn serve(
+ config: CrateConfig,
+ serve: &ConfigOptsServe,
+ hot_reload_state: Option,
+) -> Result<()> {
+ let platform = RwLock::new(P::start(&config, serve)?);
log::info!("🚀 Starting development server...");
@@ -70,14 +79,7 @@ pub async fn serve(config: CrateConfig, hot_reload_state: Option
let _watcher = setup_file_watcher(
{
let config = config.clone();
-
- move || {
- let mut current_child = currently_running_child.write().unwrap();
- current_child.kill()?;
- let (child, result) = start_desktop(&config)?;
- *current_child = child;
- Ok(result)
- }
+ move || platform.write().unwrap().rebuild(&config)
},
&config,
None,
@@ -85,19 +87,9 @@ pub async fn serve(config: CrateConfig, hot_reload_state: Option
)
.await?;
- // Print serve info
- print_console_info(
- &config,
- PrettierOptions {
- changed: vec![],
- warnings: first_build_result.warnings,
- elapsed_time: first_build_result.elapsed_time,
- },
- None,
- );
-
match hot_reload_state {
Some(hot_reload_state) => {
+ // The open interprocess sockets
start_desktop_hot_reload(hot_reload_state).await?;
}
None => {
@@ -109,7 +101,14 @@ pub async fn serve(config: CrateConfig, hot_reload_state: Option
}
async fn start_desktop_hot_reload(hot_reload_state: HotReloadState) -> Result<()> {
- match LocalSocketListener::bind("@dioxusin") {
+ 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
@@ -121,30 +120,41 @@ async fn start_desktop_hot_reload(hot_reload_state: HotReloadState) -> Result<()
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;
@@ -174,17 +184,14 @@ async fn start_desktop_hot_reload(hot_reload_state: HotReloadState) -> Result<()
Ok(())
}
-fn clear_paths() {
- if cfg!(target_os = "macos") {
+fn clear_paths(file_socket_path: &std::path::Path) {
+ if cfg!(unix) {
// 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);
}
}
}
@@ -203,9 +210,9 @@ fn send_msg(msg: HotReloadMsg, channel: &mut impl std::io::Write) -> bool {
}
}
-pub fn start_desktop(config: &CrateConfig) -> Result<(Child, BuildResult)> {
+fn start_desktop(config: &CrateConfig, skip_assets: bool) -> Result<(RAIIChild, BuildResult)> {
// Run the desktop application
- let result = crate::builder::build_desktop(config, true)?;
+ let result = crate::builder::build_desktop(config, true, skip_assets)?;
match &config.executable {
crate::ExecutableType::Binary(name)
@@ -215,9 +222,58 @@ pub fn start_desktop(config: &CrateConfig) -> Result<(Child, BuildResult)> {
if cfg!(windows) {
file.set_extension("exe");
}
- let child = Command::new(file.to_str().unwrap()).spawn()?;
+ let active = "DIOXUS_ACTIVE";
+ let child = RAIIChild(
+ Command::new(file.to_str().unwrap())
+ .env(active, "true")
+ .spawn()?,
+ );
Ok((child, result))
}
}
}
+
+pub(crate) struct DesktopPlatform {
+ currently_running_child: RAIIChild,
+ skip_assets: bool,
+}
+
+impl Platform for DesktopPlatform {
+ fn start(config: &CrateConfig, serve: &ConfigOptsServe) -> Result {
+ let (child, first_build_result) = start_desktop(config, serve.skip_assets)?;
+
+ log::info!("🚀 Starting development server...");
+
+ // Print serve info
+ print_console_info(
+ config,
+ PrettierOptions {
+ changed: vec![],
+ warnings: first_build_result.warnings,
+ elapsed_time: first_build_result.elapsed_time,
+ },
+ None,
+ );
+
+ Ok(Self {
+ currently_running_child: child,
+ skip_assets: serve.skip_assets,
+ })
+ }
+
+ fn rebuild(&mut self, config: &CrateConfig) -> Result {
+ self.currently_running_child.0.kill()?;
+ let (child, result) = start_desktop(config, self.skip_assets)?;
+ self.currently_running_child = child;
+ Ok(result)
+ }
+}
+
+struct RAIIChild(Child);
+
+impl Drop for RAIIChild {
+ fn drop(&mut self) {
+ let _ = self.0.kill();
+ }
+}
diff --git a/packages/cli/src/server/fullstack/mod.rs b/packages/cli/src/server/fullstack/mod.rs
new file mode 100644
index 000000000..fa9c77459
--- /dev/null
+++ b/packages/cli/src/server/fullstack/mod.rs
@@ -0,0 +1,161 @@
+use crate::{
+ cfg::{ConfigOptsBuild, ConfigOptsServe},
+ CrateConfig, Result, WebAssetConfigDropGuard,
+};
+
+use super::{desktop, Platform};
+
+pub async fn startup(config: CrateConfig, serve: &ConfigOptsServe) -> Result<()> {
+ desktop::startup_with_platform::(config, serve).await
+}
+
+fn start_web_build_thread(
+ config: &CrateConfig,
+ serve: &ConfigOptsServe,
+) -> std::thread::JoinHandle> {
+ let serve = serve.clone();
+ let target_directory = config.crate_dir.join(".dioxus").join("web");
+ std::fs::create_dir_all(&target_directory).unwrap();
+ std::thread::spawn(move || build_web(serve, &target_directory))
+}
+
+struct FullstackPlatform {
+ serve: ConfigOptsServe,
+ desktop: desktop::DesktopPlatform,
+ _config: WebAssetConfigDropGuard,
+}
+
+impl Platform for FullstackPlatform {
+ fn start(config: &CrateConfig, serve: &ConfigOptsServe) -> Result
+ where
+ Self: Sized,
+ {
+ let thread_handle = start_web_build_thread(config, serve);
+
+ let mut desktop_config = config.clone();
+ let desktop_feature = serve.server_feature.clone();
+ let features = &mut desktop_config.features;
+ match features {
+ Some(features) => {
+ features.push(desktop_feature);
+ }
+ None => desktop_config.features = Some(vec![desktop_feature]),
+ };
+ let config = WebAssetConfigDropGuard::new();
+ let desktop = desktop::DesktopPlatform::start(&desktop_config, serve)?;
+ thread_handle
+ .join()
+ .map_err(|_| anyhow::anyhow!("Failed to join thread"))??;
+
+ Ok(Self {
+ desktop,
+ serve: serve.clone(),
+ _config: config,
+ })
+ }
+
+ fn rebuild(&mut self, crate_config: &CrateConfig) -> Result {
+ let thread_handle = start_web_build_thread(crate_config, &self.serve);
+ let result = {
+ let mut desktop_config = crate_config.clone();
+ let desktop_feature = self.serve.server_feature.clone();
+ let features = &mut desktop_config.features;
+ match features {
+ Some(features) => {
+ features.push(desktop_feature);
+ }
+ None => desktop_config.features = Some(vec![desktop_feature]),
+ };
+ let _gaurd = FullstackServerEnvGuard::new(self.serve.force_debug, self.serve.release);
+ self.desktop.rebuild(&desktop_config)
+ };
+ thread_handle
+ .join()
+ .map_err(|_| anyhow::anyhow!("Failed to join thread"))??;
+ result
+ }
+}
+
+fn build_web(serve: ConfigOptsServe, target_directory: &std::path::Path) -> Result<()> {
+ let mut web_config: ConfigOptsBuild = serve.into();
+ let web_feature = web_config.client_feature.clone();
+ let features = &mut web_config.features;
+ match features {
+ Some(features) => {
+ features.push(web_feature);
+ }
+ None => web_config.features = Some(vec![web_feature]),
+ };
+ web_config.platform = Some(crate::cfg::Platform::Web);
+
+ let _gaurd = FullstackWebEnvGuard::new(&web_config);
+ crate::cli::build::Build { build: web_config }.build(None, Some(target_directory))
+}
+
+// Debug mode web builds have a very large size by default. If debug mode is not enabled, we strip some of the debug info by default
+// This reduces a hello world from ~40MB to ~2MB
+pub(crate) struct FullstackWebEnvGuard {
+ old_rustflags: Option,
+}
+
+impl FullstackWebEnvGuard {
+ pub fn new(serve: &ConfigOptsBuild) -> Self {
+ Self {
+ old_rustflags: (!serve.force_debug).then(|| {
+ let old_rustflags = std::env::var("RUSTFLAGS").unwrap_or_default();
+ let debug_assertions = if serve.release {
+ ""
+ } else {
+ " -C debug-assertions"
+ };
+
+ std::env::set_var(
+ "RUSTFLAGS",
+ format!(
+ "{old_rustflags} -C debuginfo=none -C strip=debuginfo{debug_assertions}"
+ ),
+ );
+ old_rustflags
+ }),
+ }
+ }
+}
+
+impl Drop for FullstackWebEnvGuard {
+ fn drop(&mut self) {
+ if let Some(old_rustflags) = self.old_rustflags.take() {
+ std::env::set_var("RUSTFLAGS", old_rustflags);
+ }
+ }
+}
+
+// Debug mode web builds have a very large size by default. If debug mode is not enabled, we strip some of the debug info by default
+// This reduces a hello world from ~40MB to ~2MB
+pub(crate) struct FullstackServerEnvGuard {
+ old_rustflags: Option,
+}
+
+impl FullstackServerEnvGuard {
+ pub fn new(debug: bool, release: bool) -> Self {
+ Self {
+ old_rustflags: (!debug).then(|| {
+ let old_rustflags = std::env::var("RUSTFLAGS").unwrap_or_default();
+ let debug_assertions = if release { "" } else { " -C debug-assertions" };
+
+ std::env::set_var(
+ "RUSTFLAGS",
+ format!("{old_rustflags} -C opt-level=2 {debug_assertions}"),
+ );
+ old_rustflags
+ }),
+ }
+ }
+}
+
+impl Drop for FullstackServerEnvGuard {
+ fn drop(&mut self) {
+ if let Some(old_rustflags) = self.old_rustflags.take() {
+ std::env::set_var("RUSTFLAGS", old_rustflags);
+ }
+ }
+}
diff --git a/packages/cli/src/server/mod.rs b/packages/cli/src/server/mod.rs
index c6073d787..895d9e51e 100644
--- a/packages/cli/src/server/mod.rs
+++ b/packages/cli/src/server/mod.rs
@@ -1,4 +1,4 @@
-use crate::{BuildResult, CrateConfig, Result};
+use crate::{cfg::ConfigOptsServe, BuildResult, CrateConfig, Result};
use cargo_metadata::diagnostic::Diagnostic;
use dioxus_core::Template;
@@ -14,6 +14,7 @@ use tokio::sync::broadcast::{self};
mod output;
use output::*;
pub mod desktop;
+pub mod fullstack;
pub mod web;
/// Sets up a file watcher
@@ -32,7 +33,7 @@ async fn setup_file_watcher Result + 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| {
@@ -55,6 +56,16 @@ async fn setup_file_watcher Result + Send + 'static>(
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);
@@ -121,16 +132,23 @@ async fn setup_file_watcher Result + Send + 'static>(
.unwrap();
for sub_path in allow_watch_path {
- watcher
- .watch(
- &config.crate_dir.join(sub_path),
- notify::RecursiveMode::Recursive,
- )
- .unwrap();
+ if let Err(err) = watcher.watch(
+ &config.crate_dir.join(sub_path),
+ notify::RecursiveMode::Recursive,
+ ) {
+ log::error!("Failed to watch path: {}", err);
+ }
}
Ok(watcher)
}
+pub(crate) trait Platform {
+ fn start(config: &CrateConfig, serve: &ConfigOptsServe) -> Result
+ where
+ Self: Sized;
+ fn rebuild(&mut self, config: &CrateConfig) -> Result;
+}
+
#[derive(Clone)]
pub struct HotReloadState {
pub messages: broadcast::Sender>,
diff --git a/packages/cli/src/server/output.rs b/packages/cli/src/server/output.rs
index ce0f55064..4e148b0f1 100644
--- a/packages/cli/src/server/output.rs
+++ b/packages/cli/src/server/output.rs
@@ -22,17 +22,20 @@ pub fn print_console_info(
options: PrettierOptions,
web_info: Option,
) {
- 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()
diff --git a/packages/cli/src/server/web/mod.rs b/packages/cli/src/server/web/mod.rs
index 0e1413597..896d08643 100644
--- a/packages/cli/src/server/web/mod.rs
+++ b/packages/cli/src/server/web/mod.rs
@@ -11,6 +11,7 @@ use axum::{
body::{Full, HttpBody},
extract::{ws::Message, Extension, TypedHeader, WebSocketUpgrade},
http::{
+ self,
header::{HeaderName, HeaderValue},
Method, Response, StatusCode,
},
@@ -47,7 +48,12 @@ struct WsReloadState {
update: broadcast::Sender<()>,
}
-pub async fn startup(port: u16, config: CrateConfig, start_browser: bool) -> Result<()> {
+pub async fn startup(
+ port: u16,
+ config: CrateConfig,
+ start_browser: bool,
+ skip_assets: bool,
+) -> Result<()> {
// ctrl-c shutdown checker
let _crate_config = config.clone();
let _ = ctrlc::set_handler(move || {
@@ -79,7 +85,15 @@ pub async fn startup(port: u16, config: CrateConfig, start_browser: bool) -> Res
false => None,
};
- serve(ip, port, config, start_browser, hot_reload_state).await?;
+ serve(
+ ip,
+ port,
+ config,
+ start_browser,
+ skip_assets,
+ hot_reload_state,
+ )
+ .await?;
Ok(())
}
@@ -90,9 +104,10 @@ pub async fn serve(
port: u16,
config: CrateConfig,
start_browser: bool,
+ skip_assets: bool,
hot_reload_state: Option,
) -> Result<()> {
- let first_build_result = crate::builder::build(&config, true)?;
+ let first_build_result = crate::builder::build(&config, false, skip_assets)?;
log::info!("🚀 Starting development server...");
@@ -105,7 +120,7 @@ pub async fn serve(
{
let config = config.clone();
let reload_tx = reload_tx.clone();
- move || build(&config, &reload_tx)
+ move || build(&config, &reload_tx, skip_assets)
},
&config,
Some(WebServerInfo {
@@ -262,7 +277,7 @@ async fn setup_router(
.override_response_header(HeaderName::from_static("cross-origin-opener-policy"), coop)
.and_then(
move |response: Response| async move {
- let response = if file_service_config
+ let mut response = if file_service_config
.dioxus_config
.web
.watcher
@@ -290,6 +305,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)
},
)
@@ -412,8 +434,8 @@ async fn ws_handler(
})
}
-fn build(config: &CrateConfig, reload_tx: &Sender<()>) -> Result {
- let result = builder::build(config, true)?;
+fn build(config: &CrateConfig, reload_tx: &Sender<()>, skip_assets: bool) -> Result {
+ let result = builder::build(config, true, skip_assets)?;
// change the websocket reload state to true;
// the page will auto-reload.
if config
@@ -423,7 +445,7 @@ fn build(config: &CrateConfig, reload_tx: &Sender<()>) -> Result {
.reload_html
.unwrap_or(false)
{
- let _ = Serve::regen_dev_page(config);
+ let _ = Serve::regen_dev_page(config, skip_assets);
}
let _ = reload_tx.send(());
Ok(result)
diff --git a/packages/core-macro/Cargo.toml b/packages/core-macro/Cargo.toml
index d928d5fec..ae3dc9cdd 100644
--- a/packages/core-macro/Cargo.toml
+++ b/packages/core-macro/Cargo.toml
@@ -15,10 +15,12 @@ proc-macro = true
[dependencies]
proc-macro2 = { version = "1.0" }
quote = "1.0"
-syn = { version = "2.0", features = ["full", "extra-traits"] }
+syn = { version = "2.0", features = ["full", "extra-traits", "visit"] }
dioxus-rsx = { workspace = true }
dioxus-core = { workspace = true }
constcat = "0.3.0"
+convert_case = "^0.6.0"
+prettyplease = "0.2.15"
# testing
[dev-dependencies]
diff --git a/packages/core-macro/README.md b/packages/core-macro/README.md
index 31b0cca90..497c7c587 100644
--- a/packages/core-macro/README.md
+++ b/packages/core-macro/README.md
@@ -23,9 +23,12 @@
`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
diff --git a/packages/core-macro/src/component_body_deserializers/component.rs b/packages/core-macro/src/component_body_deserializers/component.rs
index e43b1819f..994e51709 100644
--- a/packages/core-macro/src/component_body_deserializers/component.rs
+++ b/packages/core-macro/src/component_body_deserializers/component.rs
@@ -31,6 +31,7 @@ fn get_out_comp_fn(orig_comp_fn: &ItemFn, cx_pat: &Pat) -> ItemFn {
block: parse_quote! {
{
#[warn(non_snake_case)]
+ #[allow(clippy::inline_always)]
#[inline(always)]
#inner_comp_fn
#inner_comp_ident (#cx_pat)
@@ -57,8 +58,11 @@ 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
diff --git a/packages/core-macro/src/component_body_deserializers/inline_props.rs b/packages/core-macro/src/component_body_deserializers/inline_props.rs
index 8e4581c19..d51132623 100644
--- a/packages/core-macro/src/component_body_deserializers/inline_props.rs
+++ b/packages/core-macro/src/component_body_deserializers/inline_props.rs
@@ -30,166 +30,312 @@ impl ToTokens for InlinePropsDeserializerOutput {
impl DeserializerArgs for InlinePropsDeserializerArgs {
fn to_output(&self, component_body: &ComponentBody) -> Result {
Ok(InlinePropsDeserializerOutput {
- comp_fn: Self::get_function(component_body),
- props_struct: Self::get_props_struct(component_body),
+ comp_fn: get_function(component_body),
+ props_struct: get_props_struct(component_body),
})
}
}
-impl InlinePropsDeserializerArgs {
- fn get_props_struct(component_body: &ComponentBody) -> ItemStruct {
- let ComponentBody { item_fn, .. } = component_body;
- let ItemFn { vis, sig, .. } = item_fn;
- let Signature {
- inputs,
- ident: fn_ident,
- generics,
- ..
- } = sig;
+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
+ // 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
- }
+ 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 = component_body
- .item_fn
- .sig
- .generics
- .params
- .iter()
- .map(|it| match it {
- GenericParam::Type(tp) => {
- let mut tp = tp.clone();
- tp.bounds.push(parse_quote!( 'a ));
-
- GenericParam::Type(tp)
- }
- _ => it.clone(),
- })
- .collect();
-
- quote! { <#struct_generics> }
- } else {
- quote! { #generics }
- };
-
- parse_quote! {
- #struct_attrs
- #[allow(non_camel_case_types)]
- #vis struct #struct_ident #struct_generics
- {
- #(#struct_fields),*
- }
}
- }
+ });
- fn get_function(component_body: &ComponentBody) -> ItemFn {
- let ComponentBody {
- item_fn,
- cx_pat_type,
- ..
- } = component_body;
- let ItemFn {
- attrs: fn_attrs,
- vis,
- sig,
- block: fn_block,
- } = item_fn;
- let Signature {
- inputs,
- ident: fn_ident,
- generics,
- output: fn_output,
- asyncness,
- ..
- } = sig;
- let Generics { where_clause, .. } = generics;
+ let struct_ident = Ident::new(&format!("{fn_ident}Props"), fn_ident.span());
- let cx_pat = &cx_pat_type.pat;
- 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
+ };
- // Skip first arg since that's the context
- let struct_field_names = inputs.iter().skip(1).filter_map(|f| match f {
- FnArg::Receiver(_) => unreachable!(), // ComponentBody prohibits receiver parameters.
- FnArg::Typed(t) => Some(&t.pat),
- });
+ let struct_attrs = if first_lifetime.is_some() {
+ quote! { #[derive(Props)] }
+ } else {
+ quote! { #[derive(Props, PartialEq)] }
+ };
- let first_lifetime = if let Some(GenericParam::Lifetime(lt)) = generics.params.first() {
- Some(lt)
- } else {
- None
- };
+ let struct_generics = if first_lifetime.is_some() {
+ let struct_generics: Punctuated = 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 ));
- let (scope_lifetime, fn_generics) = if let Some(lt) = first_lifetime {
- (quote! { #lt, }, generics.clone())
- } else {
- let lifetime: LifetimeParam = parse_quote! { 'a };
+ GenericParam::Type(tp)
+ }
+ _ => it.clone(),
+ })
+ .collect();
- let mut fn_generics = generics.clone();
- fn_generics
- .params
- .insert(0, GenericParam::Lifetime(lifetime.clone()));
+ quote! { <#struct_generics> }
+ } else {
+ quote! { #generics }
+ };
- (quote! { #lifetime, }, fn_generics)
- };
-
- let generics_no_bounds = {
- let mut generics = generics.clone();
- generics.params = generics
- .params
- .iter()
- .map(|it| match it {
- GenericParam::Type(tp) => {
- let mut tp = tp.clone();
- tp.bounds.clear();
-
- GenericParam::Type(tp)
- }
- _ => it.clone(),
- })
- .collect();
-
- generics
- };
-
- parse_quote! {
- #(#fn_attrs)*
- #asyncness #vis fn #fn_ident #fn_generics (#cx_pat: Scope<#scope_lifetime #struct_ident #generics_no_bounds>) #fn_output
- #where_clause
- {
- let #struct_ident { #(#struct_field_names),* } = cx_pat.props;
- #fn_block
- }
+ 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 {
+ 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::>();
+
+ 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", "
");
+ 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("
👎 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("
");
+
+ if !input_arg_doc.is_empty() {
+ arg_doc.push_str("
");
+ }
+ }
+
+ if !input_arg_doc.is_empty() {
+ arg_doc.push_str(&format!("{input_arg_doc}
"));
+ }
+
+ 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 = 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
+}
diff --git a/packages/core-macro/src/lib.rs b/packages/core-macro/src/lib.rs
index d91c72e39..4686bc797 100644
--- a/packages/core-macro/src/lib.rs
+++ b/packages/core-macro/src/lib.rs
@@ -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 proc_macro::TokenStream;
use quote::ToTokens;
use rsx::RenderCallBody;
@@ -8,6 +12,7 @@ use syn::{parse_macro_input, Path, Token};
mod component_body;
mod component_body_deserializers;
mod props;
+mod utils;
// mod rsx;
use crate::component_body::ComponentBody;
diff --git a/packages/core-macro/src/props/mod.rs b/packages/core-macro/src/props/mod.rs
index c1931e497..fff8bcbf1 100644
--- a/packages/core-macro/src/props/mod.rs
+++ b/packages/core-macro/src/props/mod.rs
@@ -24,6 +24,10 @@ pub fn impl_my_derive(ast: &syn::DeriveInput) -> Result {
.included_fields()
.map(|f| struct_info.field_impl(f))
.collect::, _>>()?;
+ let extends = struct_info
+ .extend_fields()
+ .map(|f| struct_info.extends_impl(f))
+ .collect::, _>>()?;
let fields = quote!(#(#fields)*).into_iter();
let required_fields = struct_info
.included_fields()
@@ -36,6 +40,7 @@ pub fn impl_my_derive(ast: &syn::DeriveInput) -> Result {
#builder_creation
#conversion_helper
#( #fields )*
+ #( #extends )*
#( #required_fields )*
#build_method
}
@@ -167,8 +172,8 @@ mod field_info {
use proc_macro2::TokenStream;
use quote::quote;
use syn::spanned::Spanned;
- use syn::Expr;
use syn::{parse::Error, punctuated::Punctuated};
+ use syn::{Expr, Path};
use super::util::{
expr_to_single_string, ident_to_type, path_to_single_string, strip_raw_ident_prefix,
@@ -199,6 +204,13 @@ mod field_info {
);
}
+ // extended field is automatically empty
+ if !builder_attr.extends.is_empty() {
+ builder_attr.default = Some(
+ syn::parse(quote!(::core::default::Default::default()).into()).unwrap(),
+ );
+ }
+
// auto detect optional
let strip_option_auto = builder_attr.strip_option
|| !builder_attr.ignore_option
@@ -243,10 +255,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)]
@@ -257,6 +265,7 @@ mod field_info {
pub auto_into: bool,
pub strip_option: bool,
pub ignore_option: bool,
+ pub extends: Vec,
}
impl FieldBuilderAttr {
@@ -309,6 +318,17 @@ mod field_info {
let name = expr_to_single_string(&assign.left)
.ok_or_else(|| Error::new_spanned(&assign.left, "Expected identifier"))?;
match name.as_str() {
+ "extends" => {
+ if let syn::Expr::Path(path) = *assign.right {
+ self.extends.push(path.path);
+ Ok(())
+ } else {
+ Err(Error::new_spanned(
+ assign.right,
+ "Expected simple identifier",
+ ))
+ }
+ }
"default" => {
self.default = Some(*assign.right);
Ok(())
@@ -363,6 +383,11 @@ mod field_info {
Ok(())
}
+ "extend" => {
+ self.extends.push(path.path);
+ Ok(())
+ }
+
_ => {
macro_rules! handle_fields {
( $( $flag:expr, $field:ident, $already:expr; )* ) => {
@@ -466,11 +491,14 @@ fn type_from_inside_option(ty: &syn::Type, check_option_name: bool) -> Option<&s
}
mod struct_info {
+ use convert_case::{Case, Casing};
use proc_macro2::TokenStream;
use quote::quote;
use syn::parse::Error;
use syn::punctuated::Punctuated;
- use syn::Expr;
+ use syn::spanned::Spanned;
+ use syn::visit::Visit;
+ use syn::{parse_quote, Expr, Ident};
use super::field_info::{FieldBuilderAttr, FieldInfo};
use super::util::{
@@ -493,7 +521,46 @@ mod struct_info {
impl<'a> StructInfo<'a> {
pub fn included_fields(&self) -> impl Iterator- > {
- self.fields.iter().filter(|f| !f.builder_attr.skip)
+ self.fields
+ .iter()
+ .filter(|f| !f.builder_attr.skip && f.builder_attr.extends.is_empty())
+ }
+
+ pub fn extend_fields(&self) -> impl Iterator
- > {
+ self.fields
+ .iter()
+ .filter(|f| !f.builder_attr.extends.is_empty())
+ }
+
+ fn extend_lifetime(&self) -> syn::Result