package module

import (
	"fmt"
	"slices"
	"strings"
	"sync"

	"github.com/microsoft/typescript-go/internal/ast"
	"github.com/microsoft/typescript-go/internal/collections"
	"github.com/microsoft/typescript-go/internal/core"
	"github.com/microsoft/typescript-go/internal/diagnostics"
	"github.com/microsoft/typescript-go/internal/packagejson"
	"github.com/microsoft/typescript-go/internal/semver"
	"github.com/microsoft/typescript-go/internal/tspath"
)

type resolved struct {
	path                     string
	extension                string
	packageId                PackageId
	originalPath             string
	resolvedUsingTsExtension bool
}

func (r *resolved) shouldContinueSearching() bool {
	return r == nil
}

func (r *resolved) isResolved() bool {
	return r != nil && r.path != ""
}

func continueSearching() *resolved {
	return nil
}

func unresolved() *resolved {
	return &resolved{}
}

type resolutionKindSpecificLoader = func(extensions extensions, candidate string, onlyRecordFailures bool) *resolved

type tracer struct {
	traces []DiagAndArgs
}

type DiagAndArgs struct {
	Message *diagnostics.Message
	Args    []any
}

func (t *tracer) write(diag *diagnostics.Message, args ...any) {
	if t != nil {
		t.traces = append(t.traces, DiagAndArgs{Message: diag, Args: args})
	}
}

func (t *tracer) getTraces() []DiagAndArgs {
	if t != nil {
		return t.traces
	}
	return nil
}

type resolutionState struct {
	resolver *Resolver
	tracer   *tracer

	// request fields
	name                string
	containingDirectory string
	isConfigLookup      bool
	features            NodeResolutionFeatures
	esmMode             bool
	conditions          []string
	extensions          extensions
	compilerOptions     *core.CompilerOptions

	// state fields
	candidateIsFromPackageJsonField bool
	resolvedPackageDirectory        bool
	failedLookupLocations           []string
	affectingLocations              []string
	diagnostics                     []*ast.Diagnostic

	// Similar to whats on resolver but only done if compilerOptions are for project reference redirect
	// Cached representation for `core.CompilerOptions.paths`.
	// Doesn't handle other path patterns like in `typesVersions`.
	parsedPatternsForPathsOnce sync.Once
	parsedPatternsForPaths     *ParsedPatterns
}

func newResolutionState(
	name string,
	containingDirectory string,
	isTypeReferenceDirective bool,
	resolutionMode core.ResolutionMode,
	compilerOptions *core.CompilerOptions,
	redirectedReference ResolvedProjectReference,
	resolver *Resolver,
	traceBuilder *tracer,
) *resolutionState {
	state := &resolutionState{
		name:                name,
		containingDirectory: containingDirectory,
		compilerOptions:     GetCompilerOptionsWithRedirect(compilerOptions, redirectedReference),
		resolver:            resolver,
		tracer:              traceBuilder,
	}

	if isTypeReferenceDirective {
		state.extensions = extensionsDeclaration
	} else if compilerOptions.NoDtsResolution == core.TSTrue {
		state.extensions = extensionsImplementationFiles
	} else {
		state.extensions = extensionsTypeScript | extensionsJavaScript | extensionsDeclaration
	}

	if !isTypeReferenceDirective && compilerOptions.GetResolveJsonModule() {
		state.extensions |= extensionsJson
	}

	switch compilerOptions.GetModuleResolutionKind() {
	case core.ModuleResolutionKindNode16:
		state.features = NodeResolutionFeaturesNode16Default
		state.esmMode = resolutionMode == core.ModuleKindESNext
		state.conditions = GetConditions(compilerOptions, resolutionMode)
	case core.ModuleResolutionKindNodeNext:
		state.features = NodeResolutionFeaturesNodeNextDefault
		state.esmMode = resolutionMode == core.ModuleKindESNext
		state.conditions = GetConditions(compilerOptions, resolutionMode)
	case core.ModuleResolutionKindBundler:
		state.features = getNodeResolutionFeatures(compilerOptions)
		state.conditions = GetConditions(compilerOptions, resolutionMode)
	}
	return state
}

func GetCompilerOptionsWithRedirect(compilerOptions *core.CompilerOptions, redirectedReference ResolvedProjectReference) *core.CompilerOptions {
	if redirectedReference == nil {
		return compilerOptions
	}
	if optionsFromRedirect := redirectedReference.CompilerOptions(); optionsFromRedirect != nil {
		return optionsFromRedirect
	}
	return compilerOptions
}

type Resolver struct {
	caches
	host            ResolutionHost
	compilerOptions *core.CompilerOptions
	typingsLocation string
	projectName     string
	// reportDiagnostic: DiagnosticReporter
}

func NewResolver(
	host ResolutionHost,
	options *core.CompilerOptions,
	typingsLocation string,
	projectName string,
) *Resolver {
	return &Resolver{
		host:            host,
		caches:          newCaches(host.GetCurrentDirectory(), host.FS().UseCaseSensitiveFileNames(), options),
		compilerOptions: options,
		typingsLocation: typingsLocation,
		projectName:     projectName,
	}
}

func (r *Resolver) newTraceBuilder() *tracer {
	if r.compilerOptions.TraceResolution == core.TSTrue {
		return &tracer{}
	}
	return nil
}

func (r *Resolver) GetPackageScopeForPath(directory string) *packagejson.InfoCacheEntry {
	return (&resolutionState{compilerOptions: r.compilerOptions, resolver: r}).getPackageScopeForPath(directory)
}

func (r *Resolver) GetPackageJsonScopeIfApplicable(path string) *packagejson.InfoCacheEntry {
	if tspath.FileExtensionIsOneOf(path, []string{tspath.ExtensionMts, tspath.ExtensionCts, tspath.ExtensionMjs, tspath.ExtensionCjs}) {
		return nil
	}

	moduleResolutionKind := r.compilerOptions.GetModuleResolutionKind()
	if core.ModuleResolutionKindNode16 <= moduleResolutionKind && moduleResolutionKind <= core.ModuleResolutionKindNodeNext || strings.Contains(path, "/node_modules/") {
		return r.GetPackageScopeForPath(tspath.GetDirectoryPath(path))
	}

	return nil
}

func (r *tracer) traceResolutionUsingProjectReference(redirectedReference ResolvedProjectReference) {
	if redirectedReference != nil && redirectedReference.CompilerOptions() != nil {
		r.write(diagnostics.Using_compiler_options_of_project_reference_redirect_0, redirectedReference.ConfigName())
	}
}

func (r *Resolver) ResolveTypeReferenceDirective(
	typeReferenceDirectiveName string,
	containingFile string,
	resolutionMode core.ResolutionMode,
	redirectedReference ResolvedProjectReference,
) (*ResolvedTypeReferenceDirective, []DiagAndArgs) {
	traceBuilder := r.newTraceBuilder()

	compilerOptions := GetCompilerOptionsWithRedirect(r.compilerOptions, redirectedReference)
	containingDirectory := tspath.GetDirectoryPath(containingFile)

	typeRoots, fromConfig := compilerOptions.GetEffectiveTypeRoots(r.host.GetCurrentDirectory())
	if traceBuilder != nil {
		traceBuilder.write(diagnostics.Resolving_type_reference_directive_0_containing_file_1_root_directory_2, typeReferenceDirectiveName, containingFile, strings.Join(typeRoots, ","))
		traceBuilder.traceResolutionUsingProjectReference(redirectedReference)
	}

	state := newResolutionState(typeReferenceDirectiveName, containingDirectory, true /*isTypeReferenceDirective*/, resolutionMode, compilerOptions, redirectedReference, r, traceBuilder)
	result := state.resolveTypeReferenceDirective(typeRoots, fromConfig, strings.HasSuffix(containingFile, InferredTypesContainingFile))

	if traceBuilder != nil {
		traceBuilder.traceTypeReferenceDirectiveResult(typeReferenceDirectiveName, result)
	}
	return result, traceBuilder.getTraces()
}

func (r *Resolver) ResolveModuleName(moduleName string, containingFile string, resolutionMode core.ResolutionMode, redirectedReference ResolvedProjectReference) (*ResolvedModule, []DiagAndArgs) {
	traceBuilder := r.newTraceBuilder()
	compilerOptions := GetCompilerOptionsWithRedirect(r.compilerOptions, redirectedReference)
	if traceBuilder != nil {
		traceBuilder.write(diagnostics.Resolving_module_0_from_1, moduleName, containingFile)
		traceBuilder.traceResolutionUsingProjectReference(redirectedReference)
	}
	containingDirectory := tspath.GetDirectoryPath(containingFile)

	moduleResolution := compilerOptions.GetModuleResolutionKind()
	if compilerOptions.ModuleResolution != moduleResolution {
		if traceBuilder != nil {
			traceBuilder.write(diagnostics.Module_resolution_kind_is_not_specified_using_0, moduleResolution.String())
		}
	} else {
		if traceBuilder != nil {
			traceBuilder.write(diagnostics.Explicitly_specified_module_resolution_kind_Colon_0, moduleResolution.String())
		}
	}

	var result *ResolvedModule
	switch moduleResolution {
	case core.ModuleResolutionKindNode16, core.ModuleResolutionKindNodeNext, core.ModuleResolutionKindBundler:
		state := newResolutionState(moduleName, containingDirectory, false /*isTypeReferenceDirective*/, resolutionMode, compilerOptions, redirectedReference, r, traceBuilder)
		result = state.resolveNodeLike()
	default:
		panic(fmt.Sprintf("Unexpected moduleResolution: %d", moduleResolution))
	}

	if traceBuilder != nil {
		if result.IsResolved() {
			if result.PackageId.Name != "" {
				traceBuilder.write(diagnostics.Module_name_0_was_successfully_resolved_to_1_with_Package_ID_2, moduleName, result.ResolvedFileName, result.PackageId.String())
			} else {
				traceBuilder.write(diagnostics.Module_name_0_was_successfully_resolved_to_1, moduleName, result.ResolvedFileName)
			}
		} else {
			traceBuilder.write(diagnostics.Module_name_0_was_not_resolved, moduleName)
		}
	}

	return r.tryResolveFromTypingsLocation(moduleName, containingDirectory, result, traceBuilder), traceBuilder.getTraces()
}

func (r *Resolver) tryResolveFromTypingsLocation(moduleName string, containingDirectory string, originalResult *ResolvedModule, traceBuilder *tracer) *ResolvedModule {
	if r.typingsLocation == "" ||
		tspath.IsExternalModuleNameRelative(moduleName) ||
		(originalResult.ResolvedFileName != "" && tspath.ExtensionIsOneOf(originalResult.Extension, tspath.SupportedTSExtensionsWithJsonFlat)) {
		return originalResult
	}

	state := newResolutionState(
		moduleName,
		containingDirectory,
		false,               /*isTypeReferenceDirective*/
		core.ModuleKindNone, // resolutionMode,
		r.compilerOptions,
		nil, // redirectedReference,
		r,
		traceBuilder,
	)
	if traceBuilder != nil {
		traceBuilder.write(diagnostics.Auto_discovery_for_typings_is_enabled_in_project_0_Running_extra_resolution_pass_for_module_1_using_cache_location_2, r.projectName, moduleName, r.typingsLocation)
	}
	globalResolved := state.loadModuleFromImmediateNodeModulesDirectory(extensionsDeclaration, r.typingsLocation, false)
	if globalResolved == nil {
		return originalResult
	}
	result := state.createResolvedModule(globalResolved, true)
	result.FailedLookupLocations = append(originalResult.FailedLookupLocations, result.FailedLookupLocations...)
	result.AffectingLocations = append(originalResult.AffectingLocations, result.AffectingLocations...)
	result.ResolutionDiagnostics = append(originalResult.ResolutionDiagnostics, result.ResolutionDiagnostics...)
	return result
}

