feat(axum): expose hashed runtime asset path
Generate a SHA-256 content hash for the embedded hemx runtime, expose the hashed path from hemx-axum, serve immutable runtime headers, and update examples to load the hemx-owned path instead of fixed /hemx.js URLs. req: axum_integration/005
This commit is contained in:
@@ -5,9 +5,9 @@ pub mod ui {}
|
||||
mod tests {
|
||||
use super::ui::{board, board_card};
|
||||
use hemplate::Hemplate;
|
||||
use scraper::{Html, Selector};
|
||||
use hemx::IntoEffect;
|
||||
use hemx_test::inspect;
|
||||
use scraper::{Html, Selector};
|
||||
|
||||
#[derive(Hemplate)]
|
||||
#[hemplate = "partials"]
|
||||
|
||||
@@ -6,8 +6,8 @@ use futures_util::{stream, StreamExt};
|
||||
use hemplate::Hemplate;
|
||||
use hemx::{Html, IntoEffect};
|
||||
use hemx_axum::{
|
||||
interactions, runtime_js, sse, DispatchRegistry, DispatchRejection, EffectResponse,
|
||||
InteractionRequest, PageRequest,
|
||||
interactions, runtime_js, runtime_js_path, sse, DispatchRegistry, DispatchRejection,
|
||||
EffectResponse, InteractionRequest, PageRequest,
|
||||
};
|
||||
use hemx_kanban_example::ui::board::{self as board};
|
||||
use hemx_kanban_example::ui::board_card as card_board;
|
||||
@@ -40,6 +40,7 @@ struct Card {
|
||||
|
||||
#[derive(Hemplate)]
|
||||
struct AppShell {
|
||||
runtime_src: &'static str,
|
||||
body: Html,
|
||||
}
|
||||
|
||||
@@ -118,7 +119,7 @@ async fn main() {
|
||||
let app = Router::new()
|
||||
.route("/", get(home).post(interact))
|
||||
.route("/events", get(events))
|
||||
.route("/hemx.js", get(runtime))
|
||||
.route(runtime_js_path(), get(runtime))
|
||||
.with_state(state);
|
||||
|
||||
let addr = SocketAddr::from(([127, 0, 0, 1], 3001));
|
||||
@@ -288,7 +289,10 @@ fn page_html(board: &BoardState) -> Html {
|
||||
|
||||
fn shell(body: Html) -> Html {
|
||||
// req: html_safety/001 req: html_safety/002 req: axum_integration/001
|
||||
hemx::page(&AppShell { body })
|
||||
hemx::page(&AppShell {
|
||||
runtime_src: runtime_js_path(),
|
||||
body,
|
||||
})
|
||||
}
|
||||
|
||||
fn render_options() -> Html {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>hemx Kanban</title>
|
||||
<script src="/hemx.js" defer></script>
|
||||
<script +src="self.runtime_src" defer></script>
|
||||
<style>
|
||||
body { font-family: system-ui, sans-serif; margin: 2rem; }
|
||||
form { display: flex; gap: .5rem; flex-wrap: wrap; margin: 1rem 0; }
|
||||
|
||||
@@ -4,7 +4,8 @@ pub mod ui {}
|
||||
use hemplate::Hemplate;
|
||||
use hemx::{push, Html, IntoEffect};
|
||||
use hemx_axum::{
|
||||
interactions, Form, HandlerErrorContext, HandlerFailure, IntoHandlerFailure, Registry, State,
|
||||
interactions, runtime_js_path, Form, HandlerErrorContext, HandlerFailure, IntoHandlerFailure,
|
||||
Registry, State,
|
||||
};
|
||||
use std::convert::Infallible;
|
||||
use std::fmt::Display;
|
||||
@@ -248,12 +249,14 @@ impl SettingsPage {
|
||||
#[derive(Hemplate)]
|
||||
pub struct AppShell {
|
||||
title: &'static str,
|
||||
runtime_src: &'static str,
|
||||
body: Html,
|
||||
}
|
||||
|
||||
pub fn home_page(ctx: &AppContext) -> Html {
|
||||
hemx::page(&AppShell {
|
||||
title: "hemx SaaS tutorial",
|
||||
runtime_src: runtime_js_path(),
|
||||
body: hemx::page(&Dashboard::from_context(ctx)),
|
||||
})
|
||||
}
|
||||
@@ -261,6 +264,7 @@ pub fn home_page(ctx: &AppContext) -> Html {
|
||||
pub fn settings_page(ctx: &AppContext) -> Html {
|
||||
hemx::page(&AppShell {
|
||||
title: "hemx SaaS tutorial settings",
|
||||
runtime_src: runtime_js_path(),
|
||||
body: hemx::page(&Dashboard::settings(ctx)),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ use axum::routing::get;
|
||||
use axum::Router;
|
||||
use futures_util::stream;
|
||||
use hemx::IntoEffect;
|
||||
use hemx_axum::{runtime_js, sse, EffectResponse, InteractionRequest};
|
||||
use hemx_axum::{runtime_js, runtime_js_path, sse, EffectResponse, InteractionRequest};
|
||||
use hemx_saas_example::{home_page, live_status, registry, settings_page, ui, AppContext};
|
||||
use std::collections::BTreeMap;
|
||||
use std::convert::Infallible;
|
||||
@@ -26,7 +26,7 @@ fn app(ctx: AppContext) -> Router {
|
||||
.route("/", get(home).post(interact))
|
||||
.route("/settings", get(settings))
|
||||
.route("/events", get(events))
|
||||
.route("/hemx.js", get(runtime))
|
||||
.route(runtime_js_path(), get(runtime))
|
||||
.route("/app.css", get(css))
|
||||
.route("/metrics.js", get(metrics_js))
|
||||
.with_state(ctx)
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{+ self.title +}</title>
|
||||
<link rel="stylesheet" href="/app.css">
|
||||
<script src="/hemx.js" defer></script>
|
||||
<script +src="self.runtime_src" defer></script>
|
||||
<script src="/metrics.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -7,8 +7,8 @@ use futures_util::{stream, StreamExt};
|
||||
use hemplate::Hemplate;
|
||||
use hemx::{CssClass, CssClasses, EventName, Html, IntoEffect};
|
||||
use hemx_axum::{
|
||||
interactions, runtime_js, sse, DispatchRegistry, DispatchRejection, EffectResponse,
|
||||
InteractionRequest, PageRequest,
|
||||
interactions, runtime_js, runtime_js_path, sse, DispatchRegistry, DispatchRejection,
|
||||
EffectResponse, InteractionRequest, PageRequest,
|
||||
};
|
||||
use hemx_techdemo::ui;
|
||||
use hemx_techdemo::ui::control_center::{self as control, classes};
|
||||
@@ -156,6 +156,7 @@ struct ControlCenter {
|
||||
|
||||
#[derive(Hemplate)]
|
||||
struct AppShell {
|
||||
runtime_src: &'static str,
|
||||
body: Html,
|
||||
}
|
||||
|
||||
@@ -226,7 +227,7 @@ async fn main() {
|
||||
.route("/architecture", get(architecture))
|
||||
.route("/events", get(events))
|
||||
.route("/favicon.ico", get(favicon))
|
||||
.route("/hemx.js", get(runtime))
|
||||
.route(runtime_js_path(), get(runtime))
|
||||
.route("/app.css", get(app_css))
|
||||
.route("/control_center.css", get(control_center_css))
|
||||
.route("/island.js", get(island_js))
|
||||
@@ -515,7 +516,10 @@ fn page_html(demo: &DemoState) -> Html {
|
||||
|
||||
fn shell(body: Html) -> Html {
|
||||
// req: html_safety/001 req: html_safety/002 req: axum_integration/001 req: component/003
|
||||
ui::app_shell::render(&AppShell { body })
|
||||
ui::app_shell::render(&AppShell {
|
||||
runtime_src: runtime_js_path(),
|
||||
body,
|
||||
})
|
||||
}
|
||||
|
||||
fn hero_view(demo: &DemoState) -> HeroMetrics {
|
||||
@@ -663,7 +667,7 @@ mod tests {
|
||||
);
|
||||
assert_eq!(
|
||||
document
|
||||
.select(&selector("script[src=\"/hemx.js\"]"))
|
||||
.select(&selector(&format!("script[src=\"{}\"]", runtime_js_path())))
|
||||
.count(),
|
||||
1
|
||||
);
|
||||
@@ -691,17 +695,17 @@ mod tests {
|
||||
.count(),
|
||||
1
|
||||
);
|
||||
assert!(!html.contains("{+="));
|
||||
assert!(!html.as_str().contains("{+="));
|
||||
}
|
||||
|
||||
// req: html_safety/002 req: view/001 req: test/005
|
||||
#[test]
|
||||
fn control_center_page_is_composed_by_hemplate_not_placeholders() {
|
||||
let html = page_html(&DemoState::default());
|
||||
assert!(!html.contains("__HERO__"));
|
||||
assert!(!html.contains("__BOARD__"));
|
||||
assert!(!html.contains("__INSPECTOR__"));
|
||||
assert!(!html.contains("__ACTIVITY__"));
|
||||
assert!(!html.as_str().contains("__HERO__"));
|
||||
assert!(!html.as_str().contains("__BOARD__"));
|
||||
assert!(!html.as_str().contains("__INSPECTOR__"));
|
||||
assert!(!html.as_str().contains("__ACTIVITY__"));
|
||||
|
||||
let document = Html::parse_fragment(html.as_str());
|
||||
assert_eq!(
|
||||
@@ -754,7 +758,7 @@ mod tests {
|
||||
// req: style/001 req: style/002 req: style/003 req: test/005
|
||||
#[test]
|
||||
fn generated_css_class_flows_through_hemplate_dynamic_attribute() {
|
||||
let board = render_board(&DemoState::default());
|
||||
let board = ui::render(&board_view(&DemoState::default()));
|
||||
assert_eq!(classes::lane.as_str(), "lane");
|
||||
assert_eq!(classes::work_card.as_str(), "work-card");
|
||||
assert_eq!(classes::is_selected.as_str(), "is-selected");
|
||||
@@ -797,8 +801,8 @@ mod tests {
|
||||
);
|
||||
assert_eq!(document.select(&selector(".inspector-row")).count(), 3);
|
||||
assert!(document
|
||||
.select(&selector("code"))
|
||||
.any(|code| code.text().collect::<String>().contains("control::*")));
|
||||
.select(&selector(".inspector-row"))
|
||||
.any(|row| row.text().collect::<String>().contains("No selectors")));
|
||||
}
|
||||
|
||||
// req: html_safety/002 req: view/001 req: test/005
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>hemx Techdemo</title>
|
||||
<script src="/hemx.js" defer></script>
|
||||
<script +src="self.runtime_src" defer></script>
|
||||
<script src="/island.js" defer></script>
|
||||
<link rel="stylesheet" href="/app.css">
|
||||
<link rel="stylesheet" href="/control_center.css">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use scraper::{Html, Selector};
|
||||
use hemx_axum::runtime_js_path;
|
||||
use hemx_techdemo::ui::control_center::{self as control, launch_work, reset_demo, simulate_push};
|
||||
use hemx_techdemo::ui::issue_card::{advance_work, delete_work, spotlight_work};
|
||||
use hemx_techdemo::ui::issue_lane::move_to_lane as move_to_lane_handle;
|
||||
@@ -9,6 +9,7 @@ use hemx_test::{
|
||||
root_selector, sse_endpoint_marker, strong_text_selector, unknown_handle_form_body,
|
||||
EffectInspector,
|
||||
};
|
||||
use scraper::{Html, Selector};
|
||||
use std::io::{Read, Write};
|
||||
use std::net::TcpStream;
|
||||
use std::process::{Child, Command, Stdio};
|
||||
@@ -73,7 +74,7 @@ fn product_is_e2e_working_over_http() {
|
||||
let favicon = get("/favicon.ico");
|
||||
assert_eq!(favicon.status, 204);
|
||||
|
||||
let runtime = get("/hemx.js");
|
||||
let runtime = get(runtime_js_path());
|
||||
assert_eq!(runtime.status, 200);
|
||||
assert!(runtime.header("content-type").contains("javascript"));
|
||||
|
||||
|
||||
@@ -6,8 +6,8 @@ use futures_util::{stream, StreamExt};
|
||||
use hemplate::Hemplate;
|
||||
use hemx::{Html, IntoEffect};
|
||||
use hemx_axum::{
|
||||
interactions, runtime_js, sse, EffectResponse, Form, HandlerErrorContext, HandlerFailure,
|
||||
InteractionRequest, IntoHandlerFailure, PageRequest, Registry,
|
||||
interactions, runtime_js, runtime_js_path, sse, EffectResponse, Form, HandlerErrorContext,
|
||||
HandlerFailure, InteractionRequest, IntoHandlerFailure, PageRequest, Registry,
|
||||
};
|
||||
use hemx_v0_examples::ui;
|
||||
use hemx_v0_examples::ui::{auth, counter, notifications, page_swap, todo_row, todos, wizard};
|
||||
@@ -178,6 +178,7 @@ struct PageSwap {
|
||||
|
||||
#[derive(Hemplate)]
|
||||
struct AppShell {
|
||||
runtime_src: &'static str,
|
||||
body: Html,
|
||||
}
|
||||
|
||||
@@ -194,7 +195,7 @@ async fn main() {
|
||||
.route("/", get(home).post(interact))
|
||||
.route("/docs", get(docs).post(interact))
|
||||
.route("/events", get(events))
|
||||
.route("/hemx.js", get(runtime))
|
||||
.route(runtime_js_path(), get(runtime))
|
||||
.with_state(state);
|
||||
|
||||
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
|
||||
@@ -278,7 +279,10 @@ fn all_examples() -> Html {
|
||||
|
||||
fn shell(body: Html) -> Html {
|
||||
// req: html_safety/001 req: html_safety/002 req: axum_integration/001 req: component/003
|
||||
hemx::page(&AppShell { body })
|
||||
hemx::page(&AppShell {
|
||||
runtime_src: runtime_js_path(),
|
||||
body,
|
||||
})
|
||||
}
|
||||
|
||||
#[hemx::component("counter")]
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>hemx v0 examples</title>
|
||||
<script src="/hemx.js" defer></script>
|
||||
<script +src="self.runtime_src" defer></script>
|
||||
<style>
|
||||
body { font-family: system-ui, sans-serif; margin: 2rem; max-width: 54rem; }
|
||||
section, main { border: 1px solid #ddd; border-radius: .5rem; margin: 1rem 0; padding: 1rem; }
|
||||
|
||||
Reference in New Issue
Block a user