#[path = "common/http.rs"] mod http_fixture; #[path = "common/registry.rs"] mod registry_fixture; use http_fixture::Http; fn cli(directory: &std::path::Path, args: &[&str]) -> std::process::Output { std::process::Command::new(env!("CARGO_BIN_EXE_avril")) .current_dir(directory) .arg(args[0]) .arg("--cache-dir=cache") .args(&args[1..]) .output() .unwrap() } fn success(output: std::process::Output) -> String { assert!( output.status.success(), "{}", String::from_utf8_lossy(&output.stderr) ); String::from_utf8(output.stdout).unwrap() } fn failure(output: std::process::Output, message: &str) { assert!(!output.status.success()); assert!( String::from_utf8_lossy(&output.stderr).contains(message), "{}", String::from_utf8_lossy(&output.stderr) ); } fn write(directory: &std::path::Path, file: &str, source: impl AsRef<[u8]>) { std::fs::write(directory.join(file), source).unwrap(); } #[test] fn packages_have_private_maps_dependencies_self_imports_and_cyclic_headers() { let http = Http::new(); let dir = tempfile::tempdir().unwrap(); 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'}}"); http.source( "numeric/avril.conf", "name: 'numeric', exports: './mod.ts', deps: ['../math/']", ); http.source("numeric/mod.ts", "import {vector} from '@acme/math/vector'; export const number = 40; export const read = () => vector;"); 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};"); http.source( "math/src/vector.ts", "import {helper} from 'helper'; export const vector = helper;", ); http.source("math/helper.ts", "export const helper = 2;"); http.source("math/scoped.ts", "export const helper = 7;"); write( dir.path(), "avril.conf", format!( "deps: ['{}', {{from: '{}', name: 'renamed'}}], imports: {{helper: './wrong.ts'}}", http.name("math/"), http.name("math/") ), ); write( dir.path(), "wrong.ts", "throw new Error('consumer alias leaked');", ); write( dir.path(), "main.ts", format!( "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);", http.name("math/mod.ts") ), ); let expected = "true true 42 7 7 true 40\n"; assert_eq!(success(cli(dir.path(), &["run", "main.ts"])), expected); let requests = http.requests.lock().unwrap().clone(); assert!(!requests.iter().any(|url| url.contains("unused"))); assert_eq!( requests .iter() .filter(|url| url.ends_with("avril.conf")) .count(), 2 ); assert_eq!( success(cli( dir.path(), &["run", "--offline", "--frozen", "main.ts"] )), expected ); assert_eq!(*http.requests.lock().unwrap(), requests); let info: serde_json::Value = serde_json::from_str(&success(cli(dir.path(), &["info", "--offline", "main.ts"]))).unwrap(); let package = &info["modules"][http.name("math/mod.ts")]["package"]; assert_eq!(package["name"], "@acme/math"); assert_eq!(package["config"], http.name("math/avril.conf")); assert_eq!(package["dependencies"]["numeric"], http.name("numeric/")); assert_eq!( package["exports"]["./vector"], http.name("math/src/vector.ts") ); write(dir.path(), "main.ts", "import 'numeric';"); failure( cli(dir.path(), &["run", "--offline", "main.ts"]), "Unsupported module specifier", ); } #[test] fn configs_and_sources_are_pinned_updated_and_vendored_together() { let http = Http::new(); let dir = tempfile::tempdir().unwrap(); http.source("pkg/avril.conf", "name: 'pkg', exports: './v1.ts'"); http.source("pkg/v1.ts", "export const value = 1;"); http.source("pkg/v2.ts", "export const value = 2;"); write( dir.path(), "avril.conf", format!("deps: ['{}']", http.name("pkg/")), ); write( dir.path(), "main.ts", "import {value} from 'pkg'; console.log(value);", ); write( dir.path(), "avril.lock", "version: 1\njsrRegistry: \"https://jsr.io/\"\nspecifiers: {}\npackages: {}\nremote: {}\n", ); failure( cli(dir.path(), &["cache", "--frozen", "main.ts"]), "missing remote source", ); assert_eq!(success(cli(dir.path(), &["run", "main.ts"])), "1\n"); let lock = registry_fixture::read_lock(dir.path()); assert!(lock["remote"][http.name("pkg/avril.conf")]["checksum"].is_string()); http.source("pkg/avril.conf", "name: 'pkg', exports: './v2.ts'"); assert_eq!(success(cli(dir.path(), &["run", "main.ts"])), "1\n"); std::fs::remove_dir_all(dir.path().join("cache/http/blobs")).unwrap(); failure(cli(dir.path(), &["run", "main.ts"]), "integrity mismatch"); success(cli(dir.path(), &["update", "--prune", "main.ts"])); assert_eq!( success(cli( dir.path(), &["run", "--offline", "--frozen", "main.ts"] )), "2\n" ); success(cli(dir.path(), &["vendor", "main.ts"])); std::fs::remove_dir_all(dir.path().join("cache")).unwrap(); let requests = http.requests.lock().unwrap().len(); assert_eq!( success(cli( dir.path(), &["run", "--offline", "--frozen", "--vendor=vendor", "main.ts"] )), "2\n" ); assert_eq!(http.requests.lock().unwrap().len(), requests); let lock = registry_fixture::read_lock(dir.path()); assert!(lock["remote"].get(http.name("pkg/v1.ts")).is_none()); assert!(lock["remote"][http.name("pkg/avril.conf")].is_object()); } #[test] fn redirected_configs_resolve_exports_and_private_dependencies_at_the_final_url() { let http = Http::new(); let target = Http::new(); let dir = tempfile::tempdir().unwrap(); http.redirect("pkg/avril.conf", &target.name("nested/pkg/avril.conf")); target.source( "nested/pkg/avril.conf", "name: 'pkg', exports: './mod.ts', deps: ['../dep/']", ); target.source("nested/dep/avril.conf", "name: 'dep', exports: './mod.ts'"); target.source("nested/dep/mod.ts", "export const value = 9;"); target.source("nested/pkg/mod.ts", "export {value} from 'dep';"); write( dir.path(), "avril.conf", format!("deps: ['{}']", http.name("pkg/")), ); write( dir.path(), "main.ts", "import {value} from 'pkg'; console.log(value);", ); let allow_local = "--allow-js-exec=."; let allow_config = format!("--allow-js-exec={}", http.name("pkg/avril.conf")); let allow_target = format!("--allow-js-exec={}", target.name("nested/")); let deny_destination_config = format!("--deny-js-exec={}", target.name("nested/pkg/avril.conf")); let args = [ "run", "-0", allow_local, &allow_config, &allow_target, &deny_destination_config, "main.ts", ]; assert_eq!(success(cli(dir.path(), &args)), "9\n"); let mut offline = args.to_vec(); offline.insert(1, "--offline"); offline.insert(1, "--frozen"); assert_eq!(success(cli(dir.path(), &offline)), "9\n"); } #[test] fn configs_and_exports_require_execution_authority_even_on_cache_hits() { let http = Http::new(); let dir = tempfile::tempdir().unwrap(); http.source("pkg/avril.conf", "name: 'pkg', exports: './mod.ts'"); http.source("pkg/mod.ts", "console.log('loaded');"); write( dir.path(), "avril.conf", format!("deps: [{{from: '{}', name: 'pkg'}}]", http.name("pkg/")), ); write(dir.path(), "main.ts", "import 'pkg';"); let allow = format!("--allow-js-exec={}", http.name("pkg/")); success(cli( dir.path(), &["run", "-0", "--allow-js-exec=.", &allow, "main.ts"], )); for denied in ["pkg/avril.conf", "pkg/mod.ts"] { let deny = format!("--deny-js-exec={}", http.name(denied)); let requests = http.requests.lock().unwrap().len(); failure( cli( dir.path(), &[ "run", "-0", "--allow-js-exec=.", &allow, &deny, "--offline", "main.ts", ], ), "PermissionDenied", ); assert_eq!(http.requests.lock().unwrap().len(), requests); } } #[test] fn dependency_headers_bind_names_and_interfaces_without_fallbacks() { let http = Http::new(); let dir = tempfile::tempdir().unwrap(); write(dir.path(), "main.ts", "import 'pkg/private';"); http.source( "pkg/avril.conf", "name: 'pkg', exports: {'./other': './other.ts'}", ); http.source("pkg/private.ts", "throw new Error('private fallback');"); write( dir.path(), "avril.conf", format!("deps: ['{}']", http.name("pkg/")), ); failure(cli(dir.path(), &["run", "main.ts"]), "no export"); assert!( !http .requests .lock() .unwrap() .iter() .any(|url| url.ends_with("private.ts")) ); write( dir.path(), "avril.conf", format!( "deps: ['{}', {{from: '{}', name: 'pkg'}}]", http.name("pkg/"), http.name("another/") ), ); failure( cli(dir.path(), &["run", "main.ts"]), "Duplicate dependency binding", ); http.source("anonymous/avril.conf", "exports: './mod.ts'"); write( dir.path(), "avril.conf", format!("deps: ['{}']", http.name("anonymous/")), ); failure(cli(dir.path(), &["run", "main.ts"]), "declares no name"); http.source("anonymous/mod.ts", "console.log('anonymous');"); write( dir.path(), "avril.conf", format!( "deps: [{{from: '{}', name: 'pkg'}}]", http.name("anonymous/") ), ); write(dir.path(), "main.ts", "import 'pkg';"); assert_eq!(success(cli(dir.path(), &["run", "main.ts"])), "anonymous\n"); } #[test] fn cached_package_bindings_obey_live_config_revocation() { let http = Http::new(); let dir = tempfile::tempdir().unwrap(); http.source("pkg/avril.conf", "name: 'pkg', exports: './mod.ts'"); http.source("pkg/mod.ts", "export const value = 42;"); write( dir.path(), "avril.conf", format!("deps: ['{}']", http.name("pkg/")), ); write( dir.path(), "main.ts", format!( "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'));}}", http.name("pkg/avril.conf") ), ); assert_eq!( success(cli(dir.path(), &["run", "main.ts"])), "true\ntrue\n" ); } #[test] fn declared_nested_packages_own_exported_files() { let http = Http::new(); let dir = tempfile::tempdir().unwrap(); http.source( "pkg/avril.conf", "name: 'outer', exports: './child/mod.ts', deps: [{from: './child/', name: 'nested'}], imports: {helper: './wrong.ts'}", ); http.source( "pkg/child/avril.conf", "name: 'child', exports: './mod.ts', imports: {helper: './helper.ts'}", ); http.source("pkg/child/mod.ts", "export {value} from 'helper';"); http.source("pkg/child/helper.ts", "export const value = 7;"); http.source("pkg/wrong.ts", "throw new Error('parent map leaked');"); write( dir.path(), "avril.conf", format!( "deps: ['{}', '{}']", http.name("pkg/"), http.name("pkg/child/") ), ); write( dir.path(), "main.ts", "import * as a from 'outer'; import * as b from 'child'; console.log(a === b, a.value);", ); assert_eq!(success(cli(dir.path(), &["run", "main.ts"])), "true 7\n"); } #[test] fn remote_packages_can_bind_private_jsr_dependencies() { let http = Http::new(); let registry = registry_fixture::Registry::new(); let dir = tempfile::tempdir().unwrap(); registry.package( "@test/numeric", "1.0.0", &[("mod.ts", "export const number: number = 42;")], &[(".", "./mod.ts")], ); http.source( "pkg/avril.conf", "name: 'pkg', exports: './mod.ts', deps: [{from: 'jsr:@test/numeric@1', name: 'numeric'}]", ); http.source("pkg/mod.ts", "export {number} from 'numeric';"); write( dir.path(), "avril.conf", format!( "deps: ['{}'], imports: {{numeric: './wrong.ts'}}", http.name("pkg/") ), ); write( dir.path(), "wrong.ts", "throw new Error('consumer alias leaked');", ); write( dir.path(), "main.ts", "import {number} from 'pkg'; console.log(number);", ); let registry_arg = format!( "--jsr-registry={}", registry.options(dir.path()).jsr_registry ); assert_eq!( success(cli(dir.path(), &["run", ®istry_arg, "main.ts"])), "42\n" ); let requests = registry.requests.lock().unwrap().len(); assert_eq!( success(cli( dir.path(), &["run", ®istry_arg, "--offline", "--frozen", "main.ts"] )), "42\n" ); assert_eq!(registry.requests.lock().unwrap().len(), requests); } #[test] fn nested_ownership_and_metadata_are_independent_of_graph_traversal_order() { let http = Http::new(); let dir = tempfile::tempdir().unwrap(); http.source("pkg/avril.conf", "name: 'outer', exports: './child/mod.ts', deps: [{from: './child/', name: 'nested'}], imports: {helper: './wrong.ts'}"); http.source( "pkg/child/avril.conf", "name: 'child', exports: './mod.ts', imports: {helper: './helper.ts'}", ); http.source("pkg/child/mod.ts", "export {value} from 'helper';"); http.source("pkg/child/helper.ts", "export const value = 7;"); http.source("pkg/wrong.ts", "throw new Error('parent map leaked');"); write( dir.path(), "avril.conf", format!( "deps: ['{}', '{}']", http.name("pkg/"), http.name("pkg/child/") ), ); write( dir.path(), "x.ts", "import * as a from 'outer'; export {a};", ); write( dir.path(), "y.ts", "import * as b from 'child'; export {b};", ); for imports in [ "import {a} from './x.ts'; import {b} from './y.ts';", "import {b} from './y.ts'; import {a} from './x.ts';", ] { write( dir.path(), "main.ts", format!("{imports} console.log(a === b, a.value);"), ); assert_eq!(success(cli(dir.path(), &["run", "main.ts"])), "true 7\n"); let info: serde_json::Value = serde_json::from_str(&success(cli(dir.path(), &["info", "--offline", "main.ts"]))) .unwrap(); let module = &info["modules"][http.name("pkg/child/mod.ts")]; assert_eq!(module["package"]["name"], "child"); assert_eq!( module["imports"]["helper"], http.name("pkg/child/helper.ts") ); } } #[test] fn conflicting_owners_fail_in_both_graph_orders_instead_of_rebinding_modules() { let http = Http::new(); let dir = tempfile::tempdir().unwrap(); http.source( "pkg/avril.conf", "name: 'outer', exports: './child/mod.ts', imports: {helper: './helper.ts'}", ); http.source( "pkg/child/avril.conf", "name: 'child', exports: './mod.ts', imports: {helper: './helper.ts'}", ); http.source("pkg/child/mod.ts", "export {value} from 'helper';"); http.source("pkg/helper.ts", "export const value = 1;"); http.source("pkg/child/helper.ts", "export const value = 7;"); write( dir.path(), "avril.conf", format!( "deps: ['{}', '{}']", http.name("pkg/"), http.name("pkg/child/") ), ); write(dir.path(), "x.ts", "import 'outer';"); write(dir.path(), "y.ts", "import 'child';"); for imports in [ "import './x.ts'; import './y.ts';", "import './y.ts'; import './x.ts';", ] { write(dir.path(), "main.ts", imports); failure( cli(dir.path(), &["run", "main.ts"]), "Conflicting remote package ownership", ); } } #[test] fn selecting_a_private_dependency_does_not_change_existing_modules_maps() { let http = Http::new(); let dir = tempfile::tempdir().unwrap(); http.source( "pkg/avril.conf", "name: 'a', exports: './child/util.ts', imports: {helper: './helper.ts'}", ); http.source("pkg/helper.ts", "export const value = 1;"); http.source( "pkg/child/util.ts", "export const lazy = async () => (await import('helper')).value;", ); http.source( "pkg/child/avril.conf", "name: 'child', exports: './other.ts', imports: {helper: './helper.ts'}", ); http.source("pkg/child/helper.ts", "export const value = 99;"); http.source("pkg/child/other.ts", "export const value = 2;"); http.source( "b/avril.conf", format!( "name: 'b', exports: './mod.ts', deps: [{{from: '{}', name: 'private'}}]", http.name("pkg/child/") ) .as_str(), ); http.source("b/mod.ts", "export {value} from 'private';"); write( dir.path(), "avril.conf", format!("deps: ['{}', '{}']", http.name("pkg/"), http.name("b/")), ); write( dir.path(), "main.ts", "const a = await import('a'); const before = await a.lazy(); const b = await import('b'); console.log(before, await a.lazy(), b.value);", ); assert_eq!(success(cli(dir.path(), &["run", "main.ts"])), "1 1 2\n"); } #[test] fn a_plain_module_stays_plain_after_a_conflicting_package_import() { let http = Http::new(); let dir = tempfile::tempdir().unwrap(); http.source( "pkg/avril.conf", "name: 'pkg', exports: './util.ts', imports: {helper: './helper.ts'}", ); http.source("pkg/util.ts", "export const lazy = () => import('helper');"); http.source("pkg/helper.ts", "export const value = 7;"); std::fs::create_dir(dir.path().join("plain")).unwrap(); write( dir.path(), "avril.conf", format!("deps: ['{}']", http.name("pkg/")), ); write(dir.path(), "plain/avril.conf", ""); write( dir.path(), "plain/mod.ts", format!("export {{lazy}} from '{}';", http.name("pkg/util.ts")), ); write( dir.path(), "main.ts", "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'));}}}", ); assert_eq!( success(cli(dir.path(), &["run", "main.ts"])), "true\ntrue\ntrue\n" ); } #[test] fn registry_prefixed_configs_are_rejected_without_fetching_or_changing_the_lock() { let http = Http::new(); let dir = tempfile::tempdir().unwrap(); let registry = http.name("registry/"); let args_registry = format!("--jsr-registry={registry}"); let lock = format!( "version: 1\njsrRegistry: \"{registry}\"\nspecifiers: {{}}\npackages: {{}}\nremote: {{}}\n" ); write(dir.path(), "avril.lock", &lock); http.source( "registry/pkg/avril.conf", "name: 'pkg', exports: './mod.ts'", ); for dependency in [ format!("'{}'", http.name("registry/pkg/")), format!("{{from: '{}', name: 'pkg'}}", http.name("registry/pkg/")), ] { write(dir.path(), "avril.conf", format!("deps: [{dependency}]")); write(dir.path(), "main.ts", "import 'pkg';"); failure( cli(dir.path(), &["run", &args_registry, "main.ts"]), "configured JSR registry", ); assert_eq!( std::fs::read_to_string(dir.path().join("avril.lock")).unwrap(), lock ); assert!(http.requests.lock().unwrap().is_empty()); } } #[test] fn a_failed_config_fetch_does_not_snapshot_plain_ownership() { let http = Http::new(); let dir = tempfile::tempdir().unwrap(); http.source("pkg/mod.ts", "export const value = 7;"); let routes = http.routes.clone(); *http.hook.lock().unwrap() = Some(Box::new(move || { routes.lock().unwrap().insert( "/pkg/avril.conf".into(), http_fixture::Response { status: 200, headers: "Content-Type: text/plain\r\n".into(), body: b"name: 'pkg', exports: './mod.ts'".to_vec(), }, ); })); write( dir.path(), "avril.conf", format!("deps: [{{from: '{}', name: 'pkg'}}]", http.name("pkg/")), ); write( dir.path(), "main.ts", format!( "try {{await import('pkg');}} catch(e) {{console.log(e.message.includes('404'));}} console.log((await import('{}')).value);", http.name("pkg/mod.ts") ), ); assert_eq!(success(cli(dir.path(), &["run", "main.ts"])), "true\n7\n"); assert_eq!( http.requests .lock() .unwrap() .iter() .filter(|url| url.ends_with("avril.conf")) .count(), 2 ); } #[test] fn redirected_aliases_share_modules_only_when_config_pins_agree() { for changed in [false, true] { let http = Http::new(); let target = Http::new(); let dir = tempfile::tempdir().unwrap(); http.redirect("a/avril.conf", &target.name("pkg/avril.conf")); http.redirect("b/avril.conf", &target.name("pkg/avril.conf")); target.source("pkg/avril.conf", "exports: './mod.ts'"); target.source("pkg/mod.ts", "export const value = 7;"); if changed { let routes = target.routes.clone(); *target.hook.lock().unwrap() = Some(Box::new(move || { routes.lock().unwrap().insert( "/pkg/avril.conf".into(), http_fixture::Response { status: 200, headers: "Content-Type: text/plain\r\n".into(), body: b"exports: './mod.ts', description: 'changed'".to_vec(), }, ); })); } write( dir.path(), "avril.conf", format!( "deps: [{{from: '{}', name: 'a'}}, {{from: '{}', name: 'b'}}]", http.name("a/"), http.name("b/") ), ); write( dir.path(), "main.ts", "import * as a from 'a'; import * as b from 'b'; console.log(a === b, a.value);", ); if changed { failure( cli(dir.path(), &["run", "main.ts"]), "Conflicting remote package ownership", ); } else { assert_eq!(success(cli(dir.path(), &["run", "main.ts"])), "true 7\n"); assert_eq!( success(cli( dir.path(), &["run", "--offline", "--frozen", "main.ts"] )), "true 7\n" ); } } } #[test] fn equivalent_aliases_use_the_canonical_module_region_in_both_orders() { for root_alias in [false, true] { let http = Http::new(); let dir = tempfile::tempdir().unwrap(); let (a, b, outside) = if root_alias { http.redirect("avril.conf", &http.name("f/avril.conf")); (http.url.to_string(), http.name("f/"), "other.ts") } else { http.redirect("a/avril.conf", &http.name("f/avril.conf")); http.redirect("b/avril.conf", &http.name("f/avril.conf")); (http.name("a/"), http.name("b/"), "b/extra.ts") }; http.source( "f/avril.conf", "name: 'shared', exports: './mod.ts', imports: {helper: './helper.ts'}", ); http.source( "f/mod.ts", format!( "export const lazy = () => import('{}');", http.name(outside) ) .as_str(), ); http.source(outside, "export {value} from 'helper';"); http.source("f/helper.ts", "export const value = 7;"); write( dir.path(), "avril.conf", format!("deps: [{{from: '{a}', name: 'a'}}, {{from: '{b}', name: 'b'}}]"), ); for imports in [ "import * as a from 'a'; import * as b from 'b';", "import * as b from 'b'; import * as a from 'a';", ] { write( dir.path(), "main.ts", format!( "{imports} try {{await a.lazy();}} catch(e) {{console.log(a === b, e.message.includes('Unsupported module specifier'));}}" ), ); assert_eq!(success(cli(dir.path(), &["run", "main.ts"])), "true true\n"); let info: serde_json::Value = serde_json::from_str(&success(cli(dir.path(), &["info", "main.ts"]))).unwrap(); assert_eq!( info["modules"][http.name("f/mod.ts")]["package"]["config"], http.name("f/avril.conf") ); } } } #[test] fn revoking_any_claimed_alias_blocks_all_aliases_in_both_orders() { let http = Http::new(); let dir = tempfile::tempdir().unwrap(); http.redirect("a/avril.conf", &http.name("f/avril.conf")); http.redirect("b/avril.conf", &http.name("f/avril.conf")); http.source("f/avril.conf", "name: 'shared', exports: './mod.ts'"); http.source("f/mod.ts", "export const lazy = () => import('shared');"); write( dir.path(), "avril.conf", format!( "deps: [{{from: '{}', name: 'a'}}, {{from: '{}', name: 'b'}}]", http.name("a/"), http.name("b/") ), ); for imports in [ "import * as a from 'a'; import * as b from 'b';", "import * as b from 'b'; import * as a from 'a';", ] { write( dir.path(), "main.ts", format!( "{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'));}}", http.name("a/avril.conf") ), ); assert_eq!( success(cli(dir.path(), &["run", "main.ts"])), "true\ntrue\ntrue\n" ); } } #[test] fn rejected_registry_redirects_are_not_pinned_after_a_later_successful_graph() { let http = Http::new(); let dir = tempfile::tempdir().unwrap(); let registry = http.name("registry/"); let registry_arg = format!("--jsr-registry={registry}"); let lock = format!( "version: 1\njsrRegistry: \"{registry}\"\nspecifiers: {{}}\npackages: {{}}\nremote: {{}}\n" ); write(dir.path(), "avril.lock", &lock); http.redirect("alias/avril.conf", &http.name("registry/pkg/avril.conf")); http.source( "registry/pkg/avril.conf", "name: 'pkg', exports: './mod.ts'", ); write( dir.path(), "avril.conf", format!("deps: [{{from: '{}', name: 'pkg'}}]", http.name("alias/")), ); write(dir.path(), "other.ts", "console.log('done');"); write( dir.path(), "main.ts", "try {await import('pkg');} catch(e) {console.log(e.message.includes('configured JSR registry'));} await import('./other.ts');", ); assert_eq!( success(cli(dir.path(), &["run", ®istry_arg, "main.ts"])), "true\ndone\n" ); assert_eq!( std::fs::read_to_string(dir.path().join("avril.lock")).unwrap(), lock ); } #[test] fn nested_config_redirect_cycles_are_reported_instead_of_looping() { let http = Http::new(); let dir = tempfile::tempdir().unwrap(); http.source( "pkg/avril.conf", "name: 'pkg', exports: './child/mod.ts', deps: [{from: './child/', name: 'nested'}]", ); http.redirect("pkg/child/avril.conf", &http.name("pkg/avril.conf")); http.source("pkg/child/mod.ts", "export {};"); write( dir.path(), "avril.conf", format!("deps: ['{}']", http.name("pkg/")), ); write(dir.path(), "main.ts", "import 'pkg';"); failure( cli(dir.path(), &["run", "main.ts"]), "Cyclic remote package boundary", ); } #[test] fn remote_package_maps_never_treat_file_or_absolute_targets_as_host_files() { for filesystem_url in [false, true] { let http = Http::new(); let dir = tempfile::tempdir().unwrap(); write( dir.path(), "secret.ts", "throw new Error('host file was read');", ); let target = if filesystem_url { url::Url::from_file_path(dir.path().join("secret.ts")) .unwrap() .to_string() } else { "/secret.ts".into() }; http.source( "pkg/avril.conf", format!("name: 'pkg', exports: './mod.ts', imports: {{escape: '{target}'}}").as_str(), ); http.source("pkg/mod.ts", "export {value} from 'escape';"); http.source("secret.ts", "export const value = 7;"); write( dir.path(), "avril.conf", format!("deps: ['{}']", http.name("pkg/")), ); write( dir.path(), "main.ts", "import {value} from 'pkg'; console.log(value);", ); if filesystem_url { failure( cli(dir.path(), &["run", "main.ts"]), "Unsupported module specifier", ); } else { assert_eq!(success(cli(dir.path(), &["run", "main.ts"])), "7\n"); } } } #[test] fn redirected_export_modules_match_scopes_and_relative_imports_at_the_final_url() { let http = Http::new(); let dir = tempfile::tempdir().unwrap(); http.source("pkg/avril.conf", "name: 'pkg', exports: './src/mod.ts', imports: {helper: './default.ts'}, scopes: {'./src/': {helper: './scoped.ts'}}"); http.redirect("pkg/src/mod.ts", &http.name("pkg/final/mod.ts")); 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;"); http.source("pkg/default.ts", "export const value = 1;"); http.source( "pkg/scoped.ts", "throw new Error('requested URL scope leaked');", ); http.source("pkg/final/dep.ts", "export const extra = 2;"); write( dir.path(), "avril.conf", format!("deps: ['{}']", http.name("pkg/")), ); write( dir.path(), "main.ts", "import {result,url} from 'pkg'; console.log(result,url);", ); let deny = format!("--deny-js-exec={}", http.name("pkg/final/mod.ts")); assert_eq!( success(cli(dir.path(), &["run", &deny, "main.ts"])), format!("3 {}\n", http.name("pkg/src/mod.ts")) ); let info: serde_json::Value = serde_json::from_str(&success(cli( dir.path(), &["info", "--offline", &deny, "main.ts"], ))) .unwrap(); assert_eq!( info["modules"][http.name("pkg/src/mod.ts")]["imports"]["helper"], http.name("pkg/default.ts") ); }