func (r *Resolver) resolveConfig(moduleName string, containingFile string) *ResolvedModule {
	containingDirectory := tspath.GetDirectoryPath(containingFile)
	state := newResolutionState(moduleName, containingDirectory, false /*isTypeReferenceDirective*/, core.ModuleKindCommonJS, r.compilerOptions, nil, r, nil)
	state.isConfigLookup = true
	state.extensions = extensionsJson
	return state.resolveNodeLike()
}

func (r *tracer) traceTypeReferenceDirectiveResult(typeReferenceDirectiveName string, result *ResolvedTypeReferenceDirective) {
	if !result.IsResolved() {
		r.write(diagnostics.Type_reference_directive_0_was_not_resolved, typeReferenceDirectiveName)
	} else if result.PackageId.Name != "" {
		r.write(diagnostics.Type_reference_directive_0_was_successfully_resolved_to_1_with_Package_ID_2_primary_Colon_3,
			typeReferenceDirectiveName,
			result.ResolvedFileName,
			result.PackageId.String(),
			result.Primary,
		)
	} else {
		r.write(diagnostics.Type_reference_directive_0_was_successfully_resolved_to_1_primary_Colon_2,
			typeReferenceDirectiveName,
			result.ResolvedFileName,
			result.Primary,
		)
	}
}

func (r *resolutionState) resolveTypeReferenceDirective(typeRoots []string, fromConfig bool, fromInferredTypesContainingFile bool) *ResolvedTypeReferenceDirective {
	// Primary lookup
	if len(typeRoots) > 0 {
		if r.tracer != nil {
			r.tracer.write(diagnostics.Resolving_with_primary_search_path_0, strings.Join(typeRoots, ", "))
		}
		for _, typeRoot := range typeRoots {
			candidate := r.getCandidateFromTypeRoot(typeRoot)
			directoryExists := r.resolver.host.FS().DirectoryExists(candidate)
			if !directoryExists && r.tracer != nil {
				r.tracer.write(diagnostics.Directory_0_does_not_exist_skipping_all_lookups_in_it, typeRoot)
			}
			if fromConfig {
				// Custom typeRoots resolve as file or directory just like we do modules
				if resolvedFromFile := r.loadModuleFromFile(extensionsDeclaration, candidate, !directoryExists); !resolvedFromFile.shouldContinueSearching() {
					packageDirectory := ParseNodeModuleFromPath(resolvedFromFile.path, false)
					if packageDirectory != "" {
						resolvedFromFile.packageId = r.getPackageId(resolvedFromFile.path, r.getPackageJsonInfo(packageDirectory, false /*onlyRecordFailures*/))
					}
					return r.createResolvedTypeReferenceDirective(resolvedFromFile, true /*primary*/)
				}
			}
			if resolvedFromDirectory := r.loadNodeModuleFromDirectory(extensionsDeclaration, candidate, !directoryExists, true /*considerPackageJson*/); !resolvedFromDirectory.shouldContinueSearching() {
				return r.createResolvedTypeReferenceDirective(resolvedFromDirectory, true /*primary*/)
			}
		}
	} else if r.tracer != nil {
		r.tracer.write(diagnostics.Root_directory_cannot_be_determined_skipping_primary_search_paths)
	}

	// Secondary lookup
	var resolved *resolved
	if !fromConfig || !fromInferredTypesContainingFile {
		if r.tracer != nil {
			r.tracer.write(diagnostics.Looking_up_in_node_modules_folder_initial_location_0, r.containingDirectory)
		}
		if !tspath.IsExternalModuleNameRelative(r.name) {
			resolved = r.loadModuleFromNearestNodeModulesDirectory(false /*typesScopeOnly*/)
		} else {
			candidate := normalizePathForCJSResolution(r.containingDirectory, r.name)
			resolved = r.nodeLoadModuleByRelativeName(extensionsDeclaration, candidate, false /*onlyRecordFailures*/, true /*considerPackageJson*/)
		}
	} else if r.tracer != nil {
		r.tracer.write(diagnostics.Resolving_type_reference_directive_for_program_that_specifies_custom_typeRoots_skipping_lookup_in_node_modules_folder)
	}
	return r.createResolvedTypeReferenceDirective(resolved, false /*primary*/)
}

func (r *resolutionState) getCandidateFromTypeRoot(typeRoot string) string {
	nameForLookup := r.name
	if strings.HasSuffix(typeRoot, "/node_modules/@types") || strings.HasSuffix(typeRoot, "/node_modules/@types/") {
		nameForLookup = r.mangleScopedPackageName(r.name)
	}
	return tspath.CombinePaths(typeRoot, nameForLookup)
}

func (r *resolutionState) mangleScopedPackageName(name string) string {
	mangled := MangleScopedPackageName(name)
	if r.tracer != nil && mangled != name {
		r.tracer.write(diagnostics.Scoped_package_detected_looking_in_0, mangled)
	}
	return mangled
}

func (r *resolutionState) getPackageScopeForPath(directory string) *packagejson.InfoCacheEntry {
	result, _ := tspath.ForEachAncestorDirectory(
		directory,
		func(directory string) (*packagejson.InfoCacheEntry, bool) {
			// !!! stop at global cache
			if result := r.getPackageJsonInfo(directory, false /*onlyRecordFailures*/); result != nil {
				return result, true
			}
			return nil, false
		},
	)
	return result
}

func (r *resolutionState) resolveNodeLike() *ResolvedModule {
	if r.tracer != nil {
		conditions := strings.Join(core.Map(r.conditions, func(c string) string { return `'` + c + `'` }), ", ")
		if r.esmMode {
			r.tracer.write(diagnostics.Resolving_in_0_mode_with_conditions_1, "ESM", conditions)
		} else {
			r.tracer.write(diagnostics.Resolving_in_0_mode_with_conditions_1, "CJS", conditions)
		}
	}
	result := r.resolveNodeLikeWorker()
	if r.resolvedPackageDirectory &&
		!r.isConfigLookup &&
		r.features&NodeResolutionFeaturesExports != 0 &&
		r.extensions&(extensionsTypeScript|extensionsDeclaration) != 0 &&
		!tspath.IsExternalModuleNameRelative(r.name) &&
		result.IsResolved() &&
		result.IsExternalLibraryImport &&
		!extensionIsOk(extensionsTypeScript|extensionsDeclaration, result.Extension) &&
		slices.Contains(r.conditions, "import") {
		if r.tracer != nil {
			r.tracer.write(diagnostics.Resolution_of_non_relative_name_failed_trying_with_modern_Node_resolution_features_disabled_to_see_if_npm_library_needs_configuration_update)
		}
		r.features = r.features & ^NodeResolutionFeaturesExports
		r.extensions = r.extensions & (extensionsTypeScript | extensionsDeclaration)
		diagnosticsCount := len(r.diagnostics)
		if diagnosticResult := r.resolveNodeLikeWorker(); diagnosticResult.IsResolved() && diagnosticResult.IsExternalLibraryImport {
			result.AlternateResult = diagnosticResult.ResolvedFileName
		}
		r.diagnostics = r.diagnostics[:diagnosticsCount]
	}
	return result
}

func (r *resolutionState) resolveNodeLikeWorker() *ResolvedModule {
	if resolved := r.tryLoadModuleUsingOptionalResolutionSettings(); !resolved.shouldContinueSearching() {
		return r.createResolvedModuleHandlingSymlink(resolved)
	}

	if !tspath.IsExternalModuleNameRelative(r.name) {
		if r.features&NodeResolutionFeaturesImports != 0 && strings.HasPrefix(r.name, "#") {
			if resolved := r.loadModuleFromImports(); !resolved.shouldContinueSearching() {
				return r.createResolvedModuleHandlingSymlink(resolved)
			}
		}
		if r.features&NodeResolutionFeaturesSelfName != 0 {
			if resolved := r.loadModuleFromSelfNameReference(); !resolved.shouldContinueSearching() {
				return r.createResolvedModuleHandlingSymlink(resolved)
			}
		}
		if strings.Contains(r.name, ":") {
			if r.tracer != nil {
				r.tracer.write(diagnostics.Skipping_module_0_that_looks_like_an_absolute_URI_target_file_types_Colon_1, r.name, r.extensions.String())
			}
			return r.createResolvedModule(nil, false)
		}
		if r.tracer != nil {
			r.tracer.write(diagnostics.Loading_module_0_from_node_modules_folder_target_file_types_Colon_1, r.name, r.extensions.String())
		}
		if resolved := r.loadModuleFromNearestNodeModulesDirectory(false /*typesScopeOnly*/); !resolved.shouldContinueSearching() {
			return r.createResolvedModuleHandlingSymlink(resolved)
		}
		if r.extensions&extensionsDeclaration != 0 {
			// !!!
			// if resolved := r.resolveFromTypeRoot(); !resolved.shouldContinueSearching() {
			// 	return r.createResolvedModuleHandlingSymlink(resolved)
			// }
		}
	} else {
		candidate := normalizePathForCJSResolution(r.containingDirectory, r.name)
		resolved := r.nodeLoadModuleByRelativeName(r.extensions, candidate, false, true)
		return r.createResolvedModule(
			resolved,
			resolved != nil && strings.Contains(resolved.path, "/node_modules/"),
		)
	}
	return r.createResolvedModule(nil, false)
}

func (r *resolutionState) loadModuleFromSelfNameReference() *resolved {
	directoryPath := tspath.GetNormalizedAbsolutePath(r.containingDirectory, r.resolver.host.GetCurrentDirectory())
	scope := r.getPackageScopeForPath(directoryPath)
	if !scope.Exists() || scope.Contents.Exports.IsFalsy() {
		// !!! falsy check seems wrong?
		return continueSearching()
	}
	name, ok := scope.Contents.Name.GetValue()
	if !ok {
		return continueSearching()
	}
	parts := tspath.GetPathComponents(r.name, "")
	nameParts := tspath.GetPathComponents(name, "")
	if len(parts) < len(nameParts) || !slices.Equal(nameParts, parts[:len(nameParts)]) {
		return continueSearching()
	}
	trailingParts := parts[len(nameParts):]
	var subpath string
	if len(trailingParts) > 0 {
		subpath = tspath.CombinePaths(".", trailingParts...)
	} else {
		subpath = "."
	}
	// Maybe TODO: splitting extensions into two priorities should be unnecessary, except
	// https://github.com/microsoft/TypeScript/issues/50762 makes the behavior different.
	// As long as that bug exists, we need to do two passes here in self-name loading
	// in order to be consistent with (non-self) library-name loading in
	// `loadModuleFromNearestNodeModulesDirectoryWorker`, which uses two passes in order
	// to prioritize `@types` packages higher up the directory tree over untyped
	// implementation packages. See the selfNameModuleAugmentation.ts test for why this
	// matters.
	//
	// However, there's an exception. If the user has `allowJs` and `declaration`, we need
	// to ensure that self-name imports of their own package can resolve back to their
	// input JS files via `tryLoadInputFileForPath` at a higher priority than their output
	// declaration files, so we need to do a single pass with all extensions for that case.
	if r.compilerOptions.GetAllowJS() && !strings.Contains(r.containingDirectory, "/node_modules/") {
		return r.loadModuleFromExports(scope, r.extensions, subpath)
	}
	priorityExtensions := r.extensions & (extensionsTypeScript | extensionsDeclaration)
	secondaryExtensions := r.extensions & ^(extensionsTypeScript | extensionsDeclaration)
	if resolved := r.loadModuleFromExports(scope, priorityExtensions, subpath); !resolved.shouldContinueSearching() {
		return resolved
	}
	return r.loadModuleFromExports(scope, secondaryExtensions, subpath)
}

