vendor tsgo

This commit is contained in:
2026-07-09 16:50:43 -04:00
parent c06ea2e5a4
commit 98978e4930
5804 changed files with 1556156 additions and 101 deletions

View File

@@ -0,0 +1,90 @@
package tsctests
import (
"fmt"
"github.com/microsoft/typescript-go/internal/collections"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/execute/incremental"
"github.com/microsoft/typescript-go/internal/json"
"github.com/microsoft/typescript-go/internal/testutil/fsbaselineutil"
"github.com/microsoft/typescript-go/internal/testutil/harnessutil"
"github.com/microsoft/typescript-go/internal/tspath"
"github.com/microsoft/typescript-go/internal/vfs"
)
type testFs struct {
vfs.FS
defaultLibs *collections.SyncSet[string]
writtenFiles collections.SyncSet[string]
}
func (f *testFs) removeIgnoreLibPath(path string) {
if f.defaultLibs != nil && f.defaultLibs.Has(path) {
f.defaultLibs.Delete(path)
}
}
// ReadFile reads the file specified by path and returns the content.
// If the file fails to be read, ok will be false.
func (f *testFs) ReadFile(path string) (contents string, ok bool) {
f.removeIgnoreLibPath(path)
return f.readFileHandlingBuildInfo(path)
}
func (f *testFs) readFileHandlingBuildInfo(path string) (contents string, ok bool) {
contents, ok = f.FS.ReadFile(path)
if ok && tspath.FileExtensionIs(path, tspath.ExtensionTsBuildInfo) {
// read buildinfo and modify version
var buildInfo incremental.BuildInfo
err := json.Unmarshal([]byte(contents), &buildInfo)
if err == nil && buildInfo.Version == harnessutil.FakeTSVersion {
buildInfo.Version = core.Version()
newContents, err := json.Marshal(&buildInfo)
if err != nil {
panic("testFs.ReadFile: failed to marshal build info after fixing version: " + err.Error())
}
contents = string(newContents)
}
}
return contents, ok
}
func (f *testFs) WriteFile(path string, data string) error {
f.removeIgnoreLibPath(path)
f.writtenFiles.Add(path)
return f.writeFileHandlingBuildInfo(path, data)
}
func (f *testFs) writeFileHandlingBuildInfo(path string, data string) error {
if tspath.FileExtensionIs(path, tspath.ExtensionTsBuildInfo) {
var buildInfo incremental.BuildInfo
if err := json.Unmarshal([]byte(data), &buildInfo); err == nil {
if buildInfo.Version == core.Version() {
// Change it to harnessutil.FakeTSVersion
buildInfo.Version = harnessutil.FakeTSVersion
newData, err := json.Marshal(&buildInfo)
if err != nil {
return fmt.Errorf("testFs.WriteFile: failed to marshal build info after fixing version: %w", err)
}
data = string(newData)
}
// Write readable build info version
if err := f.WriteFile(
path+".readable.baseline.txt",
toReadableBuildInfo(&buildInfo, fsbaselineutil.SanitizeInternalSymbolName(data)),
); err != nil {
return fmt.Errorf("testFs.WriteFile: failed to write readable build info: %w", err)
}
} else {
panic("testFs.WriteFile: failed to unmarshal build info: - use underlying FS's write method if this is intended use for testcase" + err.Error())
}
}
return f.FS.WriteFile(path, data)
}
// Removes `path` and all its contents. Will return the first error it encounters.
func (f *testFs) Remove(path string) error {
f.removeIgnoreLibPath(path)
return f.FS.Remove(path)
}

View File

@@ -0,0 +1,216 @@
package tsctests
import (
"fmt"
"io"
"path"
"sort"
"strings"
"sync"
"github.com/microsoft/typescript-go/internal/execute/watchmanager"
"github.com/microsoft/typescript-go/internal/fswatch"
"github.com/microsoft/typescript-go/internal/testutil/fsbaselineutil"
)
// MockWatchBackend implements watchmanager.WatchBackend for testing. It
// records all WatchDirectory calls so tests can verify that
// the correct watches are registered. Events can be delivered through
// SendEvents, which routes them only through watches whose paths
// match, enforcing that tests fail if the wrong watches are set up.
type MockWatchBackend struct {
mu sync.Mutex
Dirs map[string]*MockWatch
DirectoryExists func(string) bool // if set, WatchDirectory fails for non-existent dirs
}
var _ watchmanager.WatchBackend = (*MockWatchBackend)(nil)
// NewMockWatchBackend creates a ready-to-use mock backend.
func NewMockWatchBackend() *MockWatchBackend {
return &MockWatchBackend{
Dirs: make(map[string]*MockWatch),
}
}
// HasWatches reports whether any watches have been registered.
func (m *MockWatchBackend) HasWatches() bool {
m.mu.Lock()
defer m.mu.Unlock()
return len(m.Dirs) > 0
}
// MockWatch records a single registered watch.
type MockWatch struct {
Path string
Callback fswatch.WatchCallback
Recursive bool
Ignore func(string) bool
Closed bool
}
func (w *MockWatch) Close() error {
w.Closed = true
return nil
}
func (m *MockWatchBackend) WatchDirectory(dir string, fn fswatch.WatchCallback, recursive bool, ignore func(string) bool) (io.Closer, error) {
closers, err := m.WatchDirectories([]watchmanager.WatchDirectoryRequest{{
Dir: dir,
Callback: fn,
Recursive: recursive,
Ignore: ignore,
}})
if err != nil {
return nil, err
}
return closers[0], nil
}
func (m *MockWatchBackend) WatchDirectories(requests []watchmanager.WatchDirectoryRequest) ([]io.Closer, error) {
m.mu.Lock()
defer m.mu.Unlock()
for _, request := range requests {
if m.DirectoryExists != nil && !m.DirectoryExists(request.Dir) {
return nil, fmt.Errorf("directory does not exist: %s", request.Dir)
}
}
closers := make([]io.Closer, len(requests))
for i, request := range requests {
w := &MockWatch{Path: request.Dir, Callback: request.Callback, Recursive: request.Recursive, Ignore: request.Ignore}
m.Dirs[request.Dir] = w
closers[i] = w
}
return closers, nil
}
// SendEvents routes events through the registered watch callbacks
// that match each event's path. Directory watches match if the event
// path is a child (or recursive descendant) of the watched directory.
// Events that match no watch are silently dropped — this is by design
// so that tests fail when the production code doesn't register the
// needed watches.
func (m *MockWatchBackend) SendEvents(events []fswatch.Event) {
// Snapshot callbacks under the lock, then invoke outside the lock
// to avoid deadlock if the callback re-enters the mock.
m.mu.Lock()
type target struct {
cb fswatch.WatchCallback
events []fswatch.Event
}
targets := make(map[*MockWatch]*target)
for _, e := range events {
// Check directory watches.
for _, w := range m.Dirs {
if w.Closed {
continue
}
if w.Ignore != nil && w.Ignore(e.Path) {
continue
}
if !pathIsUnder(e.Path, w.Path, w.Recursive) {
continue
}
if t, ok := targets[w]; ok {
t.events = append(t.events, e)
} else {
targets[w] = &target{cb: w.Callback, events: []fswatch.Event{e}}
}
}
}
m.mu.Unlock()
for _, t := range targets {
t.cb(t.events, nil)
}
}
// SendChangedPaths converts a list of file changes into fswatch
// events with appropriate event kinds and routes them through
// registered watches via SendEvents. For new/modified files, it also
// emits update events for their parent directories, simulating how
// real filesystem watchers report directory events.
func (m *MockWatchBackend) SendChangedPaths(changes []fsbaselineutil.FileChange) {
events := make([]fswatch.Event, 0, len(changes)*2)
seenDirs := make(map[string]struct{})
for _, c := range changes {
kind := fswatch.EventUpdate
if c.Deleted {
kind = fswatch.EventDelete
}
events = append(events, fswatch.Event{Kind: kind, Path: c.Path})
// Emit update events for parent directories of changed files.
// Real filesystem watchers deliver events to non-recursive watches
// when a child directory is created, which the mock must replicate.
dir := path.Dir(c.Path)
for dir != "" && dir != "/" && dir != "." {
if _, seen := seenDirs[dir]; seen {
break
}
seenDirs[dir] = struct{}{}
events = append(events, fswatch.Event{Kind: fswatch.EventUpdate, Path: dir})
parent := path.Dir(dir)
if parent == dir {
break
}
dir = parent
}
}
m.SendEvents(events)
}
// pathIsUnder reports whether eventPath is inside dir. If recursive is
// false, only direct children match.
func pathIsUnder(eventPath, dir string, recursive bool) bool {
if !strings.HasPrefix(eventPath, dir) {
return false
}
rest := eventPath[len(dir):]
if len(rest) == 0 {
return false // exact match = the dir itself, not a child
}
if rest[0] != '/' {
return false // e.g. dir="/foo", path="/foobar"
}
if !recursive {
// Direct child only: no further '/' after the separator.
return !strings.Contains(rest[1:], "/")
}
return true
}
// WatchState returns a deterministic, human-readable summary of all
// active watches. This is intended to be included in test baselines
// so that watch registration correctness is verified via snapshot diffs.
func (m *MockWatchBackend) WatchState() string {
m.mu.Lock()
defer m.mu.Unlock()
var b strings.Builder
b.WriteString("Watch Registrations::\n")
// Directory watches, sorted by path.
var dirs []string
for dir, w := range m.Dirs {
if !w.Closed {
dirs = append(dirs, dir)
}
}
sort.Strings(dirs)
b.WriteString("Directory watches::\n")
if len(dirs) == 0 {
b.WriteString(" (none)\n")
}
for _, d := range dirs {
w := m.Dirs[d]
if w.Recursive {
fmt.Fprintf(&b, " %s (recursive)\n", d)
} else {
fmt.Fprintf(&b, " %s\n", d)
}
}
return b.String()
}

View File

