char/avril
(wip) why'd you have to go and make things so complicaaaaated?
git clone https://git.t4t.associates/char/avril
7f4058a
main
1use avril_runtime:: package_config::{ PackageConfig , Source , TaskCommand }; 2use avril_runtime:: registry::{ npm:: parse_npm_request, parse_jsr_request}; 3use url:: Url ; 4 5fn base () ->Url { 6Url :: from_file_path ( std:: env:: temp_dir (). join ( "avril-schema/avril.conf" )). unwrap () 7} 8 9# [ test ] 10fn metadata_exports_and_maps_are_validated_without_filesystem_discovery () { 11let config =PackageConfig :: parse ( 12r#" 13name: '@app/demo', version: '1.2.3-beta.1', description: 'A local package', 14exports: {'.': './mod.ts', './vector': './src/vector.ts'}, 15imports: { 16assert: 'jsr:@std/assert@^1', 'assert/': 'jsr:@std/assert@^1/', blocked: null, 17'../shared/util.ts': './util.ts', 18}, 19deps: [ 20'../math', {from: '../format', name: '@app/format'}, 21'jsr:@std/path@^1', {from: 'jsr:/@std/path@^0.2', name: 'path-old'}, 22'npm:left-pad@1', {from: 'npm:@scope/widget@^2', name: 'widget'}, 23], 24scopes: {'./tests/': {assert: './test-assert.ts'}, './tests/unit/': {assert: null}}, 25"# , 26& base (), 27) 28. unwrap (); 29assert_eq! ( config. name . as_deref (), Some ( "@app/demo" )); 30assert_eq! ( config. version . as_deref (), Some ( "1.2.3-beta.1" )); 31assert_eq! ( config. description . as_deref (), Some ( "A local package" )); 32assert! ( 33 config. exports [ "./vector" ] 34. target 35. path () 36. ends_with ( "src/vector.ts" ) 37); 38let dependencies: Vec < _ > = config 39. dependencies 40. iter () 41. map ( |dependency|( dependency. name . as_deref (), & dependency. source )) 42. collect (); 43assert_eq! ( 44 dependencies, 45[ 46( 47None , 48& Source :: Local ( base (). join ( "../math" ). unwrap (). to_file_path (). unwrap ()) 49), 50( 51Some ( "@app/format" ), 52& Source :: Local ( base (). join ( "../format" ). unwrap (). to_file_path (). unwrap ()) 53), 54( 55Some ( "@std/path" ), 56& Source :: Jsr ( parse_jsr_request ( "jsr:@std/path@^1" ). unwrap ()), 57), 58( 59Some ( "path-old" ), 60& Source :: Jsr ( parse_jsr_request ( "jsr:/@std/path@^0.2" ). unwrap ()), 61), 62( 63Some ( "left-pad" ), 64& Source :: Npm ( parse_npm_request ( "npm:left-pad@1" ). unwrap ()), 65), 66( 67Some ( "widget" ), 68& Source :: Npm ( parse_npm_request ( "npm:@scope/widget@^2" ). unwrap ()), 69), 70] 71); 72let referrer =base (). join ( "./main.ts" ). unwrap (); 73for ( specifier, target) in [ 74( "@std/path" , "jsr:@std/path@^1" ), 75( "@std/path/posix" , "jsr:@std/path@^1/posix" ), 76( "path-old/posix/join" , "jsr:/@std/path@^0.2/posix/join" ), 77( "left-pad" , "npm:left-pad@1" ), 78( "left-pad/sub" , "npm:left-pad@1/sub" ), 79( "widget/sub" , "npm:@scope/widget@^2/sub" ), 80] { 81assert_eq! ( 82 config 83. registry_map 84. resolve ( specifier, & referrer) 85. unwrap () 86. as_deref (), 87Some ( target) 88); 89} 90for specifierin [ "@std/path-extra" , "path-old/../x" , "@std/path/a%2fb" ] { 91assert! ( 92 !matches! ( 93 config. registry_map . resolve ( specifier, & referrer), 94Ok ( Some ( _)) 95), 96"{specifier}" 97); 98} 99let referrer =base (). join ( "./main.ts" ). unwrap (); 100assert_eq! ( 101 config 102. import_map 103. resolve ( "assert" , & referrer) 104. unwrap () 105. as_deref (), 106Some ( "jsr:@std/assert@^1" ) 107); 108assert! ( 109 config 110. import_map 111. resolve ( "blocked" , & referrer) 112. unwrap_err () 113. to_string () 114. contains ( "blocked" ) 115); 116assert_eq! ( 117 config 118. import_map 119. resolve ( "assert/equals" , & referrer) 120. unwrap () 121. as_deref (), 122Some ( "jsr:@std/assert@^1/equals" ) 123); 124assert_eq! ( 125 config 126. import_map 127. resolve ( "../shared/util.ts" , & referrer) 128. unwrap (), 129Some ( base (). join ( "./util.ts" ). unwrap (). to_string ()) 130); 131let referrer =base (). join ( "./tests/main.ts" ). unwrap (); 132assert_eq! ( 133 config. import_map . resolve ( "assert" , & referrer). unwrap (), 134Some ( base (). join ( "./test-assert.ts" ). unwrap (). to_string ()) 135); 136let referrer =base (). join ( "./tests/unit/main.ts" ). unwrap (); 137assert! ( config. import_map . resolve ( "assert" , & referrer). is_err ()); 138assert_eq! ( 139 config 140. import_map 141. resolve ( "assert/equals" , & referrer) 142. unwrap () 143. as_deref (), 144Some ( "jsr:@std/assert@^1/equals" ) 145); 146assert_eq! ( 147PackageConfig :: parse ( "exports: './mod.ts'" , & base ()) 148. unwrap () 149. exports 150. len (), 1511 152); 153assert! ( 154PackageConfig :: parse ( "// empty" , & base ()) 155. unwrap () 156. exports 157. is_empty () 158); 159} 160 161# [ test ] 162fn unknown_options_are_ignored_at_every_config_level () { 163for extrain [ 164"null" , 165"true" , 166"42" , 167"'custom'" , 168"[null, true, 42, {nested: 'value'}]" , 169"{name: false, tasks: [], deps: {nested: 'value'}}" , 170] { 171let config =PackageConfig :: parse ( 172& format! ( 173"custom: {extra}, importMap: {{}}, 174name: 'app', imports: {{helper: './helper.ts'}}, 175deps: [{{from: '../math', name: 'math', custom: {extra}, version: '1'}}], 176tasks: {{build: {{command: 'echo built', custom: {extra}, cwd: '.'}}}}" 177), 178& base (), 179) 180. unwrap (); 181assert_eq! ( config. name . as_deref (), Some ( "app" )); 182assert_eq! ( config. dependencies . len (), 1 ); 183assert_eq! ( config. dependencies [ 0 ]. name . as_deref (), Some ( "math" )); 184assert_eq! ( 185 config. dependencies [ 0 ]. source , 186Source :: Local ( base (). join ( "../math" ). unwrap (). to_file_path (). unwrap ()) 187); 188assert_eq! ( 189 config. tasks [ "build" ]. command , 190Some ( TaskCommand :: Shell ( "echo built" . into ())) 191); 192assert_eq! ( 193 config. import_map . resolve ( "helper" , & base ()). unwrap (), 194Some ( base (). join ( "./helper.ts" ). unwrap (). to_string ()) 195); 196} 197} 198 199# [ test ] 200fn malformed_headers_and_paths_have_source_spans () { 201for ( source, message) in [ 202( "name: null" , "Expected a string" ), 203( "name: '@scope'" , "bare package name" ), 204( "name: 'a/b'" , "bare package name" ), 205( "name: 'jsr:@scope/pkg'" , "bare package name" ), 206( "version: 'next'" , "Invalid package version" ), 207( "description: []" , "Expected a string" ), 208( "exports: false" , "Expected an object" ), 209( "exports: {'./*': './mod.ts'}" , "exact" ), 210( "exports: {'./a/../b': './mod.ts'}" , "exact" ), 211( "exports: './'" , "file inside" ), 212( "exports: './sub/..'" , "file inside" ), 213( "exports: '../private.ts'" , "package-relative" ), 214( "exports: './sub/../../private.ts'" , "file inside" ), 215( "exports: './%2e%2e/private.ts'" , "file inside" ), 216( 217"exports: './x%2f..%2f..%2fprivate.ts'" , 218"Invalid package path" , 219), 220( "exports: './x%5c..%5cprivate.ts'" , "Invalid package path" ), 221( "exports: './mod.ts?raw'" , "queries or fragments" ), 222( "exports: './mod.ts#fragment'" , "queries or fragments" ), 223( "exports: './%00.ts'" , "without NUL" ), 224( "exports: './%FF.ts'" , "UTF-8" ), 225( "imports: []" , "Expected an object" ), 226( "scopes: {'./tests/': []}" , "Expected an object" ), 227( "deps: {math: '../math'}" , "Expected an array" ), 228( "deps: [false]" , "Expected a dependency path" ), 229( "deps: [['../math']]" , "Expected a dependency path" ), 230( "deps: ['/math']" , "config-relative" ), 231( "deps: ['math']" , "config-relative" ), 232( "deps: ['./%2e%2e%5cmath']" , "Invalid package path" ), 233( "deps: [{from: '../math'}]" , "require both" ), 234( "deps: [{name: 'math'}]" , "require both" ), 235( 236"deps: [{from: '../math', name: false}]" , 237"Expected a string" , 238), 239( "deps: [{from: false, name: 'math'}]" , "Expected a string" ), 240( 241"deps: [{from: '../math', name: 'math/sub'}]" , 242"binding name" , 243), 244( "deps: [{from: '/math', name: 'math'}]" , "config-relative" ), 245( "deps: ['jsr:@std/path@^1/posix']" , "whole packages" ), 246( "deps: ['jsr:@std/path@^1/']" , "whole packages" ), 247( "deps: ['jsr:@std/path/posix']" , "whole packages" ), 248( "deps: ['jsr:@std/path@latest']" , "version tags" ), 249( "deps: ['jsr:path@1']" , "must be scoped" ), 250( "deps: ['jsr:@Test/path@1']" , "Invalid JSR package name" ), 251( 252"deps: [{from: 'jsr:@test/p.kg@1', name: 'pkg'}]" , 253"Invalid JSR package name" , 254), 255( "deps: ['jsr:']" , "jsr:" ), 256( "deps: ['npm:left-pad/sub']" , "whole packages" ), 257( "deps: [{path: '../math', name: 'math'}]" , "require both" ), 258( 259"deps: [{from: './%2e%2e%2fmath', name: 'math'}]" , 260"Invalid package path" , 261), 262] { 263let error =PackageConfig :: parse ( source, & base ()). unwrap_err (); 264assert! ( error. message . contains ( message), "{source}: {error}" ); 265assert! ( !source[ error. span . clone ()]. is_empty (), "{source}: {error}" ); 266assert! ( error. render ( "avril.conf" , source). contains ( "avril.conf:1:" )); 267} 268} 269 270# [ test ] 271fn collisions_are_about_bindings_and_include_scoped_or_blocked_aliases () { 272for sourcein [ 273"name: 'math', deps: [{from: '../math', name: 'math'}]" , 274"deps: [{from: '../math', name: 'math'}, {from: '../other', name: 'math'}]" , 275"name: 'math', imports: {math: './other.ts'}" , 276"name: 'math', imports: {'math/private': null}" , 277"deps: [{from: '../math', name: 'math'}], imports: {'math/': './other/'}" , 278"deps: [{from: '../math', name: '@acme/math'}], imports: {'@acme/': './other/'}" , 279"deps: [{from: '../math', name: 'math'}], scopes: {'./tests/': {math: './other.ts'}}" , 280"deps: ['jsr:@acme/math@1', {from: 'jsr:@acme/math@2', name: '@acme/math'}]" , 281"name: '@acme/math', deps: ['jsr:@acme/math@1']" , 282"deps: ['jsr:@acme/math@1'], imports: {'@acme/math/vector': './vector.ts'}" , 283"deps: ['jsr:@acme/math@1'], scopes: {'./tests/': {'@acme/': './other/'}}" , 284] { 285let error =PackageConfig :: parse ( source, & base ()). unwrap_err (); 286assert! ( 287[ "conflicts" , "Duplicate" ] 288. iter () 289. any ( |message| error. message . contains ( message)), 290"{source}: {error}" 291); 292assert! ( 293 source[ error. span . clone ()]. contains ( "math" ), 294"{source}: {error}" 295); 296} 297PackageConfig :: parse ( "name: '@acme/math', deps: [{from: '../math', name: 'math'}, {from: '../other', name: 'math-next'}, {from: '../math', name: 'math-copy'}], imports: {'@acme/math-next': './other.ts'}" , & base ()). unwrap (); 298} 299 300# [ test ] 301fn binding_diagnostics_point_at_the_binding_and_its_first_definition () { 302for ( source, at, previous) in [ 303( 304"deps: [{from: '../a', name: 'math'}, {from: '../b', name: 'm\\u0061th'}]" , 305"'m\\u0061th'" , 306"'math'" , 307), 308( 309"name: 'math', deps: [{from: '../math', name: \"math\"}]" , 310"\"math\"" , 311"'math'" , 312), 313] { 314let error =PackageConfig :: parse ( source, & base ()). unwrap_err (); 315assert_eq! ( & source[ error. span . clone ()], at, "{source}: {error}" ); 316assert_eq! ( 317& source[ error. previous . clone (). unwrap ()], 318 previous, 319"{source}: {error}" 320); 321} 322} 323 324# [ test ] 325fn path_only_dependencies_bind_declared_names_with_the_same_validation () { 326let names: & [( & str , Option < & str >)] =& [ 327( "math" , Some ( "@acme/math" )), 328( "math-v2" , Some ( "@acme/math" )), 329( "anonymous" , None ), 330]; 331let declared = |source : & Source |{ 332let Source :: Local ( directory) = sourceelse { 333panic! ( "{source:?}" ) 334}; 335 names 336. iter () 337. find ( |( path, _) | directory. ends_with ( path)) 338. map ( |( _, name) | name. map ( str:: to_owned)) 339. ok_or_else ( ||"unreadable" . to_owned ()) 340}; 341let source ="deps: ['../math', {from: '../math-v2', name: 'math-next'}, {from: '../math', name: 'math'}]" ; 342let config =PackageConfig :: parse ( source, & base ()). unwrap (); 343let bindings = futures:: executor:: block_on ( 344 config. bindings ( |source| std:: future:: ready ( declared ( & source))), 345) 346. unwrap (); 347let bindings: Vec < _ > = bindings 348. iter () 349. map ( |( name, source) |{ 350let Source :: Local ( path) = sourceelse { 351panic! ( "{source:?}" ) 352}; 353( name. as_str (), path. file_name (). unwrap (). to_str (). unwrap ()) 354}) 355. collect (); 356assert_eq! ( 357 bindings, 358[ 359( "@acme/math" , "math" ), 360( "math" , "math" ), 361( "math-next" , "math-v2" ) 362] 363); 364for ( source, at, message, declarer) in [ 365( 366"deps: ['../anonymous']" , 367"'../anonymous'" , 368"declares no name" , 369"anonymous" , 370), 371( 372"deps: ['../unreadable']" , 373"'../unreadable'" , 374"unreadable" , 375"unreadable" , 376), 377( 378"deps: ['../math', '../math-v2']" , 379"'../math-v2'" , 380"Duplicate" , 381"math-v2" , 382), 383( 384"deps: [{from: '../other', name: '@acme/math'}, '../math']" , 385"'../math'" , 386"Duplicate" , 387"math" , 388), 389( 390"deps: ['../math', {from: '../other', name: '@acme/math'}]" , 391"'@acme/math'" , 392"Duplicate" , 393"math" , 394), 395( 396"deps: ['jsr:@acme/math@1', '../math']" , 397"'../math'" , 398"Duplicate" , 399"math" , 400), 401( 402"name: '@acme/math', deps: ['../math']" , 403"'../math'" , 404"self name" , 405"math" , 406), 407( 408"imports: {'@acme/': './vendor/'}, deps: ['../math']" , 409"'../math'" , 410"import key" , 411"math" , 412), 413( 414"scopes: {'./tests/': {'@acme/math/vector': null}}, deps: ['../math']" , 415"'../math'" , 416"import key" , 417"math" , 418), 419] { 420let config =PackageConfig :: parse ( source, & base ()). unwrap (); 421let error = futures:: executor:: block_on ( 422 config. bindings ( |source| std:: future:: ready ( declared ( & source))), 423) 424. unwrap_err (); 425assert_eq! ( & source[ error. span . clone ()], at, "{source}: {error}" ); 426assert! ( error. message . contains ( message), "{source}: {error}" ); 427let declarer =base (). join ( & format! ( "../{declarer}/avril.conf" )). unwrap (); 428assert! ( 429 error 430. message 431. contains ( declarer. to_file_path (). unwrap (). to_str (). unwrap ()), 432"{source}: {error}" 433); 434} 435} 436 437# [ test ] 438fn tasks_accept_string_and_object_forms () { 439let config =PackageConfig :: parse ( 440r#" 441tasks: { 442build: 'avril run build.ts', 443'build:client': {command: 'avril run client.ts', description: 'Build the client'}, 444serve: {command: 'avril run server.ts', dependencies: ['build', 'build:client'], envFile: '.env.serve'}, 445all: {dependencies: ['serve']}, 446}, 447"# , 448& base (), 449) 450. unwrap (); 451assert_eq! ( config. tasks . len (), 4 ); 452assert_eq! ( 453 config. tasks [ "build" ]. command , 454Some ( TaskCommand :: Shell ( "avril run build.ts" . into ())) 455); 456assert_eq! ( 457 config. tasks [ "build:client" ]. description . as_deref (), 458Some ( "Build the client" ) 459); 460let dependencies: Vec < _ > = config. tasks [ "serve" ] 461. dependencies 462. iter () 463. map ( |dependency| dependency. name . as_str ()) 464. collect (); 465assert_eq! ( dependencies, [ "build" , "build:client" ]); 466assert_eq! ( 467 config. tasks [ "serve" ]. env_file . as_ref (). unwrap (). path , 468".env.serve" 469); 470assert_eq! ( config. tasks [ "all" ]. command , None ); 471} 472 473# [ test ] 474fn parallel_tasks_run_their_dependencies_at_once () { 475let config =PackageConfig :: parse ( 476"tasks: {a: 'echo a', b: 'echo b', all: {dependencies: ['a', 'b'], parallel: true}, seq: {dependencies: ['a']}}" , 477& base (), 478) 479. unwrap (); 480assert! ( config. tasks [ "all" ]. parallel ); 481assert! ( !config. tasks [ "seq" ]. parallel ); 482assert! ( !config. tasks [ "a" ]. parallel ); 483// parallel: false needs no dependencies; it is the default. 484PackageConfig :: parse ( "tasks: {a: {command: 'echo a', parallel: false}}" , & base ()). unwrap (); 485for ( source, message) in [ 486( 487"tasks: {all: {dependencies: ['a'], parallel: 'yes'}}" , 488"Expected a boolean" , 489), 490( 491"tasks: {all: {command: 'echo a', parallel: true}}" , 492"requires 'dependencies'" , 493), 494( 495"tasks: {all: {dependencies: [], parallel: true}}" , 496"requires 'dependencies'" , 497), 498( 499"tasks: {a: {command: 'echo a', envFile: 3}}" , 500"Expected a string" , 501), 502( 503"tasks: {a: {command: 'echo a', envFile: ''}}" , 504"Task env file paths cannot be empty" , 505), 506( "tasks: {a: true}" , "'parallel'" ), 507] { 508let error =PackageConfig :: parse ( source, & base ()). unwrap_err (); 509assert! ( error. message . contains ( message), "{source}: {error}" ); 510assert! ( !source[ error. span . clone ()]. is_empty (), "{source}: {error}" ); 511} 512} 513 514# [ test ] 515fn cached_tasks_declare_inputs_outputs_and_env_names () { 516let config =PackageConfig :: parse ( 517r#" 518tasks: { 519site: { 520command: 'avril run build-site.ts', 521files: ['src/**/*.ts', 'templates/', 'avril.conf'], 522output: ['dist/'], 523env: ['SITE_ENV', '_PRIVATE2'], 524}, 525}, 526"# , 527& base (), 528) 529. unwrap (); 530let site =& config. tasks [ "site" ]; 531let files: Vec < _ > = site 532. files 533. as_ref () 534. unwrap () 535. iter () 536. map ( |pattern| pattern. pattern . as_str ()) 537. collect (); 538assert_eq! ( files, [ "src/**/*.ts" , "templates/" , "avril.conf" ]); 539assert_eq! ( site. output [ 0 ]. pattern , "dist/" ); 540assert_eq! ( site. env , [ "SITE_ENV" , "_PRIVATE2" ]); 541// Uncached tasks carry no caching fields. 542let config =PackageConfig :: parse ( "tasks: {a: 'echo hi'}" , & base ()). unwrap (); 543assert_eq! ( config. tasks [ "a" ]. files , None ); 544 545for ( source, message) in [ 546( 547"tasks: {a: {command: 'x', output: ['dist/']}}" , 548"require 'files'" , 549), 550( 551"tasks: {a: {command: 'x', env: ['HOME']}}" , 552"require 'files'" , 553), 554( 555"tasks: {a: {command: 'x', files: 'src/**'}}" , 556"array of glob patterns" , 557), 558( 559"tasks: {a: {command: 'x', files: [3]}}" , 560"Expected a string" , 561), 562( "tasks: {a: {command: 'x', files: ['']}}" , "cannot be empty" ), 563( 564"tasks: {a: {command: 'x', files: ['src\\\\**']}}" , 565"separator" , 566), 567( 568"tasks: {a: {command: 'x', files: ['src/[abc']}}" , 569"invalid range pattern" , 570), 571( 572"tasks: {a: {command: 'x', files: [], output: ['../dist']}}" , 573"inside the package" , 574), 575( 576"tasks: {a: {command: 'x', files: [], output: ['/dist']}}" , 577"inside the package" , 578), 579( 580"tasks: {a: {command: 'x', files: [], output: ['.']}}" , 581"inside the package" , 582), 583( 584"tasks: {a: {command: 'x', files: [], env: 'HOME'}}" , 585"array of environment variable names" , 586), 587( 588"tasks: {a: {command: 'x', files: [], env: ['1BAD']}}" , 589"environment variable name" , 590), 591( 592"tasks: {a: {command: 'x', files: [], env: ['A-B']}}" , 593"environment variable name" , 594), 595( 596"tasks: {a: {command: 'x', files: [], env: ['A', 'A']}}" , 597"Duplicate task env name" , 598), 599] { 600let error =PackageConfig :: parse ( source, & base ()). unwrap_err (); 601assert! ( error. message . contains ( message), "{source}: {error}" ); 602assert! ( !source[ error. span . clone ()]. is_empty (), "{source}: {error}" ); 603} 604} 605 606# [ test ] 607fn malformed_tasks_have_source_spans () { 608for ( source, message) in [ 609( "tasks: []" , "Expected an object" ), 610( "tasks: {'my task': 'echo hi'}" , "Task names" ), 611( "tasks: {'': 'echo hi'}" , "Task names" ), 612( "tasks: {build: ''}" , "cannot be empty" ), 613( "tasks: {build: ' '}" , "cannot be empty" ), 614( "tasks: {build: {}}" , "must define" ), 615( "tasks: {build: {command: 3}}" , "Expected a string" ), 616( "tasks: {build: {description: 'x'}}" , "must define" ), 617( 618"tasks: {build: {command: 3, cwd: '.'}}" , 619"Expected a string" , 620), 621( 622"tasks: {build: {command: 'x', dependencies: 'build'}}" , 623"array of task names" , 624), 625( 626"tasks: {build: {command: 'x', dependencies: [3]}}" , 627"Expected a string" , 628), 629( 630"tasks: {build: {command: 'x', dependencies: ['a b']}}" , 631"Expected a task name" , 632), 633( 634"tasks: {build: {command: 'x', dependencies: ['a', 'a']}}" , 635"Duplicate task dependency" , 636), 637( "tasks: {build: true}" , "Expected a task command" ), 638] { 639let error =PackageConfig :: parse ( source, & base ()). unwrap_err (); 640assert! ( error. message . contains ( message), "{source}: {error}" ); 641assert! ( !source[ error. span . clone ()]. is_empty (), "{source}: {error}" ); 642} 643// Unknown and cyclic references are for the runner to judge: configs are 644// parsed for many commands that never run tasks. 645PackageConfig :: parse ( 646"tasks: {a: {command: 'x', dependencies: ['b', 'missing']}, b: {command: 'y', dependencies: ['a']}}" , 647& base (), 648) 649. unwrap (); 650} 651 652# [ test ] 653fn http_packages_use_url_paths_and_confined_exports () { 654let base =Url :: parse ( "https://example.com/pkg/avril.conf" ). unwrap (); 655let config =PackageConfig :: parse ( "name: 'pkg', exports: './src/mod.ts', deps: ['../dep/', {from: 'http://other.example/lib/', name: 'other'}, 'jsr:@std/assert@1']" , & base). unwrap (); 656assert_eq! ( 657 config. exports [ "." ]. target . as_str (), 658"https://example.com/pkg/src/mod.ts" 659); 660assert_eq! ( 661 config. dependencies [ 0 ]. source , 662Source :: Http ( Url :: parse ( "https://example.com/dep/" ). unwrap ()) 663); 664assert_eq! ( config. dependencies [ 1 ]. name . as_deref (), Some ( "other" )); 665let config =PackageConfig :: parse ( "deps: ['https://example.com/pkg/']" , & self :: base ()). unwrap (); 666assert_eq! ( 667 config. dependencies [ 0 ]. source , 668Source :: Http ( base. join ( "./" ). unwrap ()) 669); 670 671for ( source, message) in [ 672( "exports: './%2e%2e/private.ts'" , "inside its package" ), 673( "exports: './sub/.'" , "inside its package" ), 674( "exports: './x%2fprivate.ts'" , "Invalid package path" ), 675( "exports: './mod.ts?raw'" , "queries or fragments" ), 676( "exports: './%00.ts'" , "without NUL" ), 677( "deps: ['../dep']" , "ending with '/'" ), 678( "deps: ['https://user@example.com/pkg/']" , "credential-free" ), 679( 680"deps: ['https://example.com/pkg/?v=1']" , 681"queries or fragments" , 682), 683( 684"deps: ['https://example.com/pkg/#v1']" , 685"queries or fragments" , 686), 687( 688"deps: ['https://example.com/pkg/%2f/']" , 689"Invalid package path" , 690), 691( "deps: ['file:///tmp/pkg/']" , "Dependencies must" ), 692] { 693let error =PackageConfig :: parse ( source, & base). unwrap_err (); 694assert! ( error. message . contains ( message), "{source}: {error}" ); 695} 696} 697 698# [ test ] 699fn compiler_options_keep_all_keys_and_validate_jsx_ones () { 700let config =PackageConfig :: parse ( 701"compilerOptions: { jsx: 'react-jsxdev', jsxImportSource: 'ui', jsxFactory: 'h.x', target: 'es2020', paths: {} }" , 702& base (), 703) 704. unwrap (); 705assert_eq! ( 706 serde_json:: Value :: Object ( config. compiler_options ), 707 serde_json:: json!({ "jsx" : "react-jsxdev" , "jsxImportSource" : "ui" , "jsxFactory" : "h.x" , "target" : "es2020" , "paths" : {}}) 708); 709for ( source, message) in [ 710( 711"compilerOptions: { jsx: 'preact' }" , 712"Unsupported compilerOptions.jsx" , 713), 714( "compilerOptions: { jsx: '' }" , "nonempty string" ), 715( "compilerOptions: { jsxFactory: 42 }" , "nonempty string" ), 716( 717"compilerOptions: { jsxFactory: 'not a factory' }" , 718"dotted member expression" , 719), 720( "compilerOptions: []" , "Expected an object" ), 721] { 722let error =PackageConfig :: parse ( source, & base ()). unwrap_err (); 723assert! ( error. message . contains ( message), "{source}: {error}" ); 724} 725}