#![cfg(all(feature = "bin", feature = "ts"))]
use std::fs;
use std::path::PathBuf;
use std::process::{Command, Stdio};
use std::sync::atomic::{AtomicU32, Ordering};
const BURN: &str = env!("CARGO_BIN_EXE_burn");
static DIR_CTR: AtomicU32 = AtomicU32::new(0);
fn scratch(label: &str) -> PathBuf {
let n = DIR_CTR.fetch_add(1, Ordering::Relaxed);
let pid = std::process::id();
let dir = std::env::temp_dir().join(format!("burn_b8_{label}_{pid}_{n}"));
fs::create_dir_all(&dir).unwrap();
dir
}
fn run_burn_file(script: &PathBuf) -> std::process::Output {
Command::new(BURN)
.env("BURN_QUIET", "1")
.env("BURN_SHARDS", "2")
.arg(script)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
.expect("spawn burn")
}
fn assert_ok(out: &std::process::Output, what: &str) {
assert!(
out.status.success(),
"{what} FAILED\nstdout: {}\nstderr: {}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
);
}
#[test]
fn ts_drops_variable_type_annotations() {
let dir = scratch("var_anno");
let script = dir.join("vars.ts");
fs::write(
&script,
"const n: number = 42;\n\
const s: string = 'forty-two';\n\
console.log(n, s);",
)
.unwrap();
let out = run_burn_file(&script);
let _ = fs::remove_dir_all(&dir);
assert_ok(&out, "var type annotations");
assert_eq!(String::from_utf8_lossy(&out.stdout).trim(), "42 forty-two");
}
#[test]
fn ts_drops_function_signatures() {
let dir = scratch("fn_sig");
let script = dir.join("fn.ts");
fs::write(
&script,
"function add(a: number, b: number): number {\n\
return a + b;\n\
}\n\
console.log(add(7, 35));",
)
.unwrap();
let out = run_burn_file(&script);
let _ = fs::remove_dir_all(&dir);
assert_ok(&out, "function signatures");
assert_eq!(String::from_utf8_lossy(&out.stdout).trim(), "42");
}
#[test]
fn ts_drops_interface_declarations() {
let dir = scratch("iface");
let script = dir.join("iface.ts");
fs::write(
&script,
"interface Point { x: number; y: number; }\n\
const p: Point = { x: 3, y: 4 };\n\
console.log(p.x * p.x + p.y * p.y);",
)
.unwrap();
let out = run_burn_file(&script);
let _ = fs::remove_dir_all(&dir);
assert_ok(&out, "interface drop");
assert_eq!(String::from_utf8_lossy(&out.stdout).trim(), "25");
}
#[test]
fn ts_drops_type_aliases() {
let dir = scratch("alias");
let script = dir.join("alias.ts");
fs::write(
&script,
"type Numeric = number | bigint;\n\
const v: Numeric = 100;\n\
console.log(v);",
)
.unwrap();
let out = run_burn_file(&script);
let _ = fs::remove_dir_all(&dir);
assert_ok(&out, "type alias drop");
assert_eq!(String::from_utf8_lossy(&out.stdout).trim(), "100");
}
#[test]
fn ts_strips_generic_parameters() {
let dir = scratch("generic");
let script = dir.join("generic.ts");
fs::write(
&script,
"function identity<T>(x: T): T { return x; }\n\
console.log(identity<number>(123));\n\
console.log(identity<string>('hi'));",
)
.unwrap();
let out = run_burn_file(&script);
let _ = fs::remove_dir_all(&dir);
assert_ok(&out, "generics");
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("123"), "stdout: {stdout}");
assert!(stdout.contains("hi"), "stdout: {stdout}");
}
#[test]
fn ts_non_const_enum_emits_runtime_object() {
let dir = scratch("enum_rt");
let script = dir.join("enum.ts");
fs::write(
&script,
"enum Color { Red = 1, Green = 2, Blue = 3 }\n\
console.log(Color.Red, Color.Green, Color.Blue);\n\
console.log(Color[1]);",
)
.unwrap();
let out = run_burn_file(&script);
let _ = fs::remove_dir_all(&dir);
assert_ok(&out, "non-const enum");
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("1 2 3"), "stdout: {stdout}");
assert!(stdout.contains("Red"), "reverse map: {stdout}");
}
#[test]
fn ts_drops_import_type_specifier() {
let dir = scratch("import_type");
fs::write(
dir.join("util.ts"),
"export function greet(s: string): string { return 'hi, ' + s; }\n\
export type Who = string;",
)
.unwrap();
let script = dir.join("main.ts");
fs::write(
&script,
"import { greet } from './util';\n\
import type { Who } from './util';\n\
const name: Who = 'afterburner';\n\
console.log(greet(name));",
)
.unwrap();
let out = run_burn_file(&script);
let _ = fs::remove_dir_all(&dir);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
!stderr.contains("'Who'") && !stderr.contains("interface"),
"type-only imports leaked into runtime: {stderr}"
);
}
#[test]
fn ts_drops_as_and_non_null_assertions() {
let dir = scratch("cast");
let script = dir.join("cast.ts");
fs::write(
&script,
"const maybe: unknown = { toString: () => 'cast-ok' };\n\
const s = (maybe as { toString(): string }).toString();\n\
const forced = (maybe as any)!;\n\
console.log(s);\n\
console.log(typeof forced);",
)
.unwrap();
let out = run_burn_file(&script);
let _ = fs::remove_dir_all(&dir);
assert_ok(&out, "as + non-null assertion");
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("cast-ok"), "stdout: {stdout}");
assert!(stdout.contains("object"), "stdout: {stdout}");
}
#[test]
fn ts_mts_extension_transpiles() {
let dir = scratch("mts");
let script = dir.join("m.mts");
fs::write(&script, "const n: number = 1 + 1;\nconsole.log(n);").unwrap();
let out = run_burn_file(&script);
let _ = fs::remove_dir_all(&dir);
assert_ok(&out, ".mts transpile");
assert_eq!(String::from_utf8_lossy(&out.stdout).trim(), "2");
}
#[test]
fn ts_cts_extension_transpiles() {
let dir = scratch("cts");
let script = dir.join("c.cts");
fs::write(
&script,
"const s: string = 'hello ' + 'cts';\nconsole.log(s);",
)
.unwrap();
let out = run_burn_file(&script);
let _ = fs::remove_dir_all(&dir);
assert_ok(&out, ".cts transpile");
assert_eq!(String::from_utf8_lossy(&out.stdout).trim(), "hello cts");
}
#[test]
fn tsx_rejected_with_typed_error() {
let dir = scratch("tsx");
let script = dir.join("jsx.tsx");
fs::write(&script, "const x: number = 1;\nconsole.log(x);").unwrap();
let out = run_burn_file(&script);
let _ = fs::remove_dir_all(&dir);
assert!(!out.status.success(), ".tsx should error out");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("JSX") || stderr.contains("tsx"),
"expected JSX-rejection message, got: {stderr}"
);
}
#[test]
fn syntax_error_reports_filename() {
let dir = scratch("syntax");
let script = dir.join("bad.ts");
fs::write(&script, "const x: number = ;;;\n").unwrap();
let out = run_burn_file(&script);
let _ = fs::remove_dir_all(&dir);
assert!(!out.status.success(), "syntax error should fail");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("bad.ts") || stderr.contains("parse"),
"parse error should mention the file: {stderr}"
);
}
#[test]
fn require_js_sibling_from_ts_entry_works() {
let dir = scratch("mixed");
fs::write(
dir.join("lib.js"),
"module.exports = { square: (n) => n * n };",
)
.unwrap();
let script = dir.join("main.ts");
fs::write(
&script,
"const lib: { square(n: number): number } = require('./lib');\n\
console.log(lib.square(6));",
)
.unwrap();
let out = run_burn_file(&script);
let _ = fs::remove_dir_all(&dir);
assert_ok(&out, "ts entry requires .js sibling");
assert_eq!(String::from_utf8_lossy(&out.stdout).trim(), "36");
}