func (r *resolutionState) loadModuleFromImports() *resolved {
	if r.name == "#" || strings.HasPrefix(r.name, "#/") {
		if r.tracer != nil {
			r.tracer.write(diagnostics.Invalid_import_specifier_0_has_no_possible_resolutions, r.name)
		}
		return continueSearching()
	}
	directoryPath := tspath.GetNormalizedAbsolutePath(r.containingDirectory, r.resolver.host.GetCurrentDirectory())
	scope := r.getPackageScopeForPath(directoryPath)
	if !scope.Exists() {
		if r.tracer != nil {
			r.tracer.write(diagnostics.Directory_0_has_no_containing_package_json_scope_Imports_will_not_resolve, directoryPath)
		}
		return continueSearching()
	}
	if scope.Contents.Imports.Type != packagejson.JSONValueTypeObject {
		// !!! Old compiler only checks for undefined, but then assumes `imports` is an object if present.
		// Maybe should have a new diagnostic for imports of an invalid type. Also, array should be handled?
		if r.tracer != nil {
			r.tracer.write(diagnostics.X_package_json_scope_0_has_no_imports_defined, scope.PackageDirectory)
		}
		return continueSearching()
	}

	if result := r.loadModuleFromExportsOrImports(r.extensions, r.name, scope.Contents.Imports.AsObject(), scope /*isImports*/, true); !result.shouldContinueSearching() {
		return result
	}

	if r.tracer != nil {
		r.tracer.write(diagnostics.Import_specifier_0_does_not_exist_in_package_json_scope_at_path_1, r.name, scope.PackageDirectory)
	}
	return continueSearching()
}

func (r *resolutionState) loadModuleFromExports(packageInfo *packagejson.InfoCacheEntry, ext extensions, subpath string) *resolved {
	// !!! This is ported exactly, but the falsy check seems wrong
	if !packageInfo.Exists() || packageInfo.Contents.Exports.IsFalsy() {
		return continueSearching()
	}

	if subpath == "." {
		var mainExport packagejson.ExportsOrImports
		switch packageInfo.Contents.Exports.Type {
		case packagejson.JSONValueTypeString, packagejson.JSONValueTypeArray:
			mainExport = packageInfo.Contents.Exports
		case packagejson.JSONValueTypeObject:
			if packageInfo.Contents.Exports.IsConditions() {
				mainExport = packageInfo.Contents.Exports
			} else if dot, ok := packageInfo.Contents.Exports.AsObject().Get("."); ok {
				mainExport = dot
			}
		}
		if mainExport.Type != packagejson.JSONValueTypeNotPresent {
			return r.loadModuleFromTargetExportOrImport(ext, subpath, packageInfo, false /*isImports*/, mainExport, "", false /*isPattern*/, ".")
		}
	} else if packageInfo.Contents.Exports.Type == packagejson.JSONValueTypeObject && packageInfo.Contents.Exports.IsSubpaths() {
		if result := r.loadModuleFromExportsOrImports(ext, subpath, packageInfo.Contents.Exports.AsObject(), packageInfo, false /*isImports*/); !result.shouldContinueSearching() {
			return result
		}
	}

	if r.tracer != nil {
		r.tracer.write(diagnostics.Export_specifier_0_does_not_exist_in_package_json_scope_at_path_1, subpath, packageInfo.PackageDirectory)
	}
	return continueSearching()
}

func (r *resolutionState) loadModuleFromExportsOrImports(
	extensions extensions,
	moduleName string,
	lookupTable *collections.OrderedMap[string, packagejson.ExportsOrImports],
	scope *packagejson.InfoCacheEntry,
	isImports bool,
) *resolved {
	if !strings.HasSuffix(moduleName, "/") && !strings.Contains(moduleName, "*") {
		if target, ok := lookupTable.Get(moduleName); ok {
			return r.loadModuleFromTargetExportOrImport(extensions, moduleName, scope, isImports, target, "", false /*isPattern*/, moduleName)
		}
	}

	expandingKeys := make([]string, 0, lookupTable.Size())
	for key := range lookupTable.Keys() {
		if strings.Count(key, "*") == 1 || strings.HasSuffix(key, "/") {
			expandingKeys = append(expandingKeys, key)
		}
	}
	slices.SortFunc(expandingKeys, ComparePatternKeys)

	for _, potentialTarget := range expandingKeys {
		if r.features&NodeResolutionFeaturesExportsPatternTrailers != 0 && matchesPatternWithTrailer(potentialTarget, moduleName) {
			target, _ := lookupTable.Get(potentialTarget)
			starPos := strings.Index(potentialTarget, "*")
			subpath := moduleName[len(potentialTarget[:starPos]) : len(moduleName)-(len(potentialTarget)-1-starPos)]
			return r.loadModuleFromTargetExportOrImport(extensions, moduleName, scope, isImports, target, subpath, true, potentialTarget)
		} else if strings.HasSuffix(potentialTarget, "*") && strings.HasPrefix(moduleName, potentialTarget[:len(potentialTarget)-1]) {
			target, _ := lookupTable.Get(potentialTarget)
			subpath := moduleName[len(potentialTarget)-1:]
			return r.loadModuleFromTargetExportOrImport(extensions, moduleName, scope, isImports, target, subpath, true, potentialTarget)
		} else if strings.HasPrefix(moduleName, potentialTarget) {
			target, _ := lookupTable.Get(potentialTarget)
			subpath := moduleName[len(potentialTarget):]
			return r.loadModuleFromTargetExportOrImport(extensions, moduleName, scope, isImports, target, subpath, false, potentialTarget)
		}
	}

	return continueSearching()
}

func (r *resolutionState) loadModuleFromTargetExportOrImport(extensions extensions, moduleName string, scope *packagejson.InfoCacheEntry, isImports bool, target packagejson.ExportsOrImports, subpath string, isPattern bool, key string) *resolved {
	switch target.Type {
	case packagejson.JSONValueTypeString:
		targetString, _ := target.Value.(string)
		if !isPattern && len(subpath) > 0 && !strings.HasSuffix(targetString, "/") {
			if r.tracer != nil {
				r.tracer.write(diagnostics.X_package_json_scope_0_has_invalid_type_for_target_of_specifier_1, scope.PackageDirectory, moduleName)
			}
			return continueSearching()
		}
		if !strings.HasPrefix(targetString, "./") {
			if isImports && !strings.HasPrefix(targetString, "../") && !strings.HasPrefix(targetString, "/") && !tspath.IsRootedDiskPath(targetString) {
				combinedLookup := targetString + subpath
				if isPattern {
					combinedLookup = strings.ReplaceAll(targetString, "*", subpath)
				}
				if r.tracer != nil {
					r.tracer.write(diagnostics.Using_0_subpath_1_with_target_2, "imports", key, combinedLookup)
					r.tracer.write(diagnostics.Resolving_module_0_from_1, combinedLookup, scope.PackageDirectory+"/")
				}
				name, containingDirectory := r.name, r.containingDirectory
				r.name, r.containingDirectory = combinedLookup, scope.PackageDirectory+"/"
				defer func() {
					r.name, r.containingDirectory = name, containingDirectory
				}()
				if result := r.resolveNodeLike(); result.IsResolved() {
					return &resolved{
						path:                     result.ResolvedFileName,
						extension:                result.Extension,
						packageId:                result.PackageId,
						originalPath:             result.OriginalPath,
						resolvedUsingTsExtension: result.ResolvedUsingTsExtension,
					}
				}
				return continueSearching()
			}
			if r.tracer != nil {
				r.tracer.write(diagnostics.X_package_json_scope_0_has_invalid_type_for_target_of_specifier_1, scope.PackageDirectory, moduleName)
			}
			return continueSearching()
		}
		var parts []string
		if tspath.PathIsRelative(targetString) {
			parts = tspath.GetPathComponents(targetString, "")[1:]
		} else {
			parts = tspath.GetPathComponents(targetString, "")
		}
		partsAfterFirst := parts[1:]
		if slices.Contains(partsAfterFirst, "..") || slices.Contains(partsAfterFirst, ".") || slices.Contains(partsAfterFirst, "node_modules") {
			if r.tracer != nil {
				r.tracer.write(diagnostics.X_package_json_scope_0_has_invalid_type_for_target_of_specifier_1, scope.PackageDirectory, moduleName)
			}
			return continueSearching()
		}
		resolvedTarget := tspath.CombinePaths(scope.PackageDirectory, targetString)
		// TODO: Assert that `resolvedTarget` is actually within the package directory? That's what the spec says.... but I'm not sure we need
		// to be in the business of validating everyone's import and export map correctness.
		subpathParts := tspath.GetPathComponents(subpath, "")
		if slices.Contains(subpathParts, "..") || slices.Contains(subpathParts, ".") || slices.Contains(subpathParts, "node_modules") {
			if r.tracer != nil {
				r.tracer.write(diagnostics.X_package_json_scope_0_has_invalid_type_for_target_of_specifier_1, scope.PackageDirectory, moduleName)
			}
			return continueSearching()
		}

		if r.tracer != nil {
			var messageTarget string
			if isPattern {
				messageTarget = strings.ReplaceAll(targetString, "*", subpath)
			} else {
				messageTarget = targetString + subpath
			}
			r.tracer.write(diagnostics.Using_0_subpath_1_with_target_2, core.IfElse(isImports, "imports", "exports"), key, messageTarget)
		}
		var finalPath string
		if isPattern {
			finalPath = tspath.GetNormalizedAbsolutePath(strings.ReplaceAll(resolvedTarget, "*", subpath), r.resolver.host.GetCurrentDirectory())
		} else {
			finalPath = tspath.GetNormalizedAbsolutePath(resolvedTarget+subpath, r.resolver.host.GetCurrentDirectory())
		}
		if inputLink := r.tryLoadInputFileForPath(finalPath, subpath, tspath.CombinePaths(scope.PackageDirectory, "package.json"), isImports); !inputLink.shouldContinueSearching() {
			return inputLink
		}
		if result := r.loadFileNameFromPackageJSONField(extensions, finalPath, targetString, false /*onlyRecordFailures*/); !result.shouldContinueSearching() {
			result.packageId = r.getPackageId(result.path, scope)
			return result
		}
		return continueSearching()

	case packagejson.JSONValueTypeObject:
		if r.tracer != nil {
			r.tracer.write(diagnostics.Entering_conditional_exports)
		}
		for condition := range target.AsObject().Keys() {
			if r.conditionMatches(condition) {
				if r.tracer != nil {
					r.tracer.write(diagnostics.Matched_0_condition_1, core.IfElse(isImports, "imports", "exports"), condition)
				}
				subTarget, _ := target.AsObject().Get(condition)
				if result := r.loadModuleFromTargetExportOrImport(extensions, moduleName, scope, isImports, subTarget, subpath, isPattern, key); !result.shouldContinueSearching() {
					if r.tracer != nil {
						r.tracer.write(diagnostics.Resolved_under_condition_0, condition)
					}
					if r.tracer != nil {
						r.tracer.write(diagnostics.Exiting_conditional_exports)
					}
					return result
				} else if r.tracer != nil {
					r.tracer.write(diagnostics.Failed_to_resolve_under_condition_0, condition)
				}
			} else {
				if r.tracer != nil {
					r.tracer.write(diagnostics.Saw_non_matching_condition_0, condition)
				}
			}
		}
		if r.tracer != nil {
			r.tracer.write(diagnostics.Exiting_conditional_exports)
		}
		return continueSearching()
	case packagejson.JSONValueTypeArray:
		if len(target.AsArray()) == 0 {
			if r.tracer != nil {
				r.tracer.write(diagnostics.X_package_json_scope_0_has_invalid_type_for_target_of_specifier_1, scope.PackageDirectory, moduleName)
			}
			return continueSearching()
		}
		for _, elem := range target.AsArray() {
			if result := r.loadModuleFromTargetExportOrImport(extensions, moduleName, scope, isImports, elem, subpath, isPattern, key); !result.shouldContinueSearching() {
				return result
			}
		}

	case packagejson.JSONValueTypeNull:
		if r.tracer != nil {
			r.tracer.write(diagnostics.X_package_json_scope_0_explicitly_maps_specifier_1_to_null, scope.PackageDirectory, moduleName)
		}
		return continueSearching()
	}

	if r.tracer != nil {
		r.tracer.write(diagnostics.X_package_json_scope_0_has_invalid_type_for_target_of_specifier_1, scope.PackageDirectory, moduleName)
	}
	return continueSearching()
}

