package module

import (
	"fmt"
	"maps"
	"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/stringutil"
	"github.com/microsoft/typescript-go/internal/tspath"
	"github.com/microsoft/typescript-go/internal/vfs/vfsmatch"
)

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) *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
	resolvePackageDirectoryOnly bool

	// state fields
	// candidateEndingIsFromConfig is set when the candidate file extension originated from
	// configuration (package.json fields, tsconfig.json paths entries, or wildcard substitutions)
	// rather than from the module specifier written in source code. When true, resolvedUsingTsExtension
	// is suppressed so the checker does not attempt to extract a TS extension from the original specifier.
	candidateEndingIsFromConfig bool
	resolvedPackageDirectory    bool
	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
}

type ResolverOptions struct {
	PackageJsonCache *packagejson.InfoCache
}

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 NewResolverWithOptions(
	host ResolutionHost,
	compilerOptions *core.CompilerOptions,
	typingsLocation string,
	projectName string,
	opts ResolverOptions,
) *Resolver {
	r := &Resolver{
		host:            host,
		compilerOptions: compilerOptions,
		typingsLocation: typingsLocation,
		projectName:     projectName,
	}
	if opts.PackageJsonCache != nil {
		r.packageJsonInfoCache = opts.PackageJsonCache
	} else {
		r.caches = newCaches(host.GetCurrentDirectory(), host.FS().UseCaseSensitiveFileNames(), compilerOptions)
	}
	return r
}

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) PackageJsonCacheEntries(f func(key tspath.Path, value *packagejson.InfoCacheEntry) bool) {
	r.caches.packageJsonInfoCache.Range(f)
}

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) {
	containingDirectory := tspath.GetDirectoryPath(containingFile)
	traceBuilder := r.newTraceBuilder()

	fromInferredTypesContainingFile := strings.HasSuffix(containingFile, InferredTypesContainingFile)

	cacheKey := typeRefDirectiveResolutionCacheKey{
		containingDirectory:             containingDirectory,
		typeReferenceName:               typeReferenceDirectiveName,
		resolutionMode:                  resolutionMode,
		redirectConfigName:              getRedirectConfigName(redirectedReference),
		fromInferredTypesContainingFile: fromInferredTypesContainingFile,
	}

	if traceBuilder == nil {
		if cached, ok := r.typeRefDirectiveResolutionCache.Get(cacheKey); ok {
			return cached, nil
		}
	}

	compilerOptions := GetCompilerOptionsWithRedirect(r.compilerOptions, redirectedReference)

	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, fromInferredTypesContainingFile)

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

	r.typeRefDirectiveResolutionCache.Set(cacheKey, result)

	return result, traceBuilder.getTraces()
}

func (r *Resolver) ResolveModuleName(moduleName string, containingFile string, resolutionMode core.ResolutionMode, redirectedReference ResolvedProjectReference) (*ResolvedModule, []DiagAndArgs) {
	containingDirectory := tspath.GetDirectoryPath(containingFile)
	traceBuilder := r.newTraceBuilder()

	cacheKey := moduleResolutionCacheKey{
		containingDirectory: containingDirectory,
		moduleName:          moduleName,
		resolutionMode:      resolutionMode,
		redirectConfigName:  getRedirectConfigName(redirectedReference),
	}

	if traceBuilder == nil {
		if cached, ok := r.moduleResolutionCache.Get(cacheKey); ok {
			return cached, nil
		}
	}

	compilerOptions := GetCompilerOptionsWithRedirect(r.compilerOptions, redirectedReference)
	if traceBuilder != nil {
		traceBuilder.write(diagnostics.Resolving_module_0_from_1, moduleName, containingFile)
		traceBuilder.traceResolutionUsingProjectReference(redirectedReference)
	}

	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)
		}
	}

	finalResult := r.tryResolveFromTypingsLocation(moduleName, containingDirectory, result, traceBuilder)
	r.moduleResolutionCache.Set(cacheKey, finalResult)

	return finalResult, traceBuilder.getTraces()
}