@@ -0,0 +1,427 @@
package tsctests
import (
"fmt"
"strings"
"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/execute/incremental"
"github.com/microsoft/typescript-go/internal/json"
)
type readableBuildInfo struct {
buildInfo *incremental.BuildInfo
Version string `json:"version,omitzero"`
// Common between incremental and tsc -b buildinfo for non incremental programs
Errors bool `json:"errors,omitzero"`
CheckPending bool `json:"checkPending,omitzero"`
Root []*readableBuildInfoRoot `json:"root,omitzero"`
PackageJsons []string `json:"packageJsons,omitzero"`
MissingPackageJsons []string `json:"missingPackageJsons,omitzero"`
// IncrementalProgram info
FileNames []string `json:"fileNames,omitzero"`
FileInfos []*readableBuildInfoFileInfo `json:"fileInfos,omitzero"`
FileIdsList [][]string `json:"fileIdsList,omitzero"`
Options *collections.OrderedMap[string, any] `json:"options,omitzero"`
ReferencedMap *collections.OrderedMap[string, []string] `json:"referencedMap,omitzero"`
SemanticDiagnosticsPerFile []*readableBuildInfoSemanticDiagnostic `json:"semanticDiagnosticsPerFile,omitzero"`
EmitDiagnosticsPerFile []*readableBuildInfoDiagnosticsOfFile `json:"emitDiagnosticsPerFile,omitzero"`
ChangeFileSet []string `json:"changeFileSet,omitzero"` // List of changed files in the program, not the whole set of files
AffectedFilesPendingEmit []*readableBuildInfoFilePendingEmit `json:"affectedFilesPendingEmit,omitzero"`
LatestChangedDtsFile string `json:"latestChangedDtsFile,omitzero"` // Because this is only output file in the program, we dont need fileId to deduplicate name
EmitSignatures []*readableBuildInfoEmitSignature `json:"emitSignatures,omitzero"`
ResolvedRoot []*readableBuildInfoResolvedRoot `json:"resolvedRoot,omitzero"`
Size int `json:"size,omitzero"` // Size of the build info file
// NonIncrementalProgram info
SemanticErrors bool `json:"semanticErrors,omitzero"`
}
type readableBuildInfoRoot struct {
Files []string `json:"files,omitzero"`
Original *incremental.BuildInfoRoot `json:"original,omitzero"`
}
type readableBuildInfoFileInfo struct {
FileName string `json:"fileName,omitzero"`
Version string `json:"version,omitzero"`
Signature string `json:"signature,omitzero"`
AffectsGlobalScope bool `json:"affectsGlobalScope,omitzero"`
ImpliedNodeFormat string `json:"impliedNodeFormat,omitzero"`
Original *incremental.BuildInfoFileInfo `json:"original,omitzero"` // Original file path, if available
}
type readableBuildInfoDiagnostic struct {
// incrementalBuildInfoFileId if it is for a File thats other than its stored for
File string `json:"file,omitzero"`
NoFile bool `json:"noFile,omitzero"`
Pos int `json:"pos,omitzero"`
End int `json:"end,omitzero"`
Code int32 `json:"code,omitzero"`
Category diagnostics.Category `json:"category,omitzero"`
MessageKey diagnostics.Key `json:"messageKey,omitzero"`
MessageArgs []string `json:"messageArgs,omitzero"`
MessageChain []*readableBuildInfoDiagnostic `json:"messageChain,omitzero"`
RelatedInformation []*readableBuildInfoDiagnostic `json:"relatedInformation,omitzero"`
ReportsUnnecessary bool `json:"reportsUnnecessary,omitzero"`
ReportsDeprecated bool `json:"reportsDeprecated,omitzero"`
SkippedOnNoEmit bool `json:"skippedOnNoEmit,omitzero"`
}
type readableBuildInfoDiagnosticsOfFile struct {
file string
diagnostics []*readableBuildInfoDiagnostic
}
func (r *readableBuildInfoDiagnosticsOfFile) MarshalJSON() ([]byte, error) {
fileIdAndDiagnostics := make([]any, 0, 2)
fileIdAndDiagnostics = append(fileIdAndDiagnostics, r.file)
fileIdAndDiagnostics = append(fileIdAndDiagnostics, r.diagnostics)
return json.Marshal(fileIdAndDiagnostics)
}
func (r *readableBuildInfoDiagnosticsOfFile) UnmarshalJSON(data []byte) error {
var fileIdAndDiagnostics []any
if err := json.Unmarshal(data, &fileIdAndDiagnostics); err != nil {
return fmt.Errorf("invalid readableBuildInfoDiagnosticsOfFile: %s", data)
}
if len(fileIdAndDiagnostics) != 2 {
return fmt.Errorf("invalid readableBuildInfoDiagnosticsOfFile: expected 2 elements, got %d", len(fileIdAndDiagnostics))
}
file, ok := fileIdAndDiagnostics[0].(string)
if !ok {
return fmt.Errorf("invalid fileId in readableBuildInfoDiagnosticsOfFile: expected string, got %T", fileIdAndDiagnostics[0])
}
if diagnostics, ok := fileIdAndDiagnostics[1].([]*readableBuildInfoDiagnostic); !ok {
return fmt.Errorf("invalid diagnostics in readableBuildInfoDiagnosticsOfFile: expected []*readableBuildInfoDiagnostic, got %T", fileIdAndDiagnostics[1])
} else {
*r = readableBuildInfoDiagnosticsOfFile{
file: file,
diagnostics: diagnostics,
}
return nil
}
}
type readableBuildInfoSemanticDiagnostic struct {
file string // File is not in changedSet and still doesnt have cached diagnostics
diagnostics *readableBuildInfoDiagnosticsOfFile // Diagnostics for file
}
func (r *readableBuildInfoSemanticDiagnostic) MarshalJSON() ([]byte, error) {
if r.file != "" {
return json.Marshal(r.file)
}
return json.Marshal(r.diagnostics)
}
func (r *readableBuildInfoSemanticDiagnostic) UnmarshalJSON(data []byte) error {
var file string
if err := json.Unmarshal(data, &file); err != nil {
var diagnostics readableBuildInfoDiagnosticsOfFile
if err := json.Unmarshal(data, &diagnostics); err != nil {
return fmt.Errorf("invalid readableBuildInfoSemanticDiagnostic: %s", data)
}
*r = readableBuildInfoSemanticDiagnostic{
diagnostics: &diagnostics,
}
return nil
}
*r = readableBuildInfoSemanticDiagnostic{
file: file,
}
return nil
}
type readableBuildInfoFilePendingEmit struct {
file string
emitKind string
original *incremental.BuildInfoFilePendingEmit
}
func (b *readableBuildInfoFilePendingEmit) MarshalJSON() ([]byte, error) {
return json.Marshal([]any{b.file, b.emitKind, b.original})
}
func (b *readableBuildInfoFilePendingEmit) UnmarshalJSON(data []byte) error {
var fileIdAndEmitKind []any
if err := json.Unmarshal(data, &fileIdAndEmitKind); err != nil {
return fmt.Errorf("invalid readableBuildInfoFilePendingEmit: %s", data)
}
if len(fileIdAndEmitKind) != 3 {
return fmt.Errorf("invalid readableBuildInfoFilePendingEmit: expected 3 elements, got %d", len(fileIdAndEmitKind))
}
file, ok := fileIdAndEmitKind[0].(string)
if !ok {
return fmt.Errorf("invalid fileId in readableBuildInfoFilePendingEmit: expected string, got %T", fileIdAndEmitKind[0])
}
var emitKind string
emitKind, ok = fileIdAndEmitKind[1].(string)
if !ok {
return fmt.Errorf("invalid emitKind in readableBuildInfoFilePendingEmit: expected string, got %T", fileIdAndEmitKind[1])
}
var original *incremental.BuildInfoFilePendingEmit
original, ok = fileIdAndEmitKind[2].(*incremental.BuildInfoFilePendingEmit)
if !ok {
return fmt.Errorf("invalid original in readableBuildInfoFilePendingEmit: expected *incremental.BuildInfoFilePendingEmit, got %T", fileIdAndEmitKind[2])
}
*b = readableBuildInfoFilePendingEmit{
file: file,
emitKind: emitKind,
original: original,
}
return nil
}
type readableBuildInfoEmitSignature struct {
File string `json:"file,omitzero"`
Signature string `json:"signature,omitzero"`
DiffersOnlyInDtsMap bool `json:"differsOnlyInDtsMap,omitzero"`
DiffersInOptions bool `json:"differsInOptions,omitzero"`
Original *incremental.BuildInfoEmitSignature `json:"original,omitzero"`
}
type readableBuildInfoResolvedRoot struct {
Resolved string
Root string
}
func (b *readableBuildInfoResolvedRoot) MarshalJSON() ([]byte, error) {
return json.Marshal([2]string{b.Resolved, b.Root})
}
func (b *readableBuildInfoResolvedRoot) UnmarshalJSON(data []byte) error {
var resolvedAndRoot [2]string
if err := json.Unmarshal(data, &resolvedAndRoot); err != nil {
return fmt.Errorf("invalid BuildInfoResolvedRoot: %s", data)
}
*b = readableBuildInfoResolvedRoot{
Resolved: resolvedAndRoot[0],
Root: resolvedAndRoot[1],
}
return nil
}
func toReadableBuildInfo(buildInfo *incremental.BuildInfo, buildInfoText string) string {
readable := readableBuildInfo{
buildInfo: buildInfo,
Version: buildInfo.Version,
Errors: buildInfo.Errors,
CheckPending: buildInfo.CheckPending,
FileNames: buildInfo.FileNames,
Options: buildInfo.Options,
LatestChangedDtsFile: buildInfo.LatestChangedDtsFile,
SemanticErrors: buildInfo.SemanticErrors,
PackageJsons: buildInfo.PackageJsons,
MissingPackageJsons: buildInfo.MissingPackageJsons,
Size: len(buildInfoText),
}
readable.setFileInfos()
readable.setRoot()
readable.setFileIdsList()
readable.setReferencedMap()
readable.setChangeFileSet()
readable.setSemanticDiagnostics()
readable.setEmitDiagnostics()
readable.setAffectedFilesPendingEmit()
readable.setEmitSignatures()
readable.setResolvedRoot()
contents, err := json.MarshalIndent(&readable, "", " ")
if err != nil {
panic("readableBuildInfo: failed to marshal readable build info: " + err.Error())
}
return string(contents)
}
func (r *readableBuildInfo) toFilePath(fileId incremental.BuildInfoFileId) string {
return r.buildInfo.FileNames[fileId-1]
}
func (r *readableBuildInfo) toFilePathSet(fileIdListId incremental.BuildInfoFileIdListId) []string {
return r.FileIdsList[fileIdListId-1]
}
func (r *readableBuildInfo) toReadableBuildInfoDiagnostic(diagnostics []*incremental.BuildInfoDiagnostic) []*readableBuildInfoDiagnostic {
return core.Map(diagnostics, func(d *incremental.BuildInfoDiagnostic) *readableBuildInfoDiagnostic {
var file string
if d.File != 0 {
file = r.toFilePath(d.File)
}
return &readableBuildInfoDiagnostic{
File: file,
NoFile: d.NoFile,
Pos: d.Pos,
End: d.End,
Code: d.Code,
Category: d.Category,
MessageKey: d.MessageKey,
MessageArgs: d.MessageArgs,
MessageChain: r.toReadableBuildInfoDiagnostic(d.MessageChain),
RelatedInformation: r.toReadableBuildInfoDiagnostic(d.RelatedInformation),
ReportsUnnecessary: d.ReportsUnnecessary,
ReportsDeprecated: d.ReportsDeprecated,
SkippedOnNoEmit: d.SkippedOnNoEmit,
}
})
}
func (r *readableBuildInfo) toReadableBuildInfoDiagnosticsOfFile(diagnostics *incremental.BuildInfoDiagnosticsOfFile) *readableBuildInfoDiagnosticsOfFile {
return &readableBuildInfoDiagnosticsOfFile{
file: r.toFilePath(diagnostics.FileId),
diagnostics: r.toReadableBuildInfoDiagnostic(diagnostics.Diagnostics),
}
}
func (r *readableBuildInfo) setFileInfos() {
r.FileInfos = core.MapIndex(r.buildInfo.FileInfos, func(original *incremental.BuildInfoFileInfo, index int) *readableBuildInfoFileInfo {
fileInfo := original.GetFileInfo()
// Dont set original for string encoding
if original.HasSignature() {
original = nil
}
return &readableBuildInfoFileInfo{
FileName: r.toFilePath(incremental.BuildInfoFileId(index + 1)),
Version: fileInfo.Version(),
Signature: fileInfo.Signature(),
AffectsGlobalScope: fileInfo.AffectsGlobalScope(),
ImpliedNodeFormat: fileInfo.ImpliedNodeFormat().String(),
Original: original,
}
})
}
func (r *readableBuildInfo) setRoot() {
r.Root = core.Map(r.buildInfo.Root, func(original *incremental.BuildInfoRoot) *readableBuildInfoRoot {
var files []string
if original.NonIncremental != "" {
files = []string{original.NonIncremental}
} else if original.End == 0 {
files = []string{r.toFilePath(original.Start)}
} else {
files = make([]string, 0, original.End-original.Start+1)
for i := original.Start; i <= original.End; i++ {
files = append(files, r.toFilePath(i))
}
}
return &readableBuildInfoRoot{
Files: files,
Original: original,
}
})
}
func (r *readableBuildInfo) setFileIdsList() {
r.FileIdsList = core.Map(r.buildInfo.FileIdsList, func(ids []incremental.BuildInfoFileId) []string {
return core.Map(ids, r.toFilePath)
})
}
func (r *readableBuildInfo) setReferencedMap() {
if r.buildInfo.ReferencedMap != nil {
r.ReferencedMap = &collections.OrderedMap[string, []string]{}
for _, entry := range r.buildInfo.ReferencedMap {
r.ReferencedMap.Set(r.toFilePath(entry.FileId), r.toFilePathSet(entry.FileIdListId))
}
}
}
func (r *readableBuildInfo) setChangeFileSet() {
r.ChangeFileSet = core.Map(r.buildInfo.ChangeFileSet, r.toFilePath)
}
func (r *readableBuildInfo) setSemanticDiagnostics() {
r.SemanticDiagnosticsPerFile = core.Map(r.buildInfo.SemanticDiagnosticsPerFile, func(diagnostics *incremental.BuildInfoSemanticDiagnostic) *readableBuildInfoSemanticDiagnostic {
if diagnostics.FileId != 0 {
return &readableBuildInfoSemanticDiagnostic{
file: r.toFilePath(diagnostics.FileId),
}
}
return &readableBuildInfoSemanticDiagnostic{
diagnostics: r.toReadableBuildInfoDiagnosticsOfFile(diagnostics.Diagnostics),
}
})
}
func (r *readableBuildInfo) setEmitDiagnostics() {
r.EmitDiagnosticsPerFile = core.Map(r.buildInfo.EmitDiagnosticsPerFile, r.toReadableBuildInfoDiagnosticsOfFile)
}
func (r *readableBuildInfo) setAffectedFilesPendingEmit() {
if r.buildInfo.AffectedFilesPendingEmit == nil {
return
}
fullEmitKind := incremental.GetFileEmitKind(r.buildInfo.GetCompilerOptions(""))
r.AffectedFilesPendingEmit = core.Map(r.buildInfo.AffectedFilesPendingEmit, func(pendingEmit *incremental.BuildInfoFilePendingEmit) *readableBuildInfoFilePendingEmit {
emitKind := core.IfElse(pendingEmit.EmitKind == 0, fullEmitKind, pendingEmit.EmitKind)
return &readableBuildInfoFilePendingEmit{
file: r.toFilePath(pendingEmit.FileId),
emitKind: toReadableFileEmitKind(emitKind),
original: pendingEmit,
}
})
}
func toReadableFileEmitKind(fileEmitKind incremental.FileEmitKind) string {
var builder strings.Builder
addFlags := func(flags string) {
if builder.Len() == 0 {
builder.WriteString(flags)
} else {
builder.WriteString("|")
builder.WriteString(flags)
}
}
if fileEmitKind != 0 {
if (fileEmitKind & incremental.FileEmitKindJs) != 0 {
addFlags("Js")
}
if (fileEmitKind & incremental.FileEmitKindJsMap) != 0 {
addFlags("JsMap")
}
if (fileEmitKind & incremental.FileEmitKindJsInlineMap) != 0 {
addFlags("JsInlineMap")
}
if (fileEmitKind & incremental.FileEmitKindDts) == incremental.FileEmitKindDts {
addFlags("Dts")
} else {
if (fileEmitKind & incremental.FileEmitKindDtsEmit) != 0 {
addFlags("DtsEmit")
}
if (fileEmitKind & incremental.FileEmitKindDtsErrors) != 0 {
addFlags("DtsErrors")
}
}
if (fileEmitKind & incremental.FileEmitKindDtsMap) != 0 {
addFlags("DtsMap")
}
}
if builder.Len() != 0 {
return builder.String()
}
return "None"
}
func (r *readableBuildInfo) setEmitSignatures() {
r.EmitSignatures = core.Map(r.buildInfo.EmitSignatures, func(signature *incremental.BuildInfoEmitSignature) *readableBuildInfoEmitSignature {
return &readableBuildInfoEmitSignature{
File: r.toFilePath(signature.FileId),
Signature: signature.Signature,
DiffersOnlyInDtsMap: signature.DiffersOnlyInDtsMap,
DiffersInOptions: signature.DiffersInOptions,
Original: signature,
}
})
}
func (r *readableBuildInfo) setResolvedRoot() {
r.ResolvedRoot = core.Map(r.buildInfo.ResolvedRoot, func(original *incremental.BuildInfoResolvedRoot) *readableBuildInfoResolvedRoot {
return &readableBuildInfoResolvedRoot{
Resolved: r.toFilePath(original.Resolved),
Root: r.toFilePath(original.Root),
}
})
}