func (r *resolutionState) tryLoadInputFileForPath(finalPath string, entry string, packagePath string, isImports bool) *resolved {
	// Replace any references to outputs for files in the program with the input files to support package self-names used with outDir
	if !r.isConfigLookup &&
		(r.compilerOptions.DeclarationDir != "" || r.compilerOptions.OutDir != "") &&
		!strings.Contains(finalPath, "/node_modules/") &&
		(r.compilerOptions.ConfigFilePath == "" || tspath.ContainsPath(
			tspath.GetDirectoryPath(packagePath),
			r.compilerOptions.ConfigFilePath,
			tspath.ComparePathsOptions{
				UseCaseSensitiveFileNames: r.resolver.host.FS().UseCaseSensitiveFileNames(),
				CurrentDirectory:          r.resolver.host.GetCurrentDirectory(),
			},
		)) {

		// Note: this differs from Strada's tryLoadInputFileForPath in that it
		// does not attempt to perform "guesses", instead requring a clear root indicator.

		var rootDir string
		if r.compilerOptions.RootDir != "" {
			// A `rootDir` compiler option strongly indicates the root location
			rootDir = r.compilerOptions.RootDir
		} else if r.compilerOptions.Composite.IsTrue() && r.compilerOptions.ConfigFilePath != "" {
			// A `composite` project is using project references and has it's common src dir set to `.`, so it shouldn't need to check any other locations
			rootDir = r.compilerOptions.ConfigFilePath
		} else {
			diagnostic := ast.NewDiagnostic(
				nil,
				core.TextRange{},
				core.IfElse(isImports,
					diagnostics.The_project_root_is_ambiguous_but_is_required_to_resolve_import_map_entry_0_in_file_1_Supply_the_rootDir_compiler_option_to_disambiguate,
					diagnostics.The_project_root_is_ambiguous_but_is_required_to_resolve_export_map_entry_0_in_file_1_Supply_the_rootDir_compiler_option_to_disambiguate,
				),
				core.IfElse(entry == "", ".", entry), // replace empty string with `.` - the reverse of the operation done when entries are built - so main entrypoint errors don't look weird
				packagePath,
			)
			r.diagnostics = append(r.diagnostics, diagnostic)
			return unresolved()
		}

		candidateDirectories := r.getOutputDirectoriesForBaseDirectory(rootDir)
		for _, candidateDir := range candidateDirectories {
			if tspath.ContainsPath(candidateDir, finalPath, tspath.ComparePathsOptions{
				UseCaseSensitiveFileNames: r.resolver.host.FS().UseCaseSensitiveFileNames(),
				CurrentDirectory:          r.resolver.host.GetCurrentDirectory(),
			}) {
				// The matched export is looking up something in either the out declaration or js dir, now map the written path back into the source dir and source extension
				pathFragment := finalPath[len(candidateDir)+1:] // +1 to also remove directory separator
				possibleInputBase := tspath.CombinePaths(rootDir, pathFragment)
				jsAndDtsExtensions := []string{tspath.ExtensionMjs, tspath.ExtensionCjs, tspath.ExtensionJs, tspath.ExtensionJson, tspath.ExtensionDmts, tspath.ExtensionDcts, tspath.ExtensionDts}
				for _, ext := range jsAndDtsExtensions {
					if tspath.FileExtensionIs(possibleInputBase, ext) {
						inputExts := r.getPossibleOriginalInputExtensionForExtension(possibleInputBase)
						for _, possibleExt := range inputExts {
							if !extensionIsOk(r.extensions, possibleExt) {
								continue
							}
							possibleInputWithInputExtension := tspath.ChangeExtension(possibleInputBase, possibleExt)
							if r.resolver.host.FS().FileExists(possibleInputWithInputExtension) {
								resolved := r.loadFileNameFromPackageJSONField(r.extensions, possibleInputWithInputExtension, "", false)
								if !resolved.shouldContinueSearching() {
									return resolved
								}
							}
						}
					}
				}
			}
		}
	}
	return continueSearching()
}

func (r *resolutionState) getOutputDirectoriesForBaseDirectory(commonSourceDirGuess string) []string {
	// Config file output paths are processed to be relative to the host's current directory, while
	// otherwise the paths are resolved relative to the common source dir the compiler puts together
	currentDir := core.IfElse(r.compilerOptions.ConfigFilePath != "", r.resolver.host.GetCurrentDirectory(), commonSourceDirGuess)
	var candidateDirectories []string
	if r.compilerOptions.DeclarationDir != "" {
		candidateDirectories = append(candidateDirectories, tspath.GetNormalizedAbsolutePath(tspath.CombinePaths(currentDir, r.compilerOptions.DeclarationDir), r.resolver.host.GetCurrentDirectory()))
	}
	if r.compilerOptions.OutDir != "" && r.compilerOptions.OutDir != r.compilerOptions.DeclarationDir {
		candidateDirectories = append(candidateDirectories, tspath.GetNormalizedAbsolutePath(tspath.CombinePaths(currentDir, r.compilerOptions.OutDir), r.resolver.host.GetCurrentDirectory()))
	}
	return candidateDirectories
}

func (r *resolutionState) getPossibleOriginalInputExtensionForExtension(path string) []string {
	if tspath.FileExtensionIsOneOf(path, []string{tspath.ExtensionDmts, tspath.ExtensionMjs, tspath.ExtensionMts}) {
		return []string{tspath.ExtensionMts, tspath.ExtensionMjs}
	}
	if tspath.FileExtensionIsOneOf(path, []string{tspath.ExtensionDcts, tspath.ExtensionCjs, tspath.ExtensionCts}) {
		return []string{tspath.ExtensionCts, tspath.ExtensionCjs}
	}
	if tspath.FileExtensionIs(path, ".d.json.ts") {
		return []string{tspath.ExtensionJson}
	}
	return []string{tspath.ExtensionTsx, tspath.ExtensionTs, tspath.ExtensionJsx, tspath.ExtensionJs}
}

func (r *resolutionState) loadModuleFromNearestNodeModulesDirectory(typesScopeOnly bool) *resolved {
	mode := core.ResolutionModeCommonJS
	if r.esmMode || r.conditionMatches("import") {
		mode = core.ResolutionModeESM
	}
	// Do (up to) two passes through node_modules:
	//   1. For each ancestor node_modules directory, try to find:
	//      i.  TS/DTS files in the implementation package
	//      ii. DTS files in the @types package
	//   2. For each ancestor node_modules directory, try to find:
	//      i.  JS files in the implementation package
	priorityExtensions := r.extensions & (extensionsTypeScript | extensionsDeclaration)
	secondaryExtensions := r.extensions & ^(extensionsTypeScript | extensionsDeclaration)
	// (1)
	if priorityExtensions != 0 {
		if r.tracer != nil {
			r.tracer.write(diagnostics.Searching_all_ancestor_node_modules_directories_for_preferred_extensions_Colon_0, priorityExtensions.String())
		}
		if result := r.loadModuleFromNearestNodeModulesDirectoryWorker(priorityExtensions, mode, typesScopeOnly); !result.shouldContinueSearching() {
			return result
		}
	}
	// (2)
	if secondaryExtensions != 0 && !typesScopeOnly {
		if r.tracer != nil {
			r.tracer.write(diagnostics.Searching_all_ancestor_node_modules_directories_for_fallback_extensions_Colon_0, secondaryExtensions.String())
		}
		return r.loadModuleFromNearestNodeModulesDirectoryWorker(secondaryExtensions, mode, typesScopeOnly)
	}
	return continueSearching()
}

func (r *resolutionState) loadModuleFromNearestNodeModulesDirectoryWorker(ext extensions, mode core.ResolutionMode, typesScopeOnly bool) *resolved {
	result, _ := tspath.ForEachAncestorDirectory(
		r.containingDirectory,
		func(directory string) (result *resolved, stop bool) {
			// !!! stop at global cache
			if tspath.GetBaseFileName(directory) != "node_modules" {
				result := r.loadModuleFromImmediateNodeModulesDirectory(ext, directory, typesScopeOnly)
				return result, !result.shouldContinueSearching()
			}
			return continueSearching(), false
		},
	)
	return result
}

func (r *resolutionState) loadModuleFromImmediateNodeModulesDirectory(extensions extensions, directory string, typesScopeOnly bool) *resolved {
	nodeModulesFolder := tspath.CombinePaths(directory, "node_modules")
	nodeModulesFolderExists := r.resolver.host.FS().DirectoryExists(nodeModulesFolder)
	if !nodeModulesFolderExists && r.tracer != nil {
		r.tracer.write(diagnostics.Directory_0_does_not_exist_skipping_all_lookups_in_it, nodeModulesFolder)
	}

	if !typesScopeOnly {
		if packageResult := r.loadModuleFromSpecificNodeModulesDirectory(extensions, r.name, nodeModulesFolder, nodeModulesFolderExists); !packageResult.shouldContinueSearching() {
			return packageResult
		}
	}

	if extensions&extensionsDeclaration != 0 {
		nodeModulesAtTypes := tspath.CombinePaths(nodeModulesFolder, "@types")
		nodeModulesAtTypesExists := nodeModulesFolderExists && r.resolver.host.FS().DirectoryExists(nodeModulesAtTypes)
		if !nodeModulesAtTypesExists && r.tracer != nil {
			r.tracer.write(diagnostics.Directory_0_does_not_exist_skipping_all_lookups_in_it, nodeModulesAtTypes)
		}
		return r.loadModuleFromSpecificNodeModulesDirectory(extensionsDeclaration, r.mangleScopedPackageName(r.name), nodeModulesAtTypes, nodeModulesAtTypesExists)
	}

	return continueSearching()
}