func (r *Resolver) ResolvePackageDirectory(moduleName string, containingFile string, resolutionMode core.ResolutionMode, redirectedReference ResolvedProjectReference) *ResolvedModule {
	compilerOptions := GetCompilerOptionsWithRedirect(r.compilerOptions, redirectedReference)
	containingDirectory := tspath.GetDirectoryPath(containingFile)
	state := newResolutionState(moduleName, containingDirectory, false /*isTypeReferenceDirective*/, resolutionMode, compilerOptions, redirectedReference, r, nil)
	state.resolvePackageDirectoryOnly = true
	if result := state.loadModuleFromNearestNodeModulesDirectory(false /*typesScopeOnly*/); result != nil && result.path != "" {
		return state.createResolvedModuleHandlingSymlink(result)
	}
	return nil
}

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.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(typeRoot)
			if !directoryExists {
				if r.tracer != nil {
					r.tracer.write(diagnostics.Directory_0_does_not_exist_skipping_all_lookups_in_it, typeRoot)
				}
				continue
			}
			if fromConfig {
				// Custom typeRoots resolve as file or directory just like we do modules
				if resolvedFromFile := r.loadModuleFromFile(extensionsDeclaration, candidate); !resolvedFromFile.shouldContinueSearching() {
					packageDirectory := ParseNodeModuleFromPath(resolvedFromFile.path, false)
					if packageDirectory != "" {
						resolvedFromFile.packageId = r.getPackageId(resolvedFromFile.path, r.getPackageJsonInfo(packageDirectory))
					}
					return r.createResolvedTypeReferenceDirective(resolvedFromFile, true /*primary*/)
				}
			}
			if resolvedFromDirectory := r.loadNodeModuleFromDirectory(extensionsDeclaration, candidate, 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, 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
}

// resolveFromTypeRoot tries to resolve a module name from the configured typeRoots.
// This is used as a fallback after node_modules resolution fails, for declaration file lookups.
// Returns nil if typeRoots is not configured or if no matching module is found in any typeRoot directory.
func (r *resolutionState) resolveFromTypeRoot() *resolved {
	if r.compilerOptions.TypeRoots == nil {
		return nil
	}
	for _, typeRoot := range r.compilerOptions.TypeRoots {
		candidate := r.getCandidateFromTypeRoot(typeRoot)
		directoryExists := r.resolver.host.FS().DirectoryExists(typeRoot)
		if !directoryExists {
			if r.tracer != nil {
				r.tracer.write(diagnostics.Directory_0_does_not_exist_skipping_all_lookups_in_it, typeRoot)
			}
			continue
		}
		if resolvedFromFile := r.loadModuleFromFile(extensionsDeclaration, candidate); !resolvedFromFile.shouldContinueSearching() {
			packageDirectory := ParseNodeModuleFromPath(resolvedFromFile.path, false)
			if packageDirectory != "" {
				resolvedFromFile.packageId = r.getPackageId(resolvedFromFile.path, r.getPackageJsonInfo(packageDirectory))
			}
			return resolvedFromFile
		}
		if resolved := r.loadNodeModuleFromDirectory(extensionsDeclaration, candidate, true /*considerPackageJson*/); !resolved.shouldContinueSearching() {
			return resolved
		}
	}
	return nil
}

func (r *resolutionState) getPackageScopeForPath(directory string) *packagejson.InfoCacheEntry {
	result := tspath.ForEachAncestorDirectoryStoppingAtGlobalCache(
		r.resolver.typingsLocation,
		directory,
		func(directory string) (*packagejson.InfoCacheEntry, bool) {
			if result := r.getPackageJsonInfo(directory); 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, 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, "#/") && (r.features&NodeResolutionFeaturesImportsPatternRoot) == 0) {
		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)
				}
				scopeContainingDirectory := tspath.EnsureTrailingDirectorySeparator(scope.PackageDirectory)
				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, scopeContainingDirectory)
				}
				name, containingDirectory := r.name, r.containingDirectory
				r.name, r.containingDirectory = combinedLookup, scopeContainingDirectory
				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() {
			inputLink.packageId = r.getPackageId(inputLink.path, scope)
			return inputLink
		}
		if result := r.loadFileNameFromPackageJSONField(extensions, finalPath, targetString); !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 result.isResolved() && 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 unresolved()
	}

	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.ConfigFilePath != "" {
			// When no explicit rootDir is set, treat the config file's directory as the project root, which establishes the common source directory, so no other locations need to be checked.
			rootDir = tspath.GetDirectoryPath(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
				var pathFragment string
				if len(finalPath) > len(candidateDir) {
					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 := tspath.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, "")
								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) 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")
	if !r.resolver.host.FS().DirectoryExists(nodeModulesFolder) {
		if r.tracer != nil {
			r.tracer.write(diagnostics.Directory_0_does_not_exist_skipping_all_lookups_in_it, nodeModulesFolder)
		}
		return continueSearching()
	}

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

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

	return continueSearching()
}

