use std::fs; use std::process::Command; #[test] fn loads_a_module_graph_from_the_cli() { let dir = tempfile::tempdir().unwrap(); fs::write( dir.path().join("main.js"), r#" import { message } from "./dep.js"; import { count } from "././dep.js"; import { getMessage } from "./cycle.js"; console.log(await Promise.resolve(message), count, getMessage()); export {}; "#, ) .unwrap(); fs::write( dir.path().join("dep.js"), r#" import "./cycle.js"; export const message = "hello"; export const count = globalThis.count = (globalThis.count ?? 0) + 1; "#, ) .unwrap(); fs::write( dir.path().join("cycle.js"), r#" import { message } from "./dep.js"; export function getMessage() { return message; } "#, ) .unwrap(); let output = Command::new(env!("CARGO_BIN_EXE_avril")) .arg("run") .current_dir(dir.path()) .arg("main.js") .output() .unwrap(); assert!( output.status.success(), "{}", String::from_utf8_lossy(&output.stderr) ); assert_eq!(output.stdout, b"hello 1 hello\n"); } #[test] fn module_failures_exit_unsuccessfully() { let dir = tempfile::tempdir().unwrap(); let path = dir.path().join("main.js"); for (source, expected) in [ ("import './missing.js';", "missing.js"), ("import('./missing.js');", "missing.js"), ("import 'package';", "Unsupported module specifier"), ("const =", "SyntaxError"), ( "Promise.reject(new Error('unhandled'));", "Error: unhandled", ), ( r#"import { stdout } from 'avril:std'; stdout.getWriter().write("text");"#, "Stdio writes require Uint8Array chunks", ), ( "function explode() { throw new Error('module failed'); } explode();", "Error: module failed\n at explode (", ), ( "await Promise.reject(new Error('await failed'));", "Error: await failed\n at ", ), ( "await new Promise(() => {});", "Module evaluation is still pending", ), ] { fs::write(&path, source).unwrap(); let output = Command::new(env!("CARGO_BIN_EXE_avril")) .arg("run") .arg(&path) .output() .unwrap(); let stderr = String::from_utf8_lossy(&output.stderr); assert!(!output.status.success(), "{source}"); assert!(stderr.contains(expected), "{stderr}"); } } fn run_graph(dir: &std::path::Path, files: &[(&str, &str)]) -> std::process::Output { for (name, source) in files { let path = dir.join(name); fs::create_dir_all(path.parent().unwrap()).unwrap(); fs::write(path, source).unwrap(); } Command::new(env!("CARGO_BIN_EXE_avril")) .arg("run") .current_dir(dir) .arg(files[0].0) .output() .unwrap() } #[test] fn dynamic_imports_share_the_module_graph_and_expose_metadata() { let dir = tempfile::tempdir().unwrap(); let output = run_graph( dir.path(), &[ ( "main #😀.ts", r#" import { count as shared } from './sub/shared.ts'; console.log(import.meta.main, import.meta.url); console.log(import.meta === import.meta, Object.getPrototypeOf(import.meta) === null); const [a, b, c] = await Promise.all([ import('./sub/lazy.mts'), import('./sub/./lazy.mts'), import('./sub/lazy.mts'), ]); console.log(a === b, b === c, a.count, a.value, shared, a.main, (await a.self()) === a, (await a.nested()).main); Promise.prototype.then = () => { throw new Error('Poisoned then'); }; Object.defineProperty(Promise, Symbol.species, { get() { throw new Error('Poisoned species'); } }); console.log((await import('./fresh.ts')).value); console.log(await import('./thenable.js')); "#, ), ( "sub/shared.ts", "export const count = globalThis.shared = (globalThis.shared ?? 0) + 1;", ), ( "sub/lazy.mts", r#" import './shared.ts'; export const count = globalThis.loads = (globalThis.loads ?? 0) + 1; export const value: string = await Promise.resolve('hello'); export const main = import.meta.main; export const self = () => import(import.meta.url.replace('file:', 'FILE:')); export const nested = () => import('../nested.js'); "#, ), ("nested.js", "export const main = import.meta.main;"), ( "fresh.ts", "export const value: number = await Promise.resolve(42);", ), ( "thenable.js", "export function then(resolve) { resolve('assimilated'); }", ), ], ); assert!( output.status.success(), "{}", String::from_utf8_lossy(&output.stderr) ); let stdout = String::from_utf8(output.stdout).unwrap(); let (metadata, rest) = stdout.split_once('\n').unwrap(); let (main, url) = metadata.split_once(' ').unwrap(); assert_eq!(main, "true"); assert!(url.starts_with("file:"), "{url}"); assert!( url.contains("%23") && url.contains("%20") && url.contains("%F0%9F%98%80"), "{url}" ); assert_eq!( url::Url::parse(url).unwrap().to_file_path().unwrap(), dir.path().canonicalize().unwrap().join("main #😀.ts") ); assert_eq!( rest, "true true\ntrue true 1 hello 1 false true false\n42\nassimilated\n" ); } #[test] fn dynamic_import_failures_reject_without_poisoning_later_imports() { let dir = tempfile::tempdir().unwrap(); let output = run_graph( dir.path(), &[ ( "main.mts", r#" for (const [path, message] of [ ['./missing.js', 'missing.js'], ['./bad.js', 'Unexpected token'], ['./link.js', 'absent.js'], ['./throws.ts', 'synchronous'], ['./await.mts', 'asynchronous'], ['package', 'Unsupported module specifier'], ['node:fs', 'not supported'], ['file://[invalid', 'Invalid file URL'], [import.meta.url + '?v=1', 'queries and fragments'], [import.meta.url + '#fragment', 'queries and fragments'], ]) { let promise; try { promise = import(path); } catch { throw new Error('Synchronous import failure'); } const error = await promise.then(() => { throw new Error('Unexpected success'); }, e => e); if (!error.message.includes(message)) throw error; } try { await import('./good.ts', { with: { type: 'json' } }); throw new Error('Unexpected attributes success'); } catch (error) { if (!error.message.includes('Import attributes are not supported')) throw error; } const [a, b] = await Promise.allSettled([import('./throws.ts'), import('./throws.ts')]); console.log(a.status, b.status, a.reason === b.reason, a.reason.stack.includes('throws.ts:5:7'), (await import('./good.ts')).value); "#, ), ("bad.js", "export const = ;"), ("link.js", "import './absent.js';"), ( "throws.ts", "type Erased = {\n field: string;\n};\nglobalThis.failed = (globalThis.failed ?? 0) + 1;\nthrow new Error('synchronous');", ), ( "await.mts", "await Promise.reject(new Error('asynchronous'));", ), ("good.ts", "export const value: number = 42;"), ], ); assert!( output.status.success(), "{}", String::from_utf8_lossy(&output.stderr) ); assert_eq!(output.stdout, b"rejected rejected true true 42\n"); } #[test] fn imports_during_evaluation_can_reimport_their_own_modules() { let dir = tempfile::tempdir().unwrap(); let output = run_graph( dir.path(), &[ ( "main.js", r#" import './dep.js'; const self = import(import.meta.url); export const value = 42; self.then(ns => console.log('entry', ns.value, ns.main)); export const main = import.meta.main; "#, ), ( "dep.js", r#" import { value } from './main.js'; import('./dep.js').then(ns => console.log('dependency', ns.read(), ns.main)); export function read() { return value; } export const main = import.meta.main; "#, ), ], ); assert!( output.status.success(), "{}", String::from_utf8_lossy(&output.stderr) ); assert_eq!(output.stdout, b"dependency 42 false\nentry 42 true\n"); } #[test] fn prepares_named_and_star_reexports_after_erasing_type_only_dependencies() { let dir = tempfile::tempdir().unwrap(); let output = run_graph( dir.path(), &[ ( "main.ts", "const path = './bridge.ts'; const a = await import(path); console.log(a.value, a.other);", ), ( "bridge.ts", "export type {Missing} from './not-present.ts'; export {value} from './named.ts'; export * from './star.ts';", ), ("named.ts", "export const value: number = 42;"), ("star.ts", "export const other = 'prepared';"), ], ); assert!( output.status.success(), "{}", String::from_utf8_lossy(&output.stderr) ); assert_eq!(output.stdout, b"42 prepared\n"); } #[test] fn reimporting_an_evaluated_module_does_not_reexecute_revoked_dependencies() { let dir = tempfile::tempdir().unwrap(); let output = run_graph( dir.path(), &[ ( "main.js", "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'));}", ), ("a.js", "import {value} from './b.js'; export {value};"), ("b.js", "export const value = 42;"), ("c.js", "export {value} from './a.js';"), ], ); assert!( output.status.success(), "{}", String::from_utf8_lossy(&output.stderr) ); assert_eq!(output.stdout, b"true 42\ntrue\n"); } #[test] fn dynamically_imported_invalid_javascript_rejects_with_a_syntax_error() { let dir = tempfile::tempdir().unwrap(); let output = run_graph( dir.path(), &[ ( "main.js", "try {await import('./invalid.js'); throw new Error('allowed');} catch(e) {console.log(e instanceof SyntaxError);}", ), ("invalid.js", "export const = ;"), ], ); assert!( output.status.success(), "{}", String::from_utf8_lossy(&output.stderr) ); assert_eq!(output.stdout, b"true\n"); }