github.com/evanw/esbuild@v0.21.4/internal/resolver/package_json.go (about) 1 package resolver 2 3 import ( 4 "fmt" 5 "net/url" 6 "path" 7 "regexp" 8 "sort" 9 "strings" 10 11 "github.com/evanw/esbuild/internal/config" 12 "github.com/evanw/esbuild/internal/helpers" 13 "github.com/evanw/esbuild/internal/js_ast" 14 "github.com/evanw/esbuild/internal/js_lexer" 15 "github.com/evanw/esbuild/internal/js_parser" 16 "github.com/evanw/esbuild/internal/logger" 17 ) 18 19 type packageJSON struct { 20 name string 21 mainFields map[string]mainField 22 moduleTypeData js_ast.ModuleTypeData 23 24 // "TypeScript will first check whether package.json contains a "tsconfig" 25 // field, and if it does, TypeScript will try to load a configuration file 26 // from that field. If neither exists, TypeScript will try to read from a 27 // tsconfig.json at the root." 28 // 29 // See: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-2.html#tsconfigjson-inheritance-via-nodejs-packages 30 tsconfig string 31 32 // Present if the "browser" field is present. This field is intended to be 33 // used by bundlers and lets you redirect the paths of certain 3rd-party 34 // modules that don't work in the browser to other modules that shim that 35 // functionality. That way you don't have to rewrite the code for those 3rd- 36 // party modules. For example, you might remap the native "util" node module 37 // to something like https://www.npmjs.com/package/util so it works in the 38 // browser. 39 // 40 // This field contains the original mapping object in "package.json". Mapping 41 // to a nil path indicates that the module is disabled. As far as I can 42 // tell, the official spec is an abandoned GitHub repo hosted by a user account: 43 // https://github.com/defunctzombie/package-browser-field-spec. The npm docs 44 // say almost nothing: https://docs.npmjs.com/files/package.json. 45 // 46 // Note that the non-package "browser" map has to be checked twice to match 47 // Webpack's behavior: once before resolution and once after resolution. It 48 // leads to some unintuitive failure cases that we must emulate around missing 49 // file extensions: 50 // 51 // * Given the mapping "./no-ext": "./no-ext-browser.js" the query "./no-ext" 52 // should match but the query "./no-ext.js" should NOT match. 53 // 54 // * Given the mapping "./ext.js": "./ext-browser.js" the query "./ext.js" 55 // should match and the query "./ext" should ALSO match. 56 // 57 browserMap map[string]*string 58 59 // If this is non-nil, each entry in this map is the absolute path of a file 60 // with side effects. Any entry not in this map should be considered to have 61 // no side effects, which means import statements for these files can be 62 // removed if none of the imports are used. This is a convention from Webpack: 63 // https://webpack.js.org/guides/tree-shaking/. 64 // 65 // Note that if a file is included, all statements that can't be proven to be 66 // free of side effects must be included. This convention does not say 67 // anything about whether any statements within the file have side effects or 68 // not. 69 sideEffectsMap map[string]bool 70 sideEffectsRegexps []*regexp.Regexp 71 sideEffectsData *SideEffectsData 72 73 // This represents the "imports" field in this package.json file. 74 importsMap *pjMap 75 76 // This represents the "exports" field in this package.json file. 77 exportsMap *pjMap 78 79 source logger.Source 80 } 81 82 type mainField struct { 83 relPath string 84 keyLoc logger.Loc 85 } 86 87 type browserPathKind uint8 88 89 const ( 90 absolutePathKind browserPathKind = iota 91 packagePathKind 92 ) 93 94 func (r resolverQuery) checkBrowserMap(resolveDirInfo *dirInfo, inputPath string, kind browserPathKind) (remapped *string, ok bool) { 95 // This only applies if the current platform is "browser" 96 if r.options.Platform != config.PlatformBrowser { 97 return nil, false 98 } 99 100 // There must be an enclosing directory with a "package.json" file with a "browser" map 101 if resolveDirInfo.enclosingBrowserScope == nil { 102 if r.debugLogs != nil { 103 r.debugLogs.addNote(fmt.Sprintf("No \"browser\" map found in directory %q", resolveDirInfo.absPath)) 104 } 105 return nil, false 106 } 107 108 packageJSON := resolveDirInfo.enclosingBrowserScope.packageJSON 109 browserMap := packageJSON.browserMap 110 111 type implicitExtensions uint8 112 113 const ( 114 includeImplicitExtensions implicitExtensions = iota 115 skipImplicitExtensions 116 ) 117 118 checkPath := func(pathToCheck string, implicitExtensions implicitExtensions) bool { 119 if r.debugLogs != nil { 120 r.debugLogs.addNote(fmt.Sprintf("Checking for %q in the \"browser\" map in %q", 121 pathToCheck, packageJSON.source.KeyPath.Text)) 122 } 123 124 // Check for equality 125 if r.debugLogs != nil { 126 r.debugLogs.addNote(fmt.Sprintf(" Checking for %q", pathToCheck)) 127 } 128 remapped, ok = browserMap[pathToCheck] 129 if ok { 130 inputPath = pathToCheck 131 return true 132 } 133 134 // If that failed, try adding implicit extensions 135 if implicitExtensions == includeImplicitExtensions { 136 for _, ext := range r.options.ExtensionOrder { 137 extPath := pathToCheck + ext 138 if r.debugLogs != nil { 139 r.debugLogs.addNote(fmt.Sprintf(" Checking for %q", extPath)) 140 } 141 remapped, ok = browserMap[extPath] 142 if ok { 143 inputPath = extPath 144 return true 145 } 146 } 147 } 148 149 // If that failed, try assuming this is a directory and looking for an "index" file 150 indexPath := path.Join(pathToCheck, "index") 151 if IsPackagePath(indexPath) && !IsPackagePath(pathToCheck) { 152 indexPath = "./" + indexPath 153 } 154 155 // Check for equality 156 if r.debugLogs != nil { 157 r.debugLogs.addNote(fmt.Sprintf(" Checking for %q", indexPath)) 158 } 159 remapped, ok = browserMap[indexPath] 160 if ok { 161 inputPath = indexPath 162 return true 163 } 164 165 // If that failed, try adding implicit extensions 166 if implicitExtensions == includeImplicitExtensions { 167 for _, ext := range r.options.ExtensionOrder { 168 extPath := indexPath + ext 169 if r.debugLogs != nil { 170 r.debugLogs.addNote(fmt.Sprintf(" Checking for %q", extPath)) 171 } 172 remapped, ok = browserMap[extPath] 173 if ok { 174 inputPath = extPath 175 return true 176 } 177 } 178 } 179 180 return false 181 } 182 183 // Turn absolute paths into paths relative to the "browser" map location 184 if kind == absolutePathKind { 185 relPath, ok := r.fs.Rel(resolveDirInfo.enclosingBrowserScope.absPath, inputPath) 186 if !ok { 187 return nil, false 188 } 189 inputPath = strings.ReplaceAll(relPath, "\\", "/") 190 } 191 192 if inputPath == "." { 193 // No bundler supports remapping ".", so we don't either 194 return nil, false 195 } 196 197 // First try the import path as a package path 198 if !checkPath(inputPath, includeImplicitExtensions) && IsPackagePath(inputPath) { 199 // If a package path didn't work, try the import path as a relative path 200 switch kind { 201 case absolutePathKind: 202 checkPath("./"+inputPath, includeImplicitExtensions) 203 204 case packagePathKind: 205 // Browserify allows a browser map entry of "./pkg" to override a package 206 // path of "require('pkg')". This is weird, and arguably a bug. But we 207 // replicate this bug for compatibility. However, Browserify only allows 208 // this within the same package. It does not allow such an entry in a 209 // parent package to override this in a child package. So this behavior 210 // is disallowed if there is a "node_modules" folder in between the child 211 // package and the parent package. 212 isInSamePackage := true 213 for info := resolveDirInfo; info != nil && info != resolveDirInfo.enclosingBrowserScope; info = info.parent { 214 if info.isNodeModules { 215 isInSamePackage = false 216 break 217 } 218 } 219 if isInSamePackage { 220 relativePathPrefix := "./" 221 222 // Use the relative path from the file containing the import path to the 223 // enclosing package.json file. This includes any subdirectories within the 224 // package if there are any. 225 if relPath, ok := r.fs.Rel(resolveDirInfo.enclosingBrowserScope.absPath, resolveDirInfo.absPath); ok && relPath != "." { 226 relativePathPrefix += strings.ReplaceAll(relPath, "\\", "/") + "/" 227 } 228 229 // Browserify lets "require('pkg')" match "./pkg" but not "./pkg.js". 230 // So don't add implicit extensions specifically in this place so we 231 // match Browserify's behavior. 232 checkPath(relativePathPrefix+inputPath, skipImplicitExtensions) 233 } 234 } 235 } 236 237 if r.debugLogs != nil { 238 if ok { 239 if remapped == nil { 240 r.debugLogs.addNote(fmt.Sprintf("Found %q marked as disabled", inputPath)) 241 } else { 242 r.debugLogs.addNote(fmt.Sprintf("Found %q mapping to %q", inputPath, *remapped)) 243 } 244 } else { 245 r.debugLogs.addNote(fmt.Sprintf("Failed to find %q", inputPath)) 246 } 247 } 248 return 249 } 250 251 func (r resolverQuery) parsePackageJSON(inputPath string) *packageJSON { 252 packageJSONPath := r.fs.Join(inputPath, "package.json") 253 contents, err, originalError := r.caches.FSCache.ReadFile(r.fs, packageJSONPath) 254 if r.debugLogs != nil && originalError != nil { 255 r.debugLogs.addNote(fmt.Sprintf("Failed to read file %q: %s", packageJSONPath, originalError.Error())) 256 } 257 if err != nil { 258 r.log.AddError(nil, logger.Range{}, 259 fmt.Sprintf("Cannot read file %q: %s", 260 PrettyPath(r.fs, logger.Path{Text: packageJSONPath, Namespace: "file"}), err.Error())) 261 return nil 262 } 263 if r.debugLogs != nil { 264 r.debugLogs.addNote(fmt.Sprintf("The file %q exists", packageJSONPath)) 265 } 266 267 keyPath := logger.Path{Text: packageJSONPath, Namespace: "file"} 268 jsonSource := logger.Source{ 269 KeyPath: keyPath, 270 PrettyPath: PrettyPath(r.fs, keyPath), 271 Contents: contents, 272 } 273 tracker := logger.MakeLineColumnTracker(&jsonSource) 274 275 json, ok := r.caches.JSONCache.Parse(r.log, jsonSource, js_parser.JSONOptions{}) 276 if !ok { 277 return nil 278 } 279 280 packageJSON := &packageJSON{ 281 source: jsonSource, 282 mainFields: make(map[string]mainField), 283 } 284 285 // Read the "name" field 286 if nameJSON, _, ok := getProperty(json, "name"); ok { 287 if nameValue, ok := getString(nameJSON); ok { 288 packageJSON.name = nameValue 289 } 290 } 291 292 // Read the "type" field 293 if typeJSON, typeKeyLoc, ok := getProperty(json, "type"); ok { 294 if typeValue, ok := getString(typeJSON); ok { 295 switch typeValue { 296 case "commonjs": 297 packageJSON.moduleTypeData = js_ast.ModuleTypeData{ 298 Type: js_ast.ModuleCommonJS_PackageJSON, 299 Source: &packageJSON.source, 300 Range: jsonSource.RangeOfString(typeJSON.Loc), 301 } 302 case "module": 303 packageJSON.moduleTypeData = js_ast.ModuleTypeData{ 304 Type: js_ast.ModuleESM_PackageJSON, 305 Source: &packageJSON.source, 306 Range: jsonSource.RangeOfString(typeJSON.Loc), 307 } 308 default: 309 notes := []logger.MsgData{{Text: "The \"type\" field must be set to either \"commonjs\" or \"module\"."}} 310 kind := logger.Warning 311 312 // If someone does something like "type": "./index.d.ts" then they 313 // likely meant "types" instead of "type". Customize the message 314 // for this and hide it if it's inside a published npm package. 315 if strings.HasSuffix(typeValue, ".d.ts") { 316 notes[0] = tracker.MsgData(jsonSource.RangeOfString(typeKeyLoc), 317 "TypeScript type declarations use the \"types\" field, not the \"type\" field:") 318 notes[0].Location.Suggestion = "\"types\"" 319 if helpers.IsInsideNodeModules(jsonSource.KeyPath.Text) { 320 kind = logger.Debug 321 } 322 } 323 324 r.log.AddIDWithNotes(logger.MsgID_PackageJSON_InvalidType, kind, &tracker, jsonSource.RangeOfString(typeJSON.Loc), 325 fmt.Sprintf("%q is not a valid value for the \"type\" field", typeValue), 326 notes) 327 } 328 } else { 329 r.log.AddID(logger.MsgID_PackageJSON_InvalidType, logger.Warning, &tracker, logger.Range{Loc: typeJSON.Loc}, 330 "The value for \"type\" must be a string") 331 } 332 } 333 334 // Read the "tsconfig" field 335 if tsconfigJSON, _, ok := getProperty(json, "tsconfig"); ok { 336 if tsconfigValue, ok := getString(tsconfigJSON); ok { 337 packageJSON.tsconfig = tsconfigValue 338 } 339 } 340 341 // Read the "main" fields 342 mainFields := r.options.MainFields 343 if mainFields == nil { 344 mainFields = defaultMainFields[r.options.Platform] 345 } 346 for _, field := range mainFields { 347 if mainJSON, mainLoc, ok := getProperty(json, field); ok { 348 if main, ok := getString(mainJSON); ok && main != "" { 349 packageJSON.mainFields[field] = mainField{keyLoc: mainLoc, relPath: main} 350 } 351 } 352 } 353 for _, field := range mainFieldsForFailure { 354 if _, ok := packageJSON.mainFields[field]; !ok { 355 if mainJSON, mainLoc, ok := getProperty(json, field); ok { 356 if main, ok := getString(mainJSON); ok && main != "" { 357 packageJSON.mainFields[field] = mainField{keyLoc: mainLoc, relPath: main} 358 } 359 } 360 } 361 } 362 363 // Read the "browser" property, but only when targeting the browser 364 if browserJSON, _, ok := getProperty(json, "browser"); ok && r.options.Platform == config.PlatformBrowser { 365 // We both want the ability to have the option of CJS vs. ESM and the 366 // option of having node vs. browser. The way to do this is to use the 367 // object literal form of the "browser" field like this: 368 // 369 // "main": "dist/index.node.cjs.js", 370 // "module": "dist/index.node.esm.js", 371 // "browser": { 372 // "./dist/index.node.cjs.js": "./dist/index.browser.cjs.js", 373 // "./dist/index.node.esm.js": "./dist/index.browser.esm.js" 374 // }, 375 // 376 if browser, ok := browserJSON.Data.(*js_ast.EObject); ok { 377 // The value is an object 378 browserMap := make(map[string]*string) 379 380 // Remap all files in the browser field 381 for _, prop := range browser.Properties { 382 if key, ok := getString(prop.Key); ok && prop.ValueOrNil.Data != nil { 383 if value, ok := getString(prop.ValueOrNil); ok { 384 // If this is a string, it's a replacement package 385 browserMap[key] = &value 386 } else if value, ok := getBool(prop.ValueOrNil); ok { 387 // If this is false, it means the package is disabled 388 if !value { 389 browserMap[key] = nil 390 } 391 } else { 392 r.log.AddID(logger.MsgID_PackageJSON_InvalidBrowser, logger.Warning, &tracker, logger.Range{Loc: prop.ValueOrNil.Loc}, 393 "Each \"browser\" mapping must be a string or a boolean") 394 } 395 } 396 } 397 398 packageJSON.browserMap = browserMap 399 } 400 } 401 402 // Read the "sideEffects" property 403 if sideEffectsJSON, sideEffectsLoc, ok := getProperty(json, "sideEffects"); ok { 404 switch data := sideEffectsJSON.Data.(type) { 405 case *js_ast.EBoolean: 406 if !data.Value { 407 // Make an empty map for "sideEffects: false", which indicates all 408 // files in this module can be considered to not have side effects. 409 packageJSON.sideEffectsMap = make(map[string]bool) 410 packageJSON.sideEffectsData = &SideEffectsData{ 411 IsSideEffectsArrayInJSON: false, 412 Source: &jsonSource, 413 Range: jsonSource.RangeOfString(sideEffectsLoc), 414 } 415 } 416 417 case *js_ast.EArray: 418 // The "sideEffects: []" format means all files in this module but not in 419 // the array can be considered to not have side effects. 420 packageJSON.sideEffectsMap = make(map[string]bool) 421 packageJSON.sideEffectsData = &SideEffectsData{ 422 IsSideEffectsArrayInJSON: true, 423 Source: &jsonSource, 424 Range: jsonSource.RangeOfString(sideEffectsLoc), 425 } 426 for _, itemJSON := range data.Items { 427 item, ok := itemJSON.Data.(*js_ast.EString) 428 if !ok || item.Value == nil { 429 r.log.AddID(logger.MsgID_PackageJSON_InvalidSideEffects, logger.Warning, &tracker, logger.Range{Loc: itemJSON.Loc}, 430 "Expected string in array for \"sideEffects\"") 431 continue 432 } 433 434 // Reference: https://github.com/webpack/webpack/blob/ed175cd22f89eb9fecd0a70572a3fd0be028e77c/lib/optimize/SideEffectsFlagPlugin.js 435 pattern := helpers.UTF16ToString(item.Value) 436 if !strings.ContainsRune(pattern, '/') { 437 pattern = "**/" + pattern 438 } 439 absPattern := r.fs.Join(inputPath, pattern) 440 absPattern = strings.ReplaceAll(absPattern, "\\", "/") // Avoid problems with Windows-style slashes 441 re, hadWildcard := globstarToEscapedRegexp(absPattern) 442 443 // Wildcard patterns require more expensive matching 444 if hadWildcard { 445 packageJSON.sideEffectsRegexps = append(packageJSON.sideEffectsRegexps, regexp.MustCompile(re)) 446 continue 447 } 448 449 // Normal strings can be matched with a map lookup 450 packageJSON.sideEffectsMap[absPattern] = true 451 } 452 453 default: 454 r.log.AddID(logger.MsgID_PackageJSON_InvalidSideEffects, logger.Warning, &tracker, logger.Range{Loc: sideEffectsJSON.Loc}, 455 "The value for \"sideEffects\" must be a boolean or an array") 456 } 457 } 458 459 // Read the "imports" map 460 if importsJSON, importsLoc, ok := getProperty(json, "imports"); ok { 461 if importsMap := parseImportsExportsMap(jsonSource, r.log, importsJSON, "imports", importsLoc); importsMap != nil { 462 if importsMap.root.kind != pjObject { 463 r.log.AddID(logger.MsgID_PackageJSON_InvalidImportsOrExports, logger.Warning, &tracker, importsMap.root.firstToken, 464 "The value for \"imports\" must be an object") 465 } 466 packageJSON.importsMap = importsMap 467 } 468 } 469 470 // Read the "exports" map 471 if exportsJSON, exportsLoc, ok := getProperty(json, "exports"); ok { 472 if exportsMap := parseImportsExportsMap(jsonSource, r.log, exportsJSON, "exports", exportsLoc); exportsMap != nil { 473 packageJSON.exportsMap = exportsMap 474 } 475 } 476 477 return packageJSON 478 } 479 480 // Reference: https://github.com/fitzgen/glob-to-regexp/blob/2abf65a834259c6504ed3b80e85f893f8cd99127/index.js 481 func globstarToEscapedRegexp(glob string) (string, bool) { 482 sb := strings.Builder{} 483 sb.WriteByte('^') 484 hadWildcard := false 485 n := len(glob) 486 487 for i := 0; i < n; i++ { 488 c := glob[i] 489 switch c { 490 case '\\', '^', '$', '.', '+', '|', '(', ')', '[', ']', '{', '}': 491 sb.WriteByte('\\') 492 sb.WriteByte(c) 493 494 case '?': 495 sb.WriteByte('.') 496 hadWildcard = true 497 498 case '*': 499 // Move over all consecutive "*"'s. 500 // Also store the previous and next characters 501 prevChar := -1 502 if i > 0 { 503 prevChar = int(glob[i-1]) 504 } 505 starCount := 1 506 for i+1 < n && glob[i+1] == '*' { 507 starCount++ 508 i++ 509 } 510 nextChar := -1 511 if i+1 < n { 512 nextChar = int(glob[i+1]) 513 } 514 515 // Determine if this is a globstar segment 516 isGlobstar := starCount > 1 && // multiple "*"'s 517 (prevChar == '/' || prevChar == -1) && // from the start of the segment 518 (nextChar == '/' || nextChar == -1) // to the end of the segment 519 520 if isGlobstar { 521 // It's a globstar, so match zero or more path segments 522 sb.WriteString("(?:[^/]*(?:/|$))*") 523 i++ // Move over the "/" 524 } else { 525 // It's not a globstar, so only match one path segment 526 sb.WriteString("[^/]*") 527 } 528 529 hadWildcard = true 530 531 default: 532 sb.WriteByte(c) 533 } 534 } 535 536 sb.WriteByte('$') 537 return sb.String(), hadWildcard 538 } 539 540 // Reference: https://nodejs.org/api/esm.html#esm_resolver_algorithm_specification 541 type pjMap struct { 542 root pjEntry 543 propertyKey string 544 propertyKeyLoc logger.Loc 545 } 546 547 type pjKind uint8 548 549 const ( 550 pjNull pjKind = iota 551 pjString 552 pjArray 553 pjObject 554 pjInvalid 555 ) 556 557 type pjEntry struct { 558 strData string 559 arrData []pjEntry 560 mapData []pjMapEntry // Can't be a "map" because order matters 561 expansionKeys expansionKeysArray 562 firstToken logger.Range 563 kind pjKind 564 } 565 566 type pjMapEntry struct { 567 key string 568 value pjEntry 569 keyRange logger.Range 570 } 571 572 // This type is just so we can use Go's native sort function 573 type expansionKeysArray []pjMapEntry 574 575 func (a expansionKeysArray) Len() int { return len(a) } 576 func (a expansionKeysArray) Swap(i int, j int) { a[i], a[j] = a[j], a[i] } 577 578 func (a expansionKeysArray) Less(i int, j int) bool { 579 // Assert: keyA ends with "/" or contains only a single "*". 580 // Assert: keyB ends with "/" or contains only a single "*". 581 keyA := a[i].key 582 keyB := a[j].key 583 584 // Let baseLengthA be the index of "*" in keyA plus one, if keyA contains "*", or the length of keyA otherwise. 585 // Let baseLengthB be the index of "*" in keyB plus one, if keyB contains "*", or the length of keyB otherwise. 586 starA := strings.IndexByte(keyA, '*') 587 starB := strings.IndexByte(keyB, '*') 588 var baseLengthA int 589 var baseLengthB int 590 if starA >= 0 { 591 baseLengthA = starA 592 } else { 593 baseLengthA = len(keyA) 594 } 595 if starB >= 0 { 596 baseLengthB = starB 597 } else { 598 baseLengthB = len(keyB) 599 } 600 601 // If baseLengthA is greater than baseLengthB, return -1. 602 // If baseLengthB is greater than baseLengthA, return 1. 603 if baseLengthA > baseLengthB { 604 return true 605 } 606 if baseLengthB > baseLengthA { 607 return false 608 } 609 610 // If keyA does not contain "*", return 1. 611 // If keyB does not contain "*", return -1. 612 if starA < 0 { 613 return false 614 } 615 if starB < 0 { 616 return true 617 } 618 619 // If the length of keyA is greater than the length of keyB, return -1. 620 // If the length of keyB is greater than the length of keyA, return 1. 621 if len(keyA) > len(keyB) { 622 return true 623 } 624 if len(keyB) > len(keyA) { 625 return false 626 } 627 628 return false 629 } 630 631 func (entry pjEntry) valueForKey(key string) (pjEntry, bool) { 632 for _, item := range entry.mapData { 633 if item.key == key { 634 return item.value, true 635 } 636 } 637 return pjEntry{}, false 638 } 639 640 func parseImportsExportsMap(source logger.Source, log logger.Log, json js_ast.Expr, propertyKey string, propertyKeyLoc logger.Loc) *pjMap { 641 var visit func(expr js_ast.Expr) pjEntry 642 tracker := logger.MakeLineColumnTracker(&source) 643 644 visit = func(expr js_ast.Expr) pjEntry { 645 var firstToken logger.Range 646 647 switch e := expr.Data.(type) { 648 case *js_ast.ENull: 649 return pjEntry{ 650 kind: pjNull, 651 firstToken: js_lexer.RangeOfIdentifier(source, expr.Loc), 652 } 653 654 case *js_ast.EString: 655 return pjEntry{ 656 kind: pjString, 657 firstToken: source.RangeOfString(expr.Loc), 658 strData: helpers.UTF16ToString(e.Value), 659 } 660 661 case *js_ast.EArray: 662 arrData := make([]pjEntry, len(e.Items)) 663 for i, item := range e.Items { 664 arrData[i] = visit(item) 665 } 666 return pjEntry{ 667 kind: pjArray, 668 firstToken: logger.Range{Loc: expr.Loc, Len: 1}, 669 arrData: arrData, 670 } 671 672 case *js_ast.EObject: 673 mapData := make([]pjMapEntry, len(e.Properties)) 674 expansionKeys := make(expansionKeysArray, 0, len(e.Properties)) 675 firstToken := logger.Range{Loc: expr.Loc, Len: 1} 676 isConditionalSugar := false 677 678 for i, property := range e.Properties { 679 keyStr, _ := property.Key.Data.(*js_ast.EString) 680 key := helpers.UTF16ToString(keyStr.Value) 681 keyRange := source.RangeOfString(property.Key.Loc) 682 683 // If exports is an Object with both a key starting with "." and a key 684 // not starting with ".", throw an Invalid Package Configuration error. 685 curIsConditionalSugar := !strings.HasPrefix(key, ".") 686 if i == 0 { 687 isConditionalSugar = curIsConditionalSugar 688 } else if isConditionalSugar != curIsConditionalSugar { 689 prevEntry := mapData[i-1] 690 log.AddIDWithNotes(logger.MsgID_PackageJSON_InvalidImportsOrExports, logger.Warning, &tracker, keyRange, 691 "This object cannot contain keys that both start with \".\" and don't start with \".\"", 692 []logger.MsgData{tracker.MsgData(prevEntry.keyRange, 693 fmt.Sprintf("The key %q is incompatible with the previous key %q:", key, prevEntry.key))}) 694 return pjEntry{ 695 kind: pjInvalid, 696 firstToken: firstToken, 697 } 698 } 699 700 entry := pjMapEntry{ 701 key: key, 702 keyRange: keyRange, 703 value: visit(property.ValueOrNil), 704 } 705 706 if strings.HasSuffix(key, "/") || strings.IndexByte(key, '*') >= 0 { 707 expansionKeys = append(expansionKeys, entry) 708 } 709 710 mapData[i] = entry 711 } 712 713 // Let expansionKeys be the list of keys of matchObj either ending in "/" 714 // or containing only a single "*", sorted by the sorting function 715 // PATTERN_KEY_COMPARE which orders in descending order of specificity. 716 sort.Stable(expansionKeys) 717 718 return pjEntry{ 719 kind: pjObject, 720 firstToken: firstToken, 721 mapData: mapData, 722 expansionKeys: expansionKeys, 723 } 724 725 case *js_ast.EBoolean: 726 firstToken = js_lexer.RangeOfIdentifier(source, expr.Loc) 727 728 case *js_ast.ENumber: 729 firstToken = source.RangeOfNumber(expr.Loc) 730 731 default: 732 firstToken.Loc = expr.Loc 733 } 734 735 log.AddID(logger.MsgID_PackageJSON_InvalidImportsOrExports, logger.Warning, &tracker, firstToken, 736 "This value must be a string, an object, an array, or null") 737 return pjEntry{ 738 kind: pjInvalid, 739 firstToken: firstToken, 740 } 741 } 742 743 root := visit(json) 744 745 if root.kind == pjNull { 746 return nil 747 } 748 749 return &pjMap{ 750 root: root, 751 propertyKey: propertyKey, 752 propertyKeyLoc: propertyKeyLoc, 753 } 754 } 755 756 func (entry pjEntry) keysStartWithDot() bool { 757 return len(entry.mapData) > 0 && strings.HasPrefix(entry.mapData[0].key, ".") 758 } 759 760 type pjStatus uint8 761 762 const ( 763 pjStatusUndefined pjStatus = iota 764 pjStatusUndefinedNoConditionsMatch // A more friendly error message for when no conditions are matched 765 pjStatusNull 766 pjStatusExact 767 pjStatusExactEndsWithStar 768 pjStatusInexact // This means we may need to try CommonJS-style extension suffixes 769 pjStatusPackageResolve // Need to re-run package resolution on the result 770 771 // Module specifier is an invalid URL, package name or package subpath specifier. 772 pjStatusInvalidModuleSpecifier 773 774 // package.json configuration is invalid or contains an invalid configuration. 775 pjStatusInvalidPackageConfiguration 776 777 // Package exports or imports define a target module for the package that is an invalid type or string target. 778 pjStatusInvalidPackageTarget 779 780 // Package exports do not define or permit a target subpath in the package for the given module. 781 pjStatusPackagePathNotExported 782 783 // Package imports do not define the specifiespecifier 784 pjStatusPackageImportNotDefined 785 786 // The package or module requested does not exist. 787 pjStatusModuleNotFound 788 pjStatusModuleNotFoundMissingExtension // The user just needs to add the missing extension 789 790 // The resolved path corresponds to a directory, which is not a supported target for module imports. 791 pjStatusUnsupportedDirectoryImport 792 pjStatusUnsupportedDirectoryImportMissingIndex // The user just needs to add the missing "/index.js" suffix 793 ) 794 795 func (status pjStatus) isUndefined() bool { 796 return status == pjStatusUndefined || status == pjStatusUndefinedNoConditionsMatch 797 } 798 799 type pjDebug struct { 800 // If the status is "pjStatusInvalidPackageTarget" or "pjStatusInvalidModuleSpecifier", 801 // then this is the reason. It always starts with " because". 802 invalidBecause string 803 804 // If the status is "pjStatusUndefinedNoConditionsMatch", this is the set of 805 // conditions that didn't match, in the order that they were found in the file. 806 // This information is used for error messages. 807 unmatchedConditions []logger.Span 808 809 // This is the range of the token to use for error messages 810 token logger.Range 811 812 // If true, the token is a "null" literal 813 isBecauseOfNullLiteral bool 814 } 815 816 func (r resolverQuery) esmHandlePostConditions( 817 resolved string, 818 status pjStatus, 819 debug pjDebug, 820 ) (string, pjStatus, pjDebug) { 821 if status != pjStatusExact && status != pjStatusExactEndsWithStar && status != pjStatusInexact { 822 return resolved, status, debug 823 } 824 825 // If resolved contains any percent encodings of "/" or "\" ("%2f" and "%5C" 826 // respectively), then throw an Invalid Module Specifier error. 827 resolvedPath, err := url.PathUnescape(resolved) 828 if err != nil { 829 if r.debugLogs != nil { 830 r.debugLogs.addNote(fmt.Sprintf("The path %q contains invalid URL escapes: %s", resolved, err.Error())) 831 } 832 return resolved, pjStatusInvalidModuleSpecifier, debug 833 } 834 var found string 835 if strings.Contains(resolved, "%2f") { 836 found = "%2f" 837 } else if strings.Contains(resolved, "%2F") { 838 found = "%2F" 839 } else if strings.Contains(resolved, "%5c") { 840 found = "%5c" 841 } else if strings.Contains(resolved, "%5C") { 842 found = "%5C" 843 } 844 if found != "" { 845 if r.debugLogs != nil { 846 r.debugLogs.addNote(fmt.Sprintf("The path %q is not allowed to contain %q", resolved, found)) 847 } 848 return resolved, pjStatusInvalidModuleSpecifier, debug 849 } 850 851 // If the file at resolved is a directory, then throw an Unsupported Directory 852 // Import error. 853 if strings.HasSuffix(resolvedPath, "/") || strings.HasSuffix(resolvedPath, "\\") { 854 if r.debugLogs != nil { 855 r.debugLogs.addNote(fmt.Sprintf("The path %q is not allowed to end with a slash", resolved)) 856 } 857 return resolved, pjStatusUnsupportedDirectoryImport, debug 858 } 859 860 // Set resolved to the real path of resolved. 861 return resolvedPath, status, debug 862 } 863 864 func (r resolverQuery) esmPackageImportsResolve( 865 specifier string, 866 imports pjEntry, 867 conditions map[string]bool, 868 ) (string, pjStatus, pjDebug) { 869 // ALGORITHM DEVIATION: Provide a friendly error message if "imports" is not an object 870 if imports.kind != pjObject { 871 return "", pjStatusInvalidPackageConfiguration, pjDebug{token: imports.firstToken} 872 } 873 874 resolved, status, debug := r.esmPackageImportsExportsResolve(specifier, imports, "/", true, conditions) 875 if status != pjStatusNull && status != pjStatusUndefined { 876 return resolved, status, debug 877 } 878 879 if r.debugLogs != nil { 880 r.debugLogs.addNote(fmt.Sprintf("The package import %q is not defined", specifier)) 881 } 882 return specifier, pjStatusPackageImportNotDefined, pjDebug{token: imports.firstToken} 883 } 884 885 func (r resolverQuery) esmPackageExportsResolve( 886 packageURL string, 887 subpath string, 888 exports pjEntry, 889 conditions map[string]bool, 890 ) (string, pjStatus, pjDebug) { 891 if exports.kind == pjInvalid { 892 if r.debugLogs != nil { 893 r.debugLogs.addNote("Invalid package configuration") 894 } 895 return "", pjStatusInvalidPackageConfiguration, pjDebug{token: exports.firstToken} 896 } 897 898 debugToReturn := pjDebug{token: exports.firstToken} 899 if subpath == "." { 900 mainExport := pjEntry{kind: pjNull} 901 if exports.kind == pjString || exports.kind == pjArray || (exports.kind == pjObject && !exports.keysStartWithDot()) { 902 mainExport = exports 903 } else if exports.kind == pjObject { 904 if dot, ok := exports.valueForKey("."); ok { 905 if r.debugLogs != nil { 906 r.debugLogs.addNote("Using the entry for \".\"") 907 } 908 mainExport = dot 909 } 910 } 911 if mainExport.kind != pjNull { 912 resolved, status, debug := r.esmPackageTargetResolve(packageURL, mainExport, "", false, false, conditions) 913 if status != pjStatusNull && status != pjStatusUndefined { 914 return resolved, status, debug 915 } else { 916 debugToReturn = debug 917 } 918 } 919 } else if exports.kind == pjObject && exports.keysStartWithDot() { 920 resolved, status, debug := r.esmPackageImportsExportsResolve(subpath, exports, packageURL, false, conditions) 921 if status != pjStatusNull && status != pjStatusUndefined { 922 return resolved, status, debug 923 } else { 924 debugToReturn = debug 925 } 926 } 927 928 if r.debugLogs != nil { 929 r.debugLogs.addNote(fmt.Sprintf("The path %q is not exported", subpath)) 930 } 931 return "", pjStatusPackagePathNotExported, debugToReturn 932 } 933 934 func (r resolverQuery) esmPackageImportsExportsResolve( 935 matchKey string, 936 matchObj pjEntry, 937 packageURL string, 938 isImports bool, 939 conditions map[string]bool, 940 ) (string, pjStatus, pjDebug) { 941 if r.debugLogs != nil { 942 r.debugLogs.addNote(fmt.Sprintf("Checking object path map for %q", matchKey)) 943 } 944 945 // If matchKey is a key of matchObj and does not end in "/" or contain "*", then 946 if !strings.HasSuffix(matchKey, "/") && strings.IndexByte(matchKey, '*') < 0 { 947 if target, ok := matchObj.valueForKey(matchKey); ok { 948 if r.debugLogs != nil { 949 r.debugLogs.addNote(fmt.Sprintf("Found exact match for %q", matchKey)) 950 } 951 return r.esmPackageTargetResolve(packageURL, target, "", false, isImports, conditions) 952 } 953 } 954 955 for _, expansion := range matchObj.expansionKeys { 956 // If expansionKey contains "*", set patternBase to the substring of 957 // expansionKey up to but excluding the first "*" character 958 if star := strings.IndexByte(expansion.key, '*'); star >= 0 { 959 patternBase := expansion.key[:star] 960 961 // If patternBase is not null and matchKey starts with but is not equal 962 // to patternBase, then 963 if strings.HasPrefix(matchKey, patternBase) { 964 // Let patternTrailer be the substring of expansionKey from the index 965 // after the first "*" character. 966 patternTrailer := expansion.key[star+1:] 967 968 // If patternTrailer has zero length, or if matchKey ends with 969 // patternTrailer and the length of matchKey is greater than or 970 // equal to the length of expansionKey, then 971 if patternTrailer == "" || (strings.HasSuffix(matchKey, patternTrailer) && len(matchKey) >= len(expansion.key)) { 972 target := expansion.value 973 subpath := matchKey[len(patternBase) : len(matchKey)-len(patternTrailer)] 974 if r.debugLogs != nil { 975 r.debugLogs.addNote(fmt.Sprintf("The key %q matched with %q left over", expansion.key, subpath)) 976 } 977 return r.esmPackageTargetResolve(packageURL, target, subpath, true, isImports, conditions) 978 } 979 } 980 } else { 981 // Otherwise if patternBase is null and matchKey starts with 982 // expansionKey, then 983 if strings.HasPrefix(matchKey, expansion.key) { 984 target := expansion.value 985 subpath := matchKey[len(expansion.key):] 986 if r.debugLogs != nil { 987 r.debugLogs.addNote(fmt.Sprintf("The key %q matched with %q left over", expansion.key, subpath)) 988 } 989 result, status, debug := r.esmPackageTargetResolve(packageURL, target, subpath, false, isImports, conditions) 990 if status == pjStatusExact || status == pjStatusExactEndsWithStar { 991 // Return the object { resolved, exact: false }. 992 status = pjStatusInexact 993 } 994 return result, status, debug 995 } 996 } 997 998 if r.debugLogs != nil { 999 r.debugLogs.addNote(fmt.Sprintf("The key %q did not match", expansion.key)) 1000 } 1001 } 1002 1003 if r.debugLogs != nil { 1004 r.debugLogs.addNote(fmt.Sprintf("No keys matched %q", matchKey)) 1005 } 1006 return "", pjStatusNull, pjDebug{token: matchObj.firstToken} 1007 } 1008 1009 // If path split on "/" or "\" contains any ".", ".." or "node_modules" 1010 // segments after the first segment, throw an Invalid Package Target error. 1011 func findInvalidSegment(path string) string { 1012 slash := strings.IndexAny(path, "/\\") 1013 if slash == -1 { 1014 return "" 1015 } 1016 path = path[slash+1:] 1017 for path != "" { 1018 slash := strings.IndexAny(path, "/\\") 1019 segment := path 1020 if slash != -1 { 1021 segment = path[:slash] 1022 path = path[slash+1:] 1023 } else { 1024 path = "" 1025 } 1026 if segment == "." || segment == ".." || segment == "node_modules" { 1027 return segment 1028 } 1029 } 1030 return "" 1031 } 1032 1033 func (r resolverQuery) esmPackageTargetResolve( 1034 packageURL string, 1035 target pjEntry, 1036 subpath string, 1037 pattern bool, 1038 internal bool, 1039 conditions map[string]bool, 1040 ) (string, pjStatus, pjDebug) { 1041 switch target.kind { 1042 case pjString: 1043 if r.debugLogs != nil { 1044 r.debugLogs.addNote(fmt.Sprintf("Checking path %q against target %q", subpath, target.strData)) 1045 r.debugLogs.increaseIndent() 1046 defer r.debugLogs.decreaseIndent() 1047 } 1048 1049 // If pattern is false, subpath has non-zero length and target 1050 // does not end with "/", throw an Invalid Module Specifier error. 1051 if !pattern && subpath != "" && !strings.HasSuffix(target.strData, "/") { 1052 if r.debugLogs != nil { 1053 r.debugLogs.addNote(fmt.Sprintf("The target %q is invalid because it doesn't end in \"/\"", target.strData)) 1054 } 1055 return target.strData, pjStatusInvalidModuleSpecifier, pjDebug{ 1056 token: target.firstToken, 1057 invalidBecause: " because it doesn't end in \"/\"", 1058 } 1059 } 1060 1061 // If target does not start with "./", then... 1062 if !strings.HasPrefix(target.strData, "./") { 1063 if internal && !strings.HasPrefix(target.strData, "../") && !strings.HasPrefix(target.strData, "/") { 1064 if pattern { 1065 result := strings.ReplaceAll(target.strData, "*", subpath) 1066 if r.debugLogs != nil { 1067 r.debugLogs.addNote(fmt.Sprintf("Substituted %q for \"*\" in %q to get %q", subpath, target.strData, result)) 1068 } 1069 return result, pjStatusPackageResolve, pjDebug{token: target.firstToken} 1070 } 1071 result := target.strData + subpath 1072 if r.debugLogs != nil { 1073 r.debugLogs.addNote(fmt.Sprintf("Joined %q to %q to get %q", target.strData, subpath, result)) 1074 } 1075 return result, pjStatusPackageResolve, pjDebug{token: target.firstToken} 1076 } 1077 if r.debugLogs != nil { 1078 r.debugLogs.addNote(fmt.Sprintf("The target %q is invalid because it doesn't start with \"./\"", target.strData)) 1079 } 1080 return target.strData, pjStatusInvalidPackageTarget, pjDebug{ 1081 token: target.firstToken, 1082 invalidBecause: " because it doesn't start with \"./\"", 1083 } 1084 } 1085 1086 // If target split on "/" or "\" contains any ".", ".." or "node_modules" 1087 // segments after the first segment, throw an Invalid Package Target error. 1088 if invalidSegment := findInvalidSegment(target.strData); invalidSegment != "" { 1089 if r.debugLogs != nil { 1090 r.debugLogs.addNote(fmt.Sprintf("The target %q is invalid because it contains invalid segment %q", target.strData, invalidSegment)) 1091 } 1092 return target.strData, pjStatusInvalidPackageTarget, pjDebug{ 1093 token: target.firstToken, 1094 invalidBecause: fmt.Sprintf(" because it contains invalid segment %q", invalidSegment), 1095 } 1096 } 1097 1098 // Let resolvedTarget be the URL resolution of the concatenation of packageURL and target. 1099 resolvedTarget := path.Join(packageURL, target.strData) 1100 1101 // If subpath split on "/" or "\" contains any ".", ".." or "node_modules" 1102 // segments, throw an Invalid Module Specifier error. 1103 if invalidSegment := findInvalidSegment(subpath); invalidSegment != "" { 1104 if r.debugLogs != nil { 1105 r.debugLogs.addNote(fmt.Sprintf("The path %q is invalid because it contains invalid segment %q", subpath, invalidSegment)) 1106 } 1107 return subpath, pjStatusInvalidModuleSpecifier, pjDebug{ 1108 token: target.firstToken, 1109 invalidBecause: fmt.Sprintf(" because it contains invalid segment %q", invalidSegment), 1110 } 1111 } 1112 1113 if pattern { 1114 // Return the URL resolution of resolvedTarget with every instance of "*" replaced with subpath. 1115 result := strings.ReplaceAll(resolvedTarget, "*", subpath) 1116 if r.debugLogs != nil { 1117 r.debugLogs.addNote(fmt.Sprintf("Substituted %q for \"*\" in %q to get %q", subpath, "."+resolvedTarget, "."+result)) 1118 } 1119 status := pjStatusExact 1120 if strings.HasSuffix(resolvedTarget, "*") && strings.IndexByte(resolvedTarget, '*') == len(resolvedTarget)-1 { 1121 status = pjStatusExactEndsWithStar 1122 } 1123 return result, status, pjDebug{token: target.firstToken} 1124 } else { 1125 // Return the URL resolution of the concatenation of subpath and resolvedTarget. 1126 result := path.Join(resolvedTarget, subpath) 1127 if r.debugLogs != nil { 1128 r.debugLogs.addNote(fmt.Sprintf("Joined %q to %q to get %q", subpath, "."+resolvedTarget, "."+result)) 1129 } 1130 return result, pjStatusExact, pjDebug{token: target.firstToken} 1131 } 1132 1133 case pjObject: 1134 if r.debugLogs != nil { 1135 keys := make([]string, 0, len(conditions)) 1136 for key := range conditions { 1137 keys = append(keys, fmt.Sprintf("%q", key)) 1138 } 1139 sort.Strings(keys) 1140 r.debugLogs.addNote(fmt.Sprintf("Checking condition map for one of [%s]", strings.Join(keys, ", "))) 1141 r.debugLogs.increaseIndent() 1142 defer r.debugLogs.decreaseIndent() 1143 } 1144 1145 var didFindMapEntry bool 1146 var lastMapEntry pjMapEntry 1147 1148 for _, p := range target.mapData { 1149 if p.key == "default" || conditions[p.key] { 1150 if r.debugLogs != nil { 1151 r.debugLogs.addNote(fmt.Sprintf("The key %q applies", p.key)) 1152 } 1153 resolved, status, debug := r.esmPackageTargetResolve(packageURL, p.value, subpath, pattern, internal, conditions) 1154 if status.isUndefined() { 1155 didFindMapEntry = true 1156 lastMapEntry = p 1157 continue 1158 } 1159 return resolved, status, debug 1160 } 1161 if r.debugLogs != nil { 1162 r.debugLogs.addNote(fmt.Sprintf("The key %q does not apply", p.key)) 1163 } 1164 } 1165 1166 if r.debugLogs != nil { 1167 r.debugLogs.addNote("No keys in the map were applicable") 1168 } 1169 1170 // ALGORITHM DEVIATION: Provide a friendly error message if no conditions matched 1171 if len(target.mapData) > 0 && !target.keysStartWithDot() { 1172 if didFindMapEntry && lastMapEntry.value.kind == pjObject && 1173 len(lastMapEntry.value.mapData) > 0 && !lastMapEntry.value.keysStartWithDot() { 1174 // If a top-level condition did match but no sub-condition matched, 1175 // complain about the sub-condition instead of the top-level condition. 1176 // This leads to a less confusing error message. For example: 1177 // 1178 // "exports": { 1179 // "node": { 1180 // "require": "./dist/bwip-js-node.js" 1181 // } 1182 // }, 1183 // 1184 // We want the warning to say this: 1185 // 1186 // note: None of the conditions in the package definition ("require") match any of the 1187 // currently active conditions ("default", "import", "node") 1188 // 14 | "node": { 1189 // | ^ 1190 // 1191 // We don't want the warning to say this: 1192 // 1193 // note: None of the conditions in the package definition ("browser", "electron", "node") 1194 // match any of the currently active conditions ("default", "import", "node") 1195 // 7 | "exports": { 1196 // | ^ 1197 // 1198 // More information: https://github.com/evanw/esbuild/issues/1484 1199 target = lastMapEntry.value 1200 } 1201 keys := make([]logger.Span, len(target.mapData)) 1202 for i, p := range target.mapData { 1203 keys[i] = logger.Span{Text: p.key, Range: p.keyRange} 1204 } 1205 return "", pjStatusUndefinedNoConditionsMatch, pjDebug{ 1206 token: target.firstToken, 1207 unmatchedConditions: keys, 1208 } 1209 } 1210 1211 return "", pjStatusUndefined, pjDebug{token: target.firstToken} 1212 1213 case pjArray: 1214 if len(target.arrData) == 0 { 1215 if r.debugLogs != nil { 1216 r.debugLogs.addNote(fmt.Sprintf("The path %q is set to an empty array", subpath)) 1217 } 1218 return "", pjStatusNull, pjDebug{token: target.firstToken} 1219 } 1220 if r.debugLogs != nil { 1221 r.debugLogs.addNote(fmt.Sprintf("Checking for %q in an array", subpath)) 1222 r.debugLogs.increaseIndent() 1223 defer r.debugLogs.decreaseIndent() 1224 } 1225 lastException := pjStatusUndefined 1226 lastDebug := pjDebug{token: target.firstToken} 1227 for _, targetValue := range target.arrData { 1228 // Let resolved be the result, continuing the loop on any Invalid Package Target error. 1229 resolved, status, debug := r.esmPackageTargetResolve(packageURL, targetValue, subpath, pattern, internal, conditions) 1230 if status == pjStatusInvalidPackageTarget || status == pjStatusNull { 1231 lastException = status 1232 lastDebug = debug 1233 continue 1234 } 1235 if status.isUndefined() { 1236 continue 1237 } 1238 return resolved, status, debug 1239 } 1240 1241 // Return or throw the last fallback resolution null return or error. 1242 return "", lastException, lastDebug 1243 1244 case pjNull: 1245 if r.debugLogs != nil { 1246 r.debugLogs.addNote(fmt.Sprintf("The path %q is set to null", subpath)) 1247 } 1248 return "", pjStatusNull, pjDebug{token: target.firstToken, isBecauseOfNullLiteral: true} 1249 } 1250 1251 if r.debugLogs != nil { 1252 r.debugLogs.addNote(fmt.Sprintf("Invalid package target for path %q", subpath)) 1253 } 1254 return "", pjStatusInvalidPackageTarget, pjDebug{token: target.firstToken} 1255 } 1256 1257 func esmParsePackageName(packageSpecifier string) (packageName string, packageSubpath string, ok bool) { 1258 if packageSpecifier == "" { 1259 return 1260 } 1261 1262 slash := strings.IndexByte(packageSpecifier, '/') 1263 if !strings.HasPrefix(packageSpecifier, "@") { 1264 if slash == -1 { 1265 slash = len(packageSpecifier) 1266 } 1267 packageName = packageSpecifier[:slash] 1268 } else { 1269 if slash == -1 { 1270 return 1271 } 1272 slash2 := strings.IndexByte(packageSpecifier[slash+1:], '/') 1273 if slash2 == -1 { 1274 slash2 = len(packageSpecifier[slash+1:]) 1275 } 1276 packageName = packageSpecifier[:slash+1+slash2] 1277 } 1278 1279 if strings.HasPrefix(packageName, ".") || strings.ContainsAny(packageName, "\\%") { 1280 return 1281 } 1282 1283 packageSubpath = "." + packageSpecifier[len(packageName):] 1284 ok = true 1285 return 1286 } 1287 1288 func (r resolverQuery) esmPackageExportsReverseResolve( 1289 query string, 1290 root pjEntry, 1291 conditions map[string]bool, 1292 ) (bool, string, logger.Range) { 1293 if root.kind == pjObject && root.keysStartWithDot() { 1294 if ok, subpath, token := r.esmPackageImportsExportsReverseResolve(query, root, conditions); ok { 1295 return true, subpath, token 1296 } 1297 } 1298 1299 return false, "", logger.Range{} 1300 } 1301 1302 func (r resolverQuery) esmPackageImportsExportsReverseResolve( 1303 query string, 1304 matchObj pjEntry, 1305 conditions map[string]bool, 1306 ) (bool, string, logger.Range) { 1307 if !strings.HasSuffix(query, "*") { 1308 for _, entry := range matchObj.mapData { 1309 if ok, subpath, token := r.esmPackageTargetReverseResolve(query, entry.key, entry.value, esmReverseExact, conditions); ok { 1310 return true, subpath, token 1311 } 1312 } 1313 } 1314 1315 for _, expansion := range matchObj.expansionKeys { 1316 if strings.HasSuffix(expansion.key, "*") { 1317 if ok, subpath, token := r.esmPackageTargetReverseResolve(query, expansion.key, expansion.value, esmReversePattern, conditions); ok { 1318 return true, subpath, token 1319 } 1320 } 1321 1322 if ok, subpath, token := r.esmPackageTargetReverseResolve(query, expansion.key, expansion.value, esmReversePrefix, conditions); ok { 1323 return true, subpath, token 1324 } 1325 } 1326 1327 return false, "", logger.Range{} 1328 } 1329 1330 type esmReverseKind uint8 1331 1332 const ( 1333 esmReverseExact esmReverseKind = iota 1334 esmReversePattern 1335 esmReversePrefix 1336 ) 1337 1338 func (r resolverQuery) esmPackageTargetReverseResolve( 1339 query string, 1340 key string, 1341 target pjEntry, 1342 kind esmReverseKind, 1343 conditions map[string]bool, 1344 ) (bool, string, logger.Range) { 1345 switch target.kind { 1346 case pjString: 1347 switch kind { 1348 case esmReverseExact: 1349 if query == target.strData { 1350 return true, key, target.firstToken 1351 } 1352 1353 case esmReversePrefix: 1354 if strings.HasPrefix(query, target.strData) { 1355 return true, key + query[len(target.strData):], target.firstToken 1356 } 1357 1358 case esmReversePattern: 1359 star := strings.IndexByte(target.strData, '*') 1360 keyWithoutTrailingStar := strings.TrimSuffix(key, "*") 1361 1362 // Handle the case of no "*" 1363 if star == -1 { 1364 if query == target.strData { 1365 return true, keyWithoutTrailingStar, target.firstToken 1366 } 1367 break 1368 } 1369 1370 // Only support tracing through a single "*" 1371 prefix := target.strData[0:star] 1372 suffix := target.strData[star+1:] 1373 if !strings.ContainsRune(suffix, '*') && strings.HasPrefix(query, prefix) { 1374 if afterPrefix := query[len(prefix):]; strings.HasSuffix(afterPrefix, suffix) { 1375 starData := afterPrefix[:len(afterPrefix)-len(suffix)] 1376 return true, keyWithoutTrailingStar + starData, target.firstToken 1377 } 1378 } 1379 } 1380 1381 case pjObject: 1382 for _, p := range target.mapData { 1383 if p.key == "default" || conditions[p.key] { 1384 if ok, subpath, token := r.esmPackageTargetReverseResolve(query, key, p.value, kind, conditions); ok { 1385 return true, subpath, token 1386 } 1387 } 1388 } 1389 1390 case pjArray: 1391 for _, targetValue := range target.arrData { 1392 if ok, subpath, token := r.esmPackageTargetReverseResolve(query, key, targetValue, kind, conditions); ok { 1393 return true, subpath, token 1394 } 1395 } 1396 } 1397 1398 return false, "", logger.Range{} 1399 }