func (r *resolutionState) loadModuleFromSpecificNodeModulesDirectory(ext extensions, moduleName string, nodeModulesDirectory string) *resolved {
	// Strip any trailing directory separator so that imports like `pkg/` and `pkg`
	// produce identical `candidate` and `packageDirectory` strings. Otherwise the
	// `package.json` info cache (which is keyed by normalized path but stores the
	// caller's `PackageDirectory` verbatim) can hand back, under concurrent
	// inserts, an entry whose `PackageDirectory` doesn't match `candidate`,
	// causing `loadNodeModuleFromDirectoryWorker`'s `ComparePaths(candidate, ...)`
	// check to fail and skip loading the package's `main`/`types` entry.
	// https://github.com/microsoft/typescript-go/issues/3526
	candidate := tspath.RemoveTrailingDirectorySeparator(tspath.NormalizePath(tspath.CombinePaths(nodeModulesDirectory, moduleName)))
	packageName, rest := ParsePackageName(moduleName)
	packageDirectory := tspath.CombinePaths(nodeModulesDirectory, packageName)
	if packageName == "" {
		packageDirectory = candidate
	}

	if r.resolvePackageDirectoryOnly {
		if r.resolver.host.FS().DirectoryExists(packageDirectory) {
			return &resolved{path: packageDirectory}
		}
		return continueSearching()
	}

	var rootPackageInfo *packagejson.InfoCacheEntry
	// First look for a nested package.json, as in `node_modules/foo/bar/package.json`
	packageInfo := r.getPackageJsonInfo(candidate)
	// 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)
		}
		if !rootPackageInfo.Exists() || rootPackageInfo.Contents.Exports.Type == packagejson.JSONValueTypeNotPresent {
			if fromFile := r.loadModuleFromFile(ext, candidate); !fromFile.shouldContinueSearching() {
				return fromFile
			}

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

	loader := func(extensions extensions, candidate string) *resolved {
		if rest != "" || !r.esmMode {
			if fromFile := r.loadModuleFromFile(extensions, candidate); !fromFile.shouldContinueSearching() {
				fromFile.packageId = r.getPackageId(fromFile.path, packageInfo)
				return fromFile
			}
		}
		if fromDirectory := r.loadNodeModuleFromDirectoryWorker(extensions, candidate, packageInfo); !fromDirectory.shouldContinueSearching() {
			fromDirectory.packageId = r.getPackageId(fromDirectory.path, packageInfo)
			return fromDirectory
		}
		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")); !indexResult.shouldContinueSearching() {
				indexResult.packageId = r.getPackageId(indexResult.path, 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)
		}
	}
	if packageInfo != nil {
		r.resolvedPackageDirectory = true
		if r.features&NodeResolutionFeaturesExports != 0 &&
			packageInfo.Exists() &&
			!packageInfo.Contents.Exports.IsFalsy() {
			// package exports are higher priority than file/directory/typesVersions lookups and (and, if there's exports present*, blocks them)
			// *Well, weirdly enough a top-level `"exports": null` does NOT block fallback resolution.
			// https://github.com/microsoft/TypeScript/pull/49327
			return r.loadModuleFromExports(packageInfo, ext, tspath.CombinePaths(".", rest))
		}
		if rest != "" && packageInfo.Exists() {
			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)
				}
				pathPatterns := TryParsePatterns(versionPaths.GetPaths())
				if fromPaths := r.tryLoadModuleUsingPaths(ext, rest, packageDirectory, versionPaths.GetPaths(), pathPatterns, loader); !fromPaths.shouldContinueSearching() {
					return fromPaths
				}
			}
		}
	}
	return loader(ext, candidate)
}

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.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.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) *resolved {
			return r.nodeLoadModuleByRelativeName(extensions, candidate, true /*considerPackageJson*/)
		},
	)
}

