//! impeccable-bundle: the page JS and the bundler behind `cargo xtask bundle`. //! //! Every browser artifact the detector ships is this crate's output: the //! in-page bundle (`dist/detect-antipatterns-browser.js`, also the tracked //! `crates/live/assets/` copy the engine embeds and serves as `/detect.js`) //! and the five `extension/detector/` pieces. The page JS is embedded with //! `include_str!`, so a crate that depends on this one gets it without //! copying `browser-bundle/*.js` anywhere. //! //! A downstream crate that links `impeccable-core` + `impeccable-wasm` plus //! its own [`RulePack`](impeccable_core::rule_pack::RulePack) into one wasm //! module builds the same artifacts for its own module: //! //! ```no_run //! use std::path::Path; //! let (glue, wasm) = impeccable_bundle::wasm_pack_build( //! Path::new("crates/my-wasm"), //! Path::new("target/wasm-bundle"), //! &[], //! )?; //! let js = impeccable_bundle::in_page_bundle(&glue, &wasm); //! let registry = impeccable_bundle::registry_json(); //! let ext = impeccable_bundle::extension_pieces(&glue, &wasm, ®istry); //! # Ok::<(), String>(()) //! ``` //! //! The pack's rows reach [`registry_json`] once the pack is installed //! (`impeccable_core::rule_pack::install`), because the registry reads //! built-ins plus every registered slice. //! //! Nothing here writes files or exits the process: the caller decides where //! the bytes go and how a failure is reported. use base64::Engine as _; use std::borrow::Cow; use std::path::{Path, PathBuf}; use std::process::Command; macro_rules! page_js { ($($name:literal),* $(,)?) => { /// The page JS, in the order [`in_page_bundle`] concatenates it: /// `(file name, source)`. Embedded at compile time from /// `browser-bundle/`. pub const PAGE_JS: &[(&str, &str)] = &[ $(($name, include_str!(concat!("../../../browser-bundle/", $name))),)* ]; }; } page_js![ "00-header.js", "10-probe.js", "15-snapshot.js", "30-scan-common.js", "35-visual.js", "40-overlay.js", "50-scan.js", "60-offscreen.js", "99-footer.js", ]; /// One embedded page file by name. pub fn page_js(name: &str) -> Option<&'static str> { PAGE_JS.iter().find(|(n, _)| *n == name).map(|(_, src)| *src) } fn src(name: &str) -> Cow<'static, str> { let s = page_js(name).unwrap_or_else(|| panic!("browser-bundle/{name}: not embedded")); if s.ends_with('\n') { Cow::Borrowed(s) } else { Cow::Owned(format!("{s}\n")) } } /// In-page bundle concatenation order. `@@GLUE@@` is the wasm-bindgen glue, /// `@@WASM@@` the embedded module + synchronous instantiation. const ORDER: &[&str] = &[ "00-header.js", "10-probe.js", "15-snapshot.js", "@@GLUE@@", "@@WASM@@", "30-scan-common.js", "35-visual.js", "40-overlay.js", "50-scan.js", "99-footer.js", ]; /// The extension pieces, each an IIFE over a subset of the same sources plus /// a `window.*` export line. `core.js` is the wasm glue + an async loader /// (the module ships beside it as `core_bg.wasm`; no base64: the offscreen /// document fetches it) + the scan plumbing and visual-contrast orchestration /// + the offscreen session protocol. const EXT_SNAPSHOT: &[&str] = &["15-snapshot.js"]; const EXT_OVERLAY: &[&str] = &["40-overlay.js"]; const EXT_CORE: &[&str] = &["@@GLUE@@", "@@LOADER@@", "30-scan-common.js", "35-visual.js", "60-offscreen.js"]; fn push_glue(out: &mut String, glue_js: &str) { out.push_str("// --- wasm-bindgen glue (generated by cargo xtask bundle) ---\n"); out.push_str(glue_js); if !glue_js.ends_with('\n') { out.push('\n'); } } /// The in-page bundle: `dist/detect-antipatterns-browser.js` and the tracked /// `crates/live/assets/` copy. `glue_js` and `wasm` are one wasm-pack /// `--target no-modules` build (see [`wasm_pack_build`]); the module is /// embedded as base64 and instantiated synchronously at load. /// /// Deterministic: the same glue and module produce the same bytes. pub fn in_page_bundle(glue_js: &str, wasm: &[u8]) -> String { let b64 = base64::engine::general_purpose::STANDARD.encode(wasm); let mut out = String::new(); for part in ORDER { match *part { "@@GLUE@@" => push_glue(&mut out, glue_js), "@@WASM@@" => { out.push_str("// --- impeccable_wasm module (generated by cargo xtask bundle) ---\n"); out.push_str(&format!("const __IMPECCABLE_WASM_BYTES = {};\n", wasm.len())); out.push_str("const __IMPECCABLE_WASM_B64 = \""); out.push_str(&b64); out.push_str("\";\n"); out.push_str(WASM_INIT); } name => out.push_str(&src(name)), } } out } /// What goes into `extension/detector/`, from [`extension_pieces`]. pub struct ExtensionPieces { /// `snapshot.js`: the content-script page snapshot producer. pub snapshot_js: String, /// `overlay.js`: the content-script overlay UI. pub overlay_js: String, /// `core.js`: the offscreen-document wasm loader and scan session. pub core_js: String, /// `core_bg.wasm`: the module `core.js` fetches beside itself. pub core_bg_wasm: Vec, /// `antipatterns.json`: the registry slice the extension panel reads. pub antipatterns_json: String, } /// The extension pieces for one wasm build. `registry_json` is written /// through unchanged, so a caller can pass [`registry_json()`] or its own. pub fn extension_pieces(glue_js: &str, wasm: &[u8], registry_json: &str) -> ExtensionPieces { let piece = |parts: &[&str], exports: &str, what: &str| -> String { let mut p = format!( "/**\n * Impeccable extension: {what}\n * Copyright (c) 2026 Paul Bakaus\n *\n * GENERATED -- do not edit. Source: browser-bundle/*.js (+ crates/core, crates/wasm for core.js).\n * Rebuild: cargo xtask bundle\n */\n" ); p.push_str("(function () {\n"); for part in parts { match *part { "@@GLUE@@" => push_glue(&mut p, glue_js), "@@LOADER@@" => p.push_str(EXT_CORE_LOADER), name => p.push_str(&src(name)), } } p.push_str(exports); p.push_str("})();\n"); p }; ExtensionPieces { snapshot_js: piece( EXT_SNAPSHOT, "window.__impeccableSnapshot = __impeccableSnapshot;\nwindow.__impeccableCreateDrawableIO = __createDrawableIO;\n", "snapshot.js, the content-script page snapshot producer (measurement only)", ), overlay_js: piece( EXT_OVERLAY, "window.__impeccableCreateOverlay = createImpeccableOverlay;\n", "overlay.js, the content-script overlay UI (draws a findings list; no rules)", ), core_js: piece( EXT_CORE, "", "core.js, the offscreen-document WASM core loader + scan session (rules run in core_bg.wasm)", ), core_bg_wasm: wasm.to_vec(), antipatterns_json: registry_json.to_string(), } } /// The `atob` + synchronous instantiation that runs at bundle load. Chrome /// (verified on 151, headless and headed) compiles multi-MB modules /// synchronously on the main thread; the historical 4 KB limit no longer /// applies to `new WebAssembly.Module`. A CSP without 'wasm-unsafe-eval' /// throws here; consumers handle that per docs/WASM-BUNDLE.md. const WASM_INIT: &str = r#"function __impeccableWasmBytes() { const bin = atob(__IMPECCABLE_WASM_B64); const bytes = new Uint8Array(bin.length); for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i); return bytes; } let __impeccable = null; let __impeccableInitError = null; try { wasm_bindgen.initSync({ module: __impeccableWasmBytes() }); __impeccable = wasm_bindgen; } catch (e) { __impeccableInitError = e; } "#; /// The offscreen document's core loader: fetch the module beside the script /// and instantiate asynchronously (the extension's own CSP carries /// 'wasm-unsafe-eval'). Also stubs the live-page probe namespace the glue /// imports, so a call that would need a page fails loudly instead of with a /// ReferenceError. const EXT_CORE_LOADER: &str = r#"const __impeccableDom = new Proxy({}, { get() { throw new Error('[impeccable] the offscreen document has no live page; load a snapshot first'); }, }); async function __impeccableLoadCore() { await wasm_bindgen({ module_or_path: chrome.runtime.getURL('detector/core_bg.wasm') }); return wasm_bindgen; } "#; /// The capture contract: the property and state lists in `15-snapshot.js` /// must equal the core's (`STYLE_PROPS`, `PSEUDO_PROPS` in snapshot.rs; /// `STATE_PSEUDOS` in selector.rs), or a rule reads a column the capture did /// not write. Returns the mismatch report on drift. pub fn check_capture_contract() -> Result<(), String> { let snapshot_js = page_js("15-snapshot.js").expect("15-snapshot.js embedded"); fn js_list(src: &str, name: &str) -> Vec { let start = src .find(&format!("const {name} = [")) .unwrap_or_else(|| panic!("15-snapshot.js: {name} not found")); let rest = &src[start..]; let end = rest.find("];").expect("list end"); rest[..end] .split('"') .skip(1) .step_by(2) .map(|s| s.to_string()) .collect() } let pairs: [(&str, Vec); 3] = [ ("__SNAP_STYLE_PROPS", impeccable_core::browser::snapshot::STYLE_PROPS.iter().map(|s| s.to_string()).collect()), ("__SNAP_PSEUDO_PROPS", impeccable_core::browser::snapshot::PSEUDO_PROPS.iter().map(|s| s.to_string()).collect()), ("__SNAP_STATE_PSEUDOS", impeccable_core::browser::selector::STATE_PSEUDOS.iter().map(|s| s.to_string()).collect()), ]; for (name, want) in pairs { let have = js_list(snapshot_js, name); if have != want { return Err(format!( "browser-bundle/15-snapshot.js {name} differs from the core's list\n js: {have:?}\n core: {want:?}" )); } } Ok(()) } /// `antipatterns.json`: `{ id, name, category, description }` per rule, in /// registry order (built-ins first, then any installed rule pack's rows), as /// 2-space JSON with a trailing newline. This is the shape `bun run build` /// and the extension panel read. pub fn registry_json() -> String { let rows: Vec = impeccable_core::registry::all_antipatterns() .map(|ap| { serde_json::json!({ "id": ap.id, "name": ap.name, "category": ap.category, "description": ap.description, }) }) .collect(); let mut s = serde_json::to_string_pretty(&rows).expect("registry json"); s.push('\n'); s } /// Build the wasm module for `crate_dir` and read back the wasm-bindgen glue /// and the `.wasm`: /// /// ```text /// wasm-pack build --target no-modules --release \ /// --no-typescript --no-pack --out-dir --out-name impeccable /// ``` /// /// `extra_cargo_args` are passed to cargo after `--` (the workspace uses this /// for `--features pure-exports`). `WASM_PACK` names the binary when it is /// not on PATH; `IMPECCABLE_BUNDLE_SKIP_WASM_PACK=1` (or the older /// `IMPECCABLE_XTASK_SKIP_WASM_PACK=1`) reuses whatever `out_dir` already /// holds, for iterating on the page JS alone. /// /// A downstream crate passes its own crate dir, so the module it gets back is /// the engine plus its rule pack. pub fn wasm_pack_build( crate_dir: &Path, out_dir: &Path, extra_cargo_args: &[&str], ) -> Result<(String, Vec), String> { let skip = std::env::var_os("IMPECCABLE_BUNDLE_SKIP_WASM_PACK").is_some() || std::env::var_os("IMPECCABLE_XTASK_SKIP_WASM_PACK").is_some(); if !skip { let wasm_pack = std::env::var("WASM_PACK").unwrap_or_else(|_| "wasm-pack".to_string()); let mut cmd = Command::new(&wasm_pack); cmd.arg("build") .arg(crate_dir) .arg("--target") .arg("no-modules") .arg("--release") .arg("--no-typescript") .arg("--no-pack") .arg("--out-dir") .arg(out_dir) .arg("--out-name") .arg("impeccable") // Size profile for the module; native builds keep opt-level 3. .env("CARGO_PROFILE_RELEASE_OPT_LEVEL", "z") // wasm-pack refuses to build a `cdylib` whose Cargo.toml lives in // a workspace with `[profile.*]` overrides only when it cannot // find the target dir; keep it explicit. .env("CARGO_TARGET_DIR", target_dir_for(crate_dir)); if !extra_cargo_args.is_empty() { cmd.arg("--"); for arg in extra_cargo_args { cmd.arg(arg); } } let status = cmd .status() .map_err(|e| format!("wasm-pack build: failed to spawn {wasm_pack}: {e}"))?; if !status.success() { return Err(format!("wasm-pack build failed ({status})")); } } let glue_path = out_dir.join("impeccable.js"); let wasm_path = out_dir.join("impeccable_bg.wasm"); let glue = std::fs::read_to_string(&glue_path) .map_err(|e| format!("{}: {e}", glue_path.display()))?; let wasm = std::fs::read(&wasm_path).map_err(|e| format!("{}: {e}", wasm_path.display()))?; Ok((glue, wasm)) } /// `CARGO_TARGET_DIR` for a wasm-pack run: the caller's, else the target dir /// of the workspace `crate_dir` sits in (nearest ancestor with a /// `Cargo.lock`), else the crate's own. fn target_dir_for(crate_dir: &Path) -> PathBuf { if let Some(dir) = std::env::var_os("CARGO_TARGET_DIR") { return PathBuf::from(dir); } let abs = std::fs::canonicalize(crate_dir).unwrap_or_else(|_| crate_dir.to_path_buf()); abs.ancestors() .find(|dir| dir.join("Cargo.lock").exists()) .unwrap_or(&abs) .join("target") } #[cfg(test)] mod tests { use super::*; fn bundle_dir() -> PathBuf { Path::new(env!("CARGO_MANIFEST_DIR")).join("../../browser-bundle") } #[test] fn embedded_files_match_the_directory() { let mut on_disk: Vec = std::fs::read_dir(bundle_dir()) .expect("browser-bundle/") .map(|e| e.expect("dir entry").file_name().to_string_lossy().into_owned()) .filter(|name| name.ends_with(".js")) .collect(); on_disk.sort(); let mut embedded: Vec = PAGE_JS.iter().map(|(n, _)| n.to_string()).collect(); embedded.sort(); assert_eq!( embedded, on_disk, "browser-bundle/*.js changed: update PAGE_JS (and ORDER, if the file belongs in the page bundle)" ); } #[test] fn embedded_sources_match_the_files_on_disk() { for (name, src) in PAGE_JS { let disk = std::fs::read_to_string(bundle_dir().join(name)).expect(name); assert_eq!(*src, disk, "{name} drifted from disk"); } } #[test] fn in_page_bundle_has_the_expected_skeleton() { let out = in_page_bundle("const glue = 1;\n", &[0, 97, 115, 109]); assert!(out.starts_with(page_js("00-header.js").unwrap())); assert!(out.ends_with(page_js("99-footer.js").unwrap())); assert!(out.contains("// --- wasm-bindgen glue (generated by cargo xtask bundle) ---\nconst glue = 1;\n")); assert!(out.contains("const __IMPECCABLE_WASM_BYTES = 4;\n")); // base64 of the four-byte wasm preamble. assert!(out.contains("const __IMPECCABLE_WASM_B64 = \"AGFzbQ==\";\n")); assert!(out.contains("wasm_bindgen.initSync({ module: __impeccableWasmBytes() });")); // 60-offscreen.js is an extension-only piece. assert!(!out.contains(page_js("60-offscreen.js").unwrap())); // Order: probe and snapshot before the glue, scan after it. let glue_at = out.find("const glue = 1;").unwrap(); assert!(out.find("__SNAP_STYLE_PROPS").unwrap() < glue_at); assert!(out.rfind("createImpeccableOverlay").unwrap() > glue_at); } #[test] fn extension_pieces_are_iifes_over_their_sources() { let ext = extension_pieces("const glue = 1;\n", &[0, 97, 115, 109], "[]\n"); for (what, js) in [("snapshot", &ext.snapshot_js), ("overlay", &ext.overlay_js), ("core", &ext.core_js)] { assert!(js.starts_with("/**\n * Impeccable extension: "), "{what}"); assert!(js.contains("(function () {\n"), "{what}"); assert!(js.ends_with("})();\n"), "{what}"); } assert!(ext.snapshot_js.contains("window.__impeccableSnapshot = __impeccableSnapshot;")); assert!(ext.overlay_js.contains("window.__impeccableCreateOverlay = createImpeccableOverlay;")); assert!(ext.core_js.contains("chrome.runtime.getURL('detector/core_bg.wasm')")); // The offscreen core fetches its module; nothing is base64 in there. assert!(!ext.core_js.contains("__IMPECCABLE_WASM_B64")); assert_eq!(ext.core_bg_wasm, vec![0, 97, 115, 109]); assert_eq!(ext.antipatterns_json, "[]\n"); } #[test] fn capture_contract_holds() { check_capture_contract().unwrap(); } #[test] fn registry_json_is_two_space_rows() { let json = registry_json(); assert!(json.ends_with("]\n")); assert!(json.contains("\n {\n \"id\": ")); let rows: Vec = serde_json::from_str(&json).expect("valid json"); assert_eq!(rows.len(), impeccable_core::registry::all_antipatterns().count()); for row in &rows { for key in ["id", "name", "category", "description"] { assert!(row.get(key).is_some(), "row missing {key}: {row}"); } } } }