1package resolver 2 3import ( 4 "errors" 5 "fmt" 6 "path" 7 "sort" 8 "strings" 9 "sync" 10 "syscall" 11 12 "github.com/evanw/esbuild/internal/ast" 13 "github.com/evanw/esbuild/internal/cache" 14 "github.com/evanw/esbuild/internal/compat" 15 "github.com/evanw/esbuild/internal/config" 16 "github.com/evanw/esbuild/internal/fs" 17 "github.com/evanw/esbuild/internal/helpers" 18 "github.com/evanw/esbuild/internal/js_ast" 19 "github.com/evanw/esbuild/internal/js_lexer" 20 "github.com/evanw/esbuild/internal/js_printer" 21 "github.com/evanw/esbuild/internal/logger" 22) 23 24var defaultMainFields = map[config.Platform][]string{ 25 // Note that this means if a package specifies "main", "module", and 26 // "browser" then "browser" will win out over "module". This is the 27 // same behavior as webpack: https://github.com/webpack/webpack/issues/4674. 28 // 29 // This is deliberate because the presence of the "browser" field is a 30 // good signal that the "module" field may have non-browser stuff in it, 31 // which will crash or fail to be bundled when targeting the browser. 32 config.PlatformBrowser: {"browser", "module", "main"}, 33 34 // Note that this means if a package specifies "module" and "main", the ES6 35 // module will not be selected. This means tree shaking will not work when 36 // targeting node environments. 37 // 38 // This is unfortunately necessary for compatibility. Some packages 39 // incorrectly treat the "module" field as "code for the browser". It 40 // actually means "code for ES6 environments" which includes both node 41 // and the browser. 42 // 43 // For example, the package "@firebase/app" prints a warning on startup about 44 // the bundler incorrectly using code meant for the browser if the bundler 45 // selects the "module" field instead of the "main" field. 46 // 47 // If you want to enable tree shaking when targeting node, you will have to 48 // configure the main fields to be "module" and then "main". Keep in mind 49 // that some packages may break if you do this. 50 config.PlatformNode: {"main", "module"}, 51 52 // The neutral platform is for people that don't want esbuild to try to 53 // pick good defaults for their platform. In that case, the list of main 54 // fields is empty by default. You must explicitly configure it yourself. 55 config.PlatformNeutral: {}, 56} 57 58// These are the main fields to use when the "main fields" setting is configured 59// to something unusual, such as something without the "main" field. 60var mainFieldsForFailure = []string{"main", "module"} 61 62// Path resolution is a mess. One tricky issue is the "module" override for the 63// "main" field in "package.json" files. Bundlers generally prefer "module" over 64// "main" but that breaks packages that export a function in "main" for use with 65// "require()", since resolving to "module" means an object will be returned. We 66// attempt to handle this automatically by having import statements resolve to 67// "module" but switch that out later for "main" if "require()" is used too. 68type PathPair struct { 69 // Either secondary will be empty, or primary will be "module" and secondary 70 // will be "main" 71 Primary logger.Path 72 Secondary logger.Path 73} 74 75func (pp *PathPair) iter() []*logger.Path { 76 result := []*logger.Path{&pp.Primary, &pp.Secondary} 77 if !pp.HasSecondary() { 78 result = result[:1] 79 } 80 return result 81} 82 83func (pp *PathPair) HasSecondary() bool { 84 return pp.Secondary.Text != "" 85} 86 87type SideEffectsData struct { 88 Source *logger.Source 89 Range logger.Range 90 91 // If non-empty, this false value came from a plugin 92 PluginName string 93 94 // If true, "sideEffects" was an array. If false, "sideEffects" was false. 95 IsSideEffectsArrayInJSON bool 96} 97 98type ResolveResult struct { 99 PathPair PathPair 100 101 // If this was resolved by a plugin, the plugin gets to store its data here 102 PluginData interface{} 103 104 // If not empty, these should override the default values 105 JSXFactory []string // Default if empty: "React.createElement" 106 JSXFragment []string // Default if empty: "React.Fragment" 107 108 DifferentCase *fs.DifferentCase 109 110 // If present, any ES6 imports to this file can be considered to have no side 111 // effects. This means they should be removed if unused. 112 PrimarySideEffectsData *SideEffectsData 113 114 TSTarget *config.TSTarget 115 116 IsExternal bool 117 118 // If true, the class field transform should use Object.defineProperty(). 119 UseDefineForClassFieldsTS config.MaybeBool 120 121 // This is the "importsNotUsedAsValues" and "preserveValueImports" fields from "package.json" 122 UnusedImportsTS config.UnusedImportsTS 123 124 // This is the "type" field from "package.json" 125 ModuleType js_ast.ModuleType 126} 127 128type DebugMeta struct { 129 notes []logger.MsgData 130 suggestionText string 131 suggestionMessage string 132} 133 134func (dm DebugMeta) LogErrorMsg(log logger.Log, source *logger.Source, r logger.Range, text string, notes []logger.MsgData) { 135 tracker := logger.MakeLineColumnTracker(source) 136 137 if source != nil && dm.suggestionMessage != "" { 138 data := tracker.MsgData(r, dm.suggestionMessage) 139 data.Location.Suggestion = dm.suggestionText 140 dm.notes = append(dm.notes, data) 141 } 142 143 msg := logger.Msg{ 144 Kind: logger.Error, 145 Data: tracker.MsgData(r, text), 146 Notes: append(dm.notes, notes...), 147 } 148 149 log.AddMsg(msg) 150} 151 152type Resolver interface { 153 Resolve(sourceDir string, importPath string, kind ast.ImportKind) (result *ResolveResult, debug DebugMeta) 154 ResolveAbs(absPath string) *ResolveResult 155 PrettyPath(path logger.Path) string 156 157 // This tries to run "Resolve" on a package path as a relative path. If 158 // successful, the user just forgot a leading "./" in front of the path. 159 ProbeResolvePackageAsRelative(sourceDir string, importPath string, kind ast.ImportKind) *ResolveResult 160} 161 162type resolver struct { 163 fs fs.FS 164 log logger.Log 165 caches *cache.CacheSet 166 options config.Options 167 168 // These are sets that represent various conditions for the "exports" field 169 // in package.json. 170 esmConditionsDefault map[string]bool 171 esmConditionsImport map[string]bool 172 esmConditionsRequire map[string]bool 173 174 // A special filtered import order for CSS "@import" imports. 175 // 176 // The "resolve extensions" setting determines the order of implicit 177 // extensions to try when resolving imports with the extension omitted. 178 // Sometimes people create a JavaScript/TypeScript file and a CSS file with 179 // the same name when they create a component. At a high level, users expect 180 // implicit extensions to resolve to the JS file when being imported from JS 181 // and to resolve to the CSS file when being imported from CSS. 182 // 183 // Different bundlers handle this in different ways. Parcel handles this by 184 // having the resolver prefer the same extension as the importing file in 185 // front of the configured "resolve extensions" order. Webpack's "css-loader" 186 // plugin just explicitly configures a special "resolve extensions" order 187 // consisting of only ".css" for CSS files. 188 // 189 // It's unclear what behavior is best here. What we currently do is to create 190 // a special filtered version of the configured "resolve extensions" order 191 // for CSS files that filters out any extension that has been explicitly 192 // configured with a non-CSS loader. This still gives users control over the 193 // order but avoids the scenario where we match an import in a CSS file to a 194 // JavaScript-related file. It's probably not perfect with plugins in the 195 // picture but it's better than some alternatives and probably pretty good. 196 atImportExtensionOrder []string 197 198 // This mutex serves two purposes. First of all, it guards access to "dirCache" 199 // which is potentially mutated during path resolution. But this mutex is also 200 // necessary for performance. The "React admin" benchmark mysteriously runs 201 // twice as fast when this mutex is locked around the whole resolve operation 202 // instead of around individual accesses to "dirCache". For some reason, 203 // reducing parallelism in the resolver helps the rest of the bundler go 204 // faster. I'm not sure why this is but please don't change this unless you 205 // do a lot of testing with various benchmarks and there aren't any regressions. 206 mutex sync.Mutex 207 208 // This cache maps a directory path to information about that directory and 209 // all parent directories 210 dirCache map[string]*dirInfo 211} 212 213type resolverQuery struct { 214 *resolver 215 debugMeta *DebugMeta 216 debugLogs *debugLogs 217 kind ast.ImportKind 218} 219 220func NewResolver(fs fs.FS, log logger.Log, caches *cache.CacheSet, options config.Options) Resolver { 221 // Filter out non-CSS extensions for CSS "@import" imports 222 atImportExtensionOrder := make([]string, 0, len(options.ExtensionOrder)) 223 for _, ext := range options.ExtensionOrder { 224 if loader, ok := options.ExtensionToLoader[ext]; ok && loader != config.LoaderCSS { 225 continue 226 } 227 atImportExtensionOrder = append(atImportExtensionOrder, ext) 228 } 229 230 // Generate the condition sets for interpreting the "exports" field 231 esmConditionsDefault := map[string]bool{"default": true} 232 esmConditionsImport := map[string]bool{"import": true} 233 esmConditionsRequire := map[string]bool{"require": true} 234 for _, condition := range options.Conditions { 235 esmConditionsDefault[condition] = true 236 } 237 switch options.Platform { 238 case config.PlatformBrowser: 239 esmConditionsDefault["browser"] = true 240 case config.PlatformNode: 241 esmConditionsDefault["node"] = true 242 } 243 for key := range esmConditionsDefault { 244 esmConditionsImport[key] = true 245 esmConditionsRequire[key] = true 246 } 247 248 return &resolver{ 249 fs: fs, 250 log: log, 251 options: options, 252 caches: caches, 253 dirCache: make(map[string]*dirInfo), 254 atImportExtensionOrder: atImportExtensionOrder, 255 esmConditionsDefault: esmConditionsDefault, 256 esmConditionsImport: esmConditionsImport, 257 esmConditionsRequire: esmConditionsRequire, 258 } 259} 260 261func (rr *resolver) Resolve(sourceDir string, importPath string, kind ast.ImportKind) (*ResolveResult, DebugMeta) { 262 var debugMeta DebugMeta 263 r := resolverQuery{ 264 resolver: rr, 265 debugMeta: &debugMeta, 266 kind: kind, 267 } 268 if r.log.Level <= logger.LevelDebug { 269 r.debugLogs = &debugLogs{what: fmt.Sprintf( 270 "Resolving import %q in directory %q of type %q", 271 importPath, sourceDir, kind.StringForMetafile())} 272 } 273 274 // Certain types of URLs default to being external for convenience 275 if r.isExternalPattern(importPath) || 276 277 // "fill: url(#filter);" 278 (kind.IsFromCSS() && strings.HasPrefix(importPath, "#")) || 279 280 // "background: url(http://example.com/images/image.png);" 281 strings.HasPrefix(importPath, "http://") || 282 283 // "background: url(https://example.com/images/image.png);" 284 strings.HasPrefix(importPath, "https://") || 285 286 // "background: url(//example.com/images/image.png);" 287 strings.HasPrefix(importPath, "//") { 288 289 if r.debugLogs != nil { 290 r.debugLogs.addNote("Marking this path as implicitly external") 291 } 292 293 r.flushDebugLogs(flushDueToSuccess) 294 return &ResolveResult{ 295 PathPair: PathPair{Primary: logger.Path{Text: importPath}}, 296 IsExternal: true, 297 }, debugMeta 298 } 299 300 // "import fs from 'fs'" 301 if r.options.Platform == config.PlatformNode && BuiltInNodeModules[importPath] { 302 if r.debugLogs != nil { 303 r.debugLogs.addNote("Marking this path as implicitly external due to it being a node built-in") 304 } 305 306 r.flushDebugLogs(flushDueToSuccess) 307 return &ResolveResult{ 308 PathPair: PathPair{Primary: logger.Path{Text: importPath}}, 309 IsExternal: true, 310 PrimarySideEffectsData: &SideEffectsData{}, // Mark this with "sideEffects: false" 311 }, debugMeta 312 } 313 314 // "import fs from 'node:fs'" 315 // "require('node:fs')" 316 if r.options.Platform == config.PlatformNode && strings.HasPrefix(importPath, "node:") { 317 if r.debugLogs != nil { 318 r.debugLogs.addNote("Marking this path as implicitly external due to the \"node:\" prefix") 319 } 320 321 // If this is a known node built-in module, mark it with "sideEffects: false" 322 var sideEffects *SideEffectsData 323 if BuiltInNodeModules[strings.TrimPrefix(importPath, "node:")] { 324 sideEffects = &SideEffectsData{} 325 } 326 327 // Check whether the path will end up as "import" or "require" 328 convertImportToRequire := !r.options.OutputFormat.KeepES6ImportExportSyntax() 329 isImport := !convertImportToRequire && (kind == ast.ImportStmt || kind == ast.ImportDynamic) 330 isRequire := kind == ast.ImportRequire || kind == ast.ImportRequireResolve || 331 (convertImportToRequire && (kind == ast.ImportStmt || kind == ast.ImportDynamic)) 332 333 // Check for support with "import" 334 if isImport && r.options.UnsupportedJSFeatures.Has(compat.NodeColonPrefixImport) { 335 if r.debugLogs != nil { 336 r.debugLogs.addNote("Removing the \"node:\" prefix because the target environment doesn't support it with \"import\" statements") 337 } 338 339 // Automatically strip the prefix if it's not supported 340 importPath = importPath[5:] 341 } 342 343 // Check for support with "require" 344 if isRequire && r.options.UnsupportedJSFeatures.Has(compat.NodeColonPrefixRequire) { 345 if r.debugLogs != nil { 346 r.debugLogs.addNote("Removing the \"node:\" prefix because the target environment doesn't support it with \"require\" calls") 347 } 348 349 // Automatically strip the prefix if it's not supported 350 importPath = importPath[5:] 351 } 352 353 r.flushDebugLogs(flushDueToSuccess) 354 return &ResolveResult{ 355 PathPair: PathPair{Primary: logger.Path{Text: importPath}}, 356 IsExternal: true, 357 PrimarySideEffectsData: sideEffects, 358 }, debugMeta 359 } 360 361 if parsed, ok := ParseDataURL(importPath); ok { 362 // "import 'data:text/javascript,console.log(123)';" 363 // "@import 'data:text/css,body{background:white}';" 364 if parsed.DecodeMIMEType() != MIMETypeUnsupported { 365 if r.debugLogs != nil { 366 r.debugLogs.addNote("Putting this path in the \"dataurl\" namespace") 367 } 368 r.flushDebugLogs(flushDueToSuccess) 369 return &ResolveResult{ 370 PathPair: PathPair{Primary: logger.Path{Text: importPath, Namespace: "dataurl"}}, 371 }, debugMeta 372 } 373 374 // "background: url(data:image/png;base64,iVBORw0KGgo=);" 375 if r.debugLogs != nil { 376 r.debugLogs.addNote("Marking this data URL as external") 377 } 378 r.flushDebugLogs(flushDueToSuccess) 379 return &ResolveResult{ 380 PathPair: PathPair{Primary: logger.Path{Text: importPath}}, 381 IsExternal: true, 382 }, debugMeta 383 } 384 385 // Fail now if there is no directory to resolve in. This can happen for 386 // virtual modules (e.g. stdin) if a resolve directory is not specified. 387 if sourceDir == "" { 388 if r.debugLogs != nil { 389 r.debugLogs.addNote("Cannot resolve this path without a directory") 390 } 391 r.flushDebugLogs(flushDueToFailure) 392 return nil, debugMeta 393 } 394 395 r.mutex.Lock() 396 defer r.mutex.Unlock() 397 398 result := r.resolveWithoutSymlinks(sourceDir, importPath) 399 if result == nil { 400 // If resolution failed, try again with the URL query and/or hash removed 401 suffix := strings.IndexAny(importPath, "?#") 402 if suffix < 1 { 403 r.flushDebugLogs(flushDueToFailure) 404 return nil, debugMeta 405 } 406 if r.debugLogs != nil { 407 r.debugLogs.addNote(fmt.Sprintf("Retrying resolution after removing the suffix %q", importPath[suffix:])) 408 } 409 if result2 := r.resolveWithoutSymlinks(sourceDir, importPath[:suffix]); result2 == nil { 410 r.flushDebugLogs(flushDueToFailure) 411 return nil, debugMeta 412 } else { 413 result = result2 414 result.PathPair.Primary.IgnoredSuffix = importPath[suffix:] 415 if result.PathPair.HasSecondary() { 416 result.PathPair.Secondary.IgnoredSuffix = importPath[suffix:] 417 } 418 } 419 } 420 421 // If successful, resolve symlinks using the directory info cache 422 r.finalizeResolve(result) 423 r.flushDebugLogs(flushDueToSuccess) 424 return result, debugMeta 425} 426 427func (r resolverQuery) isExternalPattern(path string) bool { 428 for _, pattern := range r.options.ExternalModules.Patterns { 429 if len(path) >= len(pattern.Prefix)+len(pattern.Suffix) && 430 strings.HasPrefix(path, pattern.Prefix) && 431 strings.HasSuffix(path, pattern.Suffix) { 432 return true 433 } 434 } 435 return false 436} 437 438func (rr *resolver) ResolveAbs(absPath string) *ResolveResult { 439 r := resolverQuery{resolver: rr} 440 if r.log.Level <= logger.LevelDebug { 441 r.debugLogs = &debugLogs{what: fmt.Sprintf("Getting metadata for absolute path %s", absPath)} 442 } 443 444 r.mutex.Lock() 445 defer r.mutex.Unlock() 446 447 // Just decorate the absolute path with information from parent directories 448 result := &ResolveResult{PathPair: PathPair{Primary: logger.Path{Text: absPath, Namespace: "file"}}} 449 r.finalizeResolve(result) 450 r.flushDebugLogs(flushDueToSuccess) 451 return result 452} 453 454func (rr *resolver) ProbeResolvePackageAsRelative(sourceDir string, importPath string, kind ast.ImportKind) *ResolveResult { 455 r := resolverQuery{ 456 resolver: rr, 457 kind: kind, 458 } 459 absPath := r.fs.Join(sourceDir, importPath) 460 461 r.mutex.Lock() 462 defer r.mutex.Unlock() 463 464 if pair, ok, diffCase := r.loadAsFileOrDirectory(absPath); ok { 465 result := &ResolveResult{PathPair: pair, DifferentCase: diffCase} 466 r.finalizeResolve(result) 467 r.flushDebugLogs(flushDueToSuccess) 468 return result 469 } 470 471 return nil 472} 473 474type debugLogs struct { 475 what string 476 indent string 477 notes []logger.MsgData 478} 479 480func (d *debugLogs) addNote(text string) { 481 if d.indent != "" { 482 text = d.indent + text 483 } 484 d.notes = append(d.notes, logger.MsgData{Text: text}) 485} 486 487func (d *debugLogs) increaseIndent() { 488 d.indent += " " 489} 490 491func (d *debugLogs) decreaseIndent() { 492 d.indent = d.indent[2:] 493} 494 495type flushMode uint8 496 497const ( 498 flushDueToFailure flushMode = iota 499 flushDueToSuccess 500) 501 502func (r resolverQuery) flushDebugLogs(mode flushMode) { 503 if r.debugLogs != nil { 504 if mode == flushDueToFailure { 505 r.log.AddWithNotes(logger.Debug, nil, logger.Range{}, r.debugLogs.what, r.debugLogs.notes) 506 } else if r.log.Level <= logger.LevelVerbose { 507 r.log.AddWithNotes(logger.Verbose, nil, logger.Range{}, r.debugLogs.what, r.debugLogs.notes) 508 } 509 } 510} 511 512func (r resolverQuery) finalizeResolve(result *ResolveResult) { 513 for _, path := range result.PathPair.iter() { 514 if path.Namespace == "file" { 515 if dirInfo := r.dirInfoCached(r.fs.Dir(path.Text)); dirInfo != nil { 516 base := r.fs.Base(path.Text) 517 518 // Look up this file in the "sideEffects" map in the nearest enclosing 519 // directory with a "package.json" file. 520 // 521 // Only do this for the primary path. Some packages have the primary 522 // path marked as having side effects and the secondary path marked 523 // as not having side effects. This is likely a bug in the package 524 // definition but we don't want to consider the primary path as not 525 // having side effects just because the secondary path is marked as 526 // not having side effects. 527 if pkgJSON := dirInfo.enclosingPackageJSON; pkgJSON != nil && *path == result.PathPair.Primary { 528 if pkgJSON.sideEffectsMap != nil { 529 hasSideEffects := false 530 if pkgJSON.sideEffectsMap[path.Text] { 531 // Fast path: map lookup 532 hasSideEffects = true 533 } else { 534 // Slow path: glob tests 535 for _, re := range pkgJSON.sideEffectsRegexps { 536 if re.MatchString(path.Text) { 537 hasSideEffects = true 538 break 539 } 540 } 541 } 542 if !hasSideEffects { 543 if r.debugLogs != nil { 544 r.debugLogs.addNote(fmt.Sprintf("Marking this file as having no side effects due to %q", 545 pkgJSON.source.KeyPath.Text)) 546 } 547 result.PrimarySideEffectsData = pkgJSON.sideEffectsData 548 } 549 } 550 551 // Also copy over the "type" field 552 result.ModuleType = pkgJSON.moduleType 553 } 554 555 // Copy various fields from the nearest enclosing "tsconfig.json" file if present 556 if path == &result.PathPair.Primary && dirInfo.enclosingTSConfigJSON != nil { 557 // Except don't do this if we're inside a "node_modules" directory. Package 558 // authors often publish their "tsconfig.json" files to npm because of 559 // npm's default-include publishing model and because these authors 560 // probably don't know about ".npmignore" files. 561 // 562 // People trying to use these packages with esbuild have historically 563 // complained that esbuild is respecting "tsconfig.json" in these cases. 564 // The assumption is that the package author published these files by 565 // accident. 566 // 567 // Ignoring "tsconfig.json" files inside "node_modules" directories breaks 568 // the use case of publishing TypeScript code and having it be transpiled 569 // for you, but that's the uncommon case and likely doesn't work with 570 // many other tools anyway. So now these files are ignored. 571 if helpers.IsInsideNodeModules(result.PathPair.Primary.Text) { 572 if r.debugLogs != nil { 573 r.debugLogs.addNote(fmt.Sprintf("Ignoring %q because %q is inside \"node_modules\"", 574 dirInfo.enclosingTSConfigJSON.AbsPath, 575 result.PathPair.Primary.Text)) 576 } 577 } else { 578 result.JSXFactory = dirInfo.enclosingTSConfigJSON.JSXFactory 579 result.JSXFragment = dirInfo.enclosingTSConfigJSON.JSXFragmentFactory 580 result.UseDefineForClassFieldsTS = dirInfo.enclosingTSConfigJSON.UseDefineForClassFields 581 result.UnusedImportsTS = config.UnusedImportsFromTsconfigValues( 582 dirInfo.enclosingTSConfigJSON.PreserveImportsNotUsedAsValues, 583 dirInfo.enclosingTSConfigJSON.PreserveValueImports, 584 ) 585 result.TSTarget = dirInfo.enclosingTSConfigJSON.TSTarget 586 587 if r.debugLogs != nil { 588 r.debugLogs.addNote(fmt.Sprintf("This import is under the effect of %q", 589 dirInfo.enclosingTSConfigJSON.AbsPath)) 590 if result.JSXFactory != nil { 591 r.debugLogs.addNote(fmt.Sprintf("\"jsxFactory\" is %q due to %q", 592 strings.Join(result.JSXFactory, "."), 593 dirInfo.enclosingTSConfigJSON.AbsPath)) 594 } 595 if result.JSXFragment != nil { 596 r.debugLogs.addNote(fmt.Sprintf("\"jsxFragment\" is %q due to %q", 597 strings.Join(result.JSXFragment, "."), 598 dirInfo.enclosingTSConfigJSON.AbsPath)) 599 } 600 } 601 } 602 } 603 604 if !r.options.PreserveSymlinks { 605 if entry, _ := dirInfo.entries.Get(base); entry != nil { 606 if symlink := entry.Symlink(r.fs); symlink != "" { 607 // Is this entry itself a symlink? 608 if r.debugLogs != nil { 609 r.debugLogs.addNote(fmt.Sprintf("Resolved symlink %q to %q", path.Text, symlink)) 610 } 611 path.Text = symlink 612 } else if dirInfo.absRealPath != "" { 613 // Is there at least one parent directory with a symlink? 614 symlink := r.fs.Join(dirInfo.absRealPath, base) 615 if r.debugLogs != nil { 616 r.debugLogs.addNote(fmt.Sprintf("Resolved symlink %q to %q", path.Text, symlink)) 617 } 618 path.Text = symlink 619 } 620 } 621 } 622 } 623 } 624 } 625 626 if r.debugLogs != nil { 627 r.debugLogs.addNote(fmt.Sprintf("Primary path is %q in namespace %q", result.PathPair.Primary.Text, result.PathPair.Primary.Namespace)) 628 if result.PathPair.HasSecondary() { 629 r.debugLogs.addNote(fmt.Sprintf("Secondary path is %q in namespace %q", result.PathPair.Secondary.Text, result.PathPair.Secondary.Namespace)) 630 } 631 } 632} 633 634func (r resolverQuery) resolveWithoutSymlinks(sourceDir string, importPath string) *ResolveResult { 635 // This implements the module resolution algorithm from node.js, which is 636 // described here: https://nodejs.org/api/modules.html#modules_all_together 637 var result ResolveResult 638 639 // Return early if this is already an absolute path. In addition to asking 640 // the file system whether this is an absolute path, we also explicitly check 641 // whether it starts with a "/" and consider that an absolute path too. This 642 // is because relative paths can technically start with a "/" on Windows 643 // because it's not an absolute path on Windows. Then people might write code 644 // with imports that start with a "/" that works fine on Windows only to 645 // experience unexpected build failures later on other operating systems. 646 // Treating these paths as absolute paths on all platforms means Windows 647 // users will not be able to accidentally make use of these paths. 648 if strings.HasPrefix(importPath, "/") || r.fs.IsAbs(importPath) { 649 if r.debugLogs != nil { 650 r.debugLogs.addNote(fmt.Sprintf("The import %q is being treated as an absolute path", importPath)) 651 } 652 653 // First, check path overrides from the nearest enclosing TypeScript "tsconfig.json" file 654 if dirInfo := r.dirInfoCached(sourceDir); dirInfo != nil && dirInfo.enclosingTSConfigJSON != nil && dirInfo.enclosingTSConfigJSON.Paths != nil { 655 if absolute, ok, diffCase := r.matchTSConfigPaths(dirInfo.enclosingTSConfigJSON, importPath); ok { 656 return &ResolveResult{PathPair: absolute, DifferentCase: diffCase} 657 } 658 } 659 660 if r.options.ExternalModules.AbsPaths != nil && r.options.ExternalModules.AbsPaths[importPath] { 661 // If the string literal in the source text is an absolute path and has 662 // been marked as an external module, mark it as *not* an absolute path. 663 // That way we preserve the literal text in the output and don't generate 664 // a relative path from the output directory to that path. 665 if r.debugLogs != nil { 666 r.debugLogs.addNote(fmt.Sprintf("The path %q was marked as external by the user", importPath)) 667 } 668 return &ResolveResult{PathPair: PathPair{Primary: logger.Path{Text: importPath}}, IsExternal: true} 669 } 670 671 // Run node's resolution rules (e.g. adding ".js") 672 if absolute, ok, diffCase := r.loadAsFileOrDirectory(importPath); ok { 673 return &ResolveResult{PathPair: absolute, DifferentCase: diffCase} 674 } else { 675 return nil 676 } 677 } 678 679 // Check both relative and package paths for CSS URL tokens, with relative 680 // paths taking precedence over package paths to match Webpack behavior. 681 isPackagePath := IsPackagePath(importPath) 682 checkRelative := !isPackagePath || r.kind == ast.ImportURL || r.kind == ast.ImportAt 683 checkPackage := isPackagePath 684 685 if checkRelative { 686 absPath := r.fs.Join(sourceDir, importPath) 687 688 // Check for external packages first 689 if r.options.ExternalModules.AbsPaths != nil && r.options.ExternalModules.AbsPaths[absPath] { 690 if r.debugLogs != nil { 691 r.debugLogs.addNote(fmt.Sprintf("The path %q was marked as external by the user", absPath)) 692 } 693 return &ResolveResult{PathPair: PathPair{Primary: logger.Path{Text: absPath, Namespace: "file"}}, IsExternal: true} 694 } 695 696 // Check the "browser" map 697 if importDirInfo := r.dirInfoCached(r.fs.Dir(absPath)); importDirInfo != nil { 698 if remapped, ok := r.checkBrowserMap(importDirInfo, absPath, absolutePathKind); ok { 699 if remapped == nil { 700 return &ResolveResult{PathPair: PathPair{Primary: logger.Path{Text: absPath, Namespace: "file", Flags: logger.PathDisabled}}} 701 } 702 if remappedResult, ok, diffCase := r.resolveWithoutRemapping(importDirInfo.enclosingBrowserScope, *remapped); ok { 703 result = ResolveResult{PathPair: remappedResult, DifferentCase: diffCase} 704 checkRelative = false 705 checkPackage = false 706 } 707 } 708 } 709 710 if checkRelative { 711 if absolute, ok, diffCase := r.loadAsFileOrDirectory(absPath); ok { 712 checkPackage = false 713 result = ResolveResult{PathPair: absolute, DifferentCase: diffCase} 714 } else if !checkPackage { 715 return nil 716 } 717 } 718 } 719 720 if checkPackage { 721 // Check for external packages first 722 if r.options.ExternalModules.NodeModules != nil { 723 query := importPath 724 for { 725 if r.options.ExternalModules.NodeModules[query] { 726 if r.debugLogs != nil { 727 r.debugLogs.addNote(fmt.Sprintf("The path %q was marked as external by the user", query)) 728 } 729 return &ResolveResult{PathPair: PathPair{Primary: logger.Path{Text: importPath}}, IsExternal: true} 730 } 731 732 // If the module "foo" has been marked as external, we also want to treat 733 // paths into that module such as "foo/bar" as external too. 734 slash := strings.LastIndexByte(query, '/') 735 if slash == -1 { 736 break 737 } 738 query = query[:slash] 739 } 740 } 741 742 sourceDirInfo := r.dirInfoCached(sourceDir) 743 if sourceDirInfo == nil { 744 // Bail if the directory is missing for some reason 745 return nil 746 } 747 748 // Support remapping one package path to another via the "browser" field 749 if remapped, ok := r.checkBrowserMap(sourceDirInfo, importPath, packagePathKind); ok { 750 if remapped == nil { 751 // "browser": {"module": false} 752 if absolute, ok, diffCase := r.loadNodeModules(importPath, sourceDirInfo, false /* forbidImports */); ok { 753 absolute.Primary = logger.Path{Text: absolute.Primary.Text, Namespace: "file", Flags: logger.PathDisabled} 754 if absolute.HasSecondary() { 755 absolute.Secondary = logger.Path{Text: absolute.Secondary.Text, Namespace: "file", Flags: logger.PathDisabled} 756 } 757 return &ResolveResult{PathPair: absolute, DifferentCase: diffCase} 758 } else { 759 return &ResolveResult{PathPair: PathPair{Primary: logger.Path{Text: importPath, Flags: logger.PathDisabled}}, DifferentCase: diffCase} 760 } 761 } 762 763 // "browser": {"module": "./some-file"} 764 // "browser": {"module": "another-module"} 765 importPath = *remapped 766 sourceDirInfo = sourceDirInfo.enclosingBrowserScope 767 } 768 769 if absolute, ok, diffCase := r.resolveWithoutRemapping(sourceDirInfo, importPath); ok { 770 result = ResolveResult{PathPair: absolute, DifferentCase: diffCase} 771 } else { 772 // Note: node's "self references" are not currently supported 773 return nil 774 } 775 } 776 777 return &result 778} 779 780func (r resolverQuery) resolveWithoutRemapping(sourceDirInfo *dirInfo, importPath string) (PathPair, bool, *fs.DifferentCase) { 781 if IsPackagePath(importPath) { 782 return r.loadNodeModules(importPath, sourceDirInfo, false /* forbidImports */) 783 } else { 784 return r.loadAsFileOrDirectory(r.fs.Join(sourceDirInfo.absPath, importPath)) 785 } 786} 787 788func (r *resolver) PrettyPath(path logger.Path) string { 789 if path.Namespace == "file" { 790 if rel, ok := r.fs.Rel(r.fs.Cwd(), path.Text); ok { 791 path.Text = rel 792 } 793 794 // These human-readable paths are used in error messages, comments in output 795 // files, source names in source maps, and paths in the metadata JSON file. 796 // These should be platform-independent so our output doesn't depend on which 797 // operating system it was run. Replace Windows backward slashes with standard 798 // forward slashes. 799 path.Text = strings.ReplaceAll(path.Text, "\\", "/") 800 } else if path.Namespace != "" { 801 path.Text = fmt.Sprintf("%s:%s", path.Namespace, path.Text) 802 } 803 804 if path.IsDisabled() { 805 path.Text = "(disabled):" + path.Text 806 } 807 808 return path.Text + path.IgnoredSuffix 809} 810 811//////////////////////////////////////////////////////////////////////////////// 812 813type dirInfo struct { 814 // These objects are immutable, so we can just point to the parent directory 815 // and avoid having to lock the cache again 816 parent *dirInfo 817 818 // A pointer to the enclosing dirInfo with a valid "browser" field in 819 // package.json. We need this to remap paths after they have been resolved. 820 enclosingBrowserScope *dirInfo 821 822 // All relevant information about this directory 823 absPath string 824 entries fs.DirEntries 825 isNodeModules bool // Is the base name "node_modules"? 826 hasNodeModules bool // Is there a "node_modules" subdirectory? 827 packageJSON *packageJSON // Is there a "package.json" file in this directory? 828 enclosingPackageJSON *packageJSON // Is there a "package.json" file in this directory or a parent directory? 829 enclosingTSConfigJSON *TSConfigJSON // Is there a "tsconfig.json" file in this directory or a parent directory? 830 absRealPath string // If non-empty, this is the real absolute path resolving any symlinks 831} 832 833func (r resolverQuery) dirInfoCached(path string) *dirInfo { 834 // First, check the cache 835 cached, ok := r.dirCache[path] 836 837 // Cache hit: stop now 838 if !ok { 839 // Cache miss: read the info 840 cached = r.dirInfoUncached(path) 841 842 // Update the cache unconditionally. Even if the read failed, we don't want to 843 // retry again later. The directory is inaccessible so trying again is wasted. 844 r.dirCache[path] = cached 845 } 846 847 if r.debugLogs != nil { 848 if cached == nil { 849 r.debugLogs.addNote(fmt.Sprintf("Failed to read directory %q", path)) 850 } else { 851 count := len(cached.entries.SortedKeys()) 852 entries := "entries" 853 if count == 1 { 854 entries = "entry" 855 } 856 r.debugLogs.addNote(fmt.Sprintf("Read %d %s for directory %q", count, entries, path)) 857 } 858 } 859 860 return cached 861} 862 863var errParseErrorImportCycle = errors.New("(import cycle)") 864var errParseErrorAlreadyLogged = errors.New("(error already logged)") 865 866// This may return "parseErrorAlreadyLogged" in which case there was a syntax 867// error, but it's already been reported. No further errors should be logged. 868// 869// Nested calls may also return "parseErrorImportCycle". In that case the 870// caller is responsible for logging an appropriate error message. 871func (r resolverQuery) parseTSConfig(file string, visited map[string]bool) (*TSConfigJSON, error) { 872 // Don't infinite loop if a series of "extends" links forms a cycle 873 if visited[file] { 874 return nil, errParseErrorImportCycle 875 } 876 visited[file] = true 877 878 contents, err, originalError := r.caches.FSCache.ReadFile(r.fs, file) 879 if r.debugLogs != nil && originalError != nil { 880 r.debugLogs.addNote(fmt.Sprintf("Failed to read file %q: %s", file, originalError.Error())) 881 } 882 if err != nil { 883 return nil, err 884 } 885 if r.debugLogs != nil { 886 r.debugLogs.addNote(fmt.Sprintf("The file %q exists", file)) 887 } 888 889 keyPath := logger.Path{Text: file, Namespace: "file"} 890 source := logger.Source{ 891 KeyPath: keyPath, 892 PrettyPath: r.PrettyPath(keyPath), 893 Contents: contents, 894 } 895 tracker := logger.MakeLineColumnTracker(&source) 896 fileDir := r.fs.Dir(file) 897 898 result := ParseTSConfigJSON(r.log, source, &r.caches.JSONCache, func(extends string, extendsRange logger.Range) *TSConfigJSON { 899 if IsPackagePath(extends) { 900 // If this is a package path, try to resolve it to a "node_modules" 901 // folder. This doesn't use the normal node module resolution algorithm 902 // both because it's different (e.g. we don't want to match a directory) 903 // and because it would deadlock since we're currently in the middle of 904 // populating the directory info cache. 905 current := fileDir 906 for { 907 // Skip "node_modules" folders 908 if r.fs.Base(current) != "node_modules" { 909 join := r.fs.Join(current, "node_modules", extends) 910 filesToCheck := []string{r.fs.Join(join, "tsconfig.json"), join, join + ".json"} 911 for _, fileToCheck := range filesToCheck { 912 base, err := r.parseTSConfig(fileToCheck, visited) 913 if err == nil { 914 return base 915 } else if err == syscall.ENOENT { 916 continue 917 } else if err == errParseErrorImportCycle { 918 r.log.Add(logger.Warning, &tracker, extendsRange, 919 fmt.Sprintf("Base config file %q forms cycle", extends)) 920 } else if err != errParseErrorAlreadyLogged { 921 r.log.Add(logger.Error, &tracker, extendsRange, 922 fmt.Sprintf("Cannot read file %q: %s", 923 r.PrettyPath(logger.Path{Text: fileToCheck, Namespace: "file"}), err.Error())) 924 } 925 return nil 926 } 927 } 928 929 // Go to the parent directory, stopping at the file system root 930 next := r.fs.Dir(current) 931 if current == next { 932 break 933 } 934 current = next 935 } 936 } else { 937 // If this is a regular path, search relative to the enclosing directory 938 extendsFile := extends 939 if !r.fs.IsAbs(extends) { 940 extendsFile = r.fs.Join(fileDir, extends) 941 } 942 for _, fileToCheck := range []string{extendsFile, extendsFile + ".json"} { 943 base, err := r.parseTSConfig(fileToCheck, visited) 944 if err == nil { 945 return base 946 } else if err == syscall.ENOENT { 947 continue 948 } else if err == errParseErrorImportCycle { 949 r.log.Add(logger.Warning, &tracker, extendsRange, 950 fmt.Sprintf("Base config file %q forms cycle", extends)) 951 } else if err != errParseErrorAlreadyLogged { 952 r.log.Add(logger.Error, &tracker, extendsRange, 953 fmt.Sprintf("Cannot read file %q: %s", 954 r.PrettyPath(logger.Path{Text: fileToCheck, Namespace: "file"}), err.Error())) 955 } 956 return nil 957 } 958 } 959 960 // Suppress warnings about missing base config files inside "node_modules" 961 if !helpers.IsInsideNodeModules(file) { 962 r.log.Add(logger.Warning, &tracker, extendsRange, 963 fmt.Sprintf("Cannot find base config file %q", extends)) 964 } 965 966 return nil 967 }) 968 969 if result == nil { 970 return nil, errParseErrorAlreadyLogged 971 } 972 973 if result.BaseURL != nil && !r.fs.IsAbs(*result.BaseURL) { 974 *result.BaseURL = r.fs.Join(fileDir, *result.BaseURL) 975 } 976 977 if result.Paths != nil && !r.fs.IsAbs(result.BaseURLForPaths) { 978 result.BaseURLForPaths = r.fs.Join(fileDir, result.BaseURLForPaths) 979 } 980 981 return result, nil 982} 983 984func (r resolverQuery) dirInfoUncached(path string) *dirInfo { 985 // Get the info for the parent directory 986 var parentInfo *dirInfo 987 parentDir := r.fs.Dir(path) 988 if parentDir != path { 989 parentInfo = r.dirInfoCached(parentDir) 990 991 // Stop now if the parent directory doesn't exist 992 if parentInfo == nil { 993 return nil 994 } 995 } 996 997 // List the directories 998 entries, err, originalError := r.fs.ReadDirectory(path) 999 if err == syscall.EACCES { 1000 // Just pretend this directory is empty if we can't access it. This is the 1001 // case on Unix for directories that only have the execute permission bit 1002 // set. It means we will just pass through the empty directory and 1003 // continue to check the directories above it, which is now node behaves. 1004 entries = fs.MakeEmptyDirEntries(path) 1005 err = nil 1006 } 1007 if r.debugLogs != nil && originalError != nil { 1008 r.debugLogs.addNote(fmt.Sprintf("Failed to read directory %q: %s", path, originalError.Error())) 1009 } 1010 if err != nil { 1011 // Ignore "ENOTDIR" here so that calling "ReadDirectory" on a file behaves 1012 // as if there is nothing there at all instead of causing an error due to 1013 // the directory actually being a file. This is a workaround for situations 1014 // where people try to import from a path containing a file as a parent 1015 // directory. The "pnpm" package manager generates a faulty "NODE_PATH" 1016 // list which contains such paths and treating them as missing means we just 1017 // ignore them during path resolution. 1018 if err != syscall.ENOENT && err != syscall.ENOTDIR { 1019 r.log.Add(logger.Error, nil, logger.Range{}, 1020 fmt.Sprintf("Cannot read directory %q: %s", 1021 r.PrettyPath(logger.Path{Text: path, Namespace: "file"}), err.Error())) 1022 } 1023 return nil 1024 } 1025 info := &dirInfo{ 1026 absPath: path, 1027 parent: parentInfo, 1028 entries: entries, 1029 } 1030 1031 // A "node_modules" directory isn't allowed to directly contain another "node_modules" directory 1032 base := r.fs.Base(path) 1033 if base == "node_modules" { 1034 info.isNodeModules = true 1035 } else if entry, _ := entries.Get("node_modules"); entry != nil { 1036 info.hasNodeModules = entry.Kind(r.fs) == fs.DirEntry 1037 } 1038 1039 // Propagate the browser scope into child directories 1040 if parentInfo != nil { 1041 info.enclosingPackageJSON = parentInfo.enclosingPackageJSON 1042 info.enclosingBrowserScope = parentInfo.enclosingBrowserScope 1043 info.enclosingTSConfigJSON = parentInfo.enclosingTSConfigJSON 1044 1045 // Make sure "absRealPath" is the real path of the directory (resolving any symlinks) 1046 if !r.options.PreserveSymlinks { 1047 if entry, _ := parentInfo.entries.Get(base); entry != nil { 1048 if symlink := entry.Symlink(r.fs); symlink != "" { 1049 if r.debugLogs != nil { 1050 r.debugLogs.addNote(fmt.Sprintf("Resolved symlink %q to %q", path, symlink)) 1051 } 1052 info.absRealPath = symlink 1053 } else if parentInfo.absRealPath != "" { 1054 symlink := r.fs.Join(parentInfo.absRealPath, base) 1055 if r.debugLogs != nil { 1056 r.debugLogs.addNote(fmt.Sprintf("Resolved symlink %q to %q", path, symlink)) 1057 } 1058 info.absRealPath = symlink 1059 } 1060 } 1061 } 1062 } 1063 1064 // Record if this directory has a package.json file 1065 if entry, _ := entries.Get("package.json"); entry != nil && entry.Kind(r.fs) == fs.FileEntry { 1066 info.packageJSON = r.parsePackageJSON(path) 1067 1068 // Propagate this "package.json" file into child directories 1069 if info.packageJSON != nil { 1070 info.enclosingPackageJSON = info.packageJSON 1071 if info.packageJSON.browserMap != nil { 1072 info.enclosingBrowserScope = info 1073 } 1074 } 1075 } 1076 1077 // Record if this directory has a tsconfig.json or jsconfig.json file 1078 { 1079 var tsConfigPath string 1080 if forceTsConfig := r.options.TsConfigOverride; forceTsConfig == "" { 1081 if entry, _ := entries.Get("tsconfig.json"); entry != nil && entry.Kind(r.fs) == fs.FileEntry { 1082 tsConfigPath = r.fs.Join(path, "tsconfig.json") 1083 } else if entry, _ := entries.Get("jsconfig.json"); entry != nil && entry.Kind(r.fs) == fs.FileEntry { 1084 tsConfigPath = r.fs.Join(path, "jsconfig.json") 1085 } 1086 } else if parentInfo == nil { 1087 // If there is a tsconfig.json override, mount it at the root directory 1088 tsConfigPath = forceTsConfig 1089 } 1090 if tsConfigPath != "" { 1091 var err error 1092 info.enclosingTSConfigJSON, err = r.parseTSConfig(tsConfigPath, make(map[string]bool)) 1093 if err != nil { 1094 if err == syscall.ENOENT { 1095 r.log.Add(logger.Error, nil, logger.Range{}, fmt.Sprintf("Cannot find tsconfig file %q", 1096 r.PrettyPath(logger.Path{Text: tsConfigPath, Namespace: "file"}))) 1097 } else if err != errParseErrorAlreadyLogged { 1098 r.log.Add(logger.Debug, nil, logger.Range{}, 1099 fmt.Sprintf("Cannot read file %q: %s", 1100 r.PrettyPath(logger.Path{Text: tsConfigPath, Namespace: "file"}), err.Error())) 1101 } 1102 } 1103 } 1104 } 1105 1106 return info 1107} 1108 1109var rewrittenFileExtensions = map[string][]string{ 1110 // Note that the official compiler code always tries ".ts" before 1111 // ".tsx" even if the original extension was ".jsx". 1112 ".js": {".ts", ".tsx"}, 1113 ".jsx": {".ts", ".tsx"}, 1114 ".mjs": {".mts"}, 1115 ".cjs": {".cts"}, 1116} 1117 1118func (r resolverQuery) loadAsFile(path string, extensionOrder []string) (string, bool, *fs.DifferentCase) { 1119 if r.debugLogs != nil { 1120 r.debugLogs.addNote(fmt.Sprintf("Attempting to load %q as a file", path)) 1121 r.debugLogs.increaseIndent() 1122 defer r.debugLogs.decreaseIndent() 1123 } 1124 1125 // Read the directory entries once to minimize locking 1126 dirPath := r.fs.Dir(path) 1127 entries, err, originalError := r.fs.ReadDirectory(dirPath) 1128 if r.debugLogs != nil && originalError != nil { 1129 r.debugLogs.addNote(fmt.Sprintf("Failed to read directory %q: %s", dirPath, originalError.Error())) 1130 } 1131 if err != nil { 1132 if err != syscall.ENOENT { 1133 r.log.Add(logger.Error, nil, logger.Range{}, 1134 fmt.Sprintf(" Cannot read directory %q: %s", 1135 r.PrettyPath(logger.Path{Text: dirPath, Namespace: "file"}), err.Error())) 1136 } 1137 return "", false, nil 1138 } 1139 1140 base := r.fs.Base(path) 1141 1142 // Try the plain path without any extensions 1143 if r.debugLogs != nil { 1144 r.debugLogs.addNote(fmt.Sprintf("Checking for file %q", base)) 1145 } 1146 if entry, diffCase := entries.Get(base); entry != nil && entry.Kind(r.fs) == fs.FileEntry { 1147 if r.debugLogs != nil { 1148 r.debugLogs.addNote(fmt.Sprintf("Found file %q", base)) 1149 } 1150 return path, true, diffCase 1151 } 1152 1153 // Try the path with extensions 1154 for _, ext := range extensionOrder { 1155 if r.debugLogs != nil { 1156 r.debugLogs.addNote(fmt.Sprintf("Checking for file %q", base+ext)) 1157 } 1158 if entry, diffCase := entries.Get(base + ext); entry != nil && entry.Kind(r.fs) == fs.FileEntry { 1159 if r.debugLogs != nil { 1160 r.debugLogs.addNote(fmt.Sprintf("Found file %q", base+ext)) 1161 } 1162 return path + ext, true, diffCase 1163 } 1164 } 1165 1166 // TypeScript-specific behavior: if the extension is ".js" or ".jsx", try 1167 // replacing it with ".ts" or ".tsx". At the time of writing this specific 1168 // behavior comes from the function "loadModuleFromFile()" in the file 1169 // "moduleNameResolver.ts" in the TypeScript compiler source code. It 1170 // contains this comment: 1171 // 1172 // If that didn't work, try stripping a ".js" or ".jsx" extension and 1173 // replacing it with a TypeScript one; e.g. "./foo.js" can be matched 1174 // by "./foo.ts" or "./foo.d.ts" 1175 // 1176 // We don't care about ".d.ts" files because we can't do anything with 1177 // those, so we ignore that part of the behavior. 1178 // 1179 // See the discussion here for more historical context: 1180 // https://github.com/microsoft/TypeScript/issues/4595 1181 for old, exts := range rewrittenFileExtensions { 1182 if !strings.HasSuffix(base, old) { 1183 continue 1184 } 1185 lastDot := strings.LastIndexByte(base, '.') 1186 for _, ext := range exts { 1187 if entry, diffCase := entries.Get(base[:lastDot] + ext); entry != nil && entry.Kind(r.fs) == fs.FileEntry { 1188 if r.debugLogs != nil { 1189 r.debugLogs.addNote(fmt.Sprintf("Rewrote to %q", base[:lastDot]+ext)) 1190 } 1191 return path[:len(path)-(len(base)-lastDot)] + ext, true, diffCase 1192 } 1193 if r.debugLogs != nil { 1194 r.debugLogs.addNote(fmt.Sprintf("Failed to rewrite to %q", base[:lastDot]+ext)) 1195 } 1196 } 1197 break 1198 } 1199 1200 if r.debugLogs != nil { 1201 r.debugLogs.addNote(fmt.Sprintf("Failed to find file %q", base)) 1202 } 1203 return "", false, nil 1204} 1205 1206func (r resolverQuery) loadAsIndex(dirInfo *dirInfo, path string, extensionOrder []string) (PathPair, bool, *fs.DifferentCase) { 1207 // Try the "index" file with extensions 1208 for _, ext := range extensionOrder { 1209 base := "index" + ext 1210 if entry, diffCase := dirInfo.entries.Get(base); entry != nil && entry.Kind(r.fs) == fs.FileEntry { 1211 if r.debugLogs != nil { 1212 r.debugLogs.addNote(fmt.Sprintf("Found file %q", r.fs.Join(path, base))) 1213 } 1214 return PathPair{Primary: logger.Path{Text: r.fs.Join(path, base), Namespace: "file"}}, true, diffCase 1215 } 1216 if r.debugLogs != nil { 1217 r.debugLogs.addNote(fmt.Sprintf("Failed to find file %q", r.fs.Join(path, base))) 1218 } 1219 } 1220 1221 return PathPair{}, false, nil 1222} 1223 1224func (r resolverQuery) loadAsIndexWithBrowserRemapping(dirInfo *dirInfo, path string, extensionOrder []string) (PathPair, bool, *fs.DifferentCase) { 1225 // Potentially remap using the "browser" field 1226 absPath := r.fs.Join(path, "index") 1227 if remapped, ok := r.checkBrowserMap(dirInfo, absPath, absolutePathKind); ok { 1228 if remapped == nil { 1229 return PathPair{Primary: logger.Path{Text: absPath, Namespace: "file", Flags: logger.PathDisabled}}, true, nil 1230 } 1231 remappedAbs := r.fs.Join(path, *remapped) 1232 1233 // Is this a file? 1234 absolute, ok, diffCase := r.loadAsFile(remappedAbs, extensionOrder) 1235 if ok { 1236 return PathPair{Primary: logger.Path{Text: absolute, Namespace: "file"}}, true, diffCase 1237 } 1238 1239 // Is it a directory with an index? 1240 if fieldDirInfo := r.dirInfoCached(remappedAbs); fieldDirInfo != nil { 1241 if absolute, ok, _ := r.loadAsIndex(fieldDirInfo, remappedAbs, extensionOrder); ok { 1242 return absolute, true, nil 1243 } 1244 } 1245 1246 return PathPair{}, false, nil 1247 } 1248 1249 return r.loadAsIndex(dirInfo, path, extensionOrder) 1250} 1251 1252func getProperty(json js_ast.Expr, name string) (js_ast.Expr, logger.Loc, bool) { 1253 if obj, ok := json.Data.(*js_ast.EObject); ok { 1254 for _, prop := range obj.Properties { 1255 if key, ok := prop.Key.Data.(*js_ast.EString); ok && key.Value != nil && 1256 len(key.Value) == len(name) && js_lexer.UTF16ToString(key.Value) == name { 1257 return prop.ValueOrNil, prop.Key.Loc, true 1258 } 1259 } 1260 } 1261 return js_ast.Expr{}, logger.Loc{}, false 1262} 1263 1264func getString(json js_ast.Expr) (string, bool) { 1265 if value, ok := json.Data.(*js_ast.EString); ok { 1266 return js_lexer.UTF16ToString(value.Value), true 1267 } 1268 return "", false 1269} 1270 1271func getBool(json js_ast.Expr) (bool, bool) { 1272 if value, ok := json.Data.(*js_ast.EBoolean); ok { 1273 return value.Value, true 1274 } 1275 return false, false 1276} 1277 1278func (r resolverQuery) loadAsFileOrDirectory(path string) (PathPair, bool, *fs.DifferentCase) { 1279 // Use a special import order for CSS "@import" imports 1280 extensionOrder := r.options.ExtensionOrder 1281 if r.kind == ast.ImportAt || r.kind == ast.ImportAtConditional { 1282 extensionOrder = r.atImportExtensionOrder 1283 } 1284 1285 // Is this a file? 1286 absolute, ok, diffCase := r.loadAsFile(path, extensionOrder) 1287 if ok { 1288 return PathPair{Primary: logger.Path{Text: absolute, Namespace: "file"}}, true, diffCase 1289 } 1290 1291 // Is this a directory? 1292 if r.debugLogs != nil { 1293 r.debugLogs.addNote(fmt.Sprintf("Attempting to load %q as a directory", path)) 1294 r.debugLogs.increaseIndent() 1295 defer r.debugLogs.decreaseIndent() 1296 } 1297 dirInfo := r.dirInfoCached(path) 1298 if dirInfo == nil { 1299 return PathPair{}, false, nil 1300 } 1301 1302 // Try using the main field(s) from "package.json" 1303 if absolute, ok, diffCase := r.loadAsMainField(dirInfo, path, extensionOrder); ok { 1304 return absolute, true, diffCase 1305 } 1306 1307 // Look for an "index" file with known extensions 1308 if absolute, ok, diffCase := r.loadAsIndexWithBrowserRemapping(dirInfo, path, extensionOrder); ok { 1309 return absolute, true, diffCase 1310 } 1311 1312 return PathPair{}, false, nil 1313} 1314 1315func (r resolverQuery) loadAsMainField(dirInfo *dirInfo, path string, extensionOrder []string) (PathPair, bool, *fs.DifferentCase) { 1316 if dirInfo.packageJSON == nil { 1317 return PathPair{}, false, nil 1318 } 1319 1320 mainFieldValues := dirInfo.packageJSON.mainFields 1321 mainFieldKeys := r.options.MainFields 1322 autoMain := false 1323 1324 // If the user has not explicitly specified a "main" field order, 1325 // use a default one determined by the current platform target 1326 if mainFieldKeys == nil { 1327 mainFieldKeys = defaultMainFields[r.options.Platform] 1328 autoMain = true 1329 } 1330 1331 loadMainField := func(fieldRelPath string, field string) (PathPair, bool, *fs.DifferentCase) { 1332 if r.debugLogs != nil { 1333 r.debugLogs.addNote(fmt.Sprintf("Found main field %q with path %q", field, fieldRelPath)) 1334 r.debugLogs.increaseIndent() 1335 defer r.debugLogs.decreaseIndent() 1336 } 1337 1338 // Potentially remap using the "browser" field 1339 fieldAbsPath := r.fs.Join(path, fieldRelPath) 1340 if remapped, ok := r.checkBrowserMap(dirInfo, fieldAbsPath, absolutePathKind); ok { 1341 if remapped == nil { 1342 return PathPair{Primary: logger.Path{Text: fieldAbsPath, Namespace: "file", Flags: logger.PathDisabled}}, true, nil 1343 } 1344 fieldAbsPath = r.fs.Join(path, *remapped) 1345 } 1346 1347 // Is this a file? 1348 absolute, ok, diffCase := r.loadAsFile(fieldAbsPath, extensionOrder) 1349 if ok { 1350 return PathPair{Primary: logger.Path{Text: absolute, Namespace: "file"}}, true, diffCase 1351 } 1352 1353 // Is it a directory with an index? 1354 if fieldDirInfo := r.dirInfoCached(fieldAbsPath); fieldDirInfo != nil { 1355 if absolute, ok, _ := r.loadAsIndexWithBrowserRemapping(fieldDirInfo, fieldAbsPath, extensionOrder); ok { 1356 return absolute, true, nil 1357 } 1358 } 1359 1360 return PathPair{}, false, nil 1361 } 1362 1363 if r.debugLogs != nil { 1364 r.debugLogs.addNote(fmt.Sprintf("Searching for main fields in %q", dirInfo.packageJSON.source.KeyPath.Text)) 1365 r.debugLogs.increaseIndent() 1366 defer r.debugLogs.decreaseIndent() 1367 } 1368 1369 foundSomething := false 1370 1371 for _, key := range mainFieldKeys { 1372 value, ok := mainFieldValues[key] 1373 if !ok { 1374 if r.debugLogs != nil { 1375 r.debugLogs.addNote(fmt.Sprintf("Did not find main field %q", key)) 1376 } 1377 continue 1378 } 1379 foundSomething = true 1380 1381 absolute, ok, diffCase := loadMainField(value.relPath, key) 1382 if !ok { 1383 continue 1384 } 1385 1386 // If the user did not manually configure a "main" field order, then 1387 // use a special per-module automatic algorithm to decide whether to 1388 // use "module" or "main" based on whether the package is imported 1389 // using "import" or "require". 1390 if autoMain && key == "module" { 1391 var absoluteMain PathPair 1392 var okMain bool 1393 var diffCaseMain *fs.DifferentCase 1394 1395 if main, ok := mainFieldValues["main"]; ok { 1396 if absolute, ok, diffCase := loadMainField(main.relPath, "main"); ok { 1397 absoluteMain = absolute 1398 okMain = true 1399 diffCaseMain = diffCase 1400 } 1401 } else { 1402 // Some packages have a "module" field without a "main" field but 1403 // still have an implicit "index.js" file. In that case, treat that 1404 // as the value for "main". 1405 if absolute, ok, diffCase := r.loadAsIndexWithBrowserRemapping(dirInfo, path, extensionOrder); ok { 1406 absoluteMain = absolute 1407 okMain = true 1408 diffCaseMain = diffCase 1409 } 1410 } 1411 1412 if okMain { 1413 // If both the "main" and "module" fields exist, use "main" if the 1414 // path is for "require" and "module" if the path is for "import". 1415 // If we're using "module", return enough information to be able to 1416 // fall back to "main" later if something ended up using "require()" 1417 // with this same path. The goal of this code is to avoid having 1418 // both the "module" file and the "main" file in the bundle at the 1419 // same time. 1420 if r.kind != ast.ImportRequire { 1421 if r.debugLogs != nil { 1422 r.debugLogs.addNote(fmt.Sprintf("Resolved to %q using the \"module\" field in %q", 1423 absolute.Primary.Text, dirInfo.packageJSON.source.KeyPath.Text)) 1424 r.debugLogs.addNote(fmt.Sprintf("The fallback path in case of \"require\" is %q", 1425 absoluteMain.Primary.Text)) 1426 } 1427 return PathPair{ 1428 // This is the whole point of the path pair 1429 Primary: absolute.Primary, 1430 Secondary: absoluteMain.Primary, 1431 }, true, diffCase 1432 } else { 1433 if r.debugLogs != nil { 1434 r.debugLogs.addNote(fmt.Sprintf("Resolved to %q because of \"require\"", absoluteMain.Primary.Text)) 1435 } 1436 return absoluteMain, true, diffCaseMain 1437 } 1438 } 1439 } 1440 1441 if r.debugLogs != nil { 1442 r.debugLogs.addNote(fmt.Sprintf("Resolved to %q using the %q field in %q", 1443 absolute.Primary.Text, key, dirInfo.packageJSON.source.KeyPath.Text)) 1444 } 1445 return absolute, true, diffCase 1446 } 1447 1448 // Let the user know if "main" exists but was skipped due to mis-configuration 1449 if !foundSomething { 1450 for _, field := range mainFieldsForFailure { 1451 if main, ok := mainFieldValues[field]; ok { 1452 tracker := logger.MakeLineColumnTracker(&dirInfo.packageJSON.source) 1453 keyRange := dirInfo.packageJSON.source.RangeOfString(main.keyLoc) 1454 if len(mainFieldKeys) == 0 && r.options.Platform == config.PlatformNeutral { 1455 r.debugMeta.notes = append(r.debugMeta.notes, tracker.MsgData(keyRange, 1456 fmt.Sprintf("The %q field here was ignored. Main fields must be configured explicitly when using the \"neutral\" platform.", 1457 field))) 1458 } else { 1459 quoted := make([]string, len(mainFieldKeys)) 1460 for i, key := range mainFieldKeys { 1461 quoted[i] = fmt.Sprintf("%q", key) 1462 } 1463 r.debugMeta.notes = append(r.debugMeta.notes, tracker.MsgData(keyRange, 1464 fmt.Sprintf("The %q field here was ignored because the list of main fields to use is currently set to [%s].", 1465 field, strings.Join(quoted, ", ")))) 1466 } 1467 break 1468 } 1469 } 1470 } 1471 1472 return PathPair{}, false, nil 1473} 1474 1475// This closely follows the behavior of "tryLoadModuleUsingPaths()" in the 1476// official TypeScript compiler 1477func (r resolverQuery) matchTSConfigPaths(tsConfigJSON *TSConfigJSON, path string) (PathPair, bool, *fs.DifferentCase) { 1478 if r.debugLogs != nil { 1479 r.debugLogs.addNote(fmt.Sprintf("Matching %q against \"paths\" in %q", path, tsConfigJSON.AbsPath)) 1480 } 1481 1482 absBaseURL := tsConfigJSON.BaseURLForPaths 1483 1484 // The explicit base URL should take precedence over the implicit base URL 1485 // if present. This matters when a tsconfig.json file overrides "baseUrl" 1486 // from another extended tsconfig.json file but doesn't override "paths". 1487 if tsConfigJSON.BaseURL != nil { 1488 absBaseURL = *tsConfigJSON.BaseURL 1489 } 1490 1491 if r.debugLogs != nil { 1492 r.debugLogs.addNote(fmt.Sprintf("Using %q as \"baseURL\"", absBaseURL)) 1493 } 1494 1495 // Check for exact matches first 1496 for key, originalPaths := range tsConfigJSON.Paths { 1497 if key == path { 1498 if r.debugLogs != nil { 1499 r.debugLogs.addNote(fmt.Sprintf("Found an exact match for %q in \"paths\"", key)) 1500 } 1501 for _, originalPath := range originalPaths { 1502 // Load the original path relative to the "baseUrl" from tsconfig.json 1503 absoluteOriginalPath := originalPath 1504 if !r.fs.IsAbs(originalPath) { 1505 absoluteOriginalPath = r.fs.Join(absBaseURL, originalPath) 1506 } 1507 if absolute, ok, diffCase := r.loadAsFileOrDirectory(absoluteOriginalPath); ok { 1508 return absolute, true, diffCase 1509 } 1510 } 1511 return PathPair{}, false, nil 1512 } 1513 } 1514 1515 type match struct { 1516 prefix string 1517 suffix string 1518 originalPaths []string 1519 } 1520 1521 // Check for pattern matches next 1522 longestMatchPrefixLength := -1 1523 longestMatchSuffixLength := -1 1524 var longestMatch match 1525 for key, originalPaths := range tsConfigJSON.Paths { 1526 if starIndex := strings.IndexByte(key, '*'); starIndex != -1 { 1527 prefix, suffix := key[:starIndex], key[starIndex+1:] 1528 1529 // Find the match with the longest prefix. If two matches have the same 1530 // prefix length, pick the one with the longest suffix. This second edge 1531 // case isn't handled by the TypeScript compiler, but we handle it 1532 // because we want the output to always be deterministic and Go map 1533 // iteration order is deliberately non-deterministic. 1534 if strings.HasPrefix(path, prefix) && strings.HasSuffix(path, suffix) && (len(prefix) > longestMatchPrefixLength || 1535 (len(prefix) == longestMatchPrefixLength && len(suffix) > longestMatchSuffixLength)) { 1536 longestMatchPrefixLength = len(prefix) 1537 longestMatchSuffixLength = len(suffix) 1538 longestMatch = match{ 1539 prefix: prefix, 1540 suffix: suffix, 1541 originalPaths: originalPaths, 1542 } 1543 } 1544 } 1545 } 1546 1547 // If there is at least one match, only consider the one with the longest 1548 // prefix. This matches the behavior of the TypeScript compiler. 1549 if longestMatchPrefixLength != -1 { 1550 if r.debugLogs != nil { 1551 r.debugLogs.addNote(fmt.Sprintf("Found a fuzzy match for %q in \"paths\"", longestMatch.prefix+"*"+longestMatch.suffix)) 1552 } 1553 1554 for _, originalPath := range longestMatch.originalPaths { 1555 // Swap out the "*" in the original path for whatever the "*" matched 1556 matchedText := path[len(longestMatch.prefix) : len(path)-len(longestMatch.suffix)] 1557 originalPath = strings.Replace(originalPath, "*", matchedText, 1) 1558 1559 // Load the original path relative to the "baseUrl" from tsconfig.json 1560 absoluteOriginalPath := originalPath 1561 if !r.fs.IsAbs(originalPath) { 1562 absoluteOriginalPath = r.fs.Join(absBaseURL, originalPath) 1563 } 1564 if absolute, ok, diffCase := r.loadAsFileOrDirectory(absoluteOriginalPath); ok { 1565 return absolute, true, diffCase 1566 } 1567 } 1568 } 1569 1570 return PathPair{}, false, nil 1571} 1572 1573func (r resolverQuery) loadNodeModules(importPath string, dirInfo *dirInfo, forbidImports bool) (PathPair, bool, *fs.DifferentCase) { 1574 if r.debugLogs != nil { 1575 r.debugLogs.addNote(fmt.Sprintf("Searching for %q in \"node_modules\" directories starting from %q", importPath, dirInfo.absPath)) 1576 r.debugLogs.increaseIndent() 1577 defer r.debugLogs.decreaseIndent() 1578 } 1579 1580 // First, check path overrides from the nearest enclosing TypeScript "tsconfig.json" file 1581 if dirInfo.enclosingTSConfigJSON != nil { 1582 // Try path substitutions first 1583 if dirInfo.enclosingTSConfigJSON.Paths != nil { 1584 if absolute, ok, diffCase := r.matchTSConfigPaths(dirInfo.enclosingTSConfigJSON, importPath); ok { 1585 return absolute, true, diffCase 1586 } 1587 } 1588 1589 // Try looking up the path relative to the base URL 1590 if dirInfo.enclosingTSConfigJSON.BaseURL != nil { 1591 basePath := r.fs.Join(*dirInfo.enclosingTSConfigJSON.BaseURL, importPath) 1592 if absolute, ok, diffCase := r.loadAsFileOrDirectory(basePath); ok { 1593 return absolute, true, diffCase 1594 } 1595 } 1596 } 1597 1598 // Find the parent directory with the "package.json" file 1599 dirInfoPackageJSON := dirInfo 1600 for dirInfoPackageJSON != nil && dirInfoPackageJSON.packageJSON == nil { 1601 dirInfoPackageJSON = dirInfoPackageJSON.parent 1602 } 1603 1604 // Then check for the package in any enclosing "node_modules" directories 1605 if dirInfoPackageJSON != nil && strings.HasPrefix(importPath, "#") && !forbidImports && dirInfoPackageJSON.packageJSON.importsMap != nil { 1606 packageJSON := dirInfoPackageJSON.packageJSON 1607 1608 if r.debugLogs != nil { 1609 r.debugLogs.addNote(fmt.Sprintf("Looking for %q in \"imports\" map in %q", importPath, packageJSON.source.KeyPath.Text)) 1610 r.debugLogs.increaseIndent() 1611 defer r.debugLogs.decreaseIndent() 1612 } 1613 1614 // Filter out invalid module specifiers now where we have more information for 1615 // a better error message instead of later when we're inside the algorithm 1616 if importPath == "#" || strings.HasPrefix(importPath, "#/") { 1617 if r.debugLogs != nil { 1618 r.debugLogs.addNote(fmt.Sprintf("The path %q must not equal \"#\" and must not start with \"#/\".", importPath)) 1619 } 1620 tracker := logger.MakeLineColumnTracker(&packageJSON.source) 1621 r.debugMeta.notes = append(r.debugMeta.notes, tracker.MsgData(packageJSON.importsMap.root.firstToken, 1622 fmt.Sprintf("This \"imports\" map was ignored because the module specifier %q is invalid:", importPath))) 1623 return PathPair{}, false, nil 1624 } 1625 1626 // The condition set is determined by the kind of import 1627 conditions := r.esmConditionsDefault 1628 switch r.kind { 1629 case ast.ImportStmt, ast.ImportDynamic: 1630 conditions = r.esmConditionsImport 1631 case ast.ImportRequire, ast.ImportRequireResolve: 1632 conditions = r.esmConditionsRequire 1633 } 1634 1635 resolvedPath, status, debug := r.esmPackageImportsResolve(importPath, packageJSON.importsMap.root, conditions) 1636 resolvedPath, status, debug = r.esmHandlePostConditions(resolvedPath, status, debug) 1637 1638 if status == pjStatusPackageResolve { 1639 // The import path was remapped via "imports" to another import path 1640 // that now needs to be resolved too. Set "forbidImports" to true 1641 // so we don't try to resolve "imports" again and end up in a loop. 1642 absolute, ok, diffCase := r.loadNodeModules(resolvedPath, dirInfoPackageJSON, true /* forbidImports */) 1643 if !ok { 1644 tracker := logger.MakeLineColumnTracker(&packageJSON.source) 1645 r.debugMeta.notes = append( 1646 []logger.MsgData{tracker.MsgData(debug.token, 1647 fmt.Sprintf("The remapped path %q could not be resolved:", resolvedPath))}, 1648 r.debugMeta.notes...) 1649 } 1650 return absolute, ok, diffCase 1651 } 1652 1653 return r.finalizeImportsExportsResult( 1654 dirInfoPackageJSON.absPath, conditions, *packageJSON.importsMap, packageJSON, 1655 resolvedPath, status, debug, 1656 "", "", "", 1657 ) 1658 } 1659 1660 esmPackageName, esmPackageSubpath, esmOK := esmParsePackageName(importPath) 1661 if r.debugLogs != nil && esmOK { 1662 r.debugLogs.addNote(fmt.Sprintf("Parsed package name %q and package subpath %q", esmPackageName, esmPackageSubpath)) 1663 } 1664 1665 // Then check for the package in any enclosing "node_modules" directories 1666 for { 1667 // Skip directories that are themselves called "node_modules", since we 1668 // don't ever want to search for "node_modules/node_modules" 1669 if dirInfo.hasNodeModules { 1670 absPath := r.fs.Join(dirInfo.absPath, "node_modules", importPath) 1671 if r.debugLogs != nil { 1672 r.debugLogs.addNote(fmt.Sprintf("Checking for a package in the directory %q", absPath)) 1673 } 1674 1675 // Check the package's package.json file 1676 if esmOK { 1677 absPkgPath := r.fs.Join(dirInfo.absPath, "node_modules", esmPackageName) 1678 if pkgDirInfo := r.dirInfoCached(absPkgPath); pkgDirInfo != nil { 1679 // Check the "exports" map 1680 if packageJSON := pkgDirInfo.packageJSON; packageJSON != nil && packageJSON.exportsMap != nil { 1681 if r.debugLogs != nil { 1682 r.debugLogs.addNote(fmt.Sprintf("Looking for %q in \"exports\" map in %q", esmPackageSubpath, packageJSON.source.KeyPath.Text)) 1683 r.debugLogs.increaseIndent() 1684 defer r.debugLogs.decreaseIndent() 1685 } 1686 1687 // The condition set is determined by the kind of import 1688 conditions := r.esmConditionsDefault 1689 switch r.kind { 1690 case ast.ImportStmt, ast.ImportDynamic: 1691 conditions = r.esmConditionsImport 1692 case ast.ImportRequire, ast.ImportRequireResolve: 1693 conditions = r.esmConditionsRequire 1694 } 1695 1696 // Resolve against the path "/", then join it with the absolute 1697 // directory path. This is done because ESM package resolution uses 1698 // URLs while our path resolution uses file system paths. We don't 1699 // want problems due to Windows paths, which are very unlike URL 1700 // paths. We also want to avoid any "%" characters in the absolute 1701 // directory path accidentally being interpreted as URL escapes. 1702 resolvedPath, status, debug := r.esmPackageExportsResolve("/", esmPackageSubpath, packageJSON.exportsMap.root, conditions) 1703 resolvedPath, status, debug = r.esmHandlePostConditions(resolvedPath, status, debug) 1704 1705 return r.finalizeImportsExportsResult( 1706 absPkgPath, conditions, *packageJSON.exportsMap, packageJSON, 1707 resolvedPath, status, debug, 1708 esmPackageName, esmPackageSubpath, absPath, 1709 ) 1710 } 1711 1712 // Check the "browser" map 1713 if remapped, ok := r.checkBrowserMap(pkgDirInfo, absPath, absolutePathKind); ok { 1714 if remapped == nil { 1715 return PathPair{Primary: logger.Path{Text: absPath, Namespace: "file", Flags: logger.PathDisabled}}, true, nil 1716 } 1717 if remappedResult, ok, diffCase := r.resolveWithoutRemapping(pkgDirInfo.enclosingBrowserScope, *remapped); ok { 1718 return remappedResult, true, diffCase 1719 } 1720 } 1721 } 1722 } 1723 1724 if absolute, ok, diffCase := r.loadAsFileOrDirectory(absPath); ok { 1725 return absolute, true, diffCase 1726 } 1727 } 1728 1729 // Go to the parent directory, stopping at the file system root 1730 dirInfo = dirInfo.parent 1731 if dirInfo == nil { 1732 break 1733 } 1734 } 1735 1736 // Then check the global "NODE_PATH" environment variable. 1737 // 1738 // Note: This is a deviation from node's published module resolution 1739 // algorithm. The published algorithm says "NODE_PATH" must take precedence 1740 // over "node_modules" paths, but it appears that the published algorithm is 1741 // incorrect. We follow node's actual behavior instead of following the 1742 // published algorithm. See also: https://github.com/nodejs/node/issues/38128. 1743 for _, absDir := range r.options.AbsNodePaths { 1744 absPath := r.fs.Join(absDir, importPath) 1745 if absolute, ok, diffCase := r.loadAsFileOrDirectory(absPath); ok { 1746 return absolute, true, diffCase 1747 } 1748 } 1749 1750 return PathPair{}, false, nil 1751} 1752 1753func (r resolverQuery) finalizeImportsExportsResult( 1754 absDirPath string, 1755 conditions map[string]bool, 1756 importExportMap pjMap, 1757 packageJSON *packageJSON, 1758 1759 // Resolution results 1760 resolvedPath string, 1761 status pjStatus, 1762 debug pjDebug, 1763 1764 // Only for exports 1765 esmPackageName string, 1766 esmPackageSubpath string, 1767 absImportPath string, 1768) (PathPair, bool, *fs.DifferentCase) { 1769 if (status == pjStatusExact || status == pjStatusInexact) && strings.HasPrefix(resolvedPath, "/") { 1770 absResolvedPath := r.fs.Join(absDirPath, resolvedPath[1:]) 1771 1772 switch status { 1773 case pjStatusExact: 1774 if r.debugLogs != nil { 1775 r.debugLogs.addNote(fmt.Sprintf("The resolved path %q is exact", absResolvedPath)) 1776 } 1777 resolvedDirInfo := r.dirInfoCached(r.fs.Dir(absResolvedPath)) 1778 if resolvedDirInfo == nil { 1779 status = pjStatusModuleNotFound 1780 } else if entry, diffCase := resolvedDirInfo.entries.Get(r.fs.Base(absResolvedPath)); entry == nil { 1781 status = pjStatusModuleNotFound 1782 } else if kind := entry.Kind(r.fs); kind == fs.DirEntry { 1783 if r.debugLogs != nil { 1784 r.debugLogs.addNote(fmt.Sprintf("The path %q is a directory, which is not allowed", absResolvedPath)) 1785 } 1786 status = pjStatusUnsupportedDirectoryImport 1787 } else if kind != fs.FileEntry { 1788 status = pjStatusModuleNotFound 1789 } else { 1790 if r.debugLogs != nil { 1791 r.debugLogs.addNote(fmt.Sprintf("Resolved to %q", absResolvedPath)) 1792 } 1793 return PathPair{Primary: logger.Path{Text: absResolvedPath, Namespace: "file"}}, true, diffCase 1794 } 1795 1796 case pjStatusInexact: 1797 // If this was resolved against an expansion key ending in a "/" 1798 // instead of a "*", we need to try CommonJS-style implicit 1799 // extension and/or directory detection. 1800 if r.debugLogs != nil { 1801 r.debugLogs.addNote(fmt.Sprintf("The resolved path %q is inexact", absResolvedPath)) 1802 } 1803 if absolute, ok, diffCase := r.loadAsFileOrDirectory(absResolvedPath); ok { 1804 return absolute, true, diffCase 1805 } 1806 status = pjStatusModuleNotFound 1807 } 1808 } 1809 1810 if strings.HasPrefix(resolvedPath, "/") { 1811 resolvedPath = "." + resolvedPath 1812 } 1813 1814 // Provide additional details about the failure to help with debugging 1815 tracker := logger.MakeLineColumnTracker(&packageJSON.source) 1816 switch status { 1817 case pjStatusInvalidModuleSpecifier: 1818 r.debugMeta.notes = []logger.MsgData{tracker.MsgData(debug.token, 1819 fmt.Sprintf("The module specifier %q is invalid:", resolvedPath))} 1820 1821 case pjStatusInvalidPackageConfiguration: 1822 r.debugMeta.notes = []logger.MsgData{tracker.MsgData(debug.token, 1823 "The package configuration has an invalid value here:")} 1824 1825 case pjStatusInvalidPackageTarget: 1826 why := fmt.Sprintf("The package target %q is invalid:", resolvedPath) 1827 if resolvedPath == "" { 1828 // "PACKAGE_TARGET_RESOLVE" is specified to throw an "Invalid 1829 // Package Target" error for what is actually an invalid package 1830 // configuration error 1831 why = "The package configuration has an invalid value here:" 1832 } 1833 r.debugMeta.notes = []logger.MsgData{tracker.MsgData(debug.token, why)} 1834 1835 case pjStatusPackagePathNotExported: 1836 r.debugMeta.notes = []logger.MsgData{tracker.MsgData(debug.token, 1837 fmt.Sprintf("The path %q is not exported by package %q:", esmPackageSubpath, esmPackageName))} 1838 1839 // If this fails, try to resolve it using the old algorithm 1840 if absolute, ok, _ := r.loadAsFileOrDirectory(absImportPath); ok && absolute.Primary.Namespace == "file" { 1841 if relPath, ok := r.fs.Rel(absDirPath, absolute.Primary.Text); ok { 1842 query := "." + path.Join("/", strings.ReplaceAll(relPath, "\\", "/")) 1843 1844 // If that succeeds, try to do a reverse lookup using the 1845 // "exports" map for the currently-active set of conditions 1846 if ok, subpath, token := r.esmPackageExportsReverseResolve( 1847 query, importExportMap.root, conditions); ok { 1848 r.debugMeta.notes = append(r.debugMeta.notes, tracker.MsgData(token, 1849 fmt.Sprintf("The file %q is exported at path %q:", query, subpath))) 1850 1851 // Provide an inline suggestion message with the correct import path 1852 actualImportPath := path.Join(esmPackageName, subpath) 1853 r.debugMeta.suggestionText = string(js_printer.QuoteForJSON(actualImportPath, false)) 1854 r.debugMeta.suggestionMessage = fmt.Sprintf("Import from %q to get the file %q:", 1855 actualImportPath, r.PrettyPath(absolute.Primary)) 1856 } 1857 } 1858 } 1859 1860 case pjStatusPackageImportNotDefined: 1861 r.debugMeta.notes = []logger.MsgData{tracker.MsgData(debug.token, 1862 fmt.Sprintf("The package import %q is not defined in this \"imports\" map:", resolvedPath))} 1863 1864 case pjStatusModuleNotFound: 1865 r.debugMeta.notes = []logger.MsgData{tracker.MsgData(debug.token, 1866 fmt.Sprintf("The module %q was not found on the file system:", resolvedPath))} 1867 1868 case pjStatusUnsupportedDirectoryImport: 1869 r.debugMeta.notes = []logger.MsgData{tracker.MsgData(debug.token, 1870 fmt.Sprintf("Importing the directory %q is not supported:", resolvedPath))} 1871 1872 case pjStatusUndefinedNoConditionsMatch: 1873 prettyPrintConditions := func(conditions []string) string { 1874 quoted := make([]string, len(conditions)) 1875 for i, condition := range conditions { 1876 quoted[i] = fmt.Sprintf("%q", condition) 1877 } 1878 return strings.Join(quoted, ", ") 1879 } 1880 keys := make([]string, 0, len(conditions)) 1881 for key := range conditions { 1882 keys = append(keys, key) 1883 } 1884 sort.Strings(keys) 1885 r.debugMeta.notes = []logger.MsgData{ 1886 tracker.MsgData(importExportMap.root.firstToken, 1887 fmt.Sprintf("The path %q is not currently exported by package %q:", 1888 esmPackageSubpath, esmPackageName)), 1889 tracker.MsgData(debug.token, 1890 fmt.Sprintf("None of the conditions provided (%s) match any of the currently active conditions (%s):", 1891 prettyPrintConditions(debug.unmatchedConditions), 1892 prettyPrintConditions(keys), 1893 ))} 1894 for _, key := range debug.unmatchedConditions { 1895 if key == "import" && (r.kind == ast.ImportRequire || r.kind == ast.ImportRequireResolve) { 1896 r.debugMeta.suggestionMessage = "Consider using an \"import\" statement to import this file:" 1897 } else if key == "require" && (r.kind == ast.ImportStmt || r.kind == ast.ImportDynamic) { 1898 r.debugMeta.suggestionMessage = "Consider using a \"require()\" call to import this file:" 1899 } 1900 } 1901 } 1902 1903 return PathPair{}, false, nil 1904} 1905 1906// Package paths are loaded from a "node_modules" directory. Non-package paths 1907// are relative or absolute paths. 1908func IsPackagePath(path string) bool { 1909 return !strings.HasPrefix(path, "/") && !strings.HasPrefix(path, "./") && 1910 !strings.HasPrefix(path, "../") && path != "." && path != ".." 1911} 1912 1913// This list can be obtained with the following command: 1914// 1915// node --experimental-wasi-unstable-preview1 -p "[...require('module').builtinModules].join('\n')" 1916// 1917// Be sure to use the *LATEST* version of node when updating this list! 1918var BuiltInNodeModules = map[string]bool{ 1919 "_http_agent": true, 1920 "_http_client": true, 1921 "_http_common": true, 1922 "_http_incoming": true, 1923 "_http_outgoing": true, 1924 "_http_server": true, 1925 "_stream_duplex": true, 1926 "_stream_passthrough": true, 1927 "_stream_readable": true, 1928 "_stream_transform": true, 1929 "_stream_wrap": true, 1930 "_stream_writable": true, 1931 "_tls_common": true, 1932 "_tls_wrap": true, 1933 "assert": true, 1934 "assert/strict": true, 1935 "async_hooks": true, 1936 "buffer": true, 1937 "child_process": true, 1938 "cluster": true, 1939 "console": true, 1940 "constants": true, 1941 "crypto": true, 1942 "dgram": true, 1943 "diagnostics_channel": true, 1944 "dns": true, 1945 "dns/promises": true, 1946 "domain": true, 1947 "events": true, 1948 "fs": true, 1949 "fs/promises": true, 1950 "http": true, 1951 "http2": true, 1952 "https": true, 1953 "inspector": true, 1954 "module": true, 1955 "net": true, 1956 "os": true, 1957 "path": true, 1958 "path/posix": true, 1959 "path/win32": true, 1960 "perf_hooks": true, 1961 "process": true, 1962 "punycode": true, 1963 "querystring": true, 1964 "readline": true, 1965 "repl": true, 1966 "stream": true, 1967 "stream/consumers": true, 1968 "stream/promises": true, 1969 "stream/web": true, 1970 "string_decoder": true, 1971 "sys": true, 1972 "timers": true, 1973 "timers/promises": true, 1974 "tls": true, 1975 "trace_events": true, 1976 "tty": true, 1977 "url": true, 1978 "util": true, 1979 "util/types": true, 1980 "v8": true, 1981 "vm": true, 1982 "wasi": true, 1983 "worker_threads": true, 1984 "zlib": true, 1985} 1986