feat: add support for adding CSP nonces (#1348)
This commit is contained in:
parent
77580401da
commit
8e68699435
|
@ -19,3 +19,6 @@ serde_json = "1"
|
|||
parking_lot = "0.12.1"
|
||||
regex = "1.7.0"
|
||||
tracing = "0.1.37"
|
||||
|
||||
[features]
|
||||
nonce = ["leptos/nonce"]
|
||||
|
|
|
@ -733,6 +733,8 @@ fn provide_contexts(
|
|||
provide_context(cx, res_options);
|
||||
provide_context(cx, req.clone());
|
||||
provide_server_redirect(cx, move |path| redirect(cx, path));
|
||||
#[cfg(feature = "nonce")]
|
||||
leptos::nonce::provide_nonce(cx);
|
||||
}
|
||||
|
||||
fn leptos_corrected_path(req: &HttpRequest) -> String {
|
||||
|
@ -797,8 +799,11 @@ async fn build_stream_response(
|
|||
// wait for any blocking resources to load before pulling metadata
|
||||
let first_app_chunk = stream.next().await.unwrap_or_default();
|
||||
|
||||
let (head, tail) =
|
||||
html_parts_separated(options, use_context::<MetaContext>(cx).as_ref());
|
||||
let (head, tail) = html_parts_separated(
|
||||
cx,
|
||||
options,
|
||||
use_context::<MetaContext>(cx).as_ref(),
|
||||
);
|
||||
|
||||
let mut stream = Box::pin(
|
||||
futures::stream::once(async move { head.clone() })
|
||||
|
|
|
@ -19,6 +19,9 @@ leptos_integration_utils = { workspace = true }
|
|||
serde_json = "1"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
parking_lot = "0.12.1"
|
||||
tokio-util = {version = "0.7.7", features = ["rt"] }
|
||||
tokio-util = { version = "0.7.7", features = ["rt"] }
|
||||
tracing = "0.1.37"
|
||||
once_cell = "1.17"
|
||||
|
||||
[features]
|
||||
nonce = ["leptos/nonce"]
|
||||
|
|
|
@ -693,8 +693,11 @@ async fn forward_stream(
|
|||
let mut shell = Box::pin(bundle);
|
||||
let first_app_chunk = shell.next().await.unwrap_or_default();
|
||||
|
||||
let (head, tail) =
|
||||
html_parts_separated(options, use_context::<MetaContext>(cx).as_ref());
|
||||
let (head, tail) = html_parts_separated(
|
||||
cx,
|
||||
options,
|
||||
use_context::<MetaContext>(cx).as_ref(),
|
||||
);
|
||||
|
||||
_ = tx.send(head).await;
|
||||
_ = tx.send(first_app_chunk).await;
|
||||
|
@ -828,6 +831,8 @@ fn provide_contexts(
|
|||
provide_context(cx, extractor);
|
||||
provide_context(cx, default_res_options);
|
||||
provide_server_redirect(cx, move |path| redirect(cx, path));
|
||||
#[cfg(feature = "nonce")]
|
||||
leptos::nonce::provide_nonce(cx);
|
||||
}
|
||||
|
||||
/// Returns an Axum [Handler](axum::handler::Handler) that listens for a `GET` request and tries
|
||||
|
|
|
@ -1,18 +1,18 @@
|
|||
use futures::{Stream, StreamExt};
|
||||
use leptos::{use_context, RuntimeId, ScopeId};
|
||||
use leptos::{nonce::use_nonce, use_context, RuntimeId, Scope, ScopeId};
|
||||
use leptos_config::LeptosOptions;
|
||||
use leptos_meta::MetaContext;
|
||||
|
||||
extern crate tracing;
|
||||
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
fn autoreload(options: &LeptosOptions) -> String {
|
||||
fn autoreload(nonce_str: &str, options: &LeptosOptions) -> String {
|
||||
let site_ip = &options.site_addr.ip().to_string();
|
||||
let reload_port = options.reload_port;
|
||||
match std::env::var("LEPTOS_WATCH").is_ok() {
|
||||
true => format!(
|
||||
r#"
|
||||
<script crossorigin="">(function () {{
|
||||
<script crossorigin=""{nonce_str}>(function () {{
|
||||
{}
|
||||
var ws = new WebSocket('ws://{site_ip}:{reload_port}/live_reload');
|
||||
ws.onmessage = (ev) => {{
|
||||
|
@ -42,6 +42,8 @@ fn autoreload(options: &LeptosOptions) -> String {
|
|||
false => "".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
#[deprecated = "Use html_parts_separated."]
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
pub fn html_parts(
|
||||
options: &LeptosOptions,
|
||||
|
@ -58,7 +60,7 @@ pub fn html_parts(
|
|||
wasm_output_name.push_str("_bg");
|
||||
}
|
||||
|
||||
let leptos_autoreload = autoreload(options);
|
||||
let leptos_autoreload = autoreload("".into(), options);
|
||||
|
||||
let html_metadata =
|
||||
meta.and_then(|mc| mc.html.as_string()).unwrap_or_default();
|
||||
|
@ -80,11 +82,17 @@ pub fn html_parts(
|
|||
|
||||
#[tracing::instrument(level = "trace", fields(error), skip_all)]
|
||||
pub fn html_parts_separated(
|
||||
cx: Scope,
|
||||
options: &LeptosOptions,
|
||||
meta: Option<&MetaContext>,
|
||||
) -> (String, &'static str) {
|
||||
let pkg_path = &options.site_pkg_dir;
|
||||
let output_name = &options.output_name;
|
||||
let nonce = use_nonce(cx);
|
||||
let nonce = nonce
|
||||
.as_ref()
|
||||
.map(|nonce| format!(" nonce=\"{nonce}\""))
|
||||
.unwrap_or_default();
|
||||
|
||||
// Because wasm-pack adds _bg to the end of the WASM filename, and we want to mantain compatibility with it's default options
|
||||
// we add _bg to the wasm files if cargo-leptos doesn't set the env var LEPTOS_OUTPUT_NAME at compile time
|
||||
|
@ -94,7 +102,7 @@ pub fn html_parts_separated(
|
|||
wasm_output_name.push_str("_bg");
|
||||
}
|
||||
|
||||
let leptos_autoreload = autoreload(options);
|
||||
let leptos_autoreload = autoreload(&nonce, options);
|
||||
|
||||
let html_metadata =
|
||||
meta.and_then(|mc| mc.html.as_string()).unwrap_or_default();
|
||||
|
@ -109,9 +117,9 @@ pub fn html_parts_separated(
|
|||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
{head}
|
||||
<link rel="modulepreload" href="/{pkg_path}/{output_name}.js">
|
||||
<link rel="preload" href="/{pkg_path}/{wasm_output_name}.wasm" as="fetch" type="application/wasm" crossorigin="">
|
||||
<script type="module">import init, {{ hydrate }} from '/{pkg_path}/{output_name}.js'; init('/{pkg_path}/{wasm_output_name}.wasm').then(hydrate);</script>
|
||||
<link rel="modulepreload" href="/{pkg_path}/{output_name}.js"{nonce}>
|
||||
<link rel="preload" href="/{pkg_path}/{wasm_output_name}.wasm" as="fetch" type="application/wasm" crossorigin=""{nonce}>
|
||||
<script type="module"{nonce}>import init, {{ hydrate }} from '/{pkg_path}/{output_name}.js'; init('/{pkg_path}/{wasm_output_name}.wasm').then(hydrate);</script>
|
||||
{leptos_autoreload}
|
||||
"#
|
||||
);
|
||||
|
@ -133,8 +141,11 @@ pub async fn build_async_response(
|
|||
}
|
||||
|
||||
let cx = leptos::Scope { runtime, id: scope };
|
||||
let (head, tail) =
|
||||
html_parts_separated(options, use_context::<MetaContext>(cx).as_ref());
|
||||
let (head, tail) = html_parts_separated(
|
||||
cx,
|
||||
options,
|
||||
use_context::<MetaContext>(cx).as_ref(),
|
||||
);
|
||||
|
||||
// in async, we load the meta content *now*, after the suspenses have resolved
|
||||
let meta = use_context::<MetaContext>(cx);
|
||||
|
|
|
@ -19,3 +19,6 @@ leptos_integration_utils = { workspace = true }
|
|||
serde_json = "1"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
parking_lot = "0.12.1"
|
||||
|
||||
[features]
|
||||
nonce = ["leptos/nonce"]
|
||||
|
|
|
@ -655,8 +655,11 @@ async fn forward_stream(
|
|||
mut tx: Sender<String>,
|
||||
) {
|
||||
let cx = Scope { runtime, id: scope };
|
||||
let (head, tail) =
|
||||
html_parts_separated(options, use_context::<MetaContext>(cx).as_ref());
|
||||
let (head, tail) = html_parts_separated(
|
||||
cx,
|
||||
options,
|
||||
use_context::<MetaContext>(cx).as_ref(),
|
||||
);
|
||||
|
||||
_ = tx.send(head).await;
|
||||
let mut shell = Box::pin(bundle);
|
||||
|
@ -793,6 +796,8 @@ fn provide_contexts(
|
|||
provide_context(cx, req_parts);
|
||||
provide_context(cx, default_res_options);
|
||||
provide_server_redirect(cx, move |path| redirect(cx, path));
|
||||
#[cfg(feature = "nonce")]
|
||||
leptos::nonce::provide_nonce(cx);
|
||||
}
|
||||
|
||||
/// Returns a Viz [Handler](viz::Handler) that listens for a `GET` request and tries
|
||||
|
|
|
@ -58,6 +58,7 @@ serde-lite = ["leptos_reactive/serde-lite"]
|
|||
miniserde = ["leptos_reactive/miniserde"]
|
||||
rkyv = ["leptos_reactive/rkyv"]
|
||||
tracing = ["leptos_macro/tracing"]
|
||||
nonce = ["leptos_dom/nonce"]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = [
|
||||
|
|
|
@ -164,9 +164,10 @@ pub use leptos_dom::{
|
|||
set_interval_with_handle, set_timeout, set_timeout_with_handle,
|
||||
window_event_listener, window_event_listener_untyped,
|
||||
},
|
||||
html, log, math, mount_to, mount_to_body, svg, warn, window, Attribute,
|
||||
Class, CollectView, Errors, Fragment, HtmlElement, IntoAttribute,
|
||||
IntoClass, IntoProperty, IntoStyle, IntoView, NodeRef, Property, View,
|
||||
html, log, math, mount_to, mount_to_body, nonce, svg, warn, window,
|
||||
Attribute, Class, CollectView, Errors, Fragment, HtmlElement,
|
||||
IntoAttribute, IntoClass, IntoProperty, IntoStyle, IntoView, NodeRef,
|
||||
Property, View,
|
||||
};
|
||||
|
||||
/// Types to make it easier to handle errors in your application.
|
||||
|
|
|
@ -9,10 +9,12 @@ description = "DOM operations for the Leptos web framework."
|
|||
|
||||
[dependencies]
|
||||
async-recursion = "1"
|
||||
base64 = { version = "0.21", optional = true }
|
||||
cfg-if = "1"
|
||||
drain_filter_polyfill = "0.1"
|
||||
educe = "0.4"
|
||||
futures = "0.3"
|
||||
getrandom = { version = "0.2", optional = true }
|
||||
html-escape = "0.2"
|
||||
indexmap = "2"
|
||||
itertools = "0.10"
|
||||
|
@ -22,6 +24,7 @@ server_fn = { workspace = true }
|
|||
once_cell = "1"
|
||||
pad-adapter = "0.1"
|
||||
paste = "1"
|
||||
rand = { version = "0.8", optional = true }
|
||||
rustc-hash = "1.1.0"
|
||||
serde_json = "1"
|
||||
smallvec = "1"
|
||||
|
@ -29,6 +32,9 @@ tracing = "0.1"
|
|||
wasm-bindgen = { version = "0.2", features = ["enable-interning"] }
|
||||
wasm-bindgen-futures = "0.4.31"
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
getrandom = { version = "0.2", features = ["js"] }
|
||||
|
||||
[dev-dependencies]
|
||||
leptos = { path = "../leptos" }
|
||||
|
||||
|
@ -161,6 +167,7 @@ default = []
|
|||
web = ["leptos_reactive/csr"]
|
||||
ssr = ["leptos_reactive/ssr"]
|
||||
nightly = ["leptos_reactive/nightly"]
|
||||
nonce = ["dep:base64", "dep:getrandom", "dep:rand"]
|
||||
|
||||
[package.metadata.cargo-all-features]
|
||||
denylist = ["nightly"]
|
||||
|
|
|
@ -18,6 +18,8 @@ mod logging;
|
|||
mod macro_helpers;
|
||||
pub mod math;
|
||||
mod node_ref;
|
||||
/// Utilities for exporting nonces to be used for a Content Security Policy.
|
||||
pub mod nonce;
|
||||
pub mod ssr;
|
||||
pub mod ssr_in_order;
|
||||
pub mod svg;
|
||||
|
|
|
@ -0,0 +1,158 @@
|
|||
use crate::{Attribute, IntoAttribute};
|
||||
use leptos_reactive::{use_context, Scope};
|
||||
use std::{fmt::Display, ops::Deref};
|
||||
|
||||
/// A nonce a cryptographic nonce ("number used once") which can be
|
||||
/// used by Content Security Policy to determine whether or not a given
|
||||
/// resource will be allowed to load.
|
||||
///
|
||||
/// When the `nonce` feature is enabled on one of the server integrations,
|
||||
/// a nonce is generated during server rendering and added to all inline
|
||||
/// scripts used for HTML streaming and resource loading.
|
||||
///
|
||||
/// The nonce being used during the current server response can be
|
||||
/// accessed using [`use_nonce`](use_nonce).
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// #[component]
|
||||
/// pub fn App(cx: Scope) -> impl IntoView {
|
||||
/// provide_meta_context(cx);
|
||||
///
|
||||
/// view! { cx,
|
||||
/// // use `leptos_meta` to insert a <meta> tag with the CSP
|
||||
/// <Meta
|
||||
/// http_equiv="Content-Security-Policy"
|
||||
/// content=move || {
|
||||
/// // this will insert the CSP with nonce on the server, be empty on client
|
||||
/// use_nonce(cx)
|
||||
/// .map(|nonce| {
|
||||
/// format!(
|
||||
/// "default-src 'self'; script-src 'strict-dynamic' 'nonce-{nonce}' \
|
||||
/// 'wasm-unsafe-eval'; style-src 'nonce-{nonce}';"
|
||||
/// )
|
||||
/// })
|
||||
/// .unwrap_or_default()
|
||||
/// }
|
||||
/// />
|
||||
/// // manually insert nonce during SSR on inline script
|
||||
/// <script nonce=use_nonce(cx)>"console.log('Hello, world!');"</script>
|
||||
/// // leptos_meta <Style/> and <Script/> automatically insert the nonce
|
||||
/// <Style>"body { color: blue; }"</Style>
|
||||
/// <p>"Test"</p>
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct Nonce(pub(crate) String);
|
||||
|
||||
impl Deref for Nonce {
|
||||
type Target = str;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Nonce {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoAttribute for Nonce {
|
||||
fn into_attribute(self, _cx: Scope) -> Attribute {
|
||||
Attribute::String(self.0.into())
|
||||
}
|
||||
|
||||
fn into_attribute_boxed(self: Box<Self>, _cx: Scope) -> Attribute {
|
||||
Attribute::String(self.0.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoAttribute for Option<Nonce> {
|
||||
fn into_attribute(self, cx: Scope) -> Attribute {
|
||||
Attribute::Option(cx, self.map(|n| n.0.into()))
|
||||
}
|
||||
|
||||
fn into_attribute_boxed(self: Box<Self>, cx: Scope) -> Attribute {
|
||||
Attribute::Option(cx, self.map(|n| n.0.into()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Accesses the nonce that has been generated during the current
|
||||
/// server response. This can be added to inline `<script>` and
|
||||
/// `<style>` tags for compatibility with a Content Security Policy.
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// #[component]
|
||||
/// pub fn App(cx: Scope) -> impl IntoView {
|
||||
/// provide_meta_context(cx);
|
||||
///
|
||||
/// view! { cx,
|
||||
/// // use `leptos_meta` to insert a <meta> tag with the CSP
|
||||
/// <Meta
|
||||
/// http_equiv="Content-Security-Policy"
|
||||
/// content=move || {
|
||||
/// // this will insert the CSP with nonce on the server, be empty on client
|
||||
/// use_nonce(cx)
|
||||
/// .map(|nonce| {
|
||||
/// format!(
|
||||
/// "default-src 'self'; script-src 'strict-dynamic' 'nonce-{nonce}' \
|
||||
/// 'wasm-unsafe-eval'; style-src 'nonce-{nonce}';"
|
||||
/// )
|
||||
/// })
|
||||
/// .unwrap_or_default()
|
||||
/// }
|
||||
/// />
|
||||
/// // manually insert nonce during SSR on inline script
|
||||
/// <script nonce=use_nonce(cx)>"console.log('Hello, world!');"</script>
|
||||
/// // leptos_meta <Style/> and <Script/> automatically insert the nonce
|
||||
/// <Style>"body { color: blue; }"</Style>
|
||||
/// <p>"Test"</p>
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
pub fn use_nonce(cx: Scope) -> Option<Nonce> {
|
||||
use_context::<Nonce>(cx)
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "ssr", feature = "nonce"))]
|
||||
pub use generate::*;
|
||||
|
||||
#[cfg(all(feature = "ssr", feature = "nonce"))]
|
||||
mod generate {
|
||||
use super::Nonce;
|
||||
use base64::{
|
||||
alphabet,
|
||||
engine::{self, general_purpose},
|
||||
Engine,
|
||||
};
|
||||
use leptos_reactive::{provide_context, Scope};
|
||||
use rand::{thread_rng, RngCore};
|
||||
|
||||
const NONCE_ENGINE: engine::GeneralPurpose = engine::GeneralPurpose::new(
|
||||
&alphabet::URL_SAFE,
|
||||
general_purpose::NO_PAD,
|
||||
);
|
||||
|
||||
impl Nonce {
|
||||
/// Generates a new nonce from 16 bytes (128 bits) of random data.
|
||||
pub fn new() -> Self {
|
||||
let mut thread_rng = thread_rng();
|
||||
let mut bytes = [0; 16];
|
||||
thread_rng.fill_bytes(&mut bytes);
|
||||
Nonce(NONCE_ENGINE.encode(bytes))
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Nonce {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates a nonce and provides it during server rendering.
|
||||
pub fn provide_nonce(cx: Scope) {
|
||||
provide_context(cx, Nonce::new())
|
||||
}
|
||||
}
|
|
@ -212,6 +212,9 @@ pub fn render_to_stream_with_prefix_undisposed_with_context_and_block_replacemen
|
|||
}
|
||||
});
|
||||
let cx = Scope { runtime, id: scope };
|
||||
let nonce_str = crate::nonce::use_nonce(cx)
|
||||
.map(|nonce| format!(" nonce=\"{nonce}\""))
|
||||
.unwrap_or_default();
|
||||
|
||||
let mut blocking_fragments = FuturesUnordered::new();
|
||||
let fragments = FuturesUnordered::new();
|
||||
|
@ -230,91 +233,110 @@ pub fn render_to_stream_with_prefix_undisposed_with_context_and_block_replacemen
|
|||
|
||||
let stream = futures::stream::once(
|
||||
// HTML for the view function and script to store resources
|
||||
async move {
|
||||
let resolvers = format!(
|
||||
"<script>__LEPTOS_PENDING_RESOURCES = \
|
||||
{pending_resources};__LEPTOS_RESOLVED_RESOURCES = new \
|
||||
Map();__LEPTOS_RESOURCE_RESOLVERS = new Map();</script>"
|
||||
);
|
||||
{
|
||||
let nonce_str = nonce_str.clone();
|
||||
async move {
|
||||
let resolvers = format!(
|
||||
"<script{nonce_str}>__LEPTOS_PENDING_RESOURCES = \
|
||||
{pending_resources};__LEPTOS_RESOLVED_RESOURCES = new \
|
||||
Map();__LEPTOS_RESOURCE_RESOLVERS = new Map();</script>"
|
||||
);
|
||||
|
||||
if replace_blocks {
|
||||
let mut blocks = Vec::with_capacity(blocking_fragments.len());
|
||||
while let Some((blocked_id, blocked_fragment)) =
|
||||
blocking_fragments.next().await
|
||||
{
|
||||
blocks.push((blocked_id, blocked_fragment));
|
||||
if replace_blocks {
|
||||
let mut blocks =
|
||||
Vec::with_capacity(blocking_fragments.len());
|
||||
while let Some((blocked_id, blocked_fragment)) =
|
||||
blocking_fragments.next().await
|
||||
{
|
||||
blocks.push((blocked_id, blocked_fragment));
|
||||
}
|
||||
|
||||
let prefix = prefix(cx);
|
||||
|
||||
let mut shell = shell;
|
||||
|
||||
for (blocked_id, blocked_fragment) in blocks {
|
||||
let open = format!("<!--suspense-open-{blocked_id}-->");
|
||||
let close =
|
||||
format!("<!--suspense-close-{blocked_id}-->");
|
||||
let (first, rest) =
|
||||
shell.split_once(&open).unwrap_or_default();
|
||||
let (_fallback, rest) =
|
||||
rest.split_once(&close).unwrap_or_default();
|
||||
|
||||
shell =
|
||||
format!("{first}{blocked_fragment}{rest}").into();
|
||||
}
|
||||
|
||||
format!("{prefix}{shell}{resolvers}")
|
||||
} else {
|
||||
let mut blocking = String::new();
|
||||
let mut blocking_fragments = fragments_to_chunks(
|
||||
nonce_str.clone(),
|
||||
blocking_fragments,
|
||||
);
|
||||
|
||||
while let Some(fragment) = blocking_fragments.next().await {
|
||||
blocking.push_str(&fragment);
|
||||
}
|
||||
let prefix = prefix(cx);
|
||||
format!("{prefix}{shell}{resolvers}{blocking}")
|
||||
}
|
||||
|
||||
let prefix = prefix(cx);
|
||||
|
||||
let mut shell = shell;
|
||||
|
||||
for (blocked_id, blocked_fragment) in blocks {
|
||||
let open = format!("<!--suspense-open-{blocked_id}-->");
|
||||
let close = format!("<!--suspense-close-{blocked_id}-->");
|
||||
let (first, rest) =
|
||||
shell.split_once(&open).unwrap_or_default();
|
||||
let (_fallback, rest) =
|
||||
rest.split_once(&close).unwrap_or_default();
|
||||
|
||||
shell = format!("{first}{blocked_fragment}{rest}").into();
|
||||
}
|
||||
|
||||
format!("{prefix}{shell}{resolvers}")
|
||||
} else {
|
||||
let mut blocking = String::new();
|
||||
let mut blocking_fragments =
|
||||
fragments_to_chunks(blocking_fragments);
|
||||
|
||||
while let Some(fragment) = blocking_fragments.next().await {
|
||||
blocking.push_str(&fragment);
|
||||
}
|
||||
let prefix = prefix(cx);
|
||||
format!("{prefix}{shell}{resolvers}{blocking}")
|
||||
}
|
||||
},
|
||||
)
|
||||
.chain(ooo_body_stream_recurse(cx, fragments, serializers));
|
||||
.chain(ooo_body_stream_recurse(
|
||||
cx,
|
||||
nonce_str,
|
||||
fragments,
|
||||
serializers,
|
||||
));
|
||||
|
||||
(stream, runtime, scope)
|
||||
}
|
||||
|
||||
fn ooo_body_stream_recurse(
|
||||
cx: Scope,
|
||||
nonce_str: String,
|
||||
fragments: FuturesUnordered<PinnedFuture<(String, String)>>,
|
||||
serializers: FuturesUnordered<PinnedFuture<(ResourceId, String)>>,
|
||||
) -> Pin<Box<dyn Stream<Item = String>>> {
|
||||
// resources and fragments
|
||||
// stream HTML for each <Suspense/> as it resolves
|
||||
let fragments = fragments_to_chunks(fragments);
|
||||
let fragments = fragments_to_chunks(nonce_str.clone(), fragments);
|
||||
// stream data for each Resource as it resolves
|
||||
let resources = render_serializers(serializers);
|
||||
let resources = render_serializers(nonce_str.clone(), serializers);
|
||||
|
||||
Box::pin(
|
||||
// TODO these should be combined again in a way that chains them appropriately
|
||||
// such that individual resources can resolve before all fragments are done
|
||||
fragments.chain(resources).chain(
|
||||
futures::stream::once(async move {
|
||||
let pending = cx.pending_fragments();
|
||||
if !pending.is_empty() {
|
||||
let fragments = FuturesUnordered::new();
|
||||
let serializers = cx.serialization_resolvers();
|
||||
for (fragment_id, data) in pending {
|
||||
fragments.push(Box::pin(async move {
|
||||
(fragment_id.clone(), data.out_of_order.await)
|
||||
})
|
||||
as Pin<Box<dyn Future<Output = (String, String)>>>);
|
||||
futures::stream::once({
|
||||
async move {
|
||||
let pending = cx.pending_fragments();
|
||||
if !pending.is_empty() {
|
||||
let fragments = FuturesUnordered::new();
|
||||
let serializers = cx.serialization_resolvers();
|
||||
for (fragment_id, data) in pending {
|
||||
fragments.push(Box::pin(async move {
|
||||
(fragment_id.clone(), data.out_of_order.await)
|
||||
})
|
||||
as Pin<
|
||||
Box<dyn Future<Output = (String, String)>>,
|
||||
>);
|
||||
}
|
||||
Box::pin(ooo_body_stream_recurse(
|
||||
cx,
|
||||
nonce_str.clone(),
|
||||
fragments,
|
||||
serializers,
|
||||
))
|
||||
as Pin<Box<dyn Stream<Item = String>>>
|
||||
} else {
|
||||
Box::pin(futures::stream::once(async move {
|
||||
Default::default()
|
||||
}))
|
||||
}
|
||||
Box::pin(ooo_body_stream_recurse(
|
||||
cx,
|
||||
fragments,
|
||||
serializers,
|
||||
))
|
||||
as Pin<Box<dyn Stream<Item = String>>>
|
||||
} else {
|
||||
Box::pin(futures::stream::once(async move {
|
||||
Default::default()
|
||||
}))
|
||||
}
|
||||
})
|
||||
.flatten(),
|
||||
|
@ -327,13 +349,14 @@ fn ooo_body_stream_recurse(
|
|||
instrument(level = "trace", skip_all,)
|
||||
)]
|
||||
fn fragments_to_chunks(
|
||||
nonce_str: String,
|
||||
fragments: impl Stream<Item = (String, String)>,
|
||||
) -> impl Stream<Item = String> {
|
||||
fragments.map(|(fragment_id, html)| {
|
||||
fragments.map(move |(fragment_id, html)| {
|
||||
format!(
|
||||
r#"
|
||||
<template id="{fragment_id}f">{html}</template>
|
||||
<script>
|
||||
<script{nonce_str}>
|
||||
var id = "{fragment_id}";
|
||||
var open = undefined;
|
||||
var close = undefined;
|
||||
|
@ -679,13 +702,14 @@ pub(crate) fn to_kebab_case(name: &str) -> String {
|
|||
instrument(level = "trace", skip_all,)
|
||||
)]
|
||||
pub(crate) fn render_serializers(
|
||||
nonce_str: String,
|
||||
serializers: FuturesUnordered<PinnedFuture<(ResourceId, String)>>,
|
||||
) -> impl Stream<Item = String> {
|
||||
serializers.map(|(id, json)| {
|
||||
serializers.map(move |(id, json)| {
|
||||
let id = serde_json::to_string(&id).unwrap();
|
||||
let json = json.replace('<', "\\u003c");
|
||||
format!(
|
||||
r#"<script>
|
||||
r#"<script{nonce_str}>
|
||||
var val = {json:?};
|
||||
if(__LEPTOS_RESOURCE_RESOLVERS.get({id})) {{
|
||||
__LEPTOS_RESOURCE_RESOLVERS.get({id})(val)
|
||||
|
|
|
@ -124,24 +124,33 @@ pub fn render_to_stream_in_order_with_prefix_undisposed_with_context(
|
|||
handle_chunks(cx, tx, remaining_chunks).await;
|
||||
});
|
||||
|
||||
let stream = futures::stream::once(async move {
|
||||
let prefix = prefix_rx.await.expect("to receive prefix");
|
||||
format!(
|
||||
r#"
|
||||
let nonce = crate::nonce::use_nonce(cx);
|
||||
let nonce_str = nonce
|
||||
.as_ref()
|
||||
.map(|nonce| format!(" nonce=\"{nonce}\""))
|
||||
.unwrap_or_default();
|
||||
|
||||
let stream = futures::stream::once({
|
||||
let nonce_str = nonce_str.clone();
|
||||
async move {
|
||||
let prefix = prefix_rx.await.expect("to receive prefix");
|
||||
format!(
|
||||
r#"
|
||||
{prefix}
|
||||
<script>
|
||||
<script{nonce_str}>
|
||||
__LEPTOS_PENDING_RESOURCES = {pending_resources};
|
||||
__LEPTOS_RESOLVED_RESOURCES = new Map();
|
||||
__LEPTOS_RESOURCE_RESOLVERS = new Map();
|
||||
</script>
|
||||
"#
|
||||
)
|
||||
)
|
||||
}
|
||||
})
|
||||
.chain(rx)
|
||||
.chain(
|
||||
futures::stream::once(async move {
|
||||
let serializers = cx.serialization_resolvers();
|
||||
render_serializers(serializers)
|
||||
render_serializers(nonce_str, serializers)
|
||||
})
|
||||
.flatten(),
|
||||
);
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use crate::use_head;
|
||||
use leptos::*;
|
||||
use leptos::{nonce::use_nonce, *};
|
||||
use std::borrow::Cow;
|
||||
|
||||
/// Injects an [HTMLLinkElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLLinkElement) into the document
|
||||
|
@ -109,6 +109,7 @@ pub fn Link(
|
|||
.attr("title", title)
|
||||
.attr("type", type_)
|
||||
.attr("blocking", blocking)
|
||||
.attr("nonce", use_nonce(cx))
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use crate::use_head;
|
||||
use leptos::*;
|
||||
use leptos::{nonce::use_nonce, *};
|
||||
use std::borrow::Cow;
|
||||
|
||||
/// Injects an [HTMLScriptElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLScriptElement) into the document
|
||||
|
@ -85,6 +85,7 @@ pub fn Script(
|
|||
.attr("src", src)
|
||||
.attr("type", type_)
|
||||
.attr("blocking", blocking)
|
||||
.attr("nonce", use_nonce(cx))
|
||||
}
|
||||
});
|
||||
let builder_el = if let Some(children) = children {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use crate::use_head;
|
||||
use leptos::*;
|
||||
use leptos::{nonce::use_nonce, *};
|
||||
use std::borrow::Cow;
|
||||
|
||||
/// Injects an [HTMLStyleElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLStyleElement) into the document
|
||||
|
@ -57,6 +57,7 @@ pub fn Style(
|
|||
.attr("nonce", nonce)
|
||||
.attr("title", title)
|
||||
.attr("blocking", blocking)
|
||||
.attr("nonce", use_nonce(cx))
|
||||
}
|
||||
});
|
||||
let builder_el = if let Some(children) = children {
|
||||
|
|
Loading…
Reference in New Issue