View File

@@ -0,0 +1,202 @@
package tsctests
import (
"context"
"fmt"
"path/filepath"
"slices"
"strings"
"testing"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/execute"
"github.com/microsoft/typescript-go/internal/execute/tsc"
"github.com/microsoft/typescript-go/internal/testutil/baseline"
"github.com/microsoft/typescript-go/internal/tspath"
)
type tscEdit struct {
caption string
commandLineArgs []string
edit func(*TestSys)
expectedDiff string
}
var noChange = &tscEdit{
caption: "no change",
}
var noChangeOnlyEdit = []*tscEdit{
noChange,
}
type tscInput struct {
subScenario string
commandLineArgs []string
files FileMap
cwd string
edits []*tscEdit
env map[string]string
ignoreCase bool
windowsStyleRoot string
}
func (test *tscInput) executeCommand(sys *TestSys, baselineBuilder *strings.Builder, commandLineArgs []string) tsc.CommandLineResult {
fmt.Fprint(baselineBuilder, "tsgo ", strings.Join(commandLineArgs, " "), "\n")
result := execute.CommandLine(context.Background(), sys, commandLineArgs, sys)
switch result.Status {
case tsc.ExitStatusSuccess:
baselineBuilder.WriteString("ExitStatus:: Success")
case tsc.ExitStatusDiagnosticsPresent_OutputsSkipped:
baselineBuilder.WriteString("ExitStatus:: DiagnosticsPresent_OutputsSkipped")
case tsc.ExitStatusDiagnosticsPresent_OutputsGenerated:
baselineBuilder.WriteString("ExitStatus:: DiagnosticsPresent_OutputsGenerated")
case tsc.ExitStatusInvalidProject_OutputsSkipped:
baselineBuilder.WriteString("ExitStatus:: InvalidProject_OutputsSkipped")
case tsc.ExitStatusProjectReferenceCycle_OutputsSkipped:
baselineBuilder.WriteString("ExitStatus:: ProjectReferenceCycle_OutputsSkipped")
case tsc.ExitStatusNotImplemented:
baselineBuilder.WriteString("ExitStatus:: NotImplemented")
default:
panic(fmt.Sprintf("UnknownExitStatus %d", result.Status))
}
return result
}
func (test *tscInput) run(t *testing.T, scenario string) {
t.Helper()
t.Run(test.getBaselineSubFolder()+"/"+test.subScenario, func(t *testing.T) {
t.Parallel()
// initial test tsc compile
baselineBuilder := &strings.Builder{}
sys := newTestSys(test, false)
fmt.Fprint(
baselineBuilder,
"currentDirectory::",
sys.GetCurrentDirectory(),
"\nuseCaseSensitiveFileNames::",
sys.FS().UseCaseSensitiveFileNames(),
"\nInput::\n",
)
sys.baselineFSwithDiff(baselineBuilder)
result := test.executeCommand(sys, baselineBuilder, test.commandLineArgs)
sys.serializeState(baselineBuilder)
if result.Watcher != nil && sys.mockWatchBackend.HasWatches() {
baselineBuilder.WriteString(sys.mockWatchBackend.WatchState())
}
var unexpectedDiff strings.Builder
unexpectedDiff.WriteString(sys.baselinePrograms(baselineBuilder, "Initial build"))
for index, do := range test.edits {
sys.clearOutput()
wg := core.NewWorkGroup(false)
var nonIncrementalSys *TestSys
commandLineArgs := core.IfElse(do.commandLineArgs == nil, test.commandLineArgs, do.commandLineArgs)
wg.Queue(func() {
baselineBuilder.WriteString(fmt.Sprintf("\n\nEdit [%d]:: %s\n", index, do.caption))
if do.edit != nil {
do.edit(sys)
}
changedPaths := sys.fsDiffer.ChangedPaths()
sys.baselineFSwithDiff(baselineBuilder)
if result.Watcher == nil {
test.executeCommand(sys, baselineBuilder, commandLineArgs)
} else {
sys.mockWatchBackend.SendChangedPaths(changedPaths)
result.Watcher.DoCycle()
}
sys.serializeState(baselineBuilder)
if result.Watcher != nil && sys.mockWatchBackend.HasWatches() {
baselineBuilder.WriteString(sys.mockWatchBackend.WatchState())
}
unexpectedDiff.WriteString(sys.baselinePrograms(baselineBuilder, fmt.Sprintf("Edit [%d]:: %s\n", index, do.caption)))
})
wg.Queue(func() {
// Compute build with all the edits
nonIncrementalSys = newTestSys(test, true)
for i := range index + 1 {
if test.edits[i].edit != nil {
test.edits[i].edit(nonIncrementalSys)
}
}
execute.CommandLine(context.Background(), nonIncrementalSys, commandLineArgs, nonIncrementalSys)
})
wg.RunAndWait()
diff := getDiffForIncremental(sys, nonIncrementalSys)
if diff != "" {
baselineBuilder.WriteString(fmt.Sprintf("\n\nDiff:: %s\n", core.IfElse(do.expectedDiff == "", "!!! Unexpected diff, please review and either fix or write explanation as expectedDiff !!!", do.expectedDiff)))
baselineBuilder.WriteString(diff)
if do.expectedDiff == "" {
unexpectedDiff.WriteString(fmt.Sprintf("Edit [%d]:: %s\n!!! Unexpected diff, please review and either fix or write explanation as expectedDiff !!!\n%s\n", index, do.caption, diff))
}
} else if do.expectedDiff != "" {
baselineBuilder.WriteString(fmt.Sprintf("\n\nDiff:: %s !!! Diff not found but explanation present, please review and remove the explanation !!!\n", do.expectedDiff))
unexpectedDiff.WriteString(fmt.Sprintf("Edit [%d]:: %s\n!!! Diff not found but explanation present, please review and remove the explanation !!!\n", index, do.caption))
}
}
baseline.Run(t, strings.ReplaceAll(test.subScenario, " ", "-")+".js", baselineBuilder.String(), baseline.Options{Subfolder: filepath.Join(test.getBaselineSubFolder(), scenario)})
if unexpectedDiff.String() != "" {
t.Errorf("Test %s has unexpected diff %s with incremental build, please review the baseline file", test.subScenario, unexpectedDiff.String())
}
})
}
func getDiffForIncremental(incrementalSys *TestSys, nonIncrementalSys *TestSys) string {
var diffBuilder strings.Builder
nonIncrementalOutputs := nonIncrementalSys.fs.writtenFiles.ToSlice()
slices.Sort(nonIncrementalOutputs)
for _, nonIncrementalOutput := range nonIncrementalOutputs {
if tspath.FileExtensionIs(nonIncrementalOutput, tspath.ExtensionTsBuildInfo) ||
strings.HasSuffix(nonIncrementalOutput, ".readable.baseline.txt") {
// Just check existence
if !incrementalSys.fsFromFileMap().FileExists(nonIncrementalOutput) {
diffBuilder.WriteString(baseline.DiffText("nonIncremental "+nonIncrementalOutput, "incremental "+nonIncrementalOutput, "Exists", ""))
diffBuilder.WriteString("\n")
}
} else {
nonIncrementalText, ok := nonIncrementalSys.fsFromFileMap().ReadFile(nonIncrementalOutput)
if !ok {
panic("Written file not found " + nonIncrementalOutput)
}
incrementalText, ok := incrementalSys.fsFromFileMap().ReadFile(nonIncrementalOutput)
if !ok || incrementalText != nonIncrementalText {
diffBuilder.WriteString(baseline.DiffText("nonIncremental "+nonIncrementalOutput, "incremental "+nonIncrementalOutput, nonIncrementalText, incrementalText))
diffBuilder.WriteString("\n")
}
}
}
incrementalOutput := incrementalSys.getOutput(true)
nonIncrementalOutput := nonIncrementalSys.getOutput(true)
if incrementalOutput != nonIncrementalOutput {
diffBuilder.WriteString(baseline.DiffText("nonIncremental.output.txt", "incremental.output.txt", nonIncrementalOutput, incrementalOutput))
}
return diffBuilder.String()
}
func (test *tscInput) getBaselineSubFolder() string {
commandName := "tsc"
if slices.ContainsFunc(test.commandLineArgs, func(arg string) bool {
switch arg {
case "-b", "--b", "-build", "--build":
return true
}
return false
}) {
commandName = "tsbuild"
}
w := ""
if slices.ContainsFunc(test.commandLineArgs, func(arg string) bool {
switch arg {
case "-w", "--w", "-watch", "--watch":
return true
}
return false
}) {
w = "Watch"
}
return commandName + w
}

View File

