mod common; #[path = "common/http.rs"] mod http_fixture; #[path = "common/registry.rs"] mod registry_fixture; use std::fs; use std::process::{Command, Stdio}; fn run(directory: &std::path::Path, entry: &str, args: &[String]) -> std::process::Output { let output = common::wait( Command::new(env!("CARGO_BIN_EXE_avril")) .arg("run") .current_dir(directory) .args(args) .arg(entry) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .spawn() .unwrap(), ); assert!( output.status.success(), "{}", String::from_utf8_lossy(&output.stderr) ); output } #[test] fn file_metadata_resolves_without_loading_and_binds_to_the_module() { let dir = tempfile::tempdir().unwrap(); fs::create_dir(dir.path().join("nested")).unwrap(); 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(); fs::write( dir.path().join("nested/dep.ts"), "export const resolve = import.meta.resolve; export const meta = import.meta;", ) .unwrap(); fs::write( dir.path().join("not-evaluated.js"), "throw Error('Resolution evaluated a module');", ) .unwrap(); fs::write(dir.path().join("main #😀.ts"), r#" import {resolve as other, meta} from './nested/dep.ts'; const ownURL = import.meta.url; if (import.meta.filename !== decodeURIComponent(new URL(ownURL).pathname) || import.meta.dirname !== decodeURIComponent(new URL('.',ownURL).pathname).slice(0,-1)) throw Error('File paths'); if (Object.getPrototypeOf(import.meta) !== null || !import.meta.main || meta.main) throw Error('Metadata'); const resolve = import.meta.resolve; if (resolve.length !== 1 || resolve.name !== 'resolve') throw Error('Function surface'); import.meta.url = 'http://fake.invalid/'; if (resolve('./not-evaluated.js') !== new URL('./not-evaluated.js',ownURL).href || resolve('alias') !== new URL('./not%20present%20%23%F0%9F%98%80.ts',ownURL).href || other.call(import.meta,'alias') !== new URL('./nested/missing.ts',ownURL).href || resolve('avril:std') !== 'avril:std') throw Error('Resolver identity or import map'); const marker = {}; try {resolve({toString() {throw marker;}}); throw Error('Swallowed exception');} catch (error) {if (error !== marker) throw error;} for (const operation of [() => resolve(Symbol()), () => resolve('blocked'), () => resolve('unknown'), () => resolve('ext:core/ops'), () => new resolve('./x')]) { try {operation(); throw Error('Expected TypeError');} catch (error) {if (!(error instanceof TypeError)) throw error;} } if (resolve(new URL('./missing.ts',ownURL)) !== new URL('./missing.ts',ownURL).href) throw Error('URL coercion'); console.log('metadata'); "#).unwrap(); assert_eq!(run(dir.path(), "main #😀.ts", &[]).stdout, b"metadata\n"); } #[test] fn package_exports_and_live_permissions_apply_to_resolution() { let dir = tempfile::tempdir().unwrap(); fs::create_dir(dir.path().join("library")).unwrap(); fs::write( dir.path().join("avril.conf"), "name: 'app', exports: {'.': './main.js'}, deps: ['./library']", ) .unwrap(); fs::write( dir.path().join("library/avril.conf"), "name: 'lib', exports: {'.': './missing.ts'}", ) .unwrap(); fs::write(dir.path().join("main.js"),r#" import {pledge} from 'avril:std'; if (import.meta.resolve('app') !== import.meta.url || import.meta.resolve('lib') !== new URL('./library/missing.ts',import.meta.url).href) throw Error('Exports'); const blocked = new URL('./blocked.js', import.meta.url); pledge({jsExec:{deny:[blocked]}}); try {import.meta.resolve('./blocked.js'); throw Error('Permission accepted');} catch (error) {if (!(error instanceof TypeError) || !error.message.includes('PermissionDenied')) throw error;} console.log('exports'); "#).unwrap(); assert_eq!(run(dir.path(), "main.js", &[]).stdout, b"exports\n"); } #[test] fn http_metadata_resolves_from_the_redirected_base_without_fetching() { let http = http_fixture::Http::new(); http.redirect("start", &http.name("nested/module.js")); http.source("nested/module.js",r#" if ('dirname' in import.meta || 'filename' in import.meta) throw Error('HTTP filesystem metadata'); console.log(import.meta.resolve('./missing.js?query#fragment')); "#); let dir = tempfile::tempdir().unwrap(); let output = run( dir.path(), &http.name("start"), &["--cache-dir=cache".into()], ); assert_eq!( output.stdout, format!("{}\n", http.name("nested/missing.js?query#fragment")).as_bytes() ); assert!( !http .requests .lock() .unwrap() .iter() .any(|path| path.contains("missing")) ); } #[test] fn prepared_jsr_metadata_resolves_other_exports_without_acquisition() { let registry = registry_fixture::Registry::new(); registry.package( "@test/meta", "1.0.0", &[ ("mod.ts", "export {};"), ("other.ts", "throw Error('Loaded other export');"), ], &[(".", "./mod.ts"), ("./other", "./other.ts")], ); let dir = tempfile::tempdir().unwrap(); fs::write( dir.path().join("avril.conf"), "imports: {pkg: 'jsr:@test/meta@1', 'pkg/': 'jsr:@test/meta@1/'}", ) .unwrap(); fs::write( dir.path().join("main.ts"), "import 'pkg'; console.log(import.meta.resolve('pkg/other'));", ) .unwrap(); let options = registry.options(dir.path()); let output = run( dir.path(), "main.ts", &[ format!("--jsr-registry={}", options.jsr_registry), format!("--cache-dir={}", options.cache.display()), ], ); assert_eq!( output.stdout, format!("{}@test/meta/1.0.0/other.ts\n", registry.url).as_bytes() ); assert!( !registry .requests .lock() .unwrap() .iter() .any(|path| path.ends_with("other.ts")) ); } #[test] fn resolve_matches_loaded_module_for_relative_files() { let dir = tempfile::tempdir().unwrap(); fs::create_dir(dir.path().join("sub")).unwrap(); fs::write( dir.path().join("dep.js"), "export const metaUrl = import.meta.url;\n", ) .unwrap(); fs::write( dir.path().join("sub/nested.js"), "export const metaUrl = import.meta.url;\n", ) .unwrap(); fs::write(dir.path().join("main.js"), r#" async function check(spec) { const loaded = await import(spec); const viaMeta = import.meta.resolve(spec); if (viaMeta !== loaded.metaUrl) throw new Error(`parity ${spec}: ${viaMeta} !== ${loaded.metaUrl}`); } await check('./dep.js'); await check('./sub/nested.js'); console.log('relative-parity'); "#).unwrap(); assert_eq!(run(dir.path(), "main.js", &[]).stdout, b"relative-parity\n"); } #[test] fn resolve_matches_loaded_module_for_local_binding_exports() { let dir = tempfile::tempdir().unwrap(); fs::create_dir(dir.path().join("lib")).unwrap(); fs::write( dir.path().join("avril.conf"), "name: 'app', deps: ['./lib']", ) .unwrap(); fs::write( dir.path().join("lib/avril.conf"), "name: 'mylib', exports: {'.': './index.js', './feature': './feature.js'}", ) .unwrap(); fs::write( dir.path().join("lib/index.js"), "export const metaUrl = import.meta.url;\n", ) .unwrap(); fs::write( dir.path().join("lib/feature.js"), "export const metaUrl = import.meta.url;\n", ) .unwrap(); fs::write(dir.path().join("main.js"), r#" async function check(spec) { const loaded = await import(spec); const viaMeta = import.meta.resolve(spec); if (viaMeta !== loaded.metaUrl) throw new Error(`parity ${spec}: ${viaMeta} !== ${loaded.metaUrl}`); } await check('mylib'); await check('mylib/feature'); console.log('binding-parity'); "#).unwrap(); assert_eq!(run(dir.path(), "main.js", &[]).stdout, b"binding-parity\n"); } #[test] fn resolve_matches_loaded_module_for_http_modules() { let http = http_fixture::Http::new(); http.redirect("start", &http.name("nested/module.js")); http.source( "nested/module.js", r#" const viaMeta = import.meta.resolve('./sibling.js'); const loaded = await import('./sibling.js'); if (viaMeta !== loaded.metaUrl) throw new Error(`parity ./sibling.js: ${viaMeta} !== ${loaded.metaUrl}`); console.log('http-parity'); "#, ); http.source( "nested/sibling.js", "export const metaUrl = import.meta.url;\n", ); let dir = tempfile::tempdir().unwrap(); let output = run( dir.path(), &http.name("start"), &["--cache-dir=cache".into()], ); assert_eq!(output.stdout, b"http-parity\n"); } #[test] fn resolve_matches_loaded_module_for_jsr_direct_and_binding() { let registry = registry_fixture::Registry::new(); registry.package( "@test/parity", "1.0.0", &[ ("mod.ts", "export const metaUrl = import.meta.url;"), ("sub.ts", "export const metaUrl = import.meta.url;"), ], &[(".", "./mod.ts"), ("./sub", "./sub.ts")], ); let dir = tempfile::tempdir().unwrap(); fs::write( dir.path().join("avril.conf"), "deps: ['jsr:@test/parity@^1']", ) .unwrap(); fs::write(dir.path().join("main.ts"), r#" async function check(spec) { const loaded = await import(spec); const viaMeta = import.meta.resolve(spec); if (viaMeta !== loaded.metaUrl) throw new Error(`parity ${spec}: ${viaMeta} !== ${loaded.metaUrl}`); } await check('jsr:@test/parity@^1'); await check('jsr:@test/parity@^1/sub'); await check('@test/parity'); await check('@test/parity/sub'); console.log('jsr-parity'); "#).unwrap(); let options = registry.options(dir.path()); let output = run( dir.path(), "main.ts", &[ format!("--jsr-registry={}", options.jsr_registry), format!("--cache-dir={}", options.cache.display()), ], ); assert_eq!(output.stdout, b"jsr-parity\n"); } #[test] fn resolve_matches_loaded_module_for_npm_direct_binding_and_owned_referrer() { let registry = registry_fixture::Registry::new(); registry.publish_npm( "metadep", "1.0.0", &[ ( "package.json", r#"{"name": "metadep", "version": "1.0.0", "type": "module", "exports": {".": "./index.js"}}"#, ), ("index.js", "export const metaUrl = import.meta.url;\n"), ], ); registry.publish_npm( "metaparity", "1.0.0", &[ ( "package.json", r#"{"name": "metaparity", "version": "1.0.0", "type": "module", "exports": {".": "./index.js", "./sibling": "./sibling.js"}, "dependencies": {"metadep": "^1.0.0"}}"#, ), ( "index.js", r#"export const metaUrl = import.meta.url; export async function checkOwned() { const viaSibling = import.meta.resolve('./sibling.js'); const sibling = await import('./sibling.js'); if (viaSibling !== sibling.metaUrl) throw new Error(`owned sibling parity: ${viaSibling} !== ${sibling.metaUrl}`); const viaDep = import.meta.resolve('metadep'); const dep = await import('metadep'); if (viaDep !== dep.metaUrl) throw new Error(`owned dep parity: ${viaDep} !== ${dep.metaUrl}`); } "#, ), ("sibling.js", "export const metaUrl = import.meta.url;\n"), ], ); let dir = tempfile::tempdir().unwrap(); fs::write(dir.path().join("avril.conf"), "deps: ['npm:metaparity@^1']").unwrap(); fs::write(dir.path().join("main.js"), r#" async function check(spec) { const loaded = await import(spec); const viaMeta = import.meta.resolve(spec); if (!viaMeta.startsWith('file://')) throw new Error(`npm identity is not a file URL: ${spec} -> ${viaMeta}`); if (viaMeta !== loaded.metaUrl) throw new Error(`parity ${spec}: ${viaMeta} !== ${loaded.metaUrl}`); } await check('npm:metaparity@^1'); await check('npm:metaparity@^1/sibling'); await check('metaparity'); await check('metaparity/sibling'); await (await import('npm:metaparity@^1')).checkOwned(); console.log('npm-parity'); "#).unwrap(); let options = registry.options(dir.path()); let output = run( dir.path(), "main.js", &[ format!("--npm-registry={}", options.npm_registry), format!("--cache-dir={}", options.cache.display()), ], ); assert_eq!(output.stdout, b"npm-parity\n"); } #[test] fn resolve_matches_loaded_avril_builtin() { let dir = tempfile::tempdir().unwrap(); fs::write( dir.path().join("main.js"), r#" if (import.meta.resolve('avril:std') !== 'avril:std') throw new Error('builtin identity'); const std = await import('avril:std'); if (typeof std.pledge !== 'function') throw new Error('builtin loads'); console.log('builtin-parity'); "#, ) .unwrap(); assert_eq!(run(dir.path(), "main.js", &[]).stdout, b"builtin-parity\n"); } #[test] fn resolve_and_import_agree_on_ext_and_node_rejections() { let dir = tempfile::tempdir().unwrap(); fs::write(dir.path().join("main.js"), r#" for (const spec of ['node:fs', 'ext:core/ops']) { let metaError = null; try { import.meta.resolve(spec); } catch (error) { metaError = error; } if (!(metaError instanceof TypeError)) throw new Error(`resolve should reject ${spec}`); let importError = null; try { await import(spec); } catch (error) { importError = error; } if (!importError) throw new Error(`import should reject ${spec}`); const want = spec.startsWith('node:') ? 'not supported' : 'private'; if (!String(metaError.message).includes(want) || !String(importError.message ?? importError).includes(want)) { throw new Error(`message class for ${spec}: ${metaError.message} / ${importError.message ?? importError}`); } } console.log('rejections'); "#).unwrap(); assert_eq!(run(dir.path(), "main.js", &[]).stdout, b"rejections\n"); } #[test] fn workers_get_their_own_file_metadata_and_bound_resolver() { let dir = tempfile::tempdir().unwrap(); fs::write(dir.path().join("worker.js"),r#" postMessage([import.meta.filename, import.meta.dirname, import.meta.resolve('./missing.js'), import.meta.main]); close(); "#).unwrap(); fs::write(dir.path().join("main.js"),r#" const worker = new Worker(new URL('./worker.js',import.meta.url),{type:'module'}); const data = await new Promise((resolve,reject) => {worker.onmessage = e => resolve(e.data); worker.onerror = reject;}); 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'); console.log('worker'); "#).unwrap(); assert_eq!(run(dir.path(), "main.js", &[]).stdout, b"worker\n"); }