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:: process:: Command ; 3 4# [ test ] 5fn loads_a_module_graph_from_the_cli () { 6let dir = tempfile:: tempdir (). unwrap (); 7 fs:: write ( 8 dir. path (). join ( "main.js" ), 9r#" 10import { message } from "./dep.js"; 11import { count } from "././dep.js"; 12import { getMessage } from "./cycle.js"; 13console.log(await Promise.resolve(message), count, getMessage()); 14export {}; 15"# , 16) 17. unwrap (); 18 fs:: write ( 19 dir. path (). join ( "dep.js" ), 20r#" 21import "./cycle.js"; 22export const message = "hello"; 23export const count = globalThis.count = (globalThis.count ?? 0) + 1; 24"# , 25) 26. unwrap (); 27 fs:: write ( 28 dir. path (). join ( "cycle.js" ), 29r#" 30import { message } from "./dep.js"; 31export function getMessage() { return message; } 32"# , 33) 34. unwrap (); 35 36let output =Command :: new ( env! ( "CARGO_BIN_EXE_avril" )) 37. arg ( "run" ) 38. current_dir ( dir. path ()) 39. arg ( "main.js" ) 40. output () 41. unwrap (); 42assert! ( 43 output. status . success (), 44"{}" , 45String :: from_utf8_lossy ( & output. stderr ) 46); 47assert_eq! ( output. stdout , b"hello 1 hello\n" ); 48} 49 50# [ test ] 51fn module_failures_exit_unsuccessfully () { 52let dir = tempfile:: tempdir (). unwrap (); 53let path = dir. path (). join ( "main.js" ); 54for ( source, expected) in [ 55( "import './missing.js';" , "missing.js" ), 56( "import('./missing.js');" , "missing.js" ), 57( "import 'package';" , "Unsupported module specifier" ), 58( "const =" , "SyntaxError" ), 59( 60"Promise.reject(new Error('unhandled'));" , 61"Error: unhandled" , 62), 63( 64r#"import { stdout } from 'avril:std'; stdout.getWriter().write("text");"# , 65"Stdio writes require Uint8Array chunks" , 66), 67( 68"function explode() { throw new Error('module failed'); } explode();" , 69"Error: module failed\n at explode (" , 70), 71( 72"await Promise.reject(new Error('await failed'));" , 73"Error: await failed\n at " , 74), 75( 76"await new Promise(() => {});" , 77"Module evaluation is still pending" , 78), 79] { 80 fs:: write ( & path, source). unwrap (); 81let output =Command :: new ( env! ( "CARGO_BIN_EXE_avril" )) 82. arg ( "run" ) 83. arg ( & path) 84. output () 85. unwrap (); 86let stderr =String :: from_utf8_lossy ( & output. stderr ); 87assert! ( !output. status . success (), "{source}" ); 88assert! ( stderr. contains ( expected), "{stderr}" ); 89} 90} 91 92fn run_graph ( dir : & std:: path:: Path , files : & [( & str , & str )]) -> std:: process:: Output { 93for ( name, source) in files{ 94let path = dir. join ( name); 95 fs:: create_dir_all ( path. parent (). unwrap ()). unwrap (); 96 fs:: write ( path, source). unwrap (); 97} 98Command :: new ( env! ( "CARGO_BIN_EXE_avril" )) 99. arg ( "run" ) 100. current_dir ( dir) 101. arg ( files[ 0 ]. 0 ) 102. output () 103. unwrap () 104} 105 106# [ test ] 107fn dynamic_imports_share_the_module_graph_and_expose_metadata () { 108let dir = tempfile:: tempdir (). unwrap (); 109let output =run_graph ( 110 dir. path (), 111& [ 112( 113"main #😀.ts" , 114r#" 115import { count as shared } from './sub/shared.ts'; 116console.log(import.meta.main, import.meta.url); 117console.log(import.meta === import.meta, Object.getPrototypeOf(import.meta) === null); 118const [a, b, c] = await Promise.all([ 119import('./sub/lazy.mts'), import('./sub/./lazy.mts'), import('./sub/lazy.mts'), 120]); 121console.log(a === b, b === c, a.count, a.value, shared, a.main, 122(await a.self()) === a, (await a.nested()).main); 123Promise.prototype.then = () => { throw new Error('Poisoned then'); }; 124Object.defineProperty(Promise, Symbol.species, { get() { throw new Error('Poisoned species'); } }); 125console.log((await import('./fresh.ts')).value); 126console.log(await import('./thenable.js')); 127"# , 128), 129( 130"sub/shared.ts" , 131"export const count = globalThis.shared = (globalThis.shared ?? 0) + 1;" , 132), 133( 134"sub/lazy.mts" , 135r#" 136import './shared.ts'; 137export const count = globalThis.loads = (globalThis.loads ?? 0) + 1; 138export const value: string = await Promise.resolve('hello'); 139export const main = import.meta.main; 140export const self = () => import(import.meta.url.replace('file:', 'FILE:')); 141export const nested = () => import('../nested.js'); 142"# , 143), 144( "nested.js" , "export const main = import.meta.main;" ), 145( 146"fresh.ts" , 147"export const value: number = await Promise.resolve(42);" , 148), 149( 150"thenable.js" , 151"export function then(resolve) { resolve('assimilated'); }" , 152), 153], 154); 155assert! ( 156 output. status . success (), 157"{}" , 158String :: from_utf8_lossy ( & output. stderr ) 159); 160let stdout =String :: from_utf8 ( output. stdout ). unwrap (); 161let ( metadata, rest) = stdout. split_once ( '\n' ). unwrap (); 162let ( main, url) = metadata. split_once ( ' ' ). unwrap (); 163assert_eq! ( main, "true" ); 164assert! ( url. starts_with ( "file:" ), "{url}" ); 165assert! ( 166 url. contains ( "%23" ) && url. contains ( "%20" ) && url. contains ( "%F0%9F%98%80" ), 167"{url}" 168); 169assert_eq! ( 170 url:: Url :: parse ( url). unwrap (). to_file_path (). unwrap (), 171 dir. path (). canonicalize (). unwrap (). join ( "main #😀.ts" ) 172); 173assert_eq! ( 174 rest, 175"true true\ntrue true 1 hello 1 false true false\n42\nassimilated\n" 176); 177} 178 179# [ test ] 180fn dynamic_import_failures_reject_without_poisoning_later_imports () { 181let dir = tempfile:: tempdir (). unwrap (); 182let output =run_graph ( 183 dir. path (), 184& [ 185( 186"main.mts" , 187r#" 188for (const [path, message] of [ 189['./missing.js', 'missing.js'], ['./bad.js', 'Unexpected token'], 190['./link.js', 'absent.js'], ['./throws.ts', 'synchronous'], 191['./await.mts', 'asynchronous'], ['package', 'Unsupported module specifier'], 192['node:fs', 'not supported'], 193['file://[invalid', 'Invalid file URL'], 194[import.meta.url + '?v=1', 'queries and fragments'], 195[import.meta.url + '#fragment', 'queries and fragments'], 196]) { 197let promise; 198try { promise = import(path); } catch { throw new Error('Synchronous import failure'); } 199const error = await promise.then(() => { throw new Error('Unexpected success'); }, e => e); 200if (!error.message.includes(message)) throw error; 201} 202try { 203await import('./good.ts', { with: { type: 'json' } }); 204throw new Error('Unexpected attributes success'); 205} catch (error) { 206if (!error.message.includes('Import attributes are not supported')) throw error; 207} 208const [a, b] = await Promise.allSettled([import('./throws.ts'), import('./throws.ts')]); 209console.log(a.status, b.status, a.reason === b.reason, 210a.reason.stack.includes('throws.ts:5:7'), (await import('./good.ts')).value); 211"# , 212), 213( "bad.js" , "export const = ;" ), 214( "link.js" , "import './absent.js';" ), 215( 216"throws.ts" , 217"type Erased = {\n field: string;\n};\nglobalThis.failed = (globalThis.failed ?? 0) + 1;\nthrow new Error('synchronous');" , 218), 219( 220"await.mts" , 221"await Promise.reject(new Error('asynchronous'));" , 222), 223( "good.ts" , "export const value: number = 42;" ), 224], 225); 226assert! ( 227 output. status . success (), 228"{}" , 229String :: from_utf8_lossy ( & output. stderr ) 230); 231assert_eq! ( output. stdout , b"rejected rejected true true 42\n" ); 232} 233 234# [ test ] 235fn imports_during_evaluation_can_reimport_their_own_modules () { 236let dir = tempfile:: tempdir (). unwrap (); 237let output =run_graph ( 238 dir. path (), 239& [ 240( 241"main.js" , 242r#" 243import './dep.js'; 244const self = import(import.meta.url); 245export const value = 42; 246self.then(ns => console.log('entry', ns.value, ns.main)); 247export const main = import.meta.main; 248"# , 249), 250( 251"dep.js" , 252r#" 253import { value } from './main.js'; 254import('./dep.js').then(ns => console.log('dependency', ns.read(), ns.main)); 255export function read() { return value; } 256export const main = import.meta.main; 257"# , 258), 259], 260); 261assert! ( 262 output. status . success (), 263"{}" , 264String :: from_utf8_lossy ( & output. stderr ) 265); 266assert_eq! ( output. stdout , b"dependency 42 false\nentry 42 true\n" ); 267} 268 269# [ test ] 270fn prepares_named_and_star_reexports_after_erasing_type_only_dependencies () { 271let dir = tempfile:: tempdir (). unwrap (); 272let output =run_graph ( 273 dir. path (), 274& [ 275( 276"main.ts" , 277"const path = './bridge.ts'; const a = await import(path); console.log(a.value, a.other);" , 278), 279( 280"bridge.ts" , 281"export type {Missing} from './not-present.ts'; export {value} from './named.ts'; export * from './star.ts';" , 282), 283( "named.ts" , "export const value: number = 42;" ), 284( "star.ts" , "export const other = 'prepared';" ), 285], 286); 287assert! ( 288 output. status . success (), 289"{}" , 290String :: from_utf8_lossy ( & output. stderr ) 291); 292assert_eq! ( output. stdout , b"42 prepared\n" ); 293} 294 295# [ test ] 296fn reimporting_an_evaluated_module_does_not_reexecute_revoked_dependencies () { 297let dir = tempfile:: tempdir (). unwrap (); 298let output =run_graph ( 299 dir. path (), 300& [ 301( 302"main.js" , 303"import {pledge} from 'avril:std'; const a = await import('./a.js'); pledge({jsExec: {deny: ['./b.js']}}); console.log((await import('./a.js')) === a, (await import('./c.js')).value); try {await import('./b.js'); throw new Error('allowed');} catch(e) {console.log(e.message.includes('PermissionDenied'));}" , 304), 305( "a.js" , "import {value} from './b.js'; export {value};" ), 306( "b.js" , "export const value = 42;" ), 307( "c.js" , "export {value} from './a.js';" ), 308], 309); 310assert! ( 311 output. status . success (), 312"{}" , 313String :: from_utf8_lossy ( & output. stderr ) 314); 315assert_eq! ( output. stdout , b"true 42\ntrue\n" ); 316} 317 318# [ test ] 319fn dynamically_imported_invalid_javascript_rejects_with_a_syntax_error () { 320let dir = tempfile:: tempdir (). unwrap (); 321let output =run_graph ( 322 dir. path (), 323& [ 324( 325"main.js" , 326"try {await import('./invalid.js'); throw new Error('allowed');} catch(e) {console.log(e instanceof SyntaxError);}" , 327), 328( "invalid.js" , "export const = ;" ), 329], 330); 331assert! ( 332 output. status . success (), 333"{}" , 334String :: from_utf8_lossy ( & output. stderr ) 335); 336assert_eq! ( output. stdout , b"true\n" ); 337}