@@ -0,0 +1,219 @@
package tsctests
import (
"testing"
"github.com/microsoft/typescript-go/internal/testutil/stringtestutil"
)
func TestShowConfig(t *testing.T) {
t.Parallel()
testCases := []*tscInput{
{
subScenario: "Default initialized TSConfig",
commandLineArgs: []string{"--showConfig"},
},
{
subScenario: "Show TSConfig with files options",
commandLineArgs: []string{"--showConfig", "file0.ts", "file1.ts", "file2.ts"},
},
{
subScenario: "Show TSConfig with boolean value compiler options",
commandLineArgs: []string{"--showConfig", "--noUnusedLocals"},
},
{
subScenario: "Show TSConfig with enum value compiler options",
commandLineArgs: []string{"--showConfig", "--target", "es5", "--jsx", "react"},
},
{
subScenario: "Show TSConfig with list compiler options",
commandLineArgs: []string{"--showConfig", "--types", "jquery,mocha"},
},
{
subScenario: "Show TSConfig with list compiler options with enum value",
commandLineArgs: []string{"--showConfig", "--lib", "es5,es2015.core"},
},
{
subScenario: "Show TSConfig with incorrect compiler option",
commandLineArgs: []string{"--showConfig", "--someNonExistOption"},
},
{
subScenario: "Show TSConfig with incorrect compiler option value",
commandLineArgs: []string{"--showConfig", "--lib", "nonExistLib,es5,es2015.promise"},
},
{
subScenario: "Show TSConfig with advanced options",
commandLineArgs: []string{"--showConfig", "--declaration", "--declarationDir", "lib", "--skipLibCheck", "--noErrorTruncation"},
},
{
subScenario: "Show TSConfig with compileOnSave and more",
files: FileMap{
"/home/src/workspaces/project/src/index.ts": `export const a = 1;`,
"/home/src/workspaces/project/tsconfig.json": stringtestutil.Dedent(`
{
"compilerOptions": {
"esModuleInterop": true,
"target": "es5",
"module": "commonjs",
"strict": true
},
"compileOnSave": true,
"exclude": [
"dist"
],
"files": [],
"include": [
"src/*"
],
"references": [
{ "path": "./test" }
]
}`),
},
commandLineArgs: []string{"-p", "tsconfig.json", "--showConfig"},
},
{
subScenario: "Show TSConfig with paths and more",
files: FileMap{
"/home/src/workspaces/project/src/index.ts": `export const a = 1;`,
"/home/src/workspaces/project/tsconfig.json": stringtestutil.Dedent(`
{
"compilerOptions": {
"allowJs": true,
"outDir": "./lib",
"esModuleInterop": true,
"module": "commonjs",
"moduleResolution": "node",
"target": "ES2017",
"sourceMap": true,
"baseUrl": ".",
"paths": {
"@root/*": ["./*"],
"@configs/*": ["src/configs/*"],
"@common/*": ["src/common/*"],
"*": [
"node_modules/*",
"src/types/*"
]
},
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"resolveJsonModule": true
},
"include": [
"./src/**/*"
]
}`),
},
commandLineArgs: []string{"-p", "tsconfig.json", "--showConfig"},
},
{
subScenario: "Show TSConfig with include filtering files",
files: FileMap{
"/home/src/workspaces/project/src/main.ts": `export const a = 1;`,
"/home/src/workspaces/project/src/util.ts": `export const b = 2;`,
"/home/src/workspaces/project/extra.ts": `export const c = 3;`,
"/home/src/workspaces/project/tsconfig.json": stringtestutil.Dedent(`
{
"compilerOptions": {
"strict": true
},
"include": [
"src/**/*"
]
}`),
},
commandLineArgs: []string{"-p", "tsconfig.json", "--showConfig"},
},
{
subScenario: "Show TSConfig with references",
files: FileMap{
"/home/src/workspaces/project/src/index.ts": `export const a = 1;`,
"/home/src/workspaces/project/tsconfig.json": stringtestutil.Dedent(`
{
"compilerOptions": {
"composite": true,
"strict": true
},
"references": [
{ "path": "./packages/a" },
{ "path": "./packages/b" }
]
}`),
},
commandLineArgs: []string{"-p", "tsconfig.json", "--showConfig"},
},
{
subScenario: "Show TSConfig with exclude",
files: FileMap{
"/home/src/workspaces/project/src/index.ts": `export const a = 1;`,
"/home/src/workspaces/project/test/test1.ts": `import { a } from "../src";`,
"/home/src/workspaces/project/tsconfig.json": stringtestutil.Dedent(`
{
"compilerOptions": {
"strict": true
},
"exclude": [
"test"
]
}`),
},
commandLineArgs: []string{"-p", "tsconfig.json", "--showConfig"},
},
{
subScenario: "Show TSConfig with files and include",
files: FileMap{
"/home/src/workspaces/project/src/main.ts": `export const a = 1;`,
"/home/src/workspaces/project/extra.ts": `export const c = 3;`,
"/home/src/workspaces/project/tsconfig.json": stringtestutil.Dedent(`
{
"compilerOptions": {
"strict": true
},
"files": [
"extra.ts"
],
"include": [
"src/**/*"
]
}`),
},
commandLineArgs: []string{"-p", "tsconfig.json", "--showConfig"},
},
{
subScenario: "Show TSConfig with transitively implied options",
files: FileMap{
"/home/src/workspaces/project/src/index.ts": `export const a = 1;`,
"/home/src/workspaces/project/tsconfig.json": stringtestutil.Dedent(`
{
"compilerOptions": {
"module": "nodenext"
}
}`),
},
commandLineArgs: []string{"-p", "tsconfig.json", "--showConfig"},
},
{
subScenario: "Show TSConfig with exclude and outDir",
files: FileMap{
"/home/src/workspaces/project/src/index.ts": `export const a = 1;`,
"/home/src/workspaces/project/src/bin/tool.ts": `export const b = 2;`,
"/home/src/workspaces/project/tsconfig.json": stringtestutil.Dedent(`
{
"compilerOptions": {
"strict": true,
"outDir": "./build"
},
"exclude": [
"build"
]
}`),
},
commandLineArgs: []string{"-p", "tsconfig.json", "--showConfig"},
},
}
for _, test := range testCases {
test.run(t, "showConfig")
}
}

View File

