islands-build 0.1.3

Layout-agnostic build pipeline for islands.rs apps: WASM bundling, the V8 module-namespace patch, per-page CSS, and content-hash manifests. Composed by a thin xtask in any workspace.
Documentation
//! Verifies the V8 namespace patch survives a content-hashing build's basename
//! rewrite — the load-bearing invariant for the page→core WASM import.
//!
//! The patch token (`Object.assign(Object.create(null), import1)`) must remain
//! present in every emitted page JS after the hashing pass renames
//! `islands_core.js` → `islands_core.<hash>.js`, and the importObject KEY line
//! must keep the unhashed `islands_core.js` basename so V8's two-level WASM
//! import resolves against the compiled module name.
//!
//! This threads a faked wasm-bindgen-emitted page JS through `patch_page_source`
//! then `rewrite_basenames_in_js`, mirroring the order
//! `wasm::build_wasm` (patch) and `hashing::post_build_hashing_pass` (rewrite)
//! execute in production.

use islands_build::patch::{patch_page_source, rewrite_basenames_in_js, PATCH_TOKEN};

const FAKE_PAGE_JS: &str = r#"
let wasm;
const importObject = {
    "/static/islands-core/islands_core.js": import1,
    "__wbindgen_placeholder__": import1,
};
const url = new URL('page_home_bg.wasm', import.meta.url);
import * as import1 from "/static/islands-core/islands_core.js";
"#;

fn rename_map() -> Vec<(String, String)> {
    vec![
        (
            "islands_core.js".to_owned(),
            "islands_core.a1b2c3d4.js".to_owned(),
        ),
        (
            "page_home_bg.wasm".to_owned(),
            "page_home.cafef00d_bg.wasm".to_owned(),
        ),
    ]
}

#[test]
fn patch_token_appears_after_debug_patch() {
    let patched = patch_page_source(FAKE_PAGE_JS);
    assert!(
        patched.contains(PATCH_TOKEN),
        "debug-patched JS must contain the patch token. Got:\n{patched}"
    );
    assert!(
        !patched.contains("\"/static/islands-core/islands_core.js\": import1,"),
        "pre-patch needle must be replaced. Got:\n{patched}"
    );
}

#[test]
fn patch_token_survives_release_hashing_rewrite() {
    // Order matches production: patch first (build-wasm), hash rewrite second
    // (post_build_hashing_pass).
    let after_patch = patch_page_source(FAKE_PAGE_JS);
    let after_hash = rewrite_basenames_in_js(&after_patch, &rename_map());

    assert!(
        after_hash.contains(PATCH_TOKEN),
        "release-hashed JS must still contain the patch token. Got:\n{after_hash}"
    );

    // The importObject KEY for islands_core.js must keep the unhashed basename.
    let expected_key_line = format!(r#""/static/islands-core/islands_core.js": {PATCH_TOKEN}"#);
    assert!(
        after_hash.contains(&expected_key_line),
        "importObject key for islands_core.js must remain unhashed. Got:\n{after_hash}"
    );

    // The bare import specifier must use the HASHED basename (browser fetches it).
    let expected_import_line =
        r#"import * as import1 from "/static/islands-core/islands_core.a1b2c3d4.js""#;
    assert!(
        after_hash.contains(expected_import_line),
        "bare import specifier must be rewritten to the hashed basename. Got:\n{after_hash}"
    );

    // The WASM new-URL form must use the hashed basename.
    assert!(
        after_hash.contains("page_home.cafef00d_bg.wasm"),
        "new URL('page_home_bg.wasm', ...) must be rewritten. Got:\n{after_hash}"
    );
}

#[test]
fn already_patched_source_is_idempotent() {
    let patched_once = patch_page_source(FAKE_PAGE_JS);
    let patched_twice = patch_page_source(&patched_once);
    assert_eq!(
        patched_once, patched_twice,
        "patch_page_source must be idempotent"
    );
}