char/avril

(wip) why'd you have to go and make things so complicaaaaated?

git clone https://git.t4t.associates/char/avril

Charlotte Somsnapshot: embed shared main and worker startup in the CLI688e96e

main
11.5 KiB337 linesraw
1use std::fs;
2use std::process::Command;
3
4#[test]
5fn loads_a_module_graph_from_the_cli() {
6    let dir = tempfile::tempdir().unwrap();
7    fs::write(
8        dir.path().join("main.js"),
9        r#"
10        import { message } from "./dep.js";
11        import { count } from "././dep.js";
12        import { getMessage } from "./cycle.js";
13        console.log(await Promise.resolve(message), count, getMessage());
14        export {};
15        "#,
16    )
17    .unwrap();
18    fs::write(
19        dir.path().join("dep.js"),
20        r#"
21        import "./cycle.js";
22        export const message = "hello";
23        export const count = globalThis.count = (globalThis.count ?? 0) + 1;
24        "#,
25    )
26    .unwrap();
27    fs::write(
28        dir.path().join("cycle.js"),
29        r#"
30        import { message } from "./dep.js";
31        export function getMessage() { return message; }
32        "#,
33    )
34    .unwrap();
35
36    let output = Command::new(env!("CARGO_BIN_EXE_avril"))
37        .arg("run")
38        .current_dir(dir.path())
39        .arg("main.js")
40        .output()
41        .unwrap();
42    assert!(
43        output.status.success(),
44        "{}",
45        String::from_utf8_lossy(&output.stderr)
46    );
47    assert_eq!(output.stdout, b"hello 1 hello\n");
48}
49
50#[test]
51fn module_failures_exit_unsuccessfully() {
52    let dir = tempfile::tempdir().unwrap();
53    let path = dir.path().join("main.js");
54    for (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        (
64            r#"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();
81        let output = Command::new(env!("CARGO_BIN_EXE_avril"))
82            .arg("run")
83            .arg(&path)
84            .output()
85            .unwrap();
86        let stderr = String::from_utf8_lossy(&output.stderr);
87        assert!(!output.status.success(), "{source}");
88        assert!(stderr.contains(expected), "{stderr}");
89    }
90}
91
92fn run_graph(dir: &std::path::Path, files: &[(&str, &str)]) -> std::process::Output {
93    for (name, source) in files {
94        let path = dir.join(name);
95        fs::create_dir_all(path.parent().unwrap()).unwrap();
96        fs::write(path, source).unwrap();
97    }
98    Command::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() {
108    let dir = tempfile::tempdir().unwrap();
109    let output = run_graph(
110        dir.path(),
111        &[
112            (
113                "main #😀.ts",
114                r#"
115            import { count as shared } from './sub/shared.ts';
116            console.log(import.meta.main, import.meta.url);
117            console.log(import.meta === import.meta, Object.getPrototypeOf(import.meta) === null);
118            const [a, b, c] = await Promise.all([
119                import('./sub/lazy.mts'), import('./sub/./lazy.mts'), import('./sub/lazy.mts'),
120            ]);
121            console.log(a === b, b === c, a.count, a.value, shared, a.main,
122                (await a.self()) === a, (await a.nested()).main);
123            Promise.prototype.then = () => { throw new Error('Poisoned then'); };
124            Object.defineProperty(Promise, Symbol.species, { get() { throw new Error('Poisoned species'); } });
125            console.log((await import('./fresh.ts')).value);
126            console.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",
135                r#"
136            import './shared.ts';
137            export const count = globalThis.loads = (globalThis.loads ?? 0) + 1;
138            export const value: string = await Promise.resolve('hello');
139            export const main = import.meta.main;
140            export const self = () => import(import.meta.url.replace('file:', 'FILE:'));
141            export 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    );
155    assert!(
156        output.status.success(),
157        "{}",
158        String::from_utf8_lossy(&output.stderr)
159    );
160    let stdout = String::from_utf8(output.stdout).unwrap();
161    let (metadata, rest) = stdout.split_once('\n').unwrap();
162    let (main, url) = metadata.split_once(' ').unwrap();
163    assert_eq!(main, "true");
164    assert!(url.starts_with("file:"), "{url}");
165    assert!(
166        url.contains("%23") && url.contains("%20") && url.contains("%F0%9F%98%80"),
167        "{url}"
168    );
169    assert_eq!(
170        url::Url::parse(url).unwrap().to_file_path().unwrap(),
171        dir.path().canonicalize().unwrap().join("main #😀.ts")
172    );
173    assert_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() {
181    let dir = tempfile::tempdir().unwrap();
182    let output = run_graph(
183        dir.path(),
184        &[
185            (
186                "main.mts",
187                r#"
188            for (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            ]) {
197                let promise;
198                try { promise = import(path); } catch { throw new Error('Synchronous import failure'); }
199                const error = await promise.then(() => { throw new Error('Unexpected success'); }, e => e);
200                if (!error.message.includes(message)) throw error;
201            }
202            try {
203                await import('./good.ts', { with: { type: 'json' } });
204                throw new Error('Unexpected attributes success');
205            } catch (error) {
206                if (!error.message.includes('Import attributes are not supported')) throw error;
207            }
208            const [a, b] = await Promise.allSettled([import('./throws.ts'), import('./throws.ts')]);
209            console.log(a.status, b.status, a.reason === b.reason,
210                a.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    );
226    assert!(
227        output.status.success(),
228        "{}",
229        String::from_utf8_lossy(&output.stderr)
230    );
231    assert_eq!(output.stdout, b"rejected rejected true true 42\n");
232}
233
234#[test]
235fn imports_during_evaluation_can_reimport_their_own_modules() {
236    let dir = tempfile::tempdir().unwrap();
237    let output = run_graph(
238        dir.path(),
239        &[
240            (
241                "main.js",
242                r#"
243            import './dep.js';
244            const self = import(import.meta.url);
245            export const value = 42;
246            self.then(ns => console.log('entry', ns.value, ns.main));
247            export const main = import.meta.main;
248        "#,
249            ),
250            (
251                "dep.js",
252                r#"
253            import { value } from './main.js';
254            import('./dep.js').then(ns => console.log('dependency', ns.read(), ns.main));
255            export function read() { return value; }
256            export const main = import.meta.main;
257        "#,
258            ),
259        ],
260    );
261    assert!(
262        output.status.success(),
263        "{}",
264        String::from_utf8_lossy(&output.stderr)
265    );
266    assert_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() {
271    let dir = tempfile::tempdir().unwrap();
272    let 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    );
287    assert!(
288        output.status.success(),
289        "{}",
290        String::from_utf8_lossy(&output.stderr)
291    );
292    assert_eq!(output.stdout, b"42 prepared\n");
293}
294
295#[test]
296fn reimporting_an_evaluated_module_does_not_reexecute_revoked_dependencies() {
297    let dir = tempfile::tempdir().unwrap();
298    let 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    );
310    assert!(
311        output.status.success(),
312        "{}",
313        String::from_utf8_lossy(&output.stderr)
314    );
315    assert_eq!(output.stdout, b"true 42\ntrue\n");
316}
317
318#[test]
319fn dynamically_imported_invalid_javascript_rejects_with_a_syntax_error() {
320    let dir = tempfile::tempdir().unwrap();
321    let 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    );
331    assert!(
332        output.status.success(),
333        "{}",
334        String::from_utf8_lossy(&output.stderr)
335    );
336    assert_eq!(output.stdout, b"true\n");
337}