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
32.1 KiB944 linesraw
1#[path = "common/http.rs"]
2mod http_fixture;
3#[path = "common/registry.rs"]
4mod registry_fixture;
5
6use http_fixture::Http;
7
8fn cli(directory: &std::path::Path, args: &[&str]) -> std::process::Output {
9    std::process::Command::new(env!("CARGO_BIN_EXE_avril"))
10        .current_dir(directory)
11        .arg(args[0])
12        .arg("--cache-dir=cache")
13        .args(&args[1..])
14        .output()
15        .unwrap()
16}
17
18fn success(output: std::process::Output) -> String {
19    assert!(
20        output.status.success(),
21        "{}",
22        String::from_utf8_lossy(&output.stderr)
23    );
24    String::from_utf8(output.stdout).unwrap()
25}
26
27fn failure(output: std::process::Output, message: &str) {
28    assert!(!output.status.success());
29    assert!(
30        String::from_utf8_lossy(&output.stderr).contains(message),
31        "{}",
32        String::from_utf8_lossy(&output.stderr)
33    );
34}
35
36fn write(directory: &std::path::Path, file: &str, source: impl AsRef<[u8]>) {
37    std::fs::write(directory.join(file), source).unwrap();
38}
39
40#[test]
41fn packages_have_private_maps_dependencies_self_imports_and_cyclic_headers() {
42    let http = Http::new();
43    let dir = tempfile::tempdir().unwrap();
44    http.source("math/avril.conf", "name: '@acme/math', version: '1.2.3', description: 'Math', exports: {'.': './mod.ts', './vector': './src/vector.ts'}, deps: ['../numeric/', {from: '../unused/', name: 'unused'}], imports: {helper: './helper.ts'}, scopes: {'./src/': {helper: './scoped.ts'}}");
45    http.source(
46        "numeric/avril.conf",
47        "name: 'numeric', exports: './mod.ts', deps: ['../math/']",
48    );
49    http.source("numeric/mod.ts", "import {vector} from '@acme/math/vector'; export const number = 40; export const read = () => vector;");
50    http.source("math/mod.ts", "import {number, read} from 'numeric'; import {helper} from 'helper'; export {vector} from '@acme/math/vector'; export const value: number = number + helper; export const lazy = () => import('numeric'); export {read};");
51    http.source(
52        "math/src/vector.ts",
53        "import {helper} from 'helper'; export const vector = helper;",
54    );
55    http.source("math/helper.ts", "export const helper = 2;");
56    http.source("math/scoped.ts", "export const helper = 7;");
57    write(
58        dir.path(),
59        "avril.conf",
60        format!(
61            "deps: ['{}', {{from: '{}', name: 'renamed'}}], imports: {{helper: './wrong.ts'}}",
62            http.name("math/"),
63            http.name("math/")
64        ),
65    );
66    write(
67        dir.path(),
68        "wrong.ts",
69        "throw new Error('consumer alias leaked');",
70    );
71    write(
72        dir.path(),
73        "main.ts",
74        format!(
75            "import * as a from '@acme/math'; import * as b from 'renamed'; import * as c from '{}'; import {{vector}} from 'renamed/vector'; const [x,y] = await Promise.all([a.lazy(), a.lazy()]); console.log(a === b, a === c, a.value, vector, a.read(), x === y, x.number);",
76            http.name("math/mod.ts")
77        ),
78    );
79    let expected = "true true 42 7 7 true 40\n";
80    assert_eq!(success(cli(dir.path(), &["run", "main.ts"])), expected);
81    let requests = http.requests.lock().unwrap().clone();
82    assert!(!requests.iter().any(|url| url.contains("unused")));
83    assert_eq!(
84        requests
85            .iter()
86            .filter(|url| url.ends_with("avril.conf"))
87            .count(),
88        2
89    );
90    assert_eq!(
91        success(cli(
92            dir.path(),
93            &["run", "--offline", "--frozen", "main.ts"]
94        )),
95        expected
96    );
97    assert_eq!(*http.requests.lock().unwrap(), requests);
98    let info: serde_json::Value =
99        serde_json::from_str(&success(cli(dir.path(), &["info", "--offline", "main.ts"]))).unwrap();
100    let package = &info["modules"][http.name("math/mod.ts")]["package"];
101    assert_eq!(package["name"], "@acme/math");
102    assert_eq!(package["config"], http.name("math/avril.conf"));
103    assert_eq!(package["dependencies"]["numeric"], http.name("numeric/"));
104    assert_eq!(
105        package["exports"]["./vector"],
106        http.name("math/src/vector.ts")
107    );
108    write(dir.path(), "main.ts", "import 'numeric';");
109    failure(
110        cli(dir.path(), &["run", "--offline", "main.ts"]),
111        "Unsupported module specifier",
112    );
113}
114
115#[test]
116fn configs_and_sources_are_pinned_updated_and_vendored_together() {
117    let http = Http::new();
118    let dir = tempfile::tempdir().unwrap();
119    http.source("pkg/avril.conf", "name: 'pkg', exports: './v1.ts'");
120    http.source("pkg/v1.ts", "export const value = 1;");
121    http.source("pkg/v2.ts", "export const value = 2;");
122    write(
123        dir.path(),
124        "avril.conf",
125        format!("deps: ['{}']", http.name("pkg/")),
126    );
127    write(
128        dir.path(),
129        "main.ts",
130        "import {value} from 'pkg'; console.log(value);",
131    );
132    write(
133        dir.path(),
134        "avril.lock",
135        "version: 1\njsrRegistry: \"https://jsr.io/\"\nspecifiers: {}\npackages: {}\nremote: {}\n",
136    );
137    failure(
138        cli(dir.path(), &["cache", "--frozen", "main.ts"]),
139        "missing remote source",
140    );
141    assert_eq!(success(cli(dir.path(), &["run", "main.ts"])), "1\n");
142    let lock = registry_fixture::read_lock(dir.path());
143    assert!(lock["remote"][http.name("pkg/avril.conf")]["checksum"].is_string());
144    http.source("pkg/avril.conf", "name: 'pkg', exports: './v2.ts'");
145    assert_eq!(success(cli(dir.path(), &["run", "main.ts"])), "1\n");
146    std::fs::remove_dir_all(dir.path().join("cache/http/blobs")).unwrap();
147    failure(cli(dir.path(), &["run", "main.ts"]), "integrity mismatch");
148    success(cli(dir.path(), &["update", "--prune", "main.ts"]));
149    assert_eq!(
150        success(cli(
151            dir.path(),
152            &["run", "--offline", "--frozen", "main.ts"]
153        )),
154        "2\n"
155    );
156    success(cli(dir.path(), &["vendor", "main.ts"]));
157    std::fs::remove_dir_all(dir.path().join("cache")).unwrap();
158    let requests = http.requests.lock().unwrap().len();
159    assert_eq!(
160        success(cli(
161            dir.path(),
162            &["run", "--offline", "--frozen", "--vendor=vendor", "main.ts"]
163        )),
164        "2\n"
165    );
166    assert_eq!(http.requests.lock().unwrap().len(), requests);
167    let lock = registry_fixture::read_lock(dir.path());
168    assert!(lock["remote"].get(http.name("pkg/v1.ts")).is_none());
169    assert!(lock["remote"][http.name("pkg/avril.conf")].is_object());
170}
171
172#[test]
173fn redirected_configs_resolve_exports_and_private_dependencies_at_the_final_url() {
174    let http = Http::new();
175    let target = Http::new();
176    let dir = tempfile::tempdir().unwrap();
177    http.redirect("pkg/avril.conf", &target.name("nested/pkg/avril.conf"));
178    target.source(
179        "nested/pkg/avril.conf",
180        "name: 'pkg', exports: './mod.ts', deps: ['../dep/']",
181    );
182    target.source("nested/dep/avril.conf", "name: 'dep', exports: './mod.ts'");
183    target.source("nested/dep/mod.ts", "export const value = 9;");
184    target.source("nested/pkg/mod.ts", "export {value} from 'dep';");
185    write(
186        dir.path(),
187        "avril.conf",
188        format!("deps: ['{}']", http.name("pkg/")),
189    );
190    write(
191        dir.path(),
192        "main.ts",
193        "import {value} from 'pkg'; console.log(value);",
194    );
195    let allow_local = "--allow-js-exec=.";
196    let allow_config = format!("--allow-js-exec={}", http.name("pkg/avril.conf"));
197    let allow_target = format!("--allow-js-exec={}", target.name("nested/"));
198    let deny_destination_config =
199        format!("--deny-js-exec={}", target.name("nested/pkg/avril.conf"));
200    let args = [
201        "run",
202        "-0",
203        allow_local,
204        &allow_config,
205        &allow_target,
206        &deny_destination_config,
207        "main.ts",
208    ];
209    assert_eq!(success(cli(dir.path(), &args)), "9\n");
210    let mut offline = args.to_vec();
211    offline.insert(1, "--offline");
212    offline.insert(1, "--frozen");
213    assert_eq!(success(cli(dir.path(), &offline)), "9\n");
214}
215
216#[test]
217fn configs_and_exports_require_execution_authority_even_on_cache_hits() {
218    let http = Http::new();
219    let dir = tempfile::tempdir().unwrap();
220    http.source("pkg/avril.conf", "name: 'pkg', exports: './mod.ts'");
221    http.source("pkg/mod.ts", "console.log('loaded');");
222    write(
223        dir.path(),
224        "avril.conf",
225        format!("deps: [{{from: '{}', name: 'pkg'}}]", http.name("pkg/")),
226    );
227    write(dir.path(), "main.ts", "import 'pkg';");
228    let allow = format!("--allow-js-exec={}", http.name("pkg/"));
229    success(cli(
230        dir.path(),
231        &["run", "-0", "--allow-js-exec=.", &allow, "main.ts"],
232    ));
233    for denied in ["pkg/avril.conf", "pkg/mod.ts"] {
234        let deny = format!("--deny-js-exec={}", http.name(denied));
235        let requests = http.requests.lock().unwrap().len();
236        failure(
237            cli(
238                dir.path(),
239                &[
240                    "run",
241                    "-0",
242                    "--allow-js-exec=.",
243                    &allow,
244                    &deny,
245                    "--offline",
246                    "main.ts",
247                ],
248            ),
249            "PermissionDenied",
250        );
251        assert_eq!(http.requests.lock().unwrap().len(), requests);
252    }
253}
254
255#[test]
256fn dependency_headers_bind_names_and_interfaces_without_fallbacks() {
257    let http = Http::new();
258    let dir = tempfile::tempdir().unwrap();
259    write(dir.path(), "main.ts", "import 'pkg/private';");
260    http.source(
261        "pkg/avril.conf",
262        "name: 'pkg', exports: {'./other': './other.ts'}",
263    );
264    http.source("pkg/private.ts", "throw new Error('private fallback');");
265    write(
266        dir.path(),
267        "avril.conf",
268        format!("deps: ['{}']", http.name("pkg/")),
269    );
270    failure(cli(dir.path(), &["run", "main.ts"]), "no export");
271    assert!(
272        !http
273            .requests
274            .lock()
275            .unwrap()
276            .iter()
277            .any(|url| url.ends_with("private.ts"))
278    );
279    write(
280        dir.path(),
281        "avril.conf",
282        format!(
283            "deps: ['{}', {{from: '{}', name: 'pkg'}}]",
284            http.name("pkg/"),
285            http.name("another/")
286        ),
287    );
288    failure(
289        cli(dir.path(), &["run", "main.ts"]),
290        "Duplicate dependency binding",
291    );
292    http.source("anonymous/avril.conf", "exports: './mod.ts'");
293    write(
294        dir.path(),
295        "avril.conf",
296        format!("deps: ['{}']", http.name("anonymous/")),
297    );
298    failure(cli(dir.path(), &["run", "main.ts"]), "declares no name");
299    http.source("anonymous/mod.ts", "console.log('anonymous');");
300    write(
301        dir.path(),
302        "avril.conf",
303        format!(
304            "deps: [{{from: '{}', name: 'pkg'}}]",
305            http.name("anonymous/")
306        ),
307    );
308    write(dir.path(), "main.ts", "import 'pkg';");
309    assert_eq!(success(cli(dir.path(), &["run", "main.ts"])), "anonymous\n");
310}
311
312#[test]
313fn cached_package_bindings_obey_live_config_revocation() {
314    let http = Http::new();
315    let dir = tempfile::tempdir().unwrap();
316    http.source("pkg/avril.conf", "name: 'pkg', exports: './mod.ts'");
317    http.source("pkg/mod.ts", "export const value = 42;");
318    write(
319        dir.path(),
320        "avril.conf",
321        format!("deps: ['{}']", http.name("pkg/")),
322    );
323    write(
324        dir.path(),
325        "main.ts",
326        format!(
327            "import {{pledge}} from 'avril:std'; const a = await import('pkg'); console.log((await import('pkg')) === a); pledge({{jsExec: {{deny: ['{}']}}}}); try {{await import('pkg'); console.log('allowed');}} catch (e) {{console.log(e.message.includes('PermissionDenied'));}}",
328            http.name("pkg/avril.conf")
329        ),
330    );
331    assert_eq!(
332        success(cli(dir.path(), &["run", "main.ts"])),
333        "true\ntrue\n"
334    );
335}
336
337#[test]
338fn declared_nested_packages_own_exported_files() {
339    let http = Http::new();
340    let dir = tempfile::tempdir().unwrap();
341    http.source(
342        "pkg/avril.conf",
343        "name: 'outer', exports: './child/mod.ts', deps: [{from: './child/', name: 'nested'}], imports: {helper: './wrong.ts'}",
344    );
345    http.source(
346        "pkg/child/avril.conf",
347        "name: 'child', exports: './mod.ts', imports: {helper: './helper.ts'}",
348    );
349    http.source("pkg/child/mod.ts", "export {value} from 'helper';");
350    http.source("pkg/child/helper.ts", "export const value = 7;");
351    http.source("pkg/wrong.ts", "throw new Error('parent map leaked');");
352    write(
353        dir.path(),
354        "avril.conf",
355        format!(
356            "deps: ['{}', '{}']",
357            http.name("pkg/"),
358            http.name("pkg/child/")
359        ),
360    );
361    write(
362        dir.path(),
363        "main.ts",
364        "import * as a from 'outer'; import * as b from 'child'; console.log(a === b, a.value);",
365    );
366    assert_eq!(success(cli(dir.path(), &["run", "main.ts"])), "true 7\n");
367}
368
369#[test]
370fn remote_packages_can_bind_private_jsr_dependencies() {
371    let http = Http::new();
372    let registry = registry_fixture::Registry::new();
373    let dir = tempfile::tempdir().unwrap();
374    registry.package(
375        "@test/numeric",
376        "1.0.0",
377        &[("mod.ts", "export const number: number = 42;")],
378        &[(".", "./mod.ts")],
379    );
380    http.source(
381        "pkg/avril.conf",
382        "name: 'pkg', exports: './mod.ts', deps: [{from: 'jsr:@test/numeric@1', name: 'numeric'}]",
383    );
384    http.source("pkg/mod.ts", "export {number} from 'numeric';");
385    write(
386        dir.path(),
387        "avril.conf",
388        format!(
389            "deps: ['{}'], imports: {{numeric: './wrong.ts'}}",
390            http.name("pkg/")
391        ),
392    );
393    write(
394        dir.path(),
395        "wrong.ts",
396        "throw new Error('consumer alias leaked');",
397    );
398    write(
399        dir.path(),
400        "main.ts",
401        "import {number} from 'pkg'; console.log(number);",
402    );
403    let registry_arg = format!(
404        "--jsr-registry={}",
405        registry.options(dir.path()).jsr_registry
406    );
407    assert_eq!(
408        success(cli(dir.path(), &["run", &registry_arg, "main.ts"])),
409        "42\n"
410    );
411    let requests = registry.requests.lock().unwrap().len();
412    assert_eq!(
413        success(cli(
414            dir.path(),
415            &["run", &registry_arg, "--offline", "--frozen", "main.ts"]
416        )),
417        "42\n"
418    );
419    assert_eq!(registry.requests.lock().unwrap().len(), requests);
420}
421
422#[test]
423fn nested_ownership_and_metadata_are_independent_of_graph_traversal_order() {
424    let http = Http::new();
425    let dir = tempfile::tempdir().unwrap();
426    http.source("pkg/avril.conf", "name: 'outer', exports: './child/mod.ts', deps: [{from: './child/', name: 'nested'}], imports: {helper: './wrong.ts'}");
427    http.source(
428        "pkg/child/avril.conf",
429        "name: 'child', exports: './mod.ts', imports: {helper: './helper.ts'}",
430    );
431    http.source("pkg/child/mod.ts", "export {value} from 'helper';");
432    http.source("pkg/child/helper.ts", "export const value = 7;");
433    http.source("pkg/wrong.ts", "throw new Error('parent map leaked');");
434    write(
435        dir.path(),
436        "avril.conf",
437        format!(
438            "deps: ['{}', '{}']",
439            http.name("pkg/"),
440            http.name("pkg/child/")
441        ),
442    );
443    write(
444        dir.path(),
445        "x.ts",
446        "import * as a from 'outer'; export {a};",
447    );
448    write(
449        dir.path(),
450        "y.ts",
451        "import * as b from 'child'; export {b};",
452    );
453    for imports in [
454        "import {a} from './x.ts'; import {b} from './y.ts';",
455        "import {b} from './y.ts'; import {a} from './x.ts';",
456    ] {
457        write(
458            dir.path(),
459            "main.ts",
460            format!("{imports} console.log(a === b, a.value);"),
461        );
462        assert_eq!(success(cli(dir.path(), &["run", "main.ts"])), "true 7\n");
463        let info: serde_json::Value =
464            serde_json::from_str(&success(cli(dir.path(), &["info", "--offline", "main.ts"])))
465                .unwrap();
466        let module = &info["modules"][http.name("pkg/child/mod.ts")];
467        assert_eq!(module["package"]["name"], "child");
468        assert_eq!(
469            module["imports"]["helper"],
470            http.name("pkg/child/helper.ts")
471        );
472    }
473}
474
475#[test]
476fn conflicting_owners_fail_in_both_graph_orders_instead_of_rebinding_modules() {
477    let http = Http::new();
478    let dir = tempfile::tempdir().unwrap();
479    http.source(
480        "pkg/avril.conf",
481        "name: 'outer', exports: './child/mod.ts', imports: {helper: './helper.ts'}",
482    );
483    http.source(
484        "pkg/child/avril.conf",
485        "name: 'child', exports: './mod.ts', imports: {helper: './helper.ts'}",
486    );
487    http.source("pkg/child/mod.ts", "export {value} from 'helper';");
488    http.source("pkg/helper.ts", "export const value = 1;");
489    http.source("pkg/child/helper.ts", "export const value = 7;");
490    write(
491        dir.path(),
492        "avril.conf",
493        format!(
494            "deps: ['{}', '{}']",
495            http.name("pkg/"),
496            http.name("pkg/child/")
497        ),
498    );
499    write(dir.path(), "x.ts", "import 'outer';");
500    write(dir.path(), "y.ts", "import 'child';");
501    for imports in [
502        "import './x.ts'; import './y.ts';",
503        "import './y.ts'; import './x.ts';",
504    ] {
505        write(dir.path(), "main.ts", imports);
506        failure(
507            cli(dir.path(), &["run", "main.ts"]),
508            "Conflicting remote package ownership",
509        );
510    }
511}
512
513#[test]
514fn selecting_a_private_dependency_does_not_change_existing_modules_maps() {
515    let http = Http::new();
516    let dir = tempfile::tempdir().unwrap();
517    http.source(
518        "pkg/avril.conf",
519        "name: 'a', exports: './child/util.ts', imports: {helper: './helper.ts'}",
520    );
521    http.source("pkg/helper.ts", "export const value = 1;");
522    http.source(
523        "pkg/child/util.ts",
524        "export const lazy = async () => (await import('helper')).value;",
525    );
526    http.source(
527        "pkg/child/avril.conf",
528        "name: 'child', exports: './other.ts', imports: {helper: './helper.ts'}",
529    );
530    http.source("pkg/child/helper.ts", "export const value = 99;");
531    http.source("pkg/child/other.ts", "export const value = 2;");
532    http.source(
533        "b/avril.conf",
534        format!(
535            "name: 'b', exports: './mod.ts', deps: [{{from: '{}', name: 'private'}}]",
536            http.name("pkg/child/")
537        )
538        .as_str(),
539    );
540    http.source("b/mod.ts", "export {value} from 'private';");
541    write(
542        dir.path(),
543        "avril.conf",
544        format!("deps: ['{}', '{}']", http.name("pkg/"), http.name("b/")),
545    );
546    write(
547        dir.path(),
548        "main.ts",
549        "const a = await import('a'); const before = await a.lazy(); const b = await import('b'); console.log(before, await a.lazy(), b.value);",
550    );
551    assert_eq!(success(cli(dir.path(), &["run", "main.ts"])), "1 1 2\n");
552}
553
554#[test]
555fn a_plain_module_stays_plain_after_a_conflicting_package_import() {
556    let http = Http::new();
557    let dir = tempfile::tempdir().unwrap();
558    http.source(
559        "pkg/avril.conf",
560        "name: 'pkg', exports: './util.ts', imports: {helper: './helper.ts'}",
561    );
562    http.source("pkg/util.ts", "export const lazy = () => import('helper');");
563    http.source("pkg/helper.ts", "export const value = 7;");
564    std::fs::create_dir(dir.path().join("plain")).unwrap();
565    write(
566        dir.path(),
567        "avril.conf",
568        format!("deps: ['{}']", http.name("pkg/")),
569    );
570    write(dir.path(), "plain/avril.conf", "");
571    write(
572        dir.path(),
573        "plain/mod.ts",
574        format!("export {{lazy}} from '{}';", http.name("pkg/util.ts")),
575    );
576    write(
577        dir.path(),
578        "main.ts",
579        "import {lazy} from './plain/mod.ts'; for (let i = 0; i < 2; i++) {try {await lazy();} catch(e) {console.log(e.message.includes('Unsupported module specifier'));} if (i === 0) {try {await import('pkg');} catch(e) {console.log(e.message.includes('Conflicting remote package ownership'));}}}",
580    );
581    assert_eq!(
582        success(cli(dir.path(), &["run", "main.ts"])),
583        "true\ntrue\ntrue\n"
584    );
585}
586
587#[test]
588fn registry_prefixed_configs_are_rejected_without_fetching_or_changing_the_lock() {
589    let http = Http::new();
590    let dir = tempfile::tempdir().unwrap();
591    let registry = http.name("registry/");
592    let args_registry = format!("--jsr-registry={registry}");
593    let lock = format!(
594        "version: 1\njsrRegistry: \"{registry}\"\nspecifiers: {{}}\npackages: {{}}\nremote: {{}}\n"
595    );
596    write(dir.path(), "avril.lock", &lock);
597    http.source(
598        "registry/pkg/avril.conf",
599        "name: 'pkg', exports: './mod.ts'",
600    );
601    for dependency in [
602        format!("'{}'", http.name("registry/pkg/")),
603        format!("{{from: '{}', name: 'pkg'}}", http.name("registry/pkg/")),
604    ] {
605        write(dir.path(), "avril.conf", format!("deps: [{dependency}]"));
606        write(dir.path(), "main.ts", "import 'pkg';");
607        failure(
608            cli(dir.path(), &["run", &args_registry, "main.ts"]),
609            "configured JSR registry",
610        );
611        assert_eq!(
612            std::fs::read_to_string(dir.path().join("avril.lock")).unwrap(),
613            lock
614        );
615        assert!(http.requests.lock().unwrap().is_empty());
616    }
617}
618
619#[test]
620fn a_failed_config_fetch_does_not_snapshot_plain_ownership() {
621    let http = Http::new();
622    let dir = tempfile::tempdir().unwrap();
623    http.source("pkg/mod.ts", "export const value = 7;");
624    let routes = http.routes.clone();
625    *http.hook.lock().unwrap() = Some(Box::new(move || {
626        routes.lock().unwrap().insert(
627            "/pkg/avril.conf".into(),
628            http_fixture::Response {
629                status: 200,
630                headers: "Content-Type: text/plain\r\n".into(),
631                body: b"name: 'pkg', exports: './mod.ts'".to_vec(),
632            },
633        );
634    }));
635    write(
636        dir.path(),
637        "avril.conf",
638        format!("deps: [{{from: '{}', name: 'pkg'}}]", http.name("pkg/")),
639    );
640    write(
641        dir.path(),
642        "main.ts",
643        format!(
644            "try {{await import('pkg');}} catch(e) {{console.log(e.message.includes('404'));}} console.log((await import('{}')).value);",
645            http.name("pkg/mod.ts")
646        ),
647    );
648    assert_eq!(success(cli(dir.path(), &["run", "main.ts"])), "true\n7\n");
649    assert_eq!(
650        http.requests
651            .lock()
652            .unwrap()
653            .iter()
654            .filter(|url| url.ends_with("avril.conf"))
655            .count(),
656        2
657    );
658}
659
660#[test]
661fn redirected_aliases_share_modules_only_when_config_pins_agree() {
662    for changed in [false, true] {
663        let http = Http::new();
664        let target = Http::new();
665        let dir = tempfile::tempdir().unwrap();
666        http.redirect("a/avril.conf", &target.name("pkg/avril.conf"));
667        http.redirect("b/avril.conf", &target.name("pkg/avril.conf"));
668        target.source("pkg/avril.conf", "exports: './mod.ts'");
669        target.source("pkg/mod.ts", "export const value = 7;");
670        if changed {
671            let routes = target.routes.clone();
672            *target.hook.lock().unwrap() = Some(Box::new(move || {
673                routes.lock().unwrap().insert(
674                    "/pkg/avril.conf".into(),
675                    http_fixture::Response {
676                        status: 200,
677                        headers: "Content-Type: text/plain\r\n".into(),
678                        body: b"exports: './mod.ts', description: 'changed'".to_vec(),
679                    },
680                );
681            }));
682        }
683        write(
684            dir.path(),
685            "avril.conf",
686            format!(
687                "deps: [{{from: '{}', name: 'a'}}, {{from: '{}', name: 'b'}}]",
688                http.name("a/"),
689                http.name("b/")
690            ),
691        );
692        write(
693            dir.path(),
694            "main.ts",
695            "import * as a from 'a'; import * as b from 'b'; console.log(a === b, a.value);",
696        );
697        if changed {
698            failure(
699                cli(dir.path(), &["run", "main.ts"]),
700                "Conflicting remote package ownership",
701            );
702        } else {
703            assert_eq!(success(cli(dir.path(), &["run", "main.ts"])), "true 7\n");
704            assert_eq!(
705                success(cli(
706                    dir.path(),
707                    &["run", "--offline", "--frozen", "main.ts"]
708                )),
709                "true 7\n"
710            );
711        }
712    }
713}
714
715#[test]
716fn equivalent_aliases_use_the_canonical_module_region_in_both_orders() {
717    for root_alias in [false, true] {
718        let http = Http::new();
719        let dir = tempfile::tempdir().unwrap();
720        let (a, b, outside) = if root_alias {
721            http.redirect("avril.conf", &http.name("f/avril.conf"));
722            (http.url.to_string(), http.name("f/"), "other.ts")
723        } else {
724            http.redirect("a/avril.conf", &http.name("f/avril.conf"));
725            http.redirect("b/avril.conf", &http.name("f/avril.conf"));
726            (http.name("a/"), http.name("b/"), "b/extra.ts")
727        };
728        http.source(
729            "f/avril.conf",
730            "name: 'shared', exports: './mod.ts', imports: {helper: './helper.ts'}",
731        );
732        http.source(
733            "f/mod.ts",
734            format!(
735                "export const lazy = () => import('{}');",
736                http.name(outside)
737            )
738            .as_str(),
739        );
740        http.source(outside, "export {value} from 'helper';");
741        http.source("f/helper.ts", "export const value = 7;");
742        write(
743            dir.path(),
744            "avril.conf",
745            format!("deps: [{{from: '{a}', name: 'a'}}, {{from: '{b}', name: 'b'}}]"),
746        );
747        for imports in [
748            "import * as a from 'a'; import * as b from 'b';",
749            "import * as b from 'b'; import * as a from 'a';",
750        ] {
751            write(
752                dir.path(),
753                "main.ts",
754                format!(
755                    "{imports} try {{await a.lazy();}} catch(e) {{console.log(a === b, e.message.includes('Unsupported module specifier'));}}"
756                ),
757            );
758            assert_eq!(success(cli(dir.path(), &["run", "main.ts"])), "true true\n");
759            let info: serde_json::Value =
760                serde_json::from_str(&success(cli(dir.path(), &["info", "main.ts"]))).unwrap();
761            assert_eq!(
762                info["modules"][http.name("f/mod.ts")]["package"]["config"],
763                http.name("f/avril.conf")
764            );
765        }
766    }
767}
768
769#[test]
770fn revoking_any_claimed_alias_blocks_all_aliases_in_both_orders() {
771    let http = Http::new();
772    let dir = tempfile::tempdir().unwrap();
773    http.redirect("a/avril.conf", &http.name("f/avril.conf"));
774    http.redirect("b/avril.conf", &http.name("f/avril.conf"));
775    http.source("f/avril.conf", "name: 'shared', exports: './mod.ts'");
776    http.source("f/mod.ts", "export const lazy = () => import('shared');");
777    write(
778        dir.path(),
779        "avril.conf",
780        format!(
781            "deps: [{{from: '{}', name: 'a'}}, {{from: '{}', name: 'b'}}]",
782            http.name("a/"),
783            http.name("b/")
784        ),
785    );
786    for imports in [
787        "import * as a from 'a'; import * as b from 'b';",
788        "import * as b from 'b'; import * as a from 'a';",
789    ] {
790        write(
791            dir.path(),
792            "main.ts",
793            format!(
794                "{imports} import {{pledge}} from 'avril:std'; console.log(a === b); pledge({{jsExec: {{deny: ['{}']}}}}); try {{await import('b');}} catch(e) {{console.log(e.message.includes('PermissionDenied'));}} try {{await a.lazy();}} catch(e) {{console.log(e.message.includes('PermissionDenied'));}}",
795                http.name("a/avril.conf")
796            ),
797        );
798        assert_eq!(
799            success(cli(dir.path(), &["run", "main.ts"])),
800            "true\ntrue\ntrue\n"
801        );
802    }
803}
804
805#[test]
806fn rejected_registry_redirects_are_not_pinned_after_a_later_successful_graph() {
807    let http = Http::new();
808    let dir = tempfile::tempdir().unwrap();
809    let registry = http.name("registry/");
810    let registry_arg = format!("--jsr-registry={registry}");
811    let lock = format!(
812        "version: 1\njsrRegistry: \"{registry}\"\nspecifiers: {{}}\npackages: {{}}\nremote: {{}}\n"
813    );
814    write(dir.path(), "avril.lock", &lock);
815    http.redirect("alias/avril.conf", &http.name("registry/pkg/avril.conf"));
816    http.source(
817        "registry/pkg/avril.conf",
818        "name: 'pkg', exports: './mod.ts'",
819    );
820    write(
821        dir.path(),
822        "avril.conf",
823        format!("deps: [{{from: '{}', name: 'pkg'}}]", http.name("alias/")),
824    );
825    write(dir.path(), "other.ts", "console.log('done');");
826    write(
827        dir.path(),
828        "main.ts",
829        "try {await import('pkg');} catch(e) {console.log(e.message.includes('configured JSR registry'));} await import('./other.ts');",
830    );
831    assert_eq!(
832        success(cli(dir.path(), &["run", &registry_arg, "main.ts"])),
833        "true\ndone\n"
834    );
835    assert_eq!(
836        std::fs::read_to_string(dir.path().join("avril.lock")).unwrap(),
837        lock
838    );
839}
840
841#[test]
842fn nested_config_redirect_cycles_are_reported_instead_of_looping() {
843    let http = Http::new();
844    let dir = tempfile::tempdir().unwrap();
845    http.source(
846        "pkg/avril.conf",
847        "name: 'pkg', exports: './child/mod.ts', deps: [{from: './child/', name: 'nested'}]",
848    );
849    http.redirect("pkg/child/avril.conf", &http.name("pkg/avril.conf"));
850    http.source("pkg/child/mod.ts", "export {};");
851    write(
852        dir.path(),
853        "avril.conf",
854        format!("deps: ['{}']", http.name("pkg/")),
855    );
856    write(dir.path(), "main.ts", "import 'pkg';");
857    failure(
858        cli(dir.path(), &["run", "main.ts"]),
859        "Cyclic remote package boundary",
860    );
861}
862
863#[test]
864fn remote_package_maps_never_treat_file_or_absolute_targets_as_host_files() {
865    for filesystem_url in [false, true] {
866        let http = Http::new();
867        let dir = tempfile::tempdir().unwrap();
868        write(
869            dir.path(),
870            "secret.ts",
871            "throw new Error('host file was read');",
872        );
873        let target = if filesystem_url {
874            url::Url::from_file_path(dir.path().join("secret.ts"))
875                .unwrap()
876                .to_string()
877        } else {
878            "/secret.ts".into()
879        };
880        http.source(
881            "pkg/avril.conf",
882            format!("name: 'pkg', exports: './mod.ts', imports: {{escape: '{target}'}}").as_str(),
883        );
884        http.source("pkg/mod.ts", "export {value} from 'escape';");
885        http.source("secret.ts", "export const value = 7;");
886        write(
887            dir.path(),
888            "avril.conf",
889            format!("deps: ['{}']", http.name("pkg/")),
890        );
891        write(
892            dir.path(),
893            "main.ts",
894            "import {value} from 'pkg'; console.log(value);",
895        );
896        if filesystem_url {
897            failure(
898                cli(dir.path(), &["run", "main.ts"]),
899                "Unsupported module specifier",
900            );
901        } else {
902            assert_eq!(success(cli(dir.path(), &["run", "main.ts"])), "7\n");
903        }
904    }
905}
906
907#[test]
908fn redirected_export_modules_match_scopes_and_relative_imports_at_the_final_url() {
909    let http = Http::new();
910    let dir = tempfile::tempdir().unwrap();
911    http.source("pkg/avril.conf", "name: 'pkg', exports: './src/mod.ts', imports: {helper: './default.ts'}, scopes: {'./src/': {helper: './scoped.ts'}}");
912    http.redirect("pkg/src/mod.ts", &http.name("pkg/final/mod.ts"));
913    http.source("pkg/final/mod.ts", "import {value} from 'helper'; import {extra} from './dep.ts'; export const result = value + extra; export const url = import.meta.url;");
914    http.source("pkg/default.ts", "export const value = 1;");
915    http.source(
916        "pkg/scoped.ts",
917        "throw new Error('requested URL scope leaked');",
918    );
919    http.source("pkg/final/dep.ts", "export const extra = 2;");
920    write(
921        dir.path(),
922        "avril.conf",
923        format!("deps: ['{}']", http.name("pkg/")),
924    );
925    write(
926        dir.path(),
927        "main.ts",
928        "import {result,url} from 'pkg'; console.log(result,url);",
929    );
930    let deny = format!("--deny-js-exec={}", http.name("pkg/final/mod.ts"));
931    assert_eq!(
932        success(cli(dir.path(), &["run", &deny, "main.ts"])),
933        format!("3 {}\n", http.name("pkg/src/mod.ts"))
934    );
935    let info: serde_json::Value = serde_json::from_str(&success(cli(
936        dir.path(),
937        &["info", "--offline", &deny, "main.ts"],
938    )))
939    .unwrap();
940    assert_eq!(
941        info["modules"][http.name("pkg/src/mod.ts")]["imports"]["helper"],
942        http.name("pkg/default.ts")
943    );
944}