func (r *resolutionState) loadModuleFromSpecificNodeModulesDirectory(ext extensions, moduleName string, nodeModulesDirectory string, nodeModulesDirectoryExists bool) *resolved {
	candidate := tspath.NormalizePath(tspath.CombinePaths(nodeModulesDirectory, moduleName))
	packageName, rest := ParsePackageName(moduleName)
	packageDirectory := tspath.CombinePaths(nodeModulesDirectory, packageName)

	var rootPackageInfo *packagejson.InfoCacheEntry
	// First look for a nested package.json, as in `node_modules/foo/bar/package.json`
	packageInfo := r.getPackageJsonInfo(candidate, !nodeModulesDirectoryExists)
	// But only if we're not respecting export maps (if we are, we might redirect around this location)
	if rest != "" && packageInfo.Exists() {
		if r.features&NodeResolutionFeaturesExports != 0 {
			rootPackageInfo = r.getPackageJsonInfo(packageDirectory, !nodeModulesDirectoryExists)
		}
		if !rootPackageInfo.Exists() || rootPackageInfo.Contents.Exports.Type == packagejson.JSONValueTypeNotPresent {
			if fromFile := r.loadModuleFromFile(ext, candidate, !nodeModulesDirectoryExists); !fromFile.shouldContinueSearching() {
				return fromFile
			}

			if fromDirectory := r.loadNodeModuleFromDirectoryWorker(ext, candidate, !nodeModulesDirectoryExists, packageInfo); !fromDirectory.shouldContinueSearching() {
				fromDirectory.packageId = r.getPackageId(packageDirectory, packageInfo)
				return fromDirectory
			}
		}
	}

	loader := func(extensions extensions, candidate string, onlyRecordFailures bool) *resolved {
		if rest != "" || !r.esmMode {
			if fromFile := r.loadModuleFromFile(extensions, candidate, onlyRecordFailures); !fromFile.shouldContinueSearching() {
				fromFile.packageId = r.getPackageId(packageDirectory, packageInfo)
				return fromFile
			}
		}
		if fromDirectory := r.loadNodeModuleFromDirectoryWorker(extensions, candidate, onlyRecordFailures, packageInfo); !fromDirectory.shouldContinueSearching() {
			fromDirectory.packageId = r.getPackageId(packageDirectory, packageInfo)
			return fromDirectory
		}
		// !!! this is ported exactly, but checking for null seems wrong?
		if rest == "" && packageInfo.Exists() &&
			(packageInfo.Contents.Exports.Type == packagejson.JSONValueTypeNotPresent || packageInfo.Contents.Exports.Type == packagejson.JSONValueTypeNull) &&
			r.esmMode {
			// EsmMode disables index lookup in `loadNodeModuleFromDirectoryWorker` generally, however non-relative package resolutions still assume
			// a default `index.js` entrypoint if no `main` or `exports` are present
			if indexResult := r.loadModuleFromFile(extensions, tspath.CombinePaths(candidate, "index.js"), onlyRecordFailures); !indexResult.shouldContinueSearching() {
				indexResult.packageId = r.getPackageId(packageDirectory, packageInfo)
				return indexResult
			}
		}
		return continueSearching()
	}

	if rest != "" {
		packageInfo = rootPackageInfo
		if packageInfo == nil {
			// Previous `packageInfo` may have been from a nested package.json; ensure we have the one from the package root now.
			packageInfo = r.getPackageJsonInfo(packageDirectory, !nodeModulesDirectoryExists)
		}
	}
	if packageInfo != nil {
		r.resolvedPackageDirectory = true
		if r.features&NodeResolutionFeaturesExports != 0 &&
			packageInfo.Exists() &&
			packageInfo.Contents.Exports.Type != packagejson.JSONValueTypeNotPresent {
			// package exports are higher priority than file/directory/typesVersions lookups and (and, if there's exports present, blocks them)
			return r.loadModuleFromExports(packageInfo, ext, tspath.CombinePaths(".", rest))
		}
		if rest != "" {
			versionPaths := packageInfo.Contents.GetVersionPaths(r.getTraceFunc())
			if versionPaths.Exists() {
				if r.tracer != nil {
					r.tracer.write(diagnostics.X_package_json_has_a_typesVersions_entry_0_that_matches_compiler_version_1_looking_for_a_pattern_to_match_module_name_2, versionPaths.Version, core.Version(), rest)
				}
				packageDirectoryExists := nodeModulesDirectoryExists && r.resolver.host.FS().DirectoryExists(packageDirectory)
				pathPatterns := TryParsePatterns(versionPaths.GetPaths())
				if fromPaths := r.tryLoadModuleUsingPaths(ext, rest, packageDirectory, versionPaths.GetPaths(), pathPatterns, loader, !packageDirectoryExists); !fromPaths.shouldContinueSearching() {
					return fromPaths
				}
			}
		}
	}
	return loader(ext, candidate, !nodeModulesDirectoryExists)
}

func (r *resolutionState) createResolvedModuleHandlingSymlink(resolved *resolved) *ResolvedModule {
	isExternalLibraryImport := resolved != nil && strings.Contains(resolved.path, "/node_modules/")
	if r.compilerOptions.PreserveSymlinks != core.TSTrue &&
		isExternalLibraryImport &&
		resolved.originalPath == "" &&
		!tspath.IsExternalModuleNameRelative(r.name) {
		originalPath, resolvedFileName := r.getOriginalAndResolvedFileName(resolved.path)
		if originalPath != "" {
			resolved.path = resolvedFileName
			resolved.originalPath = originalPath
		}
	}
	return r.createResolvedModule(resolved, isExternalLibraryImport)
}

func (r *resolutionState) createResolvedModule(resolved *resolved, isExternalLibraryImport bool) *ResolvedModule {
	var resolvedModule ResolvedModule
	resolvedModule.LookupLocations = LookupLocations{
		FailedLookupLocations: r.failedLookupLocations,
		AffectingLocations:    r.affectingLocations,
		ResolutionDiagnostics: r.diagnostics,
	}

	if resolved != nil {
		resolvedModule.ResolvedFileName = resolved.path
		resolvedModule.OriginalPath = resolved.originalPath
		resolvedModule.IsExternalLibraryImport = isExternalLibraryImport
		resolvedModule.ResolvedUsingTsExtension = resolved.resolvedUsingTsExtension
		resolvedModule.Extension = resolved.extension
		resolvedModule.PackageId = resolved.packageId
	}
	return &resolvedModule
}

func (r *resolutionState) createResolvedTypeReferenceDirective(resolved *resolved, primary bool) *ResolvedTypeReferenceDirective {
	var resolvedTypeReferenceDirective ResolvedTypeReferenceDirective
	resolvedTypeReferenceDirective.LookupLocations = LookupLocations{
		FailedLookupLocations: r.failedLookupLocations,
		AffectingLocations:    r.affectingLocations,
		ResolutionDiagnostics: r.diagnostics,
	}

	if resolved.isResolved() {
		if !tspath.ExtensionIsTs(resolved.extension) {
			panic("expected a TypeScript file extension")
		}
		resolvedTypeReferenceDirective.ResolvedFileName = resolved.path
		resolvedTypeReferenceDirective.Primary = primary
		resolvedTypeReferenceDirective.PackageId = resolved.packageId
		resolvedTypeReferenceDirective.IsExternalLibraryImport = strings.Contains(resolved.path, "/node_modules/")

		if r.compilerOptions.PreserveSymlinks != core.TSTrue {
			originalPath, resolvedFileName := r.getOriginalAndResolvedFileName(resolved.path)
			if originalPath != "" {
				resolvedTypeReferenceDirective.ResolvedFileName = resolvedFileName
				resolvedTypeReferenceDirective.OriginalPath = originalPath
			}
		}
	}
	return &resolvedTypeReferenceDirective
}

func (r *resolutionState) getOriginalAndResolvedFileName(fileName string) (string, string) {
	resolvedFileName := r.realPath(fileName)
	comparePathsOptions := tspath.ComparePathsOptions{
		UseCaseSensitiveFileNames: r.resolver.host.FS().UseCaseSensitiveFileNames(),
		CurrentDirectory:          r.resolver.host.GetCurrentDirectory(),
	}
	if tspath.ComparePaths(fileName, resolvedFileName, comparePathsOptions) == 0 {
		// If the fileName and realpath are differing only in casing, prefer fileName
		// so that we can issue correct errors for casing under forceConsistentCasingInFileNames
		return "", fileName
	}
	return fileName, resolvedFileName
}

func (r *resolutionState) tryLoadModuleUsingOptionalResolutionSettings() *resolved {
	if resolved := r.tryLoadModuleUsingPathsIfEligible(); !resolved.shouldContinueSearching() {
		return resolved
	}

	if !tspath.IsExternalModuleNameRelative(r.name) {
		// No more tryLoadModuleUsingBaseUrl.
		return continueSearching()
	} else {
		return r.tryLoadModuleUsingRootDirs()
	}
}

func (r *resolutionState) getParsedPatternsForPaths() *ParsedPatterns {
	if r.compilerOptions == r.resolver.compilerOptions {
		return r.resolver.getParsedPatternsForPaths()
	}
	r.parsedPatternsForPathsOnce.Do(func() {
		r.parsedPatternsForPaths = TryParsePatterns(r.compilerOptions.Paths)
	})
	return r.parsedPatternsForPaths
}

func (r *resolutionState) tryLoadModuleUsingPathsIfEligible() *resolved {
	if r.compilerOptions.Paths.Size() > 0 && !tspath.PathIsRelative(r.name) {
		if r.tracer != nil {
			r.tracer.write(diagnostics.X_paths_option_is_specified_looking_for_a_pattern_to_match_module_name_0, r.name)
		}
	} else {
		return continueSearching()
	}
	baseDirectory := r.compilerOptions.GetPathsBasePath(r.resolver.host.GetCurrentDirectory())
	pathPatterns := r.getParsedPatternsForPaths()
	return r.tryLoadModuleUsingPaths(
		r.extensions,
		r.name,
		baseDirectory,
		r.compilerOptions.Paths,
		pathPatterns,
		func(extensions extensions, candidate string, onlyRecordFailures bool) *resolved {
			return r.nodeLoadModuleByRelativeName(extensions, candidate, onlyRecordFailures, true /*considerPackageJson*/)
		},
		false, /*onlyRecordFailures*/
	)
}

func (r *resolutionState) tryLoadModuleUsingPaths(extensions extensions, moduleName string, containingDirectory string, paths *collections.OrderedMap[string, []string], pathPatterns *ParsedPatterns, loader resolutionKindSpecificLoader, onlyRecordFailures bool) *resolved {
	if matchedPattern := MatchPatternOrExact(pathPatterns, moduleName); matchedPattern.IsValid() {
		matchedStar := matchedPattern.MatchedText(moduleName)
		if r.tracer != nil {
			r.tracer.write(diagnostics.Module_name_0_matched_pattern_1, moduleName, matchedPattern.Text)
		}
		for _, subst := range paths.GetOrZero(matchedPattern.Text) {
			path := strings.Replace(subst, "*", matchedStar, 1)
			candidate := tspath.NormalizePath(tspath.CombinePaths(containingDirectory, path))
			if r.tracer != nil {
				r.tracer.write(diagnostics.Trying_substitution_0_candidate_module_location_Colon_1, subst, path)
			}
			// A path mapping may have an extension
			if extension := tspath.TryGetExtensionFromPath(subst); extension != "" {
				if path, ok := r.tryFile(candidate, onlyRecordFailures /*onlyRecordFailures*/); ok {
					return &resolved{
						path:      path,
						extension: extension,
					}
				}
			}
			if resolved := loader(extensions, candidate, onlyRecordFailures || !r.resolver.host.FS().DirectoryExists(tspath.GetDirectoryPath(candidate))); !resolved.shouldContinueSearching() {
				return resolved
			}
		}
	}
	return continueSearching()
}

