use std::fs;
use std::process::Command;
use mini_build::{Builder, CssOptions, CssTool, JsOptions, JsTool};
use tempfile::TempDir;
fn available(binary: &str) -> bool {
let found = Command::new(binary)
.arg("--version")
.output()
.map(|out| out.status.success())
.unwrap_or(false);
if !found {
eprintln!("SKIPPING: {binary} is not on PATH — install it to run this test");
}
found
}
#[test]
fn css_bundle_and_minify_produces_minified_concatenated_output() {
if !available("lightningcss") {
return;
}
let src = TempDir::new().unwrap();
let out = TempDir::new().unwrap();
fs::write(
src.path().join("reset.css"),
"* {\n margin: 0;\n padding: 0;\n}\n",
)
.unwrap();
fs::write(
src.path().join("theme.css"),
"body {\n background: white;\n}\n",
)
.unwrap();
Builder::new(out.path())
.unwrap()
.source_folder(src.path())
.unwrap()
.css_tool(
CssTool::LightningCss,
CssOptions::new().bundle(true).minify(true),
)
.build()
.expect("bundle+minify build");
let content = fs::read_to_string(out.path().join("styles.css")).unwrap();
assert!(content.contains("margin"), "expected reset CSS in bundle");
assert!(
content.contains("background"),
"expected theme CSS in bundle"
);
assert!(
!content.contains('\n'),
"minified output should not contain source formatting newlines, got: {content}"
);
}
#[test]
fn js_bundle_resolves_the_entrys_module_graph() {
if !available("esbuild") {
return;
}
let src = TempDir::new().unwrap();
let out = TempDir::new().unwrap();
fs::write(
src.path().join("helper.js"),
"export const greeting = 'hello from helper';\n",
)
.unwrap();
fs::write(
src.path().join("main.js"),
"import { greeting } from './helper.js';\nconsole.log(greeting);\n",
)
.unwrap();
Builder::new(out.path())
.unwrap()
.source_folder(src.path())
.unwrap()
.js_tool(
JsTool::Esbuild,
JsOptions::new().bundle_entry(&src.path().join("main.js"), "bundle.js"),
)
.unwrap()
.build()
.expect("js bundle build");
let content = fs::read_to_string(out.path().join("bundle.js")).unwrap();
assert!(
content.contains("hello from helper"),
"the imported module's content must be inlined into the bundle, got: {content}"
);
assert!(
!content.contains("import {"),
"the import statement should be resolved away, got: {content}"
);
}
#[test]
fn malformed_css_degrades_to_a_raw_copy_in_per_file_mode() {
if !available("lightningcss") {
return;
}
let src = TempDir::new().unwrap();
let out = TempDir::new().unwrap();
let malformed = "this is not valid css at all {{{ ;;; ";
fs::write(src.path().join("broken.css"), malformed).unwrap();
Builder::new(out.path())
.unwrap()
.source_folder(src.path())
.unwrap()
.css_tool(CssTool::LightningCss, CssOptions::new().minify(true))
.build()
.expect("a malformed source must not fail the build");
assert_eq!(
fs::read_to_string(out.path().join("broken.css")).unwrap(),
malformed,
"a file the tool rejected should reach the output as its original bytes"
);
}
#[test]
fn prune_removes_a_stale_bundle_when_no_css_sources_remain() {
if !available("lightningcss") {
return;
}
let src = TempDir::new().unwrap();
let out = TempDir::new().unwrap();
fs::write(out.path().join("styles.css"), "/* stale */").unwrap();
Builder::new(out.path())
.unwrap()
.source_folder(src.path())
.unwrap()
.css_tool(
CssTool::LightningCss,
CssOptions::new().bundle(true).minify(true),
)
.prune_output()
.build()
.expect("build with no css sources");
assert!(
!out.path().join("styles.css").exists(),
"prune should delete the bundle when no sources remain to regenerate it"
);
}
#[test]
fn same_basename_in_different_directories_survives_batching() {
if !available("lightningcss") {
return;
}
let src = TempDir::new().unwrap();
let out = TempDir::new().unwrap();
fs::create_dir_all(src.path().join("one")).unwrap();
fs::create_dir_all(src.path().join("two")).unwrap();
fs::write(
src.path().join("one/shared.css"),
".from-one { color: red }\n",
)
.unwrap();
fs::write(
src.path().join("two/shared.css"),
".from-two { color: blue }\n",
)
.unwrap();
Builder::new(out.path())
.unwrap()
.source_folder(src.path())
.unwrap()
.css_tool(
CssTool::LightningCss,
CssOptions::new().bundle(true).minify(true),
)
.build()
.expect("bundle build");
let bundle = fs::read_to_string(out.path().join("styles.css")).unwrap();
assert!(
bundle.contains("from-one"),
"the first shared.css is missing from the bundle: {bundle}"
);
assert!(
bundle.contains("from-two"),
"the second shared.css was lost — one output overwrote the other: {bundle}"
);
}
#[test]
fn bundle_order_is_sorted_by_path_not_by_batch() {
if !available("lightningcss") {
return;
}
let src = TempDir::new().unwrap();
let out = TempDir::new().unwrap();
fs::create_dir_all(src.path().join("b-dir")).unwrap();
fs::create_dir_all(src.path().join("a-dir")).unwrap();
fs::write(src.path().join("b-dir/z.css"), ".b-dir-z{color:red}\n").unwrap();
fs::write(src.path().join("a-dir/a.css"), ".a-dir-a{color:blue}\n").unwrap();
Builder::new(out.path())
.unwrap()
.source_folder(src.path())
.unwrap()
.css_tool(CssTool::LightningCss, CssOptions::new().bundle(true))
.build()
.expect("bundle build");
let bundle = fs::read_to_string(out.path().join("styles.css")).unwrap();
let a_pos = bundle.find("a-dir-a").expect("a-dir content present");
let b_pos = bundle.find("b-dir-z").expect("b-dir content present");
assert!(
a_pos < b_pos,
"a-dir/a.css sorts before b-dir/z.css and must appear first: {bundle}"
);
}
#[test]
fn one_malformed_file_does_not_stop_its_neighbours_being_minified() {
if !available("lightningcss") {
return;
}
let src = TempDir::new().unwrap();
let out = TempDir::new().unwrap();
let malformed = "this is not valid css at all {{{ ;;; ";
fs::write(src.path().join("broken.css"), malformed).unwrap();
fs::write(
src.path().join("good.css"),
".good {\n color: red;\n margin: 0;\n}\n",
)
.unwrap();
Builder::new(out.path())
.unwrap()
.source_folder(src.path())
.unwrap()
.css_tool(CssTool::LightningCss, CssOptions::new().minify(true))
.build()
.expect("a malformed source must not fail the build");
assert_eq!(
fs::read_to_string(out.path().join("broken.css")).unwrap(),
malformed,
"the rejected file should reach the output as its original bytes"
);
let good = fs::read_to_string(out.path().join("good.css")).unwrap();
assert!(
!good.contains('\n') && good.contains("good"),
"the valid neighbour must still be minified, not copied raw: {good:?}"
);
}
#[test]
fn js_per_file_mode_minifies_several_files_in_one_batch() {
if !available("esbuild") {
return;
}
let src = TempDir::new().unwrap();
let out = TempDir::new().unwrap();
fs::write(
src.path().join("a.js"),
"export const alpha = {\n value: 1,\n};\n",
)
.unwrap();
fs::write(
src.path().join("b.js"),
"export const beta = {\n value: 2,\n};\n",
)
.unwrap();
Builder::new(out.path())
.unwrap()
.source_folder(src.path())
.unwrap()
.js_tool(JsTool::Esbuild, JsOptions::new().minify(true))
.unwrap()
.build()
.expect("js per-file build");
for (name, marker) in [("a.js", "alpha"), ("b.js", "beta")] {
let content = fs::read_to_string(out.path().join(name)).unwrap();
assert!(
content.contains(marker),
"{name} lost its content: {content}"
);
assert!(
!content.contains(" value"),
"{name} was copied rather than minified: {content}"
);
}
}
#[test]
fn js_same_basename_in_different_directories_survives_batching() {
if !available("esbuild") {
return;
}
let src = TempDir::new().unwrap();
let out = TempDir::new().unwrap();
fs::create_dir_all(src.path().join("one")).unwrap();
fs::create_dir_all(src.path().join("two")).unwrap();
fs::write(
src.path().join("one/shared.js"),
"export const fromOne = 1;\n",
)
.unwrap();
fs::write(
src.path().join("two/shared.js"),
"export const fromTwo = 2;\n",
)
.unwrap();
Builder::new(out.path())
.unwrap()
.source_folder(src.path())
.unwrap()
.js_tool(JsTool::Esbuild, JsOptions::new().minify(true))
.unwrap()
.build()
.expect("js per-file build");
assert!(
fs::read_to_string(out.path().join("one/shared.js"))
.unwrap()
.contains("fromOne"),
"one/shared.js is missing or holds the wrong content"
);
assert!(
fs::read_to_string(out.path().join("two/shared.js"))
.unwrap()
.contains("fromTwo"),
"two/shared.js was overwritten by its namesake"
);
}
#[test]
fn one_malformed_js_file_does_not_stop_its_neighbours_being_minified() {
if !available("esbuild") {
return;
}
let src = TempDir::new().unwrap();
let out = TempDir::new().unwrap();
let malformed = "function ( { this is not javascript\n";
fs::write(src.path().join("broken.js"), malformed).unwrap();
fs::write(
src.path().join("good.js"),
"export const good = {\n value: 1,\n};\n",
)
.unwrap();
Builder::new(out.path())
.unwrap()
.source_folder(src.path())
.unwrap()
.js_tool(JsTool::Esbuild, JsOptions::new().minify(true))
.unwrap()
.build()
.expect("a malformed source must not fail the build");
assert_eq!(
fs::read_to_string(out.path().join("broken.js")).unwrap(),
malformed,
"the rejected file should reach the output as its original bytes"
);
let good = fs::read_to_string(out.path().join("good.js")).unwrap();
assert!(
!good.contains(" value") && good.contains("good"),
"the valid neighbour must still be minified: {good:?}"
);
}