use std::fs;
use super::{build_app, js_under, scratch, write_files};
const INDEX_HTML: &str = r#"<!doctype html><html><head><title>t</title></head><body><script type="module" src="/src/entry.js"></script></body></html>"#;
#[tokio::test]
async fn the_build_hook_gate_never_skips_a_hook_a_plugin_acts_on() {
let root = scratch("hookgate");
write_files(
&root,
&[
(
"package.json",
r#"{"name":"hookgate-app","version":"1.0.0"}"#,
),
(
"src/entry.js",
r#"import { virt } from "virtual:gate-info";
import { special } from "./data.special";
console.log("gate-entry", virt, special);
"#,
),
("src/data.special", "placeholder\n"),
("index.html", INDEX_HTML),
(
"oj.plugins.mjs",
r#"export default [
{
name: "gate-virtual",
resolveId(id) {
if (id === "virtual:gate-info") return "\0gate-info";
},
load(id) {
if (id === "\0gate-info") return 'export const virt = "virtual-crossed";';
},
},
{
name: "gate-special-load",
load: {
filter: { id: /\.special$/ },
handler(id) {
return 'export const special = "special-load-crossed";';
},
},
},
{
name: "gate-transform-match",
transform: {
filter: { id: /entry\.js$/ },
handler(code) {
return { code: code + '\nconsole.log("regex-transform-crossed");', map: null };
},
},
},
{
name: "gate-transform-never",
transform: {
filter: { id: /\.does-not-exist$/ },
handler(code) {
return { code: code + '\nconsole.log("never-transform-LEAKED");', map: null };
},
},
},
{
name: "gate-transform-string-filter",
transform: {
filter: { id: "**/entry.js" },
handler(code, id) {
if (!id.endsWith("entry.js")) return null;
return { code: code + '\nconsole.log("string-transform-crossed");', map: null };
},
},
},
{
name: "gate-module-parsed",
moduleParsed(info) {
globalThis.__ojGateSeen = globalThis.__ojGateSeen || [];
globalThis.__ojGateSeen.push(info.id);
},
generateBundle() {
this.emitFile({ type: "asset", fileName: "module-parsed.txt", source: (globalThis.__ojGateSeen || []).join("\n") });
},
},
{
name: "gate-html",
transformIndexHtml(html) {
return html.replace("</head>", '<meta name="gate" content="html-crossed" /></head>');
},
},
];
"#,
),
],
);
build_app(&root).await.expect("the hook-gate app builds");
let dist = root.join("dist");
let assets = js_under(&dist.join("assets"));
assert!(
assets.contains("virtual-crossed"),
"function-form resolveId+load crossed the gate:\n{assets}"
);
assert!(
assets.contains("special-load-crossed"),
"RegExp-filtered load crossed for a matching id:\n{assets}"
);
assert!(
assets.contains("regex-transform-crossed"),
"RegExp-filtered transform crossed for a matching id:\n{assets}"
);
assert!(
assets.contains("string-transform-crossed"),
"string-filtered plugin stayed unfiltered and crossed:\n{assets}"
);
assert!(
!assets.contains("never-transform-LEAKED"),
"non-matching filtered transform did not fire:\n{assets}"
);
let html = fs::read_to_string(dist.join("index.html")).expect("dist/index.html");
assert!(
html.contains("html-crossed"),
"transformIndexHtml still runs:\n{html}"
);
let parsed = fs::read_to_string(dist.join("module-parsed.txt"))
.expect("generateBundle wrote module-parsed.txt");
let ids: Vec<&str> = parsed.lines().filter(|l| !l.is_empty()).collect();
assert!(
ids.iter().any(|id| id.ends_with("entry.js")),
"moduleParsed saw the entry: {ids:?}"
);
assert!(
ids.iter().any(|id| id.contains("gate-info")),
"moduleParsed saw the virtual module: {ids:?}"
);
assert!(
ids.iter().any(|id| id.ends_with(".special")),
"moduleParsed saw the plugin-loaded module: {ids:?}"
);
}
#[tokio::test]
async fn a_genuinely_filtered_plan_still_crosses_for_its_matches() {
let root = scratch("hookgate-strict");
write_files(
&root,
&[
("package.json", r#"{"name":"hookgate2","version":"1.0.0"}"#),
(
"src/entry.js",
"import { special } from \"./data.special.js\";\nimport { other } from \"./other.js\";\nconsole.log(special, other);\n",
),
(
"src/data.special.js",
"export const special = \"placeholder\";\n",
),
("src/other.js", "export const other = 1;\n"),
("index.html", INDEX_HTML),
(
"oj.plugins.mjs",
r#"export default [
{
name: "strict-load",
load: {
filter: { id: /\.special\.js$/ },
handler() {
return 'export const special = "strict-load-crossed";';
},
},
},
{
name: "strict-transform",
transform: {
filter: { id: /entry\.js$/ },
handler(code) {
return { code: code + '\nconsole.log("strict-transform-crossed");', map: null };
},
},
},
];
"#,
),
],
);
build_app(&root)
.await
.expect("the strict-filter app builds");
let [_, load, transform] = gate_counts(&root);
assert!(load > 0, "the filtered load plan skipped at least one RPC");
assert!(
transform > 0,
"the filtered transform plan skipped at least one RPC"
);
let assets = js_under(&root.join("dist/assets"));
assert!(
assets.contains("strict-load-crossed"),
"filtered load still crossed for its match:\n{assets}"
);
assert!(
assets.contains("strict-transform-crossed"),
"filtered transform still crossed for its match:\n{assets}"
);
}
#[tokio::test]
async fn a_perl_class_code_filter_fails_open() {
let root = scratch("hookgate-boundary");
write_files(
&root,
&[
("package.json", r#"{"name":"hookgate3","version":"1.0.0"}"#),
(
"src/entry.js",
"import { special } from \"./data.special.js\";\nimport { other } from \"./other.js\";\nconsole.log(special, other);\n",
),
(
"src/data.special.js",
"export const special = \"placeholder\";\n",
),
("src/other.js", "export const other = 1;\n"),
("index.html", INDEX_HTML),
(
"oj.plugins.mjs",
r#"export default [
{
name: "strict-load",
load: {
filter: { id: /\.special\.js$/ },
handler() {
return 'export const special = "strict-load-crossed";';
},
},
},
{
name: "boundary-transform",
transform: {
filter: { code: /\bimport\b/ },
handler(code) {
return { code: code + '\nconsole.log("boundary-transform-crossed");', map: null };
},
},
},
];
"#,
),
],
);
build_app(&root)
.await
.expect("the boundary-filter app builds");
let assets = js_under(&root.join("dist/assets"));
assert!(
assets.contains("boundary-transform-crossed") && assets.contains("strict-load-crossed"),
"the boundary transform and the filtered load both crossed:\n{assets}"
);
let [_, load, transform] = gate_counts(&root);
assert_eq!(
transform, 0,
"a perl-class code filter pins transform unfiltered (no skips)"
);
assert!(
load > 0,
"load gating still active alongside the pinned transform"
);
}
fn gate_counts(root: &std::path::Path) -> [u64; 3] {
match crate::build::gate_skips(root).as_slice() {
[counts] => *counts,
other => panic!(
"expected one closeBundle gate report for {}, got {other:?}",
root.display()
),
}
}