func (r *resolutionState) tryLoadModuleUsingRootDirs() *resolved {
	if len(r.compilerOptions.RootDirs) == 0 {
		return continueSearching()
	}

	if r.tracer != nil {
		r.tracer.write(diagnostics.X_rootDirs_option_is_set_using_it_to_resolve_relative_module_name_0, r.name)
	}

	candidate := tspath.NormalizePath(tspath.CombinePaths(r.containingDirectory, r.name))

	var matchedRootDir string
	var matchedNormalizedPrefix string
	for _, rootDir := range r.compilerOptions.RootDirs {
		// rootDirs are expected to be absolute
		// in case of tsconfig.json this will happen automatically - compiler will expand relative names
		// using location of tsconfig.json as base location
		normalizedRoot := tspath.NormalizePath(rootDir)
		if !strings.HasSuffix(normalizedRoot, "/") {
			normalizedRoot += "/"
		}
		isLongestMatchingPrefix := strings.HasPrefix(candidate, normalizedRoot) &&
			(matchedNormalizedPrefix == "" || len(matchedNormalizedPrefix) < len(normalizedRoot))

		if r.tracer != nil {
			r.tracer.write(diagnostics.Checking_if_0_is_the_longest_matching_prefix_for_1_2, normalizedRoot, candidate, isLongestMatchingPrefix)
		}

		if isLongestMatchingPrefix {
			matchedNormalizedPrefix = normalizedRoot
			matchedRootDir = rootDir
		}
	}

	if matchedNormalizedPrefix != "" {
		if r.tracer != nil {
			r.tracer.write(diagnostics.Longest_matching_prefix_for_0_is_1, candidate, matchedNormalizedPrefix)
		}
		suffix := candidate[len(matchedNormalizedPrefix):]

		// first - try to load from a initial location
		if r.tracer != nil {
			r.tracer.write(diagnostics.Loading_0_from_the_root_dir_1_candidate_location_2, suffix, matchedNormalizedPrefix, candidate)
		}
		loader := func(extensions extensions, candidate string, onlyRecordFailures bool) *resolved {
			return r.nodeLoadModuleByRelativeName(extensions, candidate, onlyRecordFailures, true /*considerPackageJson*/)
		}
		if resolvedFileName := loader(r.extensions, candidate, !r.resolver.host.FS().DirectoryExists(r.containingDirectory)); !resolvedFileName.shouldContinueSearching() {
			return resolvedFileName
		}

		if r.tracer != nil {
			r.tracer.write(diagnostics.Trying_other_entries_in_rootDirs)
		}
		// then try to resolve using remaining entries in rootDirs
		for _, rootDir := range r.compilerOptions.RootDirs {
			if rootDir == matchedRootDir {
				// skip the initially matched entry
				continue
			}
			candidate := tspath.CombinePaths(tspath.NormalizePath(rootDir), suffix)
			if r.tracer != nil {
				r.tracer.write(diagnostics.Loading_0_from_the_root_dir_1_candidate_location_2, suffix, rootDir, candidate)
			}
			baseDirectory := tspath.GetDirectoryPath(candidate)
			if resolvedFileName := loader(r.extensions, candidate, !r.resolver.host.FS().DirectoryExists(baseDirectory)); !resolvedFileName.shouldContinueSearching() {
				return resolvedFileName
			}
		}
		if r.tracer != nil {
			r.tracer.write(diagnostics.Module_resolution_using_rootDirs_has_failed)
		}
	}
	return continueSearching()
}

func (r *resolutionState) nodeLoadModuleByRelativeName(extensions extensions, candidate string, onlyRecordFailures bool, considerPackageJson bool) *resolved {
	if r.tracer != nil {
		r.tracer.write(diagnostics.Loading_module_as_file_Slash_folder_candidate_module_location_0_target_file_types_Colon_1, candidate, extensions.String())
	}
	if !tspath.HasTrailingDirectorySeparator(candidate) {
		if !onlyRecordFailures {
			parentOfCandidate := tspath.GetDirectoryPath(candidate)
			if !r.resolver.host.FS().DirectoryExists(parentOfCandidate) {
				if r.tracer != nil {
					r.tracer.write(diagnostics.Directory_0_does_not_exist_skipping_all_lookups_in_it, parentOfCandidate)
				}
				onlyRecordFailures = true
			}
		}
		resolvedFromFile := r.loadModuleFromFile(extensions, candidate, onlyRecordFailures)
		if resolvedFromFile != nil {
			if considerPackageJson {
				if packageDirectory := ParseNodeModuleFromPath(resolvedFromFile.path /*isFolder*/, false); packageDirectory != "" {
					resolvedFromFile.packageId = r.getPackageId(resolvedFromFile.path, r.getPackageJsonInfo(packageDirectory /*onlyRecordFailures*/, false))
				}
			}
			return resolvedFromFile
		}
	}
	if !onlyRecordFailures {
		candidateExists := r.resolver.host.FS().DirectoryExists(candidate)
		if !candidateExists {
			if r.tracer != nil {
				r.tracer.write(diagnostics.Directory_0_does_not_exist_skipping_all_lookups_in_it, candidate)
			}
			onlyRecordFailures = true
		}
	}
	// esm mode relative imports shouldn't do any directory lookups (either inside `package.json`
	// files or implicit `index.js`es). This is a notable departure from cjs norms, where `./foo/pkg`
	// could have been redirected by `./foo/pkg/package.json` to an arbitrary location!
	if !r.esmMode {
		return r.loadNodeModuleFromDirectory(extensions, candidate, onlyRecordFailures, considerPackageJson)
	}
	return continueSearching()
}

func (r *resolutionState) loadModuleFromFile(extensions extensions, candidate string, onlyRecordFailures bool) *resolved {
	// ./foo.js -> ./foo.ts
	resolvedByReplacingExtension := r.loadModuleFromFileNoImplicitExtensions(extensions, candidate, onlyRecordFailures)
	if resolvedByReplacingExtension != nil {
		return resolvedByReplacingExtension
	}

	// ./foo -> ./foo.ts
	if !r.esmMode {
		return r.tryAddingExtensions(candidate, extensions, "", onlyRecordFailures)
	}

	return continueSearching()
}

func (r *resolutionState) loadModuleFromFileNoImplicitExtensions(extensions extensions, candidate string, onlyRecordFailures bool) *resolved {
	base := tspath.GetBaseFileName(candidate)
	if !strings.Contains(base, ".") {
		return continueSearching() // extensionless import, no lookups performed, since we don't support extensionless files
	}
	extensionless := tspath.RemoveFileExtension(candidate)
	if extensionless == candidate {
		// Once TS native extensions are handled, handle arbitrary extensions for declaration file mapping
		extensionless = candidate[:strings.LastIndex(candidate, ".")]
	}

	extension := candidate[len(extensionless):]
	if r.tracer != nil {
		r.tracer.write(diagnostics.File_name_0_has_a_1_extension_stripping_it, candidate, extension)
	}
	return r.tryAddingExtensions(extensionless, extensions, extension, onlyRecordFailures)
}

func (r *resolutionState) tryAddingExtensions(extensionless string, extensions extensions, originalExtension string, onlyRecordFailures bool) *resolved {
	if !onlyRecordFailures {
		directory := tspath.GetDirectoryPath(extensionless)
		onlyRecordFailures = directory != "" && !r.resolver.host.FS().DirectoryExists(directory)
	}

	switch originalExtension {
	case tspath.ExtensionMjs, tspath.ExtensionMts, tspath.ExtensionDmts:
		if extensions&extensionsTypeScript != 0 {
			if resolved := r.tryExtension(tspath.ExtensionMts, extensionless, originalExtension == tspath.ExtensionMts || originalExtension == tspath.ExtensionDmts, onlyRecordFailures); !resolved.shouldContinueSearching() {
				return resolved
			}
		}
		if extensions&extensionsDeclaration != 0 {
			if resolved := r.tryExtension(tspath.ExtensionDmts, extensionless, originalExtension == tspath.ExtensionMts || originalExtension == tspath.ExtensionDmts, onlyRecordFailures); !resolved.shouldContinueSearching() {
				return resolved
			}
		}
		if extensions&extensionsJavaScript != 0 {
			if resolved := r.tryExtension(tspath.ExtensionMjs, extensionless, false, onlyRecordFailures); !resolved.shouldContinueSearching() {
				return resolved
			}
		}
		return continueSearching()
	case tspath.ExtensionCjs, tspath.ExtensionCts, tspath.ExtensionDcts:
		if extensions&extensionsTypeScript != 0 {
			if resolved := r.tryExtension(tspath.ExtensionCts, extensionless, originalExtension == tspath.ExtensionCts || originalExtension == tspath.ExtensionDcts, onlyRecordFailures); !resolved.shouldContinueSearching() {
				return resolved
			}
		}
		if extensions&extensionsDeclaration != 0 {
			if resolved := r.tryExtension(tspath.ExtensionDcts, extensionless, originalExtension == tspath.ExtensionCts || originalExtension == tspath.ExtensionDcts, onlyRecordFailures); !resolved.shouldContinueSearching() {
				return resolved
			}
		}
		if extensions&extensionsJavaScript != 0 {
			if resolved := r.tryExtension(tspath.ExtensionCjs, extensionless, false, onlyRecordFailures); !resolved.shouldContinueSearching() {
				return resolved
			}
		}
		return continueSearching()
	case tspath.ExtensionJson:
		if extensions&extensionsDeclaration != 0 {
			if resolved := r.tryExtension(".d.json.ts", extensionless, false, onlyRecordFailures); !resolved.shouldContinueSearching() {
				return resolved
			}
		}
		if extensions&extensionsJson != 0 {
			if resolved := r.tryExtension(tspath.ExtensionJson, extensionless, false, onlyRecordFailures); !resolved.shouldContinueSearching() {
				return resolved
			}
		}
		return continueSearching()
	case tspath.ExtensionTsx, tspath.ExtensionJsx:
		// basically idendical to the ts/js case below, but prefers matching tsx and jsx files exactly before falling back to the ts or js file path
		// (historically, we disallow having both a a.ts and a.tsx file in the same compilation, since their outputs clash)
		// TODO: We should probably error if `"./a.tsx"` resolved to `"./a.ts"`, right?
		if extensions&extensionsTypeScript != 0 {
			if resolved := r.tryExtension(tspath.ExtensionTsx, extensionless, originalExtension == tspath.ExtensionTsx, onlyRecordFailures); !resolved.shouldContinueSearching() {
				return resolved
			}
			if resolved := r.tryExtension(tspath.ExtensionTs, extensionless, originalExtension == tspath.ExtensionTsx, onlyRecordFailures); !resolved.shouldContinueSearching() {
				return resolved
			}
		}
		if extensions&extensionsDeclaration != 0 {
			if resolved := r.tryExtension(tspath.ExtensionDts, extensionless, originalExtension == tspath.ExtensionTsx, onlyRecordFailures); !resolved.shouldContinueSearching() {
				return resolved
			}
		}
		if extensions&extensionsJavaScript != 0 {
			if resolved := r.tryExtension(tspath.ExtensionJsx, extensionless, false, onlyRecordFailures); !resolved.shouldContinueSearching() {
				return resolved
			}
			if resolved := r.tryExtension(tspath.ExtensionJs, extensionless, false, onlyRecordFailures); !resolved.shouldContinueSearching() {
				return resolved
			}
		}
		return continueSearching()
	case tspath.ExtensionTs, tspath.ExtensionDts, tspath.ExtensionJs, "":
		if extensions&extensionsTypeScript != 0 {
			if resolved := r.tryExtension(tspath.ExtensionTs, extensionless, originalExtension == tspath.ExtensionTs || originalExtension == tspath.ExtensionDts, onlyRecordFailures); !resolved.shouldContinueSearching() {
				return resolved
			}
			if resolved := r.tryExtension(tspath.ExtensionTsx, extensionless, originalExtension == tspath.ExtensionTs || originalExtension == tspath.ExtensionDts, onlyRecordFailures); !resolved.shouldContinueSearching() {
				return resolved
			}
		}
		if extensions&extensionsDeclaration != 0 {
			if resolved := r.tryExtension(tspath.ExtensionDts, extensionless, originalExtension == tspath.ExtensionTs || originalExtension == tspath.ExtensionDts, onlyRecordFailures); !resolved.shouldContinueSearching() {
				return resolved
			}
		}
		if extensions&extensionsJavaScript != 0 {
			if resolved := r.tryExtension(tspath.ExtensionJs, extensionless, false, onlyRecordFailures); !resolved.shouldContinueSearching() {
				return resolved
			}
			if resolved := r.tryExtension(tspath.ExtensionJsx, extensionless, false, onlyRecordFailures); !resolved.shouldContinueSearching() {
				return resolved
			}
		}
		if r.isConfigLookup {
			if resolved := r.tryExtension(tspath.ExtensionJson, extensionless, false, onlyRecordFailures); !resolved.shouldContinueSearching() {
				return resolved
			}
		}
		return continueSearching()
	default:
		if extensions&extensionsDeclaration != 0 && !tspath.IsDeclarationFileName(extensionless+originalExtension) {
			if resolved := r.tryExtension(".d"+originalExtension+".ts", extensionless, false, onlyRecordFailures); !resolved.shouldContinueSearching() {
				return resolved
			}
		}
		return continueSearching()
	}
}

