char/avril
(wip) why'd you have to go and make things so complicaaaaated?
git clone https://git.t4t.associates/char/avril
51266f9
main
1use std:: collections::{ BTreeMap , BTreeSet }; 2use std:: sync:: Arc ; 3 4use super ::{ validate_npm_name, validate_npm_version}; 5/// Failures reading npm package manifests. 6/// Resolution failures live in the resolver's own error type. 7# [ derive ( Clone , Debug , thiserror :: Error )] 8pub enum PackageError { 9# [ error ( "npm package.json is missing its name" )] 10NpmPackageJsonMissingName , 11# [ error ( "npm package.json is missing its version" )] 12NpmPackageJsonMissingVersion , 13# [ error ( "npm package.json does not match its archive identity" )] 14NpmPackageJsonIdentityMismatch , 15# [ error ( "Unsupported package type: {other}" )] 16UnsupportedPackageType { other : String }, 17# [ error ( "npm dependencies must be an object" )] 18NpmDependenciesNotObject , 19# [ error ( "npm dependency {key:?} must be a version range" )] 20NpmDependencyNotRange { key : String }, 21# [ error ( "npm optional dependency {key:?} must be a version range" )] 22NpmOptionalDependencyNotRange { key : String }, 23# [ error ( "npm dependency {key:?} has an invalid alias" )] 24NpmDependencyInvalidAlias { key : String }, 25# [ error ( "npm dependency {key:?} has an invalid version range" )] 26NpmDependencyInvalidRange { key : String }, 27# [ error ( "Invalid npm os field: {detail}" )] 28InvalidNpmOsField { detail : String }, 29# [ error ( "Invalid npm cpu field: {detail}" )] 30InvalidNpmCpuField { detail : String }, 31# [ error ( "npm optionalDependencies must be an object" )] 32NpmOptionalDependenciesNotObject , 33# [ error ( "npm peerDependencies must be an object" )] 34NpmPeerDependenciesNotObject , 35# [ error ( "npm peer dependency {key:?} must be a version range" )] 36NpmPeerDependencyNotRange { key : String }, 37# [ error ( "npm {key:?} must be a path string" )] 38NpmMustBePathString { key : String }, 39# [ error ( "npm dependency {key:?} has an empty alias" )] 40NpmDependencyEmptyAlias { key : String }, 41# [ error ( "npm dependency {key:?} alias must not have a subpath" )] 42NpmDependencyAliasSubpath { key : String }, 43 44# [ error ( "npm peer dependency {key:?} uses unsupported protocol {protocol:?}" )] 45NpmPeerDependencyProtocol { key : String , protocol : String }, 46# [ error ( "npm exports mixes subpath and condition keys" )] 47NpmExportsMixesKeys , 48# [ error ( "Invalid npm exports field" )] 49InvalidNpmExportsField , 50# [ error ( "npm imports must be an object" )] 51NpmImportsNotObject , 52# [ error ( "npm imports keys must start with '#'" )] 53NpmImportsKeyPrefix , 54# [ error ( "npm platform entries must be strings" )] 55NpmPlatformEntriesNotStrings , 56# [ error ( "npm platform conditions must be a string or array" )] 57NpmPlatformConditionsType , 58# [ error ( "npm platform conditions must not be empty" )] 59NpmPlatformConditionsEmpty , 60# [ error ( "npm platform exclusion must name a platform" )] 61NpmPlatformExclusionUnnamed , 62# [ error ( "Invalid npm export subpath: {subpath}" )] 63InvalidNpmExportSubpath { subpath : String }, 64# [ error ( "npm export \".\" is blocked" )] 65NpmExportRootBlocked , 66# [ error ( "Invalid npm export pattern: {key}" )] 67InvalidNpmExportPattern { key : String }, 68# [ error ( "npm export {subpath:?} is blocked" )] 69NpmExportBlocked { subpath : String }, 70# [ error ( "npm export target escapes its package: {target:?}" )] 71NpmExportTargetEscapes { target : String }, 72# [ error ( "Invalid npm exports entry" )] 73InvalidNpmExportsEntry , 74# [ error ( "invalid npm condition key: {key}" )] 75InvalidNpmConditionKey { key : String }, 76# [ error ( "Invalid npm imports pattern: {key}" )] 77InvalidNpmImportsPattern { key : String }, 78# [ error ( "npm import {specifier:?} is blocked" )] 79NpmImportBlocked { specifier : String }, 80# [ error ( "npm import is blocked" )] 81NpmImportBlockedBare , 82# [ error ( "npm imports target escapes its package: {target:?}" )] 83NpmImportsTargetEscapes { target : String }, 84# [ error ( "npm imports mixes subpath and condition keys" )] 85NpmImportsMixesKeys , 86# [ error ( "Invalid npm imports entry" )] 87InvalidNpmImportsEntry , 88# [ error ( "Invalid npm imports target" )] 89InvalidNpmImportsTarget , 90# [ error ( "npm imports target escapes its package" )] 91NpmImportsTargetEscapesBare , 92# [ error ( "npm imports target {target:?} is not a declared dependency" )] 93NpmImportsTargetUndeclared { target : String }, 94# [ error ( "Invalid package type in {path}" )] 95InvalidPackageTypeIn { path : String }, 96# [ error ( "Unsupported package type in {path}: {text}" )] 97UnsupportedPackageTypeIn { path : String , text : String }, 98 99# [ error ( "{detail}" )] 100NpmUnsupportedModule { detail : String }, 101# [ error ( "Invalid nested npm package.json {path}: {source}" )] 102InvalidNestedNpmPackageJson { 103path : String , 104source : Arc < serde_json:: Error >, 105}, 106# [ error ( transparent )] 107Npm ( Box < super :: NpmError >), 108} 109 110impl From < super :: NpmError > for PackageError { 111fn from ( error : super :: NpmError ) ->Self { 112Self :: Npm ( Box :: new ( error)) 113} 114} 115 116/// Parsed view of one published `package.json`. The ordered document is kept 117/// out of this struct: resolution reads `exports`/`imports` from the verified 118/// bytes through [`NpmManifest::exports_raw`]/[`imports_raw`] owners instead. 119# [ derive ( Clone , Debug , PartialEq )] 120pub struct NpmManifest { 121pub name : String , 122pub version : String , 123pub package_type : NpmPackageType , 124pub main : Option < String >, 125pub module : Option < String >, 126pub types : Option < serde_json:: Value >, 127pub typings : Option < serde_json:: Value >, 128pub types_versions : Option < serde_json:: Value >, 129pub exports_raw : Option < serde_json:: Value >, 130pub imports_raw : Option < serde_json:: Value >, 131pub dependencies : BTreeMap < String , NpmDep >, 132pub peers : BTreeMap < String , NpmPeer >, 133pub os : Option < PlatformRule >, 134pub cpu : Option < PlatformRule >, 135/// Bundler-only hints, kept raw so malformed values never break execution. 136pub browser : Option < serde_json:: Value >, 137pub side_effects : Option < serde_json:: Value >, 138} 139 140# [ derive ( Clone , Copy , Debug , PartialEq , Eq )] 141pub enum NpmPackageType { 142Module , 143CommonJs , 144} 145 146# [ derive ( Clone , Debug , PartialEq )] 147pub struct NpmDep { 148pub kind : NpmDepKind , 149pub optional : bool , 150} 151 152# [ derive ( Clone , Debug , PartialEq )] 153pub enum NpmDepKind { 154Registry { request : String }, 155Alias { target : String }, 156Unsupported { protocol : String }, 157} 158 159# [ derive ( Clone , Debug , PartialEq )] 160pub struct NpmPeer { 161pub range : String , 162pub optional : bool , 163} 164 165# [ derive ( Clone , Debug , PartialEq )] 166pub struct PlatformRule { 167pub allow : Vec < String >, 168pub deny : Vec < String >, 169} 170 171# [ derive ( Clone , Copy , Debug , PartialEq , Eq )] 172pub enum NpmModuleKind { 173Esm , 174TypeScript , 175Declaration , 176CommonJs , 177Json , 178} 179 180# [ derive ( Clone , Debug )] 181pub enum NpmUnsupported { 182Native { path : String }, 183Extension { path : String }, 184} 185 186impl std:: fmt:: Display for NpmUnsupported { 187fn fmt ( & self , f : & mut std:: fmt:: Formatter < ' _ >) -> std:: fmt:: Result { 188match self { 189Self :: Native { path} =>write! ( f, "Native addons are not supported: {path}" ), 190Self :: Extension { path} =>write! ( f, "Unsupported module extension: {path}" ), 191} 192} 193} 194 195impl std:: error:: Error for NpmUnsupported {} 196 197pub fn parse_npm_manifest ( 198name : & str , 199version : & str , 200value : & serde_json:: Value , 201) ->Result < NpmManifest , PackageError > { 202let published_name = value 203. get ( "name" ) 204. and_then ( |value| value. as_str ()) 205. ok_or ( PackageError :: NpmPackageJsonMissingName ) ?; 206let published_version = value 207. get ( "version" ) 208. and_then ( |value| value. as_str ()) 209. ok_or ( PackageError :: NpmPackageJsonMissingVersion ) ?; 210if published_name != name || published_version != version{ 211return Err ( PackageError :: NpmPackageJsonIdentityMismatch ); 212} 213validate_npm_name ( published_name) ?; 214validate_npm_version ( published_version) ?; 215let package_type =match value. get ( "type" ). and_then ( |value| value. as_str ()) { 216None =>NpmPackageType :: CommonJs , 217Some ( "module" ) =>NpmPackageType :: Module , 218Some ( "commonjs" ) =>NpmPackageType :: CommonJs , 219Some ( other) =>{ 220return Err ( PackageError :: UnsupportedPackageType { other : other. to_owned () }); 221} 222}; 223let main =read_entry_string ( value, "main" ) ?; 224let module =read_entry_string ( value, "module" ) ?; 225let types = value. get ( "types" ). cloned (); 226let typings = value. get ( "typings" ). cloned (); 227let types_versions = value. get ( "typesVersions" ). cloned (); 228let exports_raw = value. get ( "exports" ). cloned (); 229if let Some ( exports) =& exports_raw{ 230validate_exports_shape ( exports) ?; 231} 232let imports_raw = value. get ( "imports" ). cloned (); 233if let Some ( imports) =& imports_raw{ 234validate_imports_shape ( imports) ?; 235} 236let mut dependencies =BTreeMap :: new (); 237if let Some ( table) = value. get ( "dependencies" ) { 238let table = table 239. as_object () 240. ok_or ( PackageError :: NpmDependenciesNotObject ) ?; 241for ( key, spec) in table{ 242let spec = spec 243. as_str () 244. ok_or_else ( ||PackageError :: NpmDependencyNotRange { key : key. to_owned () }) ?; 245 dependencies. insert ( 246 key. clone (), 247NpmDep { 248kind : parse_dep_spec ( key, spec) ?, 249optional : false , 250}, 251); 252} 253} 254if let Some ( table) = value. get ( "optionalDependencies" ) { 255let table = table 256. as_object () 257. ok_or ( PackageError :: NpmOptionalDependenciesNotObject ) ?; 258for ( key, spec) in table{ 259let spec = 260 spec. as_str () 261. ok_or_else ( ||PackageError :: NpmOptionalDependencyNotRange { 262key : key. to_owned (), 263}) ?; 264 dependencies. insert ( 265 key. clone (), 266NpmDep { 267kind : parse_dep_spec ( key, spec) ?, 268optional : true , 269}, 270); 271} 272} 273let mut peers =BTreeMap :: new (); 274if let Some ( table) = value. get ( "peerDependencies" ) { 275let table = table 276. as_object () 277. ok_or ( PackageError :: NpmPeerDependenciesNotObject ) ?; 278let meta = value 279. get ( "peerDependenciesMeta" ) 280. and_then ( |value| value. as_object ()); 281for ( key, range) in table{ 282let range = range 283. as_str () 284. ok_or_else ( ||PackageError :: NpmPeerDependencyNotRange { key : key. to_owned () }) ?; 285// Peer ranges stay textual until selection; only the protocol shape 286// is rejected here so later chunks see the declared constraint. 287peer_range_supported ( key, range) ?; 288let optional = meta 289. and_then ( |meta| meta. get ( key)) 290. and_then ( |entry| entry. get ( "optional" )) 291. and_then ( |value| value. as_bool ()) 292. unwrap_or ( false ); 293 peers. insert ( 294 key. clone (), 295NpmPeer { 296range : range. to_owned (), 297 optional, 298}, 299); 300} 301} 302let os = value 303. get ( "os" ) 304. map ( parse_platform_rule) 305. transpose () 306. map_err ( |error|PackageError :: InvalidNpmOsField { detail : error. to_string () }) ?; 307let cpu = value 308. get ( "cpu" ) 309. map ( parse_platform_rule) 310. transpose () 311. map_err ( |error|PackageError :: InvalidNpmCpuField { detail : error. to_string () }) ?; 312Ok ( NpmManifest { 313name : name. to_owned (), 314version : version. to_owned (), 315 package_type, 316 main, 317 module, 318 types, 319 typings, 320 types_versions, 321 exports_raw, 322 imports_raw, 323 dependencies, 324 peers, 325 os, 326 cpu, 327browser : value. get ( "browser" ). cloned (), 328side_effects : value. get ( "sideEffects" ). cloned (), 329}) 330} 331 332/// Whether `sideEffects` declares side effects for a package-relative file, 333/// matched like Rolldown does. Unknown shapes and invalid patterns are `None`. 334pub fn declared_side_effects ( field : & serde_json:: Value , rel : & str ) ->Option < bool > { 335let patterns =match field{ 336 serde_json:: Value :: Bool ( value) =>return Some ( * value), 337 serde_json:: Value :: String ( _) => std:: slice:: from_ref ( field), 338 serde_json:: Value :: Array ( patterns) => patterns. as_slice (), 339 _ =>return None , 340}; 341let mut matched =false ; 342for patternin patterns{ 343let pattern = pattern. as_str () ?; 344let trimmed = pattern. trim_start_matches ( "./" ); 345let mut glob =if trimmed. len () == pattern. len () && trimmed. contains ( '/' ) { 346 trimmed. to_owned () 347} else { 348format! ( "**/{trimmed}" ) 349}; 350// The field is an allowlist: a leading `!` is a literal file name. 351if glob. starts_with ( '!' ) { 352 glob. insert ( 0 , '\\' ); 353} 354 fast_glob:: validate ( & glob). ok () ?; 355 matched |= fast_glob:: glob_match ( & glob, rel); 356} 357Some ( matched) 358} 359 360fn read_entry_string ( value : & serde_json:: Value , key : & str ) ->Result < Option < String >, PackageError > { 361match value. get ( key) { 362None =>Ok ( None ), 363// Subpath-only packages use main: false alongside explicit exports. 364Some ( serde_json:: Value :: Bool ( false )) if key =="main" =>Ok ( None ), 365Some ( entry) => entry 366. as_str () 367. map ( |entry|Some ( entry. to_owned ())) 368. ok_or_else ( ||PackageError :: NpmMustBePathString { key : key. to_owned () }), 369} 370} 371 372pub fn parse_dep_spec ( key : & str , spec : & str ) ->Result < NpmDepKind , PackageError > { 373if let Some ( target) = spec. strip_prefix ( "npm:" ) { 374if target. is_empty () { 375return Err ( PackageError :: NpmDependencyEmptyAlias { key : key. to_owned () }); 376} 377let reference =super :: parse_npm_request ( & format! ( "npm:{target}" )) 378. map_err ( |_|PackageError :: NpmDependencyInvalidAlias { key : key. to_owned () }) ?; 379if reference. subpath . is_some () { 380return Err ( PackageError :: NpmDependencyAliasSubpath { key : key. to_owned () }); 381} 382return Ok ( NpmDepKind :: Alias { target : reference. request_key () }); 383} 384if let Some (( protocol, _)) = spec. split_once ( ':' ) 385 && !protocol. contains ( '/' ) 386 && !protocol. contains ( '@' ) 387{ 388return Ok ( NpmDepKind :: Unsupported { protocol : protocol. to_owned () }); 389} 390let request =if spec. is_empty () || spec =="*" { 391format! ( "npm:{key}" ) 392} else { 393format! ( "npm:{key}@{spec}" ) 394}; 395super :: parse_npm_request ( & request) 396. map_err ( |_|PackageError :: NpmDependencyInvalidRange { key : key. to_owned () }) ?; 397Ok ( NpmDepKind :: Registry { request}) 398} 399 400fn peer_range_supported ( key : & str , range : & str ) ->Result <(), PackageError > { 401if range. is_empty () { 402return Err ( PackageError :: NpmPeerDependencyNotRange { key : key. to_owned () }); 403} 404if let Some (( protocol, _)) = range. split_once ( ':' ) 405 && !protocol. contains ( '/' ) 406 && !protocol. contains ( '@' ) 407{ 408return Err ( PackageError :: NpmPeerDependencyProtocol { 409key : key. to_owned (), 410protocol : protocol. to_owned (), 411}); 412} 413Ok (()) 414} 415 416fn validate_exports_shape ( exports : & serde_json:: Value ) ->Result <(), PackageError > { 417match exports{ 418 serde_json:: Value :: Null | serde_json:: Value :: String ( _) =>Ok (()), 419 serde_json:: Value :: Array ( _) =>Ok (()), 420 serde_json:: Value :: Object ( map) =>{ 421let subpaths = map. keys (). filter ( |key| key. starts_with ( '.' )). count (); 422if subpaths >0 && subpaths != map. len () { 423return Err ( PackageError :: NpmExportsMixesKeys ); 424} 425Ok (()) 426} 427 _ =>Err ( PackageError :: InvalidNpmExportsField ), 428} 429} 430 431fn validate_imports_shape ( imports : & serde_json:: Value ) ->Result <(), PackageError > { 432let map = imports 433. as_object () 434. ok_or ( PackageError :: NpmImportsNotObject ) ?; 435for keyin map. keys () { 436if !key. starts_with ( '#' ) { 437return Err ( PackageError :: NpmImportsKeyPrefix ); 438} 439} 440Ok (()) 441} 442 443fn parse_platform_rule ( value : & serde_json:: Value ) ->Result < PlatformRule , PackageError > { 444let entries: Vec < String > =match value{ 445 serde_json:: Value :: String ( entry) =>vec! [ entry. clone ()], 446 serde_json:: Value :: Array ( entries) => entries 447. iter () 448. map ( |entry|{ 449 entry 450. as_str () 451. map ( str:: to_owned) 452. ok_or ( PackageError :: NpmPlatformEntriesNotStrings ) 453}) 454. collect ::< Result < _ , _ >>() ?, 455 _ =>return Err ( PackageError :: NpmPlatformConditionsType ), 456}; 457if entries. is_empty () { 458return Err ( PackageError :: NpmPlatformConditionsEmpty ); 459} 460let mut allow =Vec :: new (); 461let mut deny =Vec :: new (); 462for entryin entries{ 463if let Some ( excluded) = entry. strip_prefix ( '!' ) { 464if excluded. is_empty () { 465return Err ( PackageError :: NpmPlatformExclusionUnnamed ); 466} 467 deny. push ( excluded. to_owned ()); 468} else { 469 allow. push ( entry); 470} 471} 472Ok ( PlatformRule { allow, deny}) 473} 474 475pub fn platform_satisfied ( rule : Option < & PlatformRule >, host : & str ) ->bool { 476let Some ( rule) = ruleelse { 477return true ; 478}; 479if rule. deny . iter (). any ( |entry| entry == host) { 480return false ; 481} 482if rule. allow . is_empty () { 483return true ; 484} 485 rule. allow . iter (). any ( |entry| entry == host) 486} 487 488pub fn npm_host_os () ->&' static str { 489match std:: env:: consts:: OS { 490"macos" =>"darwin" , 491"windows" =>"win32" , 492 other => other, 493} 494} 495 496pub fn npm_host_arch () ->&' static str { 497match std:: env:: consts:: ARCH { 498"x86_64" =>"x64" , 499"aarch64" =>"arm64" , 500"x86" =>"ia32" , 501 other => other, 502} 503} 504 505/// Ordered condition set for this runtime. `node`, `require`, and `browser` 506/// are deliberately absent: only ESM graphs resolve here. 507pub fn npm_conditions () ->[ &' static str ; 4 ] { 508[ "avril" , "import" , "module" , "default" ] 509} 510 511/// Ordered condition set for synchronous `require` graphs. `import`, 512/// `module`, and `node` are deliberately absent: `require` selects its own 513/// branch independently from `import`. 514pub fn npm_conditions_require () ->[ &' static str ; 3 ] { 515[ "avril" , "require" , "default" ] 516} 517 518/// Resolve `exports` for one subpath (`"."` or `"./…"`). `Ok(None)` means no 519/// entry matched; an exports map that is present never falls back to private 520/// files, so callers must turn `None` into an error. 521pub fn resolve_exports_target ( 522exports : & serde_json:: Value , 523subpath : & str , 524conditions : & [ & str ], 525) ->Result < Option < String >, PackageError > { 526if !subpath. starts_with ( "./" ) && subpath !="." { 527return Err ( PackageError :: InvalidNpmExportSubpath { subpath : subpath. to_owned () }); 528} 529match exports{ 530 serde_json:: Value :: Null =>Ok ( None ), 531 serde_json:: Value :: String ( target) =>{ 532if subpath !="." { 533return Ok ( None ); 534} 535Ok ( Some ( target. clone ())) 536} 537 serde_json:: Value :: Array ( _) =>{ 538if subpath !="." { 539return Ok ( None ); 540} 541match resolve_match ( exports, conditions, None , true ) ?{ 542Match :: Hit ( resolved) =>Ok ( Some ( resolved)), 543// Unreachable: array nulls skip inside resolve_match. 544Match :: Blocked =>Err ( PackageError :: NpmExportRootBlocked ), 545Match :: Miss =>Ok ( None ), 546} 547} 548 serde_json:: Value :: Object ( map) =>{ 549let subpaths = map. keys (). filter ( |key| key. starts_with ( '.' )). count (); 550if subpaths >0 && subpaths != map. len () { 551return Err ( PackageError :: NpmExportsMixesKeys ); 552} 553if subpaths >0 { 554if let Some ( target) = map. get ( subpath) { 555return match resolve_match ( target, conditions, None , true ) ?{ 556Match :: Hit ( resolved) =>Ok ( Some ( resolved)), 557Match :: Blocked =>{ 558Err ( PackageError :: NpmExportBlocked { subpath : subpath. to_owned () }) 559} 560Match :: Miss =>Ok ( None ), 561}; 562} 563let mut best: Option <( usize , usize , & str )> =None ; 564for keyin map. keys () { 565let Some (( prefix, suffix)) = key. split_once ( '*' ) else { 566continue ; 567}; 568if key[ prefix. len () +1 ..]. contains ( '*' ) { 569return Err ( PackageError :: InvalidNpmExportPattern { key : key. to_owned () }); 570} 571if !subpath. starts_with ( prefix) || !subpath. ends_with ( suffix) { 572continue ; 573} 574if prefix. len () + suffix. len () > subpath. len () { 575continue ; 576} 577let candidate =( prefix. len (), suffix. len (), key. as_str ()); 578if best. is_none_or ( |current|{ 579 candidate. 0 > current. 0 580 ||( candidate. 0 == current. 0 && candidate. 1 > current. 1 ) 581}) { 582 best =Some ( candidate); 583} 584} 585let Some (( _, _, key)) = bestelse { 586return Ok ( None ); 587}; 588let ( prefix, suffix) = key. split_once ( '*' ). unwrap (); 589let captured =& subpath[ prefix. len () ..subpath. len () - suffix. len ()]; 590match resolve_match ( & map[ key], conditions, Some ( captured), true ) ?{ 591Match :: Hit ( resolved) =>Ok ( Some ( resolved)), 592Match :: Blocked =>{ 593Err ( PackageError :: NpmExportBlocked { subpath : subpath. to_owned () }) 594} 595Match :: Miss =>Ok ( None ), 596} 597} else if subpath !="." { 598Ok ( None ) 599} else { 600match resolve_match ( exports, conditions, None , true ) ?{ 601Match :: Hit ( resolved) =>Ok ( Some ( resolved)), 602Match :: Blocked =>Err ( PackageError :: NpmExportRootBlocked ), 603Match :: Miss =>Ok ( None ), 604} 605} 606} 607 _ =>Err ( PackageError :: InvalidNpmExportsField ), 608} 609} 610 611enum Match { 612Hit ( String ), 613Blocked , 614Miss , 615} 616 617/// Target shape validation during selection so arrays can skip bad entries 618/// instead of failing the whole list at the final caller. 619fn validate_export_target ( target : & str ) ->Result <(), PackageError > { 620let rel = target 621. strip_prefix ( "./" ) 622. ok_or_else ( ||PackageError :: NpmExportTargetEscapes { target : target. to_owned () }) ?; 623super :: validate_npm_relpath ( rel) ?; 624if rel. split ( '/' ). any ( |part| part =="node_modules" ) { 625return Err ( PackageError :: NpmExportTargetEscapes { target : target. to_owned () }); 626} 627Ok (()) 628} 629 630/// Evaluate one exports value. Conditions follow the package document's own 631/// key order against the active set; a directly selected `null` blocks 632/// distinctly instead of falling through, while arrays skip misses. 633fn resolve_match ( 634value : & serde_json:: Value , 635conditions : & [ & str ], 636captured : Option < & str >, 637strict : bool , 638) ->Result < Match , PackageError > { 639let substitute = |target : & str |match captured{ 640Some ( part) => target. replace ( '*' , part), 641None => target. to_owned (), 642}; 643match value{ 644 serde_json:: Value :: Null =>Ok ( Match :: Blocked ), 645 serde_json:: Value :: String ( target) =>{ 646let target =substitute ( target); 647match validate_export_target ( & target) { 648Ok (()) =>Ok ( Match :: Hit ( target)), 649Err ( _) if !strict =>Ok ( Match :: Miss ), 650Err ( error) =>Err ( error), 651} 652} 653 serde_json:: Value :: Array ( targets) =>{ 654for targetin targets{ 655match resolve_match ( target, conditions, captured, false ) ?{ 656Match :: Hit ( resolved) =>return Ok ( Match :: Hit ( resolved)), 657Match :: Blocked =>continue , 658Match :: Miss =>continue , 659} 660} 661Ok ( Match :: Miss ) 662} 663 serde_json:: Value :: Object ( map) =>{ 664for ( key, target) in map{ 665if key. starts_with ( '.' ) { 666return Err ( PackageError :: NpmExportsMixesKeys ); 667} 668if key. is_empty () 669 || key. starts_with ( '#' ) 670 || key. bytes (). all ( |byte| byte. is_ascii_digit ()) 671{ 672return Err ( PackageError :: InvalidNpmConditionKey { key : key. to_owned () }); 673} 674if !conditions. contains ( & key. as_str ()) { 675continue ; 676} 677match resolve_match ( target, conditions, captured, strict) ?{ 678Match :: Hit ( resolved) =>return Ok ( Match :: Hit ( resolved)), 679Match :: Blocked =>return Ok ( Match :: Blocked ), 680Match :: Miss =>continue , 681} 682} 683Ok ( Match :: Miss ) 684} 685 _ =>Err ( PackageError :: InvalidNpmExportsEntry ), 686} 687} 688 689pub enum NpmImportsTarget { 690External { request : String }, 691File { path : String }, 692} 693 694/// Resolve an `#…` specifier through the owning package's `imports` map. 695/// Bare-word targets must name a declared dependency of the same package. 696pub fn resolve_imports_target ( 697imports : & serde_json:: Value , 698specifier : & str , 699conditions : & [ & str ], 700declared : & BTreeSet < String >, 701) ->Result < Option < NpmImportsTarget >, PackageError > { 702let map = imports 703. as_object () 704. ok_or ( PackageError :: NpmImportsNotObject ) ?; 705if let Some ( target) = map. get ( specifier) { 706return imports_value_target ( target, conditions, declared); 707} 708let mut best: Option <( usize , usize , & str )> =None ; 709for keyin map. keys () { 710if !key. starts_with ( '#' ) { 711return Err ( PackageError :: NpmImportsKeyPrefix ); 712} 713let Some (( prefix, suffix)) = key. split_once ( '*' ) else { 714continue ; 715}; 716if key[ prefix. len () +1 ..]. contains ( '*' ) { 717return Err ( PackageError :: InvalidNpmImportsPattern { key : key. to_owned () }); 718} 719if !specifier. starts_with ( prefix) || !specifier. ends_with ( suffix) { 720continue ; 721} 722if prefix. len () + suffix. len () > specifier. len () { 723continue ; 724} 725let candidate =( prefix. len (), suffix. len (), key. as_str ()); 726if best. is_none_or ( |current|{ 727 candidate. 0 > current. 0 ||( candidate. 0 == current. 0 && candidate. 1 > current. 1 ) 728}) { 729 best =Some ( candidate); 730} 731} 732let Some (( _, _, key)) = bestelse { 733return Ok ( None ); 734}; 735let ( prefix, suffix) = key. split_once ( '*' ). unwrap (); 736let captured =& specifier[ prefix. len () ..specifier. len () - suffix. len ()]; 737match imports_match ( & map[ key], conditions, Some ( captured), declared, true ) ?{ 738ImportsMatch :: Hit ( target) =>Ok ( Some ( target)), 739ImportsMatch :: Blocked =>{ 740Err ( PackageError :: NpmImportBlocked { specifier : specifier. to_owned () }) 741} 742ImportsMatch :: Miss =>Ok ( None ), 743} 744} 745 746enum ImportsMatch { 747Hit ( NpmImportsTarget ), 748Blocked , 749Miss , 750} 751 752fn imports_value_target ( 753value : & serde_json:: Value , 754conditions : & [ & str ], 755declared : & BTreeSet < String >, 756) ->Result < Option < NpmImportsTarget >, PackageError > { 757match imports_match ( value, conditions, None , declared, true ) ?{ 758ImportsMatch :: Hit ( target) =>Ok ( Some ( target)), 759ImportsMatch :: Blocked =>Err ( PackageError :: NpmImportBlockedBare ), 760ImportsMatch :: Miss =>Ok ( None ), 761} 762} 763 764fn validate_imports_file ( target : & str ) ->Result <(), PackageError > { 765let rel = target 766. strip_prefix ( "./" ) 767. ok_or_else ( ||PackageError :: NpmImportsTargetEscapes { target : target. to_owned () }) ?; 768super :: validate_npm_relpath ( rel) ?; 769if rel. split ( '/' ). any ( |part| part =="node_modules" ) { 770return Err ( PackageError :: NpmImportsTargetEscapes { target : target. to_owned () }); 771} 772Ok (()) 773} 774 775fn imports_match ( 776value : & serde_json:: Value , 777conditions : & [ & str ], 778captured : Option < & str >, 779declared : & BTreeSet < String >, 780strict : bool , 781) ->Result < ImportsMatch , PackageError > { 782let substitute = |target : & str |match captured{ 783Some ( part) => target. replace ( '*' , part), 784None => target. to_owned (), 785}; 786match value{ 787 serde_json:: Value :: Null =>Ok ( ImportsMatch :: Blocked ), 788 serde_json:: Value :: String ( target) =>{ 789let target =substitute ( target); 790if target. starts_with ( '.' ) { 791match validate_imports_file ( & target) { 792Ok (()) =>{} 793Err ( _) if !strict =>return Ok ( ImportsMatch :: Miss ), 794Err ( error) =>return Err ( error), 795} 796} 797imports_string_target ( & target, declared) 798. map ( |hit| hit. map_or ( ImportsMatch :: Miss , ImportsMatch :: Hit )) 799} 800 serde_json:: Value :: Array ( targets) =>{ 801for targetin targets{ 802match imports_match ( target, conditions, captured, declared, false ) ?{ 803ImportsMatch :: Hit ( resolved) =>return Ok ( ImportsMatch :: Hit ( resolved)), 804ImportsMatch :: Blocked =>continue , 805ImportsMatch :: Miss =>continue , 806} 807} 808Ok ( ImportsMatch :: Miss ) 809} 810 serde_json:: Value :: Object ( map) =>{ 811for ( key, target) in map{ 812if key. starts_with ( '.' ) || key. starts_with ( '#' ) { 813return Err ( PackageError :: NpmImportsMixesKeys ); 814} 815if key. is_empty () || key. bytes (). all ( |byte| byte. is_ascii_digit ()) { 816return Err ( PackageError :: InvalidNpmConditionKey { key : key. to_owned () }); 817} 818if !conditions. contains ( & key. as_str ()) { 819continue ; 820} 821match imports_match ( target, conditions, captured, declared, strict) ?{ 822ImportsMatch :: Hit ( resolved) =>return Ok ( ImportsMatch :: Hit ( resolved)), 823ImportsMatch :: Blocked =>return Ok ( ImportsMatch :: Blocked ), 824ImportsMatch :: Miss =>continue , 825} 826} 827Ok ( ImportsMatch :: Miss ) 828} 829 _ =>Err ( PackageError :: InvalidNpmImportsEntry ), 830} 831} 832 833fn imports_string_target ( 834target : & str , 835declared : & BTreeSet < String >, 836) ->Result < Option < NpmImportsTarget >, PackageError > { 837if let Some ( path) = target. strip_prefix ( "./" ) { 838if path. is_empty () { 839return Err ( PackageError :: InvalidNpmImportsTarget ); 840} 841return Ok ( Some ( NpmImportsTarget :: File { path : path. to_owned () })); 842} 843if target. starts_with ( "../" ) || target ==".." { 844return Err ( PackageError :: NpmImportsTargetEscapesBare ); 845} 846let ( name, _) =split_bare ( target); 847if !declared. contains ( name) { 848return Err ( PackageError :: NpmImportsTargetUndeclared { target : target. to_owned () }); 849} 850Ok ( Some ( NpmImportsTarget :: External { 851request : format! ( "npm:{target}" ), 852})) 853} 854 855pub fn types_package_name ( name : & str ) ->String { 856match name. strip_prefix ( '@' ) { 857Some ( scoped) =>format! ( "@types/{}" , scoped. replace ( '/' , "__" )), 858None =>format! ( "@types/{name}" ), 859} 860} 861 862/// Split a bare specifier into its package name and trailing subpath. 863pub fn split_bare ( specifier : & str ) ->( & str , Option < & str >) { 864if let Some ( rest) = specifier. strip_prefix ( '@' ) { 865match rest. split_once ( '/' ) { 866None =>( specifier, None ), 867Some (( scope, tail)) =>match tail. split_once ( '/' ) { 868None =>( specifier, None ), 869Some (( leaf, sub)) =>{ 870let name =& specifier[ ..scope. len () + leaf. len () +2 ]; 871( name, Some ( sub)) 872} 873}, 874} 875} else { 876match specifier. split_once ( '/' ) { 877None =>( specifier, None ), 878Some (( name, sub)) =>( name, Some ( sub)), 879} 880} 881} 882 883/// Classify one manifest-relative path for execution. ESM and 884/// project-pipeline TypeScript stay executable via `import`; CommonJS and 885/// JSON are executable via `require` (and CJS additionally via ESM wrappers). 886/// Declarations stay resolvable through the editor path instead. Explicit 887/// extensions never consult package metadata. 888pub fn classify_npm_module < E : From < NpmUnsupported >>( 889path : & str , 890package_type : impl FnOnce () ->Result < NpmPackageType , E >, 891) ->Result < NpmModuleKind , E > { 892if path. ends_with ( ".d.ts" ) || path. ends_with ( ".d.mts" ) || path. ends_with ( ".d.cts" ) { 893return Ok ( NpmModuleKind :: Declaration ); 894} 895if let Some ( name) = path. rsplit ( '/' ). next () 896 &&let Some ( extension) = name. rsplit ( '.' ). next () 897 && name. contains ( '.' ) 898{ 899match extension{ 900"mjs" |"mts" =>return Ok ( NpmModuleKind :: Esm ), 901"cjs" =>return Ok ( NpmModuleKind :: CommonJs ), 902// Explicit `.cts` transpiles TypeScript while preserving CJS. 903"cts" =>return Ok ( NpmModuleKind :: CommonJs ), 904"ts" |"tsx" |"jsx" =>return Ok ( NpmModuleKind :: TypeScript ), 905"js" =>{} 906"json" =>return Ok ( NpmModuleKind :: Json ), 907"node" =>{ 908return Err ( NpmUnsupported :: Native { path : path. to_owned () }. into ()); 909} 910 _ =>{ 911return Err ( NpmUnsupported :: Extension { path : path. to_owned () }. into ()); 912} 913} 914} 915match package_type () ?{ 916NpmPackageType :: Module =>Ok ( NpmModuleKind :: Esm ), 917// `.js` follows the nearest `type` boundary; explicit `.cjs`/`.cts` 918// above already returned CommonJS. 919NpmPackageType :: CommonJs =>Ok ( NpmModuleKind :: CommonJs ), 920} 921} 922 923# [ derive ( Debug )] 924pub ( crate ) struct NpmPackageScope { 925pub package_type : Option < NpmPackageType >, 926pub main : Option < String >, 927} 928 929pub ( crate ) fn parse_npm_scope ( bytes : & [ u8 ], path : & str ) ->Result < NpmPackageScope , PackageError > { 930let value: serde_json:: Value = serde_json:: from_slice ( bytes). map_err ( |source|{ 931PackageError :: InvalidNestedNpmPackageJson { 932path : path. to_owned (), 933source : Arc :: new ( source), 934} 935}) ?; 936let package_type =match value. get ( "type" ) { 937None =>None , 938Some ( entry) =>{ 939let text = entry 940. as_str () 941. ok_or_else ( ||PackageError :: InvalidPackageTypeIn { path : path. to_owned () }) ?; 942Some ( match text{ 943"module" =>NpmPackageType :: Module , 944"commonjs" =>NpmPackageType :: CommonJs , 945 _ =>{ 946return Err ( PackageError :: UnsupportedPackageTypeIn { 947path : path. to_owned (), 948text : text. to_owned (), 949}); 950} 951}) 952} 953}; 954Ok ( NpmPackageScope { 955 package_type, 956main : value 957. get ( "main" ) 958. and_then ( |entry| entry. as_str ()) 959. map ( str:: to_owned), 960}) 961} 962 963# [ cfg ( test )] 964mod tests{ 965use super :: * ; 966 967# [ test ] 968fn subpath_only_packages_may_disable_main () { 969let manifest =parse_npm_manifest ( 970"subpaths" , 971"1.0.0" , 972& serde_json:: json!({ 973"name" : "subpaths" , "version" : "1.0.0" , "main" : false , 974"exports" : { "./value" : "./value.js" } 975}), 976) 977. unwrap (); 978assert_eq! ( 979resolve_exports_target ( 980 manifest. exports_raw . as_ref (). unwrap (), 981"./value" , 982& conditions () 983) 984. unwrap (), 985Some ( "./value.js" . into ()) 986); 987assert_eq! ( 988resolve_exports_target ( manifest. exports_raw . as_ref (). unwrap (), "." , & conditions ()) 989. unwrap (), 990None 991); 992} 993 994# [ test ] 995fn side_effects_match_like_rolldown_and_fail_open () { 996let field = |text : & str | serde_json:: from_str ::< serde_json:: Value >( text). unwrap (); 997for ( value, rel, expected) in [ 998( "false" , "index.js" , Some ( false )), 999( "\"./init.js\"" , "init.js" , Some ( true )), 1000( "[\"./{init,other}.js\"]" , "init.js" , Some ( true )), 1001( "[\"./{init,other}.js\"]" , "lib/third.js" , Some ( false )), 1002( "[\"./lib/[a-c].js\"]" , "lib/b.js" , Some ( true )), 1003( "[\"*.css\"]" , "deep/style.css" , Some ( true )), 1004( "[\"src/**/*.js\"]" , "src/a/b.js" , Some ( true )), 1005( "[\"!literal.js\"]" , "!literal.js" , Some ( true )), 1006( "[\"./unclosed{\"]" , "unclosed" , None ), 1007( "[\"./unclosed[\"]" , "unclosed" , None ), 1008( "[42]" , "index.js" , None ), 1009( "{}" , "index.js" , None ), 1010] { 1011assert_eq! ( 1012declared_side_effects ( & field ( value), rel), 1013 expected, 1014"{value} {rel}" 1015); 1016} 1017} 1018 1019fn conditions () ->[ &' static str ; 4 ] { 1020npm_conditions () 1021} 1022 1023# [ test ] 1024fn conditions_follow_package_document_order () { 1025let exports: serde_json:: Value = 1026 serde_json:: from_str ( "{\"default\": \"./x.js\", \"import\": \"./y.mjs\"}" ). unwrap (); 1027assert_eq! ( 1028resolve_exports_target ( & exports, "." , & conditions ()). unwrap (), 1029Some ( "./x.js" . into ()) 1030); 1031let exports: serde_json:: Value = 1032 serde_json:: from_str ( "{\"avril\": \"./a.js\", \"import\": \"./y.mjs\"}" ). unwrap (); 1033assert_eq! ( 1034resolve_exports_target ( & exports, "." , & conditions ()). unwrap (), 1035Some ( "./a.js" . into ()) 1036); 1037} 1038 1039# [ test ] 1040fn arrays_validate_each_target_and_fall_back () { 1041let exports: serde_json:: Value = 1042 serde_json:: from_str ( "{\".\": [\"./../bad.js\", \"./good.js\"]}" ). unwrap (); 1043assert_eq! ( 1044resolve_exports_target ( & exports, "." , & conditions ()). unwrap (), 1045Some ( "./good.js" . into ()) 1046); 1047let exports: serde_json:: Value = 1048 serde_json:: from_str ( "{\".\": {\"0\": \"./x.js\", \"default\": \"./d.js\"}}" ). unwrap (); 1049assert! ( 1050resolve_exports_target ( & exports, "." , & conditions ()) 1051. unwrap_err () 1052. to_string () 1053. contains ( "condition" ) 1054); 1055} 1056 1057# [ test ] 1058fn unknown_conditions_fall_through_to_default () { 1059let exports: serde_json:: Value = serde_json:: from_str ( 1060"{\".\": {\"future-condition\": \"./u.js\", \"default\": \"./d.js\"}}" , 1061) 1062. unwrap (); 1063assert_eq! ( 1064resolve_exports_target ( & exports, "." , & conditions ()). unwrap (), 1065Some ( "./d.js" . into ()) 1066); 1067} 1068 1069# [ test ] 1070fn longest_pattern_prefix_wins_ties_by_suffix () { 1071let exports: serde_json:: Value = serde_json:: from_str ( 1072"{\"./features/*.js\": \"./a/*.js\", \"./features/x/*.js\": \"./b/*.js\"}" , 1073) 1074. unwrap (); 1075assert_eq! ( 1076resolve_exports_target ( & exports, "./features/x/y.js" , & conditions ()). unwrap (), 1077Some ( "./b/y.js" . into ()) 1078); 1079} 1080 1081# [ test ] 1082fn mixed_exports_are_rejected () { 1083let exports: serde_json:: Value = 1084 serde_json:: from_str ( "{\".\": \"./x.js\", \"import\": \"./y.mjs\"}" ). unwrap (); 1085assert! ( 1086resolve_exports_target ( & exports, "." , & conditions ()) 1087. unwrap_err () 1088. to_string () 1089. contains ( "mixes" ) 1090); 1091} 1092 1093# [ test ] 1094fn platform_negations_behave () { 1095let rule =parse_platform_rule ( & serde_json:: Value :: String ( "!arm64" . into ())). unwrap (); 1096assert! ( platform_satisfied ( Some ( & rule), "x64" )); 1097assert! ( !platform_satisfied ( Some ( & rule), "arm64" )); 1098} 1099}