char/avril
(wip) why'd you have to go and make things so complicaaaaated?
git clone https://git.t4t.associates/char/avril
f90c0ce
main
1use std:: collections:: HashSet ; 2use std:: path::{ Path , PathBuf }; 3 4use anyhow::{ Context , Result , bail}; 5use oxc:: allocator:: Allocator ; 6use oxc:: ast:: ast::{ Declaration , IdentifierReference , ImportOrExportKind , Statement }; 7use oxc:: ast:: builder:: AstBuilder ; 8use oxc:: ast_visit:: Visit ; 9use oxc:: codegen::{ Codegen , CodegenOptions }; 10use oxc:: diagnostics:: Diagnostics ; 11use oxc:: isolated_declarations::{ IsolatedDeclarations , IsolatedDeclarationsOptions }; 12use oxc:: parser:: Parser ; 13use oxc:: semantic:: SemanticBuilder ; 14use oxc:: span::{ SPAN , SourceType }; 15use oxc:: transformer::{ TransformOptions , Transformer }; 16 17/// Compile one TypeScript source from a Cargo build script. 18/// 19/// Paths are relative to the extension crate. Outputs mirror the source path 20/// relative to `src/` in `OUT_DIR`: `src/main.ts` produces `main.js` and, when a 21/// public module is named, `main.d.ts`, while `src/public/posix.ts` produces 22/// `public/posix.js` and `public/posix.d.ts`. Imports are preserved, not resolved 23/// or bundled. Public declarations support self-contained exported functions, 24/// variables, interfaces, and type aliases, plus the two shapes a facade needs 25/// beyond that: private interfaces and type aliases referenced by exported 26/// signatures (kept in the ambient module, not exported), and type-only imports 27/// and re-exports naming a public `avril:` module (preserved verbatim). 28/// 29/// This does not type-check the source; validate erasable TypeScript separately. 30pub fn build ( source : impl AsRef < Path >, public_module : Option < & str >) ->Result <()> { 31let path = source. as_ref (); 32println! ( "cargo:rerun-if-changed=build.rs" ); 33println! ( "cargo:rerun-if-changed={}" , path. display ()); 34let output =output_path ( 35& PathBuf :: from ( std:: env:: var_os ( "OUT_DIR" ). context ( "Missing OUT_DIR" ) ?), 36 path, 37) ?; 38let source = 39 std:: fs:: read_to_string ( path). with_context ( ||format! ( "Reading {}" , path. display ())) ?; 40let compiled =compile ( path, & source, public_module) 41. with_context ( ||format! ( "Compiling {}" , path. display ())) ?; 42if let Some ( parent) = output. parent () { 43 std:: fs:: create_dir_all ( parent) ?; 44} 45 std:: fs:: write ( output. with_extension ( "js" ), compiled. javascript ) ?; 46let declaration_path = output. with_extension ( "d.ts" ); 47if let Some ( declaration) = compiled. declaration { 48 std:: fs:: write ( declaration_path, declaration) ?; 49} else { 50// OUT_DIR persists when a formerly public entry becomes internal. 51match std:: fs:: remove_file ( declaration_path) { 52Ok (()) =>{} 53Err ( error) if error. kind () == std:: io:: ErrorKind :: NotFound =>{} 54Err ( error) =>return Err ( error. into ()), 55} 56} 57Ok (()) 58} 59 60/// A facade may only name another public module; anything else would leak a 61/// private module graph into the emitted ambient declaration. 62fn is_public_module ( source : & str ) ->bool { 63 source. starts_with ( "avril:" ) 64} 65 66/// Outputs mirror the source path relative to `src/`, so one crate can own 67/// several public modules without colliding. 68fn output_path ( out_dir : & Path , source : & Path ) ->Result < PathBuf > { 69let relative = source 70. strip_prefix ( "src" ) 71. context ( "Source must live under src/" ) ?; 72Ok ( out_dir. join ( relative)) 73} 74 75/// Identifiers a declaration mentions. Used to keep only the private types the 76/// public surface actually reaches into the emitted ambient module. 77# [ derive ( Default )] 78struct References < ' a >( HashSet < &' a str >); 79 80impl < ' a > Visit < ' a > for References < ' a > { 81fn visit_identifier_reference ( & mut self , reference : & IdentifierReference < ' a >) { 82self . 0 . insert ( reference. name . as_str ()); 83} 84} 85 86fn references < ' a >( statement : & Statement < ' a >) ->HashSet < &' a str > { 87let mut references =References :: default (); 88 references. visit_statement ( statement); 89 references. 0 90} 91 92fn private_name < ' a >( statement : & Statement < ' a >) ->Option < &' a str > { 93match statement{ 94Statement :: TSInterfaceDeclaration ( interface) =>Some ( interface. id . name . as_str ()), 95Statement :: TSTypeAliasDeclaration ( alias) =>Some ( alias. id . name . as_str ()), 96 _ =>None , 97} 98} 99 100fn imported_name < ' a >( specifier : &' a oxc:: ast:: ast:: ImportDeclarationSpecifier < ' a >) ->&' a str { 101use oxc:: ast:: ast:: ImportDeclarationSpecifier ::{ 102ImportDefaultSpecifier , ImportNamespaceSpecifier , ImportSpecifier , 103}; 104match specifier{ 105ImportSpecifier ( specifier) => specifier. local . name . as_str (), 106ImportDefaultSpecifier ( specifier) => specifier. local . name . as_str (), 107ImportNamespaceSpecifier ( specifier) => specifier. local . name . as_str (), 108} 109} 110 111struct Compiled { 112javascript : String , 113declaration : Option < String >, 114} 115 116fn diagnostics ( diagnostics : Diagnostics , source : & str ) ->Result <()> { 117if !diagnostics. is_empty () { 118bail! ( 119"{}" , 120 diagnostics 121. into_iter () 122. map ( |diagnostic| diagnostic. render_with_source_code ( source)) 123. collect ::< Vec < _ >>() 124. join ( "\n" ) 125); 126} 127Ok (()) 128} 129 130fn compile ( path : & Path , source : & str , public_module : Option < & str >) ->Result < Compiled > { 131let allocator =Allocator :: default (); 132let parsed =Parser :: new ( & allocator, source, SourceType :: ts ()). parse (); 133diagnostics ( parsed. diagnostics , source) ?; 134let mut program = parsed. program ; 135let declaration =if let Some ( module) = public_module{ 136let mut declaration = 137IsolatedDeclarations :: new ( & allocator, IsolatedDeclarationsOptions :: default ()) 138. build ( & program); 139diagnostics ( declaration. diagnostics , source) ?; 140 141// A public entry must carry its own types; private module graphs are 142// not bundled. Strip `declare` from the exports the surface is written 143// with, and the empty `export {}` marker oxc may emit; it is re-added 144// canonically below only when a private type survives. 145let mut index =0 ; 146while index < declaration. program . body . len () { 147let strip =match & mut declaration. program . body [ index] { 148Statement :: ExportDeclaration ( export) =>{ 149match & mut export. declaration { 150Declaration :: FunctionDeclaration ( function) => function. declare =false , 151Declaration :: VariableDeclaration ( variable) => variable. declare =false , 152Declaration :: TSInterfaceDeclaration ( interface) => interface. declare =false , 153Declaration :: TSTypeAliasDeclaration ( alias) => alias. declare =false , 154 _ =>bail! ( 155"Unsupported public declaration for {module:?}: expected an exported function, variable, interface, or type alias" 156), 157}; 158false 159} 160Statement :: TSInterfaceDeclaration ( interface) =>{ 161 interface. declare =false ; 162false 163} 164Statement :: TSTypeAliasDeclaration ( alias) =>{ 165 alias. declare =false ; 166false 167} 168Statement :: ImportDeclaration ( _) |Statement :: ExportFromDeclaration ( _) =>false , 169Statement :: ExportNamedDeclaration ( export) =>{ 170if !export. specifiers . is_empty () { 171bail! ( 172"Unsupported public declaration for {module:?}: expected an exported function, variable, interface, or type alias" 173); 174} 175true 176} 177Statement :: ExportDefaultDeclaration ( _) =>bail! ( 178"Unsupported public declaration for {module:?}: a public surface exports named declarations" 179), 180// An ambient module can only describe types. Non-exported 181// values and classes describe nothing a guest can reach, so 182// they never belong in it. 183 _ =>true , 184}; 185if strip{ 186 declaration. program . body . remove ( index); 187} else { 188 index +=1 ; 189} 190} 191 192// The ambient module carries the surface, not the implementation: drop 193// private declarations the exported ones never reach, then the imports 194// nothing retained names any more. 195let mut reachable: HashSet < & str > = declaration 196. program 197. body 198. iter () 199. filter ( |statement|matches! ( statement, Statement :: ExportDeclaration ( _))) 200. flat_map ( references) 201. collect (); 202let mut privates: Vec <( & str , HashSet < & str >)> = declaration 203. program 204. body 205. iter () 206. filter_map ( |statement|{ 207private_name ( statement). map ( |name|( name, references ( statement))) 208}) 209. collect (); 210loop { 211let mut grew =false ; 212for ( name, refs) in & privates{ 213if reachable. contains ( name) { 214for referencein refs{ 215 grew |= reachable. insert ( reference); 216} 217} 218} 219if !grew{ 220break ; 221} 222} 223 declaration. program . body . retain ( |statement|{ 224private_name ( statement). is_none_or ( |name| reachable. contains ( name)) 225}); 226 227let used: HashSet < & str > = declaration 228. program 229. body 230. iter () 231. flat_map ( references) 232. collect (); 233 declaration 234. program 235. body 236. retain ( |statement|match statement{ 237Statement :: ImportDeclaration ( import) => import 238. specifiers 239. iter () 240. flatten () 241. any ( |specifier| used. contains ( imported_name ( specifier))), 242 _ =>true , 243}); 244 privates. clear (); 245 246// What remains must still be expressible by guests: type-only 247// references that name a public module, never a private one. 248for statementin & declaration. program . body { 249let ( source, kind) =match statement{ 250Statement :: ImportDeclaration ( import) =>{ 251( import. source . value . as_str (), import. import_kind ) 252} 253Statement :: ExportFromDeclaration ( export) =>{ 254( export. source . value . as_str (), export. export_kind ) 255} 256 _ =>continue , 257}; 258if kind !=ImportOrExportKind :: Type || !is_public_module ( source) { 259bail! ( 260"Unsupported public declaration for {module:?}: reference to {source:?} must be type-only and name a public `avril:` module" 261); 262} 263} 264 265let kept_private = declaration 266. program 267. body 268. iter () 269. any ( |statement|private_name ( statement). is_some ()); 270if kept_private{ 271let ast_builder =AstBuilder :: new ( & allocator); 272 declaration 273. program 274. body 275. push ( Statement :: new_export_named_declaration ( 276SPAN , 277[], 278ImportOrExportKind :: Value , 279& ast_builder, 280)); 281} 282 283let public =Codegen :: new () 284. with_options ( CodegenOptions { 285initial_indent : 1 , 286 ..Default :: default () 287}) 288. build ( & declaration. program ) 289. code ; 290let name = serde_json:: to_string ( module) ?; 291Some ( format! ( 292"// Generated from {path:?}.\ndeclare module {name} {{\n{public}}}\n" 293)) 294} else { 295None 296}; 297 298let semantic =SemanticBuilder :: new_compiler (). build ( & program); 299diagnostics ( semantic. diagnostics , source) ?; 300// Keep native JS syntax and runtime imports; only explicit type imports are erased. 301let mut options =TransformOptions :: default (); 302 options. typescript . only_remove_type_imports =true ; 303let transformed =Transformer :: new ( & allocator, path, & options) 304. build_with_scoping ( semantic. semantic . into_scoping (), & mut program); 305diagnostics ( transformed. diagnostics , source) ?; 306Ok ( Compiled { 307javascript : Codegen :: new (). build ( & program). code , 308 declaration, 309}) 310} 311 312pub mod testing; 313 314# [ cfg ( test )] 315mod tests;