func (r *resolutionState) tryExtension(extension string, extensionless string, resolvedUsingTsExtension bool, onlyRecordFailures bool) *resolved {
	fileName := extensionless + extension
	if path, ok := r.tryFile(fileName, onlyRecordFailures); ok {
		return &resolved{
			path:                     path,
			extension:                extension,
			resolvedUsingTsExtension: !r.candidateIsFromPackageJsonField && resolvedUsingTsExtension,
		}
	}
	return continueSearching()
}

func (r *resolutionState) tryFile(fileName string, onlyRecordFailures bool) (string, bool) {
	if len(r.compilerOptions.ModuleSuffixes) == 0 {
		return fileName, r.tryFileLookup(fileName, onlyRecordFailures)
	}

	ext := tspath.TryGetExtensionFromPath(fileName)
	fileNameNoExtension := tspath.RemoveExtension(fileName, ext)
	for _, suffix := range r.compilerOptions.ModuleSuffixes {
		path := fileNameNoExtension + suffix + ext
		if r.tryFileLookup(path, onlyRecordFailures) {
			return path, true
		}
	}
	return fileName, false
}

func (r *resolutionState) tryFileLookup(fileName string, onlyRecordFailures bool) bool {
	if !onlyRecordFailures {
		if r.resolver.host.FS().FileExists(fileName) {
			if r.tracer != nil {
				r.tracer.write(diagnostics.File_0_exists_use_it_as_a_name_resolution_result, fileName)
			}
			return true
		} else if r.tracer != nil {
			r.tracer.write(diagnostics.File_0_does_not_exist, fileName)
		}
	}
	r.failedLookupLocations = append(r.failedLookupLocations, fileName)
	return false
}

func (r *resolutionState) loadNodeModuleFromDirectory(extensions extensions, candidate string, onlyRecordFailures bool, considerPackageJson bool) *resolved {
	var packageInfo *packagejson.InfoCacheEntry
	if considerPackageJson {
		packageInfo = r.getPackageJsonInfo(candidate, onlyRecordFailures)
	}

	return r.loadNodeModuleFromDirectoryWorker(extensions, candidate, onlyRecordFailures, packageInfo)
}

func (r *resolutionState) loadNodeModuleFromDirectoryWorker(ext extensions, candidate string, onlyRecordFailures bool, packageInfo *packagejson.InfoCacheEntry) *resolved {
	var (
		packageFile                      string
		onlyRecordFailuresForPackageFile bool
		versionPaths                     packagejson.VersionPaths
	)
	if packageInfo.Exists() {
		versionPaths = packageInfo.Contents.GetVersionPaths(r.getTraceFunc())
		if tspath.ComparePaths(candidate, packageInfo.PackageDirectory, tspath.ComparePathsOptions{UseCaseSensitiveFileNames: r.resolver.host.FS().UseCaseSensitiveFileNames()}) == 0 {
			if file, ok := r.getPackageFile(ext, packageInfo); ok {
				packageFile = file
				onlyRecordFailuresForPackageFile = !r.resolver.host.FS().DirectoryExists(tspath.GetDirectoryPath(file))
			}
		}
	}

	loader := func(extensions extensions, candidate string, onlyRecordFailures bool) *resolved {
		if fromFile := r.loadFileNameFromPackageJSONField(extensions, candidate, packageFile, onlyRecordFailures); !fromFile.shouldContinueSearching() {
			return fromFile
		}

		// Even if `extensions == extensionsDeclaration`, we can still look up a .ts file as a result of package.json "types"
		// !!! should we not set this before the filename lookup above?
		expandedExtensions := extensions
		if extensions == extensionsDeclaration {
			expandedExtensions = extensionsTypeScript | extensionsDeclaration
		}

		// Disable `esmMode` for the resolution of the package path for CJS-mode packages (so the `main` field can omit extensions)
		saveESMMode := r.esmMode
		saveCandidateIsFromPackageJsonField := r.candidateIsFromPackageJsonField
		r.candidateIsFromPackageJsonField = true
		if packageInfo.Exists() && packageInfo.Contents.Type.Value != "module" {
			r.esmMode = false
		}
		result := r.nodeLoadModuleByRelativeName(expandedExtensions, candidate, onlyRecordFailures, false /*considerPackageJson*/)
		r.esmMode = saveESMMode
		r.candidateIsFromPackageJsonField = saveCandidateIsFromPackageJsonField
		return result
	}

	var indexPath string
	if r.isConfigLookup {
		indexPath = tspath.CombinePaths(candidate, "tsconfig")
	} else {
		indexPath = tspath.CombinePaths(candidate, "index")
	}

	if versionPaths.Exists() && (packageFile == "" || tspath.ContainsPath(candidate, packageFile, tspath.ComparePathsOptions{})) {
		var moduleName string
		if packageFile != "" {
			moduleName = tspath.GetRelativePathFromDirectory(candidate, packageFile, tspath.ComparePathsOptions{})
		} else {
			moduleName = tspath.GetRelativePathFromDirectory(candidate, indexPath, tspath.ComparePathsOptions{})
		}
		if r.tracer != nil {
			r.tracer.write(diagnostics.X_package_json_has_a_typesVersions_entry_0_that_matches_compiler_version_1_looking_for_a_pattern_to_match_module_name_2, versionPaths.Version, core.Version(), moduleName)
		}
		pathPatterns := TryParsePatterns(versionPaths.GetPaths())
		if result := r.tryLoadModuleUsingPaths(ext, moduleName, candidate, versionPaths.GetPaths(), pathPatterns, loader, onlyRecordFailuresForPackageFile); !result.shouldContinueSearching() {
			if result.packageId.Name != "" {
				// !!! are these asserts really necessary?
				panic("expected packageId to be empty")
			}
			return result
		}
	}

	if packageFile != "" {
		if packageFileResult := loader(ext, packageFile, onlyRecordFailuresForPackageFile); !packageFileResult.shouldContinueSearching() {
			if packageFileResult.packageId.Name != "" {
				// !!! are these asserts really necessary?
				panic("expected packageId to be empty")
			}
			return packageFileResult
		}
	}

	// ESM mode resolutions don't do package 'index' lookups
	if !r.esmMode {
		return r.loadModuleFromFile(ext, indexPath, onlyRecordFailures || !r.resolver.host.FS().DirectoryExists(candidate))
	}
	return continueSearching()
}

// This function is only ever called with paths written in package.json files - never
// module specifiers written in source files - and so it always allows the
// candidate to end with a TS extension (but will also try substituting a JS extension for a TS extension).
func (r *resolutionState) loadFileNameFromPackageJSONField(extensions extensions, candidate string, packageJSONValue string, onlyRecordFailures bool) *resolved {
	if extensions&extensionsTypeScript != 0 && tspath.HasImplementationTSFileExtension(candidate) || extensions&extensionsDeclaration != 0 && tspath.IsDeclarationFileName(candidate) {
		if path, ok := r.tryFile(candidate, onlyRecordFailures); ok {
			extension := tspath.TryExtractTSExtension(path)
			return &resolved{
				path:                     path,
				extension:                extension,
				resolvedUsingTsExtension: packageJSONValue != "" && !strings.HasSuffix(packageJSONValue, extension),
			}
		}
		return continueSearching()
	}

	if r.isConfigLookup && extensions&extensionsJson != 0 && tspath.FileExtensionIs(candidate, tspath.ExtensionJson) {
		if path, ok := r.tryFile(candidate, onlyRecordFailures); ok {
			return &resolved{
				path:      path,
				extension: tspath.ExtensionJson,
			}
		}
	}

	return r.loadModuleFromFileNoImplicitExtensions(extensions, candidate, onlyRecordFailures)
}

func (r *resolutionState) getPackageFile(extensions extensions, packageInfo *packagejson.InfoCacheEntry) (string, bool) {
	if !packageInfo.Exists() {
		return "", false
	}
	if r.isConfigLookup {
		return r.getPackageJSONPathField("tsconfig", &packageInfo.Contents.TSConfig, packageInfo.PackageDirectory)
	}
	if extensions&extensionsDeclaration != 0 {
		if packageFile, ok := r.getPackageJSONPathField("typings", &packageInfo.Contents.Typings, packageInfo.PackageDirectory); ok {
			return packageFile, ok
		}
		if packageFile, ok := r.getPackageJSONPathField("types", &packageInfo.Contents.Types, packageInfo.PackageDirectory); ok {
			return packageFile, ok
		}
	}
	if extensions&(extensionsImplementationFiles|extensionsDeclaration) != 0 {
		return r.getPackageJSONPathField("main", &packageInfo.Contents.Main, packageInfo.PackageDirectory)
	}
	return "", false
}

func (r *resolutionState) getPackageJsonInfo(packageDirectory string, onlyRecordFailures bool) *packagejson.InfoCacheEntry {
	packageJsonPath := tspath.CombinePaths(packageDirectory, "package.json")
	if onlyRecordFailures {
		r.failedLookupLocations = append(r.failedLookupLocations, packageJsonPath)
		return nil
	}

	if existing := r.resolver.packageJsonInfoCache.Get(packageJsonPath); existing != nil {
		if existing.Contents != nil {
			if r.tracer != nil {
				r.tracer.write(diagnostics.File_0_exists_according_to_earlier_cached_lookups, packageJsonPath)
			}
			r.affectingLocations = append(r.affectingLocations, packageJsonPath)
			if existing.PackageDirectory == packageDirectory {
				return existing
			}
			// https://github.com/microsoft/TypeScript/pull/50740
			return &packagejson.InfoCacheEntry{
				PackageDirectory: packageDirectory,
				DirectoryExists:  true,
				Contents:         existing.Contents,
			}
		} else {
			if existing.DirectoryExists && r.tracer != nil {
				r.tracer.write(diagnostics.File_0_does_not_exist_according_to_earlier_cached_lookups, packageJsonPath)
			}
			r.failedLookupLocations = append(r.failedLookupLocations, packageJsonPath)
			return nil
		}
	}

	directoryExists := r.resolver.host.FS().DirectoryExists(packageDirectory)
	if directoryExists && r.resolver.host.FS().FileExists(packageJsonPath) {
		// Ignore error
		contents, _ := r.resolver.host.FS().ReadFile(packageJsonPath)
		packageJsonContent, err := packagejson.Parse([]byte(contents))
		if r.tracer != nil {
			r.tracer.write(diagnostics.Found_package_json_at_0, packageJsonPath)
		}
		result := &packagejson.InfoCacheEntry{
			PackageDirectory: packageDirectory,
			DirectoryExists:  true,
			Contents: &packagejson.PackageJson{
				Fields:    packageJsonContent,
				Parseable: err == nil,
			},
		}
		result = r.resolver.packageJsonInfoCache.Set(packageJsonPath, result)
		r.affectingLocations = append(r.affectingLocations, packageJsonPath)
		return result
	} else {
		if directoryExists && r.tracer != nil {
			r.tracer.write(diagnostics.File_0_does_not_exist, packageJsonPath)
		}
		_ = r.resolver.packageJsonInfoCache.Set(packageJsonPath, &packagejson.InfoCacheEntry{
			PackageDirectory: packageDirectory,
			DirectoryExists:  directoryExists,
		})
		r.failedLookupLocations = append(r.failedLookupLocations, packageJsonPath)
	}
	return nil
}

