char/avril
(wip) why'd you have to go and make things so complicaaaaated?
git clone https://git.t4t.associates/char/avril
c7a9569
main
1use super :: * ; 2use avril_runtime:: caps::{ Category , Initial }; 3use std:: fs; 4 5# [ path = "../../tests/common/http.rs" ] 6mod http; 7# [ path = "../../tests/common/registry.rs" ] 8mod registry; 9 10fn options ( root : & Path ) ->Options { 11Options { 12jsr_registry : url:: Url :: parse ( "https://example.invalid/" ). unwrap (), 13npm_registry : url:: Url :: parse ( "https://example.invalid/" ). unwrap (), 14cache : root. join ( "cache" ), 15lock : root. join ( "avril.lock" ), 16project_root : root. to_owned (), 17offline : false , 18frozen : false , 19update : false , 20ephemeral_lock : false , 21vendor : None , 22} 23} 24 25fn refresh ( root : & Path , options : Options , entries : & [ & str ]) ->ProjectInput { 26refresh_with_caps ( 27 root, 28 options, 29 entries, 30Capabilities :: new ( Initial :: new ( false ), root. to_owned ()), 31) 32} 33 34fn refresh_with_caps ( 35root : & Path , 36options : Options , 37entries : & [ & str ], 38caps : Arc < Capabilities >, 39) ->ProjectInput { 40let mut project =LiveProject { 41root : root. to_owned (), 42 caps, 43 options, 44explicit_lock : false , 45automatic_types : true , 46dirty : Arc :: new ( AtomicBool :: new ( false )), 47rewatch : Arc :: new ( AtomicBool :: new ( false )), 48watcher : Some ( notify:: recommended_watcher ( |_|{}). unwrap ()), 49watched : BTreeMap :: new (), 50tracked_config : Arc :: new ( Mutex :: new ( BTreeSet :: new ())), 51}; 52let documents = entries 53. iter () 54. map ( |entry|{ 55let path = root. join ( entry); 56( uri ( & path), fs:: read_to_string ( path). unwrap ()) 57}) 58. collect (); 59 registry:: runtime_handle () 60. block_on ( project. refresh ( documents)) 61. unwrap () 62} 63 64fn uri ( path : & Path ) ->String { 65 avril_lsp:: canonical_uri ( url:: Url :: from_file_path ( path). unwrap (). as_str ()). unwrap () 66} 67 68fn resolve ( input : & ProjectInput , referrer : & str , specifier : & str ) ->Option < String > { 69 input 70. imports 71. get ( & ( referrer. into (), specifier. into ())) 72. and_then ( |targets| targets. import . clone ()) 73} 74 75fn files ( root : & Path , files : & [( & str , & str )]) { 76for ( name, source) in files{ 77let path = root. join ( name); 78 fs:: create_dir_all ( path. parent (). unwrap ()). unwrap (); 79 fs:: write ( path, source). unwrap (); 80} 81} 82 83# [ test ] 84fn workspace_roots_and_auto_imports_keep_resolution_authoritative () { 85let dir = tempfile:: tempdir (). unwrap (); 86let root = dir. path (); 87files ( 88 root, 89& [ 90( "main.ts" , "export const main = 0;" ), 91( "target.ts" , "export const target = 1;" ), 92( "other.ts" , "export const other = 2;" ), 93( "ignored.ts" , "throw Error('ignored');" ), 94( 95"node_modules/pkg/index.ts" , 96"throw Error('not a workspace root');" , 97), 98( ".gitignore" , "ignored.ts\n" ), 99( "avril.conf" , "imports: {'./target.ts': './other.ts'}" ), 100], 101); 102let input =refresh ( root, options ( root), & [ "main.ts" ]); 103assert! ( 104 input 105. sources 106. iter () 107. any ( |s| s. uri ==uri ( & root. join ( "other.ts" ))) 108); 109assert! ( 110 !input 111. sources 112. iter () 113. any ( |s| s. uri ==uri ( & root. join ( "ignored.ts" ))) 114); 115assert! ( !input. sources . iter (). any ( |s| s. uri . contains ( "node_modules" ))); 116let edges =& input. import_candidates ; 117assert! ( edges. contains ( & ( 118uri ( & root. join ( "main.ts" )), 119uri ( & root. join ( "other.ts" )), 120"./target.ts" . into () 121))); 122assert! ( !edges. contains ( & ( 123uri ( & root. join ( "main.ts" )), 124uri ( & root. join ( "target.ts" )), 125"./target.ts" . into () 126))); 127} 128 129# [ test ] 130fn restricted_projects_do_not_discover_unopened_roots () { 131let dir = tempfile:: tempdir (). unwrap (); 132files ( 133 dir. path (), 134& [ 135( "main.ts" , "export const main = 0;" ), 136( "unopened.ts" , "export const unopened = 1;" ), 137], 138); 139let caps =Capabilities :: new ( Initial :: new ( true ), dir. path (). to_owned ()); 140let input =refresh_with_caps ( dir. path (), options ( dir. path ()), & [], caps); 141assert! ( !input. sources . iter (). any ( |s| s. uri . starts_with ( "file:" ))); 142} 143 144# [ test ] 145fn declarations_preserve_package_private_versions_without_writing_project_files () { 146let server = registry:: Registry :: new (); 147 server. package ( "@test/types" , "1.0.0" , & [ 148( "types.d.ts" , "import type {Base} from './base.d.ts'; export interface Value extends Base { first: string; }" ), 149( "base.d.ts" , "export interface Base { id: number; }" ), 150], & [( "." , "./types.d.ts" )]); 151 server. package ( 152"@test/types" , 153"2.0.0" , 154& [( "types.d.ts" , "export interface Value { second: number; }" )], 155& [( "." , "./types.d.ts" )], 156); 157let dir = tempfile:: tempdir (). unwrap (); 158files ( 159 dir. path (), 160& [ 161( "app/avril.conf" , "imports: {dep: 'jsr:@test/types@1'}" ), 162( "lib/avril.conf" , "imports: {dep: 'jsr:@test/types@2'}" ), 163( 164"app/main.ts" , 165"import type {Value} from 'dep'; import {second} from '../lib/mod.ts'; export const first: Value = {id: 1, first: 'ok'}; throw Error('must not execute');" , 166), 167( 168"lib/mod.ts" , 169"import type {Value} from 'dep'; export const second: Value = {second: 2};" , 170), 171], 172); 173let app = dir. path (). join ( "app" ); 174let input =refresh ( & app, server. options ( & app), & [ "main.ts" ]); 175let first =resolve ( & input, & uri ( & app. join ( "main.ts" )), "dep" ). unwrap (); 176let second =resolve ( & input, & uri ( & dir. path (). join ( "lib/mod.ts" )), "dep" ). unwrap (); 177assert_eq! ( 178 first, 179 avril_lsp:: canonical_uri ( & format! ( "{}@test/types/1.0.0/types.d.ts" , server. url )). unwrap () 180); 181assert_eq! ( 182 second, 183 avril_lsp:: canonical_uri ( & format! ( "{}@test/types/2.0.0/types.d.ts" , server. url )). unwrap () 184); 185let base =resolve ( & input, & first, "./base.d.ts" ). unwrap (); 186assert! ( 187 input 188. sources 189. iter () 190. any ( |source| source. uri == base && source. read_only ) 191); 192assert! ( input. diagnostics . is_empty ()); 193for pathin [ "avril.lock" , "tsconfig.json" , ".avril" ] { 194assert! ( !app. join ( path). exists ()); 195} 196 server. requests . lock (). unwrap (). clear (); 197let mut options = server. options ( & app); 198 options. offline =true ; 199let offline =refresh ( & app, options, & [ "main.ts" ]); 200assert_eq! ( 201resolve ( & offline, & uri ( & app. join ( "main.ts" )), "dep" ), 202Some ( first) 203); 204assert! ( server. requests . lock (). unwrap (). is_empty ()); 205} 206 207# [ test ] 208fn bad_library_metadata_does_not_abort_or_inherit_consumer_aliases () { 209let dir = tempfile:: tempdir (). unwrap (); 210files ( 211 dir. path (), 212& [ 213( "app/avril.conf" , "imports: {helper: './helper.ts'}" ), 214( 215"app/main.ts" , 216"import type {Value} from 'helper'; import '../lib/mod.ts'; export const value: Value = 'app';" , 217), 218( "app/helper.ts" , "export type Value = string;" ), 219( "lib/avril.conf" , "imports: []" ), 220( 221"lib/mod.ts" , 222"import type {Value} from 'helper'; export type LibraryValue = Value;" , 223), 224], 225); 226let app = dir. path (). join ( "app" ); 227let input =refresh ( & app, options ( & app), & [ "main.ts" ]); 228let library =uri ( & dir. path (). join ( "lib/mod.ts" )); 229assert! ( 230 input. diagnostics [ & library] 231. iter () 232. any ( |error| error. contains ( "Expected an object" )) 233); 234assert_eq! ( resolve ( & input, & library, "helper" ), None ); 235assert_eq! ( 236resolve ( & input, & uri ( & app. join ( "main.ts" )), "helper" ), 237Some ( uri ( & app. join ( "helper.ts" ))), 238); 239} 240 241# [ test ] 242fn denied_type_override_is_not_hidden_by_an_ambient_module () { 243let dir = tempfile:: tempdir (). unwrap (); 244let root = dir. path (). join ( "project" ); 245let outside = dir. path (). join ( "outside.d.ts" ); 246files ( 247 dir. path (), 248& [ 249( "project/avril.conf" , "{}" ), 250( 251"project/main.ts" , 252"import './ambient.d.ts'; import type {Value} from 'alias'; export type Main = Value;" , 253), 254( 255"project/ambient.d.ts" , 256"declare module 'alias' { export interface Value {} }" , 257), 258( "outside.d.ts" , "export interface Value {}" ), 259], 260); 261let ambient =refresh ( & root, options ( & root), & [ "main.ts" ]); 262assert! ( ambient. diagnostics . is_empty (), "{:?}" , ambient. diagnostics ); 263files ( 264 dir. path (), 265& [( "project/avril.conf" , "types: {imports: {alias: null}}" )], 266); 267let blocked =refresh ( & root, options ( & root), & [ "main.ts" ]); 268assert! ( 269 blocked 270. diagnostics 271. contains_key ( & uri ( & root. join ( "main.ts" ))), 272"{:?}" , 273 blocked. diagnostics 274); 275files ( 276 dir. path (), 277& [( 278"project/avril.conf" , 279"types: {imports: {alias: '../outside.d.ts'}}" , 280)], 281); 282let mut initial =Initial :: new ( true ); 283 initial 284. grant ( Category :: JsExec , false , root. to_str (). unwrap (), & root) 285. unwrap (); 286let caps =Capabilities :: new ( initial, root. clone ()); 287let input =refresh_with_caps ( & root, options ( & root), & [ "main.ts" ], caps); 288let main =uri ( & root. join ( "main.ts" )); 289assert! ( 290 input. diagnostics [ & main] 291. iter () 292. any ( |error| error. contains ( "PermissionDenied" )), 293"{:?}" , 294 input. diagnostics 295); 296assert! ( 297 !input 298. sources 299. iter () 300. any ( |source| source. uri ==uri ( & outside)) 301); 302} 303 304# [ test ] 305fn type_queries_references_jsdoc_and_redirects_survive_missing_dependencies () { 306let server = registry:: Registry :: new (); 307 server. package ( 308"@test/js" , 309"1.0.0" , 310& [ 311( 312"mod.js" , 313"/* @ts-self-types=\"./mod.d.ts\" */ export const value = 'js';" , 314), 315( "mod.d.ts" , "export const value: 'declared';" ), 316], 317& [( "." , "./mod.js" )], 318); 319let dir = tempfile:: tempdir (). unwrap (); 320files ( 321 dir. path (), 322& [ 323( "avril.conf" , "{}" ), 324( "ref.d.ts" , "export interface Reference {}" ), 325( "query.ts" , "export const value = 1;" ), 326( "lazy.ts" , "export const value = 2;" ), 327( "jsdoc.d.ts" , "export interface Doc {}" ), 328( "param.d.ts" , "export interface Param {}" ), 329( "returns.d.ts" , "export interface Result {}" ), 330( "imported.d.ts" , "export interface Imported {}" ), 331( 332"main.ts" , 333"/// <reference path=\"./ref.d.ts\"/>\nimport {value} from 'jsr:@test/js@1'; import type {Missing} from 'package:missing'; import type {Absent} from './absent.d.ts'; type Query = typeof import('./query.ts'); const load = () => import('./lazy.ts'); /** @type {import('./jsdoc.d.ts').Doc} */ let doc; /** @param {import('./param.d.ts').Param} value\n * @returns {import('./returns.d.ts').Result}\n * The result type.\n * @example\n * import('example-only-library');\n * ```ts\n * import('fenced-example-library');\n * ```\n * @import { Imported } from './imported.d.ts'\n */ function example(value) {} throw Error('must not run');" , 334), 335], 336); 337let input =refresh ( dir. path (), server. options ( dir. path ()), & [ "main.ts" ]); 338let main =uri ( & dir. path (). join ( "main.ts" )); 339assert! ( 340resolve ( & input, & main, "jsr:@test/js@1" ) 341. unwrap () 342. ends_with ( "/mod.d.ts" ) 343); 344for namein [ 345"ref.d.ts" , 346"query.ts" , 347"lazy.ts" , 348"jsdoc.d.ts" , 349"param.d.ts" , 350"returns.d.ts" , 351"imported.d.ts" , 352] { 353let target =uri ( & dir. path (). join ( name)); 354assert_eq! ( 355resolve ( & input, & main, & format! ( "./{name}" )), 356Some ( target. clone ()) 357); 358assert! ( input. sources . iter (). any ( |source| source. uri == target)); 359} 360assert! ( 361 input. diagnostics [ & main] 362. iter () 363. any ( |error| error. contains ( "Unsupported" )) 364); 365assert! ( resolve ( & input, & main, "./absent.d.ts" ). is_none ()); 366assert! ( resolve ( & input, & main, "example-only-library" ). is_none ()); 367assert! ( resolve ( & input, & main, "fenced-example-library" ). is_none ()); 368assert! ( 369 input 370. diagnostics 371. values () 372. flatten () 373. any ( |error| error. contains ( "absent.d.ts" )) 374); 375} 376 377# [ test ] 378fn jsx_collects_implicit_runtime_types_and_preserves_user_config () { 379let server = registry:: Registry :: new (); 380 server. package ( 381"@test/ui" , 382"1.0.0" , 383& [ 384( 385"jsx-runtime.js" , 386"/** @ts-self-types=\"./jsx-runtime.d.ts\" */ export const jsx = () => {};" , 387), 388( 389"jsx-runtime.d.ts" , 390"export const jsx: (tag: string, props: object) => object;" , 391), 392], 393& [( "./jsx-runtime" , "./jsx-runtime.js" )], 394); 395let dir = tempfile:: tempdir (). unwrap (); 396let config =r#"{"compilerOptions":{"jsx":"react-jsx","jsxImportSource":"jsr:@test/ui@1"}}"# ; 397files ( 398 dir. path (), 399& [ 400( "avril.conf" , "{}" ), 401( "tsconfig.json" , config), 402( "types.d.ts" , "export interface Props {title: string}" ), 403( 404"main.tsx" , 405"import type {Props} from './types.d.ts'; const props: Props = {title:'hello'}; export const node = <p {...props} />;" , 406), 407], 408); 409let input =refresh ( dir. path (), server. options ( dir. path ()), & [ "main.tsx" ]); 410let main =uri ( & dir. path (). join ( "main.tsx" )); 411assert! ( 412resolve ( & input, & main, "jsr:@test/ui@1/jsx-runtime" ) 413. unwrap () 414. ends_with ( "jsx-runtime.d.ts" ) 415); 416assert_eq! ( 417resolve ( & input, & main, "./types.d.ts" ), 418Some ( uri ( & dir. path (). join ( "types.d.ts" ))) 419); 420assert_eq! ( 421 input. per_file_options [ & main][ "jsxImportSource" ], 422"jsr:@test/ui@1" 423); 424assert! ( input. diagnostics . is_empty ()); 425assert_eq! ( 426 fs:: read_to_string ( dir. path (). join ( "tsconfig.json" )). unwrap (), 427 config 428); 429} 430 431# [ test ] 432fn preserved_jsx_and_bad_configs_do_not_abort_live_generations () { 433let dir = tempfile:: tempdir (). unwrap (); 434files ( 435 dir. path (), 436& [ 437( "avril.conf" , "{}" ), 438( "main.tsx" , "export const node = <p />;" ), 439], 440); 441let main =uri ( & dir. path (). join ( "main.tsx" )); 442for modein [ "preserve" , "react-native" ] { 443 fs:: write ( 444 dir. path (). join ( "tsconfig.json" ), 445format! ( r#"{{"compilerOptions":{{"jsx":"{mode}"}}}}"# ), 446) 447. unwrap (); 448let input =refresh ( dir. path (), options ( dir. path ()), & [ "main.tsx" ]); 449assert_eq! ( input. per_file_options [ & main][ "jsx" ], mode); 450assert! ( input. diagnostics . is_empty ()); 451} 452files ( 453 dir. path (), 454& [( "tsconfig.json" , r#"{"extends":"./absent.json"}"# )], 455); 456let input =refresh ( dir. path (), options ( dir. path ()), & [ "main.tsx" ]); 457assert! ( 458 input. diagnostics [ & main] 459. iter () 460. any ( |error| error. contains ( "Cannot find tsconfig" )) 461); 462files ( dir. path (), & [( "avril.conf" , "deps: [" )]); 463let input =refresh ( dir. path (), options ( dir. path ()), & [ "main.tsx" ]); 464assert! ( !input. diagnostics [ & main]. is_empty ()); 465assert! ( input. sources . iter (). any ( |source| source. uri == main)); 466} 467 468# [ test ] 469fn avril_conf_jsx_defaults_follow_edits_within_their_package () { 470let dir = tempfile:: tempdir (). unwrap (); 471files ( 472 dir. path (), 473& [ 474( "avril.conf" , "compilerOptions: {jsxImportSource: 'first'}" ), 475( "main.tsx" , "export const node = <p />;" ), 476( "lib/avril.conf" , "{}" ), 477( "lib/widget.tsx" , "export const widget = <b />;" ), 478], 479); 480let main =uri ( & dir. path (). join ( "main.tsx" )); 481let widget =uri ( & dir. path (). join ( "lib/widget.tsx" )); 482for ( config, tsconfig, expected) in [ 483( "compilerOptions: {jsxImportSource: 'first'}" , None , "first" ), 484( 485"compilerOptions: {jsxImportSource: 'second'}" , 486None , 487"second" , 488), 489( 490"compilerOptions: {jsxImportSource: 'second'}" , 491Some ( r#"{"compilerOptions":{"jsxImportSource":"ts"}}"# ), 492"ts" , 493), 494] { 495files ( dir. path (), & [( "avril.conf" , config)]); 496if let Some ( tsconfig) = tsconfig{ 497files ( dir. path (), & [( "tsconfig.json" , tsconfig)]); 498} 499let input =refresh ( 500 dir. path (), 501options ( dir. path ()), 502& [ "main.tsx" , "lib/widget.tsx" ], 503); 504assert_eq! ( input. per_file_options [ & main][ "jsxImportSource" ], expected); 505assert! ( 506 input. per_file_options [ & widget] 507. get ( "jsxImportSource" ) 508. is_none () 509); 510} 511} 512 513# [ test ] 514fn remote_redirects_use_final_bases_and_missing_package_names_are_diagnostics () { 515let http = http:: Http :: new (); 516let dir = tempfile:: tempdir (). unwrap (); 517 http. redirect ( "start" , "/nested/mod.ts" ); 518 http. source ( "nested/mod.ts" , "export {value} from './dep.ts';" ); 519 http. source ( "nested/dep.ts" , "export const value: number = 42;" ); 520files ( 521 dir. path (), 522& [ 523( "avril.conf" , "{}" ), 524( 525"main.ts" , 526& format! ( "import {{value}} from '{}';" , http. name ( "start" )), 527), 528], 529); 530let input =refresh ( dir. path (), options ( dir. path ()), & [ "main.ts" ]); 531let main =uri ( & dir. path (). join ( "main.ts" )); 532let remote =resolve ( & input, & main, & http. name ( "start" )). unwrap (); 533let dependency =resolve ( & input, & remote, "./dep.ts" ). unwrap (); 534assert! ( dependency. ends_with ( "/nested/dep.ts" )); 535assert! ( 536 input 537. sources 538. iter () 539. any ( |source| source. uri == dependency && source. read_only ) 540); 541files ( 542 dir. path (), 543& [( 544"avril.conf" , 545& format! ( "deps: ['{}']" , http. name ( "missing/" )), 546)], 547); 548let input =refresh ( dir. path (), options ( dir. path ()), & [ "main.ts" ]); 549assert! ( 550 input. diagnostics [ & main] 551. iter () 552. any ( |error| error. contains ( "Cannot read the name" )) 553); 554assert! ( input. sources . iter (). any ( |source| source. uri == main)); 555} 556 557# [ test ] 558fn remote_package_ownership_is_stable_in_both_import_orders () { 559let http = http:: Http :: new (); 560let dir = tempfile:: tempdir (). unwrap (); 561 http. source ( 562"pkg/child/avril.conf" , 563"name: 'child', exports: './mod.ts', imports: {helper: './helper.ts'}" , 564); 565 http. source ( "pkg/child/mod.ts" , "export {value} from 'helper';" ); 566 http. source ( "pkg/child/helper.ts" , "export const value = 7;" ); 567 http. source ( "pkg/wrong.ts" , "throw Error('parent map leaked');" ); 568files ( 569 dir. path (), 570& [ 571( 572"avril.conf" , 573& format! ( 574"deps: ['{}', '{}']" , 575 http. name ( "pkg/" ), 576 http. name ( "pkg/child/" ) 577), 578), 579( "x.ts" , "import * as a from 'outer'; export {a};" ), 580( "y.ts" , "import * as b from 'child'; export {b};" ), 581], 582); 583for nestedin [ true , false ] { 584 http. source ( "pkg/avril.conf" , & format! ( 585"name: 'outer', exports: './child/mod.ts', imports: {{helper: './wrong.ts'}}, deps: [{}]" , 586if nested{ "{from: './child/', name: 'nested'}" } else { "" }, 587)); 588for importsin [ 589"import './x.ts'; import './y.ts';" , 590"import './y.ts'; import './x.ts';" , 591] { 592files ( dir. path (), & [( "main.ts" , imports)]); 593let cache = dir. path (). join ( "cache" ); 594if cache. exists () { 595 fs:: remove_dir_all ( cache). unwrap (); 596} 597let input =refresh ( dir. path (), options ( dir. path ()), & [ "main.ts" ]); 598if nested{ 599assert! ( input. diagnostics . is_empty (), "{:?}" , input. diagnostics ); 600assert_eq! ( 601resolve ( & input, & http. name ( "pkg/child/mod.ts" ), "helper" ), 602Some ( http. name ( "pkg/child/helper.ts" )), 603); 604} else { 605assert! ( 606 input 607. diagnostics 608. values () 609. flatten () 610. any ( |error| error. contains ( "Conflicting remote package ownership" )) 611); 612} 613} 614} 615} 616 617# [ test ] 618fn registry_prefixed_configs_are_rejected_without_fetching_or_writing_locks () { 619let http = http:: Http :: new (); 620let dir = tempfile:: tempdir (). unwrap (); 621let mut options =options ( dir. path ()); 622 options. jsr_registry = http. url . clone (); 623let lock =format! ( 624"version: 1\njsrRegistry: \"{}\"\nspecifiers: {{}}\npackages: {{}}\nremote: {{}}\n" , 625 http. url 626); 627files ( 628 dir. path (), 629& [( "avril.lock" , & lock), ( "main.ts" , "import 'pkg';" )], 630); 631for dependencyin [ 632format! ( "'{}'" , http. name ( "pkg/" )), 633format! ( "{{from: '{}', name: 'pkg'}}" , http. name ( "pkg/" )), 634] { 635files ( 636 dir. path (), 637& [( "avril.conf" , & format! ( "deps: [{dependency}]" ))], 638); 639let input =refresh ( dir. path (), options. clone (), & [ "main.ts" ]); 640assert! ( 641 input 642. diagnostics 643. values () 644. flatten () 645. any ( |error| error. contains ( "configured JSR registry" )) 646); 647assert! ( http. requests . lock (). unwrap (). is_empty ()); 648assert_eq! ( 649 fs:: read_to_string ( dir. path (). join ( "avril.lock" )). unwrap (), 650 lock 651); 652} 653}