deckyfx-dioxus-react-integration 0.2.2

Serve React apps with Dioxus runtime and IPC bridge
Documentation
//! React Container Component
//!
//! Provides a Dioxus component for mounting React applications using a
//! folder asset + manifest pattern. The build step generates `metadata.json`
//! describing the output files, and `ReactApp` reads it to load the correct
//! CSS/JS chunks — no manual renaming or copying needed.
//!
//! # Breaking Changes (0.2.0)
//!
//! - Removed `ReactContainer` (single bundle.js only)
//! - `ReactApp` now uses `dir` (folder asset) + `manifest` (ReactManifest) instead
//!   of individual `bundle` / `stylesheet` asset props
//!
//! # Usage
//!
//! ```rust,ignore
//! const REACT_DIR: Asset = asset!("/assets/react");
//!
//! // Parse metadata.json at compile time
//! let manifest = ReactManifest::from_json(
//!     include_str!("../assets/react/metadata.json")
//! ).expect("invalid metadata.json");
//!
//! rsx! {
//!     ReactApp {
//!         dir: REACT_DIR,
//!         manifest: manifest,
//!         bridge: bridge,
//!     }
//! }
//! ```

use dioxus::prelude::*;
use serde::Deserialize;

/// Build manifest describing the React app's output files.
///
/// Generated by the build step as `metadata.json` in the dist folder.
/// Contains the hashed filenames so the Rust side can load them correctly.
///
/// # Example metadata.json
/// ```json
/// {
///   "scripts": ["chunk-2b2s0nqa.js"],
///   "styles": ["chunk-q7kzc8rh.css"],
///   "assets": ["logo-kygw735p.svg", "react-c5c0zhye.svg"]
/// }
/// ```
#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct ReactManifest {
    /// JS bundle filenames (order matters — loaded in sequence)
    pub scripts: Vec<String>,
    /// CSS stylesheet filenames
    #[serde(default)]
    pub styles: Vec<String>,
    /// Other asset filenames (images, fonts, etc.) — informational only
    #[serde(default)]
    pub assets: Vec<String>,
}

impl ReactManifest {
    /// Parse a ReactManifest from a JSON string.
    ///
    /// Typically used with `include_str!` to embed metadata.json at compile time:
    /// ```rust,ignore
    /// let manifest = ReactManifest::from_json(
    ///     include_str!("../assets/react/metadata.json")
    /// ).expect("invalid metadata.json");
    /// ```
    pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
        serde_json::from_str(json)
    }
}

/// React application container — loads a React app from a folder asset + manifest.
///
/// Uses Dioxus's folder asset feature to reference the entire React build output
/// directory. The manifest describes which CSS/JS files to load (with their
/// hashed filenames), so no renaming or copying is needed.
///
/// The component:
/// 1. Loads all stylesheets listed in the manifest
/// 2. Injects the IPC bridge script (if provided)
/// 3. Creates the mount point `<div>` and loads all JS bundles
///
/// # Example
/// ```rust,ignore
/// use dioxus::prelude::*;
/// use dioxus_react_integration::prelude::*;
/// use std::time::Duration;
///
/// const REACT_DIR: Asset = asset!("/assets/react");
///
/// fn app() -> Element {
///     let bridge = IpcBridge::builder()
///         .timeout(Duration::from_secs(30))
///         .build();
///
///     let manifest = ReactManifest::from_json(
///         include_str!("../assets/react/metadata.json")
///     ).expect("invalid metadata.json");
///
///     rsx! {
///         ReactApp {
///             dir: REACT_DIR,
///             manifest: manifest,
///             bridge: bridge,
///         }
///     }
/// }
/// ```
#[component]
pub fn ReactApp(
    /// Folder asset pointing to the React build output directory
    dir: Asset,
    /// Build manifest with the hashed filenames
    manifest: ReactManifest,
    /// Optional IPC bridge instance (generates and injects the bridge script)
    bridge: Option<crate::prelude::IpcBridge>,
    /// DOM element ID where React will mount (default: "root")
    #[props(default = "root".to_string())]
    mount_id: String,
) -> Element {
    let bridge_script = bridge.map(|b| b.generate_script());

    // The asset folder path (e.g. "/assets/react/") — used as <base> so that
    // relative asset references in the JS bundle (like "./logo.svg") resolve
    // correctly against the Dioxus asset directory.
    let base_href = format!("{}/", dir);

    // Build full paths: "{dir}/{filename}"
    let style_paths: Vec<String> = manifest
        .styles
        .iter()
        .map(|s| format!("{}/{}", dir, s))
        .collect();

    let script_paths: Vec<String> = manifest
        .scripts
        .iter()
        .map(|s| format!("{}/{}", dir, s))
        .collect();

    rsx! {
        // 1. Set <base> so relative URLs in the React bundle resolve to the asset folder.
        //    Dioxus desktop serves assets at /assets/react/ but the document base is "/".
        //    Without this, "./logo.svg" in JS resolves to "/logo.svg" (404) instead of
        //    "/assets/react/logo.svg".
        script {
            dangerous_inner_html: "document.head.insertBefore(Object.assign(document.createElement('base'), {{ href: '{base_href}' }}), document.head.firstChild);"
        }

        // 2. Load all stylesheets
        for css_path in &style_paths {
            link { rel: "stylesheet", href: "{css_path}" }
        }

        // 3. Inject IPC bridge script before React loads
        if let Some(ref script_content) = bridge_script {
            script { dangerous_inner_html: "{script_content}" }
        }

        // 4. React mount point
        div { id: "{mount_id}" }

        // 5. Load all JS bundles
        for js_path in &script_paths {
            script { src: "{js_path}" }
        }
    }
}