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