Skip to content

Implement maxNodeModuleJsDepth, noResolve #1189

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 22 commits into from
Jun 17, 2025
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
150 changes: 100 additions & 50 deletions internal/compiler/fileloader.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"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/module"
"github.com/microsoft/typescript-go/internal/tsoptions"
"github.com/microsoft/typescript-go/internal/tspath"
Expand Down Expand Up @@ -64,6 +65,10 @@ func processAllProgramFiles(
compilerOptions := opts.Config.CompilerOptions()
rootFiles := opts.Config.FileNames()
supportedExtensions := tsoptions.GetSupportedExtensions(compilerOptions, nil /*extraFileExtensions*/)
var maxNodeModuleJsDepth int
if p := opts.Config.CompilerOptions().MaxNodeModuleJsDepth; p != nil {
maxNodeModuleJsDepth = *p
}
loader := fileLoader{
opts: opts,
defaultLibraryPath: tspath.GetNormalizedAbsolutePath(opts.Host.DefaultLibraryPath(), opts.Host.GetCurrentDirectory()),
Expand All @@ -72,12 +77,11 @@ func processAllProgramFiles(
CurrentDirectory: opts.Host.GetCurrentDirectory(),
},
parseTasks: &fileLoaderWorker[*parseTask]{
wg: core.NewWorkGroup(singleThreaded),
getSubTasks: getSubTasksOfParseTask,
wg: core.NewWorkGroup(singleThreaded),
maxDepth: maxNodeModuleJsDepth,
},
projectReferenceParseTasks: &fileLoaderWorker[*projectReferenceParseTask]{
wg: core.NewWorkGroup(singleThreaded),
getSubTasks: getSubTasksOfProjectReferenceParseTask,
wg: core.NewWorkGroup(singleThreaded),
},
rootTasks: make([]*parseTask, 0, len(rootFiles)+len(libs)),
supportedExtensions: core.Flatten(tsoptions.GetSupportedExtensionsWithJsonIfResolveJsonModule(compilerOptions, supportedExtensions)),
Expand Down Expand Up @@ -282,30 +286,34 @@ func (p *fileLoader) parseSourceFile(t *parseTask) *ast.SourceFile {
return sourceFile
}

func (p *fileLoader) resolveTripleslashPathReference(moduleName string, containingFile string) string {
func (p *fileLoader) resolveTripleslashPathReference(moduleName string, containingFile string) resolvedRef {
basePath := tspath.GetDirectoryPath(containingFile)
referencedFileName := moduleName

if !tspath.IsRootedDiskPath(moduleName) {
referencedFileName = tspath.CombinePaths(basePath, moduleName)
}
return tspath.NormalizePath(referencedFileName)
return resolvedRef{
fileName: tspath.NormalizePath(referencedFileName),
}
}

func (p *fileLoader) resolveTypeReferenceDirectives(file *ast.SourceFile, meta *ast.SourceFileMetaData) (
toParse []string,
toParse []resolvedRef,
typeResolutionsInFile module.ModeAwareCache[*module.ResolvedTypeReferenceDirective],
) {
if len(file.TypeReferenceDirectives) != 0 {
toParse = make([]string, 0, len(file.TypeReferenceDirectives))
toParse = make([]resolvedRef, 0, len(file.TypeReferenceDirectives))
typeResolutionsInFile = make(module.ModeAwareCache[*module.ResolvedTypeReferenceDirective], len(file.TypeReferenceDirectives))
for _, ref := range file.TypeReferenceDirectives {
redirect := p.projectReferenceFileMapper.getRedirectForResolution(file)
resolutionMode := getModeForTypeReferenceDirectiveInFile(ref, file, meta, module.GetCompilerOptionsWithRedirect(p.opts.Config.CompilerOptions(), redirect))
resolved := p.resolver.ResolveTypeReferenceDirective(ref.FileName, file.FileName(), resolutionMode, redirect)
typeResolutionsInFile[module.ModeAwareCacheKey{Name: ref.FileName, Mode: resolutionMode}] = resolved
if resolved.IsResolved() {
toParse = append(toParse, resolved.ResolvedFileName)
toParse = append(toParse, resolvedRef{
fileName: resolved.ResolvedFileName,
})
}
}
}
Expand All @@ -315,19 +323,12 @@ func (p *fileLoader) resolveTypeReferenceDirectives(file *ast.SourceFile, meta *
const externalHelpersModuleNameText = "tslib" // TODO(jakebailey): dedupe

func (p *fileLoader) resolveImportsAndModuleAugmentations(file *ast.SourceFile, meta *ast.SourceFileMetaData) (
toParse []string,
toParse []resolvedRef,
resolutionsInFile module.ModeAwareCache[*module.ResolvedModule],
importHelpersImportSpecifier *ast.Node,
jsxRuntimeImportSpecifier_ *jsxRuntimeImportSpecifier,
) {
moduleNames := make([]*ast.Node, 0, len(file.Imports())+len(file.ModuleAugmentations)+2)
moduleNames = append(moduleNames, file.Imports()...)
for _, imp := range file.ModuleAugmentations {
if imp.Kind == ast.KindStringLiteral {
moduleNames = append(moduleNames, imp)
}
// Do nothing if it's an Identifier; we don't need to do module resolution for `declare global`.
}

isJavaScriptFile := ast.IsSourceFileJS(file)
isExternalModuleFile := ast.IsExternalModule(file)
Expand All @@ -352,68 +353,117 @@ func (p *fileLoader) resolveImportsAndModuleAugmentations(file *ast.SourceFile,
}
}

importsStart := len(moduleNames)

moduleNames = append(moduleNames, file.Imports()...)
for _, imp := range file.ModuleAugmentations {
if imp.Kind == ast.KindStringLiteral {
moduleNames = append(moduleNames, imp)
}
// Do nothing if it's an Identifier; we don't need to do module resolution for `declare global`.
}

if len(moduleNames) != 0 {
toParse = make([]string, 0, len(moduleNames))
toParse = make([]resolvedRef, 0, len(moduleNames))
resolutionsInFile = make(module.ModeAwareCache[*module.ResolvedModule], len(moduleNames))

resolutions := p.resolveModuleNames(moduleNames, file, meta, redirect)
for index, entry := range moduleNames {
moduleName := entry.Text()
if moduleName == "" {
continue
}

resolutionsInFile = make(module.ModeAwareCache[*module.ResolvedModule], len(resolutions))
mode := getModeForUsageLocation(file.FileName(), meta, entry, module.GetCompilerOptionsWithRedirect(p.opts.Config.CompilerOptions(), redirect))
resolvedModule := p.resolver.ResolveModuleName(moduleName, file.FileName(), mode, redirect)
resolutionsInFile[module.ModeAwareCacheKey{Name: moduleName, Mode: mode}] = resolvedModule

for _, resolution := range resolutions {
resolvedFileName := resolution.resolvedModule.ResolvedFileName
// TODO(ercornel): !!!: check if from node modules
if !resolvedModule.IsResolved() {
continue
}

mode := getModeForUsageLocation(file.FileName(), meta, resolution.node, optionsForFile)
resolutionsInFile[module.ModeAwareCacheKey{Name: resolution.node.Text(), Mode: mode}] = resolution.resolvedModule
resolvedFileName := resolvedModule.ResolvedFileName
isFromNodeModulesSearch := resolvedModule.IsExternalLibraryImport
// If this is js file source of project reference, dont treat it as js file but as d.ts
isJsFile := !tspath.FileExtensionIsOneOf(resolvedFileName, tspath.SupportedTSExtensionsWithJsonFlat) && p.projectReferenceFileMapper.getRedirectForResolution(ast.NewHasFileName(resolvedFileName, p.toPath(resolvedFileName))) == nil
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why isn't this just the following? Just a faithful translation?

Suggested change
isJsFile := !tspath.FileExtensionIsOneOf(resolvedFileName, tspath.SupportedTSExtensionsWithJsonFlat) && p.projectReferenceFileMapper.getRedirectForResolution(ast.NewHasFileName(resolvedFileName, p.toPath(resolvedFileName))) == nil
isJsFile := tspath.FileExtensionIsOneOf(resolvedFileName, tspath.SupportedJSExtensionsFlat) && p.projectReferenceFileMapper.getRedirectForResolution(ast.NewHasFileName(resolvedFileName, p.toPath(resolvedFileName))) == nil

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, all of this is a faithful port.

isJsFileFromNodeModules := isFromNodeModulesSearch && isJsFile && (resolvedFileName == "" || strings.Contains(resolvedFileName, "/node_modules/"))

// add file to program only if:
// - resolution was successful
// - noResolve is falsy
// - module name comes from the list of imports
// - it's not a top level JavaScript module that exceeded the search max

// const elideImport = isJSFileFromNodeModules && currentNodeModulesDepth > maxNodeModuleJsDepth;
importIndex := index - importsStart

// Don't add the file if it has a bad extension (e.g. 'tsx' if we don't have '--allowJs')
// This may still end up being an untyped module -- the file won't be included but imports will be allowed.
hasAllowedExtension := false
if optionsForFile.GetResolveJsonModule() {
hasAllowedExtension = tspath.FileExtensionIsOneOf(resolvedFileName, tspath.SupportedTSExtensionsWithJsonFlat)
} else if optionsForFile.AllowJs.IsTrue() {
hasAllowedExtension = tspath.FileExtensionIsOneOf(resolvedFileName, tspath.SupportedJSExtensionsFlat) || tspath.FileExtensionIsOneOf(resolvedFileName, tspath.SupportedTSExtensionsFlat)
} else {
hasAllowedExtension = tspath.FileExtensionIsOneOf(resolvedFileName, tspath.SupportedTSExtensionsFlat)
shouldAddFile := moduleName != "" &&
getResolutionDiagnostic(optionsForFile, resolvedModule, file) == nil &&
!optionsForFile.NoResolve.IsTrue() &&
!(isJsFile && !optionsForFile.GetAllowJS()) &&
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
!(isJsFile && !optionsForFile.GetAllowJS()) &&
(!isJsFile || optionsForFile.GetAllowJS()) &&

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a direct port, but I could change it.

(importIndex < 0 || (importIndex < len(file.Imports()) &&
(ast.IsInJSFile(file.Imports()[importIndex]) || file.Imports()[importIndex].Flags&ast.NodeFlagsJSDoc == 0)))

if !shouldAddFile {
continue
}
shouldAddFile := resolution.resolvedModule.IsResolved() && hasAllowedExtension
// TODO(ercornel): !!!: other checks on whether or not to add the file

if shouldAddFile {
// p.findSourceFile(resolvedFileName, FileIncludeReason{Import, 0})
toParse = append(toParse, resolvedFileName)
}
toParse = append(toParse, resolvedRef{
fileName: resolvedFileName,
isJsFileFromNodeModules: isJsFileFromNodeModules,
})
}
}

return toParse, resolutionsInFile, importHelpersImportSpecifier, jsxRuntimeImportSpecifier_
}

func (p *fileLoader) resolveModuleNames(entries []*ast.Node, file *ast.SourceFile, meta *ast.SourceFileMetaData, redirect *tsoptions.ParsedCommandLine) []*resolution {
if len(entries) == 0 {
return nil
func getResolutionDiagnostic(options *core.CompilerOptions, resolvedModule *module.ResolvedModule, file *ast.SourceFile) *diagnostics.Message {
needJsx := func() *diagnostics.Message {
if options.Jsx != core.JsxEmitNone {
return nil
}
return diagnostics.Module_0_was_resolved_to_1_but_jsx_is_not_set
}

needAllowJs := func() *diagnostics.Message {
if options.GetAllowJS() || !options.NoImplicitAny.DefaultIfUnknown(options.Strict).IsTrue() {
return nil
}
return diagnostics.Module_0_was_resolved_to_1_but_resolveJsonModule_is_not_used
}

resolvedModules := make([]*resolution, 0, len(entries))
needResolveJsonModule := func() *diagnostics.Message {
if options.GetResolveJsonModule() {
return nil
}
return diagnostics.Module_0_was_resolved_to_1_but_resolveJsonModule_is_not_used
}

for _, entry := range entries {
moduleName := entry.Text()
if moduleName == "" {
continue
needAllowArbitraryExtensions := func() *diagnostics.Message {
if file.IsDeclarationFile || options.AllowArbitraryExtensions.IsTrue() {
return nil
}
resolvedModule := p.resolver.ResolveModuleName(moduleName, file.FileName(), getModeForUsageLocation(file.FileName(), meta, entry, module.GetCompilerOptionsWithRedirect(p.opts.Config.CompilerOptions(), redirect)), redirect)
resolvedModules = append(resolvedModules, &resolution{node: entry, resolvedModule: resolvedModule})
return diagnostics.Module_0_was_resolved_to_1_but_allowArbitraryExtensions_is_not_set
}

return resolvedModules
switch resolvedModule.Extension {
case tspath.ExtensionTs, tspath.ExtensionDts,
tspath.ExtensionMts, tspath.ExtensionDmts,
tspath.ExtensionCts, tspath.ExtensionDcts:
// These are always allowed.
return nil
case tspath.ExtensionTsx:
return needJsx()
case tspath.ExtensionJsx:
return core.Coalesce(needJsx(), needAllowJs())
case tspath.ExtensionJs, tspath.ExtensionMjs, tspath.ExtensionCjs:
return needAllowJs()
case tspath.ExtensionJson:
return needResolveJsonModule()
default:
return needAllowArbitraryExtensions()
}
}

func (p *fileLoader) createSyntheticImport(text string, file *ast.SourceFile) *ast.Node {
Expand Down
75 changes: 53 additions & 22 deletions internal/compiler/fileloadertask.go
Original file line number Diff line number Diff line change
@@ -1,43 +1,74 @@
package compiler

import (
"math"
"sync"

"github.com/microsoft/typescript-go/internal/collections"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/tspath"
)

type fileLoaderWorkerTask interface {
type fileLoaderWorkerTask[T any] interface {
comparable
FileName() string
start(loader *fileLoader)
isLoaded() bool
load(loader *fileLoader)
getSubTasks() []T
shouldIncreaseDepth() bool
}

type fileLoaderWorker[K fileLoaderWorkerTask] struct {
type fileLoaderWorker[K fileLoaderWorkerTask[K]] struct {
wg core.WorkGroup
tasksByFileName collections.SyncMap[string, K]
getSubTasks func(t K) []K
tasksByFileName collections.SyncMap[string, *queuedTask[K]]
maxDepth int
}

type queuedTask[K fileLoaderWorkerTask[K]] struct {
task K
mu sync.Mutex
lowestDepth int
}

func (w *fileLoaderWorker[K]) runAndWait(loader *fileLoader, tasks []K) {
w.start(loader, tasks)
w.start(loader, tasks, 0)
w.wg.RunAndWait()
}

func (w *fileLoaderWorker[K]) start(loader *fileLoader, tasks []K) {
if len(tasks) > 0 {
for i, task := range tasks {
loadedTask, loaded := w.tasksByFileName.LoadOrStore(task.FileName(), task)
if loaded {
// dedup tasks to ensure correct file order, regardless of which task would be started first
tasks[i] = loadedTask
} else {
w.wg.Queue(func() {
task.start(loader)
subTasks := w.getSubTasks(task)
w.start(loader, subTasks)
})
}
func (w *fileLoaderWorker[K]) start(loader *fileLoader, tasks []K, depth int) {
for i, task := range tasks {
newTask := &queuedTask[K]{task: task, lowestDepth: math.MaxInt}
loadedTask, loaded := w.tasksByFileName.LoadOrStore(task.FileName(), newTask)
task = loadedTask.task
if loaded {
tasks[i] = task
}

currentDepth := depth
if task.shouldIncreaseDepth() {
currentDepth++
}

if currentDepth > w.maxDepth {
continue
}

w.wg.Queue(func() {
loadedTask.mu.Lock()
defer loadedTask.mu.Unlock()

if !task.isLoaded() {
task.load(loader)
}

if currentDepth < loadedTask.lowestDepth {
// If we're seeing this task at a lower depth than before,
// reprocess its subtasks to ensure they are loaded.
loadedTask.lowestDepth = currentDepth
subTasks := task.getSubTasks()
w.start(loader, subTasks, currentDepth)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The subtasks may or may not have been elided when they were seen at a lower depth—is there a mechanism here to skip them if they’ve already been run?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They'll be deduped by the start function; if they're at a lower depth the code in this block will skip walking any further, and the loaded check will have also skipped loading the source file and other info again.

}
})
}
}

Expand All @@ -49,12 +80,12 @@ func (w *fileLoaderWorker[K]) collectWorker(loader *fileLoader, tasks []K, itera
var results []tspath.Path
for _, task := range tasks {
// ensure we only walk each task once
if seen.Has(task) {
if !task.isLoaded() || seen.Has(task) {
continue
}
seen.Add(task)
var subResults []tspath.Path
if subTasks := w.getSubTasks(task); len(subTasks) > 0 {
if subTasks := task.getSubTasks(); len(subTasks) > 0 {
subResults = w.collectWorker(loader, subTasks, iterate, seen)
}
iterate(task, subResults)
Expand Down
Loading
Loading