func (r *resolutionState) tryLoadModuleUsingPaths(extensions extensions, moduleName string, containingDirectory string, paths *collections.OrderedMap[string, []string], pathPatterns *ParsedPatterns, loader resolutionKindSpecificLoader) *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
			extensionFromSubst := tspath.TryGetExtensionFromPath(subst)
			if extensionFromSubst != "" {
				if path, ok := r.tryFile(candidate); ok {
					return &resolved{
						path:      path,
						extension: extensionFromSubst,
					}
				}
			}
			// When the substitution path has an explicit extension, the extension came from the
			// paths config, not the module specifier. Suppress resolvedUsingTsExtension in that case.
			saveCandidateEndingIsFromConfig := r.candidateEndingIsFromConfig
			if extensionFromSubst != "" {
				r.candidateEndingIsFromConfig = true
			}
			resolved := loader(extensions, candidate)
			r.candidateEndingIsFromConfig = saveCandidateEndingIsFromConfig
			if !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) *resolved {
			return r.nodeLoadModuleByRelativeName(extensions, candidate, true /*considerPackageJson*/)
		}
		if resolvedFileName := loader(r.extensions, candidate); !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)
			}
			if resolvedFileName := loader(r.extensions, candidate); !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, 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) {
		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)
			}
			return continueSearching()
		}
		resolvedFromFile := r.loadModuleFromFile(extensions, candidate)
		if resolvedFromFile != nil {
			if considerPackageJson {
				if packageDirectory := ParseNodeModuleFromPath(resolvedFromFile.path /*isFolder*/, false); packageDirectory != "" {
					resolvedFromFile.packageId = r.getPackageId(resolvedFromFile.path, r.getPackageJsonInfo(packageDirectory))
				}
			}
			return resolvedFromFile
		}
	}
	if !r.resolver.host.FS().DirectoryExists(candidate) {
		if r.tracer != nil {
			r.tracer.write(diagnostics.Directory_0_does_not_exist_skipping_all_lookups_in_it, candidate)
		}
		return continueSearching()
	}
	// 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, considerPackageJson)
	}
	return continueSearching()
}

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

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

	return continueSearching()
}

