399 lines
15 KiB
Go
399 lines
15 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|