char/avril
(wip) why'd you have to go and make things so complicaaaaated?
git clone https://git.t4t.associates/char/avril
7325e8c
main
1# [ path = "common/registry.rs" ] 2mod fixture; 3 4use std:: fs; 5use std:: path:: Path ; 6use std:: process:: Command ; 7 8use fixture::{ Registry , failure, success}; 9 10# [ test ] 11fn jsr_and_npm_registries_are_independent_and_persisted () { 12let jsr =Registry :: new (); 13let npm =Registry :: new (); 14 jsr. package ( 15"@test/jsr" , 16"1.0.0" , 17& [( "mod.ts" , "export default 'jsr';" )], 18& [( "." , "./mod.ts" )], 19); 20 npm. publish_package ( 21"npm-only" , 22"1.0.0" , 23 serde_json:: json!({}), 24& [( "index.js" , "export default 'npm';" )], 25); 26let dir = tempfile:: tempdir (). unwrap (); 27 fs:: write ( 28 dir. path (). join ( "main.ts" ), 29"import jsr from 'jsr:@test/jsr@1'; import npm from 'npm:npm-only@1'; console.log(jsr, npm);" , 30) 31. unwrap (); 32let run = |jsr_url : & url:: Url , npm_url : & url:: Url , args : & [ & str ] |{ 33Command :: new ( env! ( "CARGO_BIN_EXE_avril" )) 34. current_dir ( dir. path ()) 35. arg ( "run" ) 36. arg ( format! ( "--jsr-registry={jsr_url}" )) 37. arg ( format! ( "--npm-registry={npm_url}" )) 38. arg ( "--cache-dir=cache" ) 39. args ( args) 40. arg ( "main.ts" ) 41. output () 42. unwrap () 43}; 44assert_eq! ( success ( & run ( & jsr. url , & npm. url , & [])), "jsr npm\n" ); 45let lock = fixture:: read_lock ( dir. path ()); 46assert_eq! ( lock[ "jsrRegistry" ], jsr. url . as_str ()); 47assert_eq! ( lock[ "npmRegistry" ], npm. url . as_str ()); 48for old_namein [ "registry" , "jsr_registry" , "npm_registry" ] { 49assert! ( lock. get ( old_name). is_none ()); 50} 51let jsr_requests = jsr. requests . lock (). unwrap (). len (); 52let npm_requests = npm. requests . lock (). unwrap (). len (); 53assert! ( jsr_requests >0 && npm_requests >0 ); 54assert_eq! ( 55success ( & run ( & jsr. url , & npm. url , & [ "--offline" , "--frozen" ])), 56"jsr npm\n" 57); 58assert! ( failure ( & run ( & npm. url , & npm. url , & [ "--offline" ])). contains ( "JSR registry" )); 59assert! ( failure ( & run ( & jsr. url , & jsr. url , & [ "--offline" ])). contains ( "npm registry" )); 60assert_eq! ( jsr. requests . lock (). unwrap (). len (), jsr_requests); 61assert_eq! ( npm. requests . lock (). unwrap (). len (), npm_requests); 62} 63 64# [ test ] 65fn scoped_and_unscoped_config_dependencies_run () { 66let server =Registry :: new (); 67 server. publish_package ( 68"leftpad" , 69"1.0.0" , 70 serde_json:: json!({}), 71& [( "index.js" , "export default 'pad';" )], 72); 73 server. publish_package ( 74"@scope/widget" , 75"2.0.0" , 76 serde_json:: json!({}), 77& [( "index.js" , "export default 'widget';" )], 78); 79let dir = tempfile:: tempdir (). unwrap (); 80 fs:: write ( 81 dir. path (). join ( "avril.conf" ), 82"deps: ['npm:leftpad@1', 'npm:@scope/widget@^2']" , 83) 84. unwrap (); 85 fs:: write ( 86 dir. path (). join ( "main.ts" ), 87"import pad from 'leftpad'; import widget from '@scope/widget'; console.log(pad, widget);" , 88) 89. unwrap (); 90assert_eq! ( 91success ( & server. run_npm ( dir. path (), & [ "run" ], "main.ts" )), 92"pad widget\n" 93); 94} 95 96# [ test ] 97fn import_map_aliases_and_prefixes_reach_subpaths () { 98let server =Registry :: new (); 99 server. publish_package ( 100"real" , 101"1.0.0" , 102 serde_json:: json!({ "exports" : { " . " : "./mod.js" , "./sub" : "./sub.js" }}), 103& [ 104( "mod.js" , "export default 'mod';" ), 105( "sub.js" , "export default 'sub';" ), 106], 107); 108let dir = tempfile:: tempdir (). unwrap (); 109 fs:: write ( 110 dir. path (). join ( "avril.conf" ), 111"imports: {alias: 'npm:real@1', 'pref/': 'npm:real@1/'}" , 112) 113. unwrap (); 114 fs:: write ( 115 dir. path (). join ( "main.ts" ), 116"import a from 'alias'; import b from 'pref/sub'; console.log(a, b);" , 117) 118. unwrap (); 119assert_eq! ( 120success ( & server. run_npm ( dir. path (), & [ "run" ], "main.ts" )), 121"mod sub\n" 122); 123} 124 125# [ test ] 126fn transitive_nested_esm_imports_coexist () { 127let server =Registry :: new (); 128 server. publish_package ( 129"util" , 130"1.0.0" , 131 serde_json:: json!({}), 132& [( "index.js" , "export const u = 'u1';" )], 133); 134 server. publish_package ( 135"util" , 136"2.0.0" , 137 serde_json:: json!({}), 138& [( "index.js" , "export const u = 'u2';" )], 139); 140 server. publish_package ( 141"lib" , 142"1.0.0" , 143 serde_json:: json!({ "dependencies" : { "util" : "^2.0.0" }}), 144& [ 145( 146"index.js" , 147"export {v as nested} from './deep/nested.js'; export {u} from 'util';" , 148), 149( "deep/nested.js" , "export {v} from './sibling.js';" ), 150( "deep/sibling.js" , "export {v} from '../shared.js';" ), 151( "shared.js" , "export const v = 'shared';" ), 152], 153); 154 server. publish_package ( 155"app" , 156"1.0.0" , 157 serde_json:: json!({ "dependencies" : { "lib" : "^1.0.0" , "util" : "^1.0.0" }}), 158& [( 159"index.js" , 160"export * from 'lib'; export {u as u1} from 'util';" , 161)], 162); 163let dir = tempfile:: tempdir (). unwrap (); 164 fs:: write ( dir. path (). join ( "avril.conf" ), "deps: ['npm:app@1']" ). unwrap (); 165 fs:: write ( 166 dir. path (). join ( "main.ts" ), 167"import {nested, u, u1} from 'app'; console.log(nested, u, u1);" , 168) 169. unwrap (); 170assert_eq! ( 171success ( & server. run_npm ( dir. path (), & [ "run" ], "main.ts" )), 172"shared u2 u1\n" 173); 174} 175 176# [ test ] 177fn dynamic_imports_share_namespaces_concurrently () { 178let server =Registry :: new (); 179 server. publish_package ( 180"dyn" , 181"1.0.0" , 182 serde_json:: json!({}), 183& [( "index.js" , "export const value = 7;" )], 184); 185let dir = tempfile:: tempdir (). unwrap (); 186 fs:: write ( 187 dir. path (). join ( "main.ts" ), 188"const [a, b] = await Promise.all([import('npm:dyn@1'), import('npm:dyn@1')]); console.log(a === b, a.value);" , 189) 190. unwrap (); 191assert_eq! ( 192success ( & server. run_npm ( dir. path (), & [ "run" ], "main.ts" )), 193"true 7\n" 194); 195} 196 197# [ test ] 198fn peer_auto_install_shares_explicit_providers () { 199let server =Registry :: new (); 200 server. publish_package ( 201"react" , 202"18.2.0" , 203 serde_json:: json!({}), 204& [ 205( 206"index.js" , 207"globalThis.count = (globalThis.count ?? 0) + 1; export const count = globalThis.count;" , 208), 209], 210); 211 server. publish_package ( 212"plugin" , 213"1.0.0" , 214 serde_json:: json!({ "peerDependencies" : { "react" : "^18.0.0" }}), 215& [( "index.js" , "export {count} from 'react';" )], 216); 217let dir = tempfile:: tempdir (). unwrap (); 218 fs:: write ( 219 dir. path (). join ( "avril.conf" ), 220"deps: ['npm:react@18.2.0', 'npm:plugin@1']" , 221) 222. unwrap (); 223 fs:: write ( 224 dir. path (). join ( "main.ts" ), 225"import {count as direct} from 'react'; import {count as via} from 'plugin'; console.log(direct, via);" , 226) 227. unwrap (); 228assert_eq! ( 229success ( & server. run_npm ( dir. path (), & [ "run" ], "main.ts" )), 230"1 1\n" 231); 232} 233 234# [ test ] 235fn direct_imports_preselect_peers_without_config () { 236let server =Registry :: new (); 237 server. publish_package ( 238"host" , 239"1.0.0" , 240 serde_json:: json!({}), 241& [( "index.js" , "export const host = 'h1';" )], 242); 243 server. publish_package ( 244"plugin" , 245"1.0.0" , 246 serde_json:: json!({ "peerDependencies" : { "host" : "^1.0.0" }}), 247& [( "index.js" , "export {host} from 'host';" )], 248); 249let dir = tempfile:: tempdir (). unwrap (); 250 fs:: write ( 251 dir. path (). join ( "main.ts" ), 252"import {host} from 'npm:plugin@1'; import {host as direct} from 'npm:host@1.0.0'; console.log(host, direct);" , 253) 254. unwrap (); 255assert_eq! ( 256success ( & server. run_npm ( dir. path (), & [ "run" ], "main.ts" )), 257"h1 h1\n" 258); 259} 260 261# [ test ] 262fn two_configs_isolate_peer_environments () { 263let server =Registry :: new (); 264for versionin [ "1.0.0" , "2.0.0" ] { 265 server. publish_package ( 266"host" , 267 version, 268 serde_json:: json!({}), 269& [( "index.js" , & format! ( "export const version = '{version}';" ))], 270); 271} 272 server. publish_package ( 273"plugin" , 274"1.0.0" , 275 serde_json:: json!({ "peerDependencies" : { "host" : "*" }}), 276& [( "index.js" , "export {version} from 'host';" )], 277); 278let dir = tempfile:: tempdir (). unwrap (); 279for ( package, host) in [( "a" , "1.0.0" ), ( "b" , "2.0.0" )] { 280let root = dir. path (). join ( package); 281 fs:: create_dir_all ( & root). unwrap (); 282 fs:: write ( 283 root. join ( "avril.conf" ), 284format! ( "deps: ['npm:host@{host}', 'npm:plugin@1']" ), 285) 286. unwrap (); 287 fs:: write ( 288 root. join ( "main.ts" ), 289"import {version} from 'plugin'; console.log(version);" , 290) 291. unwrap (); 292} 293 fs:: write ( 294 dir. path (). join ( "main.ts" ), 295"import './a/main.ts'; import './b/main.ts';" , 296) 297. unwrap (); 298assert_eq! ( 299success ( & server. run_npm ( dir. path (), & [ "run" ], "main.ts" )), 300"1.0.0\n2.0.0\n" 301); 302} 303 304# [ test ] 305fn shadowed_peers_use_the_parent_host_instance () { 306let server =Registry :: new (); 307 server. publish_package ( 308"x" , 309"1.0.0" , 310 serde_json:: json!({}), 311& [( "index.js" , "export const x = 'x1';" )], 312); 313 server. publish_package ( 314"x" , 315"2.0.0" , 316 serde_json:: json!({}), 317& [( "index.js" , "export const x = 'x2';" )], 318); 319 server. publish_package ( 320"host" , 321"1.0.0" , 322 serde_json:: json!({ "peerDependencies" : { "x" : "^1.0.0" }}), 323& [( "index.js" , "export {x} from 'x';" )], 324); 325 server. publish_package ( 326"plugin" , 327"1.0.0" , 328 serde_json:: json!({ "dependencies" : { "host" : "^1.0.0" , "x" : "^2.0.0" }}), 329& [( 330"index.js" , 331"export {x as own} from 'x'; export {x as hosted} from 'host';" , 332)], 333); 334let dir = tempfile:: tempdir (). unwrap (); 335 fs:: write ( dir. path (). join ( "avril.conf" ), "deps: ['npm:plugin@1']" ). unwrap (); 336 fs:: write ( 337 dir. path (). join ( "main.ts" ), 338"import {own, hosted} from 'plugin'; console.log(own, hosted);" , 339) 340. unwrap (); 341assert_eq! ( 342success ( & server. run_npm ( dir. path (), & [ "run" ], "main.ts" )), 343"x2 x1\n" 344); 345} 346 347# [ test ] 348fn typescript_sources_transpile () { 349let server =Registry :: new (); 350 server. publish_package ( 351"typed" , 352"1.0.0" , 353 serde_json:: json!({ "exports" : { " . " : "./index.ts" }}), 354& [( "index.ts" , "export const value: number = 40 + 2;" )], 355); 356let dir = tempfile:: tempdir (). unwrap (); 357 fs:: write ( 358 dir. path (). join ( "main.ts" ), 359"import {value} from 'npm:typed@1'; console.log(value);" , 360) 361. unwrap (); 362 363assert_eq! ( 364success ( & server. run_npm ( dir. path (), & [ "run" ], "main.ts" )), 365"42\n" 366); 367} 368 369# [ test ] 370fn unsupported_kinds_fail_with_clear_errors () { 371let server =Registry :: new (); 372 server. publish_package ( 373"cjspkg" , 374"1.0.0" , 375 serde_json:: json!({ "type" : "commonjs" }), 376& [( "index.js" , "module.exports = 1;" )], 377); 378 server. publish_package ( 379"data" , 380"1.0.0" , 381 serde_json:: json!({ "exports" : { " . " : "./data.json" }}), 382& [( "data.json" , "{\"a\": 1}" )], 383); 384let dir = tempfile:: tempdir (). unwrap (); 385// CommonJS is now supported via ESM wrappers and `require`; JSON imports 386// and Node builtins stay unsupported with clear errors. 387 fs:: write ( 388 dir. path (). join ( "main.ts" ), 389"import def from 'npm:cjspkg@1'; console.log(def);" , 390) 391. unwrap (); 392assert_eq! ( 393success ( & server. run_npm ( dir. path (), & [ "run" ], "main.ts" )), 394"1\n" 395); 396 fs:: write ( dir. path (). join ( "main.ts" ), "import 'npm:data@1';" ). unwrap (); 397assert! ( failure ( & server. run_npm ( dir. path (), & [ "run" ], "main.ts" )). contains ( "JSON" )); 398 fs:: write ( dir. path (). join ( "main.ts" ), "import 'node:fs';" ). unwrap (); 399assert! ( 400failure ( & server. run_npm ( dir. path (), & [ "run" ], "main.ts" )) 401. contains ( "node: builtin modules are not supported" ) 402); 403} 404 405# [ test ] 406fn revoked_cached_imports_and_guest_io_stay_denied () { 407let server =Registry :: new (); 408 server. publish_package ( 409"sec" , 410"1.0.0" , 411 serde_json:: json!({}), 412& [( "index.js" , "export const value = 42;" )], 413); 414let dir = tempfile:: tempdir (). unwrap (); 415let grant =format! ( "--allow-js-exec={}sec/1.0.0/" , server. url ); 416 fs:: write ( 417 dir. path (). join ( "main.ts" ), 418"import {value} from 'npm:sec@1'; import {pledge} from 'avril:std'; console.log(value); pledge({jsExec: false}); try { await import('npm:sec@1'); throw new Error('allowed'); } catch (e) { console.log(e.message.includes('PermissionDenied')); }" , 419) 420. unwrap (); 421let output = server. run_npm ( dir. path (), & [ "run" , "-0" , & grant], "main.ts" ); 422assert_eq! ( success ( & output), "42\ntrue\n" ); 423 fs:: write ( 424 dir. path (). join ( "main.ts" ), 425"import {value} from 'npm:sec@1'; console.log(value); try { await fetch('http://127.0.0.1:9/'); throw new Error('net allowed'); } catch (e) { console.log(e.message.includes('PermissionDenied')); }" , 426) 427. unwrap (); 428let output = server. run_npm ( dir. path (), & [ "run" , "-0" , & grant], "main.ts" ); 429assert_eq! ( success ( & output), "42\ntrue\n" ); 430} 431 432# [ test ] 433fn reserved_tree_bypass_attempts_fail () { 434let server =Registry :: new (); 435 server. publish_package ( 436"guard" , 437"1.0.0" , 438 serde_json:: json!({}), 439& [( "index.js" , "export default 1;" )], 440); 441let dir = tempfile:: tempdir (). unwrap (); 442 fs:: write ( 443 dir. path (). join ( "main.ts" ), 444"import 'npm:guard@1'; console.log('warmed');" , 445) 446. unwrap (); 447assert_eq! ( 448success ( & server. run_npm ( dir. path (), & [ "run" ], "main.ts" )), 449"warmed\n" 450); 451let tree = dir. path (). join ( ".avril" ). join ( "npm" ); 452let target = tree 453. join ( "default" ) 454. read_dir () 455. unwrap () 456. next () 457. unwrap () 458. unwrap () 459. path (); 460let target = target. join ( "guard" ). join ( "1.0.0" ). join ( "index.js" ); 461assert! ( target. is_file ()); 462let relative = target. strip_prefix ( dir. path ()). unwrap (); 463let relative_url = url:: Url :: from_file_path ( & target). unwrap (); 464// Without virtual authority even the normal import fails, and direct 465// reserved-tree imports fail the same way: local grants never cover 466// the npm tree. 467 fs:: write ( dir. path (). join ( "main.ts" ), "import 'npm:guard@1';" ). unwrap (); 468let output = server. run_npm ( dir. path (), & [ "run" , "-0" , "--allow-js-exec=./" ], "main.ts" ); 469assert! ( 470failure ( & output). contains ( "PermissionDenied" ), 471"{}" , 472String :: from_utf8_lossy ( & output. stderr ) 473); 474// Prepared but unauthorized: the tree is owned, so bypass attempts 475// fail on authority rather than preparation. 476let grant =format! ( "--allow-js-exec={}guard/1.0.0/" , server. url ); 477 fs:: write ( 478 dir. path (). join ( "main.ts" ), 479format! ( 480"import 'npm:guard@1';\n\ 481import {{pledge}} from 'avril:std';\n\ 482pledge({{jsExec: false}});\n\ 483for (const source of ['./{}', '{}']) {{\n\ 484try {{ await import(source); throw new Error(source + ' allowed'); }}\n\ 485catch (e) {{ if (!e.message.includes('PermissionDenied')) throw e; }}\n\ 486}}\n\ 487console.log('denied');" , 488 relative. display (), 489 relative_url, 490), 491) 492. unwrap (); 493let output = server. run_npm ( 494 dir. path (), 495& [ "run" , "-0" , "--allow-js-exec=./" , grant. as_str ()], 496"main.ts" , 497); 498assert_eq! ( success ( & output), "denied\n" ); 499# [ cfg ( unix )] 500{ 501 std:: os:: unix:: fs:: symlink ( & target, dir. path (). join ( "link.js" )). unwrap (); 502 fs:: write ( 503 dir. path (). join ( "main.ts" ), 504"import 'npm:guard@1'; await import('./link.js');" , 505) 506. unwrap (); 507let output = server. run_npm ( dir. path (), & [ "run" , "-0" , "--allow-js-exec=./" ], "main.ts" ); 508assert! ( !output. status . success ()); 509} 510} 511 512# [ test ] 513fn application_maps_do_not_leak_into_packages () { 514let server =Registry :: new (); 515 server. publish_package ( 516"lib" , 517"1.0.0" , 518 serde_json:: json!({}), 519& [( "index.js" , "import 'evil'; export default 1;" )], 520); 521let dir = tempfile:: tempdir (). unwrap (); 522 fs:: write ( 523 dir. path (). join ( "avril.conf" ), 524"imports: {evil: './evil.ts'}\ndeps: ['npm:lib@1']" , 525) 526. unwrap (); 527 fs:: write ( 528 dir. path (). join ( "evil.ts" ), 529"throw new Error('app map leaked');" , 530) 531. unwrap (); 532 fs:: write ( dir. path (). join ( "main.ts" ), "import 'npm:lib@1';" ). unwrap (); 533assert! ( 534failure ( & server. run_npm ( dir. path (), & [ "run" ], "main.ts" )) 535. contains ( "not a declared dependency" ) 536); 537} 538 539# [ test ] 540fn project_package_json_never_changes_resolution () { 541let server =Registry :: new (); 542 server. publish_package ( 543"esm" , 544"1.0.0" , 545 serde_json:: json!({}), 546& [( "index.js" , "export const value = 'esm';" )], 547); 548let dir = tempfile:: tempdir (). unwrap (); 549 fs:: write ( 550 dir. path (). join ( "package.json" ), 551"{\"type\": \"commonjs\", \"dependencies\": {\"esm\": \"evil\"}}" , 552) 553. unwrap (); 554 fs:: write ( 555 dir. path (). join ( "main.ts" ), 556"import {value} from 'npm:esm@1'; console.log(value);" , 557) 558. unwrap (); 559assert_eq! ( 560success ( & server. run_npm ( dir. path (), & [ "run" ], "main.ts" )), 561"esm\n" 562); 563} 564 565# [ test ] 566fn planted_avril_configs_are_not_authorities () { 567let server =Registry :: new (); 568 server. publish_package ( 569"pkg" , 570"1.0.0" , 571 serde_json:: json!({}), 572& [( "index.js" , "export default 1;" )], 573); 574let dir = tempfile:: tempdir (). unwrap (); 575 fs:: write ( dir. path (). join ( "avril.conf" ), "deps: ['npm:pkg@1']" ). unwrap (); 576 fs:: write ( 577 dir. path (). join ( "main.ts" ), 578"import value from 'pkg'; console.log(value);" , 579) 580. unwrap (); 581assert_eq! ( 582success ( & server. run_npm ( dir. path (), & [ "run" ], "main.ts" )), 583"1\n" 584); 585let planted = dir. path (). join ( ".avril" ). join ( "npm" ). join ( "default" ); 586 fs:: write ( planted. join ( "avril.conf" ), "imports: {pkg: './evil.ts'}" ). unwrap (); 587 fs:: write ( planted. join ( "evil.ts" ), "throw new Error('planted');" ). unwrap (); 588assert_eq! ( 589success ( & server. run_npm ( dir. path (), & [ "run" ], "main.ts" )), 590"1\n" 591); 592} 593 594# [ test ] 595fn offline_frozen_relocated_trees_run_read_only () { 596let server =Registry :: new (); 597 server. publish_package ( 598"fro" , 599"1.0.0" , 600 serde_json:: json!({}), 601& [( "index.js" , "export const value = 'fro';" )], 602); 603let warm = tempfile:: tempdir (). unwrap (); 604 fs:: write ( warm. path (). join ( "avril.conf" ), "deps: ['npm:fro@1']" ). unwrap (); 605 fs:: write ( 606 warm. path (). join ( "main.ts" ), 607"import {value} from 'fro'; console.log(value);" , 608) 609. unwrap (); 610assert_eq! ( 611success ( & server. run_npm ( warm. path (), & [ "cache" ], "main.ts" )), 612"" 613); 614let dir = tempfile:: tempdir (). unwrap (); 615 fs:: write ( dir. path (). join ( "avril.conf" ), "deps: ['npm:fro@1']" ). unwrap (); 616 fs:: write ( 617 dir. path (). join ( "main.ts" ), 618"import {value} from 'fro'; console.log(value);" , 619) 620. unwrap (); 621fn copy_tree ( from : & Path , to : & Path ) { 622 fs:: create_dir_all ( to). unwrap (); 623for entryin fs:: read_dir ( from). unwrap () { 624let entry = entry. unwrap (); 625let target = to. join ( entry. file_name ()); 626if entry. file_type (). unwrap (). is_dir () { 627copy_tree ( & entry. path (), & target); 628} else { 629 fs:: copy ( entry. path (), target). unwrap (); 630} 631} 632} 633copy_tree ( 634& warm. path (). join ( ".avril" ). join ( "npm" ), 635& dir. path (). join ( ".avril" ). join ( "npm" ), 636); 637 fs:: copy ( 638 warm. path (). join ( "avril.lock" ), 639 dir. path (). join ( "avril.lock" ), 640) 641. unwrap (); 642 std:: fs:: remove_dir_all ( dir. path (). join ( ".avril" ). join ( "npm" ). join ( ".metadata" )). unwrap (); 643 std:: fs:: remove_dir_all ( dir. path (). join ( ".avril" ). join ( "npm" ). join ( ".artifacts" )). unwrap (); 644# [ cfg ( unix )] 645{ 646use std:: os:: unix:: fs:: PermissionsExt ; 647let tree = dir. path (). join ( ".avril" ); 648let mut stack =vec! [ tree. clone ()]; 649while let Some ( path) = stack. pop () { 650for entryin fs:: read_dir ( & path). unwrap () { 651let entry = entry. unwrap (). path (); 652if entry. is_dir () { 653 stack. push ( entry); 654} 655} 656 std:: fs:: set_permissions ( & path, std:: fs:: Permissions :: from_mode ( 0o555 )). unwrap (); 657} 658 std:: fs:: set_permissions ( 659 dir. path (). join ( "avril.lock" ), 660 std:: fs:: Permissions :: from_mode ( 0o444 ), 661) 662. unwrap (); 663} 664let before = server. requests . lock (). unwrap (). len (); 665let output = server. run_npm ( dir. path (), & [ "run" , "--offline" , "--frozen" ], "main.ts" ); 666assert_eq! ( success ( & output), "fro\n" ); 667assert_eq! ( server. requests . lock (). unwrap (). len (), before); 668} 669 670# [ test ] 671fn info_lists_npm_modules_with_package_metadata () { 672let server =Registry :: new (); 673 server. publish_package ( 674"inf" , 675"1.0.0" , 676 serde_json:: json!({}), 677& [( "index.js" , "export default 1;" )], 678); 679let dir = tempfile:: tempdir (). unwrap (); 680 fs:: write ( dir. path (). join ( "avril.conf" ), "deps: ['npm:inf@1']" ). unwrap (); 681 fs:: write ( dir. path (). join ( "main.ts" ), "import 'inf';" ). unwrap (); 682let output = server. run_npm ( dir. path (), & [ "info" ], "main.ts" ); 683let info: serde_json:: Value = serde_json:: from_str ( & success ( & output)). unwrap (); 684let modules = info[ "modules" ]. as_object (). unwrap (); 685let ( path, module) = modules 686. iter () 687. find ( |( _, module) |{ 688 module[ "package" ][ "name" ] =="inf" && module[ "package" ][ "version" ] =="1.0.0" 689}) 690. unwrap (); 691assert! ( path. contains ( ".avril" )); 692assert_eq! ( 693 module[ "url" ] 694. as_str () 695. unwrap () 696. strip_prefix ( "file://" ) 697. unwrap (), 698 path. as_str () 699); 700} 701 702# [ test ] 703fn execution_ignores_unused_corruption_but_cache_audits_it () { 704let server =Registry :: new (); 705 server. publish_npm ( 706"demand-audit" , 707"1.0.0" , 708& [ 709( 710"package.json" , 711r#"{"name":"demand-audit","version":"1.0.0","type":"module"}"# , 712), 713( "index.js" , "export const value = 7;" ), 714( "README.md" , "untouched" ), 715], 716); 717let dir = tempfile:: tempdir (). unwrap (); 718 fs:: write ( 719 dir. path (). join ( "main.ts" ), 720"import { value } from 'npm:demand-audit@1'; console.log(value);" , 721) 722. unwrap (); 723assert_eq! ( 724success ( & server. run_npm ( dir. path (), & [ "run" ], "main.ts" )). trim (), 725"7" 726); 727let package = dir. path (). join ( ".avril/npm/default" ); 728let registry = fs:: read_dir ( package) 729. unwrap () 730. next () 731. unwrap () 732. unwrap () 733. path (); 734 fs:: write ( registry. join ( "demand-audit/1.0.0/README.md" ), "tampered" ). unwrap (); 735assert_eq! ( 736success ( & server. run_npm ( dir. path (), & [ "run" , "--offline" , "--frozen" ], "main.ts" )). trim (), 737"7" 738); 739let audit = server. run_npm ( dir. path (), & [ "cache" , "--offline" , "--frozen" ], "main.ts" ); 740assert! ( !audit. status . success (), "cache audit unexpectedly passed" ); 741assert! ( String :: from_utf8_lossy ( & audit. stderr ). contains ( "README.md" )); 742 fs:: write ( 743 registry. join ( "demand-audit/1.0.0/index.js" ), 744"export const value = 8;" , 745) 746. unwrap (); 747let used_file = server. run_npm ( dir. path (), & [ "run" , "--offline" , "--frozen" ], "main.ts" ); 748assert! ( 749 !used_file. status . success (), 750"tampered module unexpectedly ran" 751); 752assert! ( String :: from_utf8_lossy ( & used_file. stderr ). contains ( "index.js" )); 753 fs:: write ( 754 registry. join ( "demand-audit/1.0.0/index.js" ), 755"export const value = 7;" , 756) 757. unwrap (); 758 fs:: write ( 759 registry. join ( "demand-audit/1.0.0/package.json" ), 760r#"{"name":"changed","version":"1.0.0","type":"module"}"# , 761) 762. unwrap (); 763let used_metadata = server. run_npm ( dir. path (), & [ "run" , "--offline" , "--frozen" ], "main.ts" ); 764assert! ( 765 !used_metadata. status . success (), 766"tampered package metadata passed" 767); 768assert! ( String :: from_utf8_lossy ( & used_metadata. stderr ). contains ( "package.json" )); 769} 770 771# [ cfg ( unix )] 772# [ test ] 773fn npm_file_urls_cannot_redirect_source_through_symlinks () { 774let server =Registry :: new (); 775 server. publish_package ( "url-dependency" , "1.0.0" , serde_json:: json!({}), & []); 776 server. publish_package ( 777"url-guard" , 778"1.0.0" , 779 serde_json:: json!({ 780"dependencies" : { "url-dependency" : "1" }, 781"exports" : { " . " : "./index.js" , "./prepare" : "./prepare.js" } 782}), 783& [ 784( "index.js" , "export const value = 'verified';" ), 785( "prepare.js" , "export {};" ), 786], 787); 788let dir = tempfile:: tempdir (). unwrap (); 789let main = dir. path (). join ( "main.js" ); 790 fs:: write ( 791& main, 792"import 'npm:url-guard@1/prepare'; console.log(import.meta.resolve('npm:url-guard@1'));" , 793) 794. unwrap (); 795let url =success ( & server. run_npm ( dir. path (), & [ "run" ], "main.js" )); 796let source = url:: Url :: parse ( url. trim ()). unwrap (). to_file_path (). unwrap (); 797let outside = dir. path (). join ( "outside.js" ); 798 fs:: write ( & outside, "export const value = 'UNVERIFIED';" ). unwrap (); 799 fs:: remove_file ( & source). unwrap (); 800 std:: os:: unix:: fs:: symlink ( & outside, & source). unwrap (); 801 fs:: write ( 802& main, 803"import 'npm:url-guard@1/prepare'; console.log((await import(import.meta.resolve('npm:url-guard@1'))).value);" , 804) 805. unwrap (); 806let output = server. run_npm ( dir. path (), & [ "run" , "--offline" , "--frozen" ], "main.js" ); 807assert! ( failure ( & output). contains ( "npm file" )); 808assert! ( !String :: from_utf8_lossy ( & output. stdout ). contains ( "UNVERIFIED" )); 809} 810 811# [ test ] 812fn extensionless_local_require_resolves_prepared_npm_manifest_entry () { 813let server =Registry :: new (); 814 server. publish_package ( 815"extensionless-demand" , 816"1.0.0" , 817 serde_json:: json!({ 818"type" : "module" , 819"exports" : { " . " : "./index.js" , "./cjs" : "./cjs/index.js" } 820}), 821& [ 822( "index.js" , "export const loaded = true;" ), 823( "cjs/package.json" , "{\"type\":\"commonjs\"}" ), 824( "cjs/index.js" , "exports.value = 'verified-cjs';" ), 825], 826); 827let dir = tempfile:: tempdir (). unwrap (); 828 fs:: write ( 829 dir. path (). join ( "main.js" ), 830"import 'npm:extensionless-demand@1'; await import('./extensionless.cjs');" , 831) 832. unwrap (); 833 fs:: write ( 834 dir. path (). join ( "extensionless.cjs" ), 835"const entry = require.resolve('npm:extensionless-demand@1/cjs'); console.log(require(entry.slice(0, -3)).value);" , 836) 837. unwrap (); 838assert_eq! ( 839success ( & server. run_npm ( dir. path (), & [ "run" ], "main.js" )), 840"verified-cjs\n" 841); 842} 843 844# [ test ] 845fn failed_imports_do_not_poison_later_runs () { 846let server =Registry :: new (); 847 server. publish_package ( "ok" , "1.0.0" , serde_json:: json!({}), & []); 848let dir = tempfile:: tempdir (). unwrap (); 849 fs:: write ( dir. path (). join ( "main.ts" ), "import 'npm:ghost@9';" ). unwrap (); 850assert! ( 851 !server 852. run_npm ( dir. path (), & [ "run" ], "main.ts" ) 853. status 854. success () 855); 856 fs:: write ( 857 dir. path (). join ( "main.ts" ), 858"import 'npm:ok@1'; console.log('healed');" , 859) 860. unwrap (); 861assert_eq! ( 862success ( & server. run_npm ( dir. path (), & [ "run" ], "main.ts" )), 863"healed\n" 864); 865} 866 867# [ test ] 868fn import_meta_urls_resolve_to_the_same_instance () { 869let server =Registry :: new (); 870 server. publish_package ( 871"meta" , 872"1.0.0" , 873 serde_json:: json!({}), 874& [( "index.js" , "export const url = import.meta.url;" )], 875); 876let dir = tempfile:: tempdir (). unwrap (); 877 fs:: write ( 878 dir. path (). join ( "main.ts" ), 879"const first = await import('npm:meta@1'); const second = await import(first.url); console.log(first === second, first.url.startsWith('file:'), first.url);" , 880) 881. unwrap (); 882let out =success ( & server. run_npm ( dir. path (), & [ "run" ], "main.ts" )); 883assert! ( out. starts_with ( "true true file://" ), "{out}" ); 884assert! ( out. contains ( ".avril" ), "{out}" ); 885} 886 887# [ test ] 888fn version_scoped_grants_deny_uncovered_packages () { 889let server =Registry :: new (); 890 server. publish_package ( 891"guarded" , 892"1.0.0" , 893 serde_json:: json!({}), 894& [( "index.js" , "export default 1;" )], 895); 896 server. publish_package ( 897"other" , 898"1.0.0" , 899 serde_json:: json!({}), 900& [( "index.js" , "export default 2;" )], 901); 902let dir = tempfile:: tempdir (). unwrap (); 903 fs:: write ( 904 dir. path (). join ( "main.ts" ), 905"import 'npm:guarded@1'; import 'npm:other@1';" , 906) 907. unwrap (); 908// Authority covers only the guarded package: the other fetch never happens. 909let grant =format! ( "--allow-js-exec={}guarded/" , server. url ); 910let output = server. run_npm ( dir. path (), & [ "run" , "-0" , & grant], "main.ts" ); 911assert! ( !output. status . success ()); 912assert! ( 913 !server 914. requests 915. lock () 916. unwrap () 917. iter () 918. any ( |path| path. contains ( "other" )), 919"denied packages must not be fetched: {:?}" , 920 server. requests . lock (). unwrap () 921); 922} 923 924# [ test ] 925fn file_grants_allow_exact_exports_but_deny_siblings () { 926let server =Registry :: new (); 927 server. publish_package ( 928"files" , 929"1.0.0" , 930 serde_json:: json!({ "exports" : { " . " : "./index.js" , "./secret" : "./secret.js" }}), 931& [ 932( "index.js" , "export default 'public';" ), 933( "secret.js" , "export default 'secret';" ), 934], 935); 936let dir = tempfile:: tempdir (). unwrap (); 937let base =format! ( "{}files/1.0.0/" , server. url ); 938let allow =format! ( "--allow-js-exec={}index.js" , base); 939let deny =format! ( "--deny-js-exec={}secret.js" , base); 940 fs:: write ( 941 dir. path (). join ( "main.ts" ), 942"import value from 'npm:files@1'; console.log(value);" , 943) 944. unwrap (); 945assert_eq! ( 946success ( & server. run_npm ( dir. path (), & [ "run" , "-0" , & allow, & deny], "main.ts" )), 947"public\n" 948); 949 fs:: write ( 950 dir. path (). join ( "main.ts" ), 951"import value from 'npm:files@1/secret'; console.log(value);" , 952) 953. unwrap (); 954assert! ( 955failure ( & server. run_npm ( dir. path (), & [ "run" , "-0" , & allow, & deny], "main.ts" )) 956. contains ( "PermissionDenied" ) 957); 958} 959 960# [ test ] 961fn revocation_during_acquisition_denies_settled_imports () { 962use std:: sync:: atomic:: Ordering ; 963use std:: time::{ Duration , Instant }; 964let server =Registry :: new (); 965 server. publish_package ( 966"slow" , 967"1.0.0" , 968 serde_json:: json!({}), 969& [( "index.js" , "export default 1;" )], 970); 971 server. hold_tarballs . store ( true , Ordering :: Relaxed ); 972let dir = tempfile:: tempdir (). unwrap (); 973let grant =format! ( "--allow-js-exec={}slow/" , server. url ); 974 fs:: write ( 975 dir. path (). join ( "main.ts" ), 976"const pending = import('npm:slow@1'); await new Promise((resolve) => setTimeout(resolve, 200)); const {pledge} = await import('avril:std'); pledge({jsExec: false}); try { await pending; throw new Error('allowed'); } catch (e) { console.log(e.message.includes('PermissionDenied')); }" , 977) 978. unwrap (); 979let child = std:: process:: Command :: new ( env! ( "CARGO_BIN_EXE_avril" )) 980. current_dir ( dir. path ()) 981. args ([ "run" , "-0" , grant. as_str ()]) 982. arg ( format! ( "--jsr-registry={}" , server. url )) 983. arg ( format! ( "--npm-registry={}" , server. url )) 984. arg ( format! ( 985"--cache-dir={}" , 986 dir. path (). join ( "cache" ). display () 987)) 988. arg ( "main.ts" ) 989. stdout ( std:: process:: Stdio :: piped ()) 990. stderr ( std:: process:: Stdio :: piped ()) 991. spawn () 992. unwrap (); 993let start =Instant :: now (); 994loop { 995if server 996. requests 997. lock () 998. unwrap () 999. iter () 1000. any ( |path| path. ends_with ( ".tgz" )) 1001{ 1002break ; 1003} 1004assert! ( 1005 start. elapsed () <Duration :: from_secs ( 30 ), 1006"tarball fetch never started" 1007); 1008 std:: thread:: sleep ( Duration :: from_millis ( 10 )); 1009} 1010// The guest pledges at ~200ms; release the tarball later so the 1011// post-fetch authority recheck deterministically observes it. 1012 std:: thread:: sleep ( Duration :: from_millis ( 500 )); 1013 server. hold_tarballs . store ( false , Ordering :: Relaxed ); 1014let output = child. wait_with_output (). unwrap (); 1015assert_eq! ( success ( & output), "true\n" ); 1016} 1017 1018# [ test ] 1019fn add_pins_unversioned_npm_targets_and_caches_exports () { 1020let server =Registry :: new (); 1021 server. publish_package ( 1022"tool" , 1023"1.0.0" , 1024 serde_json:: json!({ "exports" : { " . " : "./mod.js" }}), 1025& [( "mod.js" , "export default 'tool';" )], 1026); 1027 server. publish_package ( 1028"tool" , 1029"1.1.0" , 1030 serde_json:: json!({ "exports" : { " . " : "./mod.js" }}), 1031& [( "mod.js" , "export default 'tool2';" )], 1032); 1033let dir = tempfile:: tempdir (). unwrap (); 1034 fs:: write ( dir. path (). join ( "avril.conf" ), "deps: []" ). unwrap (); 1035let output = server. run_npm ( dir. path (), & [ "add" , "npm:tool" ], "" ); 1036assert! ( 1037success ( & output). contains ( "\"tool\": \"npm:tool@^1.1.0\"" ), 1038"{output:?}" 1039); 1040let lock: serde_json:: Value = fixture:: read_lock ( dir. path ()); 1041assert_eq! ( lock[ "npm" ][ "specifiers" ][ "npm:tool@^1.1.0" ], "1.1.0" ); 1042} 1043 1044# [ test ] 1045fn add_keeps_tags_and_aliases_explicit () { 1046let server =Registry :: new (); 1047 server. publish_package ( 1048"tool" , 1049"1.0.0" , 1050 serde_json:: json!({ "exports" : { " . " : "./mod.js" , "./sub" : "./sub.js" }}), 1051& [ 1052( "mod.js" , "export default 1;" ), 1053( "sub.js" , "export default 2;" ), 1054], 1055); 1056let dir = tempfile:: tempdir (). unwrap (); 1057 fs:: write ( dir. path (). join ( "avril.conf" ), "deps: []" ). unwrap (); 1058let output = server. run_npm ( dir. path (), & [ "add" , "npm:tool@latest" ], "" ); 1059assert! ( 1060success ( & output). contains ( "\"tool\": \"npm:tool@latest\"" ), 1061"{output:?}" 1062); 1063let output = server. run_npm ( dir. path (), & [ "add" , "sub" , "npm:tool@1/sub" ], "" ); 1064assert! ( 1065success ( & output). contains ( "\"sub\": \"npm:tool@1/sub\"" ), 1066"{output:?}" 1067); 1068} 1069 1070# [ test ] 1071fn vendor_covers_jsr_http_and_npm_side_by_side () { 1072let server =Registry :: new (); 1073 server. package ( 1074"@test/value" , 1075"1.0.0" , 1076& [( "mod.ts" , "export const value = 'one';" )], 1077& [( "." , "./mod.ts" )], 1078); 1079 server. publish_package ( 1080"tool" , 1081"1.0.0" , 1082 serde_json:: json!({}), 1083& [( "index.js" , "export const value = 'tool';" )], 1084); 1085let dir = tempfile:: tempdir (). unwrap (); 1086 fs:: write ( 1087 dir. path (). join ( "avril.conf" ), 1088"deps: ['jsr:@test/value@1', 'npm:tool@1']" , 1089) 1090. unwrap (); 1091 fs:: write ( 1092 dir. path (). join ( "main.ts" ), 1093"import {value as a} from '@test/value'; import {value as b} from 'tool'; console.log(a, b);" , 1094) 1095. unwrap (); 1096assert_eq! ( 1097success ( & server. run_npm ( dir. path (), & [ "run" ], "main.ts" )), 1098"one tool\n" 1099); 1100assert_eq! ( 1101success ( & server. run_npm ( dir. path (), & [ "vendor" , "--out=vendor" ], "main.ts" )), 1102"" 1103); 1104assert! ( 1105 dir. path () 1106. join ( "vendor" ) 1107. join ( "@test/value/1.0.0/mod.ts" ) 1108. is_file () 1109); 1110let npm_files: Vec < _ > =walkdir ( & dir. path (). join ( "vendor" ). join ( "npm" )) 1111. into_iter () 1112. filter ( |path| path. extension (). is_some_and ( |ext| ext =="js" )) 1113. collect (); 1114assert! ( !npm_files. is_empty (), "vendored npm files" ); 1115assert! ( 1116 npm_files 1117. iter () 1118. any ( |path| path. to_string_lossy (). contains ( "tool" )) 1119); 1120} 1121 1122fn walkdir ( path : & std:: path:: Path ) ->Vec < std:: path:: PathBuf > { 1123let mut out =Vec :: new (); 1124for entryin std:: fs:: read_dir ( path). unwrap () { 1125let entry = entry. unwrap (). path (); 1126if entry. is_dir () { 1127 out. extend ( walkdir ( & entry)); 1128} else { 1129 out. push ( entry); 1130} 1131} 1132 out 1133} 1134 1135# [ test ] 1136fn vendored_npm_reconstructs_offline_without_caches () { 1137let server =Registry :: new (); 1138 server. publish_package ( 1139"vend" , 1140"1.0.0" , 1141 serde_json:: json!({}), 1142& [( "index.js" , "export const value = 'vend';" )], 1143); 1144let dir = tempfile:: tempdir (). unwrap (); 1145 fs:: write ( dir. path (). join ( "avril.conf" ), "deps: ['npm:vend@1']" ). unwrap (); 1146 fs:: write ( 1147 dir. path (). join ( "main.ts" ), 1148"import {value} from 'vend'; console.log(value);" , 1149) 1150. unwrap (); 1151assert_eq! ( 1152success ( & server. run_npm ( dir. path (), & [ "run" ], "main.ts" )), 1153"vend\n" 1154); 1155assert_eq! ( 1156success ( & server. run_npm ( dir. path (), & [ "vendor" , "--out=vendor" ], "main.ts" )), 1157"" 1158); 1159 std:: fs:: remove_dir_all ( dir. path (). join ( ".avril" )). unwrap (); 1160let before = server. requests . lock (). unwrap (). len (); 1161let run = server. run_npm ( 1162 dir. path (), 1163& [ "run" , "--vendor=vendor" , "--offline" , "--frozen" ], 1164"main.ts" , 1165); 1166assert_eq! ( success ( & run), "vend\n" ); 1167assert_eq! ( server. requests . lock (). unwrap (). len (), before); 1168} 1169 1170# [ test ] 1171fn file_grants_cover_exact_exports_and_nested_denials () { 1172let server =Registry :: new (); 1173 server. publish_package ( 1174"files" , 1175"1.0.0" , 1176 serde_json:: json!({ "exports" : { " . " : "./index.js" , "./nested" : "./nested/deep.js" }}), 1177& [ 1178( "index.js" , "export {value} from './secret.js';" ), 1179( "secret.js" , "export const value = 'secret';" ), 1180( "nested/deep.js" , "export const value = 'deep';" ), 1181], 1182); 1183let dir = tempfile:: tempdir (). unwrap (); 1184let base =format! ( "{}files/1.0.0/" , server. url ); 1185let allow =format! ( "--allow-js-exec={}nested/deep.js" , base); 1186let deny =format! ( "--deny-js-exec={}secret.js" , base); 1187// The exact grant loads its own file; the root needs the denied 1188// sibling through its nested import and fails. 1189 fs:: write ( 1190 dir. path (). join ( "main.ts" ), 1191"import {value} from 'npm:files@1/nested'; console.log(value);" , 1192) 1193. unwrap (); 1194assert_eq! ( 1195success ( & server. run_npm ( dir. path (), & [ "run" , "-0" , & allow, & deny], "main.ts" )), 1196"deep\n" 1197); 1198 fs:: write ( 1199 dir. path (). join ( "main.ts" ), 1200"import {value} from 'npm:files@1'; console.log(value);" , 1201) 1202. unwrap (); 1203assert! ( 1204failure ( & server. run_npm ( dir. path (), & [ "run" , "-0" , & allow, & deny], "main.ts" )) 1205. contains ( "PermissionDenied" ) 1206); 1207} 1208 1209# [ test ] 1210fn revocation_mid_acquisition_denies_before_use () { 1211use std:: sync:: atomic:: Ordering ; 1212use std:: time::{ Duration , Instant }; 1213let server =Registry :: new (); 1214 server. publish_package ( 1215"slow" , 1216"1.0.0" , 1217 serde_json:: json!({}), 1218& [( "index.js" , "export default 1;" )], 1219); 1220 server. hold_tarballs . store ( true , Ordering :: Relaxed ); 1221let dir = tempfile:: tempdir (). unwrap (); 1222let grant =format! ( "--allow-js-exec={}slow/" , server. url ); 1223 fs:: write ( 1224 dir. path (). join ( "main.ts" ), 1225"const pending = import('npm:slow@1'); await new Promise((resolve) => setTimeout(resolve, 200)); const {pledge} = await import('avril:std'); pledge({jsExec: false}); try { await pending; throw new Error('allowed'); } catch (e) { console.log(e.message.includes('PermissionDenied')); }" , 1226) 1227. unwrap (); 1228let child = std:: process:: Command :: new ( env! ( "CARGO_BIN_EXE_avril" )) 1229. current_dir ( dir. path ()) 1230. args ([ "run" , "-0" , grant. as_str ()]) 1231. arg ( format! ( "--jsr-registry={}" , server. url )) 1232. arg ( format! ( "--npm-registry={}" , server. url )) 1233. arg ( format! ( 1234"--cache-dir={}" , 1235 dir. path (). join ( "cache" ). display () 1236)) 1237. arg ( "main.ts" ) 1238. stdout ( std:: process:: Stdio :: piped ()) 1239. stderr ( std:: process:: Stdio :: piped ()) 1240. spawn () 1241. unwrap (); 1242let start =Instant :: now (); 1243loop { 1244if server 1245. requests 1246. lock () 1247. unwrap () 1248. iter () 1249. any ( |path| path. ends_with ( ".tgz" )) 1250{ 1251break ; 1252} 1253assert! ( 1254 start. elapsed () <Duration :: from_secs ( 30 ), 1255"tarball fetch never started" 1256); 1257 std:: thread:: sleep ( Duration :: from_millis ( 10 )); 1258} 1259// The guest pledges at ~200ms; release later so the post-fetch 1260// authority recheck deterministically observes the revocation. 1261 std:: thread:: sleep ( Duration :: from_millis ( 500 )); 1262 server. hold_tarballs . store ( false , Ordering :: Relaxed ); 1263let output = child. wait_with_output (). unwrap (); 1264assert_eq! ( success ( & output), "true\n" ); 1265} 1266 1267# [ test ] 1268fn lock_override_keeps_project_root_for_npm_storage () { 1269let server =Registry :: new (); 1270 server. publish_package ( 1271"rooted" , 1272"1.0.0" , 1273 serde_json:: json!({}), 1274& [( "index.js" , "export default 1;" )], 1275); 1276let dir = tempfile:: tempdir (). unwrap (); 1277 fs:: write ( dir. path (). join ( "avril.conf" ), "deps: ['npm:rooted@1']" ). unwrap (); 1278 fs:: write ( 1279 dir. path (). join ( "main.ts" ), 1280"import 'npm:rooted@1'; console.log('ok');" , 1281) 1282. unwrap (); 1283let elsewhere = tempfile:: tempdir (). unwrap (); 1284let output = std:: process:: Command :: new ( env! ( "CARGO_BIN_EXE_avril" )) 1285. current_dir ( dir. path ()) 1286. args ([ "run" ]) 1287. arg ( format! ( "--jsr-registry={}" , server. url )) 1288. arg ( format! ( "--npm-registry={}" , server. url )) 1289. arg ( format! ( 1290"--cache-dir={}" , 1291 dir. path (). join ( "cache" ). display () 1292)) 1293. arg ( format! ( 1294"--lock={}" , 1295 elsewhere. path (). join ( "elsewhere.lock" ). display () 1296)) 1297. arg ( "main.ts" ) 1298. output () 1299. unwrap (); 1300assert_eq! ( success ( & output), "ok\n" ); 1301assert! ( dir. path (). join ( ".avril" ). join ( "npm" ). is_dir ()); 1302assert! ( elsewhere. path (). join ( "elsewhere.lock" ). is_file ()); 1303} 1304 1305# [ test ] 1306fn mapped_away_npm_literals_never_touch_the_registry () { 1307let server =Registry :: new (); 1308 server. publish_package ( 1309"real" , 1310"1.0.0" , 1311 serde_json:: json!({}), 1312& [( "index.js" , "export default 'real';" )], 1313); 1314let dir = tempfile:: tempdir (). unwrap (); 1315 fs:: write ( 1316 dir. path (). join ( "avril.conf" ), 1317"imports: {'npm:blocked': './safe.ts'}" , 1318) 1319. unwrap (); 1320 fs:: write ( dir. path (). join ( "safe.ts" ), "export default 'safe';" ). unwrap (); 1321 fs:: write ( 1322 dir. path (). join ( "main.ts" ), 1323"import a from 'npm:blocked'; import b from 'npm:real@1'; console.log(a, b);" , 1324) 1325. unwrap (); 1326assert_eq! ( 1327success ( & server. run_npm ( dir. path (), & [ "run" ], "main.ts" )), 1328"safe real\n" 1329); 1330assert! ( 1331 !server 1332. requests 1333. lock () 1334. unwrap () 1335. iter () 1336. any ( |path| path. contains ( "blocked" )), 1337"mapped-away literals must not be fetched" 1338); 1339} 1340 1341# [ test ] 1342fn dynamic_misses_do_not_poison_valid_imports () { 1343let server =Registry :: new (); 1344 server. publish_package ( 1345"good" , 1346"1.0.0" , 1347 serde_json:: json!({}), 1348& [( "index.js" , "export const value = 'good';" )], 1349); 1350let dir = tempfile:: tempdir (). unwrap (); 1351 fs:: write ( 1352 dir. path (). join ( "main.ts" ), 1353"const [a, b] = await Promise.allSettled([import('npm:good@1'), import('npm:missing@9')]); console.log(a.status, b.status, a.value.value); const c = await import('npm:good@1'); console.log(c.value, a.value === c);" , 1354) 1355. unwrap (); 1356assert_eq! ( 1357success ( & server. run_npm ( dir. path (), & [ "run" ], "main.ts" )), 1358"fulfilled rejected good\ngood true\n" 1359); 1360} 1361 1362# [ test ] 1363fn update_selects_newer_versions_and_prune_keeps_the_closure () { 1364let server =Registry :: new (); 1365 server. publish_package ( "up" , "1.0.0" , serde_json:: json!({}), & []); 1366 server. publish_package ( "stale" , "1.0.0" , serde_json:: json!({}), & []); 1367let dir = tempfile:: tempdir (). unwrap (); 1368 fs:: write ( 1369 dir. path (). join ( "avril.conf" ), 1370"deps: ['npm:up@*', 'npm:stale@1']" , 1371) 1372. unwrap (); 1373 fs:: write ( 1374 dir. path (). join ( "main.ts" ), 1375"import 'npm:up@*'; import 'npm:stale@1'; console.log('ok');" , 1376) 1377. unwrap (); 1378assert_eq! ( 1379success ( & server. run_npm ( dir. path (), & [ "cache" ], "main.ts" )), 1380"" 1381); 1382 server. publish_package ( "up" , "2.0.0" , serde_json:: json!({}), & []); 1383assert_eq! ( 1384success ( & server. run_npm ( dir. path (), & [ "update" ], "main.ts" )), 1385"" 1386); 1387let lock: serde_json:: Value = fixture:: read_lock ( dir. path ()); 1388assert_eq! ( lock[ "npm" ][ "specifiers" ][ "npm:up@*" ], "2.0.0" ); 1389 fs:: write ( dir. path (). join ( "avril.conf" ), "deps: ['npm:up@*']" ). unwrap (); 1390 fs:: write ( 1391 dir. path (). join ( "main.ts" ), 1392"import 'npm:up@*'; console.log('ok');" , 1393) 1394. unwrap (); 1395assert_eq! ( 1396success ( & server. run_npm ( dir. path (), & [ "update" ], "main.ts" )), 1397"" 1398); 1399// Plain updates merge: the dropped declaration stays until pruned. 1400let lock: serde_json:: Value = fixture:: read_lock ( dir. path ()); 1401assert! ( lock[ "npm" ][ "specifiers" ]. get ( "npm:stale@1" ). is_some ()); 1402assert_eq! ( 1403success ( & server. run_npm ( dir. path (), & [ "update" , "--prune" ], "main.ts" )), 1404"" 1405); 1406let lock: serde_json:: Value = fixture:: read_lock ( dir. path ()); 1407assert! ( lock[ "npm" ][ "specifiers" ]. get ( "npm:stale@1" ). is_none ()); 1408assert! ( lock[ "npm" ][ "specifiers" ]. get ( "npm:up@*" ). is_some ()); 1409assert! ( 1410 lock[ "npm" ][ "packages" ] 1411. as_object () 1412. unwrap () 1413. keys () 1414. all ( |key| key. contains ( "|up|" )), 1415"prune drops the whole stale closure" 1416); 1417} 1418 1419# [ test ] 1420fn includes_prepare_extra_roots () { 1421let server =Registry :: new (); 1422 server. publish_package ( 1423"inc" , 1424"1.0.0" , 1425 serde_json:: json!({}), 1426& [( "index.js" , "export const value = 'inc';" )], 1427); 1428let dir = tempfile:: tempdir (). unwrap (); 1429 fs:: write ( dir. path (). join ( "main.ts" ), "console.log('main');" ). unwrap (); 1430assert_eq! ( 1431success ( & server. run_npm ( dir. path (), & [ "run" , "--include=npm:inc@1" ], "main.ts" )), 1432"main\n" 1433); 1434assert! ( 1435 server 1436. requests 1437. lock () 1438. unwrap () 1439. iter () 1440. any ( |path| path =="/inc" ), 1441"includes prepare their roots" 1442); 1443// Includes resolve from an explicit referrer too. 1444 fs:: write ( dir. path (). join ( "other.ts" ), "export const other = 1;" ). unwrap (); 1445assert_eq! ( 1446success ( & server. run_npm ( 1447 dir. path (), 1448& [ 1449"run" , 1450"--include-from=./other.ts" , 1451"--include=npm:inc@1" , 1452"--include-from=." , 1453], 1454"main.ts" 1455)), 1456"main\n" 1457); 1458} 1459 1460# [ test ] 1461fn add_prefixes_map_every_esm_export () { 1462let server =Registry :: new (); 1463 server. publish_package ( 1464"tool" , 1465"1.0.0" , 1466 serde_json:: json!({ "exports" : { 1467"." : "./mod.js" , 1468"./sub" : "./sub.js" , 1469"./data" : "./data.json" , 1470}}), 1471& [ 1472( "mod.js" , "export default 1;" ), 1473( "sub.js" , "export default 2;" ), 1474( "data.json" , "{\"n\": 3}" ), 1475], 1476); 1477let dir = tempfile:: tempdir (). unwrap (); 1478 fs:: write ( dir. path (). join ( "avril.conf" ), "imports: {}" ). unwrap (); 1479let output = server. run_npm ( dir. path (), & [ "add" , "tool/" , "npm:tool@1/" ], "" ); 1480let stdout =success ( & output); 1481assert! ( stdout. contains ( "\"tool/\": \"npm:tool@1/\"" ), "{stdout}" ); 1482// Adding the prefix warms every ESM export, so the subpath runs offline. 1483 fs:: write ( 1484 dir. path (). join ( "main.ts" ), 1485"import sub from 'tool/sub'; console.log(sub);" , 1486) 1487. unwrap (); 1488let before = server. requests . lock (). unwrap (). len (); 1489let run = server. run_npm ( dir. path (), & [ "run" , "--offline" ], "main.ts" ); 1490assert_eq! ( success ( & run), "2\n" ); 1491assert_eq! ( server. requests . lock (). unwrap (). len (), before); 1492} 1493 1494# [ test ] 1495fn declared_but_unused_dependencies_replay_frozen () { 1496let server =Registry :: new (); 1497 server. publish_package ( "used" , "1.0.0" , serde_json:: json!({}), & []); 1498 server. publish_package ( "unused" , "1.0.0" , serde_json:: json!({}), & []); 1499let dir = tempfile:: tempdir (). unwrap (); 1500 fs:: write ( 1501 dir. path (). join ( "avril.conf" ), 1502"deps: ['npm:used@1', 'npm:unused@1']" , 1503) 1504. unwrap (); 1505 fs:: write ( 1506 dir. path (). join ( "main.ts" ), 1507"import 'npm:used@1'; console.log('ok');" , 1508) 1509. unwrap (); 1510assert_eq! ( 1511success ( & server. run_npm ( dir. path (), & [ "cache" ], "main.ts" )), 1512"" 1513); 1514// Unused declarations stay replayable without network or mutation. 1515let before = server. requests . lock (). unwrap (). len (); 1516let run = server. run_npm ( dir. path (), & [ "run" , "--offline" , "--frozen" ], "main.ts" ); 1517assert_eq! ( success ( & run), "ok\n" ); 1518assert_eq! ( server. requests . lock (). unwrap (). len (), before); 1519} 1520 1521# [ test ] 1522fn relocated_trees_run_offline_without_absolute_paths () { 1523let server =Registry :: new (); 1524 server. publish_package ( 1525"reloc" , 1526"1.0.0" , 1527 serde_json:: json!({}), 1528& [( "index.js" , "export const value = 'reloc';" )], 1529); 1530let dir = tempfile:: tempdir (). unwrap (); 1531 fs:: write ( dir. path (). join ( "avril.conf" ), "deps: ['npm:reloc@1']" ). unwrap (); 1532 fs:: write ( 1533 dir. path (). join ( "main.ts" ), 1534"import {value} from 'npm:reloc@1'; console.log(value);" , 1535) 1536. unwrap (); 1537assert_eq! ( 1538success ( & server. run_npm ( dir. path (), & [ "run" ], "main.ts" )), 1539"reloc\n" 1540); 1541let lock = fs:: read_to_string ( dir. path (). join ( "avril.lock" )). unwrap (); 1542assert! ( 1543 !lock. contains ( dir. path (). to_string_lossy (). as_ref ()), 1544"locks stay relocatable" 1545); 1546let moved = tempfile:: tempdir (). unwrap (); 1547copy_tree ( dir. path (), moved. path ()); 1548let before = server. requests . lock (). unwrap (). len (); 1549let run = server. run_npm ( moved. path (), & [ "run" , "--offline" , "--frozen" ], "main.ts" ); 1550assert_eq! ( success ( & run), "reloc\n" ); 1551assert_eq! ( server. requests . lock (). unwrap (). len (), before); 1552} 1553 1554fn copy_tree ( source : & std:: path:: Path , destination : & std:: path:: Path ) { 1555for entryin std:: fs:: read_dir ( source). unwrap () { 1556let entry = entry. unwrap (); 1557let target = destination. join ( entry. file_name ()); 1558if entry. file_type (). unwrap (). is_dir () { 1559 std:: fs:: create_dir_all ( & target). unwrap (); 1560copy_tree ( & entry. path (), & target); 1561} else { 1562 std:: fs:: copy ( entry. path (), target). unwrap (); 1563} 1564} 1565} 1566 1567# [ test ] 1568fn add_warms_string_conditional_and_pattern_exports () { 1569let server =Registry :: new (); 1570 server. publish_package ( 1571"forms" , 1572"1.0.0" , 1573 serde_json:: json!({ "exports" : { 1574"." : { "node" : "./n.cjs" , "import" : "./index.js" }, 1575"./feature/*" : "./dist/*.js" , 1576}}), 1577& [ 1578( "index.js" , "export default 'root';" ), 1579( "n.cjs" , "module.exports = 'node';" ), 1580( "dist/thing.js" , "export default 'thing';" ), 1581], 1582); 1583let dir = tempfile:: tempdir (). unwrap (); 1584 fs:: write ( dir. path (). join ( "avril.conf" ), "deps: []" ). unwrap (); 1585let output = server. run_npm ( dir. path (), & [ "add" , "npm:forms" ], "" ); 1586assert! ( 1587success ( & output). contains ( "\"forms\": \"npm:forms@^1.0.0\"" ), 1588"{output:?}" 1589); 1590 fs:: write ( 1591 dir. path (). join ( "main.ts" ), 1592"import root from 'forms'; import thing from 'forms/feature/thing'; console.log(root, thing);" , 1593) 1594. unwrap (); 1595let before = server. requests . lock (). unwrap (). len (); 1596let run = server. run_npm ( dir. path (), & [ "run" , "--offline" ], "main.ts" ); 1597assert_eq! ( success ( & run), "root thing\n" ); 1598assert_eq! ( server. requests . lock (). unwrap (). len (), before); 1599// A nested prefix warms through its own scope. 1600let scoped = tempfile:: tempdir (). unwrap (); 1601 fs:: write ( scoped. path (). join ( "avril.conf" ), "imports: {}" ). unwrap (); 1602let output = server. run_npm ( scoped. path (), & [ "add" , "feat/" , "npm:forms@1/feature/" ], "" ); 1603assert! ( 1604success ( & output). contains ( "\"feat/\": \"npm:forms@1/feature/\"" ), 1605"{output:?}" 1606); 1607 fs:: write ( 1608 scoped. path (). join ( "main.ts" ), 1609"import thing from 'feat/thing'; console.log(thing);" , 1610) 1611. unwrap (); 1612let before = server. requests . lock (). unwrap (). len (); 1613let run = server. run_npm ( scoped. path (), & [ "run" , "--offline" ], "main.ts" ); 1614assert_eq! ( success ( & run), "thing\n" ); 1615assert_eq! ( server. requests . lock (). unwrap (). len (), before); 1616} 1617 1618# [ test ] 1619fn prefix_add_ignores_broken_exports_outside_its_scope () { 1620let server =Registry :: new (); 1621 server. publish_package ( 1622"sides" , 1623"1.0.0" , 1624 serde_json:: json!({ "exports" : { 1625"." : "./index.js" , 1626"./good" : "./good.js" , 1627"./bad" : "./bad.js" , 1628}}), 1629& [ 1630( "index.js" , "export default 0;" ), 1631( "good.js" , "export default 'good';" ), 1632( "bad.js" , "this is not valid javascript ((((" ), 1633], 1634); 1635let dir = tempfile:: tempdir (). unwrap (); 1636 fs:: write ( dir. path (). join ( "avril.conf" ), "imports: {}" ). unwrap (); 1637// Warming only the good prefix never parses the broken sibling. 1638let output = server. run_npm ( dir. path (), & [ "add" , "good" , "npm:sides@1/good" ], "" ); 1639assert! ( 1640success ( & output). contains ( "\"good\": \"npm:sides@1/good\"" ), 1641"{output:?}" 1642); 1643 fs:: write ( 1644 dir. path (). join ( "main.ts" ), 1645"import good from 'good'; console.log(good);" , 1646) 1647. unwrap (); 1648assert_eq! ( 1649success ( & server. run_npm ( dir. path (), & [ "run" , "--offline" ], "main.ts" )), 1650"good\n" 1651); 1652// The whole package still fails loudly: broken files are not skipped. 1653let output = server. run_npm ( dir. path (), & [ "add" , "sides/" , "npm:sides@1/" ], "" ); 1654assert! ( !output. status . success ()); 1655} 1656 1657# [ test ] 1658fn failed_caches_leave_no_npm_roots_or_artifacts () { 1659let server =Registry :: new (); 1660 server. publish_package ( 1661"broken" , 1662"1.0.0" , 1663 serde_json:: json!({ "dependencies" : { "inner" : "^1.0.0" }}), 1664& [( "index.js" , "export * from 'inner';" )], 1665); 1666 server. publish_package ( 1667"inner" , 1668"1.0.0" , 1669 serde_json:: json!({}), 1670& [( "index.js" , "export default ((((" )], 1671); 1672 server. publish_package ( 1673"uses-node" , 1674"1.0.0" , 1675 serde_json:: json!({}), 1676& [( "index.js" , "import 'node:fs'; export default 1;" )], 1677); 1678 server. publish_package ( "fine" , "1.0.0" , serde_json:: json!({}), & []); 1679for ( name, spec) in [ 1680( "bad-syntax" , "npm:broken@1" ), 1681( "node-builtin" , "npm:uses-node@1" ), 1682] { 1683let dir = tempfile:: tempdir (). unwrap (); 1684 fs:: write ( dir. path (). join ( "avril.conf" ), "deps: []" ). unwrap (); 1685 fs:: write ( 1686 dir. path (). join ( "main.ts" ), 1687format! ( "import '{spec}'; console.log('unreachable');" ), 1688) 1689. unwrap (); 1690let output = server. run_npm ( dir. path (), & [ "cache" ], "main.ts" ); 1691assert! ( !output. status . success (), "{name}" ); 1692let lock = fs:: read_to_string ( dir. path (). join ( "avril.lock" )). unwrap_or_default (); 1693assert! ( 1694 !lock. contains ( "broken" ) && !lock. contains ( "inner" ) && !lock. contains ( "uses-node" ), 1695"{name}: {lock}" 1696); 1697} 1698// The unrelated package still caches cleanly afterwards. 1699let dir = tempfile:: tempdir (). unwrap (); 1700 fs:: write ( dir. path (). join ( "avril.conf" ), "deps: []" ). unwrap (); 1701 fs:: write ( 1702 dir. path (). join ( "main.ts" ), 1703"import 'npm:fine@1'; console.log('fine');" , 1704) 1705. unwrap (); 1706assert_eq! ( 1707success ( & server. run_npm ( dir. path (), & [ "cache" ], "main.ts" )), 1708"" 1709); 1710let lock: serde_json:: Value = fixture:: read_lock ( dir. path ()); 1711assert_eq! ( lock[ "npm" ][ "specifiers" ][ "npm:fine@1" ], "1.0.0" ); 1712assert! ( lock[ "npm" ][ "specifiers" ]. as_object (). unwrap (). len () ==1 ); 1713} 1714 1715# [ test ] 1716fn peer_contexts_keep_separate_roots_across_rewrites () { 1717let server =Registry :: new (); 1718 server. publish_package ( 1719"plug" , 1720"1.0.0" , 1721 serde_json:: json!({ "peerDependencies" : { "host" : "*" }}), 1722& [( "index.js" , "export {version} from 'host';" )], 1723); 1724 server. publish_package ( 1725"host" , 1726"1.0.0" , 1727 serde_json:: json!({}), 1728& [( "index.js" , "export default ((((" )], 1729); 1730 server. publish_package ( 1731"host" , 1732"2.0.0" , 1733 serde_json:: json!({}), 1734& [( "index.js" , "export const version = 'h2';" )], 1735); 1736let dir = tempfile:: tempdir (). unwrap (); 1737 fs:: write ( 1738 dir. path (). join ( "avril.conf" ), 1739"deps: ['npm:plug@1', 'npm:host@1']" , 1740) 1741. unwrap (); 1742 fs:: write ( 1743 dir. path (). join ( "main.ts" ), 1744"import {version} from 'npm:plug@1'; console.log(version);" , 1745) 1746. unwrap (); 1747// The host@1 peer context resolves but never completes: nothing is saved. 1748let output = server. run_npm ( dir. path (), & [ "cache" ], "main.ts" ); 1749assert! ( !output. status . success ()); 1750let lock = fs:: read_to_string ( dir. path (). join ( "avril.lock" )). unwrap_or_default (); 1751assert! ( !lock. contains ( "plug" ) && !lock. contains ( "host" ), "{lock}" ); 1752// The host@2 context then caches; only its roots land in the lock. 1753 fs:: write ( 1754 dir. path (). join ( "avril.conf" ), 1755"deps: ['npm:plug@1', 'npm:host@2']" , 1756) 1757. unwrap (); 1758assert_eq! ( 1759success ( & server. run_npm ( dir. path (), & [ "cache" ], "main.ts" )), 1760"" 1761); 1762let lock: serde_json:: Value = fixture:: read_lock ( dir. path ()); 1763let roots = lock[ "npm" ][ "roots" ]. as_object (). unwrap (); 1764assert! ( 1765 roots. keys (). all ( |key| !key. contains ( "|host|1.0.0" )), 1766"{roots:?}" 1767); 1768assert_eq! ( 1769success ( & server. run_npm ( dir. path (), & [ "run" , "--offline" , "--frozen" ], "main.ts" )), 1770"h2\n" 1771); 1772} 1773 1774# [ test ] 1775fn two_add_invocations_stay_frozen_compatible () { 1776let server =Registry :: new (); 1777 server. publish_package ( 1778"plug" , 1779"1.0.0" , 1780 serde_json:: json!({ "peerDependencies" : { "host" : "^1.0.0" }}), 1781& [( "index.js" , "export {version} from 'host';" )], 1782); 1783 server. publish_package ( 1784"host" , 1785"1.0.0" , 1786 serde_json:: json!({}), 1787& [( "index.js" , "export const version = 'h1';" )], 1788); 1789let dir = tempfile:: tempdir (). unwrap (); 1790 fs:: write ( dir. path (). join ( "avril.conf" ), "deps: []" ). unwrap (); 1791assert! ( success ( & server. run_npm ( dir. path (), & [ "add" , "npm:plug@1" ], "" )). contains ( "plug" )); 1792assert! ( success ( & server. run_npm ( dir. path (), & [ "add" , "npm:host@1" ], "" )). contains ( "host" )); 1793 fs:: write ( 1794 dir. path (). join ( "main.ts" ), 1795"import {version} from 'plug'; console.log(version);" , 1796) 1797. unwrap (); 1798// Each add cached its config context, so the joint roots are pinned. 1799let before = server. requests . lock (). unwrap (). len (); 1800let run = server. run_npm ( dir. path (), & [ "run" , "--offline" , "--frozen" ], "main.ts" ); 1801assert_eq! ( success ( & run), "h1\n" ); 1802assert_eq! ( server. requests . lock (). unwrap (). len (), before); 1803} 1804 1805# [ test ] 1806fn dynamic_reimports_share_namespaces_when_frozen () { 1807let server =Registry :: new (); 1808 server. publish_package ( 1809"dyn" , 1810"1.0.0" , 1811 serde_json:: json!({}), 1812& [( "index.js" , "export const value = 'dyn';" )], 1813); 1814let dir = tempfile:: tempdir (). unwrap (); 1815 fs:: write ( 1816 dir. path (). join ( "main.ts" ), 1817"import {value} from 'npm:dyn@1'; const again = await import('npm:dyn@1'); console.log(value, again.value, again === (await import('npm:dyn@1')));" , 1818) 1819. unwrap (); 1820assert_eq! ( 1821success ( & server. run_npm ( dir. path (), & [ "cache" ], "main.ts" )), 1822"" 1823); 1824let before = server. requests . lock (). unwrap (). len (); 1825let run = server. run_npm ( dir. path (), & [ "run" , "--offline" , "--frozen" ], "main.ts" ); 1826assert_eq! ( success ( & run), "dyn dyn true\n" ); 1827assert_eq! ( server. requests . lock (). unwrap (). len (), before); 1828} 1829 1830# [ test ] 1831fn sibling_peer_contexts_keep_distinct_host_namespaces () { 1832let server =Registry :: new (); 1833// Each host instance counts its own evaluation: sharing one instance 1834// yields equal generations, while distinct peer environments diverge. 1835 server. publish_package ( 1836"plug" , 1837"1.0.0" , 1838 serde_json:: json!({ "peerDependencies" : { "host" : "*" }}), 1839& [( "index.js" , "export {generation as via} from 'host';" )], 1840); 1841 server. publish_package ( 1842"host" , 1843"1.0.0" , 1844 serde_json:: json!({ "peerDependencies" : { "flavor" : "*" }, "peerDependenciesMeta" : { "flavor" : { "optional" : true }}}), 1845& [ 1846( 1847"index.js" , 1848"globalThis.__hostEval = (globalThis.__hostEval ?? 0) + 1; export const generation = globalThis.__hostEval;" , 1849), 1850], 1851); 1852// Optional peers absent from a consumer stay absent: no fetch, no error. 1853 server. publish_package ( 1854"flavor" , 1855"1.0.0" , 1856 serde_json:: json!({}), 1857& [( "index.js" , "export const flavor = 'f';" )], 1858); 1859for ( cons, deps) in [ 1860( 1861"cons-a" , 1862 serde_json:: json!({ "dependencies" : { "plug" : "^1.0.0" , "host" : "^1.0.0" , "flavor" : "^1.0.0" }}), 1863), 1864( 1865"cons-b" , 1866 serde_json:: json!({ "dependencies" : { "plug" : "^1.0.0" , "host" : "^1.0.0" }}), 1867), 1868] { 1869 server. publish_package ( 1870 cons, 1871"1.0.0" , 1872 deps, 1873& [( 1874"index.js" , 1875"export {generation as direct} from 'host'; export {via as viaPlug} from 'plug';" , 1876)], 1877); 1878} 1879let dir = tempfile:: tempdir (). unwrap (); 1880 fs:: write ( 1881 dir. path (). join ( "avril.conf" ), 1882"deps: ['npm:cons-a@1', 'npm:cons-b@1']" , 1883) 1884. unwrap (); 1885 fs:: write ( 1886 dir. path (). join ( "main.ts" ), 1887"import {direct as aDirect, viaPlug as aVia} from 'cons-a'; import {direct as bDirect, viaPlug as bVia} from 'cons-b'; console.log(aDirect === aVia, bDirect === bVia, aDirect === bDirect);" , 1888) 1889. unwrap (); 1890assert_eq! ( 1891success ( & server. run_npm ( dir. path (), & [ "run" ], "main.ts" )), 1892"true true false\n" 1893); 1894let lock = fixture:: read_lock ( dir. path ()); 1895let default_registry = fs:: read_dir ( dir. path (). join ( ".avril/npm/default" )) 1896. unwrap () 1897. next () 1898. unwrap () 1899. unwrap () 1900. path (); 1901let mut peer_destinations =Vec :: new (); 1902for ( instance, pin) in lock[ "npm" ][ "instances" ]. as_object (). unwrap () { 1903let package = pin[ "package" ]. as_str (). unwrap (); 1904if instance == package || !instance. starts_with ( "p-" ) { 1905continue ; 1906} 1907let mut parts = package. split ( '|' ); 1908let _registry = parts. next (). unwrap (); 1909let name = parts. next (). unwrap (); 1910let version = parts. next (). unwrap (); 1911let source = default_registry. join ( name). join ( version); 1912let destination = dir 1913. path () 1914. join ( ".avril/npm" ) 1915. join ( instance) 1916. join ( default_registry. file_name (). unwrap ()) 1917. join ( name) 1918. join ( version); 1919assert! ( destination. join ( "package.json" ). is_file ()); 1920 fs:: remove_dir_all ( & destination). unwrap (); 1921 fs:: create_dir_all ( & destination). unwrap (); 1922copy_tree ( & source, & destination); 1923 peer_destinations. push ( destination); 1924} 1925assert_eq! ( 1926success ( & server. run_npm ( dir. path (), & [ "run" , "--offline" , "--frozen" ], "main.ts" )), 1927"true true false\n" 1928); 1929for entryin fs:: read_dir ( dir. path (). join ( ".avril/npm" )). unwrap () { 1930let entry = entry. unwrap (); 1931if entry. file_name (). to_string_lossy (). starts_with ( "p-" ) { 1932 fs:: remove_dir_all ( entry. path ()). unwrap (); 1933} 1934} 1935assert_eq! ( 1936success ( & server. run_npm ( dir. path (), & [ "run" , "--offline" , "--frozen" ], "main.ts" )), 1937"true true false\n" 1938); 1939assert! ( 1940 peer_destinations 1941. iter () 1942. all ( |destination| destination. join ( "package.json" ). is_file ()) 1943); 1944} 1945 1946# [ test ] 1947fn peer_instance_asset_urls_use_real_paths_and_read_authority () { 1948let server =Registry :: new (); 1949 server. publish_package ( "asset-host" , "1.0.0" , serde_json:: json!({}), & []); 1950 server. publish_package ( 1951"asset-plugin" , 1952"1.0.0" , 1953 serde_json:: json!({ "peerDependencies" : { "asset-host" : "*" }}), 1954& [ 1955( 1956"index.js" , 1957"import {readTextFile} from 'avril:fs'; export const asset = await readTextFile(new URL('./asset.txt', import.meta.url));" , 1958), 1959( "asset.txt" , "peer asset" ), 1960], 1961); 1962 server. publish_package ( 1963"asset-consumer" , 1964"1.0.0" , 1965 serde_json:: json!({ "dependencies" : { "asset-host" : "^1.0.0" , "asset-plugin" : "^1.0.0" }}), 1966& [( "index.js" , "export {asset} from 'asset-plugin';" )], 1967); 1968let dir = tempfile:: tempdir (). unwrap (); 1969 fs:: write ( 1970 dir. path (). join ( "avril.conf" ), 1971"deps: ['npm:asset-consumer@1']" , 1972) 1973. unwrap (); 1974 fs:: write ( 1975 dir. path (). join ( "main.ts" ), 1976"import {asset} from 'asset-consumer'; console.log(asset);" , 1977) 1978. unwrap (); 1979assert_eq! ( 1980success ( & server. run_npm ( dir. path (), & [ "run" , "--allow-read=.avril/npm" ], "main.ts" )), 1981"peer asset\n" 1982); 1983let denied = server. run_npm ( 1984 dir. path (), 1985& [ "run" , "--offline" , "--frozen" , "--deny-read=.avril/npm" ], 1986"main.ts" , 1987); 1988assert! ( failure ( & denied). contains ( "PermissionDenied: read" )); 1989let lock = fixture:: read_lock ( dir. path ()); 1990let ( instance, package) = lock[ "npm" ][ "instances" ] 1991. as_object () 1992. unwrap () 1993. iter () 1994. find_map ( |( instance, pin) |{ 1995let package = pin[ "package" ]. as_str () ?; 1996( package. contains ( "|asset-plugin|1.0.0" ) && instance. starts_with ( "p-" )) 1997. then_some (( instance, package)) 1998}) 1999. unwrap (); 2000let registry_dir = fs:: read_dir ( dir. path (). join ( ".avril/npm/default" )) 2001. unwrap () 2002. next () 2003. unwrap () 2004. unwrap () 2005. path (); 2006let mut parts = package. split ( '|' ); 2007let _registry = parts. next (). unwrap (); 2008let name = parts. next (). unwrap (); 2009let version = parts. next (). unwrap (); 2010let asset = dir 2011. path () 2012. join ( ".avril/npm" ) 2013. join ( instance) 2014. join ( registry_dir. file_name (). unwrap ()) 2015. join ( name) 2016. join ( version) 2017. join ( "asset.txt" ); 2018assert_eq! ( fs:: read_to_string ( & asset). unwrap (), "peer asset" ); 2019let grant =format! ( 2020"--allow-read={}" , 2021 asset. strip_prefix ( dir. path ()). unwrap (). display () 2022); 2023assert_eq! ( 2024success ( & server. run_npm ( 2025 dir. path (), 2026& [ "run" , "--offline" , "--frozen" , & grant], 2027"main.ts" 2028)), 2029"peer asset\n" 2030); 2031} 2032 2033# [ test ] 2034fn sibling_roots_share_one_host_instance () { 2035let server =Registry :: new (); 2036for versionin [ "1.0.0" , "2.0.0" ] { 2037 server. publish_package ( 2038"x" , 2039 version, 2040 serde_json:: json!({}), 2041& [( "index.js" , & format! ( "export const x = '{version}';" ))], 2042); 2043} 2044 server. publish_package ( 2045"host" , 2046"1.0.0" , 2047 serde_json:: json!({ "peerDependencies" : { "x" : "*" }}), 2048& [ 2049( 2050"index.js" , 2051"globalThis.__hostCount = (globalThis.__hostCount ?? 0) + 1; export const count = globalThis.__hostCount; export {x} from 'x';" , 2052), 2053], 2054); 2055 server. publish_package ( 2056"plugin" , 2057"1.0.0" , 2058 serde_json:: json!({ "peerDependencies" : { "host" : "*" }, "dependencies" : { "x" : "^2.0.0" }}), 2059& [( 2060"index.js" , 2061"export {count as via} from 'host'; export {x as own} from 'x';" , 2062)], 2063); 2064let dir = tempfile:: tempdir (). unwrap (); 2065// host and x@1 are selected alongside plugin: plugin's peer host must 2066// bind that exact host instance (x1) instead of reinstantiating host 2067// under its own x2 dependency. 2068 fs:: write ( 2069 dir. path (). join ( "avril.conf" ), 2070"deps: ['npm:host@1', 'npm:plugin@1', 'npm:x@1']" , 2071) 2072. unwrap (); 2073 fs:: write ( 2074 dir. path (). join ( "main.ts" ), 2075"import {count as direct} from 'host'; import {via, own} from 'plugin'; console.log(direct, via, own);" , 2076) 2077. unwrap (); 2078assert_eq! ( 2079success ( & server. run_npm ( dir. path (), & [ "run" ], "main.ts" )), 2080"1 1 2.0.0\n" 2081); 2082} 2083 2084# [ test ] 2085fn add_reports_progress_phases_and_downloads () { 2086let server =Registry :: new (); 2087 server. publish_package ( 2088"tool" , 2089"1.0.0" , 2090 serde_json:: json!({ "exports" : { " . " : "./mod.js" }}), 2091& [( "mod.js" , "export default 1;" )], 2092); 2093 server. publish_package ( 2094"tool" , 2095"1.1.0" , 2096 serde_json:: json!({ "exports" : { " . " : "./mod.js" }}), 2097& [( "mod.js" , "export default 2;" )], 2098); 2099 server. package ( 2100"@test/pkg" , 2101"1.0.0" , 2102& [( "mod.ts" , "export default 1;" )], 2103& [( "." , "./mod.ts" )], 2104); 2105let dir = tempfile:: tempdir (). unwrap (); 2106 fs:: write ( dir. path (). join ( "avril.conf" ), "deps: []" ). unwrap (); 2107let output = server. run_npm ( dir. path (), & [ "add" , "npm:tool" ], "" ); 2108let stdout =success ( & output); 2109assert! ( 2110 stdout. contains ( "\"tool\": \"npm:tool@^1.1.0\"" ), 2111"{output:?}" 2112); 2113assert! ( 2114 !stdout. contains ( "Selecting" ), 2115"stdout keeps only the mapping: {stdout:?}" 2116); 2117let stderr =String :: from_utf8_lossy ( & output. stderr ). into_owned (); 2118for needlein [ "Selecting" , "Caching" , "tool" ] { 2119assert! ( 2120 stderr. contains ( needle), 2121"stderr must show {needle}: {stderr:?}" 2122); 2123} 2124assert! ( 2125 stderr. contains ( "Fetching" ) || stderr. contains ( "Downloading" ), 2126"stderr must identify package downloads: {stderr:?}" 2127); 2128let dir = tempfile:: tempdir (). unwrap (); 2129 fs:: write ( dir. path (). join ( "avril.conf" ), "deps: []" ). unwrap (); 2130let output = server. run_npm ( dir. path (), & [ "add" , "jsr:@test/pkg" ], "" ); 2131let stdout =success ( & output); 2132assert! ( stdout. contains ( "jsr:@test/pkg@^1.0.0" ), "{output:?}" ); 2133let stderr =String :: from_utf8_lossy ( & output. stderr ). into_owned (); 2134for needlein [ "Selecting" , "Caching" ] { 2135assert! ( 2136 stderr. contains ( needle), 2137"jsr add must show {needle}: {stderr:?}" 2138); 2139} 2140} 2141 2142# [ test ] 2143fn add_failure_keeps_prior_progress_and_error () { 2144let server =Registry :: new (); 2145let dir = tempfile:: tempdir (). unwrap (); 2146 fs:: write ( dir. path (). join ( "avril.conf" ), "deps: []" ). unwrap (); 2147let output = server. run_npm ( dir. path (), & [ "add" , "npm:missing" ], "" ); 2148let stderr =failure ( & output); 2149assert! ( 2150 stderr. contains ( "Selecting" ), 2151"prior progress must remain: {stderr:?}" 2152); 2153assert! ( 2154 stderr. contains ( "404" ), 2155"registry failure must be preserved: {stderr:?}" 2156); 2157} 2158 2159# [ test ] 2160fn add_warm_offline_does_not_claim_downloads () { 2161let server =Registry :: new (); 2162 server. publish_package ( 2163"tool" , 2164"1.0.0" , 2165 serde_json:: json!({}), 2166& [( "index.js" , "export default 1;" )], 2167); 2168let dir = tempfile:: tempdir (). unwrap (); 2169 fs:: write ( dir. path (). join ( "avril.conf" ), "deps: []" ). unwrap (); 2170let output = server. run_npm ( dir. path (), & [ "add" , "npm:tool" ], "" ); 2171assert! ( output. status . success ()); 2172let first_stderr =String :: from_utf8_lossy ( & output. stderr ). into_owned (); 2173assert! ( 2174 first_stderr. contains ( "Downloading" ) || first_stderr. contains ( "Fetching" ), 2175"first add must show network work: {first_stderr:?}" 2176); 2177let output = server. run_npm ( dir. path (), & [ "add" , "--offline" , "npm:tool" ], "" ); 2178assert! ( 2179 output. status . success (), 2180"{}" , 2181String :: from_utf8_lossy ( & output. stderr ) 2182); 2183let stderr =String :: from_utf8_lossy ( & output. stderr ). into_owned (); 2184assert! ( 2185 !stderr. contains ( "Downloading" ) && !stderr. contains ( "Fetching" ), 2186"warm offline add must not claim network downloads: {stderr:?}" 2187); 2188} 2189 2190# [ test ] 2191fn other_commands_stay_silent_without_progress () { 2192let server =Registry :: new (); 2193 server. publish_package ( 2194"tool" , 2195"1.0.0" , 2196 serde_json:: json!({}), 2197& [( "index.js" , "export default 1;" )], 2198); 2199let dir = tempfile:: tempdir (). unwrap (); 2200 fs:: write ( dir. path (). join ( "avril.conf" ), "deps: ['npm:tool@1']" ). unwrap (); 2201 fs:: write ( 2202 dir. path (). join ( "main.ts" ), 2203"import value from 'tool'; console.log(value);" , 2204) 2205. unwrap (); 2206let output = server. run_npm ( dir. path (), & [ "run" ], "main.ts" ); 2207assert_eq! ( success ( & output), "1\n" ); 2208let stderr =String :: from_utf8_lossy ( & output. stderr ). into_owned (); 2209for needlein [ 2210"Selecting" , 2211"Fetching" , 2212"Downloading" , 2213"Materialized" , 2214"Caching" , 2215] { 2216assert! ( !stderr. contains ( needle), "run must stay silent: {stderr:?}" ); 2217} 2218} 2219 2220# [ test ] 2221fn compact_pin_end_to_end () { 2222let server =Registry :: new (); 2223 server. publish_package ( 2224"compact" , 2225"1.0.0" , 2226 serde_json:: json!({}), 2227& [( "index.js" , "export default 1;" )], 2228); 2229let dir = tempfile:: tempdir (). unwrap (); 2230 fs:: write ( dir. path (). join ( "avril.conf" ), "deps: ['npm:compact@1']" ). unwrap (); 2231 fs:: write ( 2232 dir. path (). join ( "main.ts" ), 2233"import value from 'compact'; console.log(value);" , 2234) 2235. unwrap (); 2236assert_eq! ( 2237success ( & server. run_npm ( dir. path (), & [ "run" ], "main.ts" )), 2238"1\n" 2239); 2240let lock: serde_json:: Value = fixture:: read_lock ( dir. path ()); 2241let pins = lock[ "npm" ][ "packages" ]. as_object (). unwrap (); 2242assert_eq! ( pins. len (), 1 ); 2243let pin = pins. values (). next (). unwrap (); 2244assert! ( 2245 pin[ "manifest" ] 2246. as_str () 2247. is_some_and ( |digest| digest. starts_with ( "sha256-" )), 2248"{pin}" 2249); 2250assert! ( pin. get ( "files" ). is_none (), "{pin}" ); 2251} 2252 2253# [ test ] 2254fn add_selects_before_fetching_broken_archives () { 2255let server =Registry :: new (); 2256 server. publish_package ( 2257"tool" , 2258"1.0.0" , 2259 serde_json:: json!({ "exports" : { " . " : "./mod.js" }}), 2260& [( "mod.js" , "export default 1;" )], 2261); 2262{ 2263let mut files = server. files . lock (). unwrap (); 2264let tarball = files 2265. keys () 2266. find ( |path| path. ends_with ( ".tgz" )) 2267. unwrap () 2268. clone (); 2269 files. insert ( tarball, b"not gzip" . to_vec ()); 2270} 2271let dir = tempfile:: tempdir (). unwrap (); 2272 fs:: write ( dir. path (). join ( "avril.conf" ), "deps: []" ). unwrap (); 2273let output = server. run_npm ( dir. path (), & [ "add" , "npm:tool" ], "" ); 2274assert! ( !output. status . success ()); 2275let stdout =String :: from_utf8_lossy ( & output. stdout ). into_owned (); 2276assert! ( 2277 stdout. contains ( "npm:tool@^1.0.0" ), 2278"config must be reported: {output:?}" 2279); 2280let config = fs:: read_to_string ( dir. path (). join ( "avril.conf" )). unwrap (); 2281assert! ( 2282 config. contains ( "npm:tool@^1.0.0" ), 2283"caret must be written: {config:?}" 2284); 2285let stderr =String :: from_utf8_lossy ( & output. stderr ). into_owned (); 2286assert! ( 2287 stderr. contains ( "Selecting" ), 2288"selection must precede warming: {stderr:?}" 2289); 2290} 2291 2292# [ test ] 2293fn add_shares_packument_between_selection_and_warming () { 2294let server =Registry :: new (); 2295 server. publish_package ( 2296"tool" , 2297"1.0.0" , 2298 serde_json:: json!({ "exports" : { " . " : "./mod.js" }}), 2299& [( "mod.js" , "export default 1;" )], 2300); 2301 server. publish_package ( 2302"tool" , 2303"1.1.0" , 2304 serde_json:: json!({ "exports" : { " . " : "./mod.js" }}), 2305& [( "mod.js" , "export default 2;" )], 2306); 2307let dir = tempfile:: tempdir (). unwrap (); 2308 fs:: write ( dir. path (). join ( "avril.conf" ), "deps: []" ). unwrap (); 2309let output = server. run_npm ( dir. path (), & [ "add" , "npm:tool" ], "" ); 2310assert! ( success ( & output). contains ( "npm:tool@^1.1.0" ), "{output:?}" ); 2311let lock: serde_json:: Value = fixture:: read_lock ( dir. path ()); 2312assert_eq! ( lock[ "npm" ][ "specifiers" ][ "npm:tool@^1.1.0" ], "1.1.0" ); 2313let packuments = server 2314. requests 2315. lock () 2316. unwrap () 2317. iter () 2318. filter ( |path| path. as_str () =="/tool" ) 2319. count (); 2320assert_eq! ( packuments, 1 , "selection and warming must share one fetch" ); 2321} 2322 2323# [ test ] 2324fn add_warns_and_skips_node_builtin_edges () { 2325let server =Registry :: new (); 2326 server. publish_package ( 2327"envtool" , 2328"1.0.0" , 2329 serde_json:: json!({ "exports" : { 2330"." : "./mod.js" , 2331"./other" : "./other.js" , 2332"./sibling" : "./sibling.js" , 2333}}), 2334& [ 2335( 2336"mod.js" , 2337"import cp from 'node:child_process';\nimport {sibling} from './sibling.js';\nexport default [cp, sibling];" , 2338), 2339( 2340"other.js" , 2341"import fs from 'node:fs';\nexport const lazy = await import('node:fs');\nexport default fs;" , 2342), 2343( "sibling.js" , "export const sibling = 1;" ), 2344], 2345); 2346let dir = tempfile:: tempdir (). unwrap (); 2347 fs:: write ( dir. path (). join ( "avril.conf" ), "deps: []" ). unwrap (); 2348let output = server. run_npm ( dir. path (), & [ "add" , "npm:envtool" ], "" ); 2349assert! ( 2350success ( & output). contains ( "\"envtool\": \"npm:envtool@^1.0.0\"" ), 2351"{output:?}" 2352); 2353let stderr =String :: from_utf8_lossy ( & output. stderr ). into_owned (); 2354assert! ( stderr. contains ( "Warning" ), "{stderr:?}" ); 2355assert! ( stderr. contains ( "node:child_process" ), "{stderr:?}" ); 2356assert_eq! ( stderr. matches ( "node:fs" ). count (), 1 , "{stderr:?}" ); 2357assert_eq! ( 2358 stderr. matches ( "node:child_process" ). count (), 23591 , 2360"{stderr:?}" 2361); 2362let lock: serde_json:: Value = fixture:: read_lock ( dir. path ()); 2363assert_eq! ( lock[ "npm" ][ "specifiers" ][ "npm:envtool@^1.0.0" ], "1.0.0" ); 2364 fs:: write ( 2365 dir. path (). join ( "main.ts" ), 2366"import {sibling} from 'envtool/sibling'; console.log(sibling);" , 2367) 2368. unwrap (); 2369let output = server. run_npm ( dir. path (), & [ "run" , "--offline" , "--frozen" ], "main.ts" ); 2370assert_eq! ( success ( & output), "1\n" ); 2371} 2372 2373# [ test ] 2374fn add_still_fails_on_broken_edges_despite_builtin_warnings () { 2375let server =Registry :: new (); 2376 server. publish_package ( 2377"envbad" , 2378"1.0.0" , 2379 serde_json:: json!({}), 2380& [( 2381"index.js" , 2382"import 'node:fs';\nimport './nope.js';\nexport default 1;" , 2383)], 2384); 2385 server. publish_package ( 2386"needsdep" , 2387"1.0.0" , 2388 serde_json:: json!({ "dependencies" : { "gone" : "^1.0.0" }}), 2389& [( "index.js" , "export default 1;" )], 2390); 2391 server. publish_package ( 2392"envsyntax" , 2393"1.0.0" , 2394 serde_json:: json!({}), 2395& [ 2396( 2397"index.js" , 2398"import 'node:fs';\nimport './broken.js';\nexport default 1;" , 2399), 2400( "broken.js" , "export const = ;\n" ), 2401], 2402); 2403 server. publish_package ( 2404"badexport" , 2405"1.0.0" , 2406 serde_json:: json!({ "exports" : { " . " : "./index.js" , "./bad" : "./missing.js" }}), 2407& [( "index.js" , "export default 1;" )], 2408); 2409for ( target, fatal) in [( "npm:envbad" , "nope.js" ), ( "npm:envsyntax" , "broken.js" )] { 2410let dir = tempfile:: tempdir (). unwrap (); 2411 fs:: write ( dir. path (). join ( "avril.conf" ), "deps: []" ). unwrap (); 2412let output = server. run_npm ( dir. path (), & [ "add" , target], "" ); 2413let stderr =failure ( & output); 2414assert! ( 2415 stderr. contains ( "Warning" ) && stderr. contains ( "node:fs" ), 2416"{target}: {stderr:?}" 2417); 2418assert! ( 2419 stderr. contains ( fatal), 2420"{target}: underlying failure must be reported, not just the warning: {stderr:?}" 2421); 2422} 2423for targetin [ "npm:needsdep" , "npm:badexport" ] { 2424let dir = tempfile:: tempdir (). unwrap (); 2425 fs:: write ( dir. path (). join ( "avril.conf" ), "deps: []" ). unwrap (); 2426let output = server. run_npm ( dir. path (), & [ "add" , target], "" ); 2427assert! ( !output. status . success (), "{target}" ); 2428} 2429} 2430 2431# [ test ] 2432fn builtin_edges_stay_fatal_outside_add () { 2433let server =Registry :: new (); 2434 server. publish_package ( 2435"envtool" , 2436"1.0.0" , 2437 serde_json:: json!({}), 2438& [( "index.js" , "import fs from 'node:fs';\nexport default fs;" )], 2439); 2440let dir = tempfile:: tempdir (). unwrap (); 2441 fs:: write ( dir. path (). join ( "avril.conf" ), "deps: []" ). unwrap (); 2442let output = server. run_npm ( dir. path (), & [ "add" , "npm:envtool" ], "" ); 2443assert! ( output. status . success ()); 2444 fs:: write ( 2445 dir. path (). join ( "main.ts" ), 2446"import fs from 'envtool'; console.log(fs);" , 2447) 2448. unwrap (); 2449for argsin [ 2450& [ "run" ] as & [ & str ], 2451& [ "cache" ], 2452& [ "run" , "--offline" , "--frozen" ], 2453] { 2454let output = server. run_npm ( dir. path (), args, "main.ts" ); 2455let stderr =failure ( & output); 2456assert! ( 2457 stderr. contains ( "builtin modules are not supported" ), 2458"{args:?}: {stderr:?}" 2459); 2460assert! ( !stderr. contains ( "Warning" ), "{args:?}: {stderr:?}" ); 2461} 2462}