leptos/hydration/
mod.rs

1#![allow(clippy::needless_lifetimes)]
2
3use crate::{prelude::*, WasmSplitManifest};
4use leptos_config::LeptosOptions;
5use leptos_macro::{component, view};
6use std::{path::PathBuf, sync::OnceLock};
7
8/// Inserts auto-reloading code used in `cargo-leptos`.
9///
10/// This should be included in the `<head>` of your application shell during development.
11#[component]
12pub fn AutoReload(
13    /// Whether the file-watching feature should be disabled.
14    #[prop(optional)]
15    disable_watch: bool,
16    /// Configuration options for this project.
17    options: LeptosOptions,
18) -> impl IntoView {
19    (!disable_watch && std::env::var("LEPTOS_WATCH").is_ok()).then(|| {
20        #[cfg(feature = "nonce")]
21        let nonce = crate::nonce::use_nonce();
22        #[cfg(not(feature = "nonce"))]
23        let nonce = None::<()>;
24
25        let reload_port = match options.reload_external_port {
26            Some(val) => val,
27            None => options.reload_port,
28        };
29        let protocol = match options.reload_ws_protocol {
30            leptos_config::ReloadWSProtocol::WS => "'ws://'",
31            leptos_config::ReloadWSProtocol::WSS => "'wss://'",
32        };
33
34        let script = format!(
35            "(function (reload_port, protocol) {{ {} {} }})({reload_port:?}, \
36             {protocol})",
37            leptos_hot_reload::HOT_RELOAD_JS,
38            include_str!("reload_script.js")
39        );
40        view! { <script nonce=nonce>{script}</script> }
41    })
42}
43
44/// Inserts hydration scripts that add interactivity to your server-rendered HTML.
45///
46/// This should be included in the `<head>` of your application shell.
47#[component]
48pub fn HydrationScripts(
49    /// Configuration options for this project.
50    options: LeptosOptions,
51    /// Should be `true` to hydrate in `islands` mode.
52    #[prop(optional)]
53    islands: bool,
54    /// Should be `true` to add the “islands router,” which enables limited client-side routing
55    /// when running in islands mode.
56    #[prop(optional)]
57    islands_router: bool,
58    /// A base url, not including a trailing slash
59    #[prop(optional, into)]
60    root: Option<String>,
61) -> impl IntoView {
62    static SPLIT_MANIFEST: OnceLock<Option<WasmSplitManifest>> =
63        OnceLock::new();
64
65    if let Some(splits) = SPLIT_MANIFEST.get_or_init(|| {
66        let root = root.clone().unwrap_or_default();
67
68        let (wasm_split_js, wasm_split_manifest) = if options.hash_files {
69            let hash_path = std::env::current_exe()
70                .map(|path| {
71                    path.parent().map(|p| p.to_path_buf()).unwrap_or_default()
72                })
73                .unwrap_or_default()
74                .join(options.hash_file.as_ref());
75            let hashes = std::fs::read_to_string(&hash_path)
76                .expect("failed to read hash file");
77
78            let mut split =
79                "__wasm_split.______________________.js".to_string();
80            let mut manifest = "__wasm_split_manifest.json".to_string();
81            for line in hashes.lines() {
82                let line = line.trim();
83                if !line.is_empty() {
84                    if let Some((file, hash)) = line.split_once(':') {
85                        if file == "manifest" {
86                            manifest.clear();
87                            manifest.push_str("__wasm_split_manifest.");
88                            manifest.push_str(hash.trim());
89                            manifest.push_str(".json");
90                        }
91                        if file == "split" {
92                            split.clear();
93                            split.push_str("__wasm_split.");
94                            split.push_str(hash.trim());
95                            split.push_str(".js");
96                        }
97                    }
98                }
99            }
100            (split, manifest)
101        } else {
102            (
103                "__wasm_split.______________________.js".to_string(),
104                "__wasm_split_manifest.json".to_string(),
105            )
106        };
107
108        let site_dir = &options.site_root;
109        let pkg_dir = &options.site_pkg_dir;
110        let path = PathBuf::from(site_dir.to_string());
111        let path = path.join(pkg_dir.to_string()).join(wasm_split_manifest);
112        let file = std::fs::read_to_string(path).ok()?;
113
114        let manifest = WasmSplitManifest(ArcStoredValue::new((
115            format!("{root}/{pkg_dir}"),
116            serde_json::from_str(&file).expect("could not read manifest file"),
117            wasm_split_js,
118        )));
119
120        Some(manifest)
121    }) {
122        provide_context(splits.clone());
123    }
124
125    let mut js_file_name = options.output_name.to_string();
126    let mut wasm_file_name = options.output_name.to_string();
127    if options.hash_files {
128        let hash_path = std::env::current_exe()
129            .map(|path| {
130                path.parent().map(|p| p.to_path_buf()).unwrap_or_default()
131            })
132            .unwrap_or_default()
133            .join(options.hash_file.as_ref());
134        if hash_path.exists() {
135            let hashes = std::fs::read_to_string(&hash_path)
136                .expect("failed to read hash file");
137            for line in hashes.lines() {
138                let line = line.trim();
139                if !line.is_empty() {
140                    if let Some((file, hash)) = line.split_once(':') {
141                        if file == "js" {
142                            js_file_name.push_str(&format!(".{}", hash.trim()));
143                        } else if file == "wasm" {
144                            wasm_file_name
145                                .push_str(&format!(".{}", hash.trim()));
146                        }
147                    }
148                }
149            }
150        } else {
151            leptos::logging::error!(
152                "File hashing is active but no hash file was found"
153            );
154        }
155    } else if std::option_env!("LEPTOS_OUTPUT_NAME").is_none() {
156        wasm_file_name.push_str("_bg");
157    }
158
159    let pkg_path = &options.site_pkg_dir;
160    #[cfg(feature = "nonce")]
161    let nonce = crate::nonce::use_nonce();
162    #[cfg(not(feature = "nonce"))]
163    let nonce = None::<String>;
164    let script = if islands {
165        if let Some(sc) = Owner::current_shared_context() {
166            sc.set_is_hydrating(false);
167        }
168        include_str!("./island_script.js")
169    } else {
170        include_str!("./hydration_script.js")
171    };
172
173    let islands_router = islands_router
174        .then_some(include_str!("./islands_routing.js"))
175        .unwrap_or_default();
176
177    let root = root.unwrap_or_default();
178    view! {
179        <link rel="modulepreload" href=format!("{root}/{pkg_path}/{js_file_name}.js") crossorigin=nonce.clone()/>
180        <link
181            rel="preload"
182            href=format!("{root}/{pkg_path}/{wasm_file_name}.wasm")
183            r#as="fetch"
184            r#type="application/wasm"
185            crossorigin=nonce.clone().unwrap_or_default()
186        />
187        <script type="module" nonce=nonce>
188            {format!("{script}({root:?}, {pkg_path:?}, {js_file_name:?}, {wasm_file_name:?});{islands_router}")}
189        </script>
190    }
191}
192
193/// If this is provided via context, it means that you are using the islands router and
194/// this is a subsequent navigation, made from the client.
195///
196/// This should be provided automatically by a server integration if it detects that the
197/// header `Islands-Router` is present in the request.
198///
199/// This is used to determine how much of the hydration script to include in the page.
200/// If it is present, then the contents of the `<HydrationScripts>` component will not be
201/// included, as they only need to be sent to the client once.
202#[derive(Debug, Clone, PartialEq, Eq)]
203pub struct IslandsRouterNavigation;