use std::fs; use std::path::Path; use std::process::{Command, Output}; fn run(dir: &Path, files: &[(&str, &str)]) -> Output { for (name, source) in files { fs::write(dir.join(name), source).unwrap(); } Command::new(env!("CARGO_BIN_EXE_avril")) .arg("run") .current_dir(dir) .arg(files[0].0) .output() .unwrap() } fn success(output: Output) -> String { assert!( output.status.success(), "{}", String::from_utf8_lossy(&output.stderr) ); String::from_utf8(output.stdout).unwrap() } #[test] fn loads_a_mixed_typescript_and_javascript_graph() { let dir = tempfile::tempdir().unwrap(); let output = run( dir.path(), &[ ( "main.ts", r#" import type { Missing } from "./missing.ts"; import { Shape } from "./also-missing.ts"; import { type Count, count, message } from "./dep.mts"; import { count as again } from "././dep.mts"; import { describe } from "./bridge.js"; const shape: Shape | Missing | undefined = undefined; const total: Count = await Promise.resolve(count + again); console.log(message, total, describe(), shape); "#, ), ( "dep.mts", r#" import { getMessage } from "./cycle.ts"; export type Count = number; export const message: string = "hello"; export const count = (globalThis.count = (globalThis.count ?? 0) + 1) as Count; export const cycled = () => getMessage(); "#, ), ( "cycle.ts", r#" import { message } from "./dep.mts"; export function getMessage(): string { return message; } "#, ), ( "bridge.js", r#" import { cycled } from "./dep.mts"; export function describe() { return `${cycled()} from js`; } "#, ), ], ); assert_eq!(success(output), "hello 2 hello from js undefined\n"); } #[test] fn runs_typescript_runtime_constructs() { let dir = tempfile::tempdir().unwrap(); let output = run( dir.path(), &[( "main.ts", r#" enum Color { Red, Green = "green", Blue = Red + 2 } const enum Size { Small = 1, Large = Small * 10 } namespace Geometry { export const origin = { x: 0, y: 0 }; export namespace Nested { export const depth = 2; } } abstract class Base { declare readonly kind: string; abstract value(): T; } class Point extends Base { constructor(public x: number, private readonly y: number = 2) { super(); } value(): number { return this.x + this.y!; } } const config = { size: Size.Large } satisfies Record; const unchecked: number = "no type checking"; console.log( Color.Red, Color[0], Color.Green, Color.Blue, config.size, Geometry.origin.x, Geometry.Nested.depth, new Point(1).value(), (3 as unknown), unchecked, ); "#, )], ); assert_eq!( success(output), "0 Red green 2 10 0 2 3 3 no type checking\n" ); } #[test] fn rejects_unsupported_and_malformed_typescript() { for (name, source, expected) in [ ("main.ts", "const value: number = ;", "main.ts:1:23"), ("main.ts", "let a = 1;\nlet a = 2;", "main.ts:1:5"), ("main.ts", "export = 1;", "Export assignment cannot be used"), ("main.ts", "import './types.d.ts';", "declaration files"), ("main.d.ts", "export {};", "declaration files"), ("main.d.mts", "export {};", "declaration files"), // Explicit `.cts` now transpiles preserving CJS, so ESM-only syntax // fails as a guest syntax error rather than "not supported". ("main.cts", "export {};", "SyntaxError"), ] { let dir = tempfile::tempdir().unwrap(); let output = run(dir.path(), &[(name, source), ("types.d.ts", "export {};")]); let stderr = String::from_utf8_lossy(&output.stderr); assert!(!output.status.success(), "{source}"); assert!(stderr.contains(expected), "{stderr}"); } } #[test] fn stack_traces_point_at_typescript_sources() { let dir = tempfile::tempdir().unwrap(); let output = run( dir.path(), &[( "main.ts", r#"type Unused = { field: string; }; function fail(message: string): never { throw new Error(message); } console.log(new Error("stack").stack); console.trace("traced"); fail("uncaught" as string); "#, )], ); let path = dir.path().canonicalize().unwrap().join("main.ts"); let path = path.display(); let stdout = String::from_utf8_lossy(&output.stdout); let stderr = String::from_utf8_lossy(&output.stderr); assert!(stdout.contains(&format!("at {path}:7:13\n")), "{stdout}"); assert!(stderr.contains(&format!("at {path}:8:9\n")), "{stderr}"); assert!( stderr.contains(&format!("at fail ({path}:5:9)\n")), "{stderr}" ); assert!(stderr.contains(&format!("at {path}:9:1")), "{stderr}"); } #[test] fn source_mapping_handles_dependency_paths_and_utf16_columns() { let dir = tempfile::tempdir().unwrap(); let line = " const emoji: string = '😀'; throw new Error(emoji);"; let source = format!( "type Dropped = {{\n name: string;\n}};\nexport function fail(): never {{\n{line}\n}}\n" ); let output = run( dir.path(), &[ ( "main.mts", "import { fail } from './dep (mapped).ts'; await Promise.resolve(); fail();", ), ("dep (mapped).ts", &source), ], ); assert!(!output.status.success()); let column = line[..line.find("new Error").unwrap()] .encode_utf16() .count() + 1; let path = dir.path().canonicalize().unwrap().join("dep (mapped).ts"); let stderr = String::from_utf8_lossy(&output.stderr); assert!( stderr.contains(&format!("at fail ({}:5:{column})", path.display())), "{stderr}" ); } #[test] fn mapped_stacks_capture_intrinsics_but_allow_custom_formatters() { let dir = tempfile::tempdir().unwrap(); let output = run( dir.path(), &[( "main.ts", r#"interface Erased { value: number; } const original = Error.prepareStackTrace; const poison = () => { throw new Error("Poisoned intrinsic"); }; Error.prototype.toString = poison; String.prototype.lastIndexOf = poison; String.prototype.slice = poison; Error.prepareStackTrace = original; console.log(new Error("mapped").stack); Error.prepareStackTrace = () => "custom"; console.log(new Error().stack); "#, )], ); let stdout = success(output); let path = dir.path().canonicalize().unwrap().join("main.ts"); assert!(stdout.starts_with("Error: mapped\n"), "{stdout}"); assert!( stdout.contains(&format!("at {}:8:13", path.display())), "{stdout}" ); assert!(stdout.ends_with("custom\n"), "{stdout}"); }