func (r *resolutionState) loadModuleFromFileNoImplicitExtensions(extensions extensions, candidate string) *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)
}

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

	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); !resolved.shouldContinueSearching() {
				return resolved
			}
		}
		if extensions&extensionsDeclaration != 0 {
			if resolved := r.tryExtension(tspath.ExtensionDmts, extensionless, originalExtension == tspath.ExtensionMts || originalExtension == tspath.ExtensionDmts); !resolved.shouldContinueSearching() {
				return resolved
			}
		}
		if extensions&extensionsJavaScript != 0 {
			if resolved := r.tryExtension(tspath.ExtensionMjs, extensionless, false); !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); !resolved.shouldContinueSearching() {
				return resolved
			}
		}
		if extensions&extensionsDeclaration != 0 {
			if resolved := r.tryExtension(tspath.ExtensionDcts, extensionless, originalExtension == tspath.ExtensionCts || originalExtension == tspath.ExtensionDcts); !resolved.shouldContinueSearching() {
				return resolved
			}
		}
		if extensions&extensionsJavaScript != 0 {
			if resolved := r.tryExtension(tspath.ExtensionCjs, extensionless, false); !resolved.shouldContinueSearching() {
				return resolved
			}
		}
		return continueSearching()
	case tspath.ExtensionJson:
		if extensions&extensionsDeclaration != 0 {
			if resolved := r.tryExtension(".d.json.ts", extensionless, false); !resolved.shouldContinueSearching() {
				return resolved
			}
		}
		if extensions&extensionsJson != 0 {
			if resolved := r.tryExtension(tspath.ExtensionJson, extensionless, false); !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); !resolved.shouldContinueSearching() {
				return resolved
			}
			if resolved := r.tryExtension(tspath.ExtensionTs, extensionless, originalExtension == tspath.ExtensionTsx); !resolved.shouldContinueSearching() {
				return resolved
			}
		}
		if extensions&extensionsDeclaration != 0 {
			if resolved := r.tryExtension(tspath.ExtensionDts, extensionless, originalExtension == tspath.ExtensionTsx); !resolved.shouldContinueSearching() {
				return resolved
			}
		}
		if extensions&extensionsJavaScript != 0 {
			if resolved := r.tryExtension(tspath.ExtensionJsx, extensionless, false); !resolved.shouldContinueSearching() {
				return resolved
			}
			if resolved := r.tryExtension(tspath.ExtensionJs, extensionless, false); !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); !resolved.shouldContinueSearching() {
				return resolved
			}
			if resolved := r.tryExtension(tspath.ExtensionTsx, extensionless, originalExtension == tspath.ExtensionTs || originalExtension == tspath.ExtensionDts); !resolved.shouldContinueSearching() {
				return resolved
			}
		}
		if extensions&extensionsDeclaration != 0 {
			if resolved := r.tryExtension(tspath.ExtensionDts, extensionless, originalExtension == tspath.ExtensionTs || originalExtension == tspath.ExtensionDts); !resolved.shouldContinueSearching() {
				return resolved
			}
		}
		if extensions&extensionsJavaScript != 0 {
			if resolved := r.tryExtension(tspath.ExtensionJs, extensionless, false); !resolved.shouldContinueSearching() {
				return resolved
			}
			if resolved := r.tryExtension(tspath.ExtensionJsx, extensionless, false); !resolved.shouldContinueSearching() {
				return resolved
			}
		}
		if r.isConfigLookup {
			if resolved := r.tryExtension(tspath.ExtensionJson, extensionless, false); !resolved.shouldContinueSearching() {
				return resolved
			}
		}
		return continueSearching()
	default:
		if extensions&extensionsDeclaration != 0 && !tspath.IsDeclarationFileName(extensionless+originalExtension) {
			if resolved := r.tryExtension(".d"+originalExtension+".ts", extensionless, false); !resolved.shouldContinueSearching() {
				return resolved
			}
		}
		return continueSearching()
	}
}

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

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

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

func (r *resolutionState) tryFileLookup(fileName string) bool {
	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)
	}
	return false
}

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

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

