mod common;
#[path = "common/http.rs"]
#[allow(dead_code)]
mod http_fixture;
#[path = "common/registry.rs"]
mod registry_fixture;
use std::fs;
use std::path::Path;
use std::process::{Command, Output, Stdio};
const RUNTIME: &str = r#"
export const Fragment = 'fragment';
export const jsx = (tag, props, key) => ({tag, props, key});
export const jsxs = jsx;
export const jsxDEV = (tag, props, key, stable, source) => ({tag, props, key, stable, source});
export const createElement = (tag, props, ...children) => ({tag, props: {...props, children}, key: props?.key});
export const h = createElement, F = Fragment;
"#;
fn files(dir: &Path, entries: &[(&str, &str)]) {
for (name, source) in entries {
let path = dir.join(name);
fs::create_dir_all(path.parent().unwrap()).unwrap();
fs::write(path, source).unwrap();
}
}
fn cli(dir: &Path, args: &[&str]) -> Output {
let child = Command::new(env!("CARGO_BIN_EXE_avril"))
.current_dir(dir)
.args(args)
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.unwrap();
common::wait(child)
}
fn success(output: Output) -> String {
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
String::from_utf8(output.stdout).unwrap()
}
fn failure(output: Output, message: &str) {
assert!(!output.status.success());
assert!(
output.stdout.is_empty(),
"{}",
String::from_utf8_lossy(&output.stdout)
);
assert!(
String::from_utf8_lossy(&output.stderr).contains(message),
"{}",
String::from_utf8_lossy(&output.stderr)
);
}
#[test]
fn jsx_and_tsx_default_to_automatic_and_generated_imports_join_the_graph() {
let dir = tempfile::tempdir().unwrap();
files(
dir.path(),
&[
(
"avril.conf",
"imports: {'react/jsx-runtime': './runtime.js', react: './runtime.js'}",
),
("runtime.js", RUNTIME),
(
"dep.jsx",
"export const child = hello & goodbye;",
),
(
"main.tsx",
r#"
import type {Missing} from './missing.ts';
import {child} from './dep.jsx';
const Component = (props: {name: string}) => props.name;
const props = {id: 1};
const node = <>
classic
>; console.log(node.tag, node.props.children[0].tag, node.props.children[0].props.children[0]); "#, ), ], ); assert_eq!( success(cli(dir.path(), &["run", "main.tsx"])), "fragment p classic\n" ); } #[test] fn development_jsx_and_stack_traces_retain_source_locations() { let dir = tempfile::tempdir().unwrap(); files( dir.path(), &[ ( "avril.conf", "imports: {'custom/jsx-dev-runtime': './runtime.js'}", ), ( "tsconfig.json", r#"{"compilerOptions":{"jsx":"react-jsxdev","jsxImportSource":"custom"}}"#, ), ("runtime.js", RUNTIME), ( "main.tsx", "type Gone = number;\nconst node = ;\nconsole.log(node.source.lineNumber, node.source.columnNumber, node.source.fileName.endsWith('main.tsx'));\nthrow Error('mapped');\n", ), ], ); let output = cli(dir.path(), &["run", "main.tsx"]); assert!(!output.status.success()); assert_eq!(output.stdout, b"2 14 true\n"); let stderr = String::from_utf8_lossy(&output.stderr); assert!(stderr.contains("main.tsx:4:7"), "{stderr}"); } #[test] fn nearest_configs_apply_only_within_their_own_package() { let dir = tempfile::tempdir().unwrap(); files( dir.path(), &[ ("avril.conf", "{}"), ( "tsconfig.json", r#"{"compilerOptions":{"jsx":"react","jsxFactory":"h"}}"#, ), ("runtime.js", RUNTIME), ( "main.tsx", "import {h} from './runtime.js'; import {nested} from './src/nested.tsx'; import {library} from './lib/mod.tsx'; console.log(().tag, nested.tag, library.tag);", ), ( "src/tsconfig.json", r#"{"compilerOptions":{"jsx":"react","jsxFactory":"custom"}}"#, ), ( "src/nested.tsx", "import {h as custom} from '../runtime.js'; export const nested ={{n}}
).props.children);", http.name("ui"))); http.redirect("entry", "/nested/main.tsx"); assert_eq!( success(cli( dir.path(), &[ "run", "-0", &format!("--allow-js-exec={}", http.url), "--cache-dir=cache", &http.name("entry") ] )), "remote\n" ); } #[test] fn invalid_configs_and_preserved_jsx_fail_before_application_execution() { for (config, expected) in [ ( r#"{"compilerOptions":{"jsx":"preserve"}}"#, "cannot execute preserved JSX", ), ( r#"{"compilerOptions":{"jsx":"react-native"}}"#, "cannot execute preserved JSX", ), ( r#"{"compilerOptions":{"jsx":"unknown"}}"#, "Unsupported compilerOptions.jsx", ), ( r#"{"compilerOptions":{"jsxImportSource":3}}"#, "jsxImportSource must be", ), ( r#"{"compilerOptions":{"jsxFactory":""}}"#, "jsxFactory must be", ), ( r#"{"compilerOptions":{"jsx":"react","jsxFactory":"h()"} }"#, "jsxFactory must be an identifier", ), ( r#"{"compilerOptions":{"jsxFragmentFactory":"a..b"}}"#, "jsxFragmentFactory must be an identifier", ), ( r#"{"compilerOptions":[]}"#, "compilerOptions must be an object", ), ( r#"{"extends":"npm:config"}"#, "extends must use relative or absolute", ), (r#"{"extends":"./missing.json"}"#, "Cannot find tsconfig"), ( r#"{"extends":"./tsconfig.json"}"#, "Cyclic tsconfig extends", ), (r#"{"extends":[3]}"#, "extends entries must be paths"), (r#"{"extends":3}"#, "extends must be a path"), ("[]", "tsconfig must be an object"), ("{bad}", "tsconfig.json"), ("{'compilerOptions': {}}", "tsconfig.json"), ] { let dir = tempfile::tempdir().unwrap(); files( dir.path(), &[ ("tsconfig.json", config), ( "main.tsx", "console.log('executed'); export const node = ;", ), ], ); failure(cli(dir.path(), &["run", "main.tsx"]), expected); } let dir = tempfile::tempdir().unwrap(); files( dir.path(), &[("main.tsx", "const n =; console.log('executed');")], ); failure(cli(dir.path(), &["run", "main.tsx"]), "main.tsx"); } #[test] fn config_discovery_and_explicit_extends_respect_execution_authority() { let dir = tempfile::tempdir().unwrap(); files( dir.path(), &[ ("tsconfig.json", "not json"), ( "main.tsx", "/** @jsxRuntime classic @jsx h */\nconst h = (tag: string) => tag; console.log(
);", ), ], ); assert_eq!(success(cli(dir.path(), &["run", "-0", "main.tsx"])), "p\n"); failure( cli(dir.path(), &["run", "-0", "--allow-js-exec=.", "main.tsx"]), "tsconfig.json", ); files( dir.path(), &[ ("tsconfig.json", r#"{"extends":"./private/base.json"}"#), ( "private/base.json", r#"{"compilerOptions":{"jsx":"react"}}"#, ), ], ); failure( cli(dir.path(), &["run", "--deny-js-exec=./private", "main.tsx"]), "PermissionDenied", ); } #[test] fn generated_imports_require_the_same_authority_as_written_imports() { let dir = tempfile::tempdir().unwrap(); files( dir.path(), &[ ( "avril.conf", "imports: {'react/jsx-runtime': './runtime.js'}", ), ("runtime.js", RUNTIME), ( "main.tsx", "console.log('executed'); export const node = ;", ), ], ); failure( cli( dir.path(), &["run", "--deny-js-exec=./runtime.js", "main.tsx"], ), "PermissionDenied", ); } #[cfg(unix)] #[test] fn symlink_and_untrusted_configs_are_not_compiler_authorities() { use std::os::unix::fs::{PermissionsExt, symlink}; let dir = tempfile::tempdir().unwrap(); files( dir.path(), &[ ("target.json", r#"{"compilerOptions":{"jsx":"react"}}"#), ( "main.tsx", "/** @jsxRuntime classic @jsx h */\nconst h = (tag: string) => tag; console.log();", ), ], ); symlink("target.json", dir.path().join("tsconfig.json")).unwrap(); failure(cli(dir.path(), &["run", "main.tsx"]), "non-symlink"); assert_eq!(success(cli(dir.path(), &["run", "-0", "main.tsx"])), "p\n"); fs::remove_file(dir.path().join("tsconfig.json")).unwrap(); files( dir.path(), &[("tsconfig.json", r#"{"compilerOptions":{"jsx":"react"}}"#)], ); fs::set_permissions( dir.path().join("tsconfig.json"), fs::Permissions::from_mode(0o666), ) .unwrap(); failure(cli(dir.path(), &["run", "main.tsx"]), "Untrusted config"); } #[test] fn test_discovery_and_workers_execute_tsx_using_their_own_loader() { let dir = tempfile::tempdir().unwrap(); files( dir.path(), &[ ( "avril.conf", "imports: {'react/jsx-runtime': './runtime.js'}", ), ("runtime.js", RUNTIME), ("worker.tsx", "postMessage((