package module_test import ( "io" "os" "path/filepath" "regexp" "slices" "sync" "testing" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/core" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/jsonutil" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/module" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/repo" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/testutil/baseline" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/tspath" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/vfs" "efprojects.com/kitten-ipc/kitcom/internal/tsgo/vfs/vfstest" "github.com/go-json-experiment/json" "github.com/go-json-experiment/json/jsontext" "gotest.tools/v3/assert" "gotest.tools/v3/assert/cmp" ) var skip = []string{ "allowJsCrossMonorepoPackage.ts", "APILibCheck.ts", "APISample_compile.ts", "APISample_jsdoc.ts", "APISample_linter.ts", "APISample_parseConfig.ts", "APISample_transform.ts", "APISample_Watch.ts", "APISample_watcher.ts", "APISample_WatchWithDefaults.ts", "APISample_WatchWithOwnWatchHost.ts", "bundlerNodeModules1(module=esnext).ts", "bundlerNodeModules1(module=preserve).ts", "commonJsExportTypeDeclarationError.ts", "commonSourceDir5.ts", "commonSourceDirectory.ts", "computedEnumMemberSyntacticallyString2(isolatedmodules=false).ts", "computedEnumMemberSyntacticallyString2(isolatedmodules=true).ts", "declarationEmitCommonSourceDirectoryDoesNotContainAllFiles.ts", "declarationEmitForGlobalishSpecifierSymlink.ts", "declarationEmitForGlobalishSpecifierSymlink2.ts", "declarationEmitReexportedSymlinkReference.ts", "declarationEmitReexportedSymlinkReference2.ts", "declarationEmitReexportedSymlinkReference3.ts", "declarationEmitSymlinkPaths.ts", "decoratorMetadataTypeOnlyExport.ts", "decoratorMetadataTypeOnlyImport.ts", "enumNoInitializerFollowsNonLiteralInitializer.ts", "enumWithNonLiteralStringInitializer.ts", "es6ImportWithJsDocTags.ts", "importAttributes9.ts", "importSpecifiers_js.ts", "importTag17.ts", "isolatedModulesShadowGlobalTypeNotValue(isolatedmodules=false,verbatimmodulesyntax=false).ts", "isolatedModulesShadowGlobalTypeNotValue(isolatedmodules=false,verbatimmodulesyntax=true).ts", "isolatedModulesShadowGlobalTypeNotValue(isolatedmodules=true,verbatimmodulesyntax=false).ts", "isolatedModulesShadowGlobalTypeNotValue(isolatedmodules=true,verbatimmodulesyntax=true).ts", "jsDeclarationEmitExportedClassWithExtends.ts", "jsxNamespaceGlobalReexport.tsx", "jsxNamespaceGlobalReexportMissingAliasTarget.tsx", "mergeSymbolReexportedTypeAliasInstantiation.ts", "mergeSymbolReexportInterface.ts", "mergeSymbolRexportFunction.ts", "missingMemberErrorHasShortPath.ts", "moduleResolutionAsTypeReferenceDirective.ts", "moduleResolutionAsTypeReferenceDirectiveAmbient.ts", "moduleResolutionAsTypeReferenceDirectiveScoped.ts", "moduleResolutionWithSymlinks_notInNodeModules.ts", "moduleResolutionWithSymlinks_preserveSymlinks.ts", "moduleResolutionWithSymlinks_referenceTypes.ts", "moduleResolutionWithSymlinks_withOutDir.ts", "moduleResolutionWithSymlinks.ts", "nodeAllowJsPackageSelfName(module=node16).ts", "nodeAllowJsPackageSelfName(module=nodenext).ts", "nodeAllowJsPackageSelfName2.ts", "nodeModulesAllowJsConditionalPackageExports(module=node16).ts", "nodeModulesAllowJsConditionalPackageExports(module=nodenext).ts", "nodeModulesAllowJsPackageExports(module=node16).ts", "nodeModulesAllowJsPackageExports(module=nodenext).ts", "nodeModulesAllowJsPackageImports(module=node16).ts", "nodeModulesAllowJsPackageImports(module=nodenext).ts", "nodeModulesAllowJsPackagePatternExports(module=node16).ts", "nodeModulesAllowJsPackagePatternExports(module=nodenext).ts", "nodeModulesAllowJsPackagePatternExportsTrailers(module=node16).ts", "nodeModulesAllowJsPackagePatternExportsTrailers(module=nodenext).ts", "nodeModulesConditionalPackageExports(module=node16).ts", "nodeModulesConditionalPackageExports(module=nodenext).ts", "nodeModulesDeclarationEmitWithPackageExports(module=node16).ts", "nodeModulesDeclarationEmitWithPackageExports(module=nodenext).ts", "nodeModulesExportsBlocksTypesVersions(module=node16).ts", "nodeModulesExportsBlocksTypesVersions(module=nodenext).ts", "nodeModulesImportResolutionIntoExport(module=node16).ts", "nodeModulesImportResolutionIntoExport(module=nodenext).ts", "nodeModulesImportResolutionNoCycle(module=node16).ts", "nodeModulesImportResolutionNoCycle(module=nodenext).ts", "nodeModulesPackageExports(module=node16).ts", "nodeModulesPackageExports(module=nodenext).ts", "nodeModulesPackagePatternExports(module=node16).ts", "nodeModulesPackagePatternExports(module=nodenext).ts", "nodeModulesPackagePatternExportsExclude(module=node16).ts", "nodeModulesPackagePatternExportsExclude(module=nodenext).ts", "nodeModulesPackagePatternExportsTrailers(module=node16).ts", "nodeModulesPackagePatternExportsTrailers(module=nodenext).ts", "nodeNextImportModeImplicitIndexResolution.ts", "nodeNextImportModeImplicitIndexResolution2.ts", "nodeNextPackageImportMapRootDir.ts", "nodeNextPackageSelfNameWithOutDir.ts", "nodeNextPackageSelfNameWithOutDirDeclDir.ts", "nodeNextPackageSelfNameWithOutDirDeclDirComposite.ts", "nodeNextPackageSelfNameWithOutDirDeclDirCompositeNestedDirs.ts", "nodeNextPackageSelfNameWithOutDirDeclDirNestedDirs.ts", "nodeNextPackageSelfNameWithOutDirDeclDirRootDir.ts", "nodeNextPackageSelfNameWithOutDirRootDir.ts", "resolutionModeImportType1(moduleresolution=bundler).ts", "resolutionModeImportType1(moduleresolution=node10).ts", "resolutionModeTypeOnlyImport1(moduleresolution=bundler).ts", "resolutionModeTypeOnlyImport1(moduleresolution=node10).ts", "selfNameAndImportsEmitInclusion.ts", "selfNameModuleAugmentation.ts", "symbolLinkDeclarationEmitModuleNames.ts", "symbolLinkDeclarationEmitModuleNamesImportRef.ts", "symbolLinkDeclarationEmitModuleNamesRootDir.ts", "symlinkedWorkspaceDependenciesNoDirectLinkGeneratesDeepNonrelativeName.ts", "symlinkedWorkspaceDependenciesNoDirectLinkGeneratesNonrelativeName.ts", "symlinkedWorkspaceDependenciesNoDirectLinkOptionalGeneratesNonrelativeName.ts", "symlinkedWorkspaceDependenciesNoDirectLinkPeerGeneratesNonrelativeName.ts", "typeGuardNarrowsIndexedAccessOfKnownProperty8.ts", "typesVersions.ambientModules.ts", "typesVersions.multiFile.ts", "typesVersionsDeclarationEmit.ambient.ts", "typesVersionsDeclarationEmit.multiFile.ts", "typesVersionsDeclarationEmit.multiFileBackReferenceToSelf.ts", "typesVersionsDeclarationEmit.multiFileBackReferenceToUnmapped.ts", } type vfsModuleResolutionHost struct { mu sync.Mutex fs vfs.FS currentDirectory string traces []string } func fixRoot(path string) string { rootLength := tspath.GetRootLength(path) if rootLength == 0 { return tspath.CombinePaths("/.src", path) } return path } func newVFSModuleResolutionHost(files map[string]string, currentDirectory string) *vfsModuleResolutionHost { fs := make(map[string]string, len(files)) for name, content := range files { fs[fixRoot(name)] = content } if currentDirectory == "" { currentDirectory = "/.src" } else if currentDirectory[0] != '/' { currentDirectory = "/.src/" + currentDirectory } return &vfsModuleResolutionHost{ fs: vfstest.FromMap(fs, true /*useCaseSensitiveFileNames*/), currentDirectory: currentDirectory, } } func (v *vfsModuleResolutionHost) FS() vfs.FS { return v.fs } // GetCurrentDirectory implements ModuleResolutionHost. func (v *vfsModuleResolutionHost) GetCurrentDirectory() string { return v.currentDirectory } // Trace implements ModuleResolutionHost. func (v *vfsModuleResolutionHost) Trace(msg string) { v.mu.Lock() defer v.mu.Unlock() v.traces = append(v.traces, msg) } type functionCall struct { call string args rawArgs returnValue map[string]any } type traceTestCase struct { name string currentDirectory string trace bool compilerOptions *core.CompilerOptions files map[string]string calls []functionCall } type rawFile struct { Name string `json:"name"` Content string `json:"content"` } type rawArgs struct { // getPackageScopeForPath Directory string `json:"directory"` // resolveModuleName, resolveTypeReferenceDirective Name string `json:"name"` ContainingFile string `json:"containingFile"` CompilerOptions *core.CompilerOptions `json:"compilerOptions"` ResolutionMode int `json:"resolutionMode"` RedirectedRef *struct { SourceFile struct { FileName string `json:"fileName"` } `json:"sourceFile"` CommandLine struct { CompilerOptions *core.CompilerOptions `json:"options"` } `json:"commandLine"` } `json:"redirectedReference"` } type rawTest struct { Test string `json:"test"` CurrentDirectory string `json:"currentDirectory"` Trace bool `json:"trace"` Files []rawFile `json:"files"` Call string `json:"call"` Args rawArgs `json:"args"` Return map[string]any `json:"return"` } var typesVersionsMessageRegex = regexp.MustCompile(`that matches compiler version '[^']+'`) func sanitizeTraceOutput(trace string) string { return typesVersionsMessageRegex.ReplaceAllString(trace, "that matches compiler version '3.1.0-dev'") } type RedirectRef struct { fileName string options *core.CompilerOptions } func (r *RedirectRef) ConfigName() string { return r.fileName } func (r *RedirectRef) CompilerOptions() *core.CompilerOptions { return r.options } var _ module.ResolvedProjectReference = (*RedirectRef)(nil) func doCall(t *testing.T, resolver *module.Resolver, call functionCall, skipLocations bool) { switch call.call { case "resolveModuleName", "resolveTypeReferenceDirective": var redirectedReference module.ResolvedProjectReference if call.args.RedirectedRef != nil { redirectedReference = &RedirectRef{ fileName: call.args.RedirectedRef.SourceFile.FileName, options: call.args.RedirectedRef.CommandLine.CompilerOptions, } } errorMessageArgs := []any{call.args.Name, call.args.ContainingFile} if call.call == "resolveModuleName" { resolved, _ := resolver.ResolveModuleName(call.args.Name, call.args.ContainingFile, core.ModuleKind(call.args.ResolutionMode), redirectedReference) assert.Check(t, resolved != nil, "ResolveModuleName should not return nil", errorMessageArgs) if expectedResolvedModule, ok := call.returnValue["resolvedModule"].(map[string]any); ok { assert.Check(t, resolved.IsResolved(), errorMessageArgs) assert.Check(t, cmp.Equal(resolved.ResolvedFileName, expectedResolvedModule["resolvedFileName"].(string)), errorMessageArgs) assert.Check(t, cmp.Equal(resolved.Extension, expectedResolvedModule["extension"].(string)), errorMessageArgs) assert.Check(t, cmp.Equal(resolved.ResolvedUsingTsExtension, expectedResolvedModule["resolvedUsingTsExtension"].(bool)), errorMessageArgs) assert.Check(t, cmp.Equal(resolved.IsExternalLibraryImport, expectedResolvedModule["isExternalLibraryImport"].(bool)), errorMessageArgs) } else { assert.Check(t, !resolved.IsResolved(), errorMessageArgs) } } else { resolved, _ := resolver.ResolveTypeReferenceDirective(call.args.Name, call.args.ContainingFile, core.ModuleKind(call.args.ResolutionMode), redirectedReference) assert.Check(t, resolved != nil, "ResolveTypeReferenceDirective should not return nil", errorMessageArgs) if expectedResolvedTypeReferenceDirective, ok := call.returnValue["resolvedTypeReferenceDirective"].(map[string]any); ok { assert.Check(t, resolved.IsResolved(), errorMessageArgs) assert.Check(t, cmp.Equal(resolved.ResolvedFileName, expectedResolvedTypeReferenceDirective["resolvedFileName"].(string)), errorMessageArgs) assert.Check(t, cmp.Equal(resolved.Primary, expectedResolvedTypeReferenceDirective["primary"].(bool)), errorMessageArgs) assert.Check(t, cmp.Equal(resolved.IsExternalLibraryImport, expectedResolvedTypeReferenceDirective["isExternalLibraryImport"].(bool)), errorMessageArgs) } else { assert.Check(t, !resolved.IsResolved(), errorMessageArgs) } } case "getPackageScopeForPath": resolver.GetPackageScopeForPath(call.args.Directory) default: t.Errorf("Unexpected call: %s", call.call) } } func runTraceBaseline(t *testing.T, test traceTestCase) { t.Run(test.name, func(t *testing.T) { t.Parallel() host := newVFSModuleResolutionHost(test.files, test.currentDirectory) resolver := module.NewResolver(host, test.compilerOptions, "", "") for _, call := range test.calls { doCall(t, resolver, call, false /*skipLocations*/) if t.Failed() { t.FailNow() } } t.Run("concurrent", func(t *testing.T) { concurrentHost := newVFSModuleResolutionHost(test.files, test.currentDirectory) concurrentResolver := module.NewResolver(concurrentHost, test.compilerOptions, "", "") var wg sync.WaitGroup for _, call := range test.calls { wg.Add(1) go func() { defer wg.Done() doCall(t, concurrentResolver, call, true /*skipLocations*/) }() } wg.Wait() }) if test.trace { t.Run("trace", func(t *testing.T) { output, err := jsonutil.MarshalIndent(resolver, "", " ") if err != nil { t.Fatal(err) } baseline.Run( t, tspath.RemoveFileExtension(test.name)+".trace.json", sanitizeTraceOutput(string(output)), baseline.Options{Subfolder: "module/resolver"}, ) }) } }) } func TestModuleResolver(t *testing.T) { t.Parallel() testsFilePath := filepath.Join(repo.TestDataPath, "fixtures", "module", "resolvertests.json") // Read file one line at a time file, err := os.Open(testsFilePath) if err != nil { t.Fatal(err) } t.Cleanup(func() { file.Close() }) decoder := jsontext.NewDecoder(file) var currentTestCase traceTestCase for { if decoder.PeekKind() == 0 { _, err := decoder.ReadToken() if err == io.EOF { //nolint:errorlint break } t.Fatal(err) } var testJSON rawTest if err := json.UnmarshalDecode(decoder, &testJSON); err != nil { t.Fatal(err) } if testJSON.Files != nil { if currentTestCase.name != "" && !slices.Contains(skip, currentTestCase.name) { runTraceBaseline(t, currentTestCase) } currentTestCase = traceTestCase{ name: testJSON.Test, currentDirectory: testJSON.CurrentDirectory, // !!! no traces are passing yet because of missing cache implementation trace: false, files: make(map[string]string, len(testJSON.Files)), } for _, file := range testJSON.Files { currentTestCase.files[file.Name] = file.Content } } else if testJSON.Call != "" { currentTestCase.calls = append(currentTestCase.calls, functionCall{ call: testJSON.Call, args: testJSON.Args, returnValue: testJSON.Return, }) if currentTestCase.compilerOptions == nil && testJSON.Args.CompilerOptions != nil { currentTestCase.compilerOptions = testJSON.Args.CompilerOptions } } else { t.Fatalf("Unexpected JSON: %v", testJSON) } } }