func (r *resolutionState) loadNodeModuleFromDirectoryWorker(ext extensions, candidate string, packageInfo *packagejson.InfoCacheEntry) *resolved {
	var (
		packageFile  string
		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
			}
		}
	}

	loader := func(extensions extensions, candidate string) *resolved {
		if fromFile := r.loadFileNameFromPackageJSONField(extensions, candidate, packageFile); !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
		saveCandidateEndingIsFromConfig := r.candidateEndingIsFromConfig
		r.candidateEndingIsFromConfig = true
		if packageInfo.Exists() && packageInfo.Contents.Type.Value != "module" {
			r.esmMode = false
		}
		result := r.nodeLoadModuleByRelativeName(expandedExtensions, candidate, false /*considerPackageJson*/)
		r.esmMode = saveESMMode
		r.candidateEndingIsFromConfig = saveCandidateEndingIsFromConfig
		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); !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); !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 {
		if !r.resolver.host.FS().DirectoryExists(candidate) {
			return continueSearching()
		}
		return r.loadModuleFromFile(ext, indexPath)
	}
	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) *resolved {
	if extensions&extensionsTypeScript != 0 && tspath.HasImplementationTSFileExtension(candidate) || extensions&extensionsDeclaration != 0 && tspath.IsDeclarationFileName(candidate) {
		if path, ok := r.tryFile(candidate); ok {
			extension := tspath.TryExtractTSExtension(path)
			// resolvedUsingTsExtension should be true when the pattern ends with * and the
			// candidate file ends in a TS extension. This means the * matched a TS extension
			// from the module specifier. For example:
			// - import "pkg/foo.ts" with pattern "./*" -> true
			// - import "pkg/foo.ts.omg" with pattern "./*.omg" -> true (star matched .ts)
			// - import "pkg/foo" with pattern "./*.ts" -> false (extension in pattern, not specifier)
			resolvedUsingTsExtension := strings.HasSuffix(packageJSONValue, "*") && extension != ""
			return &resolved{
				path:                     path,
				extension:                extension,
				resolvedUsingTsExtension: resolvedUsingTsExtension,
			}
		}
		return continueSearching()
	}

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

	return r.loadModuleFromFileNoImplicitExtensions(extensions, candidate)
}

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) *packagejson.InfoCacheEntry {
	packageJsonPath := tspath.CombinePaths(packageDirectory, "package.json")

	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)
			}
			return existing.WithPackageDirectory(packageDirectory)
		} else {
			if existing.DirectoryExists && r.tracer != nil {
				r.tracer.write(diagnostics.File_0_does_not_exist_according_to_earlier_cached_lookups, 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)
		return result.WithPackageDirectory(packageDirectory)
	} 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,
		})
	}
	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")] + "/"
	names := slices.AppendSeq(make([]string, 0, len(peerDependencies.Value)), maps.Keys(peerDependencies.Value))
	slices.Sort(names)
	builder := strings.Builder{}
	for _, name := range names {
		peerPackageJson := r.getPackageJsonInfo(nodeModules + name)
		if peerPackageJson.Exists() {
			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
	}
	return IsApplicableVersionedTypesKey(condition)
}

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 := -1
	if offset <= len(path) {
		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.UsesWildcardTypes() {
		if options.Types != nil {
			return options.Types
		}
		return []string{}
	}

	// Walk the primary type lookup locations
	var wildcardMatches []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, ".") {
						wildcardMatches = append(wildcardMatches, baseFileName)
					}
				}
			}
		}
	}

	// Order potentially matters in program construction, so substitute
	// in the wildcard in the position it was specified in the types array
	var result []string
	for _, t := range options.Types {
		if t == "*" {
			result = append(result, wildcardMatches...)
		} else {
			result = append(result, t)
		}
	}
	return core.Deduplicate(result)
}

type Ending int

const (
	// EndingFixed indicates that the module specifier cannot be changed without changing its resolution.
	EndingFixed Ending = iota
	// EndingExtensionChangeable indicates that the module specifier's extension portion was inferred from a
	// file on disk, so an interchangeable one could be used instead (e.g. replacing .d.ts with .js).
	EndingExtensionChangeable
	// EndingChangeable indicates that the module specifier's file name and extension portion were inferred
	// from a file on disk without being matched as part of an 'exports' pattern, so can be changed according
	// to the importer's module resolution rules (e.g. an /index.d.ts may be dropped entirely in CommonJS settings).
	EndingChangeable
)

type ResolvedEntrypoint struct {
	// OriginalFileName is the symlink path if the entrypoint was discovered at a symlink. Empty otherwise.
	OriginalFileName string
	// ResolvedFileName is the real path to the entrypoint file.
	ResolvedFileName string
	ModuleSpecifier  string
	// Ending indicates whether the file name and extension portion of ModuleSpecifier is fixed or can be changed.
	Ending Ending
	// IncludeConditions are the conditions that a resolver must have to reach this entrypoint.
	IncludeConditions *collections.Set[string]
	// ExcludeConditions are the conditions that a resolver must not have to reach this entrypoint.
	ExcludeConditions *collections.Set[string]
}

func (e *ResolvedEntrypoint) SymlinkOrRealpath() string {
	if e.OriginalFileName != "" {
		return e.OriginalFileName
	}
	return e.ResolvedFileName
}

