char/avril
(wip) why'd you have to go and make things so complicaaaaated?
git clone https://git.t4t.associates/char/avril
688e96e
main
1use avril_runtime:: config::{ ValueKind , parse}; 2# [ path = "common/registry.rs" ] 3mod fixture; 4 5use avril_runtime:: config_edit::{ Addition , Change , add, add_file, unversioned, with_caret}; 6use fixture:: Registry ; 7use std:: fs; 8use std:: path:: Path ; 9use std:: process::{ Command , Output }; 10 11fn registry () ->Registry { 12let server =Registry :: new (); 13for versionin [ "1.0.0" , "2.0.0" ] { 14 server. package ( 15"@test/pkg" , 16 version, 17& [ 18( "mod.js" , & format! ( "export const version = '{version}';" )), 19( "sub.js" , "export const sub = true;" ), 20], 21& [( "." , "./mod.js" ), ( "./sub" , "./sub.js" )], 22); 23} 24 server 25} 26 27/// Runs `avril add` against `server`, caching beneath `root`. 28fn avril_add ( server : & Registry , root : & Path , cwd : & Path , args : & [ & str ]) ->Output { 29let options = server. options ( root); 30Command :: new ( env! ( "CARGO_BIN_EXE_avril" )) 31. current_dir ( cwd) 32. arg ( "add" ) 33. arg ( format! ( "--jsr-registry={}" , options. jsr_registry )) 34. arg ( format! ( "--cache-dir={}" , options. cache . display ())) 35. args ( args) 36. output () 37. unwrap () 38} 39 40fn import < ' a >( alias : &' a str , target : &' a str ) ->Addition < ' a > { 41Addition :: Import { alias, target} 42} 43 44/// The package bound to `binding` in a config's deps. 45fn dependency ( source : & str , binding : & str ) ->String { 46let ValueKind :: Object ( root) =parse ( source). unwrap (). kind else { 47panic! () 48}; 49let ValueKind :: Array ( deps) =& root 50. iter () 51. find ( |member| member. key =="deps" ) 52. unwrap () 53. value 54. kind 55else { 56panic! () 57}; 58let string = |value : & avril_runtime:: config:: Value |match & value. kind { 59ValueKind :: String ( value) => value. clone (), 60 _ =>panic! (), 61}; 62 deps. iter () 63. find_map ( |entry|match & entry. kind { 64ValueKind :: String ( from) =>( from. starts_with ( & format! ( "jsr:{binding}@" )) 65 ||* from ==format! ( "jsr:{binding}" )) 66. then ( || from. clone ()), 67ValueKind :: Object ( fields) =>{ 68let field = 69 |key : & str |& fields. iter (). find ( |field| field. key == key). unwrap (). value ; 70( string ( field ( "name" )) == binding). then ( ||string ( field ( "from" ))) 71} 72 _ =>None , 73}) 74. unwrap () 75} 76 77fn target ( source : & str , alias : & str ) ->String { 78let ValueKind :: Object ( root) =parse ( source). unwrap (). kind else { 79panic! () 80}; 81let ValueKind :: Object ( imports) =& root 82. iter () 83. find ( |member| member. key =="imports" ) 84. unwrap () 85. value 86. kind 87else { 88panic! () 89}; 90let ValueKind :: String ( value) =& imports 91. iter () 92. find ( |member| member. key == alias) 93. unwrap () 94. value 95. kind 96else { 97panic! () 98}; 99 value. clone () 100} 101 102# [ test ] 103fn additions_preserve_comments_other_entries_and_decoded_keys () { 104for sourcein [ 105"" , 106"// last comment" , 107"{/* inside */}" , 108"imports: {/* keep */}" , 109"{imports: {old: './old.ts' /* old comment */}, scopes: {} /* scope comment */}" , 110"// heading\nimports: {\n old: './old.ts', // trailing\n}, // end\n" , 111] { 112let edited =add ( source, & import ( "alias" , "jsr:@test/pkg@^1" )). unwrap (). 0 ; 113assert_eq! ( target ( & edited, "alias" ), "jsr:@test/pkg@^1" ); 114for commentin [ 115"// last comment" , 116"/* inside */" , 117"/* keep */" , 118"/* old comment */" , 119"/* scope comment */" , 120"// heading" , 121"// trailing" , 122"// end" , 123] { 124if source. contains ( comment) { 125assert! ( edited. contains ( comment)); 126} 127} 128if source. contains ( "old:" ) { 129assert_eq! ( target ( & edited, "old" ), "./old.ts" ); 130} 131} 132let source ="imports: {'de\\u0070': /* choice */ 'jsr:@test/pkg@1' /* reason */}" ; 133let ( edited, change) =add ( source, & import ( "dep" , "jsr:@test/pkg@2" )). unwrap (); 134assert_eq! ( 135 change, 136Change :: Updated { 137previous : Some ( "jsr:@test/pkg@1" . into ()) 138} 139); 140assert_eq! ( 141 edited, 142 source. replace ( "'jsr:@test/pkg@1'" , "'jsr:@test/pkg@2'" ) 143); 144assert_eq! ( target ( & edited, "dep" ), "jsr:@test/pkg@2" ); 145let edited =add ( "imports:{}" , & import ( "lib/" , "jsr:@test/pkg@^1/" )) 146. unwrap () 147. 0 ; 148assert_eq! ( target ( & edited, "lib/" ), "jsr:@test/pkg@^1/" ); 149} 150 151# [ test ] 152fn additions_work_with_implicit_commas_without_rewriting_existing_entries () { 153let source ="name: 'app'\ndeps: [\n 'jsr:@test/old@1' // pinned\n '../local'\n]\nimports: {\n old: './old.ts'\n other: './other.ts'\n}\n" ; 154for newlinein [ "\n" , "\r\n" , "\r" ] { 155let source = source. replace ( '\n' , newline); 156let ( edited, change) =add ( & source, & import ( "alias" , "jsr:@test/pkg@^1" )). unwrap (); 157assert_eq! ( change, Change :: Added ); 158assert_eq! ( target ( & edited, "alias" ), "jsr:@test/pkg@^1" ); 159assert_eq! ( target ( & edited, "old" ), "./old.ts" ); 160assert_eq! ( target ( & edited, "other" ), "./other.ts" ); 161let addition =Addition :: Dependency { 162name : None , 163package : "jsr:@test/pkg@2" , 164}; 165let ( edited, change) =add ( & edited, & addition). unwrap (); 166assert_eq! ( change, Change :: Added ); 167assert_eq! ( dependency ( & edited, "@test/pkg" ), "jsr:@test/pkg@2" ); 168assert_eq! ( dependency ( & edited, "@test/old" ), "jsr:@test/old@1" ); 169for keptin [ 170"name: 'app'" , 171"'jsr:@test/old@1' // pinned\n '../local'" , 172"old: './old.ts'\n other: './other.ts'" , 173] { 174assert! ( edited. contains ( & kept. replace ( '\n' , newline)), "{edited:?}" ); 175} 176} 177} 178 179# [ test ] 180fn invalid_edits_do_not_replace_files_and_existing_modes_are_preserved () { 181let dir = tempfile:: tempdir (). unwrap (); 182let path = dir. path (). join ( "avril.conf" ); 183 fs:: write ( & path, "imports: {old: './old.ts'}" ). unwrap (); 184for ( alias, target) in [ 185( "../dep" , "npm:pkg@1" ), 186( "dep/" , "jsr:@test/pkg@1" ), 187( "../dep" , "jsr:@test/pkg@1" ), 188( "" , "jsr:@test/pkg@1" ), 189] { 190assert! ( add_file ( & path, & import ( alias, target)). is_err ()); 191assert_eq! ( 192 fs:: read_to_string ( & path). unwrap (), 193"imports: {old: './old.ts'}" 194); 195} 196# [ cfg ( unix )] 197{ 198use std:: os:: unix:: fs:: PermissionsExt ; 199 fs:: set_permissions ( & path, fs:: Permissions :: from_mode ( 0o600 )). unwrap (); 200add_file ( & path, & import ( "dep" , "jsr:@test/pkg@1" )). unwrap (); 201assert_eq! ( 202 fs:: metadata ( & path). unwrap (). permissions (). mode () & 0o777 , 2030o600 204); 205} 206} 207 208# [ test ] 209fn add_command_uses_the_nearest_config_without_serializing_it () { 210let dir = tempfile:: tempdir (). unwrap (); 211 fs:: create_dir ( dir. path (). join ( "src" )). unwrap (); 212 fs:: write ( 213 dir. path (). join ( "avril.conf" ), 214"// chosen by hand\nimports: {old: './old.ts'},\n" , 215) 216. unwrap (); 217let server =registry (); 218let output =avril_add ( 219& server, 220 dir. path (), 221& dir. path (). join ( "src" ), 222& [ "dep" , "jsr:@test/pkg@^1" ], 223); 224assert! ( 225 output. status . success (), 226"{}" , 227String :: from_utf8_lossy ( & output. stderr ) 228); 229let source = fs:: read_to_string ( dir. path (). join ( "avril.conf" )). unwrap (); 230assert! ( source. starts_with ( "// chosen by hand\n" )); 231assert! ( source. contains ( "old: './old.ts'" )); 232assert_eq! ( dependency ( & source, "dep" ), "jsr:@test/pkg@^1" ); 233assert! ( !dir. path (). join ( "src/avril.conf" ). exists ()); 234} 235 236# [ cfg ( unix )] 237# [ test ] 238fn config_outputs_do_not_follow_symlinks_or_open_fifos () { 239use std:: os:: unix:: fs:: symlink; 240let dir = tempfile:: tempdir (). unwrap (); 241let destination = dir. path (). join ( "secret" ); 242let path = dir. path (). join ( "avril.conf" ); 243 fs:: write ( & destination, "sentinel" ). unwrap (); 244symlink ( & destination, & path). unwrap (); 245assert! ( add_file ( & path, & import ( "dep" , "jsr:@test/pkg@1" )). is_err ()); 246assert_eq! ( fs:: read_to_string ( & destination). unwrap (), "sentinel" ); 247 fs:: remove_file ( & path). unwrap (); 248let name = std:: ffi:: CString :: new ( path. as_os_str (). as_encoded_bytes ()). unwrap (); 249assert_eq! ( unsafe { libc:: mkfifo ( name. as_ptr (), 0o600 ) }, 0 ); 250assert! ( add_file ( & path, & import ( "dep" , "jsr:@test/pkg@1" )). is_err ()); 251} 252 253# [ test ] 254fn editing_and_loading_malformed_configs_have_the_same_diagnostics () { 255let base = url:: Url :: parse ( if cfg! ( windows) { 256"file:///C:/package/avril.conf" 257} else { 258"file:///package/avril.conf" 259}) 260. unwrap (); 261for sourcein [ 262"imports { }" , 263"name 'app'" , 264"[]" , 265"{imports: {old 'broken'}}" , 266] { 267let error = avril_runtime:: package_config:: PackageConfig :: parse ( source, & base). unwrap_err (); 268assert_eq! ( 269add ( source, & import ( "dep" , "jsr:@test/pkg@1" )). unwrap_err (), 270 error. render ( "avril.conf" , source) 271); 272} 273} 274 275# [ cfg ( unix )] 276# [ test ] 277fn add_does_not_shadow_an_untrusted_ancestor_config () { 278use std:: os:: unix:: fs:: symlink; 279let dir = tempfile:: tempdir (). unwrap (); 280 fs:: create_dir ( dir. path (). join ( "src" )). unwrap (); 281 fs:: write ( dir. path (). join ( "other.conf" ), "// sentinel" ). unwrap (); 282symlink ( "other.conf" , dir. path (). join ( "avril.conf" )). unwrap (); 283let output =Command :: new ( env! ( "CARGO_BIN_EXE_avril" )) 284. current_dir ( dir. path (). join ( "src" )) 285. args ([ "add" , "dep" , "jsr:@test/pkg@1" ]) 286. output () 287. unwrap (); 288assert! ( !output. status . success ()); 289assert! ( String :: from_utf8_lossy ( & output. stderr ). contains ( "Untrusted config" )); 290assert! ( !dir. path (). join ( "src/avril.conf" ). exists ()); 291assert_eq! ( 292 fs:: read_to_string ( dir. path (). join ( "other.conf" )). unwrap (), 293"// sentinel" 294); 295} 296 297# [ test ] 298fn additions_preserve_extension_fields () { 299let source =r#"// project extensions 300custom: {nested: [null, true, 42], name: false} 301importMap: {specifierMap: {old: './old.ts'}} 302tasks: {build: {command: 'echo built', custom: {color: 'green'}}} 303deps: [{from: 'jsr:@test/pkg@1', name: 'pkg', custom: {optional: true}}] 304"# ; 305let dir = tempfile:: tempdir (). unwrap (); 306let path = dir. path (). join ( "avril.conf" ); 307 fs:: write ( & path, source). unwrap (); 308add_file ( & path, & import ( "helper" , "jsr:@std/assert@^1" )). unwrap (); 309let edited = fs:: read_to_string ( & path). unwrap (); 310assert! ( edited. starts_with ( source), "{edited}" ); 311assert_eq! ( target ( & edited, "helper" ), "jsr:@std/assert@^1" ); 312 313add_file ( 314& path, 315& Addition :: Dependency { 316name : Some ( "pkg" ), 317package : "jsr:@test/pkg@2" , 318}, 319) 320. unwrap (); 321let updated = fs:: read_to_string ( & path). unwrap (); 322assert_eq! ( updated, edited. replace ( "@test/pkg@1" , "@test/pkg@2" )); 323assert_eq! ( dependency ( & updated, "pkg" ), "jsr:@test/pkg@2" ); 324} 325 326# [ test ] 327fn additions_preserve_metadata_dependencies_and_scope_maps () { 328for sourcein [ 329"name: '@app/demo', version: '0.1.0', description: 'Demo', exports: './mod.ts'" , 330"name: '@app/demo', imports: {/* maps */}" , 331"name: '@app/demo', deps: ['../unread' /* local */, {from: '../math', name: 'math'}], scopes: {'./test/': {helper: './helper.ts'}}" , 332] { 333let edited =add ( source, & import ( "assert" , "jsr:@std/assert@^1" )) 334. unwrap () 335. 0 ; 336assert_eq! ( target ( & edited, "assert" ), "jsr:@std/assert@^1" ); 337for unchangedin [ 338"name: '@app/demo'" , 339"version: '0.1.0'" , 340"description: 'Demo'" , 341"exports: './mod.ts'" , 342"/* maps */" , 343"deps: ['../unread' /* local */, {from: '../math', name: 'math'}]" , 344"scopes: {'./test/': {helper: './helper.ts'}}" , 345] { 346if source. contains ( unchanged) { 347assert! ( edited. contains ( unchanged)); 348} 349} 350} 351} 352 353# [ test ] 354fn add_rejects_invalid_fields_and_reserved_package_names_without_replacing_files () { 355let dir = tempfile:: tempdir (). unwrap (); 356let path = dir. path (). join ( "avril.conf" ); 357for ( source, alias, message) in [ 358( "imports: []" , "dep" , "Expected an object" ), 359( "name: 'math', exports: './mod.ts'" , "math" , "conflicts" ), 360( 361"deps: [{from: '../math', name: 'math'}]" , 362"math/vector" , 363"conflicts" , 364), 365] { 366 fs:: write ( & path, source). unwrap (); 367assert! ( 368add_file ( & path, & import ( alias, "jsr:@test/pkg@1" )) 369. unwrap_err () 370. to_string () 371. contains ( message) 372); 373assert_eq! ( fs:: read_to_string ( & path). unwrap (), source); 374} 375} 376 377# [ test ] 378fn add_without_an_alias_depends_on_the_whole_package_and_aliases_choose_the_field () { 379let dir = tempfile:: tempdir (). unwrap (); 380let server =registry (); 381let add = |args : & [ & str ] |{ 382let output =avril_add ( & server, dir. path (), dir. path (), args); 383assert! ( 384 output. status . success (), 385"{}" , 386String :: from_utf8_lossy ( & output. stderr ) 387); 388String :: from_utf8 ( output. stderr ). unwrap () 389}; 390let source = || fs:: read_to_string ( dir. path (). join ( "avril.conf" )). unwrap (); 391assert! ( add ( & [ "jsr:@test/pkg" ]). contains ( "Added " )); 392assert_eq! ( dependency ( & source (), "@test/pkg" ), "jsr:@test/pkg@^2.0.0" ); 393assert! ( add ( & [ "jsr:@test/pkg@^1/sub" ]). contains ( "Updated " )); 394assert! ( add ( & [ "@test/pkg" , "jsr:@test/pkg@^1" ]). contains ( "Unchanged " )); 395assert! ( add ( & [ "pkg" , "jsr:@test/pkg@^1" ]). contains ( "Added " )); 396assert! ( add ( & [ "sub" , "jsr:@test/pkg@^1/sub" ]). contains ( "Added " )); 397assert! ( add ( & [ "prefix/" , "jsr:@test/pkg@^1/" ]). contains ( "Added " )); 398assert! ( add ( & [ "slashed" , "jsr:/@test/pkg@^1" ]). contains ( "Added " )); 399let source =source (); 400assert_eq! ( dependency ( & source, "@test/pkg" ), "jsr:@test/pkg@^1" ); 401assert_eq! ( dependency ( & source, "pkg" ), "jsr:@test/pkg@^1" ); 402assert_eq! ( dependency ( & source, "slashed" ), "jsr:@test/pkg@^1" ); 403assert_eq! ( target ( & source, "sub" ), "jsr:@test/pkg@^1/sub" ); 404assert_eq! ( target ( & source, "prefix/" ), "jsr:@test/pkg@^1/" ); 405} 406 407# [ test ] 408fn edits_report_whether_the_alias_was_added_updated_or_already_mapped () { 409let source ="imports: {blocked: null, same: 'jsr:@test/\\u0070kg@1' /* kept */}" ; 410for ( alias, change) in [ 411( "fresh" , Change :: Added ), 412( "blocked" , Change :: Updated { previous : None }), 413( "same" , Change :: Unchanged ), 414] { 415let ( edited, actual) =add ( source, & import ( alias, "jsr:@test/pkg@1" )). unwrap (); 416assert_eq! ( actual, change, "{alias}" ); 417assert_eq! ( edited == source, change ==Change :: Unchanged , "{alias}" ); 418assert_eq! ( target ( & edited, alias), "jsr:@test/pkg@1" ); 419} 420} 421 422# [ test ] 423fn add_command_reports_the_mapping_on_stdout_and_its_status_on_stderr () { 424let dir = tempfile:: tempdir (). unwrap (); 425 fs:: create_dir ( dir. path (). join ( "src" )). unwrap (); 426 fs:: write ( dir. path (). join ( "avril.conf" ), "" ). unwrap (); 427let server =registry (); 428let add = |cwd : & str , args : & [ & str ] |{ 429let output =avril_add ( & server, dir. path (), & dir. path (). join ( cwd), args); 430assert! ( 431 output. status . success (), 432"{}" , 433String :: from_utf8_lossy ( & output. stderr ) 434); 435( 436String :: from_utf8 ( output. stdout ). unwrap (), 437String :: from_utf8 ( output. stderr ). unwrap (), 438) 439}; 440let config = dir. path (). join ( "avril.conf" ); 441for ( cwd, args, stdout, stderr) in [ 442( 443"." , 444& [ "jsr:@test/pkg@1" ][ ..], 445"\"@test/pkg\": \"jsr:@test/pkg@1\" in avril.conf\n" . to_owned (), 446"Added " , 447), 448( 449"src" , 450& [ "@test/pkg" , "jsr:@test/pkg@2" ], 451format! ( 452"\"@test/pkg\": \"jsr:@test/pkg@1\" → \"jsr:@test/pkg@2\" in {}\n" , 453 config. display () 454), 455"Updated " , 456), 457( 458"." , 459& [ "--config=avril.conf" , "@test/pkg" , "jsr:@test/pkg@2" ], 460"\"@test/pkg\": \"jsr:@test/pkg@2\" in avril.conf\n" . to_owned (), 461"Unchanged " , 462), 463] { 464let ( actual_stdout, actual_stderr) =add ( cwd, args); 465assert_eq! ( actual_stdout, stdout, "{args:?}" ); 466assert! ( 467 actual_stderr. contains ( stderr), 468"{args:?}: {actual_stderr:?}" 469); 470} 471} 472 473# [ cfg ( unix )] 474# [ test ] 475fn add_command_escapes_terminal_controls_in_config_paths () { 476let dir = tempfile:: tempdir (). unwrap (); 477let server =registry (); 478let output =avril_add ( 479& server, 480 dir. path (), 481 dir. path (), 482& [ "--config=a\x1b[2J\nb/avril.conf" , "jsr:@test/pkg@1" ], 483); 484assert! ( 485 output. status . success (), 486"{}" , 487String :: from_utf8_lossy ( & output. stderr ) 488); 489assert_eq! ( 490String :: from_utf8 ( output. stdout ). unwrap (), 491"\"@test/pkg\": \"jsr:@test/pkg@1\" in a\\u{1b}[2J\\nb/avril.conf\n" 492); 493} 494 495# [ test ] 496fn unversioned_targets_gain_a_caret_range_that_keeps_their_subpath () { 497for ( target, pinned) in [ 498( "jsr:@std/path" , "jsr:@std/path@^1.2.3" ), 499( "jsr:@std/path/posix" , "jsr:@std/path@^1.2.3/posix" ), 500( "jsr:@std/path/" , "jsr:@std/path@^1.2.3/" ), 501( "jsr:/@std/path/posix/" , "jsr:@std/path@^1.2.3/posix/" ), 502] { 503let name =unversioned ( target). unwrap (). unwrap (); 504assert_eq! ( name, "@std/path" ); 505assert_eq! ( with_caret ( target, & name, "1.2.3" ), pinned); 506} 507for targetin [ 508"jsr:@std/path@1" , 509"jsr:@std/path@*/posix" , 510"jsr:/@std/path@^1/" , 511] { 512assert_eq! ( unversioned ( target). unwrap (), None , "{target}" ); 513} 514} 515 516# [ test ] 517fn add_selects_the_newest_stable_release_unless_prereleases_are_requested () { 518let server =registry (); 519for versionin [ "2.1.0" , "3.0.0-rc.1" ] { 520 server. package ( 521"@test/pkg" , 522 version, 523& [( "mod.js" , "export const version = 'later';" )], 524& [( "." , "./mod.js" )], 525); 526} 527 server. package ( 528"@test/next" , 529"1.0.0-beta.1" , 530& [( "mod.js" , "export {};" )], 531& [( "." , "./mod.js" )], 532); 533{ 534let mut files = server. files . lock (). unwrap (); 535let index = files. get_mut ( "/@test/pkg/meta.json" ). unwrap (); 536let mut parsed: serde_json:: Value = serde_json:: from_slice ( index). unwrap (); 537 parsed[ "versions" ][ "2.1.0" ][ "yanked" ] =true . into (); 538* index = serde_json:: to_vec ( & parsed). unwrap (); 539} 540let dir = tempfile:: tempdir (). unwrap (); 541let add = |args : & [ & str ] |avril_add ( & server, dir. path (), dir. path (), args); 542let source = || fs:: read_to_string ( dir. path (). join ( "avril.conf" )). unwrap (); 543assert! ( add ( & [ "jsr:@test/pkg" ]). status . success ()); 544assert_eq! ( dependency ( & source (), "@test/pkg" ), "jsr:@test/pkg@^2.0.0" ); 545assert! ( add ( & [ "--pre" , "jsr:@test/pkg" ]). status . success ()); 546assert_eq! ( 547dependency ( & source (), "@test/pkg" ), 548"jsr:@test/pkg@^3.0.0-rc.1" 549); 550let before =source (); 551for ( args, message) in [ 552( & [ "jsr:@test/next" ][ ..], "--pre allows prereleases" ), 553( 554& [ "--pre" , "jsr:@test/pkg@1" ], 555"only applies to targets without a version" , 556), 557] { 558let output =add ( args); 559assert! ( !output. status . success (), "{args:?}" ); 560assert! ( 561String :: from_utf8_lossy ( & output. stderr ). contains ( message), 562"{args:?}: {}" , 563String :: from_utf8_lossy ( & output. stderr ) 564); 565assert_eq! ( source (), before); 566} 567assert! ( add ( & [ "--pre" , "jsr:@test/next" ]). status . success ()); 568assert_eq! ( 569dependency ( & source (), "@test/next" ), 570"jsr:@test/next@^1.0.0-beta.1" 571); 572} 573 574# [ test ] 575fn add_caches_what_the_mapping_can_import_so_programs_run_offline () { 576let server =registry (); 577 server. package ( 578"@test/other" , 579"1.0.0" , 580& [( "unused.js" , "export {};" )], 581& [( "./unused" , "./unused.js" )], 582); 583let dir = tempfile:: tempdir (). unwrap (); 584for argsin [ & [ "jsr:@test/pkg@1" ][ ..], & [ "next/" , "jsr:@test/pkg/" ]] { 585let output =avril_add ( & server, dir. path (), dir. path (), args); 586assert! ( 587 output. status . success (), 588"{}" , 589String :: from_utf8_lossy ( & output. stderr ) 590); 591} 592let lock = fixture:: read_lock ( dir. path ()); 593assert_eq! ( lock[ "specifiers" ][ "jsr:@test/pkg@1" ], "1.0.0" ); 594assert_eq! ( lock[ "specifiers" ][ "jsr:@test/pkg@^2.0.0" ], "2.0.0" ); 595 fs:: write ( 596 dir. path (). join ( "main.js" ), 597"import {version} from '@test/pkg'; import {sub} from '@test/pkg/sub'; import {sub as next} from 'next/sub'; console.log(version, sub, next);" , 598) 599. unwrap (); 600let requests = server. requests . lock (). unwrap (). len (); 601let output =Command :: new ( env! ( "CARGO_BIN_EXE_avril" )) 602. arg ( "run" ) 603. current_dir ( dir. path ()) 604. arg ( format! ( "--jsr-registry={}" , server. url )) 605. arg ( format! ( 606"--cache-dir={}" , 607 dir. path (). join ( "cache" ). display () 608)) 609. args ([ "--offline" , "--frozen" , "main.js" ]) 610. output () 611. unwrap (); 612assert! ( 613 output. status . success (), 614"{}" , 615String :: from_utf8_lossy ( & output. stderr ) 616); 617assert_eq! ( output. stdout , b"1.0.0 true true\n" ); 618assert_eq! ( server. requests . lock (). unwrap (). len (), requests); 619let output =avril_add ( 620& server, 621 dir. path (), 622 dir. path (), 623& [ "--offline" , "jsr:@test/other" ], 624); 625assert! ( !output. status . success ()); 626assert_eq! ( server. requests . lock (). unwrap (). len (), requests); 627} 628 629# [ test ] 630fn add_caches_registry_targets_without_the_consumers_maps_and_keeps_edits_on_failure () { 631let server =registry (); 632 server. package ( 633"@test/root" , 634"1.0.0" , 635& [ 636( "mod.js" , "export const root = 1;" ), 637( "slash.js" , "export const slash = 1;" ), 638], 639& [( "." , "./mod.js" ), ( "./slash/" , "./slash.js" )], 640); 641 server. package ( 642"@test/broken" , 643"1.0.0" , 644& [( "mod.js" , "import './missing.js';" )], 645& [( "." , "./mod.js" )], 646); 647let dir = tempfile:: tempdir (). unwrap (); 648 fs:: write ( 649 dir. path (). join ( "avril.conf" ), 650"imports: {'jsr:@test/pkg@1': null}" , 651) 652. unwrap (); 653let add = |args : & [ & str ] |avril_add ( & server, dir. path (), dir. path (), args); 654let lock = || fixture:: read_lock ( dir. path ()); 655for argsin [ 656& [ "lib" , "jsr:@test/pkg@1" ][ ..], 657& [ "root/" , "jsr:@test/root@1/" ], 658& [ "two" , "jsr:@test/pkg@2" ], 659] { 660let output =add ( args); 661assert! ( 662 output. status . success (), 663"{args:?}: {}" , 664String :: from_utf8_lossy ( & output. stderr ) 665); 666} 667assert_eq! ( lock ()[ "specifiers" ][ "jsr:@test/pkg@1" ], "1.0.0" ); 668assert_eq! ( lock ()[ "specifiers" ][ "jsr:@test/root@1" ], "1.0.0" ); 669let requests = server. requests . lock (). unwrap (). len (); 670let output =add ( & [ "--offline" , "latest" , "jsr:@test/pkg" ]); 671assert! ( 672 output. status . success (), 673"{}" , 674String :: from_utf8_lossy ( & output. stderr ) 675); 676assert_eq! ( server. requests . lock (). unwrap (). len (), requests); 677let source = fs:: read_to_string ( dir. path (). join ( "avril.conf" )). unwrap (); 678assert_eq! ( dependency ( & source, "latest" ), "jsr:@test/pkg@^2.0.0" ); 679// An unchanged edit still caches, refilling a removed cache. 680 fs:: remove_dir_all ( dir. path (). join ( "cache" )). unwrap (); 681let output =add ( & [ "lib" , "jsr:@test/pkg@1" ]); 682assert! ( 683String :: from_utf8_lossy ( & output. stderr ). contains ( "Unchanged " ), 684"{}" , 685String :: from_utf8_lossy ( & output. stderr ) 686); 687let fetched = |path : & str |{ 688let requests = server. requests . lock (). unwrap (); 689 requests. iter (). filter ( |request|* request == path). count () 690}; 691assert_eq! ( fetched ( "/@test/pkg/1.0.0/mod.js" ), 2 ); 692assert_eq! ( fetched ( "/@test/root/1.0.0/mod.js" ), 1 ); 693assert_eq! ( fetched ( "/@test/root/1.0.0/slash.js" ), 1 ); 694let output =add ( & [ "broken" , "jsr:@test/broken@1" ]); 695assert! ( !output. status . success ()); 696assert! ( String :: from_utf8_lossy ( & output. stderr ). contains ( "missing.js" )); 697let source = fs:: read_to_string ( dir. path (). join ( "avril.conf" )). unwrap (); 698assert_eq! ( dependency ( & source, "broken" ), "jsr:@test/broken@1" ); 699} 700 701# [ test ] 702fn dependency_additions_insert_replace_and_keep_formatting () { 703let dependency_of = |name, package|Addition :: Dependency { name, package}; 704for ( source, name) in [ 705( "" , None ), 706( "// keep\r\nname: 'app',\r\n" , Some ( "pkg" )), 707( "deps: [\n '../local', // local\n]" , None ), 708( "deps: []" , Some ( "@test/pkg" )), 709] { 710let ( edited, change) =add ( source, & dependency_of ( name, "jsr:@test/pkg@2" )). unwrap (); 711assert_eq! ( change, Change :: Added , "{source:?}" ); 712 713assert_eq! ( 714dependency ( & edited, name. unwrap_or ( "@test/pkg" )), 715"jsr:@test/pkg@2" 716); 717for keptin [ "// keep" , "name: 'app',\r\n" , " '../local', // local\n" ] { 718assert_eq! ( edited. contains ( kept), source. contains ( kept)); 719} 720if source. contains ( "\r\n" ) { 721assert! ( !edited. replace ( "\r\n" , "" ). contains ( '\n' ), "{edited:?}" ); 722} 723} 724for ( source, name) in [ 725( "deps: ['jsr:@test/pkg@1' /* pinned */, '../local']" , None ), 726( 727"deps: [{from: 'jsr:@test/pkg@1', name: 'pkg'}]" , 728Some ( "pkg" ), 729), 730( "deps: [{from: '../local', name: 'pkg'}]" , Some ( "pkg" )), 731] { 732let ( edited, change) =add ( source, & dependency_of ( name, "jsr:@test/pkg@2" )). unwrap (); 733let previous =if source. contains ( "jsr:" ) { 734"jsr:@test/pkg@1" 735} else { 736"../local" 737}; 738assert_eq! ( 739 change, 740Change :: Updated { previous : Some ( previous. into ()) }, 741"{source}" 742); 743assert_eq! ( 744 edited, 745 source. replace ( & format! ( "'{previous}'" ), "'jsr:@test/pkg@2'" ) 746); 747let ( unchanged, change) =add ( & edited, & dependency_of ( name, "jsr:@test/pkg@2" )). unwrap (); 748assert_eq! (( unchanged, change), ( edited, Change :: Unchanged )); 749} 750for ( source, addition, message) in [ 751( 752"" , 753dependency_of ( None , "jsr:@test/pkg@2/sub" ), 754"whole packages" , 755), 756( 757"" , 758dependency_of ( None , "jsr:@test/pkg@2/" ), 759"whole packages" , 760), 761( 762"" , 763dependency_of ( None , "jsr:@Test/pkg@2" ), 764"Invalid JSR package name" , 765), 766( 767"" , 768dependency_of ( Some ( "a/b" ), "jsr:@test/pkg@2" ), 769"binding name" , 770), 771( 772"imports: {'@test/pkg/x': './x.ts'}" , 773dependency_of ( None , "jsr:@test/pkg@2" ), 774"conflicts" , 775), 776] { 777let error =add ( source, & addition). unwrap_err (); 778assert! ( error. contains ( message), "{source}: {error}" ); 779} 780} 781 782# [ test ] 783fn renaming_a_package_keeps_the_binding_it_replaces () { 784let source ="deps: ['jsr:@test/old@1', '../local']" ; 785let addition =Addition :: Dependency { 786name : Some ( "@test/old" ), 787package : "jsr:@test/new@2" , 788}; 789let ( edited, change) =add ( source, & addition). unwrap (); 790assert_eq! ( 791 change, 792Change :: Updated { 793previous : Some ( "jsr:@test/old@1" . into ()) 794} 795); 796assert_eq! ( dependency ( & edited, "@test/old" ), "jsr:@test/new@2" ); 797assert! ( edited. starts_with ( "deps: [" )); 798assert! ( edited. ends_with ( ", '../local']" )); 799} 800 801# [ test ] 802fn npm_targets_gain_caret_ranges_keep_subpaths_and_reject_versions_in_deps () { 803use avril_runtime:: config_edit::{ selects_whole_package, whole_package}; 804for ( target, pinned, whole) in [ 805( "npm:left-pad" , "npm:left-pad@^1.2.3" , true ), 806( "npm:left-pad/sub" , "npm:left-pad@^1.2.3/sub" , false ), 807( "npm:@scope/pkg" , "npm:@scope/pkg@^1.2.3" , true ), 808] { 809let name =unversioned ( target). unwrap (). unwrap (); 810assert_eq! ( with_caret ( target, & name, "1.2.3" ), pinned); 811assert_eq! ( selects_whole_package ( pinned). unwrap (), whole); 812} 813for ( target, whole) in [ 814( "npm:left-pad@1" , true ), 815( "npm:left-pad@*/sub" , false ), 816( "npm:left-pad@latest" , true ), 817] { 818assert_eq! ( unversioned ( target). unwrap (), None , "{target}" ); 819assert_eq! ( selects_whole_package ( target). unwrap (), whole, "{target}" ); 820} 821assert! ( !selects_whole_package ( "npm:left-pad/sub" ). unwrap ()); 822assert! ( !selects_whole_package ( "npm:left-pad/" ). unwrap ()); 823assert_eq! ( 824whole_package ( "npm:left-pad@^1/sub" ). unwrap (), 825"npm:left-pad@^1" 826); 827assert! ( whole_package ( "npm:left-pad/sub" ). is_ok ()); 828} 829 830# [ test ] 831fn add_warns_and_skips_node_builtin_imports_for_jsr () { 832let server =Registry :: new (); 833 server. package ( 834"@test/env" , 835"1.0.0" , 836& [ 837( 838"mod.ts" , 839"import 'node:fs';\nimport {helper} from './helper.ts';\nexport const value = helper;" , 840), 841( "helper.ts" , "export const helper = 'helped';" ), 842], 843& [( "." , "./mod.ts" )], 844); 845 server. package ( 846"@test/typed" , 847"1.0.0" , 848& [( 849"mod.ts" , 850"import type {Stats} from 'node:fs';\nexport const value = 1;" , 851)], 852& [( "." , "./mod.ts" )], 853); 854let dir = tempfile:: tempdir (). unwrap (); 855let output =avril_add ( & server, dir. path (), dir. path (), & [ "jsr:@test/env@1" ]); 856assert! ( 857 output. status . success (), 858"{}" , 859String :: from_utf8_lossy ( & output. stderr ) 860); 861let stderr =String :: from_utf8_lossy ( & output. stderr ). into_owned (); 862assert! ( 863 stderr. contains ( "Warning" ) && stderr. contains ( "node:fs" ), 864"{stderr:?}" 865); 866assert! ( 867 server 868. requests 869. lock () 870. unwrap () 871. iter () 872. any ( |path| path =="/@test/env/1.0.0/helper.ts" ), 873"traversal continues past the builtin edge" 874); 875let output =avril_add ( & server, dir. path (), dir. path (), & [ "jsr:@test/typed@1" ]); 876assert! ( 877 output. status . success (), 878"{}" , 879String :: from_utf8_lossy ( & output. stderr ) 880); 881let stderr =String :: from_utf8_lossy ( & output. stderr ). into_owned (); 882assert! ( 883 !stderr. contains ( "node:" ), 884"erased type-only imports warn nothing: {stderr:?}" 885); 886}