refactor!: rename slhx to hemx
Rename the tracked product identity, crate/package names, Rust paths/macros, generated artifacts, runtime files, public attributes, examples, docs, requirements, and tests from slhx to hemx without compatibility shims. Verified with cargo run -p hemx-xtask -- test, cargo test -p hemx-derive --test compile_fail, cargo test -p hemx-js, cargo test -p hemx-axum, cargo test -p hemx-v0-examples, cargo check --workspace, redgate list, redgate refs, redgate health --strict, git diff --check, and git grep/ls-files legacy-name audits. req: misc/001 req: codegen/001 req: component/004 req: runtime/001
This commit is contained in:
@@ -0,0 +1,330 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[test]
|
||||
fn canonical_examples_do_not_author_browser_javascript() {
|
||||
// req: examples/005
|
||||
let root = Path::new(env!("CARGO_MANIFEST_DIR")).parent().unwrap();
|
||||
let examples = root.join("examples");
|
||||
let mut failures = Vec::new();
|
||||
scan_examples(&examples, &mut |path, text| {
|
||||
if path.components().any(|part| part.as_os_str() == "tests") {
|
||||
return;
|
||||
}
|
||||
for (line_no, line) in text.lines().enumerate() {
|
||||
let trimmed = line.trim();
|
||||
if trimmed.contains("<script") && !allowed_example_script(path, trimmed) {
|
||||
failures.push(format!(
|
||||
"{}:{}: only the hemx runtime or explicit opaque-island scripts are allowed",
|
||||
path.display(),
|
||||
line_no + 1
|
||||
));
|
||||
}
|
||||
if contains_inline_event_handler(trimmed) {
|
||||
failures.push(format!(
|
||||
"{}:{}: inline on*= handler is not allowed",
|
||||
path.display(),
|
||||
line_no + 1
|
||||
));
|
||||
}
|
||||
if trimmed.to_ascii_lowercase().contains("javascript:") {
|
||||
failures.push(format!(
|
||||
"{}:{}: javascript: URL is not allowed",
|
||||
path.display(),
|
||||
line_no + 1
|
||||
));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
assert!(failures.is_empty(), "{}", failures.join("\n"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canonical_examples_do_not_author_low_level_resource_plumbing() {
|
||||
// req: dx/002 req: ceremony/002 req: ceremony/004 req: examples/003
|
||||
let root = Path::new(env!("CARGO_MANIFEST_DIR")).parent().unwrap();
|
||||
let examples = root.join("examples");
|
||||
let forbidden = [
|
||||
"ResourceId::new",
|
||||
".id().id",
|
||||
"include!(concat!",
|
||||
"OUT_DIR",
|
||||
"global_exports(",
|
||||
"EffectWriter",
|
||||
"RuntimeOpcode",
|
||||
"data-hid",
|
||||
"data-sid",
|
||||
"data-fid",
|
||||
"data-aid",
|
||||
"data-handle-id",
|
||||
"data-slot-id",
|
||||
"data-form-id",
|
||||
"data-atom-id",
|
||||
"::lower(include_str!",
|
||||
"lower_html(",
|
||||
"render_html(",
|
||||
"HandlerRegistry",
|
||||
"InteractionForm",
|
||||
"register_handle(",
|
||||
"SafeHtml",
|
||||
"hemx::advanced",
|
||||
"advanced::slots",
|
||||
"KeyedSlot",
|
||||
"Slot<",
|
||||
"Effect::",
|
||||
"Payload::",
|
||||
"NavigateMode",
|
||||
"ScopeKey",
|
||||
"opcode",
|
||||
"wire format",
|
||||
"manual generated",
|
||||
"RenderSlotExt",
|
||||
"RenderKeyedSlotExt",
|
||||
".render(&",
|
||||
".html(ui::render",
|
||||
".html(hemx::render",
|
||||
".html(super::ui::render",
|
||||
"ui::put(",
|
||||
"ui::append(",
|
||||
"ui::replace(",
|
||||
"application/hemx",
|
||||
"EffectBatch",
|
||||
"__h=",
|
||||
"name=\"__h\"",
|
||||
"name='__h'",
|
||||
];
|
||||
let mut failures = Vec::new();
|
||||
scan_examples(&examples, &mut |path, text| {
|
||||
if path.components().any(|part| part.as_os_str() == "tests") {
|
||||
return;
|
||||
}
|
||||
let Some(ext) = path.extension().and_then(|ext| ext.to_str()) else {
|
||||
return;
|
||||
};
|
||||
if !matches!(ext, "rs" | "heml" | "html") {
|
||||
return;
|
||||
}
|
||||
let runtime_source = if ext == "rs" {
|
||||
text.split("#[cfg(test)]").next().unwrap_or(text)
|
||||
} else {
|
||||
text
|
||||
};
|
||||
for (line_no, line) in runtime_source.lines().enumerate() {
|
||||
if let Some(token) = forbidden.iter().find(|token| line.contains(*token)) {
|
||||
failures.push(format!(
|
||||
"{}:{}: example runtime code must use generated resources, not `{token}`",
|
||||
path.display(),
|
||||
line_no + 1
|
||||
));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
assert!(failures.is_empty(), "{}", failures.join("\n"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn public_prelude_does_not_export_low_level_primitives() {
|
||||
// req: public_api/005 req: dx/006
|
||||
let root = Path::new(env!("CARGO_MANIFEST_DIR")).parent().unwrap();
|
||||
let facade = std::fs::read_to_string(root.join("hemx/src/lib.rs")).unwrap();
|
||||
let prelude = facade
|
||||
.split("pub mod prelude {")
|
||||
.nth(1)
|
||||
.and_then(|tail| tail.split("\n}").next())
|
||||
.expect("hemx facade exposes prelude module");
|
||||
let exported = prelude
|
||||
.split(|c: char| !(c == '_' || c.is_ascii_alphanumeric()))
|
||||
.filter(|part| !part.is_empty())
|
||||
.collect::<Vec<_>>();
|
||||
for token in [
|
||||
"SafeHtml",
|
||||
"Effect",
|
||||
"Slot",
|
||||
"KeyedSlot",
|
||||
"ResourceId",
|
||||
"EffectBatch",
|
||||
"BuildFingerprint",
|
||||
] {
|
||||
assert!(
|
||||
!exported.contains(&token),
|
||||
"hemx::prelude must not export low-level `{token}`"
|
||||
);
|
||||
}
|
||||
assert!(prelude.contains("Html"), "hemx::prelude should export Html");
|
||||
assert!(
|
||||
prelude.contains("IntoEffect"),
|
||||
"hemx::prelude should export IntoEffect"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canonical_example_docs_do_not_teach_low_level_plumbing() {
|
||||
// req: dx/002 req: dx/003 req: examples/003
|
||||
let root = Path::new(env!("CARGO_MANIFEST_DIR")).parent().unwrap();
|
||||
let examples = root.join("examples");
|
||||
let forbidden = [
|
||||
"generated handles and slots",
|
||||
"generated hemx handles",
|
||||
"generated handle ids",
|
||||
"numeric handle ids",
|
||||
"runtime lowering",
|
||||
"application/hemx responses",
|
||||
"EffectBatch",
|
||||
"manual registry",
|
||||
"HandlerRegistry",
|
||||
"InteractionForm",
|
||||
"register_handle(",
|
||||
"lower_html(",
|
||||
"render_html(",
|
||||
"SafeHtml",
|
||||
"KeyedSlot",
|
||||
"Slot<",
|
||||
"Effect::",
|
||||
"opcode",
|
||||
"wire format",
|
||||
"manual generated",
|
||||
"Effect::batch",
|
||||
"Effect::class",
|
||||
"Effect::move",
|
||||
"Effect::set",
|
||||
"Effect::broadcast",
|
||||
"Effect::ack",
|
||||
"SyncEffect::",
|
||||
"postcard DOM ops",
|
||||
"data-hid",
|
||||
"data-sid",
|
||||
".render(&",
|
||||
"addEventListener(",
|
||||
"querySelector",
|
||||
"querySelectorAll",
|
||||
];
|
||||
let mut failures = Vec::new();
|
||||
scan_examples(&examples, &mut |path, text| {
|
||||
if path.extension().and_then(|ext| ext.to_str()) != Some("md")
|
||||
|| is_advanced_boundary_doc(text)
|
||||
{
|
||||
return;
|
||||
}
|
||||
for (line_no, line) in text.lines().enumerate() {
|
||||
if let Some(token) = forbidden.iter().find(|token| line.contains(*token)) {
|
||||
failures.push(format!(
|
||||
"{}:{}: example docs must teach generated ergonomic APIs, not `{token}`",
|
||||
path.display(),
|
||||
line_no + 1
|
||||
));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
assert!(failures.is_empty(), "{}", failures.join("\n"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn example_docs_do_not_show_runtime_metadata_as_authoring_contract() {
|
||||
// req: dx/006 req: misc/006
|
||||
let root = Path::new(env!("CARGO_MANIFEST_DIR")).parent().unwrap();
|
||||
let examples = root.join("examples");
|
||||
let forbidden = [
|
||||
"data-hid",
|
||||
"data-sid",
|
||||
"data-aid",
|
||||
"data-fid",
|
||||
"[data-hid",
|
||||
"[data-sid",
|
||||
];
|
||||
let mut failures = Vec::new();
|
||||
scan_examples(&examples, &mut |path, text| {
|
||||
if path.extension().and_then(|ext| ext.to_str()) != Some("md") {
|
||||
return;
|
||||
}
|
||||
for (line_no, line) in text.lines().enumerate() {
|
||||
if let Some(token) = forbidden.iter().find(|token| line.contains(*token)) {
|
||||
failures.push(format!(
|
||||
"{}:{}: example docs must describe generated authoring APIs, not runtime metadata `{token}`",
|
||||
path.display(),
|
||||
line_no + 1
|
||||
));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
assert!(failures.is_empty(), "{}", failures.join("\n"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn browser_e2e_does_not_shortcut_product_interactions() {
|
||||
// req: examples/005
|
||||
let root = Path::new(env!("CARGO_MANIFEST_DIR")).parent().unwrap();
|
||||
let browser_e2e = root.join("examples/techdemo/tests/browser_e2e.rs");
|
||||
let source = std::fs::read_to_string(browser_e2e).unwrap();
|
||||
|
||||
assert!(
|
||||
!source.contains("fetch(\"/\""),
|
||||
"browser E2E must drive UI, not post directly"
|
||||
);
|
||||
assert!(
|
||||
!source.contains("applyBatch(buffer"),
|
||||
"browser E2E must not apply wire batches manually"
|
||||
);
|
||||
}
|
||||
|
||||
fn scan_examples(dir: &Path, visit: &mut impl FnMut(&Path, &str)) {
|
||||
for entry in std::fs::read_dir(dir).unwrap() {
|
||||
let entry = entry.unwrap();
|
||||
let path = entry.path();
|
||||
if path.is_dir() {
|
||||
scan_examples(&path, visit);
|
||||
continue;
|
||||
}
|
||||
if !is_example_source(&path) {
|
||||
continue;
|
||||
}
|
||||
let text = std::fs::read_to_string(&path).unwrap();
|
||||
visit(&path, &text);
|
||||
}
|
||||
}
|
||||
|
||||
fn is_advanced_boundary_doc(text: &str) -> bool {
|
||||
text.contains("advanced/low-level north-star boundary sketch")
|
||||
}
|
||||
|
||||
fn is_example_source(path: &PathBuf) -> bool {
|
||||
matches!(
|
||||
path.extension().and_then(|ext| ext.to_str()),
|
||||
Some("rs" | "heml" | "html" | "md")
|
||||
)
|
||||
}
|
||||
|
||||
fn allowed_example_script(path: &Path, line: &str) -> bool {
|
||||
// req: examples/005
|
||||
line.contains(r#"<script src="/hemx.js" defer></script>"#)
|
||||
|| (path.ends_with("examples/techdemo/templates/app_shell.heml")
|
||||
&& line.contains(r#"<script src="/island.js" defer></script>"#))
|
||||
}
|
||||
|
||||
fn contains_inline_event_handler(line: &str) -> bool {
|
||||
let bytes = line.as_bytes();
|
||||
let mut i = 0;
|
||||
while i + 3 < bytes.len() {
|
||||
let boundary = bytes[i].is_ascii_whitespace() || bytes[i] == b'<';
|
||||
if boundary
|
||||
&& bytes[i + 1] == b'o'
|
||||
&& bytes[i + 2] == b'n'
|
||||
&& bytes[i + 3].is_ascii_lowercase()
|
||||
{
|
||||
let mut j = i + 4;
|
||||
while j < bytes.len() && bytes[j].is_ascii_lowercase() {
|
||||
j += 1;
|
||||
}
|
||||
while j < bytes.len() && bytes[j].is_ascii_whitespace() {
|
||||
j += 1;
|
||||
}
|
||||
if j < bytes.len() && bytes[j] == b'=' {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
false
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
use hemx_core::{
|
||||
Atom, Effect, GeneratedTarget, KeyedSlot, Payload, ResourceId, ResourceKind, ResourceRef, Slot,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn inspects_tuple_effects() {
|
||||
let count = Slot::<u32>::new(1);
|
||||
let user = Atom::<String>::new(2);
|
||||
|
||||
let inspected = hemx_test::run(|value| (count.text(value), user.set("alice")), 42);
|
||||
|
||||
assert!(inspected.has_slot(count));
|
||||
assert!(inspected.has_atom(user));
|
||||
assert!(inspected.contains(&Effect::Put {
|
||||
target: ResourceRef::unscoped(count.id()),
|
||||
payload: Payload::text(42),
|
||||
}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn finds_keyed_slot_targets() {
|
||||
let rows = KeyedSlot::<u32, String>::new(9);
|
||||
let inspected = hemx_test::inspect(rows.append_text(7, String::from("row")));
|
||||
|
||||
assert!(inspected.has_keyed_slot(rows));
|
||||
assert_eq!(inspected.ops().len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builds_generated_handle_form_bodies() {
|
||||
let handle = hemx_core::Handle::<()>::new(7);
|
||||
|
||||
let body = hemx_test::handle_form_body(handle, &[("title", "hello world"), ("tag", "a&b")]);
|
||||
|
||||
assert_eq!(body, "__h=7&title=hello+world&tag=a%26b");
|
||||
assert_eq!(hemx_test::unknown_handle_form_body(99), "__h=99");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builds_authoring_boundary_selectors() {
|
||||
assert_eq!(
|
||||
hemx_test::root_selector("techdemo"),
|
||||
r#"[data-hemx-root="techdemo"]"#
|
||||
);
|
||||
assert_eq!(
|
||||
hemx_test::island_selector("orbit"),
|
||||
r#"[data-hemx-island="orbit"]"#
|
||||
);
|
||||
assert_eq!(hemx_test::island_attribute_name(), "data-hemx-island");
|
||||
assert_eq!(hemx_test::island_event_name("orbit"), "hemx:island-orbit");
|
||||
assert_eq!(
|
||||
hemx_test::sse_endpoint_marker("/events"),
|
||||
r#"data-hemx-sse="/events""#
|
||||
);
|
||||
assert_eq!(hemx_test::any_root_selector(), "[data-hemx-root]");
|
||||
assert_eq!(
|
||||
hemx_test::root_element_selector("main", "docs"),
|
||||
r#"main[data-hemx-root="docs"]"#
|
||||
);
|
||||
assert_eq!(hemx_test::document_body_selector(), "body");
|
||||
assert_eq!(hemx_test::document_title_selector(), "title");
|
||||
assert_eq!(
|
||||
hemx_test::runtime_script_selector(),
|
||||
r#"script[src="/hemx.js"]"#
|
||||
);
|
||||
assert_eq!(
|
||||
hemx_test::target_selector(TestTarget(ResourceKind::Slot, 42)),
|
||||
r#"[data-sid="42"]"#
|
||||
);
|
||||
assert_eq!(
|
||||
hemx_test::handle_button_selector(hemx_core::Handle::<()>::new(7)),
|
||||
r#"button[data-hid="7"]"#
|
||||
);
|
||||
assert_eq!(hemx_test::article_selector(), "article");
|
||||
assert_eq!(hemx_test::strong_text_selector(), "strong");
|
||||
assert_eq!(hemx_test::small_text_selector(), "small");
|
||||
assert_eq!(hemx_test::escaped_markup_selector("b"), "b");
|
||||
assert_eq!(hemx_test::heading_selector("article", 1), "article h1");
|
||||
assert_eq!(hemx_test::list_item_selector("ul"), "ul li");
|
||||
assert_eq!(hemx_test::prose_selector("article"), "article p");
|
||||
assert_eq!(hemx_test::form_selector("header"), "header form");
|
||||
assert_eq!(
|
||||
hemx_test::select_options_selector("column"),
|
||||
r#"select[name="column"] > option"#
|
||||
);
|
||||
assert_eq!(hemx_test::class_selector("lane"), ".lane");
|
||||
assert_eq!(
|
||||
hemx_test::element_class_selector("span", "presence"),
|
||||
"span.presence"
|
||||
);
|
||||
assert_eq!(
|
||||
hemx_test::class_child_selector("columns", "section", "column"),
|
||||
".columns > section.column"
|
||||
);
|
||||
assert_eq!(
|
||||
hemx_test::class_descendant_selector("impact", "i"),
|
||||
".impact i"
|
||||
);
|
||||
assert_eq!(hemx_test::disabled_button_selector(), "button[disabled]");
|
||||
assert_eq!(
|
||||
hemx_test::nav_link_selector("/architecture"),
|
||||
r#"a[href="/architecture"]"#
|
||||
);
|
||||
assert_eq!(
|
||||
hemx_test::page_nav_link_selector("/docs"),
|
||||
r#"a[href="/docs"][data-hemx-nav]:not([data-hemx-handle])"#
|
||||
);
|
||||
assert_eq!(hemx_test::island_snapshot_marker(), "data-island-snapshot=");
|
||||
assert_eq!(
|
||||
hemx_test::island_readout_selector(),
|
||||
"[data-island-readout]"
|
||||
);
|
||||
assert_eq!(
|
||||
hemx_test::scoped_island_readout_selector("#probe-island"),
|
||||
"#probe-island [data-island-readout]"
|
||||
);
|
||||
assert_eq!(
|
||||
hemx_test::keyed_selector(".work-card", 4),
|
||||
r#".work-card[data-key="4"]"#
|
||||
);
|
||||
assert_eq!(hemx_test::keyed_items_selector("li"), "li[data-key]");
|
||||
|
||||
let probe = hemx_test::island_probe_script(
|
||||
"probe-island",
|
||||
"orbit",
|
||||
"1|1|1|probe waiting",
|
||||
"7|2|8|probe live",
|
||||
);
|
||||
assert!(probe.contains("probe-island"));
|
||||
assert!(probe.contains("probe live"));
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct TestTarget(ResourceKind, u32);
|
||||
|
||||
impl GeneratedTarget for TestTarget {
|
||||
fn __hemx_resource_id(self) -> ResourceId {
|
||||
ResourceId::new(self.0, self.1)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user