char/avril
(wip) why'd you have to go and make things so complicaaaaated?
git clone https://git.t4t.associates/char/avril
688e96e
main
1use std:: fs; 2use std:: path:: Path ; 3use std:: process::{ Command , Output }; 4 5fn run ( dir : & Path , files : & [( & str , & str )]) ->Output { 6for ( name, source) in files{ 7 fs:: write ( dir. join ( name), source). unwrap (); 8} 9Command :: new ( env! ( "CARGO_BIN_EXE_avril" )) 10. arg ( "run" ) 11. current_dir ( dir) 12. arg ( files[ 0 ]. 0 ) 13. output () 14. unwrap () 15} 16 17fn success ( output : Output ) ->String { 18assert! ( 19 output. status . success (), 20"{}" , 21String :: from_utf8_lossy ( & output. stderr ) 22); 23String :: from_utf8 ( output. stdout ). unwrap () 24} 25 26# [ test ] 27fn loads_a_mixed_typescript_and_javascript_graph () { 28let dir = tempfile:: tempdir (). unwrap (); 29let output =run ( 30 dir. path (), 31& [ 32( 33"main.ts" , 34r#" 35import type { Missing } from "./missing.ts"; 36import { Shape } from "./also-missing.ts"; 37import { type Count, count, message } from "./dep.mts"; 38import { count as again } from "././dep.mts"; 39import { describe } from "./bridge.js"; 40const shape: Shape | Missing | undefined = undefined; 41const total: Count = await Promise.resolve(count + again); 42console.log(message, total, describe(), shape); 43"# , 44), 45( 46"dep.mts" , 47r#" 48import { getMessage } from "./cycle.ts"; 49export type Count = number; 50export const message: string = "hello"; 51export const count = (globalThis.count = (globalThis.count ?? 0) + 1) as Count; 52export const cycled = () => getMessage(); 53"# , 54), 55( 56"cycle.ts" , 57r#" 58import { message } from "./dep.mts"; 59export function getMessage(): string { return message; } 60"# , 61), 62( 63"bridge.js" , 64r#" 65import { cycled } from "./dep.mts"; 66export function describe() { return `${cycled()} from js`; } 67"# , 68), 69], 70); 71assert_eq! ( success ( output), "hello 2 hello from js undefined\n" ); 72} 73 74# [ test ] 75fn runs_typescript_runtime_constructs () { 76let dir = tempfile:: tempdir (). unwrap (); 77let output =run ( 78 dir. path (), 79& [( 80"main.ts" , 81r#" 82enum Color { Red, Green = "green", Blue = Red + 2 } 83const enum Size { Small = 1, Large = Small * 10 } 84namespace Geometry { 85export const origin = { x: 0, y: 0 }; 86export namespace Nested { export const depth = 2; } 87} 88abstract class Base<T> { 89declare readonly kind: string; 90abstract value(): T; 91} 92class Point extends Base<number> { 93constructor(public x: number, private readonly y: number = 2) { super(); } 94value(): number { return this.x + this.y!; } 95} 96const config = { size: Size.Large } satisfies Record<string, number>; 97const unchecked: number = "no type checking"; 98console.log( 99Color.Red, Color[0], Color.Green, Color.Blue, 100config.size, Geometry.origin.x, Geometry.Nested.depth, 101new Point(1).value(), <number>(3 as unknown), unchecked, 102); 103"# , 104)], 105); 106assert_eq! ( 107success ( output), 108"0 Red green 2 10 0 2 3 3 no type checking\n" 109); 110} 111 112# [ test ] 113fn rejects_unsupported_and_malformed_typescript () { 114for ( name, source, expected) in [ 115( "main.ts" , "const value: number = ;" , "main.ts:1:23" ), 116( "main.ts" , "let a = 1;\nlet a = 2;" , "main.ts:1:5" ), 117( "main.ts" , "export = 1;" , "Export assignment cannot be used" ), 118( "main.ts" , "import './types.d.ts';" , "declaration files" ), 119( "main.d.ts" , "export {};" , "declaration files" ), 120( "main.d.mts" , "export {};" , "declaration files" ), 121// Explicit `.cts` now transpiles preserving CJS, so ESM-only syntax 122// fails as a guest syntax error rather than "not supported". 123( "main.cts" , "export {};" , "SyntaxError" ), 124] { 125let dir = tempfile:: tempdir (). unwrap (); 126let output =run ( dir. path (), & [( name, source), ( "types.d.ts" , "export {};" )]); 127let stderr =String :: from_utf8_lossy ( & output. stderr ); 128assert! ( !output. status . success (), "{source}" ); 129assert! ( stderr. contains ( expected), "{stderr}" ); 130} 131} 132 133# [ test ] 134fn stack_traces_point_at_typescript_sources () { 135let dir = tempfile:: tempdir (). unwrap (); 136let output =run ( 137 dir. path (), 138& [( 139"main.ts" , 140r#"type Unused = { 141field: string; 142}; 143function fail(message: string): never { 144throw new Error(message); 145} 146console.log(new Error("stack").stack); 147console.trace("traced"); 148fail("uncaught" as string); 149"# , 150)], 151); 152let path = dir. path (). canonicalize (). unwrap (). join ( "main.ts" ); 153let path = path. display (); 154let stdout =String :: from_utf8_lossy ( & output. stdout ); 155let stderr =String :: from_utf8_lossy ( & output. stderr ); 156assert! ( stdout. contains ( & format! ( "at {path}:7:13\n" )), "{stdout}" ); 157assert! ( stderr. contains ( & format! ( "at {path}:8:9\n" )), "{stderr}" ); 158assert! ( 159 stderr. contains ( & format! ( "at fail ({path}:5:9)\n" )), 160"{stderr}" 161); 162assert! ( stderr. contains ( & format! ( "at {path}:9:1" )), "{stderr}" ); 163} 164 165# [ test ] 166fn source_mapping_handles_dependency_paths_and_utf16_columns () { 167let dir = tempfile:: tempdir (). unwrap (); 168let line =" const emoji: string = '😀'; throw new Error(emoji);" ; 169let source =format! ( 170"type Dropped = {{\n name: string;\n}};\nexport function fail(): never {{\n{line}\n}}\n" 171); 172let output =run ( 173 dir. path (), 174& [ 175( 176"main.mts" , 177"import { fail } from './dep (mapped).ts'; await Promise.resolve(); fail();" , 178), 179( "dep (mapped).ts" , & source), 180], 181); 182assert! ( !output. status . success ()); 183let column = line[ ..line. find ( "new Error" ). unwrap ()] 184. encode_utf16 () 185. count () 186 +1 ; 187let path = dir. path (). canonicalize (). unwrap (). join ( "dep (mapped).ts" ); 188let stderr =String :: from_utf8_lossy ( & output. stderr ); 189assert! ( 190 stderr. contains ( & format! ( "at fail ({}:5:{column})" , path. display ())), 191"{stderr}" 192); 193} 194 195# [ test ] 196fn mapped_stacks_capture_intrinsics_but_allow_custom_formatters () { 197let dir = tempfile:: tempdir (). unwrap (); 198let output =run ( 199 dir. path (), 200& [( 201"main.ts" , 202r#"interface Erased { value: number; } 203const original = Error.prepareStackTrace; 204const poison = () => { throw new Error("Poisoned intrinsic"); }; 205Error.prototype.toString = poison; 206String.prototype.lastIndexOf = poison; 207String.prototype.slice = poison; 208Error.prepareStackTrace = original; 209console.log(new Error("mapped").stack); 210Error.prepareStackTrace = () => "custom"; 211console.log(new Error().stack); 212"# , 213)], 214); 215let stdout =success ( output); 216let path = dir. path (). canonicalize (). unwrap (). join ( "main.ts" ); 217assert! ( stdout. starts_with ( "Error: mapped\n" ), "{stdout}" ); 218assert! ( 219 stdout. contains ( & format! ( "at {}:8:13" , path. display ())), 220"{stdout}" 221); 222assert! ( stdout. ends_with ( "custom\n" ), "{stdout}" ); 223}