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:
@@ -54,3 +54,4 @@ Keep it stable. Prefer pointers to canonical sources over copied structure, file
|
|||||||
- Public examples and beginner APIs should use generated resources and `IntoEffect`, not raw ids or runtime opcodes.
|
- Public examples and beginner APIs should use generated resources and `IntoEffect`, not raw ids or runtime opcodes.
|
||||||
- Hemlate examples must use real hemplate syntax, not Vue/Handlebars sketches: `{+ expr +}` for escaped text, `{+= expr =+}` only for trusted/rendered HTML, `+attr="expr"` for dynamic attributes, and Rust-shaped `h-if`, `h-for`, `h-match`, `h-case` directives (`h-case="_"` is the default arm).
|
- Hemlate examples must use real hemplate syntax, not Vue/Handlebars sketches: `{+ expr +}` for escaped text, `{+= expr =+}` only for trusted/rendered HTML, `+attr="expr"` for dynamic attributes, and Rust-shaped `h-if`, `h-for`, `h-match`, `h-case` directives (`h-case="_"` is the default arm).
|
||||||
- JS runtime changes must preserve root-scoped lookup and avoid selectors, VDOM, expressions, and per-node listeners.
|
- JS runtime changes must preserve root-scoped lookup and avoid selectors, VDOM, expressions, and per-node listeners.
|
||||||
|
- Axum apps should serve and load the shared runtime through hemx-axum helpers such as `runtime_js_path()` and `runtime_js()`, not hard-coded `/hemx.js` URLs or app-owned cache-busting strings.
|
||||||
|
|||||||
Generated
+68
@@ -105,6 +105,15 @@ version = "2.11.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
|
checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "block-buffer"
|
||||||
|
version = "0.10.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
|
||||||
|
dependencies = [
|
||||||
|
"generic-array",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bumpalo"
|
name = "bumpalo"
|
||||||
version = "3.20.2"
|
version = "3.20.2"
|
||||||
@@ -175,6 +184,25 @@ dependencies = [
|
|||||||
"unicode-xid",
|
"unicode-xid",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cpufeatures"
|
||||||
|
version = "0.2.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crypto-common"
|
||||||
|
version = "0.1.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
|
||||||
|
dependencies = [
|
||||||
|
"generic-array",
|
||||||
|
"typenum",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cssparser"
|
name = "cssparser"
|
||||||
version = "0.34.0"
|
version = "0.34.0"
|
||||||
@@ -209,6 +237,16 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "digest"
|
||||||
|
version = "0.10.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
||||||
|
dependencies = [
|
||||||
|
"block-buffer",
|
||||||
|
"crypto-common",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "displaydoc"
|
name = "displaydoc"
|
||||||
version = "0.2.5"
|
version = "0.2.5"
|
||||||
@@ -347,6 +385,16 @@ dependencies = [
|
|||||||
"byteorder",
|
"byteorder",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "generic-array"
|
||||||
|
version = "0.14.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
|
||||||
|
dependencies = [
|
||||||
|
"typenum",
|
||||||
|
"version_check",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "getopts"
|
name = "getopts"
|
||||||
version = "0.2.24"
|
version = "0.2.24"
|
||||||
@@ -473,6 +521,9 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "hemx-js"
|
name = "hemx-js"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"sha2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hemx-kanban-example"
|
name = "hemx-kanban-example"
|
||||||
@@ -1459,6 +1510,17 @@ dependencies = [
|
|||||||
"stable_deref_trait",
|
"stable_deref_trait",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sha2"
|
||||||
|
version = "0.10.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"cpufeatures",
|
||||||
|
"digest",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "shlex"
|
name = "shlex"
|
||||||
version = "1.3.0"
|
version = "1.3.0"
|
||||||
@@ -1845,6 +1907,12 @@ version = "0.2.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typenum"
|
||||||
|
version = "1.20.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-ident"
|
name = "unicode-ident"
|
||||||
version = "1.0.24"
|
version = "1.0.24"
|
||||||
|
|||||||
@@ -479,6 +479,9 @@ what a valid business email is.
|
|||||||
### req: axum/004
|
### req: axum/004
|
||||||
004 Query-string demo endpoints may be migrated to typed handler params from `data-*` attributes or forms. `Query<T>` remains available in normal Axum routes but is not the hemx happy path.
|
004 Query-string demo endpoints may be migrated to typed handler params from `data-*` attributes or forms. `Query<T>` remains available in normal Axum routes but is not the hemx happy path.
|
||||||
|
|
||||||
|
### req: axum/005
|
||||||
|
005 hemx-axum exposes the shared JS runtime through a content-hashed script path and immutable runtime response headers. Applications load the hemx-owned path instead of hand-written cache-busting query strings; changing runtime bytes changes the exposed path automatically.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## auth
|
## auth
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ pub mod ui {}
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::ui::{board, board_card};
|
use super::ui::{board, board_card};
|
||||||
use hemplate::Hemplate;
|
use hemplate::Hemplate;
|
||||||
use scraper::{Html, Selector};
|
|
||||||
use hemx::IntoEffect;
|
use hemx::IntoEffect;
|
||||||
use hemx_test::inspect;
|
use hemx_test::inspect;
|
||||||
|
use scraper::{Html, Selector};
|
||||||
|
|
||||||
#[derive(Hemplate)]
|
#[derive(Hemplate)]
|
||||||
#[hemplate = "partials"]
|
#[hemplate = "partials"]
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ use futures_util::{stream, StreamExt};
|
|||||||
use hemplate::Hemplate;
|
use hemplate::Hemplate;
|
||||||
use hemx::{Html, IntoEffect};
|
use hemx::{Html, IntoEffect};
|
||||||
use hemx_axum::{
|
use hemx_axum::{
|
||||||
interactions, runtime_js, sse, DispatchRegistry, DispatchRejection, EffectResponse,
|
interactions, runtime_js, runtime_js_path, sse, DispatchRegistry, DispatchRejection,
|
||||||
InteractionRequest, PageRequest,
|
EffectResponse, InteractionRequest, PageRequest,
|
||||||
};
|
};
|
||||||
use hemx_kanban_example::ui::board::{self as board};
|
use hemx_kanban_example::ui::board::{self as board};
|
||||||
use hemx_kanban_example::ui::board_card as card_board;
|
use hemx_kanban_example::ui::board_card as card_board;
|
||||||
@@ -40,6 +40,7 @@ struct Card {
|
|||||||
|
|
||||||
#[derive(Hemplate)]
|
#[derive(Hemplate)]
|
||||||
struct AppShell {
|
struct AppShell {
|
||||||
|
runtime_src: &'static str,
|
||||||
body: Html,
|
body: Html,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,7 +119,7 @@ async fn main() {
|
|||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
.route("/", get(home).post(interact))
|
.route("/", get(home).post(interact))
|
||||||
.route("/events", get(events))
|
.route("/events", get(events))
|
||||||
.route("/hemx.js", get(runtime))
|
.route(runtime_js_path(), get(runtime))
|
||||||
.with_state(state);
|
.with_state(state);
|
||||||
|
|
||||||
let addr = SocketAddr::from(([127, 0, 0, 1], 3001));
|
let addr = SocketAddr::from(([127, 0, 0, 1], 3001));
|
||||||
@@ -288,7 +289,10 @@ fn page_html(board: &BoardState) -> Html {
|
|||||||
|
|
||||||
fn shell(body: Html) -> Html {
|
fn shell(body: Html) -> Html {
|
||||||
// req: html_safety/001 req: html_safety/002 req: axum_integration/001
|
// 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 {
|
fn render_options() -> Html {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>hemx Kanban</title>
|
<title>hemx Kanban</title>
|
||||||
<script src="/hemx.js" defer></script>
|
<script +src="self.runtime_src" defer></script>
|
||||||
<style>
|
<style>
|
||||||
body { font-family: system-ui, sans-serif; margin: 2rem; }
|
body { font-family: system-ui, sans-serif; margin: 2rem; }
|
||||||
form { display: flex; gap: .5rem; flex-wrap: wrap; margin: 1rem 0; }
|
form { display: flex; gap: .5rem; flex-wrap: wrap; margin: 1rem 0; }
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ pub mod ui {}
|
|||||||
use hemplate::Hemplate;
|
use hemplate::Hemplate;
|
||||||
use hemx::{push, Html, IntoEffect};
|
use hemx::{push, Html, IntoEffect};
|
||||||
use hemx_axum::{
|
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::convert::Infallible;
|
||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
@@ -248,12 +249,14 @@ impl SettingsPage {
|
|||||||
#[derive(Hemplate)]
|
#[derive(Hemplate)]
|
||||||
pub struct AppShell {
|
pub struct AppShell {
|
||||||
title: &'static str,
|
title: &'static str,
|
||||||
|
runtime_src: &'static str,
|
||||||
body: Html,
|
body: Html,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn home_page(ctx: &AppContext) -> Html {
|
pub fn home_page(ctx: &AppContext) -> Html {
|
||||||
hemx::page(&AppShell {
|
hemx::page(&AppShell {
|
||||||
title: "hemx SaaS tutorial",
|
title: "hemx SaaS tutorial",
|
||||||
|
runtime_src: runtime_js_path(),
|
||||||
body: hemx::page(&Dashboard::from_context(ctx)),
|
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 {
|
pub fn settings_page(ctx: &AppContext) -> Html {
|
||||||
hemx::page(&AppShell {
|
hemx::page(&AppShell {
|
||||||
title: "hemx SaaS tutorial settings",
|
title: "hemx SaaS tutorial settings",
|
||||||
|
runtime_src: runtime_js_path(),
|
||||||
body: hemx::page(&Dashboard::settings(ctx)),
|
body: hemx::page(&Dashboard::settings(ctx)),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use axum::routing::get;
|
|||||||
use axum::Router;
|
use axum::Router;
|
||||||
use futures_util::stream;
|
use futures_util::stream;
|
||||||
use hemx::IntoEffect;
|
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 hemx_saas_example::{home_page, live_status, registry, settings_page, ui, AppContext};
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use std::convert::Infallible;
|
use std::convert::Infallible;
|
||||||
@@ -26,7 +26,7 @@ fn app(ctx: AppContext) -> Router {
|
|||||||
.route("/", get(home).post(interact))
|
.route("/", get(home).post(interact))
|
||||||
.route("/settings", get(settings))
|
.route("/settings", get(settings))
|
||||||
.route("/events", get(events))
|
.route("/events", get(events))
|
||||||
.route("/hemx.js", get(runtime))
|
.route(runtime_js_path(), get(runtime))
|
||||||
.route("/app.css", get(css))
|
.route("/app.css", get(css))
|
||||||
.route("/metrics.js", get(metrics_js))
|
.route("/metrics.js", get(metrics_js))
|
||||||
.with_state(ctx)
|
.with_state(ctx)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>{+ self.title +}</title>
|
<title>{+ self.title +}</title>
|
||||||
<link rel="stylesheet" href="/app.css">
|
<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>
|
<script src="/metrics.js" defer></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ use futures_util::{stream, StreamExt};
|
|||||||
use hemplate::Hemplate;
|
use hemplate::Hemplate;
|
||||||
use hemx::{CssClass, CssClasses, EventName, Html, IntoEffect};
|
use hemx::{CssClass, CssClasses, EventName, Html, IntoEffect};
|
||||||
use hemx_axum::{
|
use hemx_axum::{
|
||||||
interactions, runtime_js, sse, DispatchRegistry, DispatchRejection, EffectResponse,
|
interactions, runtime_js, runtime_js_path, sse, DispatchRegistry, DispatchRejection,
|
||||||
InteractionRequest, PageRequest,
|
EffectResponse, InteractionRequest, PageRequest,
|
||||||
};
|
};
|
||||||
use hemx_techdemo::ui;
|
use hemx_techdemo::ui;
|
||||||
use hemx_techdemo::ui::control_center::{self as control, classes};
|
use hemx_techdemo::ui::control_center::{self as control, classes};
|
||||||
@@ -156,6 +156,7 @@ struct ControlCenter {
|
|||||||
|
|
||||||
#[derive(Hemplate)]
|
#[derive(Hemplate)]
|
||||||
struct AppShell {
|
struct AppShell {
|
||||||
|
runtime_src: &'static str,
|
||||||
body: Html,
|
body: Html,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -226,7 +227,7 @@ async fn main() {
|
|||||||
.route("/architecture", get(architecture))
|
.route("/architecture", get(architecture))
|
||||||
.route("/events", get(events))
|
.route("/events", get(events))
|
||||||
.route("/favicon.ico", get(favicon))
|
.route("/favicon.ico", get(favicon))
|
||||||
.route("/hemx.js", get(runtime))
|
.route(runtime_js_path(), get(runtime))
|
||||||
.route("/app.css", get(app_css))
|
.route("/app.css", get(app_css))
|
||||||
.route("/control_center.css", get(control_center_css))
|
.route("/control_center.css", get(control_center_css))
|
||||||
.route("/island.js", get(island_js))
|
.route("/island.js", get(island_js))
|
||||||
@@ -515,7 +516,10 @@ fn page_html(demo: &DemoState) -> Html {
|
|||||||
|
|
||||||
fn shell(body: Html) -> Html {
|
fn shell(body: Html) -> Html {
|
||||||
// req: html_safety/001 req: html_safety/002 req: axum_integration/001 req: component/003
|
// 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 {
|
fn hero_view(demo: &DemoState) -> HeroMetrics {
|
||||||
@@ -663,7 +667,7 @@ mod tests {
|
|||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
document
|
document
|
||||||
.select(&selector("script[src=\"/hemx.js\"]"))
|
.select(&selector(&format!("script[src=\"{}\"]", runtime_js_path())))
|
||||||
.count(),
|
.count(),
|
||||||
1
|
1
|
||||||
);
|
);
|
||||||
@@ -691,17 +695,17 @@ mod tests {
|
|||||||
.count(),
|
.count(),
|
||||||
1
|
1
|
||||||
);
|
);
|
||||||
assert!(!html.contains("{+="));
|
assert!(!html.as_str().contains("{+="));
|
||||||
}
|
}
|
||||||
|
|
||||||
// req: html_safety/002 req: view/001 req: test/005
|
// req: html_safety/002 req: view/001 req: test/005
|
||||||
#[test]
|
#[test]
|
||||||
fn control_center_page_is_composed_by_hemplate_not_placeholders() {
|
fn control_center_page_is_composed_by_hemplate_not_placeholders() {
|
||||||
let html = page_html(&DemoState::default());
|
let html = page_html(&DemoState::default());
|
||||||
assert!(!html.contains("__HERO__"));
|
assert!(!html.as_str().contains("__HERO__"));
|
||||||
assert!(!html.contains("__BOARD__"));
|
assert!(!html.as_str().contains("__BOARD__"));
|
||||||
assert!(!html.contains("__INSPECTOR__"));
|
assert!(!html.as_str().contains("__INSPECTOR__"));
|
||||||
assert!(!html.contains("__ACTIVITY__"));
|
assert!(!html.as_str().contains("__ACTIVITY__"));
|
||||||
|
|
||||||
let document = Html::parse_fragment(html.as_str());
|
let document = Html::parse_fragment(html.as_str());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -754,7 +758,7 @@ mod tests {
|
|||||||
// req: style/001 req: style/002 req: style/003 req: test/005
|
// req: style/001 req: style/002 req: style/003 req: test/005
|
||||||
#[test]
|
#[test]
|
||||||
fn generated_css_class_flows_through_hemplate_dynamic_attribute() {
|
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::lane.as_str(), "lane");
|
||||||
assert_eq!(classes::work_card.as_str(), "work-card");
|
assert_eq!(classes::work_card.as_str(), "work-card");
|
||||||
assert_eq!(classes::is_selected.as_str(), "is-selected");
|
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_eq!(document.select(&selector(".inspector-row")).count(), 3);
|
||||||
assert!(document
|
assert!(document
|
||||||
.select(&selector("code"))
|
.select(&selector(".inspector-row"))
|
||||||
.any(|code| code.text().collect::<String>().contains("control::*")));
|
.any(|row| row.text().collect::<String>().contains("No selectors")));
|
||||||
}
|
}
|
||||||
|
|
||||||
// req: html_safety/002 req: view/001 req: test/005
|
// req: html_safety/002 req: view/001 req: test/005
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>hemx Techdemo</title>
|
<title>hemx Techdemo</title>
|
||||||
<script src="/hemx.js" defer></script>
|
<script +src="self.runtime_src" defer></script>
|
||||||
<script src="/island.js" defer></script>
|
<script src="/island.js" defer></script>
|
||||||
<link rel="stylesheet" href="/app.css">
|
<link rel="stylesheet" href="/app.css">
|
||||||
<link rel="stylesheet" href="/control_center.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::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_card::{advance_work, delete_work, spotlight_work};
|
||||||
use hemx_techdemo::ui::issue_lane::move_to_lane as move_to_lane_handle;
|
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,
|
root_selector, sse_endpoint_marker, strong_text_selector, unknown_handle_form_body,
|
||||||
EffectInspector,
|
EffectInspector,
|
||||||
};
|
};
|
||||||
|
use scraper::{Html, Selector};
|
||||||
use std::io::{Read, Write};
|
use std::io::{Read, Write};
|
||||||
use std::net::TcpStream;
|
use std::net::TcpStream;
|
||||||
use std::process::{Child, Command, Stdio};
|
use std::process::{Child, Command, Stdio};
|
||||||
@@ -73,7 +74,7 @@ fn product_is_e2e_working_over_http() {
|
|||||||
let favicon = get("/favicon.ico");
|
let favicon = get("/favicon.ico");
|
||||||
assert_eq!(favicon.status, 204);
|
assert_eq!(favicon.status, 204);
|
||||||
|
|
||||||
let runtime = get("/hemx.js");
|
let runtime = get(runtime_js_path());
|
||||||
assert_eq!(runtime.status, 200);
|
assert_eq!(runtime.status, 200);
|
||||||
assert!(runtime.header("content-type").contains("javascript"));
|
assert!(runtime.header("content-type").contains("javascript"));
|
||||||
|
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ use futures_util::{stream, StreamExt};
|
|||||||
use hemplate::Hemplate;
|
use hemplate::Hemplate;
|
||||||
use hemx::{Html, IntoEffect};
|
use hemx::{Html, IntoEffect};
|
||||||
use hemx_axum::{
|
use hemx_axum::{
|
||||||
interactions, runtime_js, sse, EffectResponse, Form, HandlerErrorContext, HandlerFailure,
|
interactions, runtime_js, runtime_js_path, sse, EffectResponse, Form, HandlerErrorContext,
|
||||||
InteractionRequest, IntoHandlerFailure, PageRequest, Registry,
|
HandlerFailure, InteractionRequest, IntoHandlerFailure, PageRequest, Registry,
|
||||||
};
|
};
|
||||||
use hemx_v0_examples::ui;
|
use hemx_v0_examples::ui;
|
||||||
use hemx_v0_examples::ui::{auth, counter, notifications, page_swap, todo_row, todos, wizard};
|
use hemx_v0_examples::ui::{auth, counter, notifications, page_swap, todo_row, todos, wizard};
|
||||||
@@ -178,6 +178,7 @@ struct PageSwap {
|
|||||||
|
|
||||||
#[derive(Hemplate)]
|
#[derive(Hemplate)]
|
||||||
struct AppShell {
|
struct AppShell {
|
||||||
|
runtime_src: &'static str,
|
||||||
body: Html,
|
body: Html,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,7 +195,7 @@ async fn main() {
|
|||||||
.route("/", get(home).post(interact))
|
.route("/", get(home).post(interact))
|
||||||
.route("/docs", get(docs).post(interact))
|
.route("/docs", get(docs).post(interact))
|
||||||
.route("/events", get(events))
|
.route("/events", get(events))
|
||||||
.route("/hemx.js", get(runtime))
|
.route(runtime_js_path(), get(runtime))
|
||||||
.with_state(state);
|
.with_state(state);
|
||||||
|
|
||||||
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
|
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
|
||||||
@@ -278,7 +279,10 @@ fn all_examples() -> Html {
|
|||||||
|
|
||||||
fn shell(body: Html) -> Html {
|
fn shell(body: Html) -> Html {
|
||||||
// req: html_safety/001 req: html_safety/002 req: axum_integration/001 req: component/003
|
// 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")]
|
#[hemx::component("counter")]
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>hemx v0 examples</title>
|
<title>hemx v0 examples</title>
|
||||||
<script src="/hemx.js" defer></script>
|
<script +src="self.runtime_src" defer></script>
|
||||||
<style>
|
<style>
|
||||||
body { font-family: system-ui, sans-serif; margin: 2rem; max-width: 54rem; }
|
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; }
|
section, main { border: 1px solid #ddd; border-radius: .5rem; margin: 1rem 0; padding: 1rem; }
|
||||||
|
|||||||
@@ -27,6 +27,16 @@ pub const fn runtime_js() -> RuntimeJs {
|
|||||||
RuntimeJs
|
RuntimeJs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// req: axum_integration/005
|
||||||
|
pub const fn runtime_js_hash() -> &'static str {
|
||||||
|
hemx_js::RUNTIME_JS_HASH
|
||||||
|
}
|
||||||
|
|
||||||
|
// req: axum_integration/005
|
||||||
|
pub const fn runtime_js_path() -> &'static str {
|
||||||
|
hemx_js::RUNTIME_JS_PATH
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
pub enum PageMode {
|
pub enum PageMode {
|
||||||
Full,
|
Full,
|
||||||
@@ -1382,6 +1392,16 @@ impl IntoResponse for RuntimeJs {
|
|||||||
header::CACHE_CONTROL,
|
header::CACHE_CONTROL,
|
||||||
HeaderValue::from_static("public, max-age=31536000, immutable"),
|
HeaderValue::from_static("public, max-age=31536000, immutable"),
|
||||||
);
|
);
|
||||||
|
response.headers_mut().insert(
|
||||||
|
header::ETAG,
|
||||||
|
HeaderValue::from_str(&format!("\"{}\"", runtime_js_hash()))
|
||||||
|
.expect("runtime hash is a valid ETag"),
|
||||||
|
);
|
||||||
|
response.headers_mut().insert(
|
||||||
|
header::CONTENT_LENGTH,
|
||||||
|
HeaderValue::from_str(hemx_js::RUNTIME_JS.len().to_string().as_str())
|
||||||
|
.expect("runtime length is a valid header value"),
|
||||||
|
);
|
||||||
response
|
response
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1390,6 +1410,14 @@ pub fn runtime_js_source() -> &'static str {
|
|||||||
hemx_js::RUNTIME_JS
|
hemx_js::RUNTIME_JS
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn runtime_js_script_src() -> &'static str {
|
||||||
|
runtime_js_path()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn runtime_js_route_path() -> &'static str {
|
||||||
|
runtime_js_path()
|
||||||
|
}
|
||||||
|
|
||||||
// req: push/001, req: push/003, req: push/004
|
// req: push/001, req: push/003, req: push/004
|
||||||
pub fn sse<S, E>(batches: S) -> Sse<impl Stream<Item = Result<Event, E>> + Send>
|
pub fn sse<S, E>(batches: S) -> Sse<impl Stream<Item = Result<Event, E>> + Send>
|
||||||
where
|
where
|
||||||
|
|||||||
@@ -2,9 +2,10 @@ use axum::extract::State;
|
|||||||
use axum::http::{header, HeaderMap};
|
use axum::http::{header, HeaderMap};
|
||||||
use axum::response::IntoResponse;
|
use axum::response::IntoResponse;
|
||||||
use hemx_axum::{
|
use hemx_axum::{
|
||||||
interactions, runtime_js, DispatchRejection, EffectResponse, Form, HandlerErrorContext,
|
interactions, runtime_js, runtime_js_hash, runtime_js_path, runtime_js_route_path,
|
||||||
HandlerFailure, InteractionForm, InteractionFormRejection, InteractionRequest,
|
runtime_js_script_src, runtime_js_source, DispatchRejection, EffectResponse, Form,
|
||||||
IntoHandlerFailure, PageMode, PageRequest, PageResponse, HEMX_CONTENT_TYPE,
|
HandlerErrorContext, HandlerFailure, InteractionForm, InteractionFormRejection,
|
||||||
|
InteractionRequest, IntoHandlerFailure, PageMode, PageRequest, PageResponse, HEMX_CONTENT_TYPE,
|
||||||
HEMX_FINGERPRINT_HEADER, HEMX_PARTIAL_HEADER, HEMX_RUNTIME_CONTENT_TYPE, HEMX_TITLE_HEADER,
|
HEMX_FINGERPRINT_HEADER, HEMX_PARTIAL_HEADER, HEMX_RUNTIME_CONTENT_TYPE, HEMX_TITLE_HEADER,
|
||||||
};
|
};
|
||||||
use hemx_core::{push, BuildFingerprint, Handle, IntoEffect, SafeHtml, Slot};
|
use hemx_core::{push, BuildFingerprint, Handle, IntoEffect, SafeHtml, Slot};
|
||||||
@@ -480,5 +481,29 @@ fn runtime_js_response_serves_embedded_runtime() {
|
|||||||
response.headers()[header::CONTENT_TYPE],
|
response.headers()[header::CONTENT_TYPE],
|
||||||
HEMX_RUNTIME_CONTENT_TYPE
|
HEMX_RUNTIME_CONTENT_TYPE
|
||||||
);
|
);
|
||||||
assert!(response.headers().contains_key(header::CACHE_CONTROL));
|
assert_eq!(
|
||||||
|
response.headers()[header::CACHE_CONTROL],
|
||||||
|
"public, max-age=31536000, immutable"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
response.headers()[header::ETAG],
|
||||||
|
format!("\"{}\"", runtime_js_hash())
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
response.headers()[header::CONTENT_LENGTH],
|
||||||
|
runtime_js_source().len().to_string()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// req: axum_integration/005
|
||||||
|
#[test]
|
||||||
|
fn runtime_js_path_is_content_hashed() {
|
||||||
|
let path = runtime_js_path();
|
||||||
|
|
||||||
|
assert!(path.starts_with("/hemx."));
|
||||||
|
assert!(path.ends_with(".js"));
|
||||||
|
assert!(path.contains(runtime_js_hash()));
|
||||||
|
assert_eq!(runtime_js_script_src(), path);
|
||||||
|
assert_eq!(runtime_js_route_path(), path);
|
||||||
|
assert_eq!(runtime_js_hash().len(), 64);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,3 +5,6 @@ edition.workspace = true
|
|||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
path = "src/lib.rs"
|
path = "src/lib.rs"
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
sha2 = "0.10"
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
use std::env;
|
||||||
|
use std::fs;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let runtime_path = Path::new("runtime/hemx.js");
|
||||||
|
println!("cargo:rerun-if-changed={}", runtime_path.display());
|
||||||
|
|
||||||
|
let runtime = fs::read(runtime_path).expect("read hemx runtime");
|
||||||
|
let hash = format!("{:x}", Sha256::digest(&runtime));
|
||||||
|
let out_dir = env::var("OUT_DIR").expect("OUT_DIR");
|
||||||
|
fs::write(
|
||||||
|
Path::new(&out_dir).join("runtime_asset.rs"),
|
||||||
|
format!(
|
||||||
|
"pub const RUNTIME_JS_HASH: &str = \"{hash}\";\n\
|
||||||
|
pub const RUNTIME_JS_PATH: &str = \"/hemx.{hash}.js\";\n"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.expect("write runtime asset constants");
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
pub const RUNTIME_ABI_VERSION: u32 = 1;
|
pub const RUNTIME_ABI_VERSION: u32 = 1;
|
||||||
pub const RUNTIME_JS: &str = include_str!("../runtime/hemx.js");
|
pub const RUNTIME_JS: &str = include_str!("../runtime/hemx.js");
|
||||||
pub const RUNTIME_D_TS: &str = include_str!("../runtime/hemx.d.ts");
|
pub const RUNTIME_D_TS: &str = include_str!("../runtime/hemx.d.ts");
|
||||||
|
|
||||||
|
include!(concat!(env!("OUT_DIR"), "/runtime_asset.rs"));
|
||||||
|
|||||||
@@ -298,7 +298,7 @@ fn is_example_source(path: &PathBuf) -> bool {
|
|||||||
|
|
||||||
fn allowed_example_script(path: &Path, line: &str) -> bool {
|
fn allowed_example_script(path: &Path, line: &str) -> bool {
|
||||||
// req: examples/005
|
// req: examples/005
|
||||||
line.contains(r#"<script src="/hemx.js" defer></script>"#)
|
line.contains(r#"<script +src="self.runtime_src" defer></script>"#)
|
||||||
|| (path.ends_with("examples/techdemo/templates/app_shell.heml")
|
|| (path.ends_with("examples/techdemo/templates/app_shell.heml")
|
||||||
&& line.contains(r#"<script src="/island.js" defer></script>"#))
|
&& line.contains(r#"<script src="/island.js" defer></script>"#))
|
||||||
|| (path.ends_with("examples/saas/templates/app_shell.heml")
|
|| (path.ends_with("examples/saas/templates/app_shell.heml")
|
||||||
|
|||||||
Reference in New Issue
Block a user