char/avril
(wip) why'd you have to go and make things so complicaaaaated?
git clone https://git.t4t.associates/char/avril
688e96e
main
1mod common; 2# [ path = "common/http.rs" ] 3# [ allow ( dead_code )] 4mod http_fixture; 5# [ path = "common/registry.rs" ] 6mod registry_fixture; 7 8use std:: fs; 9use std:: path:: Path ; 10use std:: process::{ Command , Output , Stdio }; 11 12const RUNTIME : & str =r#" 13export const Fragment = 'fragment'; 14export const jsx = (tag, props, key) => ({tag, props, key}); 15export const jsxs = jsx; 16export const jsxDEV = (tag, props, key, stable, source) => ({tag, props, key, stable, source}); 17export const createElement = (tag, props, ...children) => ({tag, props: {...props, children}, key: props?.key}); 18export const h = createElement, F = Fragment; 19"# ; 20 21fn files ( dir : & Path , entries : & [( & str , & str )]) { 22for ( name, source) in entries{ 23let path = dir. join ( name); 24 fs:: create_dir_all ( path. parent (). unwrap ()). unwrap (); 25 fs:: write ( path, source). unwrap (); 26} 27} 28 29fn cli ( dir : & Path , args : & [ & str ]) ->Output { 30let child =Command :: new ( env! ( "CARGO_BIN_EXE_avril" )) 31. current_dir ( dir) 32. args ( args) 33. stdin ( Stdio :: null ()) 34. stdout ( Stdio :: piped ()) 35. stderr ( Stdio :: piped ()) 36. spawn () 37. unwrap (); 38 common:: wait ( child) 39} 40 41fn success ( output : Output ) ->String { 42assert! ( 43 output. status . success (), 44"{}" , 45String :: from_utf8_lossy ( & output. stderr ) 46); 47String :: from_utf8 ( output. stdout ). unwrap () 48} 49 50fn failure ( output : Output , message : & str ) { 51assert! ( !output. status . success ()); 52assert! ( 53 output. stdout . is_empty (), 54"{}" , 55String :: from_utf8_lossy ( & output. stdout ) 56); 57assert! ( 58String :: from_utf8_lossy ( & output. stderr ). contains ( message), 59"{}" , 60String :: from_utf8_lossy ( & output. stderr ) 61); 62} 63 64# [ test ] 65fn jsx_and_tsx_default_to_automatic_and_generated_imports_join_the_graph () { 66let dir = tempfile:: tempdir (). unwrap (); 67files ( 68 dir. path (), 69& [ 70( 71"avril.conf" , 72"imports: {'react/jsx-runtime': './runtime.js', react: './runtime.js'}" , 73), 74( "runtime.js" , RUNTIME ), 75( 76"dep.jsx" , 77"export const child = <b>hello & goodbye</b>;" , 78), 79( 80"main.tsx" , 81r#" 82import type {Missing} from './missing.ts'; 83import {child} from './dep.jsx'; 84const Component = (props: {name: string}) => props.name; 85const props = {id: 1}; 86const node = <><Component name="value" />{child}<i {...props} key="key" /></>; 87const children = node.props.children; 88console.log(node.tag, children[0].tag === Component, children[1].props.children, 89children[2].props.id, children[2].key, await Promise.resolve('done')); 90"# , 91), 92], 93); 94assert_eq! ( 95success ( cli ( dir. path (), & [ "run" , "main.tsx" ])), 96"fragment true hello & goodbye 1 key done\n" 97); 98let info: serde_json:: Value = 99 serde_json:: from_str ( & success ( cli ( dir. path (), & [ "info" , "main.tsx" ]))). unwrap (); 100let main = dir. path (). join ( "main.tsx" ); 101let imports =& info[ "modules" ][ main. to_str (). unwrap ()][ "imports" ]; 102assert! ( imports[ "react/jsx-runtime" ]. is_string ()); 103assert! ( imports[ "react" ]. is_string ()); 104assert! ( imports. get ( "./missing.ts" ). is_none ()); 105} 106 107# [ test ] 108fn tsconfig_redirects_automatic_jsx_without_adopting_typescript_paths () { 109let dir = tempfile:: tempdir (). unwrap (); 110files ( 111 dir. path (), 112& [ 113( 114"avril.conf" , 115"imports: {'custom/jsx-runtime': './runtime.js'}" , 116), 117( 118"tsconfig.json" , 119r#"{ 120// Only JSX compiler options affect execution. 121"compilerOptions": { 122"jsx": "react-jsx", "jsxImportSource": "custom", 123"paths": {"custom/jsx-runtime": ["./wrong.js"]}, 124}, 125}"# , 126), 127( "runtime.js" , RUNTIME ), 128( 129"wrong.js" , 130"throw Error('TypeScript paths changed runtime resolution');" , 131), 132( 133"main.jsx" , 134"console.log((<a title='hello' />).props.title);" , 135), 136], 137); 138assert_eq! ( success ( cli ( dir. path (), & [ "run" , "main.jsx" ])), "hello\n" ); 139} 140 141# [ test ] 142fn jsonc_extends_merges_in_order_and_preserves_classic_factory_imports () { 143let dir = tempfile:: tempdir (). unwrap (); 144files ( 145 dir. path (), 146& [ 147( "avril.conf" , "{}" ), 148( 149"config/first.json" , 150r#"{"compilerOptions":{"jsx":"react","jsxFactory":"wrong","jsxFragmentFactory":"Wrong"}}"# , 151), 152( 153"config/nested/shared.base.json" , 154r#"{"compilerOptions":{"jsxFragmentFactory":"F"}}"# , 155), 156( 157"config/second.json" , 158r#"{"extends":"./nested/shared.base","compilerOptions":{"jsxFactory":"h"}}"# , 159), 160( 161"tsconfig.json" , 162"\u{feff}{\"extends\":[\"./config/first\",\"./config/second.json\"],\"compilerOptions\":{\"jsxFactory\":\"helpers.h\",},}" , 163), 164( "runtime.js" , RUNTIME ), 165( 166"main.tsx" , 167r#" 168import * as helpers from './runtime.js'; 169import {F} from './runtime.js'; 170const node = <><p>classic</p></>; 171console.log(node.tag, node.props.children[0].tag, node.props.children[0].props.children[0]); 172"# , 173), 174], 175); 176assert_eq! ( 177success ( cli ( dir. path (), & [ "run" , "main.tsx" ])), 178"fragment p classic\n" 179); 180} 181 182# [ test ] 183fn development_jsx_and_stack_traces_retain_source_locations () { 184let dir = tempfile:: tempdir (). unwrap (); 185files ( 186 dir. path (), 187& [ 188( 189"avril.conf" , 190"imports: {'custom/jsx-dev-runtime': './runtime.js'}" , 191), 192( 193"tsconfig.json" , 194r#"{"compilerOptions":{"jsx":"react-jsxdev","jsxImportSource":"custom"}}"# , 195), 196( "runtime.js" , RUNTIME ), 197( 198"main.tsx" , 199"type Gone = number;\nconst node = <p />;\nconsole.log(node.source.lineNumber, node.source.columnNumber, node.source.fileName.endsWith('main.tsx'));\nthrow Error('mapped');\n" , 200), 201], 202); 203let output =cli ( dir. path (), & [ "run" , "main.tsx" ]); 204assert! ( !output. status . success ()); 205assert_eq! ( output. stdout , b"2 14 true\n" ); 206let stderr =String :: from_utf8_lossy ( & output. stderr ); 207assert! ( stderr. contains ( "main.tsx:4:7" ), "{stderr}" ); 208} 209 210# [ test ] 211fn nearest_configs_apply_only_within_their_own_package () { 212let dir = tempfile:: tempdir (). unwrap (); 213files ( 214 dir. path (), 215& [ 216( "avril.conf" , "{}" ), 217( 218"tsconfig.json" , 219r#"{"compilerOptions":{"jsx":"react","jsxFactory":"h"}}"# , 220), 221( "runtime.js" , RUNTIME ), 222( 223"main.tsx" , 224"import {h} from './runtime.js'; import {nested} from './src/nested.tsx'; import {library} from './lib/mod.tsx'; console.log((<p />).tag, nested.tag, library.tag);" , 225), 226( 227"src/tsconfig.json" , 228r#"{"compilerOptions":{"jsx":"react","jsxFactory":"custom"}}"# , 229), 230( 231"src/nested.tsx" , 232"import {h as custom} from '../runtime.js'; export const nested = <nested />;" , 233), 234( 235"lib/avril.conf" , 236"imports: {'react/jsx-runtime': '../runtime.js'}" , 237), 238( "lib/mod.tsx" , "export const library = <library />;" ), 239], 240); 241assert_eq! ( 242success ( cli ( dir. path (), & [ "run" , "main.tsx" ])), 243"p nested library\n" 244); 245} 246 247# [ test ] 248fn avril_conf_defaults_yield_to_tsconfig_and_stay_in_their_package () { 249let dir = tempfile:: tempdir (). unwrap (); 250files ( 251 dir. path (), 252& [ 253( 254"avril.conf" , 255"compilerOptions: {jsx: 'react', jsxFactory: 'conf', paths: {}}" , 256), 257( "runtime.js" , RUNTIME ), 258( 259"main.tsx" , 260"import {h as conf} from './runtime.js'; import {library} from './lib/mod.tsx'; console.log((<p />).tag, library.tag);" , 261), 262( 263"lib/avril.conf" , 264"imports: {'react/jsx-runtime': '../runtime.js'}" , 265), 266( "lib/mod.tsx" , "export const library = <library />;" ), 267], 268); 269assert_eq! ( 270success ( cli ( dir. path (), & [ "run" , "main.tsx" ])), 271"p library\n" 272); 273files ( 274 dir. path (), 275& [( 276"main.tsx" , 277"import {h as ts} from './runtime.js'; console.log((<p />).tag);" , 278)], 279); 280failure ( cli ( dir. path (), & [ "run" , "main.tsx" ]), "conf is not defined" ); 281files ( 282 dir. path (), 283& [( 284"tsconfig.json" , 285r#"{"compilerOptions":{"jsxFactory":"ts"}}"# , 286)], 287); 288assert_eq! ( success ( cli ( dir. path (), & [ "run" , "main.tsx" ])), "p\n" ); 289} 290 291# [ test ] 292fn configs_without_avril_packages_are_discovered_in_trusted_ancestors () { 293let dir = tempfile:: tempdir (). unwrap (); 294files ( 295 dir. path (), 296& [ 297( 298"tsconfig.json" , 299r#"{"compilerOptions":{"jsx":"react","jsxFactory":"h","jsxFragmentFactory":"F"}}"# , 300), 301( "runtime.js" , RUNTIME ), 302( 303"src/main.tsx" , 304"import {h,F} from '../runtime.js'; console.log((<><p /></>).tag);" , 305), 306], 307); 308assert_eq! ( 309success ( cli ( dir. path (), & [ "run" , "src/main.tsx" ])), 310"fragment\n" 311); 312} 313 314# [ test ] 315fn pragmas_override_config_and_remote_jsx_never_inherits_local_settings () { 316let dir = tempfile:: tempdir (). unwrap (); 317let http = http_fixture:: Http :: new (); 318files ( 319 dir. path (), 320& [ 321( "avril.conf" , "{}" ), 322( 323"tsconfig.json" , 324r#"{"compilerOptions":{"jsx":"react","jsxFactory":"missing"}}"# , 325), 326( 327"main.tsx" , 328"/** @jsxRuntime classic @jsx h @jsxFrag F */\nconst h = (tag: string) => tag, F = 'fragment'; console.log(<><p /></>);" , 329), 330], 331); 332assert_eq! ( success ( cli ( dir. path (), & [ "run" , "main.tsx" ])), "fragment\n" ); 333 http. source ( "ui/jsx-runtime" , RUNTIME ); 334 http. source ( "nested/main.tsx" , & format! ( "/** @jsxImportSource {} */\nconst n: string = 'remote'; console.log((<p>{{n}}</p>).props.children);" , http. name ( "ui" ))); 335 http. redirect ( "entry" , "/nested/main.tsx" ); 336assert_eq! ( 337success ( cli ( 338 dir. path (), 339& [ 340"run" , 341"-0" , 342& format! ( "--allow-js-exec={}" , http. url ), 343"--cache-dir=cache" , 344& http. name ( "entry" ) 345] 346)), 347"remote\n" 348); 349} 350 351# [ test ] 352fn invalid_configs_and_preserved_jsx_fail_before_application_execution () { 353for ( config, expected) in [ 354( 355r#"{"compilerOptions":{"jsx":"preserve"}}"# , 356"cannot execute preserved JSX" , 357), 358( 359r#"{"compilerOptions":{"jsx":"react-native"}}"# , 360"cannot execute preserved JSX" , 361), 362( 363r#"{"compilerOptions":{"jsx":"unknown"}}"# , 364"Unsupported compilerOptions.jsx" , 365), 366( 367r#"{"compilerOptions":{"jsxImportSource":3}}"# , 368"jsxImportSource must be" , 369), 370( 371r#"{"compilerOptions":{"jsxFactory":""}}"# , 372"jsxFactory must be" , 373), 374( 375r#"{"compilerOptions":{"jsx":"react","jsxFactory":"h()"} }"# , 376"jsxFactory must be an identifier" , 377), 378( 379r#"{"compilerOptions":{"jsxFragmentFactory":"a..b"}}"# , 380"jsxFragmentFactory must be an identifier" , 381), 382( 383r#"{"compilerOptions":[]}"# , 384"compilerOptions must be an object" , 385), 386( 387r#"{"extends":"npm:config"}"# , 388"extends must use relative or absolute" , 389), 390( r#"{"extends":"./missing.json"}"# , "Cannot find tsconfig" ), 391( 392r#"{"extends":"./tsconfig.json"}"# , 393"Cyclic tsconfig extends" , 394), 395( r#"{"extends":[3]}"# , "extends entries must be paths" ), 396( r#"{"extends":3}"# , "extends must be a path" ), 397( "[]" , "tsconfig must be an object" ), 398( "{bad}" , "tsconfig.json" ), 399( "{'compilerOptions': {}}" , "tsconfig.json" ), 400] { 401let dir = tempfile:: tempdir (). unwrap (); 402files ( 403 dir. path (), 404& [ 405( "tsconfig.json" , config), 406( 407"main.tsx" , 408"console.log('executed'); export const node = <p />;" , 409), 410], 411); 412failure ( cli ( dir. path (), & [ "run" , "main.tsx" ]), expected); 413} 414let dir = tempfile:: tempdir (). unwrap (); 415files ( 416 dir. path (), 417& [( "main.tsx" , "const n = <p></q>; console.log('executed');" )], 418); 419failure ( cli ( dir. path (), & [ "run" , "main.tsx" ]), "main.tsx" ); 420} 421 422# [ test ] 423fn config_discovery_and_explicit_extends_respect_execution_authority () { 424let dir = tempfile:: tempdir (). unwrap (); 425files ( 426 dir. path (), 427& [ 428( "tsconfig.json" , "not json" ), 429( 430"main.tsx" , 431"/** @jsxRuntime classic @jsx h */\nconst h = (tag: string) => tag; console.log(<p />);" , 432), 433], 434); 435assert_eq! ( success ( cli ( dir. path (), & [ "run" , "-0" , "main.tsx" ])), "p\n" ); 436failure ( 437cli ( dir. path (), & [ "run" , "-0" , "--allow-js-exec=." , "main.tsx" ]), 438"tsconfig.json" , 439); 440files ( 441 dir. path (), 442& [ 443( "tsconfig.json" , r#"{"extends":"./private/base.json"}"# ), 444( 445"private/base.json" , 446r#"{"compilerOptions":{"jsx":"react"}}"# , 447), 448], 449); 450failure ( 451cli ( dir. path (), & [ "run" , "--deny-js-exec=./private" , "main.tsx" ]), 452"PermissionDenied" , 453); 454} 455 456# [ test ] 457fn generated_imports_require_the_same_authority_as_written_imports () { 458let dir = tempfile:: tempdir (). unwrap (); 459files ( 460 dir. path (), 461& [ 462( 463"avril.conf" , 464"imports: {'react/jsx-runtime': './runtime.js'}" , 465), 466( "runtime.js" , RUNTIME ), 467( 468"main.tsx" , 469"console.log('executed'); export const node = <p />;" , 470), 471], 472); 473failure ( 474cli ( 475 dir. path (), 476& [ "run" , "--deny-js-exec=./runtime.js" , "main.tsx" ], 477), 478"PermissionDenied" , 479); 480} 481 482# [ cfg ( unix )] 483# [ test ] 484fn symlink_and_untrusted_configs_are_not_compiler_authorities () { 485use std:: os:: unix:: fs::{ PermissionsExt , symlink}; 486let dir = tempfile:: tempdir (). unwrap (); 487files ( 488 dir. path (), 489& [ 490( "target.json" , r#"{"compilerOptions":{"jsx":"react"}}"# ), 491( 492"main.tsx" , 493"/** @jsxRuntime classic @jsx h */\nconst h = (tag: string) => tag; console.log(<p />);" , 494), 495], 496); 497symlink ( "target.json" , dir. path (). join ( "tsconfig.json" )). unwrap (); 498failure ( cli ( dir. path (), & [ "run" , "main.tsx" ]), "non-symlink" ); 499assert_eq! ( success ( cli ( dir. path (), & [ "run" , "-0" , "main.tsx" ])), "p\n" ); 500 fs:: remove_file ( dir. path (). join ( "tsconfig.json" )). unwrap (); 501files ( 502 dir. path (), 503& [( "tsconfig.json" , r#"{"compilerOptions":{"jsx":"react"}}"# )], 504); 505 fs:: set_permissions ( 506 dir. path (). join ( "tsconfig.json" ), 507 fs:: Permissions :: from_mode ( 0o666 ), 508) 509. unwrap (); 510failure ( cli ( dir. path (), & [ "run" , "main.tsx" ]), "Untrusted config" ); 511} 512 513# [ test ] 514fn test_discovery_and_workers_execute_tsx_using_their_own_loader () { 515let dir = tempfile:: tempdir (). unwrap (); 516files ( 517 dir. path (), 518& [ 519( 520"avril.conf" , 521"imports: {'react/jsx-runtime': './runtime.js'}" , 522), 523( "runtime.js" , RUNTIME ), 524( "worker.tsx" , "postMessage((<worker />).tag); close();" ), 525( 526"ui_test.tsx" , 527r#" 528import {test} from 'avril:test'; 529test('tsx and worker', async () => { 530if ((<p />).tag !== 'p') throw Error('Wrong element'); 531const worker = new Worker(new URL('./worker.tsx', import.meta.url), {type:'module'}); 532const tag = await new Promise((resolve, reject) => { 533worker.onmessage = ({data}) => resolve(data); 534worker.onerror = e => {e.preventDefault(); reject(Error(e.message));}; 535}); 536worker.terminate(); 537if (tag !== 'worker') throw Error('Wrong worker element'); 538}); 539"# , 540), 541], 542); 543assert! ( success ( cli ( dir. path (), & [ "test" ])). contains ( "ok - tsx and worker" )); 544} 545 546# [ test ] 547fn implicit_jsr_runtime_imports_are_locked_vendored_and_usable_offline () { 548let dir = tempfile:: tempdir (). unwrap (); 549let registry = registry_fixture:: Registry :: new (); 550let runtime =format! ( "/** @ts-self-types=\"./jsx-runtime.d.ts\" */\n{RUNTIME}" ); 551 registry. package ( "@test/ui" , "1.0.0" , & [ 552( "jsx-runtime.js" , & runtime), 553( "jsx-runtime.d.ts" , "export const jsx: (tag: string, props: object) => object; export namespace JSX { export interface IntrinsicElements { p: {}; } }" ), 554], & [( "./jsx-runtime" , "./jsx-runtime.js" )]); 555files ( 556 dir. path (), 557& [ 558( 559"tsconfig.json" , 560r#"{"compilerOptions":{"jsxImportSource":"jsr:@test/ui@1"}}"# , 561), 562( "main.tsx" , "console.log((<p />).tag);" ), 563], 564); 565let registry_option =format! ( "--jsr-registry={}" , registry. url ); 566assert_eq! ( 567success ( cli ( 568 dir. path (), 569& [ "run" , & registry_option, "--cache-dir=cache" , "main.tsx" ] 570)), 571"p\n" 572); 573let lock = registry_fixture:: read_lock ( dir. path ()); 574assert! ( lock. to_string (). contains ( "@test/ui" )); 575success ( cli ( 576 dir. path (), 577& [ "vendor" , & registry_option, "--cache-dir=cache" , "main.tsx" ], 578)); 579 fs:: remove_dir_all ( dir. path (). join ( "cache" )). unwrap (); 580let before = registry. requests . lock (). unwrap (). len (); 581assert_eq! ( 582success ( cli ( 583 dir. path (), 584& [ 585"run" , 586& registry_option, 587"--cache-dir=cache" , 588"--offline" , 589"--frozen" , 590"--vendor=vendor" , 591"main.tsx" 592] 593)), 594"p\n" 595); 596assert_eq! ( registry. requests . lock (). unwrap (). len (), before); 597} 598 599# [ test ] 600fn denied_package_boundaries_and_later_config_revocation_do_not_leak_ancestor_settings () { 601let dir = tempfile:: tempdir (). unwrap (); 602files ( 603 dir. path (), 604& [ 605( 606"tsconfig.json" , 607r#"{"compilerOptions":{"jsx":"react","jsxFactory":"wrong"}}"# , 608), 609( "child/avril.conf" , "{}" ), 610( "child/main.tsx" , "export const node = <p />;" ), 611], 612); 613failure ( 614cli ( 615 dir. path (), 616& [ "run" , "--deny-js-exec=./child/avril.conf" , "child/main.tsx" ], 617), 618"react/jsx-runtime" , 619); 620files ( 621 dir. path (), 622& [ 623( "avril.conf" , "{}" ), 624( 625"tsconfig.json" , 626r#"{"compilerOptions":{"jsxImportSource":"./runtime"}}"# , 627), 628( "runtime/jsx-runtime" , RUNTIME ), 629( "first.tsx" , "export const node = <first />;" ), 630( "second.tsx" , "export const node = <second />;" ), 631( 632"main.js" , 633r#" 634import {pledge} from 'avril:std'; 635console.log((await import('./first.tsx')).node.tag); 636pledge({jsExec: {deny: ['./tsconfig.json']}}); 637try { await import('./second.tsx'); throw Error('Revoked config used'); } 638catch (error) { 639if (!error.message.includes('react/jsx-runtime')) throw error; 640console.log('revoked'); 641} 642"# , 643), 644], 645); 646assert_eq! ( 647success ( cli ( dir. path (), & [ "run" , "main.js" ])), 648"first\nrevoked\n" 649); 650}