char/avril
(wip) why'd you have to go and make things so complicaaaaated?
git clone https://git.t4t.associates/char/avril
1c8ffed
main
1mod common; 2# [ path = "common/http.rs" ] 3# [ allow ( dead_code )] 4mod http; 5# [ path = "common/registry.rs" ] 6mod registry; 7# [ path = "common/lsp.rs" ] 8# [ allow ( dead_code )] 9mod support; 10 11use base64:: Engine ; 12use serde_json:: json; 13use std:: fs; 14use std:: path:: Path ; 15use std:: process::{ Command , Output , Stdio }; 16 17fn data ( mime : & str , source : & str ) ->String { 18format! ( 19"data:{mime};base64,{}" , 20 base64:: engine:: general_purpose:: STANDARD . encode ( source) 21) 22} 23 24fn run ( root : & Path , args : & [ & str ]) ->Output { 25let child =Command :: new ( env! ( "CARGO_BIN_EXE_avril" )) 26. current_dir ( root) 27. args ( args) 28. stdout ( Stdio :: piped ()) 29. stderr ( Stdio :: piped ()) 30. spawn () 31. unwrap (); 32 common:: wait ( child) 33} 34 35fn stdout ( output : Output ) ->String { 36assert! ( 37 output. status . success (), 38"{}{}" , 39String :: from_utf8_lossy ( & output. stdout ), 40String :: from_utf8_lossy ( & output. stderr ) 41); 42String :: from_utf8 ( output. stdout ). unwrap () 43} 44 45# [ test ] 46fn static_dynamic_and_mapped_data_imports_share_url_identity () { 47let dir = tempfile:: tempdir (). unwrap (); 48let url =data ( 49"text/typescript" , 50r#" 51export const count: number = globalThis.loads = (globalThis.loads ?? 0) + 1; 52export const value: string = 'héllo?#%'; 53export const url = import.meta.url; 54export const main = import.meta.main; 55export const self = () => import(import.meta.url); 56"# , 57); 58 fs:: write ( 59 dir. path (). join ( "avril.conf" ), 60json! ({ "imports" : { "inline" : url}}). to_string (), 61) 62. unwrap (); 63 fs:: write ( dir. path (). join ( "main.ts" ), format! ( r#" 64import * as a from {url:?}; 65import * as mapped from 'inline'; 66export {{value}} from {url:?}; 67const [b, c] = await Promise.all([import({url:?}), import({upper:?})]); 68const other = await import({fragment:?}); 69console.log(a === b, b === c, a === mapped, await a.self() === a); 70console.log(a.count, other.count, a.value, a.main, a.url === {url:?}); 71console.log(import.meta.resolve('inline') === a.url, import.meta.resolve({upper:?}) === a.url); 72"# , upper = url. replacen ( "data:" , "DATA:" , 1 ), fragment =format! ( "{url}#second" ))). unwrap (); 73assert_eq! ( 74stdout ( run ( dir. path (), & [ "run" , "main.ts" ])), 75"true true true true\n1 2 héllo?#% false true\ntrue true\n" 76); 77if dir. path (). join ( "avril.lock" ). exists () { 78assert! ( 79 !fs:: read_to_string ( dir. path (). join ( "avril.lock" )) 80. unwrap () 81. contains ( "data:" ) 82); 83} 84} 85 86# [ test ] 87fn importer_maps_can_redirect_or_block_data_urls () { 88let dir = tempfile:: tempdir (). unwrap (); 89let original ="data:text/javascript,export%20const%20value=1" ; 90let mapped ="data:text/typescript,export%20const%20value%3Anumber=42" ; 91let blocked ="data:text/javascript,export%20const%20value=0" ; 92 fs:: write ( 93 dir. path (). join ( "avril.conf" ), 94json! ({ "imports" : { original: mapped, blocked: null}}). to_string (), 95) 96. unwrap (); 97 fs:: write ( 98 dir. path (). join ( "main.ts" ), 99format! ( 100r#" 101import {{value}} from {original:?}; 102console.log(value, import.meta.resolve({original:?}) === {mapped:?}); 103for (const action of [() => import({blocked:?}), () => import.meta.resolve({blocked:?})]) {{ 104try {{ await action(); throw Error('unexpected success'); }} 105catch (e) {{ if (!e.message.includes('blocked by the import map')) throw e; }} 106}} 107"# 108), 109) 110. unwrap (); 111assert_eq! ( stdout ( run ( dir. path (), & [ "run" , "main.ts" ])), "42 true\n" ); 112} 113 114# [ test ] 115fn data_entrypoints_decode_standard_payloads_without_io_grants () { 116let dir = tempfile:: tempdir (). unwrap (); 117for urlin [ 118"data:text/javascript,console.log('hello%20%F0%9F%98%80%3F%23%25%2B'%2Cimport.meta.main)" , 119"data:TEXT/JAVASCRIPT;charset=iso-8859-1,console.log('hello%20%F0%9F%98%80%3F%23%25%2B'%2Cimport.meta.main)#ignored" , 120"data:application/ecmascript,console.log('hello%20%F0%9F%98%80%3F%23%25%2B'%2Cimport.meta.main)" , 121] { 122assert_eq! ( 123stdout ( run ( dir. path (), & [ "run" , "-0" , url])), 124"hello 😀?#%+ true\n" 125); 126} 127let encoded = base64:: engine:: general_purpose:: STANDARD_NO_PAD . encode ( "console.log(42)" ); 128let url =format! ( 129"data:application/typescript;base64,{}%20{}" , 130& encoded[ ..4 ], 131& encoded[ 4 ..] 132); 133assert_eq! ( stdout ( run ( dir. path (), & [ "run" , "-0" , & url])), "42\n" ); 134let url ="data:text/javascript,console.log('literal?query')" ; 135assert_eq! ( 136stdout ( run ( dir. path (), & [ "run" , "-0" , url])), 137"literal?query\n" 138); 139let url ="data:text/typescript,const%20x%3Anumber=42%3Bconsole.log(x)%3B%2F%2Ffile.cjs" ; 140assert_eq! ( stdout ( run ( dir. path (), & [ "run" , "-0" , url])), "42\n" ); 141assert! ( !dir. path (). join ( "avril.lock" ). exists ()); 142} 143 144# [ test ] 145fn malformed_and_unsupported_data_modules_fail_without_path_fallback () { 146let dir = tempfile:: tempdir (). unwrap (); 147for ( url, expected) in [ 148( "data:text/javascript" , "Invalid data module URL" ), 149( 150"data:text/javascript;base64,!!!" , 151"Invalid base64 in data module" , 152), 153( 154"data:text/javascript;base64,YQ===" , 155"Invalid base64 in data module" , 156), 157( 158"data:,console.log(1)" , 159"Unsupported data module MIME type: text/plain" , 160), 161( 162"data:application/json,%7B%7D" , 163"Unsupported data module MIME type: application/json" , 164), 165( "data:text/javascript,const%20x:number=1" , "SyntaxError" ), 166] { 167let output =run ( dir. path (), & [ "run" , "-0" , url]); 168assert! ( !output. status . success (), "{url}" ); 169let error =String :: from_utf8_lossy ( & output. stderr ); 170assert! ( error. contains ( expected), "{url}: {error}" ); 171} 172} 173 174# [ test ] 175fn data_modules_have_no_relative_or_inherited_package_context () { 176let dir = tempfile:: tempdir (). unwrap (); 177 fs:: write ( dir. path (). join ( "dep.ts" ), "export const value = 42;" ). unwrap (); 178 fs:: write ( dir. path (). join ( "avril.conf" ), "imports:{alias:'./dep.ts'}" ). unwrap (); 179let url =data ( 180"text/typescript" , 181r#" 182for (const name of ['./dep.ts', '../dep.ts', '/dep.ts', '?query', '#fragment', 'alias']) { 183for (const action of [() => import(name), () => import.meta.resolve(name)]) { 184try { await action(); throw Error('unexpected success'); } 185catch (e) { 186if (!/absolute import URLs|Unsupported module specifier/.test(e.message)) throw e; 187} 188} 189} 190console.log('no base'); 191"# , 192); 193 fs:: write ( 194 dir. path (). join ( "main.ts" ), 195format! ( "await import({url:?});" ), 196) 197. unwrap (); 198assert_eq! ( stdout ( run ( dir. path (), & [ "run" , "main.ts" ])), "no base\n" ); 199} 200 201# [ test ] 202fn data_module_dependencies_obey_live_execution_authority () { 203let dir = tempfile:: tempdir (). unwrap (); 204let dep = dir. path (). join ( "dep.ts" ); 205 fs:: write ( 206& dep, 207"export const value: number = 42; export const url = import.meta.url;" , 208) 209. unwrap (); 210let dep = url:: Url :: from_file_path ( dep). unwrap (). to_string (); 211let url =data ( 212"text/typescript" , 213& format! ( 214r#" 215import {{pledge}} from 'avril:std'; 216const target = {dep:?}; 217const first = await import(target); 218console.log(first.value, import.meta.resolve(target) === first.url); 219pledge({{jsExec: false}}); 220try {{ await import(target); throw Error('allowed cached import'); }} 221catch (e) {{ if (!e.message.includes('PermissionDenied')) throw e; }} 222console.log('narrowed'); 223"# 224), 225); 226let denied =run ( dir. path (), & [ "run" , "-0" , & url]); 227assert! ( !denied. status . success ()); 228assert! ( String :: from_utf8_lossy ( & denied. stderr ). contains ( "PermissionDenied" )); 229assert_eq! ( 230stdout ( run ( 231 dir. path (), 232& [ "run" , "-0" , "--allow-js-exec=dep.ts" , & url] 233)), 234"42 true\nnarrowed\n" 235); 236 237let static_url =data ( 238"text/typescript" , 239& format! ( "import {{value}} from {dep:?}; console.log(value);" ), 240); 241assert_eq! ( 242stdout ( run ( 243 dir. path (), 244& [ "run" , "-0" , "--allow-js-exec=dep.ts" , & static_url] 245)), 246"42\n" 247); 248} 249 250# [ test ] 251fn data_modules_import_http_under_normal_authority () { 252let server = http:: Http :: new (); 253 server. source ( 254"dep.ts" , 255"export const value: number = 42; export const url = import.meta.url;" , 256); 257let dir = tempfile:: tempdir (). unwrap (); 258let dep = server. url . join ( "dep.ts" ). unwrap (). to_string (); 259let url =data ( 260"text/typescript" , 261& format! ( 262"import {{value, url}} from {dep:?}; console.log(value, import.meta.resolve({dep:?}) === url);" 263), 264); 265let denied =run ( dir. path (), & [ "run" , "-0" , & url]); 266assert! ( !denied. status . success ()); 267assert! ( String :: from_utf8_lossy ( & denied. stderr ). contains ( "PermissionDenied" )); 268assert! ( server. requests . lock (). unwrap (). is_empty ()); 269let grant =format! ( "--allow-js-exec={}" , server. url ); 270assert_eq! ( 271stdout ( run ( dir. path (), & [ "run" , "-0" , & grant, & url])), 272"42 true\n" 273); 274} 275 276# [ test ] 277fn data_modules_import_registry_packages_without_inheriting_bare_bindings () { 278let server = registry:: Registry :: new (); 279let npm_inline =data ( "text/javascript" , "export const value = 42;" ); 280let npm_source =format! ( 281"export {{value}} from {npm_inline:?}; if(import.meta.resolve({npm_inline:?}) !== {npm_inline:?}) throw Error('data URL identity');" 282); 283 server. publish_npm ( 284"inline-dep" , 285"1.0.0" , 286& [ 287( 288"package.json" , 289r#"{"name":"inline-dep","version":"1.0.0","type":"module","exports":"./mod.js"}"# , 290), 291( "mod.js" , & npm_source), 292], 293); 294let inline =data ( "text/typescript" , "export const value: number = 43;" ); 295let jsr_source =format! ( "export {{value}} from {inline:?};" ); 296 server. package ( 297"@test/inline" , 298"1.0.0" , 299& [( "mod.ts" , & jsr_source)], 300& [( "." , "./mod.ts" )], 301); 302let dir = tempfile:: tempdir (). unwrap (); 303let registry =format! ( "--npm-registry={}" , server. url ); 304let jsr_registry =format! ( "--jsr-registry={}" , server. url ); 305let cache =format! ( "--cache-dir={}" , dir. path (). join ( "cache" ). display ()); 306let url =data ( 307"text/typescript" , 308r#" 309import {value} from 'npm:inline-dep@1'; 310import {value as jsr} from 'jsr:@test/inline@1'; 311console.log(value, (await import(import.meta.resolve('npm:inline-dep@1'))).value, jsr); 312"# , 313); 314let denied =run ( dir. path (), & [ "run" , "-0" , & registry, & cache, & url]); 315assert! ( !denied. status . success ()); 316assert! ( String :: from_utf8_lossy ( & denied. stderr ). contains ( "PermissionDenied" )); 317assert! ( server. requests . lock (). unwrap (). is_empty ()); 318let grant =format! ( "--allow-js-exec={}" , server. url ); 319assert_eq! ( 320stdout ( run ( 321 dir. path (), 322& [ "run" , "-0" , & grant, & registry, & jsr_registry, & cache, & url] 323)), 324"42 42 43\n" 325); 326} 327 328# [ test ] 329fn data_workers_use_typescript_and_inherit_authority () { 330let dir = tempfile:: tempdir (). unwrap (); 331let dep = dir. path (). join ( "dep.ts" ); 332 fs:: write ( & dep, "export const value: number = 42;" ). unwrap (); 333let dep = url:: Url :: from_file_path ( dep). unwrap (). to_string (); 334let worker =data ( 335"text/typescript" , 336& format! ( 337r#" 338import {{value}} from {dep:?}; 339const answer: number = value; 340postMessage([answer, import.meta.url === location.href, import.meta.main]); 341close(); 342"# 343), 344); 345let main =data ( 346"text/typescript" , 347& format! ( 348r#" 349const worker = new Worker({worker:?}, {{type: 'module'}}); 350const result = await new Promise((resolve, reject) => {{ 351worker.onmessage = event => resolve(event.data); 352worker.onerror = reject; 353}}); 354console.log(JSON.stringify(result)); 355"# 356), 357); 358assert_eq! ( 359stdout ( run ( 360 dir. path (), 361& [ "run" , "-0" , "--allow-js-exec=dep.ts" , & main] 362)), 363"[42,true,true]\n" 364); 365let denied =run ( dir. path (), & [ "run" , "-0" , & main]); 366assert! ( !denied. status . success ()); 367assert! ( String :: from_utf8_lossy ( & denied. stderr ). contains ( "PermissionDenied" )); 368} 369 370# [ test ] 371fn bundling_uses_data_module_mime_types_and_preserves_source_maps () { 372let dir = tempfile:: tempdir (). unwrap (); 373let dep =data ( 374"text/typescript" , 375"export const value: number = globalThis.answer ?? 42; // misleading.cjs" , 376); 377 fs:: write ( 378 dir. path (). join ( "main.ts" ), 379format! ( "import {{value}} from {dep:?}; console.log(value);" ), 380) 381. unwrap (); 382stdout ( run ( 383 dir. path (), 384& [ "bundle" , "main.ts" , "-o" , "dist/main.js" , "--sourcemap" ], 385)); 386assert_eq! ( stdout ( run ( dir. path (), & [ "run" , "dist/main.js" ])), "42\n" ); 387let map: serde_json:: Value = 388 serde_json:: from_slice ( & fs:: read ( dir. path (). join ( "dist/main.js.map" )). unwrap ()). unwrap (); 389assert! ( 390 map[ "sources" ]. as_array (). unwrap (). contains ( & json! ( dep)), 391"{map}" 392); 393let entry =data ( 394"text/typescript" , 395"const value: number = 43; console.log(value);" , 396); 397stdout ( run ( dir. path (), & [ "bundle" , & entry, "-o" , "dist/entry.js" ])); 398assert_eq! ( stdout ( run ( dir. path (), & [ "run" , "dist/entry.js" ])), "43\n" ); 399let unnamed =run ( dir. path (), & [ "bundle" , & entry, "--outdir" , "other" ]); 400assert! ( !unnamed. status . success ()); 401assert! ( String :: from_utf8_lossy ( & unnamed. stderr ). contains ( "explicit -o" )); 402} 403 404# [ test ] 405fn typescript_data_errors_map_back_to_original_source_locations () { 406let dir = tempfile:: tempdir (). unwrap (); 407let bad =data ( 408"text/typescript" , 409"type Erased = {\n field: string;\n};\nconst value: number = 42;\nthrow new Error('data failure');" , 410); 411let main =data ( 412"text/typescript" , 413& format! ( 414r#" 415try {{ await import({bad:?}); throw Error('did not throw'); }} 416catch (e) {{ 417if (!e.stack.includes({location:?})) throw e; 418console.log('mapped'); 419}} 420"# , 421 location =format! ( "{bad}:5:7" ) 422), 423); 424assert_eq! ( stdout ( run ( dir. path (), & [ "run" , "-0" , & main])), "mapped\n" ); 425} 426 427# [ test ] 428fn editor_navigation_opens_immutable_data_module_sources () { 429let Some ( backend) = support:: backend () else { 430eprintln! ( "skipping: AVRIL_TEST_TYPESCRIPT is not set" ); 431return ; 432}; 433let dir = tempfile:: tempdir (). unwrap (); 434let url ="data:text/typescript,export%20const%20value%3A42=42%3B%20%2F%2F%20%FF.cjs" ; 435let other = url. replace ( "value" , "%76alue" ); 436let main =format! ( 437"import {{value}} from {url:?}; import {{value as other}} from {other:?}; value; other;" 438); 439 fs:: write ( dir. path (). join ( "main.ts" ), & main). unwrap (); 440let mut server = support:: Server :: start ( dir. path (), & backend, & [ "--offline" ]); 441 server. initialize (); 442 server. open ( "main.ts" , & main); 443assert! ( 444 server 445. hover ( "main.ts" , & main, "value" ) 446. contains ( "value: 42" ) 447); 448let links = server. definitions ( "main.ts" , & main, "value" ); 449let target = links[ 0 ][ "targetUri" ]. as_str (). unwrap (); 450assert! ( target. starts_with ( "avril-builtin:/data/" ), "{links:?}" ); 451assert! ( target. ends_with ( ".ts" ), "{target}" ); 452let contents = server 453. call ( "avril/readDocument" , json! ({ "uri" : target})) 454. unwrap (); 455assert_eq! ( contents, "export const value:42=42; // �.cjs" ); 456assert! ( 457 server 458. hover_uri ( target, contents. as_str (). unwrap (), "value" ) 459. contains ( "value: 42" ) 460); 461let other_links = server. definitions ( "main.ts" , & main, "other" ); 462assert_ne! ( links[ 0 ][ "targetUri" ], other_links[ 0 ][ "targetUri" ]); 463let rename = server. call ( 464"textDocument/rename" , 465json! ({ 466"textDocument" : { "uri" : target}, 467"position" : { "line" : 0 , "character" : 14 }, 468"newName" : "changed" 469}), 470); 471assert! ( 472 rename. is_err (), 473"immutable data source accepted rename: {rename:?}" 474); 475 server. close (); 476} 477 478# [ test ] 479fn checking_data_modules_uses_mime_languages_and_reports_type_errors () { 480let Some ( backend) = support:: backend () else { 481eprintln! ( "skipping: AVRIL_TEST_TYPESCRIPT is not set" ); 482return ; 483}; 484let dir = tempfile:: tempdir (). unwrap (); 485let backend =format! ( "--typescript={}" , backend. display ()); 486let ts =data ( 487"text/typescript" , 488"export const value: number = 42; // misleading.cjs" , 489); 490let js =data ( 491"text/javascript" , 492"export const text = 'hello'; // misleading.ts" , 493); 494let source =format! ( 495"import {{value}} from {ts:?}; import {{text}} from {js:?}; const n: number = value; const s: string = text;" 496); 497 fs:: write ( dir. path (). join ( "main.ts" ), & source). unwrap (); 498stdout ( run ( 499 dir. path (), 500& [ "check" , & backend, "--offline" , "main.ts" ], 501)); 502let broken = source. replace ( "const n: number" , "const n: string" ); 503 fs:: write ( dir. path (). join ( "main.ts" ), broken). unwrap (); 504let output =run ( dir. path (), & [ "check" , & backend, "--offline" , "main.ts" ]); 505assert! ( !output. status . success ()); 506assert! ( String :: from_utf8_lossy ( & output. stdout ). contains ( "TS2322" )); 507let bad_js =data ( 508"text/javascript" , 509"// @ts-check\n/** @type {number} */ export const value = 'wrong';" , 510); 511let output =run ( dir. path (), & [ "check" , & backend, "--offline" , & bad_js]); 512assert! ( !output. status . success ()); 513assert! ( String :: from_utf8_lossy ( & output. stdout ). contains ( "TS2322" )); 514let bad =data ( "text/typescript" , "export const value: number = 'wrong';" ); 515let output =run ( dir. path (), & [ "check" , & backend, "--offline" , & bad]); 516assert! ( !output. status . success ()); 517let diagnostics =String :: from_utf8_lossy ( & output. stdout ); 518assert! ( diagnostics. contains ( "TS2322" ), "{diagnostics}" ); 519assert! ( diagnostics. contains ( & format! ( "{bad}:1:" )), "{diagnostics}" ); 520for ( url, message) in [ 521( 522"data:application/json,%7B%7D" , 523"Unsupported data module MIME type" , 524), 525( 526"data:text/typescript;base64,!!!" , 527"Invalid base64 in data module" , 528), 529] { 530let output =run ( dir. path (), & [ "check" , & backend, "--offline" , url]); 531assert! ( !output. status . success ()); 532let diagnostics =String :: from_utf8_lossy ( & output. stdout ); 533assert! ( diagnostics. contains ( message), "{diagnostics}" ); 534assert! ( diagnostics. contains ( & format! ( "{url}:1:" )), "{diagnostics}" ); 535} 536}