func (r *Resolver) GetEntrypointsFromPackageJsonInfo(packageJson *packagejson.InfoCacheEntry, packageName string, enableDirectorySearch bool) []*ResolvedEntrypoint {
	extensions := extensionsTypeScript | extensionsDeclaration
	features := NodeResolutionFeaturesAll
	state := &resolutionState{resolver: r, extensions: extensions, features: features, compilerOptions: r.compilerOptions}
	if packageJson.Exists() && packageJson.Contents.Exports.IsPresent() {
		entrypoints := state.loadEntrypointsFromExportMap(packageJson, packageName, packageJson.Contents.Exports)
		return entrypoints
	}

	var result []*ResolvedEntrypoint
	mainResolution := state.loadNodeModuleFromDirectoryWorker(
		extensions,
		packageJson.PackageDirectory,
		packageJson,
	)

	if mainResolution.isResolved() {
		result = append(result, r.createResolvedEntrypointHandlingSymlink(
			mainResolution.path,
			packageName,
			nil,
			nil,
			EndingFixed,
		))
	}

	if enableDirectorySearch {
		otherFiles := vfsmatch.ReadDirectory(
			r.host.FS(),
			r.host.GetCurrentDirectory(),
			packageJson.PackageDirectory,
			extensions.Array(),
			[]string{"node_modules"},
			[]string{"**/*"},
			vfsmatch.UnlimitedDepth,
		)

		comparePathsOptions := tspath.ComparePathsOptions{UseCaseSensitiveFileNames: r.host.FS().UseCaseSensitiveFileNames()}
		for _, file := range otherFiles {
			if mainResolution.isResolved() && tspath.ComparePaths(file, mainResolution.path, comparePathsOptions) == 0 {
				continue
			}

			result = append(result, r.createResolvedEntrypointHandlingSymlink(
				file,
				tspath.ResolvePath(packageName, tspath.GetRelativePathFromDirectory(packageJson.PackageDirectory, file, comparePathsOptions)),
				nil,
				nil,
				EndingChangeable,
			))
		}
	}

	if len(result) > 0 {
		return result
	}
	return nil
}

func (r *Resolver) createResolvedEntrypointHandlingSymlink(fileName string, moduleSpecifier string, includeConditions *collections.Set[string], excludeConditions *collections.Set[string], ending Ending) *ResolvedEntrypoint {
	var originalFileName string
	resolvedFileName := fileName
	if realPath := r.host.FS().Realpath(fileName); realPath != fileName {
		originalFileName = fileName
		resolvedFileName = realPath
	}
	return &ResolvedEntrypoint{
		OriginalFileName:  originalFileName,
		ResolvedFileName:  resolvedFileName,
		ModuleSpecifier:   moduleSpecifier,
		IncludeConditions: includeConditions,
		ExcludeConditions: excludeConditions,
		Ending:            ending,
	}
}