@@ -0,0 +1,592 @@
package tsctests
import (
"context"
"fmt"
"io"
"maps"
"strconv"
"strings"
"sync"
"time"
"github.com/microsoft/typescript-go/internal/collections"
"github.com/microsoft/typescript-go/internal/compiler"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/diagnostics"
"github.com/microsoft/typescript-go/internal/execute"
"github.com/microsoft/typescript-go/internal/execute/incremental"
"github.com/microsoft/typescript-go/internal/execute/tsc"
"github.com/microsoft/typescript-go/internal/execute/watchmanager"
"github.com/microsoft/typescript-go/internal/locale"
"github.com/microsoft/typescript-go/internal/testutil/fsbaselineutil"
"github.com/microsoft/typescript-go/internal/testutil/harnessutil"
"github.com/microsoft/typescript-go/internal/testutil/stringtestutil"
"github.com/microsoft/typescript-go/internal/tsoptions"
"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/vfstest"
"golang.org/x/text/language"
)
type FileMap map[string]any
var tscLibPath = "/home/src/tslibs/TS/Lib"
var tscDefaultLibContent = stringtestutil.Dedent(`
/// <reference no-default-lib="true"/>
interface Boolean {}
interface Function {}
interface CallableFunction {}
interface NewableFunction {}
interface IArguments {}
interface Number { toExponential: any; }
interface Object {}
interface RegExp {}
interface String { charAt: any; }
interface Array<T> { length: number; [n: number]: T; }
interface ReadonlyArray<T> {}
interface SymbolConstructor {
(desc?: string | number): symbol;
for(name: string): symbol;
readonly toStringTag: symbol;
}
declare var Symbol: SymbolConstructor;
interface Symbol {
readonly [Symbol.toStringTag]: string;
}
declare const console: { log(msg: any): void; };
`)
func getTestLibPathFor(libName string) string {
var libFile string
if value, ok := tsoptions.LibMap.Get(libName); ok {
libFile = value.(string)
} else {
libFile = "lib." + libName + ".d.ts"
}
return tscLibPath + "/" + libFile
}
type TestClock struct {
start time.Time
now time.Time
nowMu sync.Mutex
}
func (t *TestClock) Now() time.Time {
t.nowMu.Lock()
defer t.nowMu.Unlock()
if t.now.IsZero() {
t.now = t.start
}
t.now = t.now.Add(1 * time.Second) // Simulate some time passing
return t.now
}
func (t *TestClock) SinceStart() time.Duration {
return t.Now().Sub(t.start)
}
func NewTscSystem(files FileMap, useCaseSensitiveFileNames bool, cwd string) *TestSys {
clock := &TestClock{start: time.Now()}
return &TestSys{
fs: &testFs{
FS: vfstest.FromMapWithClock(files, useCaseSensitiveFileNames, clock),
},
cwd: cwd,
clock: clock,
}
}
func GetFileMapWithBuild(files FileMap, commandLineArgs []string) FileMap {
sys := newTestSys(&tscInput{
files: maps.Clone(files),
}, false)
execute.CommandLine(context.Background(), sys, commandLineArgs, sys)
sys.fs.writtenFiles.Range(func(key string) bool {
if text, ok := sys.fsFromFileMap().ReadFile(key); ok {
files[key] = text
}
return true
})
return files
}
func newTestSys(tscInput *tscInput, forIncrementalCorrectness bool) *TestSys {
cwd := tscInput.cwd
if cwd == "" {
cwd = "/home/src/workspaces/project"
}
libPath := tscLibPath
if tscInput.windowsStyleRoot != "" {
libPath = tscInput.windowsStyleRoot + libPath[1:]
}
currentWrite := &strings.Builder{}
sys := NewTscSystem(tscInput.files, !tscInput.ignoreCase, cwd)
sys.defaultLibraryPath = libPath
sys.currentWrite = currentWrite
sys.tracer = harnessutil.NewTracerForBaselining(tspath.ComparePathsOptions{
UseCaseSensitiveFileNames: !tscInput.ignoreCase,
CurrentDirectory: cwd,
}, currentWrite)
sys.env = tscInput.env
sys.forIncrementalCorrectness = forIncrementalCorrectness
sys.mockWatchBackend = NewMockWatchBackend()
sys.mockWatchBackend.DirectoryExists = sys.fs.FS.DirectoryExists
sys.fsDiffer = &fsbaselineutil.FSDiffer{
FS: sys.fs.FS.(iovfs.FsWithSys),
DefaultLibs: func() *collections.SyncSet[string] { return sys.fs.defaultLibs },
WrittenFiles: &sys.fs.writtenFiles,
}
// Ensure the default library file is present
sys.ensureLibPathExists("lib.d.ts")
for _, libFile := range tsoptions.TargetToLibMap() {
sys.ensureLibPathExists(libFile)
}
for libFile := range tsoptions.LibFilesSet.Keys() {
sys.ensureLibPathExists(libFile)
}
return sys
}
type TestSys struct {
currentWrite *strings.Builder
programBaselines strings.Builder
programIncludeBaselines strings.Builder
tracer *harnessutil.TracerForBaselining
fsDiffer *fsbaselineutil.FSDiffer
forIncrementalCorrectness bool
mockWatchBackend *MockWatchBackend
fs *testFs
defaultLibraryPath string
cwd string
env map[string]string
clock *TestClock
}
var (
_ tsc.System = (*TestSys)(nil)
_ tsc.CommandLineTesting = (*TestSys)(nil)
)
func (s *TestSys) Now() time.Time {
return s.clock.Now()
}
func (s *TestSys) SinceStart() time.Duration {
return s.clock.SinceStart()
}
func (s *TestSys) FS() vfs.FS {
return s.fs
}
func (s *TestSys) fsFromFileMap() iovfs.FsWithSys {
return s.fsDiffer.FS
}
func (s *TestSys) mapFs() *vfstest.MapFS {
return s.fsDiffer.MapFs()
}
func (s *TestSys) ensureLibPathExists(path string) {
path = s.defaultLibraryPath + "/" + path
if _, ok := s.fsFromFileMap().ReadFile(path); !ok {
if s.fs.defaultLibs == nil {
s.fs.defaultLibs = &collections.SyncSet[string]{}
}
s.fs.defaultLibs.Add(path)
err := s.fsFromFileMap().WriteFile(path, tscDefaultLibContent)
if err != nil {
panic("Failed to write default library file: " + err.Error())
}
}
}
func (s *TestSys) DefaultLibraryPath() string {
return s.defaultLibraryPath
}
func (s *TestSys) GetCurrentDirectory() string {
return s.cwd
}
func (s *TestSys) Writer() io.Writer {
return s.currentWrite
}
func (s *TestSys) WriteOutputIsTTY() bool {
return true
}
func (s *TestSys) GetWidthOfTerminal() int {
if widthStr := s.GetEnvironmentVariable("TS_TEST_TERMINAL_WIDTH"); widthStr != "" {
return core.Must(strconv.Atoi(widthStr))
}
return 0
}
func (s *TestSys) GetEnvironmentVariable(name string) string {
return s.env[name]
}
func (s *TestSys) OnEmittedFiles(result *compiler.EmitResult, mTimesCache *collections.SyncMap[tspath.Path, time.Time]) {
if result != nil {
for _, file := range result.EmittedFiles {
modTime := s.mapFs().GetModTime(file)
if serializedDiff := s.fsDiffer.SerializedDiff(); serializedDiff != nil {
if diff, ok := serializedDiff.Snap[file]; ok && diff.MTime.Equal(modTime) {
// Even though written, timestamp was reverted
continue
}
}
// Ensure that the timestamp for emitted files is in the order
now := s.Now()
if err := s.fsFromFileMap().Chtimes(file, time.Time{}, now); err != nil {
panic("Failed to change time for emitted file: " + file + ": " + err.Error())
}
// Update the mTime cache in --b mode to store the updated timestamp so tests will behave deteministically when finding newest output
if mTimesCache != nil {
path := tspath.ToPath(file, s.GetCurrentDirectory(), s.FS().UseCaseSensitiveFileNames())
if _, found := mTimesCache.Load(path); found {
mTimesCache.Store(path, now)
}
}
}
}
}
func (s *TestSys) OnListFilesStart(w io.Writer) {
fmt.Fprintln(w, listFileStart)
}
func (s *TestSys) OnListFilesEnd(w io.Writer) {
fmt.Fprintln(w, listFileEnd)
}
func (s *TestSys) OnStatisticsStart(w io.Writer) {
fmt.Fprintln(w, statisticsStart)
}
func (s *TestSys) OnStatisticsEnd(w io.Writer) {
fmt.Fprintln(w, statisticsEnd)
}
func (s *TestSys) OnBuildStatusReportStart(w io.Writer) {
fmt.Fprintln(w, buildStatusReportStart)
}
func (s *TestSys) OnBuildStatusReportEnd(w io.Writer) {
fmt.Fprintln(w, buildStatusReportEnd)
}
func (s *TestSys) OnWatchStatusReportStart() {
fmt.Fprintln(s.Writer(), watchStatusReportStart)
}
func (s *TestSys) OnWatchStatusReportEnd() {
fmt.Fprintln(s.Writer(), watchStatusReportEnd)
}
func (s *TestSys) GetTrace(w io.Writer, locale locale.Locale) func(msg *diagnostics.Message, args ...any) {
return func(msg *diagnostics.Message, args ...any) {
fmt.Fprintln(w, traceStart)
defer fmt.Fprintln(w, traceEnd)
// With tsc -b building projects in parallel we cannot serialize the package.json lookup trace
// so trace as if it wasnt cached
str := msg.Localize(locale, args...)
s.tracer.TraceWithWriter(w, str, w == s.Writer())
}
}
func (s *TestSys) writeHeaderToBaseline(builder *strings.Builder, program *incremental.Program) {
if builder.Len() != 0 {
builder.WriteString("\n")
}
if configFilePath := program.Options().ConfigFilePath; configFilePath != "" {
builder.WriteString(tspath.GetRelativePathFromDirectory(s.cwd, configFilePath, tspath.ComparePathsOptions{
UseCaseSensitiveFileNames: s.FS().UseCaseSensitiveFileNames(),
CurrentDirectory: s.GetCurrentDirectory(),
}))
builder.WriteString("::\n")
}
}
func (s *TestSys) WatchBackend() watchmanager.WatchBackend {
return s.mockWatchBackend
}
func (s *TestSys) OnProgram(program *incremental.Program) {
s.writeHeaderToBaseline(&s.programBaselines, program)
testingData := program.GetTestingData()
s.programBaselines.WriteString("SemanticDiagnostics::\n")
for _, file := range program.GetProgram().GetSourceFiles() {
if diagnostics, ok := testingData.SemanticDiagnosticsPerFile.Load(file.Path()); ok {
if oldDiagnostics, ok := testingData.OldProgramSemanticDiagnosticsPerFile.Load(file.Path()); !ok || oldDiagnostics != diagnostics {
s.programBaselines.WriteString("*refresh* ")
s.programBaselines.WriteString(file.FileName())
s.programBaselines.WriteString("\n")
}
} else {
s.programBaselines.WriteString("*not cached* ")
s.programBaselines.WriteString(file.FileName())
s.programBaselines.WriteString("\n")
}
}
// Write signature updates
s.programBaselines.WriteString("Signatures::\n")
for _, file := range program.GetProgram().GetSourceFiles() {
if kind, ok := testingData.UpdatedSignatureKinds[file.Path()]; ok {
switch kind {
case incremental.SignatureUpdateKindComputedDts:
s.programBaselines.WriteString("(computed .d.ts) ")
s.programBaselines.WriteString(file.FileName())
s.programBaselines.WriteString("\n")
case incremental.SignatureUpdateKindStoredAtEmit:
s.programBaselines.WriteString("(stored at emit) ")
s.programBaselines.WriteString(file.FileName())
s.programBaselines.WriteString("\n")
case incremental.SignatureUpdateKindUsedVersion:
s.programBaselines.WriteString("(used version) ")
s.programBaselines.WriteString(file.FileName())
s.programBaselines.WriteString("\n")
}
}
}
var filesWithoutIncludeReason []string
var fileNotInProgramWithIncludeReason []string
includeReasons := program.GetProgram().GetIncludeReasons()
for _, file := range program.GetProgram().GetSourceFiles() {
if _, ok := includeReasons[file.Path()]; !ok {
filesWithoutIncludeReason = append(filesWithoutIncludeReason, string(file.Path()))
}
}
for path := range includeReasons {
if program.GetProgram().GetSourceFileByPath(path) == nil && !program.GetProgram().IsMissingPath(path) {
fileNotInProgramWithIncludeReason = append(fileNotInProgramWithIncludeReason, string(path))
}
}
if len(filesWithoutIncludeReason) > 0 || len(fileNotInProgramWithIncludeReason) > 0 {
s.writeHeaderToBaseline(&s.programIncludeBaselines, program)
s.programIncludeBaselines.WriteString("!!! Expected all files to have include reasons\nfilesWithoutIncludeReason::\n")
for _, file := range filesWithoutIncludeReason {
s.programIncludeBaselines.WriteString(" ")
s.programIncludeBaselines.WriteString(file)
s.programIncludeBaselines.WriteString("\n")
}
s.programIncludeBaselines.WriteString("filesNotInProgramWithIncludeReason::\n")
for _, file := range fileNotInProgramWithIncludeReason {
s.programIncludeBaselines.WriteString(" ")
s.programIncludeBaselines.WriteString(file)
s.programIncludeBaselines.WriteString("\n")
}
}
}
func (s *TestSys) baselinePrograms(baseline *strings.Builder, header string) string {
baseline.WriteString(s.programBaselines.String())
s.programBaselines.Reset()
var result string
if s.programIncludeBaselines.Len() > 0 {
result += fmt.Sprintf("\n\n%s\n!!! Include reasons expectations don't match pls review!!!\n", header)
result += s.programIncludeBaselines.String()
s.programIncludeBaselines.Reset()
baseline.WriteString(result)
}
return result
}
func (s *TestSys) serializeState(baseline *strings.Builder) {
s.baselineOutput(baseline)
s.baselineFSwithDiff(baseline)
// todo watch
// this.serializeWatches(baseline);
// this.timeoutCallbacks.serialize(baseline);
// this.immediateCallbacks.serialize(baseline);
// this.pendingInstalls.serialize(baseline);
// this.service?.baseline();
}
var (
fakeTimeStamp = "HH:MM:SS AM"
fakeDuration = "d.ddds"
buildStartingAt = "build starting at "
buildFinishedIn = "build finished in "
listFileStart = "!!! List files start"
listFileEnd = "!!! List files end"
statisticsStart = "!!! Statistics start"
statisticsEnd = "!!! Statistics end"
buildStatusReportStart = "!!! Build Status Report Start"
buildStatusReportEnd = "!!! Build Status Report End"
watchStatusReportStart = "!!! Watch Status Report Start"
watchStatusReportEnd = "!!! Watch Status Report End"
traceStart = "!!! Trace start"
traceEnd = "!!! Trace end"
)
func (s *TestSys) baselineOutput(baseline io.Writer) {
fmt.Fprint(baseline, "\nOutput::\n")
output := s.getOutput(false)
fmt.Fprint(baseline, output)
}
type outputSanitizer struct {
forComparing bool
lines []string
index int
outputLines []string
}
var (
englishVersion = diagnostics.Version_0.Localize(locale.Default, core.Version())
fakeEnglishVersion = diagnostics.Version_0.Localize(locale.Default, harnessutil.FakeTSVersion)
czech = locale.Locale(language.MustParse("cs"))
czechVersion = diagnostics.Version_0.Localize(czech, core.Version())
fakeCzechVersion = diagnostics.Version_0.Localize(czech, harnessutil.FakeTSVersion)
)
func (o *outputSanitizer) addOutputLine(s string) {
s = strings.ReplaceAll(s, fmt.Sprintf("'%s'", core.Version()), fmt.Sprintf("'%s'", harnessutil.FakeTSVersion))
s = strings.ReplaceAll(s, englishVersion, fakeEnglishVersion)
s = strings.ReplaceAll(s, czechVersion, fakeCzechVersion)
s = fsbaselineutil.SanitizeInternalSymbolName(s)
o.outputLines = append(o.outputLines, s)
}
func (o *outputSanitizer) sanitizeBuildStatusTimeStamp() string {
statusLine := o.lines[o.index]
hhSeparator := strings.IndexRune(statusLine, ':')
if hhSeparator < 2 {
panic("Expected timestamp")
}
return statusLine[:hhSeparator-2] + fakeTimeStamp + statusLine[hhSeparator+len(fakeTimeStamp)-2:]
}
func (o *outputSanitizer) transformLines() string {
for ; o.index < len(o.lines); o.index++ {
line := o.lines[o.index]
if strings.HasPrefix(line, buildStartingAt) {
if !o.forComparing {
o.addOutputLine(buildStartingAt + fakeTimeStamp)
}
continue
}
if strings.HasPrefix(line, buildFinishedIn) {
if !o.forComparing {
o.addOutputLine(buildFinishedIn + fakeDuration)
}
continue
}
if !o.addOrSkipLinesForComparing(listFileStart, listFileEnd, false, nil) &&
!o.addOrSkipLinesForComparing(statisticsStart, statisticsEnd, true, nil) &&
!o.addOrSkipLinesForComparing(traceStart, traceEnd, false, nil) &&
!o.addOrSkipLinesForComparing(buildStatusReportStart, buildStatusReportEnd, false, o.sanitizeBuildStatusTimeStamp) &&
!o.addOrSkipLinesForComparing(watchStatusReportStart, watchStatusReportEnd, false, o.sanitizeBuildStatusTimeStamp) {
o.addOutputLine(line)
}
}
return strings.Join(o.outputLines, "\n")
}
func (o *outputSanitizer) addOrSkipLinesForComparing(
lineStart string,
lineEnd string,
skipEvenIfNotComparing bool,
sanitizeFirstLine func() string,
) bool {
if o.lines[o.index] != lineStart {
return false
}
o.index++
isFirstLine := true
for ; o.index < len(o.lines); o.index++ {
if o.lines[o.index] == lineEnd {
return true
}
if !o.forComparing && !skipEvenIfNotComparing {
line := o.lines[o.index]
if isFirstLine && sanitizeFirstLine != nil {
line = sanitizeFirstLine()
isFirstLine = false
}
o.addOutputLine(line)
}
}
panic("Expected lineEnd" + lineEnd + " not found after " + lineStart)
}
func (s *TestSys) getOutput(forComparing bool) string {
lines := strings.Split(s.currentWrite.String(), "\n")
transformer := &outputSanitizer{
forComparing: forComparing,
lines: lines,
outputLines: make([]string, 0, len(lines)),
}
return transformer.transformLines()
}
func (s *TestSys) clearOutput() {
s.currentWrite.Reset()
s.tracer.Reset()
}
func (s *TestSys) baselineFSwithDiff(baseline io.Writer) {
s.fsDiffer.BaselineFSwithDiff(baseline)
}
func (s *TestSys) writeFileNoError(path string, content string) {
if err := s.fsFromFileMap().WriteFile(path, content); err != nil {
panic(err)
}
}
func (s *TestSys) removeNoError(path string) {
if err := s.fsFromFileMap().Remove(path); err != nil {
panic(err)
}
}
func (s *TestSys) readFileNoError(path string) string {
content, ok := s.fsFromFileMap().ReadFile(path)
if !ok {
panic("File not found: " + path)
}
return content
}
func (s *TestSys) renameFileNoError(oldPath string, newPath string) {
s.writeFileNoError(newPath, s.readFileNoError(oldPath))
s.removeNoError(oldPath)
}
func (s *TestSys) replaceFileText(path string, oldText string, newText string) {
content := s.readFileNoError(path)
content = strings.Replace(content, oldText, newText, 1)
s.writeFileNoError(path, content)
}
func (s *TestSys) replaceFileTextAll(path string, oldText string, newText string) {
content := s.readFileNoError(path)
content = strings.ReplaceAll(content, oldText, newText)
s.writeFileNoError(path, content)
}
func (s *TestSys) appendFile(path string, text string) {
content := s.readFileNoError(path)
s.writeFileNoError(path, content+text)
}
func (s *TestSys) prependFile(path string, text string) {
content := s.readFileNoError(path)
s.writeFileNoError(path, text+content)
}

