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 avril_runtime:: registry::{ 5PackageStore , 6 npm:: resolution::{ NpmResolver , NpmRootRequest , ResolutionMode , ResolvedNpmModule }, 7}; 8use fixture:: Registry ; 9use futures:: executor:: block_on; 10use std:: collections:: BTreeMap ; 11use std:: sync:: Arc ; 12 13fn publish_stub ( server : & Registry , name : & str , manifest : serde_json:: Value , paths : & [ & str ]) { 14let mut sources: Vec < _ > = paths. iter (). map ( |path|( * path, "export {};" )). collect (); 15if !paths. contains ( & "index.js" ) { 16 sources. push (( "index.js" , "export default 0;" )); 17} 18 server. publish_package ( name, "1.0.0" , manifest, & sources); 19} 20 21fn resolve_types ( resolver : & NpmResolver , request : & str ) ->ResolvedNpmModule { 22block_on ( resolver. resolve_request ( request, ResolutionMode :: TypeImport )). unwrap () 23} 24 25# [ test ] 26fn type_resolution_can_read_declarations_without_changing_execution_behavior () { 27use avril_runtime:: registry:: npm:: package:: NpmModuleKind ; 28 29let server =Registry :: new (); 30publish_stub ( 31& server, 32"declared-types" , 33 serde_json:: json!({ "types" : "./index.d.ts" }), 34& [ "index.d.ts" ], 35); 36let dir = tempfile:: tempdir (). unwrap (); 37let resolver = server. resolver ( dir. path ()); 38let root =resolve ( & resolver, "npm:declared-types@1" ); 39let declaration = resolver 40. resolve_from_file ( "./index.d.ts" , & root. path , ResolutionMode :: TypeImport ) 41. unwrap (); 42assert_eq! ( declaration. format , NpmModuleKind :: Declaration ); 43assert! ( 44 resolver 45. resolve_from_file ( "./index.d.ts" , & root. path , ResolutionMode :: Import ) 46. is_err () 47); 48} 49 50# [ test ] 51fn package_declared_types_and_declaration_extension_substitution_resolve () { 52use avril_runtime:: registry:: npm:: package:: NpmModuleKind ; 53 54let server =Registry :: new (); 55publish_stub ( 56& server, 57"types-entry" , 58 serde_json:: json!({ 59"types" : "types/main.d.ts" , 60"typings" : "./types/legacy.d.ts" 61}), 62& [ 63"types/main.d.ts" , 64"types/legacy.d.ts" , 65"src/value.d.ts" , 66"index.d.ts" , 67], 68); 69let dir = tempfile:: tempdir (). unwrap (); 70let resolver = server. resolver ( dir. path ()); 71let entry =resolve_types ( & resolver, "npm:types-entry@1" ); 72assert! ( entry. path . ends_with ( "types/main.d.ts" )); 73assert_eq! ( entry. format , NpmModuleKind :: Declaration ); 74let reference = resolver 75. resolve_from_file ( "../src/value.js" , & entry. path , ResolutionMode :: TypeImport ) 76. unwrap (); 77assert! ( reference. path . ends_with ( "src/value.d.ts" )); 78assert_eq! ( reference. format , NpmModuleKind :: Declaration ); 79assert! ( 80 resolver 81. resolve_from_file ( "../src/value.js" , & entry. path , ResolutionMode :: Import ) 82. is_err () 83); 84} 85 86# [ test ] 87fn types_versions_requires_an_explicit_backend_version_and_only_affects_types () { 88let server =Registry :: new (); 89publish_stub ( 90& server, 91"types-versions-version" , 92 serde_json:: json!({ 93"types" : "./types/index.d.ts" , 94"typesVersions" : { ">=5.0.0 <6.0.0" : { "types/index.d.ts" : [ "ts5/index.d.ts" ] } } 95}), 96& [ "index.js" , "types/index.d.ts" , "ts5/index.d.ts" ], 97); 98let dir = tempfile:: tempdir (). unwrap (); 99let resolver = server. resolver ( dir. path ()); 100let runtime =resolve ( & resolver, "npm:types-versions-version@1" ); 101assert! ( runtime. path . ends_with ( "index.js" )); 102let request ="npm:types-versions-version@1" ; 103let missing = 104block_on ( resolver. resolve_request ( request, ResolutionMode :: TypeImport )). unwrap_err (); 105assert! ( missing. to_string (). contains ( "set_typescript_version" )); 106assert! ( resolver. set_typescript_version ( "latest" ). is_err ()); 107 resolver. set_typescript_version ( "5.4.3" ). unwrap (); 108let typed =block_on ( resolver. resolve_request ( request, ResolutionMode :: TypeImport )). unwrap (); 109assert! ( typed. path . ends_with ( "ts5/index.d.ts" )); 110assert! ( 111resolve ( & resolver, "npm:types-versions-version@1" ) 112. path 113. ends_with ( "index.js" ) 114); 115} 116 117# [ test ] 118fn types_versions_uses_first_matching_range_exact_and_best_prefix_patterns () { 119let server =Registry :: new (); 120publish_stub ( 121& server, 122"types-versions-patterns" , 123 serde_json:: json!({ 124"types" : "./types/main.d.ts" , 125"typings" : "./types/legacy.d.ts" , 126"typesVersions" : { 127">=5.0.0" : { 128"types/main.d.ts" : [ "ts5/missing.d.ts" , "ts5/main.d.ts" ], 129"feature/*" : [ "ts5/general/*.d.ts" ], 130"feature/*.js" : [ "ts5/suffix/*.d.ts" ], 131"feature/specific/*" : [ "ts5/specific/*.d.ts" ], 132"exact" : [ "ts5/exact.d.ts" ] 133}, 134">=4.0.0" : { "types/main.d.ts" : [ "ts4/main.d.ts" ] } 135} 136}), 137& [ 138"types/main.d.ts" , 139"types/legacy.d.ts" , 140"ts5/main.d.ts" , 141"ts4/main.d.ts" , 142"ts5/general/other.d.ts" , 143"ts5/specific/item.d.ts" , 144"ts5/exact.d.ts" , 145"ts5/general/other.js.d.ts" , 146"ts5/suffix/other.d.ts" , 147"feature/specific/fallback.d.ts" , 148], 149); 150let dir = tempfile:: tempdir (). unwrap (); 151let resolver = server. resolver ( dir. path ()); 152 resolver. set_typescript_version ( "5.4.2" ). unwrap (); 153let roots =block_on ( resolver. prepare ( & [ NpmRootRequest { 154request : "npm:types-versions-patterns@1" . into (), 155context : BTreeMap :: new (), 156}])) 157. unwrap (); 158for ( subpath, target) in [ 159( "" , "ts5/main.d.ts" ), 160( "/feature/other" , "ts5/general/other.d.ts" ), 161( "/feature/specific/item" , "ts5/specific/item.d.ts" ), 162( "/feature/other.js" , "ts5/general/other.js.d.ts" ), 163( "/exact" , "ts5/exact.d.ts" ), 164( 165"/feature/specific/fallback" , 166"feature/specific/fallback.d.ts" , 167), 168] { 169let request =format! ( "npm:types-versions-patterns@1{subpath}" ); 170let resolved = resolver 171. resolve_in ( & roots, & request, ResolutionMode :: TypeImport ) 172. unwrap (); 173assert! ( 174 resolved. path . ends_with ( target), 175"{request}: {}" , 176 resolved. path . display () 177); 178} 179} 180 181# [ test ] 182fn types_versions_no_match_and_malformed_metadata_fall_back_without_affecting_runtime () { 183let server =Registry :: new (); 184publish_stub ( 185& server, 186"types-versions-fallback" , 187 serde_json:: json!({ 188"types" : "./types/index.d.ts" , 189"typesVersions" : { 190"not a range" : { "*" : [ "ts5/*.d.ts" ] }, 191">=5.0.0" : { " * " : [ "../escape . d . ts " ] } 192} 193}), 194& [ "index.js" , "types/index.d.ts" ], 195); 196let dir = tempfile:: tempdir (). unwrap (); 197let resolver = server. resolver ( dir. path ()); 198assert! ( 199resolve ( & resolver, "npm:types-versions-fallback@1" ) 200. path 201. ends_with ( "index.js" ) 202); 203let module =block_on ( 204 resolver. resolve_request ( "npm:types-versions-fallback@1" , ResolutionMode :: TypeImport ), 205) 206. unwrap (); 207assert! ( module. path . ends_with ( "types/index.d.ts" )); 208 209publish_stub ( 210& server, 211"types-versions-no-match" , 212 serde_json:: json!({ 213"types" : "./types/base.d.ts" , 214"typesVersions" : { ">=5.0.0" : { "*" : [ "ts5/*" ] } } 215}), 216& [ "types/base.d.ts" , "ts5/index.d.ts" ], 217); 218let no_match =block_on ( 219 resolver. resolve_request ( "npm:types-versions-no-match@1" , ResolutionMode :: TypeImport ), 220); 221assert! ( no_match. is_err ()); 222 resolver. set_typescript_version ( "4.9.5" ). unwrap (); 223let no_match =resolve_types ( & resolver, "npm:types-versions-no-match@1" ); 224assert! ( no_match. path . ends_with ( "types/base.d.ts" )); 225} 226 227# [ test ] 228fn exports_precede_types_versions_and_do_not_require_a_backend_version () { 229let server =Registry :: new (); 230publish_stub ( 231& server, 232"types-versions-exports" , 233 serde_json:: json!({ 234"exports" : { "." : { "types" : "./exports.d.ts" , "default" : "./index.js" } }, 235"typesVersions" : { " * " : { " * " : [ "mapped/*" ] } } 236}), 237& [ "index.js" , "exports.d.ts" , "mapped/index.d.ts" ], 238); 239let dir = tempfile:: tempdir (). unwrap (); 240let resolver = server. resolver ( dir. path ()); 241let typed =resolve_types ( & resolver, "npm:types-versions-exports@1" ); 242assert! ( typed. path . ends_with ( "exports.d.ts" )); 243} 244 245# [ test ] 246fn types_versions_matches_typescript_prereleases_like_the_compiler () { 247let server =Registry :: new (); 248let cases =[ 249( "wildcard" , "*" , "7.1.0-dev.20261013.1" , true ), 250( "partial_lower" , ">=4.9" , "4.9.0-beta" , true ), 251( "partial_upper" , "<4.9" , "4.9.0-beta" , false ), 252( "explicit_upper" , "<4.9.0" , "4.9.0-beta" , true ), 253( "explicit_lower" , ">=4.9.0" , "4.9.0-beta" , false ), 254( "spaced" , ">= 4.9 < 5" , "4.9.0-beta" , true ), 255( 256"prerelease_bound" , 257">=7.1.0-dev.20261013.1" , 258"7.1.0-dev.20261013.2" , 259true , 260), 261( "tilde_upper" , "~4.9" , "4.10.0-beta" , true ), 262( "tilde_lower" , "~4.9" , "4.9.0-beta" , false ), 263( "caret_upper" , "^4.9" , "5.0.0-beta" , true ), 264( "hyphen_upper" , "4.9 - 5" , "6.0.0-beta" , true ), 265( "hyphen_exact" , "4.9 - 5.0.0-beta" , "5.0.0" , false ), 266( "prefixed_partial" , "v4.9" , "4.9.0" , false ), 267( 268"wildcard_prerelease_lower" , 269"4.9.x-beta" , 270"4.9.0-alpha" , 271true , 272), 273( 274"wildcard_prerelease_upper" , 275"4.9.x-beta" , 276"4.10.0-beta" , 277false , 278), 279]; 280for ( name, range, version, mapped) in cases{ 281let package =format! ( "types-versions-prerelease-{name}" ); 282let mut types_versions = serde_json:: Map :: new (); 283 types_versions. insert ( 284 range. to_owned (), 285 serde_json:: json!({ "types/base.d.ts" : [ "mapped.d.ts" ] }), 286); 287publish_stub ( 288& server, 289& package, 290 serde_json:: json!({ 291"types" : "./types/base.d.ts" , 292"typesVersions" : types_versions 293}), 294& [ "types/base.d.ts" , "mapped.d.ts" ], 295); 296let dir = tempfile:: tempdir (). unwrap (); 297let resolver = server. resolver ( dir. path ()); 298 resolver. set_typescript_version ( version). unwrap (); 299let request =format! ( "npm:{package}@1" ); 300let resolved =resolve_types ( & resolver, & request); 301assert_eq! ( 302 resolved. path . ends_with ( "mapped.d.ts" ), 303 mapped, 304"{range} with {version}" 305); 306} 307} 308 309# [ test ] 310fn legacy_type_subpaths_resolve_declarations_without_runtime_files () { 311let server =Registry :: new (); 312publish_stub ( 313& server, 314"legacy-types-subpaths" , 315 serde_json:: json!({}), 316& [ "sub.d.ts" , "nested/index.d.ts" ], 317); 318let dir = tempfile:: tempdir (). unwrap (); 319let resolver = server. resolver ( dir. path ()); 320let roots =block_on ( resolver. prepare ( & [ NpmRootRequest { 321request : "npm:legacy-types-subpaths@1" . into (), 322context : BTreeMap :: new (), 323}])) 324. unwrap (); 325assert! ( 326 resolver 327. resolve_in ( 328& roots, 329"npm:legacy-types-subpaths@1/sub" , 330ResolutionMode :: TypeImport 331) 332. unwrap () 333. path 334. ends_with ( "sub.d.ts" ) 335); 336assert! ( 337 resolver 338. resolve_in ( 339& roots, 340"npm:legacy-types-subpaths@1/nested" , 341ResolutionMode :: TypeImport 342) 343. unwrap () 344. path 345. ends_with ( "nested/index.d.ts" ) 346); 347} 348 349# [ test ] 350fn malformed_type_fields_do_not_break_execution_but_fail_type_resolution () { 351let server =Registry :: new (); 352publish_stub ( 353& server, 354"malformed-types" , 355 serde_json:: json!({ "types" : 17 }), 356& [ "index.js" ], 357); 358let dir = tempfile:: tempdir (). unwrap (); 359let resolver = server. resolver ( dir. path ()); 360let runtime =resolve ( & resolver, "npm:malformed-types@1" ); 361assert! ( runtime. path . ends_with ( "index.js" )); 362let error = 363block_on ( resolver. resolve_request ( "npm:malformed-types@1" , ResolutionMode :: TypeImport )) 364. unwrap_err (); 365assert! ( error. to_string (). contains ( "types" ), "{error}" ); 366} 367 368# [ test ] 369fn hash_import_type_targets_use_declaration_substitution () { 370let server =Registry :: new (); 371publish_stub ( 372& server, 373"hash-types" , 374 serde_json:: json!({ "imports" : { "#types" : "./types/alias.js" }}), 375& [ "types/alias.d.ts" ], 376); 377let dir = tempfile:: tempdir (). unwrap (); 378let resolver = server. resolver ( dir. path ()); 379let root =resolve ( & resolver, "npm:hash-types@1" ); 380let declaration = resolver 381. resolve_from_file ( "#types" , & root. path , ResolutionMode :: TypeImport ) 382. unwrap (); 383assert! ( declaration. path . ends_with ( "types/alias.d.ts" )); 384} 385 386# [ test ] 387fn declaration_import_equals_require_uses_require_type_conditions () { 388let server =Registry :: new (); 389publish_stub ( 390& server, 391"require-types-dep" , 392 serde_json:: json!({ 393"exports" : { " . " : { "require" : { "types" : "./index.d.cts" , "default" : "./index.cjs" }}} 394}), 395& [ "index.d.cts" ], 396); 397publish_stub ( 398& server, 399"require-types-owner" , 400 serde_json:: json!({ 401"types" : "./index.d.ts" , 402"dependencies" : { "require-types-dep" : "1.0.0" } 403}), 404& [ "index.d.ts" ], 405); 406let dir = tempfile:: tempdir (). unwrap (); 407let resolver = server. resolver ( dir. path ()); 408let declaration =resolve_types ( & resolver, "npm:require-types-owner@1" ); 409let dep = resolver 410. resolve_from_file ( 411"require-types-dep" , 412& declaration. path , 413ResolutionMode :: TypeRequire , 414) 415. unwrap (); 416assert! ( dep. path . ends_with ( "index.d.cts" )); 417} 418 419# [ test ] 420fn declaration_index_is_the_legacy_fallback_without_declared_entry_fields () { 421let server =Registry :: new (); 422publish_stub ( 423& server, 424"types-index-fallback" , 425 serde_json:: json!({ "main" : "./runtime.js" }), 426& [ "runtime.js" , "index.d.ts" ], 427); 428let dir = tempfile:: tempdir (). unwrap (); 429let resolver = server. resolver ( dir. path ()); 430let module =resolve_types ( & resolver, "npm:types-index-fallback@1" ); 431assert! ( module. path . ends_with ( "index.d.ts" )); 432} 433 434# [ test ] 435fn exports_type_conditions_keep_import_require_and_encapsulation () { 436let server =Registry :: new (); 437publish_stub ( 438& server, 439"types-conditions" , 440 serde_json:: json!({ 441"exports" : { 442"." : { 443"import" : { "types" : "./types/import.d.ts" , "default" : "./index.js" }, 444"require" : { "types" : "./types/require.d.cts" , "default" : "./index.cjs" } 445}, 446"./public" : { "types" : "./types/public.d.ts" , "default" : "./public.js" } 447} 448}), 449& [ 450"types/import.d.ts" , 451"types/require.d.cts" , 452"types/public.d.ts" , 453"private.d.ts" , 454], 455); 456let dir = tempfile:: tempdir (). unwrap (); 457let resolver = server. resolver ( dir. path ()); 458let roots =block_on ( resolver. prepare ( & [ NpmRootRequest { 459request : "npm:types-conditions@1" . into (), 460context : BTreeMap :: new (), 461}])) 462. unwrap (); 463let import = resolver 464. resolve_in ( & roots, "npm:types-conditions@1" , ResolutionMode :: TypeImport ) 465. unwrap (); 466let require = resolver 467. resolve_in ( 468& roots, 469"npm:types-conditions@1" , 470ResolutionMode :: TypeRequire , 471) 472. unwrap (); 473let require_from_file = resolver 474. resolve_from_file ( 475& require. path . display (). to_string (), 476& import. path , 477ResolutionMode :: TypeRequire , 478) 479. unwrap (); 480assert! ( require_from_file. path . ends_with ( "types/require.d.cts" )); 481assert! ( import. path . ends_with ( "types/import.d.ts" )); 482assert! ( require. path . ends_with ( "types/require.d.cts" )); 483let public = resolver 484. resolve_in ( 485& roots, 486"npm:types-conditions@1/public" , 487ResolutionMode :: TypeImport , 488) 489. unwrap (); 490assert! ( public. path . ends_with ( "types/public.d.ts" )); 491assert! ( 492 resolver 493. resolve_in ( 494& roots, 495"npm:types-conditions@1/private.d.ts" , 496ResolutionMode :: TypeImport 497) 498. is_err () 499); 500} 501 502# [ test ] 503fn warm_instance_checks_its_own_nested_metadata () { 504let server =Registry :: new (); 505 server. publish_package ( 506"instance-metadata" , 507"1.0.0" , 508 serde_json:: json!({ 509"exports" : "./lib/index.js" , 510"dependencies" : { "instance-metadata" : "1.0.0" } 511}), 512& [ 513( "lib/package.json" , r#"{"type":"module"}"# ), 514( "lib/index.js" , "export default 1;" ), 515], 516); 517let dir = tempfile:: tempdir (). unwrap (); 518let resolver = server. resolver ( dir. path ()); 519let module =resolve ( & resolver, "npm:instance-metadata@1" ); 520 resolver. store (). save (). unwrap (); 521let metadata = module. path . parent (). unwrap (). join ( "package.json" ); 522// Replace only this instance's entry, leaving the shared backing intact. 523 std:: fs:: remove_file ( & metadata). unwrap (); 524 std:: fs:: write ( & metadata, r#"{"type":"commonjs"}"# ). unwrap (); 525let mut options = server. options ( dir. path ()); 526 options. offline =true ; 527 options. frozen =true ; 528let warm = fixture:: resolver ( options); 529let error =block_on ( warm. resolve_request ( "npm:instance-metadata@1" , ResolutionMode :: Import )) 530. unwrap_err () 531. to_string (); 532assert! ( error. contains ( "integrity mismatch" ), "{error}" ); 533assert! ( error. contains ( "lib/package.json" ), "{error}" ); 534} 535 536# [ test ] 537fn nested_metadata_is_consumed_only_when_resolution_needs_it () { 538use avril_runtime:: registry:: npm:: package:: NpmModuleKind ; 539 540let server =Registry :: new (); 541 server. publish_package ( 542"lazy-scopes" , 543"1.0.0" , 544 serde_json:: json!({}), 545& [ 546( "broken/package.json" , "{oops" ), 547( "broken/mod.js" , "export default 1;" ), 548( "broken/mod.mjs" , "export default 1;" ), 549( "broken/mod.cjs" , "module.exports = 1;" ), 550( "broken/mod.ts" , "export default 1;" ), 551( "broken/mod.d.ts" , "export declare const x: number;" ), 552( "bad-type/package.json" , r#"{"type":"invalid"}"# ), 553( "bad-type/mod.js" , "export default 1;" ), 554], 555); 556let dir = tempfile:: tempdir (). unwrap (); 557let resolver = server. resolver ( dir. path ()); 558let root =resolve ( & resolver, "npm:lazy-scopes@1" ); 559for ( file, kind) in [ 560( "mod.mjs" , NpmModuleKind :: Esm ), 561( "mod.cjs" , NpmModuleKind :: CommonJs ), 562( "mod.ts" , NpmModuleKind :: TypeScript ), 563] { 564let module = resolver 565. resolve_from_file ( 566& format! ( "./broken/{file}" ), 567& root. path , 568ResolutionMode :: Import , 569) 570. unwrap (); 571assert_eq! ( module. format , kind); 572} 573assert_eq! ( 574 resolver 575. check_access ( & root. path . parent (). unwrap (). join ( "broken/mod.d.ts" )) 576. unwrap (), 577NpmModuleKind :: Declaration 578); 579for ( path, message) in [ 580( "./broken/mod.js" , "Invalid nested" ), 581( "./bad-type/mod.js" , "Unsupported package type" ), 582] { 583let error = resolver 584. resolve_from_file ( path, & root. path , ResolutionMode :: Import ) 585. unwrap_err (); 586assert! ( error. to_string (). contains ( message), "{error}" ); 587} 588let error = resolver 589. resolve_from_file ( "./broken" , & root. path , ResolutionMode :: Require ) 590. unwrap_err (); 591assert! ( error. to_string (). contains ( "Invalid nested" ), "{error}" ); 592} 593 594# [ test ] 595fn export_warming_propagates_required_scope_errors_but_skips_unsupported_files () { 596let server =Registry :: new (); 597let dir = tempfile:: tempdir (). unwrap (); 598let resolver = server. resolver ( dir. path ()); 599for ( name, fields) in [ 600( "legacy-scopes" , serde_json:: json!({})), 601( 602"mapped-scopes" , 603 serde_json:: json!({ "exports" : { " . /*" : "./*" }}), 604), 605] { 606 server. publish_package ( 607 name, 608"1.0.0" , 609 fields, 610& [ 611( "safe/package.json" , "{oops" ), 612( "safe/mod.mjs" , "export default 1;" ), 613( "safe/addon.node" , "unused" ), 614( "broken/package.json" , "{oops" ), 615( "broken/mod.js" , "export default 1;" ), 616], 617); 618assert_eq! ( 619block_on ( resolver. export_requests ( & format! ( "npm:{name}@1/safe" ))). unwrap (), 620[ format! ( "npm:{name}@1/safe/mod.mjs" )] 621); 622let error = 623block_on ( resolver. export_requests ( & format! ( "npm:{name}@1/broken" ))). unwrap_err (); 624assert! ( error. to_string (). contains ( "Invalid nested" ), "{error}" ); 625} 626} 627 628# [ test ] 629fn nested_scopes_share_verified_metadata_and_preserve_type_inheritance () { 630use avril_runtime:: registry:: npm:: package:: NpmModuleKind ; 631 632let server =Registry :: new (); 633 server. publish_package ( 634"scope-snapshot" , 635"1.0.0" , 636 serde_json:: json!({}), 637& [ 638( "cjs/package.json" , r#"{"type":"commonjs"}"# ), 639( "cjs/sub/package.json" , r#"{"main":"./chosen.js"}"# ), 640( "cjs/sub/chosen.js" , "module.exports = 1;" ), 641( "cjs/sub/index.js" , "module.exports = 2;" ), 642( "cjs/sub/esm/package.json" , r#"{"type":"module"}"# ), 643( "cjs/sub/esm/mod.js" , "export default 3;" ), 644], 645); 646let dir = tempfile:: tempdir (). unwrap (); 647let resolver = server. resolver ( dir. path ()); 648let root =resolve ( & resolver, "npm:scope-snapshot@1" ); 649let chosen = resolver 650. resolve_from_file ( "./cjs/sub/chosen.js" , & root. path , ResolutionMode :: Import ) 651. unwrap (); 652assert_eq! ( chosen. format , NpmModuleKind :: CommonJs ); 653let esm = resolver 654. resolve_from_file ( "./cjs/sub/esm/mod.js" , & root. path , ResolutionMode :: Import ) 655. unwrap (); 656assert_eq! ( esm. format , NpmModuleKind :: Esm ); 657let metadata = chosen. path . parent (). unwrap (). join ( "package.json" ); 658 std:: fs:: write ( & metadata, r#"{"type":"module","main":"./index.js"}"# ). unwrap (); 659// Already consumed metadata remains one verified snapshot, not new disk input. 660let directory = resolver 661. resolve_from_file ( "./cjs/sub" , & root. path , ResolutionMode :: Require ) 662. unwrap (); 663assert_eq! ( directory. path , chosen. path ); 664assert_eq! ( directory. format , NpmModuleKind :: CommonJs ); 665assert! ( resolver. audit (). is_err ()); 666} 667 668# [ test ] 669fn unused_nested_metadata_tampering_is_detected_on_use_and_by_audit () { 670let server =Registry :: new (); 671 server. publish_package ( 672"unused-scope" , 673"1.0.0" , 674 serde_json:: json!({}), 675& [ 676( "sub/package.json" , r#"{"type":"module"}"# ), 677( "sub/mod.js" , "export default 1;" ), 678], 679); 680let dir = tempfile:: tempdir (). unwrap (); 681let first = server. resolver ( dir. path ()); 682let root =resolve ( & first, "npm:unused-scope@1" ); 683 first. store (). save (). unwrap (); 684let metadata = root. path . parent (). unwrap (). join ( "sub/package.json" ); 685 std:: fs:: write ( & metadata, r#"{"type":"commonjs"}"# ). unwrap (); 686let mut options = server. options ( dir. path ()); 687 options. offline =true ; 688 options. frozen =true ; 689let warm = fixture:: resolver ( options); 690let root =resolve ( & warm, "npm:unused-scope@1" ); 691let error = warm 692. resolve_from_file ( "./sub/mod.js" , & root. path , ResolutionMode :: Import ) 693. unwrap_err (); 694assert! ( error. to_string (). contains ( "integrity mismatch" ), "{error}" ); 695assert! ( error. to_string (). contains ( "sub/package.json" ), "{error}" ); 696assert! ( warm. audit (). is_err ()); 697} 698 699# [ test ] 700fn cached_nested_scopes_do_not_bypass_live_authority () { 701use avril_runtime:: caps::{ Capabilities , Category , Initial }; 702 703let server =Registry :: new (); 704 server. publish_package ( 705"scope-authority" , 706"1.0.0" , 707 serde_json:: json!({}), 708& [ 709( "sub/package.json" , r#"{"type":"module","main":"./mod.js"}"# ), 710( "sub/mod.js" , "export default 1;" ), 711], 712); 713let dir = tempfile:: tempdir (). unwrap (); 714let version_url =format! ( "{}scope-authority/1.0.0/" , server. url ); 715let mut initial =Initial :: new ( true ); 716 initial 717. grant ( Category :: JsExec , false , & version_url, dir. path ()) 718. unwrap (); 719let caps =Capabilities :: new ( initial, dir. path (). to_owned ()); 720let store =PackageStore :: new ( fixture:: runtime_handle (), server. options ( dir. path ())) 721. unwrap () 722. with_capabilities ( caps. clone ()); 723let resolver =NpmResolver :: new ( Arc :: new ( store)); 724let root =resolve ( & resolver, "npm:scope-authority@1" ); 725 resolver 726. resolve_from_file ( "./sub" , & root. path , ResolutionMode :: Require ) 727. unwrap (); 728 caps. pledge ( vec! [( Category :: JsExec , None , vec! [ version_url])]) 729. unwrap (); 730for specifierin [ "./sub" , "./sub/mod.js" ] { 731let error = resolver 732. resolve_from_file ( specifier, & root. path , ResolutionMode :: Require ) 733. unwrap_err (); 734assert! ( error. to_string (). contains ( "PermissionDenied" ), "{error}" ); 735} 736} 737 738# [ test ] 739fn full_audit_obeys_live_source_authority () { 740use avril_runtime:: caps::{ Capabilities , Category , Initial }; 741 742let server =Registry :: new (); 743 server. publish_package ( "audit-authority" , "1.0.0" , serde_json:: json!({}), & []); 744let dir = tempfile:: tempdir (). unwrap (); 745let version_url =format! ( "{}audit-authority/1.0.0/" , server. url ); 746let mut initial =Initial :: new ( true ); 747 initial 748. grant ( Category :: JsExec , false , & version_url, dir. path ()) 749. unwrap (); 750let caps =Capabilities :: new ( initial, dir. path (). to_owned ()); 751let store =PackageStore :: new ( fixture:: runtime_handle (), server. options ( dir. path ())) 752. unwrap () 753. with_capabilities ( caps. clone ()); 754let resolver =NpmResolver :: new ( Arc :: new ( store)); 755resolve ( & resolver, "npm:audit-authority@1" ); 756 resolver. audit (). unwrap (); 757 caps. pledge ( vec! [( Category :: JsExec , None , vec! [ version_url])]) 758. unwrap (); 759assert! ( 760 resolver 761. audit () 762. unwrap_err () 763. to_string () 764. contains ( "PermissionDenied" ) 765); 766} 767 768fn identity ( server : & Registry , name : & str , version : & str ) ->String { 769format! ( "{}|{name}|{version}" , server. url . as_str ()) 770} 771 772fn resolve ( resolver : & NpmResolver , spec : & str ) ->ResolvedNpmModule { 773block_on ( resolver. resolve_request ( spec, ResolutionMode :: Import )). unwrap () 774} 775 776# [ test ] 777fn module_entry_hint_is_file_scoped_and_independent_of_resolution_order () { 778use avril_runtime:: registry:: npm:: package:: NpmModuleKind ; 779 780let server =Registry :: new (); 781for modulein [ "lib/entry.js" , "./lib/entry.js" ] { 782let dir = tempfile:: tempdir (). unwrap (); 783 server. publish_npm ( 784"dual-entry" , 785"1.0.0" , 786& [ 787( 788"package.json" , 789& serde_json:: json!({ 790"name" : "dual-entry" , "version" : "1.0.0" , 791"module" : module, "main" : "./lib/main.js" 792}) 793. to_string (), 794), 795( "lib/entry.js" , "export default 42;" ), 796( "lib/main.js" , "module.exports = 7;" ), 797( "lib/sibling.js" , "module.exports = 8;" ), 798], 799); 800let resolver = server. resolver ( dir. path ()); 801let direct =resolve ( & resolver, "npm:dual-entry@1/lib/entry.js" ); 802assert_eq! ( direct. format , NpmModuleKind :: Esm ); 803assert_eq! ( 804 resolver. check_access ( & direct. path ). unwrap (), 805NpmModuleKind :: Esm 806); 807let root =resolve ( & resolver, "npm:dual-entry@1" ); 808assert_eq! ( root. path , direct. path ); 809assert_eq! ( root. format , NpmModuleKind :: Esm ); 810for modein [ ResolutionMode :: Import , ResolutionMode :: Require ] { 811let same = resolver 812. resolve_from_file ( "./entry.js" , & root. path , mode) 813. unwrap (); 814assert_eq! ( same. path , root. path ); 815assert_eq! ( same. format , NpmModuleKind :: Esm ); 816let sibling = resolver 817. resolve_from_file ( "./sibling.js" , & root. path , mode) 818. unwrap (); 819assert_eq! ( sibling. format , NpmModuleKind :: CommonJs ); 820} 821let main =block_on ( resolver. resolve_request ( "npm:dual-entry@1" , ResolutionMode :: Require )) 822. unwrap (); 823assert! ( main. path . ends_with ( "lib/main.js" )); 824assert_eq! ( main. format , NpmModuleKind :: CommonJs ); 825} 826} 827 828# [ test ] 829fn entry_hints_preserve_explicit_extensions_and_main_package_types () { 830use avril_runtime:: registry:: npm:: package:: NpmModuleKind ; 831 832let server =Registry :: new (); 833for ( fields, file, expected) in [ 834( 835 serde_json:: json!({ "module" : "entry.cjs" }), 836"entry.cjs" , 837NpmModuleKind :: CommonJs , 838), 839( 840 serde_json:: json!({ "main" : "entry.js" }), 841"entry.js" , 842NpmModuleKind :: CommonJs , 843), 844( 845 serde_json:: json!({ "main" : "entry.js" , "type" : "module" }), 846"entry.js" , 847NpmModuleKind :: Esm , 848), 849( 850 serde_json:: json!({ "main" : "entry.mjs" }), 851"entry.mjs" , 852NpmModuleKind :: Esm , 853), 854( 855 serde_json:: json!({ "main" : "entry.cjs" , "type" : "module" }), 856"entry.cjs" , 857NpmModuleKind :: CommonJs , 858), 859] { 860let dir = tempfile:: tempdir (). unwrap (); 861let mut package = serde_json:: json!({ "name" : "entry-format" , "version" : "1.0.0" }); 862 package 863. as_object_mut () 864. unwrap () 865. extend ( fields. as_object (). unwrap (). clone ()); 866 server. publish_npm ( 867"entry-format" , 868"1.0.0" , 869& [( "package.json" , & package. to_string ()), ( file, "" )], 870); 871let resolver = server. resolver ( dir. path ()); 872let entry =resolve ( & resolver, "npm:entry-format@1" ); 873assert! ( entry. path . ends_with ( file)); 874assert_eq! ( entry. format , expected); 875assert_eq! ( resolver. check_access ( & entry. path ). unwrap (), expected); 876} 877} 878 879# [ test ] 880fn roots_alias_subpaths_share_prepared_packages () { 881let server =Registry :: new (); 882 server. publish_package ( 883"lib" , 884"1.0.0" , 885 serde_json:: json!({ "exports" : { " . " : "./mod.js" , "./sub" : "./sub.js" }}), 886& [ 887( "mod.js" , "export default 1;" ), 888( "sub.js" , "export default 2;" ), 889], 890); 891let dir = tempfile:: tempdir (). unwrap (); 892let resolver = server. resolver ( dir. path ()); 893block_on ( resolver. prepare ( & [ NpmRootRequest { 894request : "npm:lib@^1/sub" . into (), 895context : BTreeMap :: new (), 896}])) 897. unwrap (); 898let sub =resolve ( & resolver, "npm:lib@^1/sub" ); 899assert! ( sub. path . ends_with ( "sub.js" )); 900let root =resolve ( & resolver, "npm:lib@^1" ); 901assert! ( root. path . ends_with ( "mod.js" )); 902assert_eq! ( sub. instance , root. instance ); 903} 904 905# [ test ] 906fn transitive_versions_coexist_per_consumer () { 907let server =Registry :: new (); 908 server. publish_package ( "util" , "1.0.0" , serde_json:: json!({}), & []); 909 server. publish_package ( "util" , "2.0.0" , serde_json:: json!({}), & []); 910 server. publish_package ( 911"lib" , 912"1.0.0" , 913 serde_json:: json!({ "dependencies" : { "util" : "^2.0.0" }}), 914& [( "index.js" , "export * from 'util';" )], 915); 916 server. publish_package ( 917"app" , 918"1.0.0" , 919 serde_json:: json!({ "dependencies" : { "lib" : "^1.0.0" , "util" : "^1.0.0" }}), 920& [( "index.js" , "export * from 'lib'; export * from 'util';" )], 921); 922let dir = tempfile:: tempdir (). unwrap (); 923let resolver = server. resolver ( dir. path ()); 924block_on ( resolver. prepare ( & [ NpmRootRequest { 925request : "npm:app@1" . into (), 926context : BTreeMap :: new (), 927}])) 928. unwrap (); 929let app =resolve ( & resolver, "npm:app@1" ); 930let from_app = resolver 931. resolve_from_file ( "util" , & app. path , ResolutionMode :: Import ) 932. unwrap (); 933assert! ( from_app. path . to_string_lossy (). contains ( "/util/1.0.0/" )); 934let lib = resolver 935. resolve_from_file ( "lib" , & app. path , ResolutionMode :: Import ) 936. unwrap (); 937let from_lib = resolver 938. resolve_from_file ( "util" , & lib. path , ResolutionMode :: Import ) 939. unwrap (); 940assert! ( from_lib. path . to_string_lossy (). contains ( "/util/2.0.0/" )); 941assert_ne! ( from_app. path , from_lib. path ); 942} 943 944# [ test ] 945fn shared_dependencies_share_instances () { 946let server =Registry :: new (); 947 server. publish_package ( "shared" , "1.0.0" , serde_json:: json!({}), & []); 948 server. publish_package ( 949"a" , 950"1.0.0" , 951 serde_json:: json!({ "dependencies" : { "shared" : "^1.0.0" }}), 952& [], 953); 954 server. publish_package ( 955"b" , 956"1.0.0" , 957 serde_json:: json!({ "dependencies" : { "shared" : "~1.0.0" }}), 958& [], 959); 960let dir = tempfile:: tempdir (). unwrap (); 961let resolver = server. resolver ( dir. path ()); 962block_on ( resolver. prepare ( & [ 963NpmRootRequest { 964request : "npm:a@1" . into (), 965context : BTreeMap :: new (), 966}, 967NpmRootRequest { 968request : "npm:b@1" . into (), 969context : BTreeMap :: new (), 970}, 971])) 972. unwrap (); 973let a =resolve ( & resolver, "npm:a@1" ); 974let b =resolve ( & resolver, "npm:b@1" ); 975let from_a = resolver 976. resolve_from_file ( "shared" , & a. path , ResolutionMode :: Import ) 977. unwrap (); 978let from_b = resolver 979. resolve_from_file ( "shared" , & b. path , ResolutionMode :: Import ) 980. unwrap (); 981assert_eq! ( from_a. path , from_b. path ); 982assert_eq! ( from_a. instance , from_b. instance ); 983} 984 985# [ test ] 986fn peer_bindings_share_providers_and_ignore_import_order () { 987let server =Registry :: new (); 988 server. publish_package ( "react" , "18.2.0" , serde_json:: json!({}), & []); 989 server. publish_package ( 990"plugin" , 991"1.0.0" , 992 serde_json:: json!({ "peerDependencies" : { "react" : "^18.0.0" }}), 993& [], 994); 995 server. publish_package ( 996"app" , 997"1.0.0" , 998 serde_json:: json!({ "dependencies" : { "react" : "18.2.0" , "plugin" : "1.0.0" }}), 999& [], 1000); 1001let context =BTreeMap :: from ([( "react" . into (), identity ( & server, "react" , "18.2.0" ))]); 1002let dir = tempfile:: tempdir (). unwrap (); 1003let resolver = server. resolver ( dir. path ()); 1004block_on ( resolver. prepare ( & [ 1005NpmRootRequest { 1006request : "npm:app@1" . into (), 1007context : context. clone (), 1008}, 1009NpmRootRequest { 1010request : "npm:plugin@1" . into (), 1011context : context. clone (), 1012}, 1013])) 1014. unwrap (); 1015let app =resolve ( & resolver, "npm:app@1" ); 1016let plugin =resolve ( & resolver, "npm:plugin@1" ); 1017let via_app = resolver 1018. resolve_from_file ( "react" , & app. path , ResolutionMode :: Import ) 1019. unwrap (); 1020let via_plugin = resolver 1021. resolve_from_file ( "react" , & plugin. path , ResolutionMode :: Import ) 1022. unwrap (); 1023assert_eq! ( via_app. path , via_plugin. path ); 1024 1025let dir2 = tempfile:: tempdir (). unwrap (); 1026let reversed = server. resolver ( dir2. path ()); 1027block_on ( reversed. prepare ( & [ 1028NpmRootRequest { 1029request : "npm:plugin@1" . into (), 1030context : context. clone (), 1031}, 1032NpmRootRequest { 1033request : "npm:app@1" . into (), 1034 context, 1035}, 1036])) 1037. unwrap (); 1038let first =resolve ( & resolver, "npm:plugin@1" ); 1039let second =resolve ( & reversed, "npm:plugin@1" ); 1040assert_eq! ( 1041 second. instance , first. instance , 1042"instance ids must not depend on root order" 1043); 1044} 1045 1046# [ test ] 1047fn peer_contexts_fork_instances () { 1048let server =Registry :: new (); 1049 server. publish_package ( "react" , "17.0.0" , serde_json:: json!({}), & []); 1050 server. publish_package ( "react" , "18.2.0" , serde_json:: json!({}), & []); 1051 server. publish_package ( 1052"plugin" , 1053"1.0.0" , 1054 serde_json:: json!({ "peerDependencies" : { "react" : "*" }}), 1055& [], 1056); 1057 server. publish_package ( 1058"h17" , 1059"1.0.0" , 1060 serde_json:: json!({ "dependencies" : { "react" : "17.0.0" , "plugin" : "1.0.0" }}), 1061& [], 1062); 1063 server. publish_package ( 1064"h18" , 1065"1.0.0" , 1066 serde_json:: json!({ "dependencies" : { "react" : "18.2.0" , "plugin" : "1.0.0" }}), 1067& [], 1068); 1069let dir = tempfile:: tempdir (). unwrap (); 1070let resolver = server. resolver ( dir. path ()); 1071block_on ( resolver. prepare ( & [ 1072NpmRootRequest { 1073request : "npm:h17@1" . into (), 1074context : BTreeMap :: new (), 1075}, 1076NpmRootRequest { 1077request : "npm:h18@1" . into (), 1078context : BTreeMap :: new (), 1079}, 1080])) 1081. unwrap (); 1082let h17 =resolve ( & resolver, "npm:h17@1" ); 1083let h18 =resolve ( & resolver, "npm:h18@1" ); 1084let p17 = resolver 1085. resolve_from_file ( "plugin" , & h17. path , ResolutionMode :: Import ) 1086. unwrap (); 1087let p18 = resolver 1088. resolve_from_file ( "plugin" , & h18. path , ResolutionMode :: Import ) 1089. unwrap (); 1090assert_ne! ( p17. instance , p18. instance ); 1091assert_ne! ( p17. path , p18. path ); 1092let r17 = resolver 1093. resolve_from_file ( "react" , & p17. path , ResolutionMode :: Import ) 1094. unwrap (); 1095let r18 = resolver 1096. resolve_from_file ( "react" , & p18. path , ResolutionMode :: Import ) 1097. unwrap (); 1098assert! ( r17. path . to_string_lossy (). contains ( "/react/17.0.0/" )); 1099assert! ( r18. path . to_string_lossy (). contains ( "/react/18.2.0/" )); 1100} 1101 1102# [ test ] 1103fn ordinary_cycles_terminate () { 1104let server =Registry :: new (); 1105 server. publish_package ( 1106"a" , 1107"1.0.0" , 1108 serde_json:: json!({ "dependencies" : { "b" : " 1.0 . 0 " }}), 1109& [], 1110); 1111 server. publish_package ( 1112"b" , 1113"1.0.0" , 1114 serde_json:: json!({ "dependencies" : { "a" : "1.0.0" }}), 1115& [], 1116); 1117let dir = tempfile:: tempdir (). unwrap (); 1118let resolver = server. resolver ( dir. path ()); 1119block_on ( resolver. prepare ( & [ NpmRootRequest { 1120request : "npm:a@1" . into (), 1121context : BTreeMap :: new (), 1122}])) 1123. unwrap (); 1124let a =resolve ( & resolver, "npm:a@1" ); 1125let b = resolver 1126. resolve_from_file ( "b" , & a. path , ResolutionMode :: Import ) 1127. unwrap (); 1128let back = resolver 1129. resolve_from_file ( "a" , & b. path , ResolutionMode :: Import ) 1130. unwrap (); 1131assert_eq! ( back. path , a. path ); 1132} 1133 1134# [ test ] 1135fn absent_optional_peers_are_skipped () { 1136let server =Registry :: new (); 1137 server. publish_package ( 1138"op" , 1139"1.0.0" , 1140 serde_json:: json!({ 1141"peerDependencies" : { "ghost" : "^1.0.0" }, 1142"peerDependenciesMeta" : { "ghost" : { "optional" : true }}, 1143}), 1144& [], 1145); 1146let dir = tempfile:: tempdir (). unwrap (); 1147let resolver = server. resolver ( dir. path ()); 1148block_on ( resolver. prepare ( & [ NpmRootRequest { 1149request : "npm:op@1" . into (), 1150context : BTreeMap :: new (), 1151}])) 1152. unwrap (); 1153let module =resolve ( & resolver, "npm:op@1" ); 1154assert_eq! ( module. instance , identity ( & server, "op" , "1.0.0" )); 1155} 1156 1157# [ test ] 1158fn exports_select_exact_patterns_and_arrays () { 1159let server =Registry :: new (); 1160 server. publish_package ( 1161"exp" , 1162"1.0.0" , 1163 serde_json:: json!({ "exports" : { 1164"." : "./mod.js" , 1165"./feature" : "./feature.js" , 1166"./features/*.js" : "./mapped/*.js" , 1167"./nested" : { "avril" : "./avril.js" , "default" : "./default.js" }, 1168"./ordered" : [{ "avril" : "./ordered-avril.js" }, "./ordered-fallback.js" ], 1169}}), 1170& [ 1171( "mod.js" , "export default 1;" ), 1172( "feature.js" , "export default 2;" ), 1173( "mapped/thing.js" , "export default 3;" ), 1174( "avril.js" , "export default 4;" ), 1175( "default.js" , "export default 5;" ), 1176( "ordered-avril.js" , "export default 6;" ), 1177( "ordered-fallback.js" , "export default 7;" ), 1178], 1179); 1180let dir = tempfile:: tempdir (). unwrap (); 1181let resolver = server. resolver ( dir. path ()); 1182block_on ( resolver. prepare ( & [ NpmRootRequest { 1183request : "npm:exp@1" . into (), 1184context : BTreeMap :: new (), 1185}])) 1186. unwrap (); 1187for ( request, file) in [ 1188( "npm:exp@1" , "mod.js" ), 1189( "npm:exp@1/feature" , "feature.js" ), 1190( "npm:exp@1/features/thing.js" , "mapped/thing.js" ), 1191( "npm:exp@1/nested" , "avril.js" ), 1192( "npm:exp@1/ordered" , "ordered-avril.js" ), 1193] { 1194assert! ( 1195resolve ( & resolver, request). path . ends_with ( file), 1196"{request}" 1197); 1198} 1199assert! ( 1200block_on ( resolver. resolve_request ( "npm:exp@1/private" , ResolutionMode :: Import )) 1201. unwrap_err () 1202. to_string () 1203. contains ( "no export" ) 1204); 1205} 1206 1207# [ test ] 1208fn null_exports_block_and_mixed_exports_fail () { 1209let server =Registry :: new (); 1210 server. publish_package ( 1211"nulled" , 1212"1.0.0" , 1213 serde_json:: json!({ "exports" : { " . " : "./mod.js" , "./blocked" : null}}), 1214& [ 1215( "mod.js" , "export default 1;" ), 1216( "blocked.js" , "export default 2;" ), 1217], 1218); 1219 server. publish_package ( 1220"mixed" , 1221"1.0.0" , 1222 serde_json:: json!({ "exports" : { " . " : "./mod.js" , "import" : "./mod.js" }}), 1223& [( "mod.js" , "export default 1;" )], 1224); 1225let dir = tempfile:: tempdir (). unwrap (); 1226let resolver = server. resolver ( dir. path ()); 1227block_on ( resolver. prepare ( & [ NpmRootRequest { 1228request : "npm:nulled@1" . into (), 1229context : BTreeMap :: new (), 1230}])) 1231. unwrap (); 1232assert! ( 1233block_on ( resolver. resolve_request ( "npm:nulled@1/blocked" , ResolutionMode :: Import )) 1234. unwrap_err () 1235. to_string () 1236. contains ( "blocked" ) 1237); 1238assert! ( 1239block_on ( resolver. prepare ( & [ NpmRootRequest { 1240request : "npm:mixed@1" . into (), 1241context : BTreeMap :: new (), 1242}])) 1243. unwrap_err () 1244. to_string () 1245. contains ( "mixes" ) 1246); 1247} 1248 1249# [ test ] 1250fn module_kinds_allow_cjs_for_require_wrappers_but_reject_json_native () { 1251let server =Registry :: new (); 1252 server. publish_package ( 1253"cjs" , 1254"1.0.0" , 1255 serde_json:: json!({ "type" : "commonjs" }), 1256& [( "index.js" , "module.exports = 1;" )], 1257); 1258 server. publish_package ( 1259"esm" , 1260"1.0.0" , 1261 serde_json:: json!({ "type" : "module" }), 1262& [( "index.js" , "export default 1;" )], 1263); 1264 server. publish_package ( 1265"legacy" , 1266"1.0.0" , 1267 serde_json:: json!({ "exports" : { " . " : "./main.cjs" , "./typed" : "./typed.cts" }}), 1268& [ 1269( "main.cjs" , "module.exports = 1;" ), 1270( "typed.cts" , "export default 1;" ), 1271], 1272); 1273 server. publish_package ( 1274"data" , 1275"1.0.0" , 1276 serde_json:: json!({ "exports" : { " . " : "./data.json" }}), 1277& [( "data.json" , "{}" )], 1278); 1279 server. publish_package ( 1280"native" , 1281"1.0.0" , 1282 serde_json:: json!({ "exports" : { " . " : "./binding.node" }}), 1283& [( "binding.node" , "binary" )], 1284); 1285 server. publish_package ( 1286"nested" , 1287"1.0.0" , 1288 serde_json:: json!({ "type" : "commonjs" }), 1289& [ 1290( "index.js" , "module.exports = 1;" ), 1291( "esm/package.json" , "{\"type\": \"module\"}" ), 1292( "esm/mod.js" , "export default 1;" ), 1293], 1294); 1295 server. publish_package ( 1296"types" , 1297"1.0.0" , 1298 serde_json:: json!({ "exports" : { " . " : "./index.d.ts" }}), 1299& [ 1300( "index.js" , "export default 0;" ), 1301( "index.d.ts" , "export declare const x: number;" ), 1302], 1303); 1304let dir = tempfile:: tempdir (). unwrap (); 1305let resolver = server. resolver ( dir. path ()); 1306for rootin [ 1307"npm:cjs@1" , 1308"npm:esm@1" , 1309"npm:legacy@1" , 1310"npm:data@1" , 1311"npm:native@1" , 1312"npm:nested@1" , 1313"npm:types@1" , 1314] { 1315block_on ( resolver. prepare ( & [ NpmRootRequest { 1316request : root. into (), 1317context : BTreeMap :: new (), 1318}])) 1319. unwrap (); 1320} 1321for ( specifier, rel) in [ 1322( "npm:cjs@1" , "index.js" ), 1323( "npm:esm@1" , "index.js" ), 1324( "npm:legacy@1" , "main.cjs" ), 1325( "npm:data@1" , "data.json" ), 1326] { 1327let path =if specifier =="npm:data@1" { 1328let registry = std:: fs:: read_dir ( dir. path (). join ( ".avril/npm/default" )) 1329. unwrap () 1330. next () 1331. unwrap () 1332. unwrap () 1333. path (); 1334 registry. join ( "data/1.0.0/data.json" ) 1335} else { 1336resolve ( & resolver, specifier). path 1337}; 1338assert! ( path. ends_with ( rel)); 1339let original = std:: fs:: read ( & path). unwrap (); 1340 std:: fs:: write ( & path, b"tampered" ). unwrap (); 1341let error = resolver. npm_source ( & path). unwrap_err (); 1342assert! ( error. to_string (). contains ( rel), "{error}" ); 1343 std:: fs:: write ( path, original). unwrap (); 1344} 1345assert! ( 1346block_on ( resolver. resolve_request ( "npm:cjs@1" , ResolutionMode :: Import )) 1347. unwrap () 1348. format 1349 == avril_runtime:: registry:: npm:: package:: NpmModuleKind :: CommonJs 1350); 1351assert! ( 1352block_on ( resolver. resolve_request ( "npm:legacy@1" , ResolutionMode :: Import )) 1353. unwrap () 1354. format 1355 == avril_runtime:: registry:: npm:: package:: NpmModuleKind :: CommonJs 1356); 1357// Explicit `.cts` is CommonJS (transpiled preserving CJS) for `require` 1358// and ESM wrappers, even when its content uses ESM syntax (which then 1359// fails at execution, not resolution). 1360assert! ( 1361block_on ( resolver. resolve_request ( "npm:legacy@1/typed" , ResolutionMode :: Import )) 1362. unwrap () 1363. format 1364 == avril_runtime:: registry:: npm:: package:: NpmModuleKind :: CommonJs 1365); 1366assert! ( 1367block_on ( resolver. resolve_request ( "npm:data@1" , ResolutionMode :: Import )) 1368. unwrap_err () 1369. to_string () 1370. contains ( "JSON" ) 1371); 1372assert! ( 1373block_on ( resolver. resolve_request ( "npm:native@1" , ResolutionMode :: Import )) 1374. unwrap_err () 1375. to_string () 1376. contains ( "Native" ) 1377); 1378let nested =resolve ( & resolver, "npm:nested@1/esm/mod.js" ); 1379assert! ( nested. path . ends_with ( "mod.js" )); 1380assert! ( 1381block_on ( resolver. resolve_request ( "npm:types@1" , ResolutionMode :: Import )) 1382. unwrap_err () 1383. to_string () 1384. contains ( "no executable code" ) 1385); 1386} 1387 1388# [ test ] 1389fn resolution_is_confined_to_manifests () { 1390let server =Registry :: new (); 1391 server. publish_package ( 1392"confined" , 1393"1.0.0" , 1394 serde_json:: json!({ "exports" : { " . " : "./mod.js" , "./evil" : "./node_modules/x/index.js" }}), 1395& [ 1396( "mod.js" , "export default 1;" ), 1397( "node_modules/x/index.js" , "export default 2;" ), 1398], 1399); 1400let dir = tempfile:: tempdir (). unwrap (); 1401 std:: fs:: write ( dir. path (). join ( "local.js" ), "local" ). unwrap (); 1402let resolver = server. resolver ( dir. path ()); 1403block_on ( resolver. prepare ( & [ NpmRootRequest { 1404request : "npm:confined@1" . into (), 1405context : BTreeMap :: new (), 1406}])) 1407. unwrap (); 1408let root =resolve ( & resolver, "npm:confined@1" ); 1409assert! ( 1410 resolver 1411. resolve_from_file ( "../local.js" , & root. path , ResolutionMode :: Import ) 1412. unwrap_err () 1413. to_string () 1414. contains ( "escapes" ) 1415); 1416assert! ( 1417block_on ( resolver. resolve_request ( "npm:confined@1/evil" , ResolutionMode :: Import )) 1418. unwrap_err () 1419. to_string () 1420. contains ( "escapes" ) 1421); 1422assert! ( 1423 resolver 1424. resolve_from_file ( "fs" , & root. path , ResolutionMode :: Import ) 1425. unwrap_err () 1426. to_string () 1427. contains ( "not a declared dependency" ) 1428); 1429assert! ( 1430 resolver 1431. resolve_from_file ( 1432"./mod.js" , 1433& dir. path (). join ( "local.js" ), 1434ResolutionMode :: Import 1435) 1436. unwrap_err () 1437. to_string () 1438. contains ( "does not own" ) 1439); 1440let unowned = dir 1441. path () 1442. join ( ".avril" ) 1443. join ( "npm" ) 1444. join ( "phantom" ) 1445. join ( "x.js" ); 1446assert! ( resolver. is_reserved_npm_path ( & unowned)); 1447assert! ( 1448 resolver 1449. resolve_from_file ( "./mod.js" , & unowned, ResolutionMode :: Import ) 1450. unwrap_err () 1451. to_string () 1452. contains ( "not owned" ) 1453); 1454} 1455 1456# [ test ] 1457fn frozen_offline_graphs_read_from_locks_and_trees () { 1458let server =Registry :: new (); 1459 server. publish_package ( 1460"fro" , 1461"1.0.0" , 1462 serde_json:: json!({}), 1463& [( "index.js" , "export default 0;" )], 1464); 1465let dir = tempfile:: tempdir (). unwrap (); 1466let resolver = server. resolver ( dir. path ()); 1467block_on ( resolver. prepare ( & [ NpmRootRequest { 1468request : "npm:fro@1" . into (), 1469context : BTreeMap :: new (), 1470}])) 1471. unwrap (); 1472 resolver. store (). save (). unwrap (); 1473let lock_before = std:: fs:: read ( dir. path (). join ( "avril.lock" )). unwrap (); 1474// Download caches go away; the tree and lock stay. 1475 std:: fs:: remove_dir_all ( dir. path (). join ( ".avril" ). join ( "npm" ). join ( ".metadata" )). unwrap (); 1476 std:: fs:: remove_dir_all ( dir. path (). join ( ".avril" ). join ( "npm" ). join ( ".artifacts" )). unwrap (); 1477let warmed = server. requests . lock (). unwrap (). len (); 1478let mut offline = server. options ( dir. path ()); 1479 offline. offline =true ; 1480 offline. frozen =true ; 1481let store = std:: sync:: Arc :: new ( PackageStore :: new ( fixture:: runtime_handle (), offline). unwrap ()); 1482let cold =NpmResolver :: new ( store); 1483block_on ( cold. prepare ( & [ NpmRootRequest { 1484request : "npm:fro@1" . into (), 1485context : BTreeMap :: new (), 1486}])) 1487. unwrap (); 1488let module =resolve ( & cold, "npm:fro@1" ); 1489assert_eq! ( cold. npm_source ( & module. path ). unwrap (), "export default 0;" ); 1490assert_eq! ( server. requests . lock (). unwrap (). len (), warmed); 1491 cold. store (). save (). unwrap (); 1492assert_eq! ( 1493 std:: fs:: read ( dir. path (). join ( "avril.lock" )). unwrap (), 1494 lock_before 1495); 1496} 1497 1498# [ test ] 1499fn scoped_instances_and_sibling_prefixes_have_component_ownership () { 1500let server =Registry :: new (); 1501 server. publish_package ( "@scope/pkg" , "1.0.0" , serde_json:: json!({}), & []); 1502 server. publish_package ( "@scope/pkg-sibling" , "1.0.0" , serde_json:: json!({}), & []); 1503let dir = tempfile:: tempdir (). unwrap (); 1504let resolver = server. resolver ( dir. path ()); 1505let scoped =resolve ( & resolver, "npm:@scope/pkg@1" ); 1506let sibling =resolve ( & resolver, "npm:@scope/pkg-sibling@1" ); 1507 1508assert_eq! ( 1509 resolver 1510. resolve_from_file ( "./index.js" , & scoped. path , ResolutionMode :: Import ) 1511. unwrap () 1512. instance , 1513 scoped. instance 1514); 1515assert_eq! ( 1516 resolver 1517. resolve_from_file ( "./index.js" , & sibling. path , ResolutionMode :: Import ) 1518. unwrap () 1519. instance , 1520 sibling. instance 1521); 1522let lookalike = scoped 1523. path 1524. parent () 1525. unwrap () 1526. with_file_name ( "1.0.0-sibling" ) 1527. join ( "index.js" ); 1528assert! ( resolver. is_reserved_npm_path ( & lookalike)); 1529assert! ( 1530 resolver 1531. resolve_from_file ( "./index.js" , & lookalike, ResolutionMode :: Import ) 1532. unwrap_err () 1533. to_string () 1534. contains ( "not owned" ) 1535); 1536} 1537 1538# [ test ] 1539fn ownership_misses_are_not_cached_before_late_preparation () { 1540let server =Registry :: new (); 1541 server. publish_package ( "late-owner" , "1.0.0" , serde_json:: json!({}), & []); 1542let dir = tempfile:: tempdir (). unwrap (); 1543let writer = server. resolver ( dir. path ()); 1544let root =resolve ( & writer, "npm:late-owner@1" ); 1545 writer. store (). save (). unwrap (); 1546 1547let resolver = server. resolver ( dir. path ()); 1548assert! ( 1549 resolver 1550. resolve_from_file ( "./index.js" , & root. path , ResolutionMode :: Import ) 1551. unwrap_err () 1552. to_string () 1553. contains ( "not owned" ) 1554); 1555block_on ( resolver. prepare ( & [ NpmRootRequest { 1556request : "npm:late-owner@1" . into (), 1557context : BTreeMap :: new (), 1558}])) 1559. unwrap (); 1560assert_eq! ( 1561 resolver 1562. resolve_from_file ( "./index.js" , & root. path , ResolutionMode :: Import ) 1563. unwrap () 1564. instance , 1565 root. instance 1566); 1567} 1568 1569# [ test ] 1570fn late_roots_never_rebind_evaluated_instances () { 1571let server =Registry :: new (); 1572 server. publish_package ( "one" , "1.0.0" , serde_json:: json!({}), & []); 1573 server. publish_package ( "two" , "1.0.0" , serde_json:: json!({}), & []); 1574let dir = tempfile:: tempdir (). unwrap (); 1575let resolver = server. resolver ( dir. path ()); 1576block_on ( resolver. prepare ( & [ NpmRootRequest { 1577request : "npm:one@1" . into (), 1578context : BTreeMap :: new (), 1579}])) 1580. unwrap (); 1581let first =resolve ( & resolver, "npm:one@1" ); 1582block_on ( resolver. prepare ( & [ 1583NpmRootRequest { 1584request : "npm:one@1" . into (), 1585context : BTreeMap :: new (), 1586}, 1587NpmRootRequest { 1588request : "npm:two@1" . into (), 1589context : BTreeMap :: new (), 1590}, 1591])) 1592. unwrap (); 1593assert_eq! ( resolve ( & resolver, "npm:one@1" ). instance , first. instance ); 1594assert_eq! ( 1595 resolver 1596. resolve_from_file ( "./index.js" , & first. path , ResolutionMode :: Import ) 1597. unwrap () 1598. instance , 1599 first. instance 1600); 1601} 1602 1603# [ test ] 1604fn incompatible_existing_peers_are_explicit_errors () { 1605let server =Registry :: new (); 1606 server. publish_package ( "react" , "17.0.0" , serde_json:: json!({}), & []); 1607 server. publish_package ( 1608"plugin" , 1609"1.0.0" , 1610 serde_json:: json!({ "peerDependencies" : { "react" : "^18.0.0" }}), 1611& [], 1612); 1613let dir = tempfile:: tempdir (). unwrap (); 1614let resolver = server. resolver ( dir. path ()); 1615let context =BTreeMap :: from ([( "react" . into (), identity ( & server, "react" , "17.0.0" ))]); 1616assert! ( 1617block_on ( resolver. prepare ( & [ NpmRootRequest { 1618request : "npm:plugin@1" . into (), 1619 context, 1620}])) 1621. unwrap_err () 1622. to_string () 1623. contains ( "wants ^18.0.0" ) 1624); 1625} 1626 1627# [ test ] 1628fn platform_gating_applies_to_regular_but_skips_optional () { 1629let server =Registry :: new (); 1630 server. publish_package ( 1631"alien" , 1632"1.0.0" , 1633 serde_json:: json!({ "os" : [ "fictitious-os-xyz" ]}), 1634& [], 1635); 1636 server. publish_package ( 1637"host" , 1638"1.0.0" , 1639 serde_json:: json!({ "optionalDependencies" : { "alien" : "^1.0.0" }}), 1640& [], 1641); 1642let dir = tempfile:: tempdir (). unwrap (); 1643let resolver = server. resolver ( dir. path ()); 1644assert! ( 1645block_on ( resolver. prepare ( & [ NpmRootRequest { 1646request : "npm:alien@1" . into (), 1647context : BTreeMap :: new (), 1648}])) 1649. unwrap_err () 1650. to_string () 1651. contains ( "not supported on this platform" ) 1652); 1653block_on ( resolver. prepare ( & [ NpmRootRequest { 1654request : "npm:host@1" . into (), 1655context : BTreeMap :: new (), 1656}])) 1657. unwrap (); 1658let host =resolve ( & resolver, "npm:host@1" ); 1659assert! ( 1660 resolver 1661. resolve_from_file ( "alien" , & host. path , ResolutionMode :: Import ) 1662. unwrap_err () 1663. to_string () 1664. contains ( "not a declared dependency" ) 1665); 1666} 1667 1668# [ test ] 1669fn dev_dependencies_are_ignored_and_aliases_resolve () { 1670let server =Registry :: new (); 1671 server. publish_package ( "other" , "2.0.0" , serde_json:: json!({}), & []); 1672 server. publish_package ( 1673"app" , 1674"1.0.0" , 1675 serde_json:: json!({ 1676"dependencies" : { "real" : "npm:other@^2.0.0" }, 1677"devDependencies" : { "ghost-dev" : "^9.9.9" }, 1678}), 1679& [], 1680); 1681let dir = tempfile:: tempdir (). unwrap (); 1682let resolver = server. resolver ( dir. path ()); 1683block_on ( resolver. prepare ( & [ NpmRootRequest { 1684request : "npm:app@1" . into (), 1685context : BTreeMap :: new (), 1686}])) 1687. unwrap (); 1688let app =resolve ( & resolver, "npm:app@1" ); 1689let real = resolver 1690. resolve_from_file ( "real" , & app. path , ResolutionMode :: Import ) 1691. unwrap (); 1692assert! ( real. path . to_string_lossy (). contains ( "/other/2.0.0/" )); 1693} 1694 1695# [ test ] 1696fn unsupported_dependency_protocols_are_clear_errors () { 1697let server =Registry :: new (); 1698 server. publish_package ( 1699"weird" , 1700"1.0.0" , 1701 serde_json:: json!({ "dependencies" : { "local" : "file:../other" }}), 1702& [], 1703); 1704let dir = tempfile:: tempdir (). unwrap (); 1705let resolver = server. resolver ( dir. path ()); 1706assert! ( 1707block_on ( resolver. prepare ( & [ NpmRootRequest { 1708request : "npm:weird@1" . into (), 1709context : BTreeMap :: new (), 1710}])) 1711. unwrap_err () 1712. to_string () 1713. contains ( "unsupported protocol" ) 1714); 1715} 1716 1717# [ test ] 1718fn frozen_graphs_validate_completeness () { 1719let server =Registry :: new (); 1720 server. publish_package ( "whole" , "1.0.0" , serde_json:: json!({}), & []); 1721let dir = tempfile:: tempdir (). unwrap (); 1722let resolver = server. resolver ( dir. path ()); 1723block_on ( resolver. prepare ( & [ NpmRootRequest { 1724request : "npm:whole@1" . into (), 1725context : BTreeMap :: new (), 1726}])) 1727. unwrap (); 1728 resolver. store (). save (). unwrap (); 1729let mut lock: serde_json:: Value = fixture:: read_lock ( dir. path ()); 1730let instances = lock[ "npm" ][ "instances" ]. as_object_mut (). unwrap (); 1731let victim = instances. keys (). next (). unwrap (). clone (); 1732 instances. remove ( & victim); 1733 std:: fs:: write ( dir. path (). join ( "avril.lock" ), format! ( "{lock}\n" )). unwrap (); 1734// The pruned lock references an instance that no longer exists. 1735let mut frozen = server. options ( dir. path ()); 1736 frozen. frozen =true ; 1737assert! ( 1738PackageStore :: new ( fixture:: runtime_handle (), frozen) 1739. map ( |_|()) 1740. unwrap_err () 1741. to_string () 1742. contains ( "missing npm instance" ) 1743); 1744} 1745 1746# [ test ] 1747fn update_reselects_new_versions () { 1748let server =Registry :: new (); 1749 server. publish_package ( "up" , "1.0.0" , serde_json:: json!({}), & []); 1750let dir = tempfile:: tempdir (). unwrap (); 1751let resolver = server. resolver ( dir. path ()); 1752block_on ( resolver. prepare ( & [ NpmRootRequest { 1753request : "npm:up" . into (), 1754context : BTreeMap :: new (), 1755}])) 1756. unwrap (); 1757assert! ( 1758resolve ( & resolver, "npm:up" ) 1759. path 1760. to_string_lossy () 1761. contains ( "/up/1.0.0/" ) 1762); 1763 resolver. store (). save (). unwrap (); 1764 server. publish_package ( "up" , "2.0.0" , serde_json:: json!({}), & []); 1765let mut updating = server. options ( dir. path ()); 1766 updating. update =true ; 1767let store = 1768 std:: sync:: Arc :: new ( PackageStore :: new ( fixture:: runtime_handle (), updating). unwrap ()); 1769let resolver =NpmResolver :: new ( store); 1770block_on ( resolver. prepare ( & [ NpmRootRequest { 1771request : "npm:up" . into (), 1772context : BTreeMap :: new (), 1773}])) 1774. unwrap (); 1775assert! ( 1776resolve ( & resolver, "npm:up" ) 1777. path 1778. to_string_lossy () 1779. contains ( "/up/2.0.0/" ) 1780); 1781} 1782 1783# [ test ] 1784fn nested_relative_imports_resolve_from_the_referrer () { 1785let server =Registry :: new (); 1786 server. publish_package ( 1787"deep" , 1788"1.0.0" , 1789 serde_json:: json!({ "type" : "module" }), 1790& [ 1791( "index.js" , "export * from './lib/mod.js';" ), 1792( 1793"lib/mod.js" , 1794"export * from './sibling.js'; export * from '../shared.js';" , 1795), 1796( "lib/sibling.js" , "export default 1;" ), 1797( "shared.js" , "export default 2;" ), 1798], 1799); 1800let dir = tempfile:: tempdir (). unwrap (); 1801let resolver = server. resolver ( dir. path ()); 1802block_on ( resolver. prepare ( & [ NpmRootRequest { 1803request : "npm:deep@1" . into (), 1804context : BTreeMap :: new (), 1805}])) 1806. unwrap (); 1807let entry =resolve ( & resolver, "npm:deep@1/lib/mod.js" ); 1808assert! ( entry. path . ends_with ( "lib/mod.js" )); 1809let sibling = resolver 1810. resolve_from_file ( "./sibling.js" , & entry. path , ResolutionMode :: Import ) 1811. unwrap (); 1812assert! ( sibling. path . ends_with ( "lib/sibling.js" )); 1813let shared = resolver 1814. resolve_from_file ( "../shared.js" , & entry. path , ResolutionMode :: Import ) 1815. unwrap (); 1816assert! ( shared. path . ends_with ( "shared.js" )); 1817assert! ( 1818 resolver 1819. resolve_from_file ( "../../escape.js" , & entry. path , ResolutionMode :: Import ) 1820. unwrap_err () 1821. to_string () 1822. contains ( "escapes" ) 1823); 1824assert! ( 1825 resolver 1826. resolve_from_file ( "..%2fshared.js" , & entry. path , ResolutionMode :: Import ) 1827. unwrap_err () 1828. to_string () 1829. contains ( "Invalid" ) 1830); 1831} 1832 1833# [ test ] 1834fn external_imports_keep_their_subpaths () { 1835let server =Registry :: new (); 1836 server. publish_package ( 1837"other" , 1838"1.0.0" , 1839 serde_json:: json!({ "exports" : { " . " : "./mod.js" , "./util" : "./util.js" }}), 1840& [ 1841( "mod.js" , "export default 1;" ), 1842( "util.js" , "export default 2;" ), 1843], 1844); 1845 server. publish_package ( 1846"user" , 1847"1.0.0" , 1848 serde_json:: json!({ 1849"dependencies" : { "other" : "^1.0.0" }, 1850"imports" : { "#tool" : "other/util" }, 1851}), 1852& [( "index.js" , "import '#tool';" )], 1853); 1854let dir = tempfile:: tempdir (). unwrap (); 1855let resolver = server. resolver ( dir. path ()); 1856block_on ( resolver. prepare ( & [ NpmRootRequest { 1857request : "npm:user@1" . into (), 1858context : BTreeMap :: new (), 1859}])) 1860. unwrap (); 1861let entry =resolve ( & resolver, "npm:user@1" ); 1862let tool = resolver 1863. resolve_from_file ( "#tool" , & entry. path , ResolutionMode :: Import ) 1864. unwrap (); 1865assert! ( tool. path . ends_with ( "util.js" )); 1866assert! ( tool. path . to_string_lossy (). contains ( "/other/1.0.0/" )); 1867} 1868 1869# [ test ] 1870fn conditional_exports_never_serve_unlisted_subpaths () { 1871let server =Registry :: new (); 1872 server. publish_package ( 1873"guarded" , 1874"1.0.0" , 1875 serde_json:: json!({ "exports" : { "import" : "./main.js" }}), 1876& [ 1877( "main.js" , "export default 1;" ), 1878( "private.js" , "export default 2;" ), 1879], 1880); 1881let dir = tempfile:: tempdir (). unwrap (); 1882let resolver = server. resolver ( dir. path ()); 1883block_on ( resolver. prepare ( & [ NpmRootRequest { 1884request : "npm:guarded@1" . into (), 1885context : BTreeMap :: new (), 1886}])) 1887. unwrap (); 1888assert! ( 1889resolve ( & resolver, "npm:guarded@1" ) 1890. path 1891. ends_with ( "main.js" ) 1892); 1893assert! ( 1894block_on ( resolver. resolve_request ( "npm:guarded@1/private" , ResolutionMode :: Import )) 1895. unwrap_err () 1896. to_string () 1897. contains ( "no export" ) 1898); 1899} 1900 1901# [ test ] 1902fn concurrent_prepares_share_work_without_deadlock () { 1903let server =Registry :: new (); 1904 server. publish_package ( "slow" , "1.0.0" , serde_json:: json!({}), & []); 1905 server. publish_package ( 1906"top" , 1907"1.0.0" , 1908 serde_json:: json!({ "dependencies" : { "slow" : "^1.0.0" }}), 1909& [], 1910); 1911let dir = tempfile:: tempdir (). unwrap (); 1912let resolver = server. resolver ( dir. path ()); 1913let root = |request : & str |NpmRootRequest { 1914request : request. into (), 1915context : BTreeMap :: new (), 1916}; 1917// Bounded: a deadlock would trip the timeout instead of hanging the suite. 1918let ( sender, receiver) = std:: sync:: mpsc:: channel (); 1919 std:: thread:: scope ( |scope|{ 1920 scope. spawn ( ||{ 1921let pair =block_on ( futures:: future:: join ( 1922 resolver. prepare ( & [ root ( "npm:top@1" )]), 1923 resolver. prepare ( & [ root ( "npm:slow@1" )]), 1924)); 1925let _ = sender. send ( pair); 1926}); 1927let ( first, second) = receiver 1928. recv_timeout ( std:: time:: Duration :: from_secs ( 60 )) 1929. unwrap (); 1930 first. unwrap (); 1931 second. unwrap (); 1932}); 1933for requestin [ "npm:top@1" , "npm:slow@1" ] { 1934let module =resolve ( & resolver, request); 1935assert! ( resolver. npm_source ( & module. path ). is_ok ()); 1936} 1937} 1938 1939# [ test ] 1940fn peerless_wrappers_fork_across_host_contexts () { 1941let server =Registry :: new (); 1942 server. publish_package ( "react" , "17.0.0" , serde_json:: json!({}), & []); 1943 server. publish_package ( "react" , "18.2.0" , serde_json:: json!({}), & []); 1944 server. publish_package ( 1945"plugin" , 1946"1.0.0" , 1947 serde_json:: json!({ "peerDependencies" : { "react" : "*" }}), 1948& [], 1949); 1950 server. publish_package ( 1951"wrapper" , 1952"1.0.0" , 1953 serde_json:: json!({ "dependencies" : { "plugin" : "1.0.0" }}), 1954& [], 1955); 1956 server. publish_package ( 1957"h17" , 1958"1.0.0" , 1959 serde_json:: json!({ "dependencies" : { "react" : "17.0.0" , "wrapper" : "1.0.0" }}), 1960& [], 1961); 1962 server. publish_package ( 1963"h18" , 1964"1.0.0" , 1965 serde_json:: json!({ "dependencies" : { "react" : "18.2.0" , "wrapper" : "1.0.0" }}), 1966& [], 1967); 1968let dir = tempfile:: tempdir (). unwrap (); 1969let resolver = server. resolver ( dir. path ()); 1970block_on ( resolver. prepare ( & [ 1971NpmRootRequest { 1972request : "npm:h17@1" . into (), 1973context : BTreeMap :: new (), 1974}, 1975NpmRootRequest { 1976request : "npm:h18@1" . into (), 1977context : BTreeMap :: new (), 1978}, 1979])) 1980. unwrap (); 1981let h17 =resolve ( & resolver, "npm:h17@1" ); 1982let h18 =resolve ( & resolver, "npm:h18@1" ); 1983let w17 = resolver 1984. resolve_from_file ( "wrapper" , & h17. path , ResolutionMode :: Import ) 1985. unwrap (); 1986let w18 = resolver 1987. resolve_from_file ( "wrapper" , & h18. path , ResolutionMode :: Import ) 1988. unwrap (); 1989assert_ne! ( 1990 w17. instance , w18. instance , 1991"wrapper contexts must not share" 1992); 1993assert_ne! ( w17. path , w18. path ); 1994} 1995 1996# [ test ] 1997fn peer_cycles_terminate () { 1998let server =Registry :: new (); 1999 server. publish_package ( 2000"pa" , 2001"1.0.0" , 2002 serde_json:: json!({ "peerDependencies" : { "pb" : "^1.0.0" }}), 2003& [], 2004); 2005 server. publish_package ( 2006"pb" , 2007"1.0.0" , 2008 serde_json:: json!({ "peerDependencies" : { "pa" : "^1.0.0" }}), 2009& [], 2010); 2011 server. publish_package ( 2012"top" , 2013"1.0.0" , 2014 serde_json:: json!({ "dependencies" : { "pa" : "1.0.0" , "pb" : "1.0.0" }}), 2015& [], 2016); 2017let dir = tempfile:: tempdir (). unwrap (); 2018let resolver = server. resolver ( dir. path ()); 2019block_on ( resolver. prepare ( & [ NpmRootRequest { 2020request : "npm:top@1" . into (), 2021context : BTreeMap :: new (), 2022}])) 2023. unwrap (); 2024let top =resolve ( & resolver, "npm:top@1" ); 2025let pa = resolver 2026. resolve_from_file ( "pa" , & top. path , ResolutionMode :: Import ) 2027. unwrap (); 2028let pb = resolver 2029. resolve_from_file ( "pb" , & top. path , ResolutionMode :: Import ) 2030. unwrap (); 2031assert_ne! ( pa. path , pb. path ); 2032assert! ( resolver. npm_source ( & pa. path ). is_ok ()); 2033assert! ( resolver. npm_source ( & pb. path ). is_ok ()); 2034} 2035 2036# [ test ] 2037fn missing_deep_frozen_instances_are_errors () { 2038let server =Registry :: new (); 2039 server. publish_package ( 2040"leaf" , 2041"1.0.0" , 2042 serde_json:: json!({ "peerDependencies" : { "react" : "^18.0.0" }}), 2043& [], 2044); 2045 server. publish_package ( "react" , "18.2.0" , serde_json:: json!({}), & []); 2046 server. publish_package ( 2047"mid" , 2048"1.0.0" , 2049 serde_json:: json!({ "dependencies" : { "leaf" : "^1.0.0" , "react" : "18.2.0" }}), 2050& [], 2051); 2052let dir = tempfile:: tempdir (). unwrap (); 2053let resolver = server. resolver ( dir. path ()); 2054block_on ( resolver. prepare ( & [ NpmRootRequest { 2055request : "npm:mid@1" . into (), 2056context : BTreeMap :: new (), 2057}])) 2058. unwrap (); 2059 resolver. store (). save (). unwrap (); 2060let mut lock: serde_json:: Value = fixture:: read_lock ( dir. path ()); 2061let root_targets: Vec < String > = lock[ "npm" ][ "roots" ] 2062. as_object () 2063. unwrap () 2064. values () 2065. map ( |value| value. as_str (). unwrap (). to_owned ()) 2066. collect (); 2067let victim = lock[ "npm" ][ "instances" ] 2068. as_object () 2069. unwrap () 2070. keys () 2071. find ( |key| !root_targets. contains ( key)) 2072. cloned () 2073. unwrap (); 2074 lock[ "npm" ][ "instances" ] 2075. as_object_mut () 2076. unwrap () 2077. remove ( & victim); 2078 std:: fs:: write ( dir. path (). join ( "avril.lock" ), format! ( "{lock}\n" )). unwrap (); 2079// Dangling instance edges are rejected when the frozen lock opens, so a 2080// frozen prepare can never run on a partial graph. 2081let mut frozen = server. options ( dir. path ()); 2082 frozen. frozen =true ; 2083assert! ( 2084PackageStore :: new ( fixture:: runtime_handle (), frozen) 2085. map ( |_|()) 2086. unwrap_err () 2087. to_string () 2088. contains ( "missing npm instance" ) 2089); 2090} 2091 2092# [ test ] 2093fn updated_dependency_versions_reselect_without_conflicts () { 2094let server =Registry :: new (); 2095 server. publish_package ( "dep" , "1.0.0" , serde_json:: json!({}), & []); 2096 server. publish_package ( 2097"app" , 2098"1.0.0" , 2099 serde_json:: json!({ "dependencies" : { "dep" : ">=1.0.0 <3.0.0" }}), 2100& [], 2101); 2102let dir = tempfile:: tempdir (). unwrap (); 2103let resolver = server. resolver ( dir. path ()); 2104block_on ( resolver. prepare ( & [ NpmRootRequest { 2105request : "npm:app@1" . into (), 2106context : BTreeMap :: new (), 2107}])) 2108. unwrap (); 2109let before =resolve ( & resolver, "npm:app@1" ); 2110 resolver. store (). save (). unwrap (); 2111 server. publish_package ( "dep" , "2.0.0" , serde_json:: json!({}), & []); 2112let mut updating = server. options ( dir. path ()); 2113 updating. update =true ; 2114let store = 2115 std:: sync:: Arc :: new ( PackageStore :: new ( fixture:: runtime_handle (), updating). unwrap ()); 2116let resolver =NpmResolver :: new ( store); 2117block_on ( resolver. prepare ( & [ NpmRootRequest { 2118request : "npm:app@1" . into (), 2119context : BTreeMap :: new (), 2120}])) 2121. unwrap (); 2122let after =resolve ( & resolver, "npm:app@1" ); 2123assert_ne! ( before. instance , after. instance ); 2124 resolver. store (). save (). unwrap (); 2125} 2126 2127# [ test ] 2128fn preselected_providers_win_regardless_of_root_order () { 2129let server =Registry :: new (); 2130 server. publish_package ( "host" , "1.0.0" , serde_json:: json!({}), & []); 2131 server. publish_package ( "host" , "1.1.0" , serde_json:: json!({}), & []); 2132 server. publish_package ( 2133"plugin" , 2134"1.0.0" , 2135 serde_json:: json!({ "peerDependencies" : { "host" : "^1.0.0" }}), 2136& [], 2137); 2138for orderin [ false , true ] { 2139let dir = tempfile:: tempdir (). unwrap (); 2140let resolver = server. resolver ( dir. path ()); 2141let mut roots =vec! [ 2142NpmRootRequest { 2143request : "npm:host@1.0.0" . into (), 2144context : BTreeMap :: new (), 2145}, 2146NpmRootRequest { 2147request : "npm:plugin@^1" . into (), 2148context : BTreeMap :: new (), 2149}, 2150]; 2151if order{ 2152 roots. reverse (); 2153} 2154block_on ( resolver. prepare ( & roots)). unwrap (); 2155let plugin =resolve ( & resolver, "npm:plugin@^1" ); 2156let host = resolver 2157. resolve_from_file ( "host" , & plugin. path , ResolutionMode :: Import ) 2158. unwrap (); 2159assert! ( 2160 host. path . to_string_lossy (). contains ( "/host/1.0.0/" ), 2161"order {order}: plugin must bind the preselected host 1.0.0" 2162); 2163} 2164// Without the host root the same plugin floats to latest instead. 2165let dir = tempfile:: tempdir (). unwrap (); 2166let resolver = server. resolver ( dir. path ()); 2167block_on ( resolver. prepare ( & [ NpmRootRequest { 2168request : "npm:plugin@^1" . into (), 2169context : BTreeMap :: new (), 2170}])) 2171. unwrap (); 2172let plugin =resolve ( & resolver, "npm:plugin@^1" ); 2173let host = resolver 2174. resolve_from_file ( "host" , & plugin. path , ResolutionMode :: Import ) 2175. unwrap (); 2176assert! ( host. path . to_string_lossy (). contains ( "/host/1.1.0/" )); 2177} 2178 2179# [ test ] 2180fn cyclic_identities_ignore_unrelated_roots_and_batches () { 2181let server =Registry :: new (); 2182 server. publish_package ( 2183"ca" , 2184"1.0.0" , 2185 serde_json:: json!({ "dependencies" : { "cb" : "1.0.0" }}), 2186& [], 2187); 2188 server. publish_package ( 2189"cb" , 2190"1.0.0" , 2191 serde_json:: json!({ "dependencies" : { "ca" : "1.0.0" }}), 2192& [], 2193); 2194 server. publish_package ( "solo" , "1.0.0" , serde_json:: json!({}), & []); 2195let dir = tempfile:: tempdir (). unwrap (); 2196let resolver = server. resolver ( dir. path ()); 2197let alone =block_on ( resolver. prepare ( & [ NpmRootRequest { 2198request : "npm:ca@1" . into (), 2199context : BTreeMap :: new (), 2200}])) 2201. unwrap (); 2202let with_other =block_on ( resolver. prepare ( & [ NpmRootRequest { 2203request : "npm:solo@1" . into (), 2204context : BTreeMap :: new (), 2205}])) 2206. unwrap (); 2207assert_eq! ( 2208 alone. roots . values (). next (). unwrap (), 2209& resolve ( & resolver, "npm:ca@1" ). instance , 2210"re-preparing alongside an unrelated root must not rebind" 2211); 2212assert! ( with_other. roots . len () ==1 ); 2213 2214let dir = tempfile:: tempdir (). unwrap (); 2215let fresh = server. resolver ( dir. path ()); 2216block_on ( fresh. prepare ( & [ 2217NpmRootRequest { 2218request : "npm:solo@1" . into (), 2219context : BTreeMap :: new (), 2220}, 2221NpmRootRequest { 2222request : "npm:ca@1" . into (), 2223context : BTreeMap :: new (), 2224}, 2225])) 2226. unwrap (); 2227assert_eq! ( 2228resolve ( & fresh, "npm:ca@1" ). instance , 2229resolve ( & resolver, "npm:ca@1" ). instance , 2230"cycle identities are batch independent" 2231); 2232} 2233 2234# [ test ] 2235fn same_request_in_two_contexts_resolves_through_handles () { 2236let server =Registry :: new (); 2237 server. publish_package ( "host" , "1.0.0" , serde_json:: json!({}), & []); 2238 server. publish_package ( "host" , "2.0.0" , serde_json:: json!({}), & []); 2239 server. publish_package ( 2240"plugin" , 2241"1.0.0" , 2242 serde_json:: json!({ "peerDependencies" : { "host" : "*" }}), 2243& [], 2244); 2245let dir = tempfile:: tempdir (). unwrap (); 2246let resolver = server. resolver ( dir. path ()); 2247let handle_a =block_on ( resolver. prepare ( & [ NpmRootRequest { 2248request : "npm:plugin@1" . into (), 2249context : BTreeMap :: from ([( "host" . into (), identity ( & server, "host" , "1.0.0" ))]), 2250}])) 2251. unwrap (); 2252let handle_b =block_on ( resolver. prepare ( & [ NpmRootRequest { 2253request : "npm:plugin@1" . into (), 2254context : BTreeMap :: from ([( "host" . into (), identity ( & server, "host" , "2.0.0" ))]), 2255}])) 2256. unwrap (); 2257let a = resolver 2258. resolve_in ( & handle_a, "npm:plugin@1" , ResolutionMode :: Import ) 2259. unwrap (); 2260let b = resolver 2261. resolve_in ( & handle_b, "npm:plugin@1" , ResolutionMode :: Import ) 2262. unwrap (); 2263assert_ne! ( a. instance , b. instance ); 2264assert_ne! ( a. path , b. path ); 2265assert_eq! ( 2266 resolver 2267. resolve_in ( & handle_a, "npm:plugin@1" , ResolutionMode :: Import ) 2268. unwrap () 2269. path , 2270 a. path 2271); 2272// A bare request across both contexts stays ambiguous instead of 2273// silently picking one. 2274assert! ( 2275block_on ( resolver. resolve_request ( "npm:plugin@1" , ResolutionMode :: Import )) 2276. unwrap_err () 2277. to_string () 2278. contains ( "several peer contexts" ) 2279); 2280} 2281 2282# [ test ] 2283fn missing_roots_rebuild_from_specifier_pins () { 2284let server =Registry :: new (); 2285 server. publish_package ( 2286"re" , 2287"3.0.0" , 2288 serde_json:: json!({}), 2289& [( "index.js" , "export default 0;" )], 2290); 2291let dir = tempfile:: tempdir (). unwrap (); 2292let resolver = server. resolver ( dir. path ()); 2293block_on ( resolver. prepare ( & [ NpmRootRequest { 2294request : "npm:re@3" . into (), 2295context : BTreeMap :: new (), 2296}])) 2297. unwrap (); 2298 resolver. store (). save (). unwrap (); 2299let mut lock: serde_json:: Value = fixture:: read_lock ( dir. path ()); 2300 lock[ "npm" ][ "roots" ]. as_object_mut (). unwrap (). clear (); 2301 lock[ "npm" ][ "instances" ]. as_object_mut (). unwrap (). clear (); 2302 std:: fs:: write ( dir. path (). join ( "avril.lock" ), format! ( "{lock}\n" )). unwrap (); 2303// Specifier and artifact pins survive; the root entry does not. 2304let store = std:: sync:: Arc :: new ( 2305PackageStore :: new ( fixture:: runtime_handle (), server. options ( dir. path ())). unwrap (), 2306); 2307let rebuilt =NpmResolver :: new ( store); 2308block_on ( rebuilt. prepare ( & [ NpmRootRequest { 2309request : "npm:re@3" . into (), 2310context : BTreeMap :: new (), 2311}])) 2312. unwrap (); 2313let module =resolve ( & rebuilt, "npm:re@3" ); 2314assert_eq! ( 2315 rebuilt. npm_source ( & module. path ). unwrap (), 2316"export default 0;" 2317); 2318} 2319 2320# [ test ] 2321fn new_contexts_cannot_overwrite_locked_instance_bindings () { 2322let server =Registry :: new (); 2323 server. publish_package ( "left" , "1.0.0" , serde_json:: json!({}), & []); 2324 server. publish_package ( "right" , "1.0.0" , serde_json:: json!({}), & []); 2325 server. publish_package ( 2326"top" , 2327"1.0.0" , 2328 serde_json:: json!({ "dependencies" : { "left" : "1.0.0" }}), 2329& [], 2330); 2331let dir = tempfile:: tempdir (). unwrap (); 2332let resolver = server. resolver ( dir. path ()); 2333block_on ( resolver. prepare ( & [ 2334NpmRootRequest { 2335request : "npm:top@1" . into (), 2336context : BTreeMap :: new (), 2337}, 2338NpmRootRequest { 2339request : "npm:right@1" . into (), 2340context : BTreeMap :: new (), 2341}, 2342])) 2343. unwrap (); 2344 resolver. store (). save (). unwrap (); 2345let mut lock: serde_json:: Value = fixture:: read_lock ( dir. path ()); 2346let right =identity ( & server, "right" , "1.0.0" ); 2347let instances = lock[ "npm" ][ "instances" ]. as_object_mut (). unwrap (); 2348let top_key = instances 2349. iter () 2350. find ( |( _, pin) |{ 2351 pin[ "package" ] 2352. as_str () 2353. is_some_and ( |id| id. contains ( "|top|" )) 2354}) 2355. map ( |( key, _) | key. clone ()) 2356. unwrap (); 2357 instances[ top_key. as_str ()][ "deps" ][ "left" ] = serde_json:: Value :: String ( right); 2358 std:: fs:: write ( dir. path (). join ( "avril.lock" ), format! ( "{lock}\n" )). unwrap (); 2359let store = std:: sync:: Arc :: new ( 2360PackageStore :: new ( fixture:: runtime_handle (), server. options ( dir. path ())). unwrap (), 2361); 2362let tampered =NpmResolver :: new ( store); 2363// This root-only context was never recorded. Its new resolution must not 2364// overwrite an instance already assigned different authoritative bindings. 2365assert! ( 2366block_on ( tampered. prepare ( & [ NpmRootRequest { 2367request : "npm:top@1" . into (), 2368context : BTreeMap :: new (), 2369}])) 2370. unwrap_err () 2371. to_string () 2372. contains ( "changed" ) 2373); 2374} 2375 2376# [ test ] 2377fn failed_prepares_leave_retryable_state () { 2378let server =Registry :: new (); 2379 server. publish_package ( 2380"flaky" , 2381"1.0.0" , 2382 serde_json:: json!({ "dependencies" : { "ghost" : "^9.9.9" }}), 2383& [], 2384); 2385let dir = tempfile:: tempdir (). unwrap (); 2386let resolver = server. resolver ( dir. path ()); 2387assert! ( 2388block_on ( resolver. prepare ( & [ NpmRootRequest { 2389request : "npm:flaky@1" . into (), 2390context : BTreeMap :: new (), 2391}])) 2392. is_err () 2393); 2394assert! ( resolver. owning_instance ( & dir. path (). join ( "x.js" )). is_none ()); 2395 server. publish_package ( "ghost" , "9.9.9" , serde_json:: json!({}), & []); 2396block_on ( resolver. prepare ( & [ NpmRootRequest { 2397request : "npm:flaky@1" . into (), 2398context : BTreeMap :: new (), 2399}])) 2400. unwrap (); 2401let module =resolve ( & resolver, "npm:flaky@1" ); 2402assert! ( resolver. npm_source ( & module. path ). is_ok ()); 2403} 2404 2405# [ test ] 2406fn malformed_peer_ranges_are_errors () { 2407let server =Registry :: new (); 2408 server. publish_package ( 2409"bad" , 2410"1.0.0" , 2411 serde_json:: json!({ "peerDependencies" : { "ghost" : "^^invalid^^" }}), 2412& [], 2413); 2414let dir = tempfile:: tempdir (). unwrap (); 2415let resolver = server. resolver ( dir. path ()); 2416assert! ( 2417block_on ( resolver. prepare ( & [ NpmRootRequest { 2418request : "npm:bad@1" . into (), 2419context : BTreeMap :: new (), 2420}])) 2421. unwrap_err () 2422. to_string () 2423. contains ( "Invalid" ) 2424); 2425} 2426 2427# [ test ] 2428fn frozen_save_graph_writes_nothing () { 2429let server =Registry :: new (); 2430 server. publish_package ( "sg" , "1.0.0" , serde_json:: json!({}), & []); 2431let dir = tempfile:: tempdir (). unwrap (); 2432let resolver = server. resolver ( dir. path ()); 2433block_on ( resolver. prepare ( & [ NpmRootRequest { 2434request : "npm:sg@1" . into (), 2435context : BTreeMap :: new (), 2436}])) 2437. unwrap (); 2438 resolver. store (). save (). unwrap (); 2439let lock_before = std:: fs:: read ( dir. path (). join ( "avril.lock" )). unwrap (); 2440let mut frozen = server. options ( dir. path ()); 2441 frozen. frozen =true ; 2442let store = std:: sync:: Arc :: new ( PackageStore :: new ( fixture:: runtime_handle (), frozen). unwrap ()); 2443 store 2444. save_graph ( 2445& std:: collections:: BTreeSet :: from ([ "npm:sg@1" . to_owned ()]), 2446& std:: collections:: HashSet :: new (), 2447) 2448. unwrap (); 2449assert_eq! ( 2450 std:: fs:: read ( dir. path (). join ( "avril.lock" )). unwrap (), 2451 lock_before 2452); 2453let _ = resolver; 2454} 2455 2456# [ test ] 2457fn frozen_new_roots_are_missing_without_network () { 2458let server =Registry :: new (); 2459 server. publish_package ( "known" , "1.0.0" , serde_json:: json!({}), & []); 2460 server. publish_package ( "fresh" , "1.0.0" , serde_json:: json!({}), & []); 2461let dir = tempfile:: tempdir (). unwrap (); 2462let resolver = server. resolver ( dir. path ()); 2463block_on ( resolver. prepare ( & [ NpmRootRequest { 2464request : "npm:known@1" . into (), 2465context : BTreeMap :: new (), 2466}])) 2467. unwrap (); 2468 resolver. store (). save (). unwrap (); 2469let requests = server. requests . lock (). unwrap (). len (); 2470let mut frozen = server. options ( dir. path ()); 2471 frozen. frozen =true ; 2472let store = std:: sync:: Arc :: new ( PackageStore :: new ( fixture:: runtime_handle (), frozen). unwrap ()); 2473let frozen_resolver =NpmResolver :: new ( store); 2474assert! ( 2475block_on ( frozen_resolver. prepare ( & [ NpmRootRequest { 2476request : "npm:fresh@1" . into (), 2477context : BTreeMap :: new (), 2478}])) 2479. unwrap_err () 2480. to_string () 2481. contains ( "Frozen lockfile is missing" ) 2482); 2483assert_eq! ( server. requests . lock (). unwrap (). len (), requests); 2484} 2485 2486# [ test ] 2487fn failed_contexts_leave_no_lock_pollution () { 2488let server =Registry :: new (); 2489 server. publish_package ( "host" , "1.0.0" , serde_json:: json!({}), & []); 2490 server. publish_package ( 2491"plugin" , 2492"1.0.0" , 2493 serde_json:: json!({ "peerDependencies" : { "host" : "^1.0.0" }}), 2494& [], 2495); 2496let dir = tempfile:: tempdir (). unwrap (); 2497let resolver = server. resolver ( dir. path ()); 2498let bad =BTreeMap :: from ([( "host" . into (), "http://127.0.0.1:9/|host|9.9.9" . into ())]); 2499assert! ( 2500block_on ( resolver. prepare ( & [ NpmRootRequest { 2501request : "npm:plugin@1" . into (), 2502context : bad. clone (), 2503}])) 2504. is_err () 2505); 2506let good =BTreeMap :: from ([( "host" . into (), identity ( & server, "host" , "1.0.0" ))]); 2507block_on ( resolver. prepare ( & [ NpmRootRequest { 2508request : "npm:plugin@1" . into (), 2509context : good, 2510}])) 2511. unwrap (); 2512 resolver. store (). save (). unwrap (); 2513let lock = std:: fs:: read_to_string ( dir. path (). join ( "avril.lock" )). unwrap (); 2514assert! ( !lock. contains ( "9.9.9" ), "{lock}" ); 2515assert! ( !lock. contains ( "127.0.0.1:9" ), "{lock}" ); 2516} 2517 2518# [ test ] 2519fn same_plugin_shares_host_across_matching_contexts () { 2520let server =Registry :: new (); 2521 server. publish_package ( "host" , "1.0.0" , serde_json:: json!({}), & []); 2522 server. publish_package ( 2523"plugin" , 2524"1.0.0" , 2525 serde_json:: json!({ "peerDependencies" : { "host" : "^1.0.0" }}), 2526& [], 2527); 2528let dir = tempfile:: tempdir (). unwrap (); 2529let resolver = server. resolver ( dir. path ()); 2530let context =BTreeMap :: from ([( "host" . into (), identity ( & server, "host" , "1.0.0" ))]); 2531let first =block_on ( resolver. prepare ( & [ NpmRootRequest { 2532request : "npm:plugin@1" . into (), 2533context : context. clone (), 2534}])) 2535. unwrap (); 2536let second =block_on ( resolver. prepare ( & [ NpmRootRequest { 2537request : "npm:plugin@1" . into (), 2538 context, 2539}])) 2540. unwrap (); 2541assert_eq! ( first. roots , second. roots ); 2542} 2543 2544# [ test ] 2545fn concurrent_selects_share_one_identity () { 2546let server =Registry :: new (); 2547 server. publish_package ( "race" , "1.2.0" , serde_json:: json!({}), & []); 2548let dir = tempfile:: tempdir (). unwrap (); 2549let resolver = server. resolver ( dir. path ()); 2550let ( a, b) =block_on ( futures:: future:: join ( 2551 resolver. prepare ( & [ NpmRootRequest { 2552request : "npm:race@^1" . into (), 2553context : BTreeMap :: new (), 2554}]), 2555 resolver. prepare ( & [ NpmRootRequest { 2556request : "npm:race@~1.2.0" . into (), 2557context : BTreeMap :: new (), 2558}]), 2559)); 2560 a. unwrap (); 2561 b. unwrap (); 2562let first =resolve ( & resolver, "npm:race@^1" ); 2563let second =resolve ( & resolver, "npm:race@~1.2.0" ); 2564assert_eq! ( first. path , second. path ); 2565assert_eq! ( first. instance , second. instance ); 2566} 2567 2568# [ test ] 2569fn late_dynamic_roots_reuse_prepared_namespaces () { 2570let server =Registry :: new (); 2571 server. publish_package ( 2572"plug" , 2573"1.0.0" , 2574 serde_json:: json!({ "peerDependencies" : { "host" : "^1.0.0" }}), 2575& [( "index.js" , "export {version} from 'host';" )], 2576); 2577 server. publish_package ( 2578"host" , 2579"1.0.0" , 2580 serde_json:: json!({}), 2581& [( "index.js" , "export const version = 'h1';" )], 2582); 2583 server. publish_package ( 2584"extra" , 2585"1.0.0" , 2586 serde_json:: json!({ "dependencies" : { "plug" : "^1.0.0" }}), 2587& [( "index.js" , "export * from 'plug';" )], 2588); 2589let dir = tempfile:: tempdir (). unwrap (); 2590let resolver = server. resolver ( dir. path ()); 2591block_on ( resolver. prepare ( & [ NpmRootRequest { 2592request : "npm:plug@1" . into (), 2593context : BTreeMap :: new (), 2594}])) 2595. unwrap (); 2596let before =resolve ( & resolver, "npm:plug@1" ); 2597block_on ( resolver. prepare ( & [ NpmRootRequest { 2598request : "npm:extra@1" . into (), 2599context : BTreeMap :: new (), 2600}])) 2601. unwrap (); 2602let after =resolve ( & resolver, "npm:plug@1" ); 2603assert_eq! ( before. instance , after. instance ); 2604assert_eq! ( before. path , after. path ); 2605let via_extra =resolve ( & resolver, "npm:extra@1" ); 2606assert! ( resolver. npm_source ( & via_extra. path ). is_ok ()); 2607} 2608 2609fn export_requests ( resolver : & NpmResolver , specifier : & str ) ->Vec < String > { 2610block_on ( resolver. export_requests ( specifier)). unwrap () 2611} 2612 2613# [ test ] 2614fn export_requests_covers_string_conditional_and_legacy_roots () { 2615let server =Registry :: new (); 2616 server. publish_package ( 2617"str" , 2618"1.0.0" , 2619 serde_json:: json!({ "exports" : "./index.js" }), 2620& [( "index.js" , "export default 1;" )], 2621); 2622 server. publish_package ( 2623"cond" , 2624"1.0.0" , 2625 serde_json:: json!({ "exports" : { "require" : "./c.cjs" , "import" : "./e.mjs" }}), 2626& [ 2627( "e.mjs" , "export default 1;" ), 2628( "c.cjs" , "module.exports = 1;" ), 2629], 2630); 2631 server. publish_package ( 2632"leg" , 2633"1.0.0" , 2634 serde_json:: json!({}), 2635& [( "index.js" , "export default 1;" )], 2636); 2637let dir = tempfile:: tempdir (). unwrap (); 2638let resolver = server. resolver ( dir. path ()); 2639// Range forms, so frozen runs pin the same roots the runtime prepares. 2640assert_eq! ( export_requests ( & resolver, "npm:str@1" ), [ "npm:str@1" ]); 2641assert_eq! ( export_requests ( & resolver, "npm:cond@1" ), [ "npm:cond@1" ]); 2642assert_eq! ( export_requests ( & resolver, "npm:leg@1" ), [ "npm:leg@1" ]); 2643// The root selects the ESM branch, never the CommonJS one. 2644let root =resolve ( & resolver, "npm:cond@1" ); 2645assert! ( root. path . to_string_lossy (). ends_with ( "e.mjs" )); 2646} 2647 2648# [ test ] 2649fn export_requests_inverts_wildcards_to_public_subpaths () { 2650let server =Registry :: new (); 2651 server. publish_package ( 2652"wild" , 2653"1.0.0" , 2654 serde_json:: json!({ "exports" : { 2655"." : "./index.js" , 2656"./feature/*" : "./dist/*.js" , 2657"./blocked" : null, 2658"./cjs" : "./c.cjs" , 2659}}), 2660& [ 2661( "index.js" , "export default 0;" ), 2662( "dist/foo.js" , "export default 1;" ), 2663( "dist/bar.js" , "export default 2;" ), 2664( "feature/foo.js" , "export default 'private';" ), 2665( "c.cjs" , "module.exports = 3;" ), 2666], 2667); 2668let dir = tempfile:: tempdir (). unwrap (); 2669let resolver = server. resolver ( dir. path ()); 2670let requests =export_requests ( & resolver, "npm:wild@1" ); 2671// CommonJS is warmable now that ESM wrappers support it. 2672assert_eq! ( 2673 requests, 2674[ 2675"npm:wild@1" , 2676"npm:wild@1/cjs" , 2677"npm:wild@1/feature/bar" , 2678"npm:wild@1/feature/foo" 2679] 2680); 2681// The public subpath resolves through the pattern, not the private file. 2682let feature = 2683block_on ( resolver. resolve_request ( "npm:wild@1/feature/foo" , ResolutionMode :: Import )) 2684. unwrap (); 2685assert! ( feature. path . to_string_lossy (). ends_with ( "dist/foo.js" )); 2686} 2687 2688# [ test ] 2689fn export_requests_respects_scopes_and_condition_arrays () { 2690let server =Registry :: new (); 2691 server. publish_package ( 2692"scoped" , 2693"1.0.0" , 2694 serde_json:: json!({ "exports" : { 2695"." : "./index.js" , 2696"./sub" : "./sub.js" , 2697"./sub/deep" : { "import" : [ null, "./deep.mjs" ], "default" : "./deep.cjs" }, 2698"./other" : "./other.js" , 2699}}), 2700& [ 2701( "index.js" , "export default 0;" ), 2702( "sub.js" , "export default 1;" ), 2703( "deep.mjs" , "export default 2;" ), 2704( "other.js" , "export default 3;" ), 2705], 2706); 2707let dir = tempfile:: tempdir (). unwrap (); 2708let resolver = server. resolver ( dir. path ()); 2709assert_eq! ( 2710export_requests ( & resolver, "npm:scoped@1/sub/" ), 2711[ "npm:scoped@1/sub" , "npm:scoped@1/sub/deep" ] 2712); 2713// A missing fallback entry does not fail the array; the ESM hit wins. 2714let deep =block_on ( resolver. resolve_request ( "npm:scoped@1/sub/deep" , ResolutionMode :: Import )) 2715. unwrap (); 2716assert! ( deep. path . to_string_lossy (). ends_with ( "deep.mjs" )); 2717} 2718 2719fn tarball_path ( name : & str , version : & str ) ->String { 2720let leaf = name. rsplit ( '/' ). next (). unwrap (); 2721format! ( "/{name}/-/{leaf}-{version}.tgz" ) 2722} 2723 2724fn wait_for_requests ( server : & Registry , want : & [ String ], since : usize ) { 2725use std:: sync:: atomic:: Ordering ; 2726use std:: time::{ Duration , Instant }; 2727let deadline =Instant :: now () +Duration :: from_secs ( 15 ); 2728loop { 2729let logged = server. requests . lock (). unwrap (); 2730if want. iter (). all ( |path| logged[ since..]. contains ( path)) { 2731return ; 2732} 2733drop ( logged); 2734if Instant :: now () > deadline{ 2735 server. hold_tarballs . store ( false , Ordering :: Relaxed ); 2736 server. hold_paths . lock (). unwrap (). clear (); 2737panic! ( "timed out waiting for requests {want:?}" ); 2738} 2739 std:: thread:: sleep ( Duration :: from_millis ( 10 )); 2740} 2741} 2742 2743fn wait_for_responses ( server : & Registry , want : & [ String ], since : usize ) { 2744use std:: sync:: atomic:: Ordering ; 2745use std:: time::{ Duration , Instant }; 2746let deadline =Instant :: now () +Duration :: from_secs ( 15 ); 2747loop { 2748let done = server. responses . lock (). unwrap (); 2749if want. iter (). all ( |path| done[ since..]. contains ( path)) { 2750return ; 2751} 2752drop ( done); 2753if Instant :: now () > deadline{ 2754 server. hold_tarballs . store ( false , Ordering :: Relaxed ); 2755 server. hold_paths . lock (). unwrap (). clear (); 2756panic! ( "timed out waiting for responses {want:?}" ); 2757} 2758 std:: thread:: sleep ( Duration :: from_millis ( 10 )); 2759} 2760} 2761 2762fn wait_for_materialized ( events : & Arc < std:: sync:: Mutex < Vec < String >>>, want : & str ) { 2763use std:: time::{ Duration , Instant }; 2764let deadline =Instant :: now () +Duration :: from_secs ( 15 ); 2765loop { 2766if events. lock (). unwrap (). iter (). any ( |event| event == want) { 2767return ; 2768} 2769if Instant :: now () > deadline{ 2770panic! ( "timed out waiting for materialized {want:?}" ); 2771} 2772 std:: thread:: sleep ( Duration :: from_millis ( 10 )); 2773} 2774} 2775 2776# [ test ] 2777fn dependency_archives_download_concurrently () { 2778use std:: sync:: atomic:: Ordering ; 2779let server =Registry :: new (); 2780 server. concurrent . store ( true , Ordering :: Relaxed ); 2781 server. publish_package ( 2782"app" , 2783"1.0.0" , 2784 serde_json:: json!({ "dependencies" : { "left" : "^1.0.0" , "right" : "^1.0.0" }}), 2785& [( "index.js" , "export default 0;" )], 2786); 2787for namein [ "left" , "right" ] { 2788 server. publish_package ( 2789 name, 2790"1.0.0" , 2791 serde_json:: json!({}), 2792& [( "index.js" , "export default 0;" )], 2793); 2794} 2795let held =[ 2796tarball_path ( "left" , "1.0.0" ), 2797tarball_path ( "right" , "1.0.0" ), 2798]; 2799 server 2800. hold_paths 2801. lock () 2802. unwrap () 2803. extend ( held. iter (). cloned ()); 2804let dir = tempfile:: tempdir (). unwrap (); 2805let resolver = server. resolver ( dir. path ()); 2806let handle = std:: thread:: spawn ( move ||{ 2807block_on ( resolver. prepare ( & [ NpmRootRequest { 2808request : "npm:app@1" . into (), 2809context : BTreeMap :: new (), 2810}])) 2811}); 2812wait_for_requests ( & server, & held, 0 ); 2813assert! ( 2814 !handle. is_finished (), 2815"both archives must be in flight at once" 2816); 2817assert! ( 2818 server. max_tarballs . load ( Ordering :: SeqCst ) >=2 , 2819"downloads must overlap" 2820); 2821 server. hold_paths . lock (). unwrap (). clear (); 2822 handle. join (). unwrap (). unwrap (); 2823} 2824 2825# [ test ] 2826fn wide_graphs_stay_within_the_download_bound () { 2827use std:: sync:: atomic:: Ordering ; 2828let server =Registry :: new (); 2829 server. concurrent . store ( true , Ordering :: Relaxed ); 2830let mut deps = serde_json:: Map :: new (); 2831for indexin 0 ..20 { 2832let name =format! ( "leaf{index:02}" ); 2833 deps. insert ( name. clone (), serde_json:: Value :: String ( "^1.0.0" . into ())); 2834 server. publish_package ( 2835& name, 2836"1.0.0" , 2837 serde_json:: json!({}), 2838& [( "index.js" , "export default 0;" )], 2839); 2840} 2841 server. publish_package ( 2842"app1" , 2843"1.0.0" , 2844 serde_json:: json!({ "dependencies" : serde_json:: Value :: Object ( deps. clone ())}), 2845& [( "index.js" , "export default 0;" )], 2846); 2847let mut deps2 = serde_json:: Map :: new (); 2848for indexin 20 ..30 { 2849let name =format! ( "leaf{index:02}" ); 2850 deps2. insert ( name. clone (), serde_json:: Value :: String ( "^1.0.0" . into ())); 2851 server. publish_package ( 2852& name, 2853"1.0.0" , 2854 serde_json:: json!({}), 2855& [( "index.js" , "export default 0;" )], 2856); 2857} 2858 server. publish_package ( 2859"app2" , 2860"1.0.0" , 2861 serde_json:: json!({ "dependencies" : serde_json:: Value :: Object ( deps2)}), 2862& [( "index.js" , "export default 0;" )], 2863); 2864let dir = tempfile:: tempdir (). unwrap (); 2865let store = 2866Arc :: new ( PackageStore :: new ( fixture:: runtime_handle (), server. options ( dir. path ())). unwrap ()); 2867let first =NpmResolver :: new ( store. clone ()); 2868let second =NpmResolver :: new ( store); 2869let hold_all: Vec < String > =( 0 ..30 ) 2870. map ( |index|tarball_path ( & format! ( "leaf{index:02}" ), "1.0.0" )) 2871. collect (); 2872 server 2873. hold_paths 2874. lock () 2875. unwrap () 2876. extend ( hold_all. iter (). cloned ()); 2877let prepare = |resolver : NpmResolver , request : & str |{ 2878let request = request. to_owned (); 2879 std:: thread:: spawn ( move ||{ 2880block_on ( resolver. prepare ( & [ NpmRootRequest { 2881 request, 2882context : BTreeMap :: new (), 2883}])) 2884}) 2885}; 2886let first_handle =prepare ( first, "npm:app1@1" ); 2887let second_handle =prepare ( second, "npm:app2@1" ); 2888{ 2889use std:: time::{ Duration , Instant }; 2890let deadline =Instant :: now () +Duration :: from_secs ( 15 ); 2891while server. active_tarballs . load ( Ordering :: SeqCst ) <8 { 2892if Instant :: now () > deadline{ 2893 server. hold_paths . lock (). unwrap (). clear (); 2894panic! ( "parallel downloads never reached the bound" ); 2895} 2896 std:: thread:: sleep ( Duration :: from_millis ( 10 )); 2897} 2898} 2899let max = server. max_tarballs . load ( Ordering :: SeqCst ); 2900assert! ( max >1 , "downloads must overlap: {max}" ); 2901assert! ( max <=8 , "downloads must stay bounded: {max}" ); 2902 server. hold_paths . lock (). unwrap (). clear (); 2903 first_handle. join (). unwrap (). unwrap (); 2904 second_handle. join (). unwrap (). unwrap (); 2905assert! ( 2906 server. max_tarballs . load ( Ordering :: SeqCst ) <=8 , 2907"bound holds through completion" 2908); 2909} 2910 2911# [ test ] 2912fn same_name_aliases_resolve_independently_of_sibling_timing () { 2913use std:: sync:: atomic:: Ordering ; 2914let server =Registry :: new (); 2915 server. concurrent . store ( true , Ordering :: Relaxed ); 2916for versionin [ "1.0.0" , "2.0.0" ] { 2917 server. publish_package ( 2918"foo" , 2919 version, 2920 serde_json:: json!({}), 2921& [( "index.js" , "export default 0;" )], 2922); 2923} 2924for namein [ "x" , "y" ] { 2925 server. publish_package ( 2926 name, 2927"1.0.0" , 2928 serde_json:: json!({}), 2929& [( "index.js" , "export default 0;" )], 2930); 2931} 2932 server. publish_package ( 2933"app" , 2934"1.0.0" , 2935 serde_json:: json!({ "dependencies" : { 2936"a" : "npm:foo@^1.0.0" , 2937"b" : "npm:foo@>=1.0.0 <3.0.0" , 2938"x" : "npm:x@1" , 2939"y" : "npm:y@1" , 2940}}), 2941& [( "index.js" , "export default 0;" )], 2942); 2943let run = |held : & str |{ 2944use avril_runtime:: registry:: ProgressEvent ; 2945let since = server. requests . lock (). unwrap (). len (); 2946let dir = tempfile:: tempdir (). unwrap (); 2947let materialized =Arc :: new ( std:: sync:: Mutex :: new ( Vec ::< String >:: new ())); 2948let sink_materialized = materialized. clone (); 2949let store =Arc :: new ( 2950PackageStore :: new ( fixture:: runtime_handle (), server. options ( dir. path ())) 2951. unwrap () 2952. with_progress ( Arc :: new ( move |event|{ 2953if let ProgressEvent :: NpmMaterialized { name, version} = event{ 2954 sink_materialized 2955. lock () 2956. unwrap () 2957. push ( format! ( "{name}@{version}" )); 2958} 2959})), 2960); 2961let resolver =NpmResolver :: new ( store. clone ()); 2962 server 2963. hold_paths 2964. lock () 2965. unwrap () 2966. insert ( tarball_path ( held, "1.0.0" )); 2967let handle = std:: thread:: spawn ( move ||{ 2968block_on ( resolver. prepare ( & [ NpmRootRequest { 2969request : "npm:app@1" . into (), 2970context : BTreeMap :: new (), 2971}])) 2972}); 2973let other =if held =="x" { "y" } else { "x" }; 2974wait_for_requests ( & server, & [ tarball_path ( held, "1.0.0" )], since); 2975wait_for_materialized ( & materialized, & format! ( "{other}@1.0.0" )); 2976 server. hold_paths . lock (). unwrap (). clear (); 2977 handle. join (). unwrap (). unwrap (); 2978 store. save (). unwrap (); 2979let lock = fixture:: read_lock ( dir. path ()); 2980assert_eq! ( lock[ "npm" ][ "specifiers" ][ "npm:foo@^1.0.0" ], "1.0.0" ); 2981assert_eq! ( lock[ "npm" ][ "specifiers" ][ "npm:foo@>=1.0.0 <3.0.0" ], "1.0.0" ); 2982 lock 2983}; 2984let first =run ( "x" ); 2985let second =run ( "y" ); 2986assert_eq! ( first, second); 2987} 2988 2989# [ test ] 2990fn sibling_failures_report_the_first_request_and_retry_cleanly () { 2991use std:: sync:: atomic:: Ordering ; 2992let server =Registry :: new (); 2993 server. concurrent . store ( true , Ordering :: Relaxed ); 2994for namein [ "bad1" , "bad2" , "good" ] { 2995 server. publish_package ( 2996 name, 2997"1.0.0" , 2998 serde_json:: json!({}), 2999& [( "index.js" , "export default 0;" )], 3000); 3001} 3002let bad2_tarball =tarball_path ( "bad2" , "1.0.0" ); 3003let bad1_tarball =tarball_path ( "bad1" , "1.0.0" ); 3004let good_tarball =tarball_path ( "good" , "1.0.0" ); 3005let bad2_original = server. files . lock (). unwrap ()[ & bad2_tarball]. clone (); 3006 server. files . lock (). unwrap (). remove ( & bad1_tarball); 3007 server 3008. files 3009. lock () 3010. unwrap () 3011. insert ( bad2_tarball. clone (), b"not gzip" . to_vec ()); 3012 server 3013. hold_paths 3014. lock () 3015. unwrap () 3016. insert ( bad1_tarball. clone ()); 3017 server. publish_package ( 3018"app" , 3019"1.0.0" , 3020 serde_json:: json!({ "dependencies" : { "abad" : "npm:bad1@1" , "bads" : "npm:bad2@1" , "fine" : "npm:good@1" }}), 3021& [ 3022( "index.js" , "export default 0;" ), 3023], 3024); 3025let dir = tempfile:: tempdir (). unwrap (); 3026let store = 3027Arc :: new ( PackageStore :: new ( fixture:: runtime_handle (), server. options ( dir. path ())). unwrap ()); 3028let resolver =Arc :: new ( NpmResolver :: new ( store)); 3029let logged = server. requests . lock (). unwrap (). len (); 3030let responded = server. responses . lock (). unwrap (). len (); 3031let worker = resolver. clone (); 3032let handle = std:: thread:: spawn ( move ||{ 3033block_on ( worker. prepare ( & [ NpmRootRequest { 3034request : "npm:app@1" . into (), 3035context : BTreeMap :: new (), 3036}])) 3037}); 3038wait_for_requests ( & server, std:: slice:: from_ref ( & bad1_tarball), logged); 3039wait_for_responses ( & server, & [ bad2_tarball. clone (), good_tarball], responded); 3040 server. hold_paths . lock (). unwrap (). clear (); 3041let error = handle. join (). unwrap (). unwrap_err (). to_string (); 3042assert! ( 3043 error. contains ( "bad1" ), 3044"first input error must win: {error:?}" 3045); 3046 server 3047. files 3048. lock () 3049. unwrap () 3050. insert ( bad2_tarball, bad2_original); 3051 server. publish_package ( 3052"bad1" , 3053"1.0.0" , 3054 serde_json:: json!({}), 3055& [( "index.js" , "export default 0;" )], 3056); 3057let handle =block_on ( resolver. prepare ( & [ NpmRootRequest { 3058request : "npm:app@1" . into (), 3059context : BTreeMap :: new (), 3060}])) 3061. unwrap (); 3062assert! ( !handle. roots . is_empty (), "retry must establish roots" ); 3063} 3064 3065# [ test ] 3066fn export_requests_propagates_resolution_errors () { 3067let server =Registry :: new (); 3068 server. publish_package ( 3069"gone" , 3070"1.0.0" , 3071 serde_json:: json!({ "exports" : { " . " : "./missing.js" }}), 3072& [( "index.js" , "export default 0;" )], 3073); 3074let dir = tempfile:: tempdir (). unwrap (); 3075let resolver = server. resolver ( dir. path ()); 3076block_on ( resolver. export_requests ( "npm:gone@1" )). unwrap_err (); 3077}