func (r *resolutionState) loadEntrypointsFromExportMap(
	packageJson *packagejson.InfoCacheEntry,
	packageName string,
	exports packagejson.ExportsOrImports,
) []*ResolvedEntrypoint {
	var loadEntrypointsFromTargetExports func(subpath string, includeConditions *collections.Set[string], excludeConditions *collections.Set[string], exports packagejson.ExportsOrImports)
	var entrypoints []*ResolvedEntrypoint

	loadEntrypointsFromTargetExports = func(subpath string, includeConditions *collections.Set[string], excludeConditions *collections.Set[string], exports packagejson.ExportsOrImports) {
		if exports.Type == packagejson.JSONValueTypeString && strings.HasPrefix(exports.AsString(), "./") {
			if strings.ContainsRune(exports.AsString(), '*') {
				if strings.IndexByte(exports.AsString(), '*') != strings.LastIndexByte(exports.AsString(), '*') {
					return
				}
				patternPath := tspath.ResolvePath(packageJson.PackageDirectory, exports.AsString())
				leadingSlice, trailingSlice, _ := strings.Cut(patternPath, "*")
				caseSensitive := r.resolver.host.FS().UseCaseSensitiveFileNames()
				files := vfsmatch.ReadDirectory(
					r.resolver.host.FS(),
					r.resolver.host.GetCurrentDirectory(),
					packageJson.PackageDirectory,
					r.extensions.Array(),
					nil,
					[]string{
						tspath.ChangeFullExtension(strings.Replace(exports.AsString(), "*", "**/*", 1), ".*"),
					},
					vfsmatch.UnlimitedDepth,
				)
				for _, file := range files {
					matchedStar, ok := r.getMatchedStarForPatternEntrypoint(file, leadingSlice, trailingSlice, caseSensitive)
					if !ok {
						continue
					}
					moduleSpecifier := tspath.ResolvePath(packageName, strings.Replace(subpath, "*", matchedStar, 1))
					entrypoints = append(entrypoints, r.resolver.createResolvedEntrypointHandlingSymlink(
						file,
						moduleSpecifier,
						includeConditions,
						excludeConditions,
						core.IfElse(strings.HasSuffix(exports.AsString(), "*"), EndingExtensionChangeable, EndingFixed),
					))
				}
			} else {
				partsAfterFirst := tspath.GetPathComponents(exports.AsString(), "")[2:]
				if slices.Contains(partsAfterFirst, "..") || slices.Contains(partsAfterFirst, ".") || slices.Contains(partsAfterFirst, "node_modules") {
					return
				}
				resolvedTarget := tspath.ResolvePath(packageJson.PackageDirectory, exports.AsString())
				if result := r.loadFileNameFromPackageJSONField(r.extensions, resolvedTarget, exports.AsString()); result.isResolved() {
					entrypoints = append(entrypoints, r.resolver.createResolvedEntrypointHandlingSymlink(
						result.path,
						tspath.ResolvePath(packageName, subpath),
						includeConditions,
						excludeConditions,
						core.IfElse(strings.HasSuffix(exports.AsString(), "*"), EndingExtensionChangeable, EndingFixed),
					))
				}
			}
		} else if exports.Type == packagejson.JSONValueTypeArray {
			for _, element := range exports.AsArray() {
				loadEntrypointsFromTargetExports(subpath, includeConditions, excludeConditions, element)
			}
		} else if exports.Type == packagejson.JSONValueTypeObject {
			var prevConditions []string
			for condition, export := range exports.AsObject().Entries() {
				if excludeConditions != nil && excludeConditions.Has(condition) {
					continue
				}

				conditionAlwaysMatches := condition == "default" || condition == "types" || IsApplicableVersionedTypesKey(condition)
				newIncludeConditions := includeConditions
				if !conditionAlwaysMatches {
					newIncludeConditions = includeConditions.Clone()
					excludeConditions = excludeConditions.Clone()
					if newIncludeConditions == nil {
						newIncludeConditions = &collections.Set[string]{}
					}
					newIncludeConditions.Add(condition)
					for _, prevCondition := range prevConditions {
						if excludeConditions == nil {
							excludeConditions = &collections.Set[string]{}
						}
						excludeConditions.Add(prevCondition)
					}
				}

				prevConditions = append(prevConditions, condition)
				loadEntrypointsFromTargetExports(subpath, newIncludeConditions, excludeConditions, export)
				if conditionAlwaysMatches {
					break
				}
			}
		}
	}

	switch exports.Type {
	case packagejson.JSONValueTypeArray:
		for _, element := range exports.AsArray() {
			loadEntrypointsFromTargetExports(".", nil, nil, element)
		}
	case packagejson.JSONValueTypeObject:
		if exports.IsSubpaths() {
			for subpath, export := range exports.AsObject().Entries() {
				loadEntrypointsFromTargetExports(subpath, nil, nil, export)
			}
		} else {
			loadEntrypointsFromTargetExports(".", nil, nil, exports)
		}
	default:
		loadEntrypointsFromTargetExports(".", nil, nil, exports)
	}

	return entrypoints
}

func (r *resolutionState) getMatchedStarForPatternEntrypoint(file string, leadingSlice string, trailingSlice string, caseSensitive bool) (string, bool) {
	if stringutil.HasPrefixAndSuffixWithoutOverlap(file, leadingSlice, trailingSlice, caseSensitive) {
		return file[len(leadingSlice) : len(file)-len(trailingSlice)], true
	}

	if jsExtension := TryGetJSExtensionForFile(file, r.compilerOptions); len(jsExtension) > 0 {
		swapped := tspath.ChangeFullExtension(file, jsExtension)
		if stringutil.HasPrefixAndSuffixWithoutOverlap(swapped, leadingSlice, trailingSlice, caseSensitive) {
			return swapped[len(leadingSlice) : len(swapped)-len(trailingSlice)], true
		}
	}

	return "", false
}
