package projecttestutil import ( "context" "fmt" "os" "slices" "strings" "sync" "testing" "github.com/microsoft/typescript-go/internal/bundled" "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/glob" "github.com/microsoft/typescript-go/internal/lsp/lsproto" "github.com/microsoft/typescript-go/internal/project" "github.com/microsoft/typescript-go/internal/project/logging" "github.com/microsoft/typescript-go/internal/testutil/baseline" "github.com/microsoft/typescript-go/internal/tspath" "github.com/microsoft/typescript-go/internal/vfs" "github.com/microsoft/typescript-go/internal/vfs/iovfs" "github.com/microsoft/typescript-go/internal/vfs/osvfs" "github.com/microsoft/typescript-go/internal/vfs/vfstest" ) //go:generate go tool github.com/matryer/moq -stub -fmt goimports -pkg projecttestutil -out clientmock_generated.go ../../project Client //go:generate npx dprint fmt clientmock_generated.go //go:generate go tool github.com/matryer/moq -stub -fmt goimports -pkg projecttestutil -out npmexecutormock_generated.go ../../project/ata NpmExecutor //go:generate npx dprint fmt npmexecutormock_generated.go const ( TestTypingsLocation = "/home/src/Library/Caches/typescript" ) type TypingsInstallerOptions struct { TypesRegistry []string PackageToFile map[string]string } type SessionUtils struct { currentDirectory string fsFromFileMap iovfs.FsWithSys fs vfs.FS client *ClientMock npmExecutor *NpmExecutorMock tiOptions *TypingsInstallerOptions logger logging.LogCollector } func (h *SessionUtils) FsFromFileMap() iovfs.FsWithSys { return h.fsFromFileMap } func (h *SessionUtils) Client() *ClientMock { return h.client } func (h *SessionUtils) NpmExecutor() *NpmExecutorMock { return h.npmExecutor } func (h *SessionUtils) SetupNpmExecutorForTypingsInstaller() { if h.tiOptions == nil { return } h.npmExecutor.NpmInstallFunc = func(cwd string, packageNames []string) ([]byte, error) { // packageNames is actually npmInstallArgs due to interface misnaming npmInstallArgs := packageNames lenNpmInstallArgs := len(npmInstallArgs) if lenNpmInstallArgs < 3 { return nil, fmt.Errorf("unexpected npm install: %s %v", cwd, npmInstallArgs) } if lenNpmInstallArgs == 3 && npmInstallArgs[2] == "types-registry@latest" { // Write typings file err := h.fs.WriteFile(cwd+"/node_modules/types-registry/index.json", h.createTypesRegistryFileContent()) return nil, err } // Find the packages: they start at index 2 and continue until we hit a flag starting with -- packageEnd := lenNpmInstallArgs for i := 2; i < lenNpmInstallArgs; i++ { if strings.HasPrefix(npmInstallArgs[i], "--") { packageEnd = i break } } for _, atTypesPackageTs := range npmInstallArgs[2:packageEnd] { // @types/packageName@TsVersionToUse atTypesPackage := atTypesPackageTs // Remove version suffix if versionIndex := strings.LastIndex(atTypesPackage, "@"); versionIndex > 6 { // "@types/".length is 7, so version @ must be after atTypesPackage = atTypesPackage[:versionIndex] } // Extract package name from @types/packageName packageBaseName := atTypesPackage[7:] // Remove "@types/" prefix content, ok := h.tiOptions.PackageToFile[packageBaseName] if !ok { return nil, fmt.Errorf("content not provided for %s", packageBaseName) } err := h.fs.WriteFile(cwd+"/node_modules/@types/"+packageBaseName+"/index.d.ts", content) if err != nil { return nil, err } } return nil, nil } } func (h *SessionUtils) ToPath(fileName string) tspath.Path { return tspath.ToPath(fileName, h.currentDirectory, h.fs.UseCaseSensitiveFileNames()) } func (h *SessionUtils) FS() vfs.FS { return h.fs } // WatchesFile reports whether any registered file watcher would match the given // file path. It handles both absolute glob patterns and relative patterns with // a base URI. On case-insensitive file systems the paths in glob patterns are // lowercased, so callers should pass the lowercased path. func (h *SessionUtils) WatchesFile(filePath string) bool { for _, call := range h.client.WatchFilesCalls() { for _, watcher := range call.Watchers { if watcher.GlobPattern.Pattern != nil { if g, err := glob.Parse(*watcher.GlobPattern.Pattern); err == nil && g.Match(filePath) { return true } } else if watcher.GlobPattern.RelativePattern != nil { rp := watcher.GlobPattern.RelativePattern baseUri := string(*rp.BaseUri.URI) // Convert base URI (e.g. "file:///home/projects") to a directory path // with trailing separator for proper prefix matching on path boundaries. baseDir := lsproto.DocumentUri(baseUri).FileName() baseDir = tspath.EnsureTrailingDirectorySeparator(baseDir) if strings.HasPrefix(filePath, baseDir) { relativePath := filePath[len(baseDir):] if g, err := glob.Parse(rp.Pattern); err == nil && g.Match(relativePath) { return true } } } } } return false } func (h *SessionUtils) Logs() string { return h.logger.String() } func (h *SessionUtils) BaselineLogs(t *testing.T) { baseline.Run(t, t.Name()+".log", h.Logs(), baseline.Options{ Subfolder: "project", }) } var ( typesRegistryConfigTextOnce sync.Once typesRegistryConfigText string ) func TypesRegistryConfigText() string { typesRegistryConfigTextOnce.Do(func() { var result strings.Builder for key, value := range TypesRegistryConfig() { if result.Len() != 0 { result.WriteString(",") } result.WriteString(fmt.Sprintf("\n \"%s\": \"%s\"", key, value)) } typesRegistryConfigText = result.String() }) return typesRegistryConfigText } var ( typesRegistryConfigOnce sync.Once typesRegistryConfig map[string]string ) func TypesRegistryConfig() map[string]string { typesRegistryConfigOnce.Do(func() { typesRegistryConfig = map[string]string{ "latest": "1.3.0", "ts2.0": "1.0.0", "ts2.1": "1.0.0", "ts2.2": "1.2.0", "ts2.3": "1.3.0", "ts2.4": "1.3.0", "ts2.5": "1.3.0", "ts2.6": "1.3.0", "ts2.7": "1.3.0", } }) return typesRegistryConfig } func (h *SessionUtils) createTypesRegistryFileContent() string { var builder strings.Builder builder.WriteString("{\n \"entries\": {") for index, entry := range h.tiOptions.TypesRegistry { h.appendTypesRegistryConfig(&builder, index, entry) } index := len(h.tiOptions.TypesRegistry) for key := range h.tiOptions.PackageToFile { if !slices.Contains(h.tiOptions.TypesRegistry, key) { h.appendTypesRegistryConfig(&builder, index, key) index++ } } builder.WriteString("\n }\n}") return builder.String() } func (h *SessionUtils) appendTypesRegistryConfig(builder *strings.Builder, index int, entry string) { if index > 0 { builder.WriteString(",") } builder.WriteString(fmt.Sprintf("\n \"%s\": {%s\n }", entry, TypesRegistryConfigText())) } func Setup(files map[string]any) (*project.Session, *SessionUtils) { return SetupWithTypingsInstaller(files, &TypingsInstallerOptions{}) } func SetupWithRealFS() (*project.Session, *SessionUtils) { fs := bundled.WrapFS(osvfs.FS()) clientMock := &ClientMock{} npmExecutorMock := &NpmExecutorMock{} wd, err := os.Getwd() if err != nil { panic(err) } sessionUtils := &SessionUtils{ currentDirectory: wd, fs: fs, client: clientMock, npmExecutor: npmExecutorMock, logger: logging.NewTestLogger(), } return project.NewSession(&project.SessionInit{ BackgroundCtx: context.Background(), FS: fs, Client: clientMock, NpmExecutor: npmExecutorMock, Logger: sessionUtils.logger, Options: &project.SessionOptions{ CurrentDirectory: wd, DefaultLibraryPath: bundled.LibPath(), PositionEncoding: lsproto.PositionEncodingKindUTF8, WatchEnabled: true, LoggingEnabled: true, PushDiagnosticsEnabled: true, }, }), sessionUtils } func SetupWithOptions(files map[string]any, options *project.SessionOptions) (*project.Session, *SessionUtils) { return SetupWithOptionsAndTypingsInstaller(files, options, &TypingsInstallerOptions{}) } func SetupWithTypingsInstaller(files map[string]any, tiOptions *TypingsInstallerOptions) (*project.Session, *SessionUtils) { return SetupWithOptionsAndTypingsInstaller(files, nil, tiOptions) } func SetupWithOptionsAndTypingsInstaller(files map[string]any, options *project.SessionOptions, tiOptions *TypingsInstallerOptions) (*project.Session, *SessionUtils) { init, sessionUtils := GetSessionInitOptions(files, options, tiOptions) session := project.NewSession(init) return session, sessionUtils } func WithRequestID(ctx context.Context) context.Context { return core.WithRequestID(ctx, "0") } func GetSessionInitOptions(files map[string]any, options *project.SessionOptions, tiOptions *TypingsInstallerOptions) (*project.SessionInit, *SessionUtils) { fsFromFileMap := vfstest.FromMap(files, false /*useCaseSensitiveFileNames*/) fs := bundled.WrapFS(fsFromFileMap) clientMock := &ClientMock{} npmExecutorMock := &NpmExecutorMock{} sessionUtils := &SessionUtils{ currentDirectory: "/", fsFromFileMap: fsFromFileMap.(iovfs.FsWithSys), fs: fs, client: clientMock, npmExecutor: npmExecutorMock, tiOptions: tiOptions, logger: logging.NewTestLogger(), } // Configure the npm executor mock to handle typings installation sessionUtils.SetupNpmExecutorForTypingsInstaller() // Use provided options or create default ones if options == nil { options = &project.SessionOptions{ CurrentDirectory: "/", DefaultLibraryPath: bundled.LibPath(), TypingsLocation: TestTypingsLocation, PositionEncoding: lsproto.PositionEncodingKindUTF8, WatchEnabled: true, LoggingEnabled: true, PushDiagnosticsEnabled: true, } } return &project.SessionInit{ BackgroundCtx: context.Background(), Options: options, FS: fs, Client: clientMock, NpmExecutor: npmExecutorMock, Logger: sessionUtils.logger, }, sessionUtils }