#[path = "common/registry.rs"] mod fixture; use fixture::{Registry, failure, manifest, success, write}; use std::fs; use std::path::{Path, PathBuf}; use std::process::{Command, Output}; fn run(dir: &Path, args: &[&str], entry: &str) -> Output { Command::new(env!("CARGO_BIN_EXE_avril")) .arg("run") .current_dir(dir) .args(args) .arg(entry) .output() .unwrap() } #[test] fn local_cjs_executes_once_with_shared_identity() { let dir = tempfile::tempdir().unwrap(); write( dir.path(), "main.cjs", r#" const a1 = require('./a.cjs'); const a2 = require('./a.cjs'); console.log(a1 === a2, globalThis.count, a1.foo); import('./a.cjs').then((ns) => console.log(ns.default === a1, globalThis.count)); "#, ); write( dir.path(), "a.cjs", "globalThis.count = (globalThis.count ?? 0) + 1; exports.foo = 7;", ); assert_eq!( success(&run(dir.path(), &[], "main.cjs")), "true 1 7\ntrue 1\n" ); } #[test] fn esm_default_and_named_import_of_cjs() { let dir = tempfile::tempdir().unwrap(); write( dir.path(), "main.js", r#" import def, { foo, bar } from './a.cjs'; import * as ns from './a.cjs'; console.log(def.foo, foo, bar, ns.bar, def === ns.default); "#, ); write( dir.path(), "a.cjs", "exports.foo = 1; module.exports.bar = 2; module.exports.foo = 10;", ); assert_eq!( success(&run(dir.path(), &[], "main.js")), "10 10 2 2 true\n" ); } #[test] fn function_primitive_and_reassigned_exports() { let dir = tempfile::tempdir().unwrap(); write( dir.path(), "main.cjs", r#" console.log(require('./prim.cjs')); const fn_ = require('./fn.cjs'); console.log(typeof fn_, fn_(), fn_.extra); console.log(require('./reassign.cjs').foo); "#, ); write(dir.path(), "prim.cjs", "module.exports = 42;"); write( dir.path(), "fn.cjs", "module.exports = function() { return 3; }; module.exports.extra = 5;", ); write( dir.path(), "reassign.cjs", "exports.a = 1; module.exports = { foo: 9 };", ); assert_eq!( success(&run(dir.path(), &[], "main.cjs")), "42\nfunction 3 5\n9\n" ); write( dir.path(), "main2.js", r#" import prim from './prim.cjs'; import fn_, { extra } from './fn.cjs'; console.log(prim, fn_(), extra); "#, ); assert_eq!(success(&run(dir.path(), &[], "main2.js")), "42 3 5\n"); } #[test] fn cycles_observe_partial_and_replaced_exports() { let dir = tempfile::tempdir().unwrap(); write( dir.path(), "main.cjs", "console.log(JSON.stringify(require('./a.cjs')));", ); write( dir.path(), "a.cjs", r#" exports.partial = 'a-partial'; const b = require('./b.cjs'); module.exports.done = 'a-done'; module.exports.bSeen = b.partial; "#, ); write( dir.path(), "b.cjs", r#" exports.partial = 'b-partial'; const a = require('./a.cjs'); module.exports = { done: 'b-done', aSeen: a.partial }; "#, ); // `b` replaces its exports during the cycle, dropping `partial`; `a` // observes the replaced object, so `bSeen` is absent (undefined omitted). assert_eq!( success(&run(dir.path(), &[], "main.cjs")), "{\"partial\":\"a-partial\",\"done\":\"a-done\"}\n" ); } #[test] fn lazy_conditional_trycatch_and_failed_retry() { let dir = tempfile::tempdir().unwrap(); write( dir.path(), "main.cjs", r#" globalThis.executed = false; if (false) { require('./lazy.cjs'); } try { require('./missing.cjs'); } catch (e) { console.log('missing'); } try { require('./bad.cjs'); } catch (e) { console.log('bad1', e.message); } try { require('./bad.cjs'); } catch (e) { console.log('bad2', e.message); } console.log('lazy', globalThis.executed); "#, ); write( dir.path(), "lazy.cjs", "globalThis.executed = true; module.exports = 1;", ); write(dir.path(), "bad.cjs", "throw new Error('boom');"); let out = success(&run(dir.path(), &[], "main.cjs")); assert!(out.contains("missing"), "{out}"); assert!( out.contains("bad1 boom") && out.contains("bad2 boom"), "{out}" ); assert!(out.contains("lazy false"), "{out}"); } #[test] fn computed_and_aliased_require_and_resolve_has_no_side_effects() { let dir = tempfile::tempdir().unwrap(); write( dir.path(), "main.cjs", r#" const r = require; const base = './a'; const mod = r(base + '.cjs'); const alias = r('./a.cjs'); console.log(mod === alias, mod.foo); const p = require.resolve('./lazy.cjs'); console.log(p.endsWith('lazy.cjs'), globalThis.lazy === undefined); "#, ); write(dir.path(), "a.cjs", "exports.foo = 11;"); write( dir.path(), "lazy.cjs", "globalThis.lazy = true; module.exports = 1;", ); assert_eq!( success(&run(dir.path(), &[], "main.cjs")), "true 11\ntrue true\n" ); } #[test] fn json_require_and_import_rejection() { let dir = tempfile::tempdir().unwrap(); write(dir.path(), "data.json", "{\"a\": 5}"); write( dir.path(), "main.cjs", "console.log(require('./data.json').a);", ); assert_eq!(success(&run(dir.path(), &[], "main.cjs")), "5\n"); write(dir.path(), "main2.js", "import './data.json';"); assert!( failure(&run(dir.path(), &[], "main2.js")).contains("JSON"), "import JSON stays unsupported" ); write(dir.path(), "bad.json", "{oops"); write( dir.path(), "main3.cjs", "try { require('./bad.json'); } catch (e) { console.log(e.name); }", ); assert_eq!(success(&run(dir.path(), &[], "main3.cjs")), "SyntaxError\n"); } #[test] fn wrapper_this_return_filename_dirname_and_module_fields() { let dir = tempfile::tempdir().unwrap(); write( dir.path(), "main.cjs", r#" console.log(this === exports, typeof require, module.id.endsWith('main.cjs')); console.log(module.filename.endsWith('main.cjs'), module.loaded === false); console.log(__filename.endsWith('main.cjs'), __dirname.length > 0); return; "#, ); // Top-level `return` is legal inside the wrapper; reaching here means it parsed. assert_eq!( success(&run(dir.path(), &[], "main.cjs")), "true function true\ntrue true\ntrue true\n" ); write( dir.path(), "shebang.cjs", "#!/usr/bin/env node\nmodule.exports = 3;", ); write( dir.path(), "main2.cjs", "console.log(require('./shebang.cjs'));", ); assert_eq!(success(&run(dir.path(), &[], "main2.cjs")), "3\n"); } #[test] fn import_from_cjs_uses_correct_referrer() { let dir = tempfile::tempdir().unwrap(); write(dir.path(), "esm.js", "export const x = 9;"); write(dir.path(), "sub/inner.cjs", "module.exports = 1;"); write( dir.path(), "main.cjs", r#" import('./esm.js').then((m) => console.log('dyn', m.x)); import('./sub/inner.cjs').then((m) => console.log('cjs-dyn', m.default)); "#, ); let out = success(&run(dir.path(), &[], "main.cjs")); assert!(out.contains("dyn 9"), "{out}"); assert!(out.contains("cjs-dyn 1"), "{out}"); } #[test] fn require_esm_is_explicitly_unsupported() { let dir = tempfile::tempdir().unwrap(); write(dir.path(), "esm.js", "export const x = 1;"); write( dir.path(), "main.cjs", "try { require('./esm.js'); } catch (e) { console.log(e.message.slice(0, 20)); }", ); let out = success(&run(dir.path(), &[], "main.cjs")); assert!(out.contains("require() of ESM"), "{out}"); } #[test] fn extension_and_directory_lookup() { let dir = tempfile::tempdir().unwrap(); write(dir.path(), "sub/bar.json", "{\"b\": 2}"); write( dir.path(), "main.cjs", "console.log(require('./sub/bar').b, require('./sub/bar.json').b);", ); assert_eq!(success(&run(dir.path(), &[], "main.cjs")), "2 2\n"); write(dir.path(), "sub/foo.js", "export const x = 1;"); write( dir.path(), "main2.cjs", "try { require('./sub/foo'); } catch (e) { console.log(e.message.slice(0, 16)); }", ); assert!( success(&run(dir.path(), &[], "main2.cjs")).contains("require() of ESM"), ".js stays ESM when required" ); fs::create_dir_all(dir.path().join("mydir")).unwrap(); fs::write( dir.path().join("mydir/package.json"), "{\"main\": \"./entry.cjs\"}", ) .unwrap(); write(dir.path(), "mydir/entry.cjs", "module.exports = 99;"); write(dir.path(), "main3.cjs", "console.log(require('./mydir'));"); assert_eq!(success(&run(dir.path(), &[], "main3.cjs")), "99\n"); fs::create_dir_all(dir.path().join("idx")).unwrap(); write(dir.path(), "idx/index.cjs", "module.exports = 100;"); write(dir.path(), "main4.cjs", "console.log(require('./idx'));"); assert_eq!(success(&run(dir.path(), &[], "main4.cjs")), "100\n"); fs::write(dir.path().join("bad.node"), "binary").unwrap(); write( dir.path(), "main5.cjs", "try { require('./bad'); } catch (e) { console.log(e.message.slice(0, 13)); }", ); assert!( success(&run(dir.path(), &[], "main5.cjs")).contains("Native addons"), ".node rejects" ); } #[test] fn malformed_syntax_is_guest_error() { let dir = tempfile::tempdir().unwrap(); write(dir.path(), "bad.cjs", "const = ="); write( dir.path(), "main.cjs", "try { require('./bad.cjs'); } catch (e) { console.log(e.name); }", ); assert_eq!(success(&run(dir.path(), &[], "main.cjs")), "SyntaxError\n"); } #[test] fn cached_require_respects_pledge_and_transitive_denial() { let dir = tempfile::tempdir().unwrap(); write(dir.path(), "a.cjs", "module.exports = 42;"); write( dir.path(), "b.cjs", "const a = require('./a.cjs'); module.exports = a + 1;", ); write( dir.path(), "main.js", r#" import { pledge } from 'avril:std'; import b from './b.cjs'; console.log(b); pledge({jsExec:{deny:['./a.cjs']}}); try { await import('./b.cjs'); console.log('unexpected'); } catch (e) { console.log('denied'); } "#, ); let output = Command::new(env!("CARGO_BIN_EXE_avril")) .arg("run") .current_dir(dir.path()) .arg("main.js") .output() .unwrap(); let out = success(&output); assert_eq!(out, "43\ndenied\n"); } #[test] fn npm_cjs_default_and_named_import_and_require() { let server = Registry::new(); server.publish_npm( "cjslib", "1.0.0", &[ ( "package.json", &manifest("cjslib", "1.0.0", serde_json::json!({"type": "commonjs"})), ), ("index.js", "exports.foo = 1; module.exports.bar = 2;"), ], ); let dir = tempfile::tempdir().unwrap(); fs::write(dir.path().join("avril.conf"), "deps: ['npm:cjslib@1']").unwrap(); write( dir.path(), "main.js", r#" import def, { foo, bar } from 'npm:cjslib@1'; console.log(def.foo, foo, bar); "#, ); assert_eq!( success(&server.run_npm(dir.path(), &["run"], "main.js")), "1 1 2\n" ); write( dir.path(), "main2.cjs", r#" const lib = require('npm:cjslib@1'); console.log(lib.foo, lib.bar); const again = require('npm:cjslib@1'); console.log(lib === again); "#, ); assert_eq!( success(&server.run_npm(dir.path(), &["run"], "main2.cjs")), "1 2\ntrue\n" ); // Offline frozen rerun uses the pinned tree without network. assert_eq!( success(&server.run_npm(dir.path(), &["run", "--offline", "--frozen"], "main2.cjs")), "1 2\ntrue\n" ); } #[test] fn npm_module_entry_and_commonjs_main_keep_distinct_formats() { let server = Registry::new(); server.publish_npm( "dual-entry", "1.0.0", &[ ( "package.json", r#"{"name":"dual-entry","version":"1.0.0","module":"./esm.js","main":"./main.js"}"#, ), ( "esm.js", "import main from './main.js'; export default { value: main.value + 1 };", ), ("main.js", "exports.value = 41;"), ], ); let dir = tempfile::tempdir().unwrap(); write(dir.path(), "avril.conf", "deps: ['npm:dual-entry@1']"); write( dir.path(), "main.js", r#" import direct from 'npm:dual-entry@1/esm.js'; import entry from 'npm:dual-entry@1'; import required from './require.cjs'; const dynamic = (await import('npm:dual-entry@1')).default; console.log(direct === entry, entry.value, required.value, required.denied, dynamic === entry); "#, ); write( dir.path(), "require.cjs", r#" exports.value = require('npm:dual-entry@1').value; try { require('npm:dual-entry@1/esm.js'); } catch (error) { exports.denied = error.message.startsWith('require() of ESM'); } "#, ); for args in [vec!["run"], vec!["run", "--offline", "--frozen"]] { assert_eq!( success(&server.run_npm(dir.path(), &args, "main.js")), "true 42 41 true true\n" ); } } #[test] fn npm_conditional_exports_choose_require_branch() { let server = Registry::new(); server.publish_npm( "cond", "1.0.0", &[ ( "package.json", &manifest( "cond", "1.0.0", serde_json::json!({"exports": {".": {"require": "./c.cjs", "import": "./e.mjs"}}}), ), ), ("c.cjs", "module.exports = 'cjs';"), ("e.mjs", "export default 'esm';"), ], ); let dir = tempfile::tempdir().unwrap(); fs::write(dir.path().join("avril.conf"), "deps: ['npm:cond@1']").unwrap(); write( dir.path(), "main.js", "import def from 'npm:cond@1'; console.log(def);", ); assert_eq!( success(&server.run_npm(dir.path(), &["run"], "main.js")), "esm\n" ); write( dir.path(), "main2.cjs", "console.log(require('npm:cond@1'));", ); assert_eq!( success(&server.run_npm(dir.path(), &["run"], "main2.cjs")), "cjs\n" ); } #[test] fn npm_nested_types_and_relative_lookup() { let server = Registry::new(); server.publish_npm( "nested", "1.0.0", &[ ( "package.json", &manifest("nested", "1.0.0", serde_json::json!({"type": "commonjs"})), ), ("index.js", "module.exports = require('./sub/util');"), ("sub/util.js", "module.exports = 5;"), ("sub/data.json", "{\"v\": 6}"), ("esm/package.json", "{\"type\": \"module\"}"), ("esm/mod.js", "export default 7;"), ], ); let dir = tempfile::tempdir().unwrap(); fs::write(dir.path().join("avril.conf"), "deps: ['npm:nested@1']").unwrap(); write( dir.path(), "main.cjs", r#" console.log(require('npm:nested@1')); console.log(require('npm:nested@1/sub/data.json').v); "#, ); assert_eq!( success(&server.run_npm(dir.path(), &["run"], "main.cjs")), "5\n6\n" ); write( dir.path(), "main2.js", "import mod from 'npm:nested@1/esm/mod.js'; console.log(mod);", ); assert_eq!( success(&server.run_npm(dir.path(), &["run"], "main2.js")), "7\n" ); } #[test] fn npm_json_require_and_unprepared_require_fails_clearly() { let server = Registry::new(); server.publish_npm( "data", "1.0.0", &[ ( "package.json", &manifest( "data", "1.0.0", serde_json::json!({"exports": {".": "./data.json"}}), ), ), ("data.json", "{\"a\": 8}"), ], ); let dir = tempfile::tempdir().unwrap(); fs::write(dir.path().join("avril.conf"), "deps: ['npm:data@1']").unwrap(); write( dir.path(), "main.cjs", "console.log(require('npm:data@1').a);", ); assert_eq!( success(&server.run_npm(dir.path(), &["run"], "main.cjs")), "8\n" ); // Unprepared new npm acquisition via sync require fails without network. let dir2 = tempfile::tempdir().unwrap(); write( dir2.path(), "main.cjs", "try { require('npm:data@1'); } catch (e) { console.log(e.message); }", ); let out = success(&server.run_npm(dir2.path(), &["run"], "main.cjs")); assert!(out.contains("not prepared"), "{out}"); } #[test] fn npm_require_esm_is_unsupported_and_native_rejects() { let server = Registry::new(); server.publish_npm( "esmlib", "1.0.0", &[ ( "package.json", &manifest("esmlib", "1.0.0", serde_json::json!({"type": "module"})), ), ("index.js", "export default 1;"), ], ); server.publish_npm( "nativelib", "1.0.0", &[ ( "package.json", &manifest( "nativelib", "1.0.0", serde_json::json!({"exports": {".": "./binding.node"}}), ), ), ("binding.node", "binary"), ], ); let dir = tempfile::tempdir().unwrap(); fs::write( dir.path().join("avril.conf"), "deps: ['npm:esmlib@1', 'npm:nativelib@1']", ) .unwrap(); write( dir.path(), "main.cjs", "try { require('npm:esmlib@1'); } catch (e) { console.log(e.message.slice(0, 16)); }", ); assert!( success(&server.run_npm(dir.path(), &["run"], "main.cjs")).contains("require() of ESM"), "require(ESM) is explicitly unsupported" ); write( dir.path(), "main2.cjs", "try { require('npm:nativelib@1'); } catch (e) { console.log(e.message.slice(0, 13)); }", ); assert!( success(&server.run_npm(dir.path(), &["run"], "main2.cjs")).contains("Native addons"), "native rejects" ); } #[test] fn reentry_getter_no_panic_and_single_read() { let dir = tempfile::tempdir().unwrap(); write( dir.path(), "a.cjs", r#" let n = 0; Object.defineProperty(module, 'exports', { get() { if (++n === 3) return require('./b.cjs'); return { n }; }}); "#, ); write(dir.path(), "b.cjs", "module.exports = 42;"); write( dir.path(), "main.cjs", "require('./a.cjs'); console.log('second'); console.log(JSON.stringify(require('./a.cjs'))); console.log(require('./a.cjs'));", ); assert_eq!( success(&run(dir.path(), &[], "main.cjs")), "second\n{\"n\":2}\n42\n" ); write( dir.path(), "single.cjs", "let n = 0; Object.defineProperty(module, 'exports', { get() { return ++n; } });", ); write( dir.path(), "main2.cjs", "console.log(require('./single.cjs'));", ); assert_eq!(success(&run(dir.path(), &[], "main2.cjs")), "1\n"); } #[test] fn require_args_are_strings_not_coerced() { let dir = tempfile::tempdir().unwrap(); write(dir.path(), "a.cjs", "module.exports = 1;"); write( dir.path(), "main.cjs", r#" for (const bad of [42, {}, null]) { try { require(bad); console.log('coerced'); } catch (e) { console.log(e.name); } } try { require.resolve(); } catch (e) { console.log(e.name); } "#, ); assert_eq!( success(&run(dir.path(), &[], "main.cjs")), "TypeError\nTypeError\nTypeError\nTypeError\n" ); } #[test] fn prototype_pollution_does_not_fire_on_creation() { let dir = tempfile::tempdir().unwrap(); write(dir.path(), "a.cjs", "module.exports = 7;"); write( dir.path(), "main.cjs", r#" Object.defineProperty(Object.prototype, 'exports', { set(v) { throw new Error('setter fired'); } }); Object.defineProperty(Object.prototype, 'resolve', { set(v) { throw new Error('resolve setter'); } }); console.log(require('./a.cjs')); "#, ); assert_eq!(success(&run(dir.path(), &[], "main.cjs")), "7\n"); } #[test] fn json_semantics_match_v8_not_serde() { let dir = tempfile::tempdir().unwrap(); write(dir.path(), "big.json", "{\"n\":1e999}"); write(dir.path(), "bom.json", "\u{FEFF}{\"a\":1}"); fs::write( dir.path().join("deep.json"), format!("{{\"a\":{}}}", "[".repeat(50) + &"]".repeat(50)), ) .unwrap(); write(dir.path(), "bad.json", "{oops"); write( dir.path(), "main.cjs", r#" console.log(require('./big.json').n); console.log(require('./bom.json').a); console.log(typeof require('./deep.json').a); try { require('./bad.json'); } catch (e) { console.log(e.name); } "#, ); assert_eq!( success(&run(dir.path(), &[], "main.cjs")), "Infinity\n1\nobject\nSyntaxError\n" ); } #[test] fn cjs_source_maps_preserve_throw_lines() { let dir = tempfile::tempdir().unwrap(); write( dir.path(), "a.cjs", "exports.ok = 1;\nthrow new Error('boom');\n", ); write( dir.path(), "main.cjs", "try { require('./a.cjs'); } catch (e) { console.log(e.stack.split('\\n')[1]); }", ); let out = success(&run(dir.path(), &[], "main.cjs")); assert!(out.contains("a.cjs:2:"), "{out}"); write( dir.path(), "b.cts", "const x: number = 1;\nthrow new Error('cts-boom');\n", ); write( dir.path(), "main2.cjs", "try { require('./b.cts'); } catch (e) { console.log(e.stack.split('\\n')[1]); }", ); let out2 = success(&run(dir.path(), &[], "main2.cjs")); assert!(out2.contains("b.cts:2:"), "{out2}"); } #[test] fn local_dotted_basename_and_trailing_slash() { let dir = tempfile::tempdir().unwrap(); write(dir.path(), "a.b.json", "{\"v\":3}"); write(dir.path(), "main.cjs", "console.log(require('./a.b').v);"); assert_eq!(success(&run(dir.path(), &[], "main.cjs")), "3\n"); } #[test] fn esm_intermediary_does_not_hide_revoked_cjs() { let dir = tempfile::tempdir().unwrap(); write(dir.path(), "a.cjs", "module.exports = 1;"); write( dir.path(), "mid.js", "export { default } from './a.cjs'; export { default as v } from './a.cjs';", ); write( dir.path(), "main.js", r#" import { pledge } from 'avril:std'; import { v } from './mid.js'; console.log(v); pledge({jsExec:{deny:['./a.cjs']}}); try { await import('./mid.js'); console.log('unexpected'); } catch (e) { console.log('denied'); } "#, ); let output = Command::new(env!("CARGO_BIN_EXE_avril")) .arg("run") .current_dir(dir.path()) .arg("main.js") .output() .unwrap(); assert_eq!(success(&output), "1\ndenied\n"); } #[test] fn bare_and_aliased_npm_require() { let server = Registry::new(); server.publish_npm( "cjslib", "1.0.0", &[ ( "package.json", &manifest("cjslib", "1.0.0", serde_json::json!({"type": "commonjs"})), ), ("index.js", "module.exports = 21;"), ], ); server.publish_npm( "alib", "1.0.0", &[ ( "package.json", &manifest("alib", "1.0.0", serde_json::json!({"type": "commonjs"})), ), ("index.js", "module.exports = 22;"), ], ); let dir = tempfile::tempdir().unwrap(); fs::write( dir.path().join("avril.conf"), "deps: ['npm:cjslib@1', {from: 'npm:alib@1', name: 'ui'}]", ) .unwrap(); write( dir.path(), "main.cjs", "console.log(require('cjslib'), require('ui'));", ); assert_eq!( success(&server.run_npm(dir.path(), &["run"], "main.cjs")), "21 22\n" ); } #[test] fn fresh_cjs_only_offline_and_cache() { let server = Registry::new(); server.publish_npm( "cjslib", "1.0.0", &[ ( "package.json", &manifest("cjslib", "1.0.0", serde_json::json!({"type": "commonjs"})), ), ("index.js", "module.exports = 33;"), ], ); let dir = tempfile::tempdir().unwrap(); fs::write(dir.path().join("avril.conf"), "deps: ['npm:cjslib@1']").unwrap(); write( dir.path(), "main.cjs", "console.log(require('npm:cjslib@1'));", ); assert_eq!( success(&server.run_npm(dir.path(), &["run"], "main.cjs")), "33\n" ); assert_eq!( success(&server.run_npm(dir.path(), &["run", "--offline", "--frozen"], "main.cjs")), "33\n" ); let cache_out = server.run_npm(dir.path(), &["cache"], "main.cjs"); assert!( cache_out.status.success(), "{}", String::from_utf8_lossy(&cache_out.stderr) ); assert_eq!( success(&server.run_npm(dir.path(), &["run", "--offline", "--frozen"], "main.cjs")), "33\n" ); } #[test] fn npm_legacy_main_without_extension_and_subdir() { let server = Registry::new(); server.publish_npm( "leg", "1.0.0", &[ ( "package.json", &manifest( "leg", "1.0.0", serde_json::json!({"type": "commonjs", "main": "lib/start"}), ), ), ("lib/start.js", "module.exports = 44;"), ("lib/index.js", "module.exports = 45;"), ("sub.js", "module.exports = 46;"), ], ); let dir = tempfile::tempdir().unwrap(); fs::write(dir.path().join("avril.conf"), "deps: ['npm:leg@1']").unwrap(); write( dir.path(), "main.cjs", "console.log(require('npm:leg@1')); console.log(require('npm:leg@1/sub'));", ); assert_eq!( success(&server.run_npm(dir.path(), &["run"], "main.cjs")), "44\n46\n" ); server.publish_npm( "legdir", "1.0.0", &[ ( "package.json", &manifest( "legdir", "1.0.0", serde_json::json!({"type": "commonjs", "main": "lib"}), ), ), ("lib/index.js", "module.exports = 47;"), ], ); let dir2 = tempfile::tempdir().unwrap(); fs::write(dir2.path().join("avril.conf"), "deps: ['npm:legdir@1']").unwrap(); write( dir2.path(), "main.cjs", "console.log(require('npm:legdir@1'));", ); assert_eq!( success(&server.run_npm(dir2.path(), &["run"], "main.cjs")), "47\n" ); } #[test] fn cached_cjs_require_after_pledge_denies() { let dir = tempfile::tempdir().unwrap(); write(dir.path(), "a.cjs", "module.exports = 1;"); write( dir.path(), "main.cjs", r#" console.log(require('./a.cjs')); import('avril:std').then(({ pledge }) => { pledge({ jsExec: { deny: ['./a.cjs'] } }); try { console.log(require('./a.cjs')); } catch (e) { console.log('denied'); } }); "#, ); let out = success(&run(dir.path(), &[], "main.cjs")); assert_eq!(out, "1\ndenied\n"); } #[test] fn named_getter_transitive_denial() { let dir = tempfile::tempdir().unwrap(); write(dir.path(), "b.cjs", "module.exports = 42;"); write( dir.path(), "a.cjs", "Object.defineProperty(exports, 'foo', { enumerable: true, get() { return require('./b.cjs'); } });", ); write( dir.path(), "main.js", r#" import { pledge } from 'avril:std'; import { foo } from './a.cjs'; console.log(foo); pledge({jsExec:{deny:['./b.cjs']}}); try { await import('./a.cjs'); console.log('unexpected'); } catch (e) { console.log('denied'); } "#, ); let output = Command::new(env!("CARGO_BIN_EXE_avril")) .arg("run") .current_dir(dir.path()) .arg("main.js") .output() .unwrap(); assert_eq!(success(&output), "42\ndenied\n"); } #[test] fn npm_cache_path_needs_virtual_authority() { let server = Registry::new(); server.publish_npm( "cjslib", "1.0.0", &[ ( "package.json", &manifest("cjslib", "1.0.0", serde_json::json!({"type": "commonjs"})), ), ("index.js", "module.exports = 1;"), ], ); let dir = tempfile::tempdir().unwrap(); fs::write(dir.path().join("avril.conf"), "deps: ['npm:cjslib@1']").unwrap(); write( dir.path(), "main.cjs", "console.log(require('npm:cjslib@1'));", ); // Local-only grant cannot authorize npm virtual files. let out = Command::new(env!("CARGO_BIN_EXE_avril")) .arg("run") .current_dir(dir.path()) .arg("-0") .arg("--allow-js-exec=./") .arg(format!("--jsr-registry={}", server.url)) .arg(format!("--npm-registry={}", server.url)) .arg(format!( "--cache-dir={}", dir.path().join("cache").display() )) .arg("main.cjs") .output() .unwrap(); assert!( !out.status.success(), "local grant must not authorize npm cache" ); assert!( String::from_utf8_lossy(&out.stderr).contains("PermissionDenied"), "{}", String::from_utf8_lossy(&out.stderr) ); } #[test] fn npm_private_exports_and_imports_reject() { let server = Registry::new(); server.publish_npm( "priv", "1.0.0", &[ ( "package.json", &manifest( "priv", "1.0.0", serde_json::json!({ "type": "commonjs", "exports": { ".": "./index.js" }, "imports": { "#u": "./utils.js" } }), ), ), ("index.js", "module.exports = require('#u');"), ("utils.js", "module.exports = 55;"), ("secret.js", "module.exports = 99;"), ], ); let dir = tempfile::tempdir().unwrap(); fs::write(dir.path().join("avril.conf"), "deps: ['npm:priv@1']").unwrap(); write( dir.path(), "main.cjs", "console.log(require('npm:priv@1'));", ); assert_eq!( success(&server.run_npm(dir.path(), &["run"], "main.cjs")), "55\n" ); write( dir.path(), "main2.cjs", "try { require('npm:priv@1/secret.js'); } catch (e) { console.log('blocked'); }", ); assert_eq!( success(&server.run_npm(dir.path(), &["run"], "main2.cjs")), "blocked\n" ); } #[test] fn npm_imports_choose_require_branch() { let server = Registry::new(); server.publish_npm( "imp", "1.0.0", &[ ( "package.json", &manifest( "imp", "1.0.0", serde_json::json!({ "type": "commonjs", "imports": { "#x": { "require": "./c.cjs", "import": "./e.mjs" } } }), ), ), ("index.js", "module.exports = require('#x');"), ("c.cjs", "module.exports = 'c';"), ("e.mjs", "export default 'e';"), ], ); let dir = tempfile::tempdir().unwrap(); fs::write(dir.path().join("avril.conf"), "deps: ['npm:imp@1']").unwrap(); write(dir.path(), "main.cjs", "console.log(require('npm:imp@1'));"); assert_eq!( success(&server.run_npm(dir.path(), &["run"], "main.cjs")), "c\n" ); } #[test] fn import_map_mapped_require() { let dir = tempfile::tempdir().unwrap(); fs::write( dir.path().join("avril.conf"), "imports: { a: './a.cjs', dir: './mydir', alias: './b.cjs' }", ) .unwrap(); write(dir.path(), "a.cjs", "module.exports = 42;"); write(dir.path(), "b.cjs", "module.exports = 43;"); fs::create_dir_all(dir.path().join("mydir")).unwrap(); fs::write( dir.path().join("mydir/package.json"), "{\"main\":\"./entry.cjs\"}", ) .unwrap(); write(dir.path(), "mydir/entry.cjs", "module.exports = 44;"); write( dir.path(), "main.cjs", "console.log(require('a'), require('dir'), require('alias'));", ); assert_eq!(success(&run(dir.path(), &[], "main.cjs")), "42 44 43\n"); // Symlink identity: symlinked file resolves to canonical target. #[cfg(unix)] { std::os::unix::fs::symlink(dir.path().join("a.cjs"), dir.path().join("link.cjs")).unwrap(); write( dir.path(), "main2.cjs", "console.log(require('./link.cjs') === require('./a.cjs'));", ); assert_eq!(success(&run(dir.path(), &[], "main2.cjs")), "true\n"); } let dir2 = tempfile::tempdir().unwrap(); fs::write( dir2.path().join("avril.conf"), "imports: { bad: 'ext:foo', nd: 'node:fs' }", ) .unwrap(); write( dir2.path(), "main.cjs", "try { require('bad'); } catch (e) { console.log('denied-ext'); } try { require('nd'); } catch (e) { console.log('denied-node'); }", ); assert_eq!( success(&run(dir2.path(), &[], "main.cjs")), "denied-ext\ndenied-node\n" ); } #[test] fn local_trailing_slash_directory() { let dir = tempfile::tempdir().unwrap(); fs::create_dir_all(dir.path().join("idx")).unwrap(); write(dir.path(), "idx/index.cjs", "module.exports = 100;"); write(dir.path(), "main.cjs", "console.log(require('./idx/'));"); assert_eq!(success(&run(dir.path(), &[], "main.cjs")), "100\n"); } #[test] fn denied_getter_not_executed() { let dir = tempfile::tempdir().unwrap(); write(dir.path(), "b.cjs", "module.exports = 1;"); write( dir.path(), "a.cjs", r#" const b = require('./b.cjs'); globalThis.count = 0; Object.defineProperty(module, 'exports', { enumerable: true, get() { globalThis.count++; return { b }; } }); "#, ); write(dir.path(), "require.cjs", "module.exports = require;"); write( dir.path(), "main.js", r#" import { pledge } from 'avril:std'; const r = (await import('./require.cjs')).default; console.log(r('./a.cjs').b, globalThis.count); pledge({jsExec:{deny:['./b.cjs']}}); try { r('./a.cjs'); console.log('unexpected'); } catch (e) { console.log('denied', globalThis.count); } "#, ); let output = Command::new(env!("CARGO_BIN_EXE_avril")) .arg("run") .current_dir(dir.path()) .arg("main.js") .output() .unwrap(); assert_eq!(success(&output), "1 1\ndenied 1\n"); } #[test] fn getter_invokes_pledge() { let dir = tempfile::tempdir().unwrap(); write(dir.path(), "b.cjs", "module.exports = 2;"); write( dir.path(), "a.cjs", r#" const cached = require('./b.cjs'); Object.defineProperty(module, 'exports', { enumerable: true, get() { globalThis.pledge({ jsExec: { deny: ['./b.cjs'] } }); return cached; }}); "#, ); write( dir.path(), "named.cjs", r#" const cached = require('./b.cjs'); Object.defineProperty(exports, 'foo', { enumerable: true, get() { globalThis.pledge({ jsExec: { deny: ['./b.cjs'] } }); return cached; }}); "#, ); write( dir.path(), "main.js", r#" import { pledge } from 'avril:std'; globalThis.pledge = pledge; try { (await import('./a.cjs')).default; console.log('unexpected'); } catch (e) { console.log('denied-module'); } globalThis.pledge = pledge; try { (await import('./named.cjs')).foo; console.log('unexpected'); } catch (e) { console.log('denied-named'); } "#, ); let output = Command::new(env!("CARGO_BIN_EXE_avril")) .arg("run") .current_dir(dir.path()) .arg("main.js") .output() .unwrap(); assert_eq!(success(&output), "denied-module\ndenied-named\n"); } #[test] fn npm_root_and_trailing_require() { let server = Registry::new(); server.publish_npm( "rootpkg", "1.0.0", &[ ( "package.json", &manifest( "rootpkg", "1.0.0", serde_json::json!({"type": "commonjs", "main": "./index.js"}), ), ), ("index.js", "module.exports = { value: 61 };"), ("helper.cjs", "module.exports = require('./');"), ], ); let dir = tempfile::tempdir().unwrap(); fs::write(dir.path().join("avril.conf"), "deps: ['npm:rootpkg@1']").unwrap(); write( dir.path(), "main.cjs", "const h = require('npm:rootpkg@1/helper.cjs'); const r = require('npm:rootpkg@1'); console.log(h === r, h.value);", ); assert_eq!( success(&server.run_npm(dir.path(), &["run"], "main.cjs")), "true 61\n" ); server.publish_npm( "dirpkg", "1.0.0", &[ ( "package.json", &manifest("dirpkg", "1.0.0", serde_json::json!({"type": "commonjs"})), ), ("index.js", "module.exports = require('./sub/');"), ("sub/package.json", "{\"main\": \"./inner.js\"}"), ("sub/inner.js", "module.exports = 63;"), ], ); let dir2 = tempfile::tempdir().unwrap(); fs::write(dir2.path().join("avril.conf"), "deps: ['npm:dirpkg@1']").unwrap(); write( dir2.path(), "main.cjs", "console.log(require('npm:dirpkg@1'));\n console.log(require('npm:dirpkg@1/sub/'));", ); assert_eq!( success(&server.run_npm(dir2.path(), &["run"], "main.cjs")), "63\n63\n" ); } #[test] fn npm_malformed_and_tampered_metadata_fail() { let server = Registry::new(); server.publish_npm( "badmeta", "1.0.0", &[ ( "package.json", &manifest("badmeta", "1.0.0", serde_json::json!({"type": "commonjs"})), ), ("index.js", "module.exports = require('./sub');"), ("sub/package.json", "{oops"), ("sub/index.js", "module.exports = 1;"), ], ); let dir = tempfile::tempdir().unwrap(); fs::write(dir.path().join("avril.conf"), "deps: ['npm:badmeta@1']").unwrap(); write( dir.path(), "main.cjs", "console.log(require('npm:badmeta@1'));", ); // Malformed nested metadata fails when consumed, not via fallback. assert!( failure(&server.run_npm(dir.path(), &["run"], "main.cjs")).contains("Invalid nested"), "malformed nested package.json must propagate" ); // Post-acquisition tampering fails verification on use. let server2 = Registry::new(); server2.publish_npm( "tamper", "1.0.0", &[ ( "package.json", &manifest("tamper", "1.0.0", serde_json::json!({"type": "commonjs"})), ), ("index.js", "module.exports = 1;"), ], ); let dir3 = tempfile::tempdir().unwrap(); fs::write(dir3.path().join("avril.conf"), "deps: ['npm:tamper@1']").unwrap(); write( dir3.path(), "main.cjs", "console.log(require('npm:tamper@1'));", ); assert_eq!( success(&server2.run_npm(dir3.path(), &["run"], "main.cjs")), "1\n" ); // Corrupt one materialized file; next use must fail integrity, not fallback. fn find_index(dir: &Path) -> Option { let mut stack = vec![dir.to_owned()]; while let Some(d) = stack.pop() { for e in fs::read_dir(&d).ok()? { let e = e.ok()?; let path = e.path(); if path.is_dir() { stack.push(path); } else if path.file_name().and_then(|n| n.to_str()) == Some("index.js") { return Some(path); } } } None } let tampered_path = find_index(&dir3.path().join(".avril/npm")).expect("expected materialized index.js"); fs::write(&tampered_path, "module.exports = 2; // tampered").unwrap(); assert!( failure(&server2.run_npm(dir3.path(), &["run"], "main.cjs")).contains("integrity"), "tampered npm file must fail verification" ); } #[test] fn npm_mapping_leakage_and_peer_cjs_identity() { let server = Registry::new(); server.publish_npm( "peer-dep", "1.0.0", &[ ( "package.json", &manifest("peer-dep", "1.0.0", serde_json::json!({"type": "commonjs"})), ), ("index.js", "module.exports = { tag: 'peer' };"), ], ); server.publish_npm( "has-peer", "1.0.0", &[ ( "package.json", &manifest( "has-peer", "1.0.0", serde_json::json!({ "type": "commonjs", "peerDependencies": { "peer-dep": "1" } }), ), ), ("index.js", "module.exports = require('peer-dep');"), ], ); let dir = tempfile::tempdir().unwrap(); fs::write( dir.path().join("avril.conf"), "deps: ['npm:has-peer@1', 'npm:peer-dep@1'], imports: { mapped: './local.cjs' }", ) .unwrap(); write(dir.path(), "local.cjs", "module.exports = 72;"); write( dir.path(), "main.cjs", "console.log(require('npm:has-peer@1').tag, require('mapped')); console.log(require('npm:has-peer@1') === require('npm:peer-dep@1'));", ); assert_eq!( success(&server.run_npm(dir.path(), &["run"], "main.cjs")), "peer 72\ntrue\n" ); // npm-owned file must not use project import map: publish npm file requiring 'mapped' should fail (not a declared dep). server.publish_npm( "leaky", "1.0.0", &[ ( "package.json", &manifest("leaky", "1.0.0", serde_json::json!({"type": "commonjs"})), ), ("index.js", "module.exports = require('mapped');"), ], ); let dir2 = tempfile::tempdir().unwrap(); fs::write( dir2.path().join("avril.conf"), "deps: ['npm:leaky@1'], imports: { mapped: './local.cjs' }", ) .unwrap(); write(dir2.path(), "local.cjs", "module.exports = 1;"); write( dir2.path(), "main.cjs", "try { require('npm:leaky@1'); console.log('unexpected'); } catch (e) { console.log('blocked'); }", ); assert_eq!( success(&server.run_npm(dir2.path(), &["run"], "main.cjs")), "blocked\n" ); } #[test] fn cts_erased_positions_and_shebang() { let dir = tempfile::tempdir().unwrap(); write( dir.path(), "a.cts", "#!/usr/bin/env node\ntype A = { x: number };\nenum E { A, B }\nconst v: A = { x: 1 };\nthrow new Error('cts-boom');\n", ); write( dir.path(), "main.cjs", "try { require('./a.cts'); } catch (e) { console.log(e.stack.split('\\n')[1]); }", ); let out = success(&run(dir.path(), &[], "main.cjs")); assert!(out.contains("a.cts:5:"), "{out}"); } #[test] fn cjs_snapshot_stable_across_mutation() { let dir = tempfile::tempdir().unwrap(); write(dir.path(), "a.cjs", "exports.foo = 1;"); write( dir.path(), "mutate.js", r#" import { writeTextFile } from 'avril:fs'; await writeTextFile('./a.cjs', 'exports.bar = 2;'); "#, ); write( dir.path(), "main.js", r#" import './mutate.js'; import def, { foo } from './a.cjs'; console.log(def.foo, foo, 'bar' in def); "#, ); // Link stores foo from prepared snapshot; mutate rewrites before CJS eval, // but eval uses prepared snapshot + stored names, so no fatal and foo survives. assert_eq!(success(&run(dir.path(), &[], "main.js")), "1 1 false\n"); } #[test] fn fresh_cache_in_distinct_project_offline() { let server = Registry::new(); server.publish_npm( "cjslib2", "1.0.0", &[ ( "package.json", &manifest("cjslib2", "1.0.0", serde_json::json!({"type": "commonjs"})), ), ("index.js", "module.exports = 77;"), ], ); let dir = tempfile::tempdir().unwrap(); fs::write(dir.path().join("avril.conf"), "deps: ['npm:cjslib2@1']").unwrap(); write( dir.path(), "main.cjs", "console.log(require('npm:cjslib2@1'));", ); let cache_out = server.run_npm(dir.path(), &["cache"], "main.cjs"); assert!( cache_out.status.success(), "{}", String::from_utf8_lossy(&cache_out.stderr) ); assert_eq!( success(&server.run_npm(dir.path(), &["run", "--offline", "--frozen"], "main.cjs")), "77\n" ); } #[test] fn npm_absolute_paths_share_identity_and_block_escape() { let server = Registry::new(); server.publish_npm( "abspkg", "1.0.0", &[ ( "package.json", &manifest("abspkg", "1.0.0", serde_json::json!({"type": "commonjs"})), ), ("index.js", "module.exports = { v: 81 };"), ("data.js", "module.exports = { v: 82 };"), ( "caller.js", "const rel = require('./data.js'); const viaResolve = require(require.resolve('./data.js')); const viaDir = require(__dirname + '/data.js'); let blocked = false; try { require('/etc/passwd'); } catch (_) { blocked = true; } console.log(rel === viaResolve, rel === viaDir, rel.v, blocked); module.exports = { rel, viaResolve };", ), ], ); let dir_a = tempfile::tempdir().unwrap(); fs::write(dir_a.path().join("avril.conf"), "deps: ['npm:abspkg@1']").unwrap(); write( dir_a.path(), "main.cjs", "const a = require('npm:abspkg@1/data.js'); const p = require.resolve('npm:abspkg@1/data.js'); const b = require(p); console.log(a === b, a.v);", ); assert_eq!( success(&server.run_npm(dir_a.path(), &["run"], "main.cjs")), "true 82\n" ); write( dir_a.path(), "main2.cjs", "const c = require('npm:abspkg@1/caller.js'); console.log(c.rel.v);", ); // Within-instance require.resolve('./data') + __dirname absolute share identity; outside absolute blocked. assert_eq!( success(&server.run_npm(dir_a.path(), &["run"], "main2.cjs")), "true true 82 true\n82\n" ); } #[test] fn npm_cached_absolute_after_pledge() { let server = Registry::new(); server.publish_npm( "pledgeabs", "1.0.0", &[ ( "package.json", &manifest( "pledgeabs", "1.0.0", serde_json::json!({"type": "commonjs"}), ), ), ("index.js", "module.exports = require('./data.js');"), ("data.js", "module.exports = { v: 83 };"), ], ); let dir = tempfile::tempdir().unwrap(); fs::write(dir.path().join("avril.conf"), "deps: ['npm:pledgeabs@1']").unwrap(); write(dir.path(), "require.cjs", "module.exports = require;"); write( dir.path(), "main.js", r#" import { pledge } from 'avril:std'; const r = (await import('./require.cjs')).default; const first = r('npm:pledgeabs@1/data.js'); const abs = r.resolve('npm:pledgeabs@1/data.js'); console.log(first.v, first === r(abs)); pledge({ jsExec: false }); try { r(abs); console.log('unexpected'); } catch (e) { console.log('denied'); } "#, ); assert_eq!( success(&server.run_npm(dir.path(), &["run"], "main.js")), "83 true\ndenied\n" ); } #[test] fn npm_dot_main_and_nested_dot_fallback() { let server = Registry::new(); server.publish_npm( "dotmain", "1.0.0", &[ ( "package.json", &manifest( "dotmain", "1.0.0", serde_json::json!({"type": "commonjs", "main": "."}), ), ), ("index.js", "module.exports = 91;"), ], ); server.publish_npm( "dotdir", "1.0.0", &[ ( "package.json", &manifest("dotdir", "1.0.0", serde_json::json!({"type": "commonjs"})), ), ("index.js", "module.exports = require('./sub');"), ("sub/package.json", "{\"main\": \".\"}"), ("sub/index.js", "module.exports = 92;"), ], ); let dir = tempfile::tempdir().unwrap(); fs::write( dir.path().join("avril.conf"), "deps: ['npm:dotmain@1', 'npm:dotdir@1']", ) .unwrap(); write( dir.path(), "main.cjs", "console.log(require('npm:dotmain@1'), require('npm:dotdir@1'));", ); assert_eq!( success(&server.run_npm(dir.path(), &["run"], "main.cjs")), "91 92\n" ); } #[test] fn npm_same_graph_tampered_metadata_fails_not_fallback() { let server = Registry::new(); server.publish_npm( "metagraph2", "1.0.0", &[ ( "package.json", &manifest("metagraph2", "1.0.0", serde_json::json!({"type": "commonjs"})), ), ( "index.js", r#"module.exports.run = () => { const pkg = require.resolve('./sub/package.json'); globalThis.syncWrite(pkg, '{oops'); return require('./sub'); };"#, ), ("sub/package.json", "{\"main\": \"./inner.js\"}"), ("sub/inner.js", "module.exports = 1;"), ("sub/index.js", "module.exports = 2;"), ], ); let dir = tempfile::tempdir().unwrap(); fs::write(dir.path().join("avril.conf"), "deps: ['npm:metagraph2@1']").unwrap(); write( dir.path(), "main.js", r#" import { writeTextFile } from 'avril:fs/sync'; globalThis.syncWrite = writeTextFile; const pkg = (await import('npm:metagraph2@1')).default; try { pkg.run(); console.log('unexpected'); } catch (e) { console.log(e.message.includes('Invalid nested') || e.message.includes('integrity') ? 'failed' : 'other:' + e.message); } "#, ); assert_eq!( success(&server.run_npm(dir.path(), &["run"], "main.js")), "failed\n" ); } #[test] fn local_symlink_npm_metadata_denied_and_tamper_rejected() { let server = Registry::new(); server.publish_npm( "metapkg", "1.0.0", &[ ( "package.json", &manifest( "metapkg", "1.0.0", serde_json::json!({"type": "commonjs", "main": "./entry.js"}), ), ), ("entry.js", "module.exports = 91;"), ], ); let dir = tempfile::tempdir().unwrap(); fs::write(dir.path().join("avril.conf"), "deps: ['npm:metapkg@1']").unwrap(); write( dir.path(), "main.cjs", "console.log(require('npm:metapkg@1'));", ); assert_eq!( success(&server.run_npm(dir.path(), &["run"], "main.cjs")), "91\n" ); fn find_manifest(root: &Path) -> Option { let mut stack = vec![root.join(".avril/npm")]; while let Some(d) = stack.pop() { for e in fs::read_dir(&d).ok()? { let e = e.ok()?; let path = e.path(); if path.is_dir() { stack.push(path); } else if path.file_name().and_then(|n| n.to_str()) == Some("package.json") && fs::read_to_string(&path).ok()?.contains("\"metapkg\"") { return Some(path); } } } None } let manifest_path = find_manifest(dir.path()).expect("expected materialized npm manifest"); #[cfg(unix)] { fs::create_dir_all(dir.path().join("aliased")).unwrap(); std::os::unix::fs::symlink(&manifest_path, dir.path().join("aliased/package.json")) .unwrap(); write( dir.path(), "main2.cjs", "try { console.log(require('./aliased')); } catch (e) { console.log(e.message.includes('PermissionDenied') || e.message.includes('not owned') ? 'denied' : 'other:' + e.message); }", ); // No npm deps in config so prepare succeeds with local-only grant; // aliased metadata read must still deny (no virtual authority / unowned). fs::write(dir.path().join("avril.conf"), "deps: []").unwrap(); let out = Command::new(env!("CARGO_BIN_EXE_avril")) .arg("run") .current_dir(dir.path()) .arg("-0") .arg("--allow-js-exec=./") .arg(format!("--jsr-registry={}", server.url)) .arg(format!("--npm-registry={}", server.url)) .arg(format!( "--cache-dir={}", dir.path().join("cache").display() )) .arg("main2.cjs") .output() .unwrap(); assert_eq!(success(&out), "denied\n"); // Restore dep so npm instance is prepared; tampered manifest must fail integrity, not fallback. fs::write(dir.path().join("avril.conf"), "deps: ['npm:metapkg@1']").unwrap(); fs::write(&manifest_path, "{\"name\":\"metapkg\",\"version\":\"1.0.0\",\"main\":\"./entry.js\",\"tampered\":true}").unwrap(); write( dir.path(), "main3.cjs", "console.log(require('./aliased'));", ); assert!( failure(&server.run_npm(dir.path(), &["run"], "main3.cjs")).contains("integrity"), "tampered aliased metadata must fail verification" ); } }