Files
pbakaus_impeccable/crates/bundle/src/lib.rs
T
Paul BakausandClaude Fable 5.1 453a6e1500 bundle: the page JS and the bundler become a library crate downstream packs can reuse
The in-page bundle, the extension pieces, the registry JSON and the
wasm-pack call were reachable only through `cargo xtask bundle`, which read
`browser-bundle/*.js` from the repo root. A downstream crate that links
impeccable-core + impeccable-wasm with its own rule pack had to copy the
page JS to produce a detector bundle for its module.

They move to `impeccable-bundle` (crates/bundle), which embeds every
`browser-bundle/*.js` with `include_str!` and exposes `in_page_bundle`,
`extension_pieces`, `registry_json`, `check_capture_contract` and
`wasm_pack_build`. Nothing writes files or exits the process; the caller
places the bytes. `registry_json` now reads `all_antipatterns()`, so an
installed pack's rows land in `antipatterns.json` too (no built-in change).

xtask becomes the workspace's caller and writes the same files to the same
places; `cargo xtask bundle` is byte-identical, tracked live asset included.
`IMPECCABLE_BUNDLE_SKIP_WASM_PACK` is the skip switch's new name, the old
`IMPECCABLE_XTASK_SKIP_WASM_PACK` still works.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY
2026-09-03 10:49:42 -07:00

442 lines
18 KiB
Rust

//! 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, &registry);
//! # 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<u8>,
/// `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<String> {
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<String>); 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<serde_json::Value> = 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 <crate_dir> --target no-modules --release \
/// --no-typescript --no-pack --out-dir <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<u8>), 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<String> = 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<String> = 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::Value> = 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}");
}
}
}
}