func (r *resolutionState) getPackageId(resolvedFileName string, packageInfo *packagejson.InfoCacheEntry) PackageId {
	if packageInfo.Exists() {
		packageJsonContent := packageInfo.Contents
		if name, ok := packageJsonContent.Name.GetValue(); ok {
			if version, ok := packageJsonContent.Version.GetValue(); ok {
				var subModuleName string
				if len(resolvedFileName) > len(packageInfo.PackageDirectory) {
					subModuleName = resolvedFileName[len(packageInfo.PackageDirectory)+1:]
				}
				return PackageId{
					Name:             name,
					Version:          version,
					SubModuleName:    subModuleName,
					PeerDependencies: r.readPackageJsonPeerDependencies(packageInfo),
				}
			}
		}
	}
	return PackageId{}
}

func (r *resolutionState) readPackageJsonPeerDependencies(packageJsonInfo *packagejson.InfoCacheEntry) string {
	peerDependencies := packageJsonInfo.Contents.PeerDependencies
	ok := r.validatePackageJSONField("peerDependencies", &peerDependencies)
	if !ok || len(peerDependencies.Value) == 0 {
		return ""
	}
	if r.tracer != nil {
		r.tracer.write(diagnostics.X_package_json_has_a_peerDependencies_field)
	}
	packageDirectory := r.realPath(packageJsonInfo.PackageDirectory)
	nodeModulesIndex := strings.LastIndex(packageDirectory, "/node_modules")
	if nodeModulesIndex == -1 {
		return ""
	}
	nodeModules := packageDirectory[:nodeModulesIndex+len("/node_modules")] + "/"
	builder := strings.Builder{}
	for name := range peerDependencies.Value {
		peerPackageJson := r.getPackageJsonInfo(nodeModules+name /*onlyRecordFailures*/, false)
		if peerPackageJson != nil {
			version := peerPackageJson.Contents.Version.Value
			builder.WriteString("+")
			builder.WriteString(name)
			builder.WriteString("@")
			builder.WriteString(version)
			if r.tracer != nil {
				r.tracer.write(diagnostics.Found_peerDependency_0_with_1_version, name, version)
			}
		} else if r.tracer != nil {
			r.tracer.write(diagnostics.Failed_to_find_peerDependency_0, name)
		}
	}
	return builder.String()
}

func (r *resolutionState) realPath(path string) string {
	rp := tspath.NormalizePath(r.resolver.host.FS().Realpath(path))
	if r.tracer != nil {
		r.tracer.write(diagnostics.Resolving_real_path_for_0_result_1, path, rp)
	}
	return rp
}

func (r *resolutionState) validatePackageJSONField(fieldName string, field packagejson.TypeValidatedField) bool {
	if field.IsPresent() {
		if field.IsValid() {
			return true
		}
		if r.tracer != nil {
			r.tracer.write(diagnostics.Expected_type_of_0_field_in_package_json_to_be_1_got_2, fieldName, field.ExpectedJSONType(), field.ActualJSONType())
		}
	}
	if r.tracer != nil {
		r.tracer.write(diagnostics.X_package_json_does_not_have_a_0_field, fieldName)
	}
	return false
}

func (r *resolutionState) getPackageJSONPathField(fieldName string, field *packagejson.Expected[string], directory string) (string, bool) {
	if !r.validatePackageJSONField(fieldName, field) {
		return "", false
	}
	if field.Value == "" {
		if r.tracer != nil {
			r.tracer.write(diagnostics.X_package_json_had_a_falsy_0_field, fieldName)
		}
		return "", false
	}
	path := tspath.NormalizePath(tspath.CombinePaths(directory, field.Value))
	if r.tracer != nil {
		r.tracer.write(diagnostics.X_package_json_has_0_field_1_that_references_2, fieldName, field.Value, path)
	}
	return path, true
}

func (r *resolutionState) conditionMatches(condition string) bool {
	if condition == "default" || slices.Contains(r.conditions, condition) {
		return true
	}
	if !slices.Contains(r.conditions, "types") {
		return false // only apply versioned types conditions if the types condition is applied
	}
	if !strings.HasPrefix(condition, "types@") {
		return false
	}
	if versionRange, ok := semver.TryParseVersionRange(condition[len("types@"):]); ok {
		return versionRange.Test(&typeScriptVersion)
	}
	return false
}

func (r *resolutionState) getTraceFunc() func(m *diagnostics.Message, args ...any) {
	if r.tracer != nil {
		return r.tracer.write
	}
	return nil
}

func GetConditions(options *core.CompilerOptions, resolutionMode core.ResolutionMode) []string {
	moduleResolution := options.GetModuleResolutionKind()
	if resolutionMode == core.ModuleKindNone && moduleResolution == core.ModuleResolutionKindBundler {
		resolutionMode = core.ModuleKindESNext
	}
	conditions := make([]string, 0, 3+len(options.CustomConditions))
	if resolutionMode == core.ModuleKindESNext {
		conditions = append(conditions, "import")
	} else {
		conditions = append(conditions, "require")
	}

	if options.NoDtsResolution != core.TSTrue {
		conditions = append(conditions, "types")
	}
	if moduleResolution != core.ModuleResolutionKindBundler {
		conditions = append(conditions, "node")
	}
	conditions = core.Concatenate(conditions, options.CustomConditions)
	return conditions
}

func getNodeResolutionFeatures(options *core.CompilerOptions) NodeResolutionFeatures {
	features := NodeResolutionFeaturesNone

	switch options.GetModuleResolutionKind() {
	case core.ModuleResolutionKindNode16:
		features = NodeResolutionFeaturesNode16Default
	case core.ModuleResolutionKindNodeNext:
		features = NodeResolutionFeaturesNodeNextDefault
	case core.ModuleResolutionKindBundler:
		features = NodeResolutionFeaturesBundlerDefault
	}
	if options.ResolvePackageJsonExports == core.TSTrue {
		features |= NodeResolutionFeaturesExports
	} else if options.ResolvePackageJsonExports == core.TSFalse {
		features &^= NodeResolutionFeaturesExports
	}
	if options.ResolvePackageJsonImports == core.TSTrue {
		features |= NodeResolutionFeaturesImports
	} else if options.ResolvePackageJsonImports == core.TSFalse {
		features &^= NodeResolutionFeaturesImports
	}
	return features
}

func moveToNextDirectorySeparatorIfAvailable(path string, prevSeparatorIndex int, isFolder bool) int {
	offset := prevSeparatorIndex + 1
	nextSeparatorIndex := strings.Index(path[offset:], "/")
	if nextSeparatorIndex == -1 {
		if isFolder {
			return len(path)
		}
		return prevSeparatorIndex
	}
	return nextSeparatorIndex + offset
}

type ParsedPatterns struct {
	matchableStringSet collections.Set[string]
	patterns           []core.Pattern
}

func (r *Resolver) getParsedPatternsForPaths() *ParsedPatterns {
	r.parsedPatternsForPathsOnce.Do(func() {
		r.parsedPatternsForPaths = TryParsePatterns(r.compilerOptions.Paths)
	})
	return r.parsedPatternsForPaths
}

func TryParsePatterns(pathMappings *collections.OrderedMap[string, []string]) *ParsedPatterns {
	paths := pathMappings.Keys()

	numPatterns := 0
	for path := range paths {
		if pattern := core.TryParsePattern(path); pattern.IsValid() && pattern.StarIndex == -1 {
			numPatterns++
		}
	}
	numMatchables := pathMappings.Size() - numPatterns

	var patterns []core.Pattern
	var matchableStringSet collections.Set[string]
	if numPatterns != 0 {
		patterns = make([]core.Pattern, 0, numPatterns)
	}
	if numMatchables != 0 {
		matchableStringSet = *collections.NewSetWithSizeHint[string](numMatchables)
	}

	for path := range paths {
		if pattern := core.TryParsePattern(path); pattern.IsValid() {
			if pattern.StarIndex == -1 {
				matchableStringSet.Add(path)
			} else {
				patterns = append(patterns, pattern)
			}
		}
	}
	return &ParsedPatterns{
		matchableStringSet: matchableStringSet,
		patterns:           patterns,
	}
}

func MatchPatternOrExact(patterns *ParsedPatterns, candidate string) core.Pattern {
	if patterns.matchableStringSet.Has(candidate) {
		return core.Pattern{
			Text:      candidate,
			StarIndex: -1,
		}
	}
	if len(patterns.patterns) == 0 {
		return core.Pattern{}
	}
	return core.FindBestPatternMatch(patterns.patterns, core.Identity, candidate)
}

// If you import from "." inside a containing directory "/foo", the result of `tspath.NormalizePath`
// would be "/foo", but this loses the information that `foo` is a directory and we intended
// to look inside of it. The Node CommonJS resolution algorithm doesn't call this out
// (https://nodejs.org/api/modules.html#all-together), but it seems that module paths ending
// in `.` are actually normalized to `./` before proceeding with the resolution algorithm.
func normalizePathForCJSResolution(containingDirectory string, moduleName string) string {
	combined := tspath.CombinePaths(containingDirectory, moduleName)
	parts := tspath.GetPathComponents(combined, "")
	lastPart := parts[len(parts)-1]
	if lastPart == "." || lastPart == ".." {
		return tspath.EnsureTrailingDirectorySeparator(tspath.NormalizePath(combined))
	}
	return tspath.NormalizePath(combined)
}

func matchesPatternWithTrailer(target string, name string) bool {
	if strings.HasSuffix(target, "*") {
		return false
	}
	before, after, ok := strings.Cut(target, "*")
	if !ok {
		return false
	}
	return strings.HasPrefix(name, before) && strings.HasSuffix(name, after)
}

/** True if `extension` is one of the supported `extensions`. */
func extensionIsOk(extensions extensions, extension string) bool {
	return (extensions&extensionsJavaScript != 0 && (extension == tspath.ExtensionJs || extension == tspath.ExtensionJsx || extension == tspath.ExtensionMjs || extension == tspath.ExtensionCjs) ||
		(extensions&extensionsTypeScript != 0 && (extension == tspath.ExtensionTs || extension == tspath.ExtensionTsx || extension == tspath.ExtensionMts || extension == tspath.ExtensionCts)) ||
		(extensions&extensionsDeclaration != 0 && (extension == tspath.ExtensionDts || extension == tspath.ExtensionDmts || extension == tspath.ExtensionDcts)) ||
		(extensions&extensionsJson != 0 && extension == tspath.ExtensionJson))
}

func ResolveConfig(moduleName string, containingFile string, host ResolutionHost) *ResolvedModule {
	resolver := NewResolver(host, &core.CompilerOptions{ModuleResolution: core.ModuleResolutionKindNodeNext}, "", "")
	return resolver.resolveConfig(moduleName, containingFile)
}

func GetAutomaticTypeDirectiveNames(options *core.CompilerOptions, host ResolutionHost) []string {
	if options.Types != nil {
		return options.Types
	}

	var result []string
	typeRoots, _ := options.GetEffectiveTypeRoots(host.GetCurrentDirectory())
	for _, root := range typeRoots {
		if host.FS().DirectoryExists(root) {
			for _, typeDirectivePath := range host.FS().GetAccessibleEntries(root).Directories {
				normalized := tspath.NormalizePath(typeDirectivePath)
				packageJsonPath := tspath.CombinePaths(root, normalized, "package.json")
				isNotNeededPackage := false
				if host.FS().FileExists(packageJsonPath) {
					contents, _ := host.FS().ReadFile(packageJsonPath)
					packageJsonContent, _ := packagejson.Parse([]byte(contents))
					// `types-publisher` sometimes creates packages with `"typings": null` for packages that don't provide their own types.
					// See `createNotNeededPackageJSON` in the types-publisher` repo.
					isNotNeededPackage = packageJsonContent.Typings.Null
				}
				if !isNotNeededPackage {
					baseFileName := tspath.GetBaseFileName(normalized)
					if !strings.HasPrefix(baseFileName, ".") {
						result = append(result, baseFileName)
					}
				}
			}
		}
	}
	return result
}
