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
15.8 KiB417 linesraw
1mod common;
2#[path = "common/http.rs"]
3mod http_fixture;
4#[path = "common/registry.rs"]
5mod registry_fixture;
6
7use std::fs;
8use std::process::{Command, Stdio};
9
10fn run(directory: &std::path::Path, entry: &str, args: &[String]) -> std::process::Output {
11    let output = common::wait(
12        Command::new(env!("CARGO_BIN_EXE_avril"))
13            .arg("run")
14            .current_dir(directory)
15            .args(args)
16            .arg(entry)
17            .stdout(Stdio::piped())
18            .stderr(Stdio::piped())
19            .spawn()
20            .unwrap(),
21    );
22    assert!(
23        output.status.success(),
24        "{}",
25        String::from_utf8_lossy(&output.stderr)
26    );
27    output
28}
29
30#[test]
31fn file_metadata_resolves_without_loading_and_binds_to_the_module() {
32    let dir = tempfile::tempdir().unwrap();
33    fs::create_dir(dir.path().join("nested")).unwrap();
34    fs::write(dir.path().join("avril.conf"), "imports: { alias: './not%20present%20%23%F0%9F%98%80.ts', blocked: null }, scopes: {'./nested/': {alias: './nested/missing.ts'}}").unwrap();
35    fs::write(
36        dir.path().join("nested/dep.ts"),
37        "export const resolve = import.meta.resolve; export const meta = import.meta;",
38    )
39    .unwrap();
40    fs::write(
41        dir.path().join("not-evaluated.js"),
42        "throw Error('Resolution evaluated a module');",
43    )
44    .unwrap();
45    fs::write(dir.path().join("main #😀.ts"), r#"
46        import {resolve as other, meta} from './nested/dep.ts';
47        const ownURL = import.meta.url;
48        if (import.meta.filename !== decodeURIComponent(new URL(ownURL).pathname) ||
49            import.meta.dirname !== decodeURIComponent(new URL('.',ownURL).pathname).slice(0,-1)) throw Error('File paths');
50        if (Object.getPrototypeOf(import.meta) !== null || !import.meta.main || meta.main) throw Error('Metadata');
51        const resolve = import.meta.resolve;
52        if (resolve.length !== 1 || resolve.name !== 'resolve') throw Error('Function surface');
53        import.meta.url = 'http://fake.invalid/';
54        if (resolve('./not-evaluated.js') !== new URL('./not-evaluated.js',ownURL).href ||
55            resolve('alias') !== new URL('./not%20present%20%23%F0%9F%98%80.ts',ownURL).href ||
56            other.call(import.meta,'alias') !== new URL('./nested/missing.ts',ownURL).href ||
57            resolve('avril:std') !== 'avril:std') throw Error('Resolver identity or import map');
58        const marker = {};
59        try {resolve({toString() {throw marker;}}); throw Error('Swallowed exception');} catch (error) {if (error !== marker) throw error;}
60        for (const operation of [() => resolve(Symbol()), () => resolve('blocked'), () => resolve('unknown'), () => resolve('ext:core/ops'), () => new resolve('./x')]) {
61            try {operation(); throw Error('Expected TypeError');} catch (error) {if (!(error instanceof TypeError)) throw error;}
62        }
63        if (resolve(new URL('./missing.ts',ownURL)) !== new URL('./missing.ts',ownURL).href) throw Error('URL coercion');
64        console.log('metadata');
65    "#).unwrap();
66    assert_eq!(run(dir.path(), "main #😀.ts", &[]).stdout, b"metadata\n");
67}
68
69#[test]
70fn package_exports_and_live_permissions_apply_to_resolution() {
71    let dir = tempfile::tempdir().unwrap();
72    fs::create_dir(dir.path().join("library")).unwrap();
73    fs::write(
74        dir.path().join("avril.conf"),
75        "name: 'app', exports: {'.': './main.js'}, deps: ['./library']",
76    )
77    .unwrap();
78    fs::write(
79        dir.path().join("library/avril.conf"),
80        "name: 'lib', exports: {'.': './missing.ts'}",
81    )
82    .unwrap();
83    fs::write(dir.path().join("main.js"),r#"
84        import {pledge} from 'avril:std';
85        if (import.meta.resolve('app') !== import.meta.url || import.meta.resolve('lib') !== new URL('./library/missing.ts',import.meta.url).href) throw Error('Exports');
86        const blocked = new URL('./blocked.js', import.meta.url);
87        pledge({jsExec:{deny:[blocked]}});
88        try {import.meta.resolve('./blocked.js'); throw Error('Permission accepted');}
89        catch (error) {if (!(error instanceof TypeError) || !error.message.includes('PermissionDenied')) throw error;}
90        console.log('exports');
91    "#).unwrap();
92    assert_eq!(run(dir.path(), "main.js", &[]).stdout, b"exports\n");
93}
94
95#[test]
96fn http_metadata_resolves_from_the_redirected_base_without_fetching() {
97    let http = http_fixture::Http::new();
98    http.redirect("start", &http.name("nested/module.js"));
99    http.source("nested/module.js",r#"
100        if ('dirname' in import.meta || 'filename' in import.meta) throw Error('HTTP filesystem metadata');
101        console.log(import.meta.resolve('./missing.js?query#fragment'));
102    "#);
103    let dir = tempfile::tempdir().unwrap();
104    let output = run(
105        dir.path(),
106        &http.name("start"),
107        &["--cache-dir=cache".into()],
108    );
109    assert_eq!(
110        output.stdout,
111        format!("{}\n", http.name("nested/missing.js?query#fragment")).as_bytes()
112    );
113    assert!(
114        !http
115            .requests
116            .lock()
117            .unwrap()
118            .iter()
119            .any(|path| path.contains("missing"))
120    );
121}
122
123#[test]
124fn prepared_jsr_metadata_resolves_other_exports_without_acquisition() {
125    let registry = registry_fixture::Registry::new();
126    registry.package(
127        "@test/meta",
128        "1.0.0",
129        &[
130            ("mod.ts", "export {};"),
131            ("other.ts", "throw Error('Loaded other export');"),
132        ],
133        &[(".", "./mod.ts"), ("./other", "./other.ts")],
134    );
135    let dir = tempfile::tempdir().unwrap();
136    fs::write(
137        dir.path().join("avril.conf"),
138        "imports: {pkg: 'jsr:@test/meta@1', 'pkg/': 'jsr:@test/meta@1/'}",
139    )
140    .unwrap();
141    fs::write(
142        dir.path().join("main.ts"),
143        "import 'pkg'; console.log(import.meta.resolve('pkg/other'));",
144    )
145    .unwrap();
146    let options = registry.options(dir.path());
147    let output = run(
148        dir.path(),
149        "main.ts",
150        &[
151            format!("--jsr-registry={}", options.jsr_registry),
152            format!("--cache-dir={}", options.cache.display()),
153        ],
154    );
155    assert_eq!(
156        output.stdout,
157        format!("{}@test/meta/1.0.0/other.ts\n", registry.url).as_bytes()
158    );
159    assert!(
160        !registry
161            .requests
162            .lock()
163            .unwrap()
164            .iter()
165            .any(|path| path.ends_with("other.ts"))
166    );
167}
168
169#[test]
170fn resolve_matches_loaded_module_for_relative_files() {
171    let dir = tempfile::tempdir().unwrap();
172    fs::create_dir(dir.path().join("sub")).unwrap();
173    fs::write(
174        dir.path().join("dep.js"),
175        "export const metaUrl = import.meta.url;\n",
176    )
177    .unwrap();
178    fs::write(
179        dir.path().join("sub/nested.js"),
180        "export const metaUrl = import.meta.url;\n",
181    )
182    .unwrap();
183    fs::write(dir.path().join("main.js"), r#"
184        async function check(spec) {
185            const loaded = await import(spec);
186            const viaMeta = import.meta.resolve(spec);
187            if (viaMeta !== loaded.metaUrl) throw new Error(`parity ${spec}: ${viaMeta} !== ${loaded.metaUrl}`);
188        }
189        await check('./dep.js');
190        await check('./sub/nested.js');
191        console.log('relative-parity');
192    "#).unwrap();
193    assert_eq!(run(dir.path(), "main.js", &[]).stdout, b"relative-parity\n");
194}
195
196#[test]
197fn resolve_matches_loaded_module_for_local_binding_exports() {
198    let dir = tempfile::tempdir().unwrap();
199    fs::create_dir(dir.path().join("lib")).unwrap();
200    fs::write(
201        dir.path().join("avril.conf"),
202        "name: 'app', deps: ['./lib']",
203    )
204    .unwrap();
205    fs::write(
206        dir.path().join("lib/avril.conf"),
207        "name: 'mylib', exports: {'.': './index.js', './feature': './feature.js'}",
208    )
209    .unwrap();
210    fs::write(
211        dir.path().join("lib/index.js"),
212        "export const metaUrl = import.meta.url;\n",
213    )
214    .unwrap();
215    fs::write(
216        dir.path().join("lib/feature.js"),
217        "export const metaUrl = import.meta.url;\n",
218    )
219    .unwrap();
220    fs::write(dir.path().join("main.js"), r#"
221        async function check(spec) {
222            const loaded = await import(spec);
223            const viaMeta = import.meta.resolve(spec);
224            if (viaMeta !== loaded.metaUrl) throw new Error(`parity ${spec}: ${viaMeta} !== ${loaded.metaUrl}`);
225        }
226        await check('mylib');
227        await check('mylib/feature');
228        console.log('binding-parity');
229    "#).unwrap();
230    assert_eq!(run(dir.path(), "main.js", &[]).stdout, b"binding-parity\n");
231}
232
233#[test]
234fn resolve_matches_loaded_module_for_http_modules() {
235    let http = http_fixture::Http::new();
236    http.redirect("start", &http.name("nested/module.js"));
237    http.source(
238        "nested/module.js",
239        r#"
240        const viaMeta = import.meta.resolve('./sibling.js');
241        const loaded = await import('./sibling.js');
242        if (viaMeta !== loaded.metaUrl) throw new Error(`parity ./sibling.js: ${viaMeta} !== ${loaded.metaUrl}`);
243        console.log('http-parity');
244    "#,
245    );
246    http.source(
247        "nested/sibling.js",
248        "export const metaUrl = import.meta.url;\n",
249    );
250    let dir = tempfile::tempdir().unwrap();
251    let output = run(
252        dir.path(),
253        &http.name("start"),
254        &["--cache-dir=cache".into()],
255    );
256    assert_eq!(output.stdout, b"http-parity\n");
257}
258
259#[test]
260fn resolve_matches_loaded_module_for_jsr_direct_and_binding() {
261    let registry = registry_fixture::Registry::new();
262    registry.package(
263        "@test/parity",
264        "1.0.0",
265        &[
266            ("mod.ts", "export const metaUrl = import.meta.url;"),
267            ("sub.ts", "export const metaUrl = import.meta.url;"),
268        ],
269        &[(".", "./mod.ts"), ("./sub", "./sub.ts")],
270    );
271    let dir = tempfile::tempdir().unwrap();
272    fs::write(
273        dir.path().join("avril.conf"),
274        "deps: ['jsr:@test/parity@^1']",
275    )
276    .unwrap();
277    fs::write(dir.path().join("main.ts"), r#"
278        async function check(spec) {
279            const loaded = await import(spec);
280            const viaMeta = import.meta.resolve(spec);
281            if (viaMeta !== loaded.metaUrl) throw new Error(`parity ${spec}: ${viaMeta} !== ${loaded.metaUrl}`);
282        }
283        await check('jsr:@test/parity@^1');
284        await check('jsr:@test/parity@^1/sub');
285        await check('@test/parity');
286        await check('@test/parity/sub');
287        console.log('jsr-parity');
288    "#).unwrap();
289    let options = registry.options(dir.path());
290    let output = run(
291        dir.path(),
292        "main.ts",
293        &[
294            format!("--jsr-registry={}", options.jsr_registry),
295            format!("--cache-dir={}", options.cache.display()),
296        ],
297    );
298    assert_eq!(output.stdout, b"jsr-parity\n");
299}
300
301#[test]
302fn resolve_matches_loaded_module_for_npm_direct_binding_and_owned_referrer() {
303    let registry = registry_fixture::Registry::new();
304    registry.publish_npm(
305        "metadep",
306        "1.0.0",
307        &[
308            (
309                "package.json",
310                r#"{"name": "metadep", "version": "1.0.0", "type": "module", "exports": {".": "./index.js"}}"#,
311            ),
312            ("index.js", "export const metaUrl = import.meta.url;\n"),
313        ],
314    );
315    registry.publish_npm(
316        "metaparity",
317        "1.0.0",
318        &[
319            (
320                "package.json",
321                r#"{"name": "metaparity", "version": "1.0.0", "type": "module", "exports": {".": "./index.js", "./sibling": "./sibling.js"}, "dependencies": {"metadep": "^1.0.0"}}"#,
322            ),
323            (
324                "index.js",
325                r#"export const metaUrl = import.meta.url;
326                export async function checkOwned() {
327                    const viaSibling = import.meta.resolve('./sibling.js');
328                    const sibling = await import('./sibling.js');
329                    if (viaSibling !== sibling.metaUrl) throw new Error(`owned sibling parity: ${viaSibling} !== ${sibling.metaUrl}`);
330                    const viaDep = import.meta.resolve('metadep');
331                    const dep = await import('metadep');
332                    if (viaDep !== dep.metaUrl) throw new Error(`owned dep parity: ${viaDep} !== ${dep.metaUrl}`);
333                }
334                "#,
335            ),
336            ("sibling.js", "export const metaUrl = import.meta.url;\n"),
337        ],
338    );
339    let dir = tempfile::tempdir().unwrap();
340    fs::write(dir.path().join("avril.conf"), "deps: ['npm:metaparity@^1']").unwrap();
341    fs::write(dir.path().join("main.js"), r#"
342        async function check(spec) {
343            const loaded = await import(spec);
344            const viaMeta = import.meta.resolve(spec);
345            if (!viaMeta.startsWith('file://')) throw new Error(`npm identity is not a file URL: ${spec} -> ${viaMeta}`);
346            if (viaMeta !== loaded.metaUrl) throw new Error(`parity ${spec}: ${viaMeta} !== ${loaded.metaUrl}`);
347        }
348        await check('npm:metaparity@^1');
349        await check('npm:metaparity@^1/sibling');
350        await check('metaparity');
351        await check('metaparity/sibling');
352        await (await import('npm:metaparity@^1')).checkOwned();
353        console.log('npm-parity');
354    "#).unwrap();
355    let options = registry.options(dir.path());
356    let output = run(
357        dir.path(),
358        "main.js",
359        &[
360            format!("--npm-registry={}", options.npm_registry),
361            format!("--cache-dir={}", options.cache.display()),
362        ],
363    );
364    assert_eq!(output.stdout, b"npm-parity\n");
365}
366
367#[test]
368fn resolve_matches_loaded_avril_builtin() {
369    let dir = tempfile::tempdir().unwrap();
370    fs::write(
371        dir.path().join("main.js"),
372        r#"
373        if (import.meta.resolve('avril:std') !== 'avril:std') throw new Error('builtin identity');
374        const std = await import('avril:std');
375        if (typeof std.pledge !== 'function') throw new Error('builtin loads');
376        console.log('builtin-parity');
377    "#,
378    )
379    .unwrap();
380    assert_eq!(run(dir.path(), "main.js", &[]).stdout, b"builtin-parity\n");
381}
382
383#[test]
384fn resolve_and_import_agree_on_ext_and_node_rejections() {
385    let dir = tempfile::tempdir().unwrap();
386    fs::write(dir.path().join("main.js"), r#"
387        for (const spec of ['node:fs', 'ext:core/ops']) {
388            let metaError = null;
389            try { import.meta.resolve(spec); } catch (error) { metaError = error; }
390            if (!(metaError instanceof TypeError)) throw new Error(`resolve should reject ${spec}`);
391            let importError = null;
392            try { await import(spec); } catch (error) { importError = error; }
393            if (!importError) throw new Error(`import should reject ${spec}`);
394            const want = spec.startsWith('node:') ? 'not supported' : 'private';
395            if (!String(metaError.message).includes(want) || !String(importError.message ?? importError).includes(want)) {
396                throw new Error(`message class for ${spec}: ${metaError.message} / ${importError.message ?? importError}`);
397            }
398        }
399        console.log('rejections');
400    "#).unwrap();
401    assert_eq!(run(dir.path(), "main.js", &[]).stdout, b"rejections\n");
402}
403
404#[test]
405fn workers_get_their_own_file_metadata_and_bound_resolver() {
406    let dir = tempfile::tempdir().unwrap();
407    fs::write(dir.path().join("worker.js"),r#"
408        postMessage([import.meta.filename, import.meta.dirname, import.meta.resolve('./missing.js'), import.meta.main]); close();
409    "#).unwrap();
410    fs::write(dir.path().join("main.js"),r#"
411        const worker = new Worker(new URL('./worker.js',import.meta.url),{type:'module'});
412        const data = await new Promise((resolve,reject) => {worker.onmessage = e => resolve(e.data); worker.onerror = reject;});
413        if (data[0] !== import.meta.dirname + '/worker.js' || data[1] !== import.meta.dirname || data[2] !== import.meta.resolve('./missing.js') || data[3] !== true) throw Error('Worker metadata');
414        console.log('worker');
415    "#).unwrap();
416    assert_eq!(run(dir.path(), "main.js", &[]).stdout, b"worker\n");
417}