View File

@@ -0,0 +1,14 @@
package tsctests
import (
"testing"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/testutil/baseline"
)
func TestMain(m *testing.M) {
core.ApplyDebugStackLimit()
defer baseline.Track()()
m.Run()
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,688 @@
package tsctests
import (
"fmt"
"strings"
"testing"
"github.com/microsoft/typescript-go/internal/vfs/vfstest"
)
func TestWatch(t *testing.T) {
t.Parallel()
bunDependencyTest := func() *tscInput {
files := FileMap{}
var index strings.Builder
var fileNames strings.Builder
fileNames.WriteString(`"index.ts"`)
for i := range 12 {
name := fmt.Sprintf("pkg%d", i)
value := fmt.Sprintf("value%d", i)
index.WriteString(fmt.Sprintf(`import { %[1]s } from "./node_modules/.bun/%[2]s/index"; %[1]s;`, value, name))
index.WriteString("\n")
files["/home/src/workspaces/project/node_modules/.bun/"+name+"/index.ts"] = fmt.Sprintf("export const %s = %d;", value, i)
fileNames.WriteString(fmt.Sprintf(`, "node_modules/.bun/%s/index.ts"`, name))
}
files["/home/src/workspaces/project/index.ts"] = index.String()
files["/home/src/workspaces/project/tsconfig.json"] = fmt.Sprintf(`{
"compilerOptions": {},
"files": [%s]
}`, fileNames.String())
return &tscInput{
subScenario: "watch handles many bun dependency files",
files: files,
commandLineArgs: []string{"--watch"},
}
}
testCases := []*tscInput{
{
subScenario: "watch with no tsconfig",
files: FileMap{
"/home/src/workspaces/project/index.ts": "",
},
commandLineArgs: []string{"index.ts", "--watch"},
},
{
subScenario: "watch with tsconfig and incremental",
files: FileMap{
"/home/src/workspaces/project/index.ts": "",
"/home/src/workspaces/project/tsconfig.json": "{}",
},
commandLineArgs: []string{"--watch", "--incremental"},
},
bunDependencyTest(),
{
subScenario: "watch skips build when no files change",
files: FileMap{
"/home/src/workspaces/project/index.ts": `const x: number = 1;`,
"/home/src/workspaces/project/tsconfig.json": "{}",
},
commandLineArgs: []string{"--watch"},
edits: []*tscEdit{
noChange,
},
},
{
subScenario: "watch rebuilds when file is modified",
files: FileMap{
"/home/src/workspaces/project/index.ts": `const x: number = 1;`,
"/home/src/workspaces/project/tsconfig.json": "{}",
},
commandLineArgs: []string{"--watch"},
edits: []*tscEdit{
newTscEdit("modify file", func(sys *TestSys) {
sys.writeFileNoError("/home/src/workspaces/project/index.ts", `const x: number = 2;`)
}),
},
},
{
subScenario: "watch rebuilds when source file is deleted",
files: FileMap{
"/home/src/workspaces/project/a.ts": `import { b } from "./b";`,
"/home/src/workspaces/project/b.ts": `export const b = 1;`,
"/home/src/workspaces/project/tsconfig.json": "{}",
},
commandLineArgs: []string{"--watch"},
edits: []*tscEdit{
{
caption: "delete imported file",
edit: func(sys *TestSys) {
sys.removeNoError("/home/src/workspaces/project/b.ts")
},
expectedDiff: "incremental resolves to .js output from prior build (TS7016) while clean build cannot find module at all (TS2307)",
},
},
},
{
subScenario: "watch detects new file resolving failed import",
files: FileMap{
"/home/src/workspaces/project/a.ts": `import { b } from "./b";`,
"/home/src/workspaces/project/tsconfig.json": "{}",
},
commandLineArgs: []string{"--watch"},
edits: []*tscEdit{
newTscEdit("create missing file", func(sys *TestSys) {
sys.writeFileNoError("/home/src/workspaces/project/b.ts", `export const b = 1;`)
}),
},
},
// Directory-level change detection via imports
{
subScenario: "watch detects imported file added in new directory",
files: FileMap{
"/home/src/workspaces/project/index.ts": `import { util } from "./lib/util";`,
"/home/src/workspaces/project/tsconfig.json": "{}",
},
commandLineArgs: []string{"--watch"},
edits: []*tscEdit{
newTscEdit("create directory and imported file", func(sys *TestSys) {
sys.writeFileNoError("/home/src/workspaces/project/lib/util.ts", `export const util = "hello";`)
}),
},
},
{
subScenario: "watch detects imported directory removed",
files: FileMap{
"/home/src/workspaces/project/index.ts": `import { util } from "./lib/util";`,
"/home/src/workspaces/project/lib/util.ts": `export const util = "hello";`,
"/home/src/workspaces/project/tsconfig.json": "{}",
},
commandLineArgs: []string{"--watch"},
edits: []*tscEdit{
{
caption: "remove directory with imported file",
edit: func(sys *TestSys) {
sys.removeNoError("/home/src/workspaces/project/lib/util.ts")
},
expectedDiff: "incremental resolves to .js output from prior build (TS7016) while clean build cannot find module at all (TS2307)",
},
},
},
{
subScenario: "watch detects import path restructured",
files: FileMap{
"/home/src/workspaces/project/index.ts": `import { util } from "./lib/util";`,
"/home/src/workspaces/project/lib/util.ts": `export const util = "v1";`,
"/home/src/workspaces/project/tsconfig.json": "{}",
},
commandLineArgs: []string{"--watch"},
edits: []*tscEdit{
newTscEdit("move file to new path and update import", func(sys *TestSys) {
sys.removeNoError("/home/src/workspaces/project/lib/util.ts")
sys.writeFileNoError("/home/src/workspaces/project/src/util.ts", `export const util = "v2";`)
sys.writeFileNoError("/home/src/workspaces/project/index.ts", `import { util } from "./src/util";`)
}),
},
},
// tsconfig include/exclude change detection
{
subScenario: "watch rebuilds when tsconfig include pattern adds file",
files: FileMap{
"/home/src/workspaces/project/index.ts": `const x = 1;`,
"/home/src/workspaces/project/tsconfig.json": `{
"compilerOptions": {},
"include": ["*.ts"]
}`,
},
commandLineArgs: []string{"--watch"},
edits: []*tscEdit{
newTscEdit("widen include pattern to add src dir", func(sys *TestSys) {
sys.writeFileNoError("/home/src/workspaces/project/src/extra.ts", `export const extra = 2;`)
sys.writeFileNoError("/home/src/workspaces/project/tsconfig.json", `{
"compilerOptions": {},
"include": ["*.ts", "src/**/*.ts"]
}`)
}),
},
},
{
subScenario: "watch rebuilds when tsconfig is modified to change strict",
files: FileMap{
"/home/src/workspaces/project/index.ts": `const x = null; const y: string = x;`,
"/home/src/workspaces/project/tsconfig.json": `{}`,
},
commandLineArgs: []string{"--watch"},
edits: []*tscEdit{
newTscEdit("enable strict mode", func(sys *TestSys) {
sys.writeFileNoError("/home/src/workspaces/project/tsconfig.json", `{"compilerOptions": {"strict": true}}`)
}),
},
},
// Path resolution: tsconfig include pointing to non-existent directory
{
subScenario: "watch detects file added to previously non-existent include path",
files: FileMap{
"/home/src/workspaces/project/index.ts": `const x = 1;`,
"/home/src/workspaces/project/tsconfig.json": `{
"compilerOptions": {},
"include": ["index.ts", "src/**/*.ts"]
}`,
},
commandLineArgs: []string{"--watch"},
edits: []*tscEdit{
newTscEdit("create src dir with ts file matching include", func(sys *TestSys) {
sys.writeFileNoError("/home/src/workspaces/project/src/helper.ts", `export const helper = "added";`)
}),
},
},
{
subScenario: "watch detects new file in existing include directory",
files: FileMap{
"/home/src/workspaces/project/src/a.ts": `export const a = 1;`,
"/home/src/workspaces/project/tsconfig.json": `{
"compilerOptions": {},
"include": ["src/**/*.ts"]
}`,
},
commandLineArgs: []string{"--watch"},
edits: []*tscEdit{
newTscEdit("add new file to existing src directory", func(sys *TestSys) {
sys.writeFileNoError("/home/src/workspaces/project/src/b.ts", `export const b = 2;`)
}),
},
},
// Wildcard include: nested subdirectory detection
{
subScenario: "watch detects file added in new nested subdirectory",
files: FileMap{
"/home/src/workspaces/project/src/a.ts": `export const a = 1;`,
"/home/src/workspaces/project/tsconfig.json": `{
"compilerOptions": {},
"include": ["src/**/*.ts"]
}`,
},
commandLineArgs: []string{"--watch"},
edits: []*tscEdit{
newTscEdit("create nested dir with ts file", func(sys *TestSys) {
sys.writeFileNoError("/home/src/workspaces/project/src/deep/nested/util.ts", `export const util = "nested";`)
}),
},
},
{
subScenario: "watch detects file added in multiple new subdirectories simultaneously",
files: FileMap{
"/home/src/workspaces/project/src/a.ts": `export const a = 1;`,
"/home/src/workspaces/project/tsconfig.json": `{
"compilerOptions": {},
"include": ["src/**/*.ts"]
}`,
},
commandLineArgs: []string{"--watch"},
edits: []*tscEdit{
newTscEdit("create multiple new subdirs with files", func(sys *TestSys) {
sys.writeFileNoError("/home/src/workspaces/project/src/models/user.ts", `export interface User { name: string; }`)
sys.writeFileNoError("/home/src/workspaces/project/src/utils/format.ts", `export function format(s: string): string { return s.trim(); }`)
}),
},
},
{
subScenario: "watch detects nested subdirectory removed and recreated",
files: FileMap{
"/home/src/workspaces/project/src/lib/helper.ts": `export const helper = "v1";`,
"/home/src/workspaces/project/tsconfig.json": `{
"compilerOptions": {},
"include": ["src/**/*.ts"]
}`,
},
commandLineArgs: []string{"--watch"},
edits: []*tscEdit{
{
caption: "remove nested dir",
expectedDiff: "incremental has prior state and does not report no-inputs error",
edit: func(sys *TestSys) {
sys.removeNoError("/home/src/workspaces/project/src/lib/helper.ts")
},
},
newTscEdit("recreate nested dir with new content", func(sys *TestSys) {
sys.writeFileNoError("/home/src/workspaces/project/src/lib/helper.ts", `export const helper = "v2";`)
}),
},
},
// Path resolution: import from non-existent node_modules package
{
subScenario: "watch detects node modules package added",
files: FileMap{
"/home/src/workspaces/project/index.ts": `import { lib } from "mylib";`,
"/home/src/workspaces/project/tsconfig.json": `{}`,
},
commandLineArgs: []string{"--watch"},
edits: []*tscEdit{
newTscEdit("install package in node_modules", func(sys *TestSys) {
sys.writeFileNoError("/home/src/workspaces/project/node_modules/mylib/package.json", `{"name": "mylib", "main": "index.js", "types": "index.d.ts"}`)
sys.writeFileNoError("/home/src/workspaces/project/node_modules/mylib/index.js", `exports.lib = "hello";`)
sys.writeFileNoError("/home/src/workspaces/project/node_modules/mylib/index.d.ts", `export declare const lib: string;`)
}),
},
},
// Path resolution: node_modules package removed
{
subScenario: "watch detects node modules package removed",
files: FileMap{
"/home/src/workspaces/project/index.ts": `import { lib } from "mylib";`,
"/home/src/workspaces/project/tsconfig.json": `{}`,
"/home/src/workspaces/project/node_modules/mylib/package.json": `{"name": "mylib", "main": "index.js", "types": "index.d.ts"}`,
"/home/src/workspaces/project/node_modules/mylib/index.js": `exports.lib = "hello";`,
"/home/src/workspaces/project/node_modules/mylib/index.d.ts": `export declare const lib: string;`,
},
commandLineArgs: []string{"--watch"},
edits: []*tscEdit{
{
caption: "remove node_modules package",
edit: func(sys *TestSys) {
sys.removeNoError("/home/src/workspaces/project/node_modules/mylib/index.d.ts")
sys.removeNoError("/home/src/workspaces/project/node_modules/mylib/index.js")
sys.removeNoError("/home/src/workspaces/project/node_modules/mylib/package.json")
},
},
},
},
// Path resolution: node_modules removed then reinstalled (npm ci after rm -rf)
{
subScenario: "watch detects node modules reinstalled after deletion",
files: FileMap{
"/home/src/workspaces/project/index.ts": `import { lib } from "mylib";`,
"/home/src/workspaces/project/tsconfig.json": `{}`,
"/home/src/workspaces/project/node_modules/mylib/package.json": `{"name": "mylib", "main": "index.js", "types": "index.d.ts"}`,
"/home/src/workspaces/project/node_modules/mylib/index.js": `exports.lib = "hello";`,
"/home/src/workspaces/project/node_modules/mylib/index.d.ts": `export declare const lib: string;`,
},
commandLineArgs: []string{"--watch"},
edits: []*tscEdit{
{
caption: "delete node_modules entirely",
edit: func(sys *TestSys) {
sys.removeNoError("/home/src/workspaces/project/node_modules/mylib/index.d.ts")
sys.removeNoError("/home/src/workspaces/project/node_modules/mylib/index.js")
sys.removeNoError("/home/src/workspaces/project/node_modules/mylib/package.json")
},
},
newTscEdit("reinstall node_modules", func(sys *TestSys) {
sys.writeFileNoError("/home/src/workspaces/project/node_modules/mylib/package.json", `{"name": "mylib", "main": "index.js", "types": "index.d.ts"}`)
sys.writeFileNoError("/home/src/workspaces/project/node_modules/mylib/index.js", `exports.lib = "hello";`)
sys.writeFileNoError("/home/src/workspaces/project/node_modules/mylib/index.d.ts", `export declare const lib: string;`)
}),
},
},
// Config file lifecycle
{
subScenario: "watch handles tsconfig deleted",
files: FileMap{
"/home/src/workspaces/project/index.ts": `const x = 1;`,
"/home/src/workspaces/project/tsconfig.json": `{}`,
},
commandLineArgs: []string{"--watch"},
edits: []*tscEdit{
{
caption: "delete tsconfig",
expectedDiff: "incremental reports config read error while clean build without tsconfig prints usage help",
edit: func(sys *TestSys) {
sys.removeNoError("/home/src/workspaces/project/tsconfig.json")
},
},
},
},
{
subScenario: "watch handles tsconfig with extends base modified",
files: FileMap{
"/home/src/workspaces/project/index.ts": `const x = null; const y: string = x;`,
"/home/src/workspaces/project/base.json": `{
"compilerOptions": { "strict": false }
}`,
"/home/src/workspaces/project/tsconfig.json": `{
"extends": "./base.json"
}`,
},
commandLineArgs: []string{"--watch"},
edits: []*tscEdit{
newTscEdit("modify base config to enable strict", func(sys *TestSys) {
sys.writeFileNoError("/home/src/workspaces/project/base.json", `{
"compilerOptions": { "strict": true }
}`)
}),
},
},
{
subScenario: "watch rebuilds when tsconfig is touched but content unchanged",
files: FileMap{
"/home/src/workspaces/project/index.ts": `const x = 1;`,
"/home/src/workspaces/project/tsconfig.json": `{}`,
},
commandLineArgs: []string{"--watch"},
edits: []*tscEdit{
newTscEdit("touch tsconfig without changing content", func(sys *TestSys) {
content := sys.readFileNoError("/home/src/workspaces/project/tsconfig.json")
sys.writeFileNoError("/home/src/workspaces/project/tsconfig.json", content)
}),
},
},
{
subScenario: "watch with tsconfig files list entry deleted",
files: FileMap{
"/home/src/workspaces/project/a.ts": `export const a = 1;`,
"/home/src/workspaces/project/b.ts": `export const b = 2;`,
"/home/src/workspaces/project/tsconfig.json": `{
"compilerOptions": {},
"files": ["a.ts", "b.ts"]
}`,
},
commandLineArgs: []string{"--watch"},
edits: []*tscEdit{
newTscEdit("delete file listed in files array", func(sys *TestSys) {
sys.removeNoError("/home/src/workspaces/project/b.ts")
}),
},
},
// Module resolution & dependencies
{
subScenario: "watch detects module going missing then coming back",
files: FileMap{
"/home/src/workspaces/project/index.ts": `import { util } from "./util";`,
"/home/src/workspaces/project/util.ts": `export const util = "v1";`,
"/home/src/workspaces/project/tsconfig.json": `{}`,
},
commandLineArgs: []string{"--watch"},
edits: []*tscEdit{
{
caption: "delete util module",
edit: func(sys *TestSys) {
sys.removeNoError("/home/src/workspaces/project/util.ts")
},
expectedDiff: "incremental resolves to .js output from prior build while clean build cannot find module",
},
newTscEdit("recreate util module with new content", func(sys *TestSys) {
sys.writeFileNoError("/home/src/workspaces/project/util.ts", `export const util = "v2";`)
}),
},
},
{
subScenario: "watch detects scoped package installed",
files: FileMap{
"/home/src/workspaces/project/index.ts": `import { lib } from "@scope/mylib";`,
"/home/src/workspaces/project/tsconfig.json": `{}`,
},
commandLineArgs: []string{"--watch"},
edits: []*tscEdit{
newTscEdit("install scoped package", func(sys *TestSys) {
sys.writeFileNoError("/home/src/workspaces/project/node_modules/@scope/mylib/package.json", `{"name": "@scope/mylib", "types": "index.d.ts"}`)
sys.writeFileNoError("/home/src/workspaces/project/node_modules/@scope/mylib/index.d.ts", `export declare const lib: string;`)
}),
},
},
{
subScenario: "watch detects package json types field edited",
files: FileMap{
"/home/src/workspaces/project/index.ts": `import { lib } from "mylib";`,
"/home/src/workspaces/project/tsconfig.json": `{}`,
"/home/src/workspaces/project/node_modules/mylib/package.json": `{"name": "mylib", "types": "old.d.ts"}`,
"/home/src/workspaces/project/node_modules/mylib/old.d.ts": `export declare const lib: number;`,
"/home/src/workspaces/project/node_modules/mylib/new.d.ts": `export declare const lib: string;`,
},
commandLineArgs: []string{"--watch"},
edits: []*tscEdit{
newTscEdit("change package.json types field", func(sys *TestSys) {
sys.writeFileNoError("/home/src/workspaces/project/node_modules/mylib/package.json", `{"name": "mylib", "types": "new.d.ts"}`)
}),
},
},
{
subScenario: "watch detects at-types package installed later",
files: FileMap{
"/home/src/workspaces/project/index.ts": `import * as lib from "untyped-lib";`,
"/home/src/workspaces/project/tsconfig.json": `{}`,
"/home/src/workspaces/project/node_modules/untyped-lib/index.js": `module.exports = {};`,
},
commandLineArgs: []string{"--watch"},
edits: []*tscEdit{
newTscEdit("install @types for the library", func(sys *TestSys) {
sys.writeFileNoError("/home/src/workspaces/project/node_modules/@types/untyped-lib/index.d.ts", `declare module "untyped-lib" { export const value: string; }`)
sys.writeFileNoError("/home/src/workspaces/project/node_modules/@types/untyped-lib/package.json", `{"name": "@types/untyped-lib", "types": "index.d.ts"}`)
}),
},
},
// File operations
{
subScenario: "watch detects file renamed and renamed back",
files: FileMap{
"/home/src/workspaces/project/index.ts": `import { helper } from "./helper";`,
"/home/src/workspaces/project/helper.ts": `export const helper = 1;`,
"/home/src/workspaces/project/tsconfig.json": `{}`,
},
commandLineArgs: []string{"--watch"},
edits: []*tscEdit{
{
caption: "rename helper to helper2",
edit: func(sys *TestSys) {
sys.renameFileNoError("/home/src/workspaces/project/helper.ts", "/home/src/workspaces/project/helper2.ts")
},
expectedDiff: "incremental resolves to .js output from prior build while clean build cannot find module",
},
newTscEdit("rename back to helper", func(sys *TestSys) {
sys.renameFileNoError("/home/src/workspaces/project/helper2.ts", "/home/src/workspaces/project/helper.ts")
}),
},
},
{
subScenario: "watch detects file deleted and new file added simultaneously",
files: FileMap{
"/home/src/workspaces/project/a.ts": `import { b } from "./b";`,
"/home/src/workspaces/project/b.ts": `export const b = 1;`,
"/home/src/workspaces/project/tsconfig.json": `{}`,
},
commandLineArgs: []string{"--watch"},
edits: []*tscEdit{
newTscEdit("delete b.ts and create c.ts with updated import", func(sys *TestSys) {
sys.removeNoError("/home/src/workspaces/project/b.ts")
sys.writeFileNoError("/home/src/workspaces/project/c.ts", `export const c = 2;`)
sys.writeFileNoError("/home/src/workspaces/project/a.ts", `import { c } from "./c";`)
}),
},
},
{
subScenario: "watch handles file rapidly recreated",
files: FileMap{
"/home/src/workspaces/project/index.ts": `import { val } from "./data";`,
"/home/src/workspaces/project/data.ts": `export const val = "original";`,
"/home/src/workspaces/project/tsconfig.json": `{}`,
},
commandLineArgs: []string{"--watch"},
edits: []*tscEdit{
newTscEdit("delete and immediately recreate with new content", func(sys *TestSys) {
sys.removeNoError("/home/src/workspaces/project/data.ts")
sys.writeFileNoError("/home/src/workspaces/project/data.ts", `export const val = "recreated";`)
}),
},
},
// Symlinks — only node_modules symlinks are resolved via Realpath,
// matching the TypeScript compiler's behavior (see program.ts:2119).
{
subScenario: "watch detects change in symlinked node_modules file",
files: FileMap{
"/home/src/workspaces/project/index.ts": `import { shared } from "shared";`,
"/home/src/workspaces/shared/index.ts": `export const shared = "v1";`,
"/home/src/workspaces/project/node_modules/shared/index.ts": vfstest.Symlink("/home/src/workspaces/shared/index.ts"),
"/home/src/workspaces/project/tsconfig.json": `{}`,
},
commandLineArgs: []string{"--watch"},
edits: []*tscEdit{
newTscEdit("modify symlink target", func(sys *TestSys) {
sys.writeFileNoError("/home/src/workspaces/shared/index.ts", `export const shared = "v2";`)
}),
},
},
// Ancestor fallback stability — when a tsconfig include references a
// directory that doesn't exist
{
subScenario: "watch stability with ancestor directory fallback",
files: FileMap{
"/home/src/workspaces/project/index.ts": `const x: number = 1;`,
"/home/src/workspaces/project/tsconfig.json": `{ "include": ["*.ts", "missing/**/*"] }`,
},
commandLineArgs: []string{"--watch"},
edits: []*tscEdit{
newTscEdit("trivial file change", func(sys *TestSys) {
sys.writeFileNoError("/home/src/workspaces/project/index.ts", `const x: number = 2;`)
}),
},
},
// Ancestor fallback: creating deeply nested directories that didn't
// exist at initial build time should trigger a rebuild and re-watch.
{
subScenario: "watch detects file added in deeply nested non-existent include path",
files: FileMap{
"/home/src/workspaces/project/index.ts": `const x: number = 1;`,
"/home/src/workspaces/project/tsconfig.json": `{ "include": ["*.ts", "deep/nested/dir/**/*"] }`,
},
commandLineArgs: []string{"--watch"},
edits: []*tscEdit{
newTscEdit("create deeply nested file matching include", func(sys *TestSys) {
sys.writeFileNoError("/home/src/workspaces/project/deep/nested/dir/added.ts", `export const added = 1;`)
}),
},
},
}
for _, test := range testCases {
test.run(t, "commandLineWatch")
}
}
func listToTsconfig(base string, tsconfigOpts ...string) (string, string) {
optionString := strings.Join(tsconfigOpts, ",\n ")
tsconfigText := `{
"compilerOptions": {
`
after := " "
if base != "" {
tsconfigText += " " + base
after = ",\n "
}
if len(tsconfigOpts) != 0 {
tsconfigText += after + optionString
}
tsconfigText += `
}
}`
return tsconfigText, optionString
}
func toTsconfig(base string, compilerOpts string) string {
tsconfigText, _ := listToTsconfig(base, compilerOpts)
return tsconfigText
}
func noEmitWatchTestInput(
subScenario string,
commandLineArgs []string,
aText string,
tsconfigOptions []string,
) *tscInput {
noEmitOpt := `"noEmit": true`
tsconfigText, optionString := listToTsconfig(noEmitOpt, tsconfigOptions...)
return &tscInput{
subScenario: subScenario,
commandLineArgs: commandLineArgs,
files: FileMap{
"/home/src/workspaces/project/a.ts": aText,
"/home/src/workspaces/project/tsconfig.json": tsconfigText,
},
edits: []*tscEdit{
newTscEdit("fix error", func(sys *TestSys) {
sys.writeFileNoError("/home/src/workspaces/project/a.ts", `const a = "hello";`)
}),
newTscEdit("emit after fixing error", func(sys *TestSys) {
sys.writeFileNoError("/home/src/workspaces/project/tsconfig.json", toTsconfig("", optionString))
}),
newTscEdit("no emit run after fixing error", func(sys *TestSys) {
sys.writeFileNoError("/home/src/workspaces/project/tsconfig.json", toTsconfig(noEmitOpt, optionString))
}),
newTscEdit("introduce error", func(sys *TestSys) {
sys.writeFileNoError("/home/src/workspaces/project/a.ts", aText)
}),
newTscEdit("emit when error", func(sys *TestSys) {
sys.writeFileNoError("/home/src/workspaces/project/tsconfig.json", toTsconfig("", optionString))
}),
newTscEdit("no emit run when error", func(sys *TestSys) {
sys.writeFileNoError("/home/src/workspaces/project/tsconfig.json", toTsconfig(noEmitOpt, optionString))
}),
},
}
}
func newTscEdit(name string, edit func(sys *TestSys)) *tscEdit {
return &tscEdit{caption: name, edit: edit}
}
func TestTscNoEmitWatch(t *testing.T) {
t.Parallel()
testCases := []*tscInput{
noEmitWatchTestInput(
"syntax errors",
[]string{"-w"},
`const a = "hello`,
nil,
),
noEmitWatchTestInput(
"semantic errors",
[]string{"-w"},
`const a: number = "hello"`,
nil,
),
noEmitWatchTestInput(
"dts errors without dts enabled",
[]string{"-w"},
`const a = class { private p = 10; };`,
nil,
),
noEmitWatchTestInput(
"dts errors",
[]string{"-w"},
`const a = class { private p = 10; };`,
[]string{`"declaration": true`},
),
}
for _, test := range testCases {
test.run(t, "noEmit")
}
}

View File

@@ -0,0 +1,292 @@
package tsctests
import (
"context"
"fmt"
"sync"
"testing"
"time"
"github.com/microsoft/typescript-go/internal/execute"
"github.com/microsoft/typescript-go/internal/execute/tsc"
"gotest.tools/v3/assert"
)
// createTestWatcher sets up a minimal project with a tsconfig and
// returns a Watcher ready for concurrent testing, plus the TestSys
// for file manipulation.
func createTestWatcher(t *testing.T) (*execute.Watcher, *TestSys) {
t.Helper()
input := &tscInput{
files: FileMap{
"/home/src/workspaces/project/a.ts": `const a: number = 1;`,
"/home/src/workspaces/project/b.ts": `import { a } from "./a"; export const b = a;`,
"/home/src/workspaces/project/tsconfig.json": `{}`,
},
commandLineArgs: []string{"--watch"},
}
sys := newTestSys(input, false)
result := execute.CommandLine(context.Background(), sys, []string{"--watch"}, sys)
if result.Watcher == nil {
t.Fatal("expected Watcher to be non-nil in watch mode")
}
w, ok := result.Watcher.(*execute.Watcher)
if !ok {
t.Fatalf("expected *execute.Watcher, got %T", result.Watcher)
}
return w, sys
}
// TestWatcherConcurrentDoCycle calls DoCycle from multiple goroutines
// while modifying source files, exposing data races on Watcher fields
// such as configModified, program, config, and the underlying
// FileWatcher state. Run with -race to detect.
func TestWatcherConcurrentDoCycle(t *testing.T) {
t.Parallel()
w, sys := createTestWatcher(t)
var wg sync.WaitGroup
for i := range 8 {
wg.Add(1)
go func(i int) {
defer wg.Done()
for j := range 10 {
_ = sys.fsFromFileMap().WriteFile(
"/home/src/workspaces/project/a.ts",
fmt.Sprintf("const a: number = %d;", i*10+j),
)
w.DoCycle()
}
}(i)
}
wg.Wait()
}
// TestWatcherDoCycleWithConcurrentStateReads calls DoCycle from
// multiple goroutines, some modifying files and some not, to test
// concurrent access to all Watcher and FileWatcher state.
func TestWatcherDoCycleWithConcurrentStateReads(t *testing.T) {
t.Parallel()
w, sys := createTestWatcher(t)
var wg sync.WaitGroup
// DoCycle goroutines
for i := range 4 {
wg.Add(1)
go func(i int) {
defer wg.Done()
for j := range 15 {
_ = sys.fsFromFileMap().WriteFile(
"/home/src/workspaces/project/a.ts",
fmt.Sprintf("const a: number = %d;", i*15+j),
)
w.DoCycle()
}
}(i)
}
// State reader goroutines
for range 8 {
wg.Go(func() {
for range 50 {
w.DoCycle()
w.DoCycle()
w.DoCycle()
w.DoCycle()
}
})
}
wg.Wait()
}
// TestWatcherConcurrentFileChangesAndDoCycle creates, modifies, and
// deletes files from multiple goroutines while DoCycle runs, testing
// races between FS mutations and watch state updates.
func TestWatcherConcurrentFileChangesAndDoCycle(t *testing.T) {
t.Parallel()
w, sys := createTestWatcher(t)
var wg sync.WaitGroup
// File creators
for i := range 4 {
wg.Add(1)
go func(i int) {
defer wg.Done()
for j := range 20 {
path := fmt.Sprintf("/home/src/workspaces/project/gen_%d_%d.ts", i, j)
_ = sys.fsFromFileMap().WriteFile(path, fmt.Sprintf("export const x%d_%d = %d;", i, j, j))
}
}(i)
}
// File deleters
wg.Go(func() {
for j := range 20 {
_ = sys.fsFromFileMap().Remove(
fmt.Sprintf("/home/src/workspaces/project/gen_0_%d.ts", j),
)
}
})
// DoCycle callers
for range 4 {
wg.Go(func() {
for range 10 {
w.DoCycle()
}
})
}
wg.Wait()
}
// TestWatcherRapidConfigChanges modifies tsconfig.json rapidly from
// multiple goroutines while DoCycle runs, testing races on
// config-related fields (configModified, configHasErrors,
// configFilePaths, config, extendedConfigCache).
func TestWatcherRapidConfigChanges(t *testing.T) {
t.Parallel()
w, sys := createTestWatcher(t)
var wg sync.WaitGroup
configs := []string{
`{}`,
`{"compilerOptions": {"strict": true}}`,
`{"compilerOptions": {"target": "ES2020"}}`,
`{"compilerOptions": {"noEmit": true}}`,
}
// Config modifiers + DoCycle
for i := range 3 {
wg.Add(1)
go func(i int) {
defer wg.Done()
for j := range 10 {
_ = sys.fsFromFileMap().WriteFile(
"/home/src/workspaces/project/tsconfig.json",
configs[(i+j)%len(configs)],
)
w.DoCycle()
}
}(i)
}
// Concurrent source file modifications
for i := range 2 {
wg.Add(1)
go func(i int) {
defer wg.Done()
for j := range 15 {
_ = sys.fsFromFileMap().WriteFile(
"/home/src/workspaces/project/a.ts",
fmt.Sprintf("const a: number = %d;", i*15+j),
)
w.DoCycle()
}
}(i)
}
// State readers
for range 4 {
wg.Go(func() {
for range 30 {
w.DoCycle()
w.DoCycle()
}
})
}
wg.Wait()
}
// TestWatcherConcurrentDoCycleNoChanges calls DoCycle from many
// goroutines when no files have changed, testing the early-return
// path where WatchState is read and HasChanges is called.
func TestWatcherConcurrentDoCycleNoChanges(t *testing.T) {
t.Parallel()
w, _ := createTestWatcher(t)
var wg sync.WaitGroup
for range 16 {
wg.Go(func() {
for range 50 {
w.DoCycle()
}
})
}
wg.Wait()
}
// TestWatcherAlternatingModifyAndDoCycle alternates between modifying
// a file and calling DoCycle from different goroutines, creating a
// realistic scenario where the file watcher detects changes mid-cycle.
func TestWatcherAlternatingModifyAndDoCycle(t *testing.T) {
t.Parallel()
w, sys := createTestWatcher(t)
var wg sync.WaitGroup
// Writer goroutine: continuously modifies files
wg.Go(func() {
for j := range 100 {
_ = sys.fsFromFileMap().WriteFile(
"/home/src/workspaces/project/a.ts",
fmt.Sprintf("const a: number = %d;", j),
)
}
})
// Multiple DoCycle goroutines
for range 4 {
wg.Go(func() {
for range 25 {
w.DoCycle()
}
})
}
// State reader goroutines
for range 4 {
wg.Go(func() {
for range 100 {
w.DoCycle()
}
})
}
wg.Wait()
}
func TestBuildWatchStopsWhenContextIsCancelled(t *testing.T) {
t.Parallel()
sys := newTestSys(&tscInput{
files: FileMap{
"/home/src/workspaces/project/tsconfig.json": `{"compilerOptions":{"composite":true},"files":["index.ts"]}`,
"/home/src/workspaces/project/index.ts": `export const x = 1;`,
},
}, false)
ctx, cancel := context.WithCancel(context.Background())
cancel()
resultCh := make(chan tsc.CommandLineResult, 1)
go func() {
resultCh <- execute.CommandLine(ctx, sys, []string{"--build", "--watch", "--watchInterval", "60000"}, sys)
}()
select {
case result := <-resultCh:
assert.Equal(t, result.Status, tsc.ExitStatusSuccess)
assert.Assert(t, result.Watcher != nil)
case <-time.After(2 * time.Second):
t.Fatal("build watch did not stop after context cancellation")
}
}