Repository: microsoft/typescript-go (go)

Issue:
`--watch` assumes a config file exists, crashes if not
```
mkdir foo
cd foo
touch index.ts
../built/local/tsgo.exe tsc index.ts --watch
```

```
panic: runtime error: invalid memory address or nil pointer dereference
[signal 0xc0000005 code=0x0 addr=0x20 pc=0x7ff6fde2bc94]

goroutine 1 [running]:
github.com/microsoft/typescript-go/internal/execute.createWatcher(...)
        D:/typescript-go/internal/execute/watcher.go:28
github.com/microsoft/typescript-go/internal/execute.executeCommandLineWorker({0x7ff6fe128bf0, 0x4000112a00}, 0x0, 0x4000064680)
        D:/typescript-go/internal/execute/tsc.go:125 +0x5b4
github.com/microsoft/typescript-go/internal/execute.CommandLine({0x7ff6fe128bf0, 0x4000112a00}, 0x0, {0x40000640a0?, 0x2?, 0x2?})
        D:/typescript-go/internal/execute/tsc.go:18 +0x7c
main.runMain()
        D:/typescript-go/cmd/tsgo/main.go:136 +0x174
main.main()
        D:/typescript-go/cmd/tsgo/main.go:126 +0x1c

Pull request:
fix `--watch` crash if no tsconfig found
fixes #840

Patch:
diff --git a/internal/execute/watch.go b/internal/execute/watch.go
index 60ce4a214..392ab13de 100644
--- a/internal/execute/watch.go
+++ b/internal/execute/watch.go
@@ -8,9 +8,8 @@ import (
 )
 
 func start(w *watcher) ExitStatus {
-	if w.configFileName == "" {
-		w.host = compiler.NewCompilerHost(w.options.CompilerOptions(), w.sys.GetCurrentDirectory(), w.sys.FS(), w.sys.DefaultLibraryPath())
-	}
+	w.initialize()
+
 	watchInterval := 1000 * time.Millisecond
 	if w.options.ParsedConfig.WatchOptions != nil {
 		watchInterval = time.Duration(*w.options.ParsedConfig.WatchOptions.Interval) * time.Millisecond
@@ -21,7 +20,16 @@ func start(w *watcher) ExitStatus {
 	}
 }
 
+func (w *watcher) initialize() {
+	// if this function is updated, make sure to update `StartForTest` in export_test.go as needed
+	if w.configFileName == "" {
+		w.host = compiler.NewCompilerHost(w.options.CompilerOptions(), w.sys.GetCurrentDirectory(), w.sys.FS(), w.sys.DefaultLibraryPath())
+	}
+}
+
 func (w *watcher) doCycle() {
+	// if this function is updated, make sure to update `RunWatchCycle` in export_test.go as needed
+
 	if w.hasErrorsInTsConfig() {
 		// these are unrecoverable errors--report them and do not build
 		return
diff --git a/internal/execute/watcher.go b/internal/execute/watcher.go
index 047deeda8..d9187c6ff 100644
--- a/internal/execute/watcher.go
+++ b/internal/execute/watcher.go
@@ -23,13 +23,16 @@ type watcher struct {
 }
 
 func createWatcher(sys System, configParseResult *tsoptions.ParsedCommandLine, reportDiagnostic diagnosticReporter) *watcher {
-	return &watcher{
+	w := &watcher{
 		sys:              sys,
-		configFileName:   configParseResult.ConfigFile.SourceFile.FileName(),
 		options:          configParseResult,
 		reportDiagnostic: reportDiagnostic,
 		// reportWatchStatus: createWatchStatusReporter(sys, configParseResult.CompilerOptions().Pretty),
 	}
+	if configParseResult.ConfigFile != nil {
+		w.configFileName = configParseResult.ConfigFile.SourceFile.FileName()
+	}
+	return w
 }
 
 func (w *watcher) compileAndEmit() {


Test patch:
diff --git a/internal/execute/export_test.go b/internal/execute/export_test.go
index 4ab7e3f32..8c4776306 100644
--- a/internal/execute/export_test.go
+++ b/internal/execute/export_test.go
@@ -17,6 +17,11 @@ func CommandLineTestWatch(sys System, cb cbType, commandLineArgs []string) (*tso
 	return parsedCommandLine, w
 }
 
+func StartForTest(w *watcher) {
+	// this function should perform any initializations before w.doCycle() in `start(watcher)`
+	w.initialize()
+}
+
 func RunWatchCycle(w *watcher) {
 	// this function should perform the same stuff as w.doCycle() without printing time-related output
 	if w.hasErrorsInTsConfig() {
diff --git a/internal/execute/verifytscwatch_test.go b/internal/execute/verifytscwatch_test.go
index a9623af29..6a8fc6ad3 100644
--- a/internal/execute/verifytscwatch_test.go
+++ b/internal/execute/verifytscwatch_test.go
@@ -27,6 +27,7 @@ func verifyWatch(t *testing.T, test *tscInput, scenario string, edits []*testTsc
 			baselineBuilder.WriteString("\n\n")
 
 			// build initial state
+			execute.StartForTest(watcher)
 			execute.RunWatchCycle(watcher)
 			test.sys.serializeState(baselineBuilder)
 
@@ -44,6 +45,29 @@ func verifyWatch(t *testing.T, test *tscInput, scenario string, edits []*testTsc
 	})
 }
 
+func TestWatch(t *testing.T) {
+	t.Parallel()
+	if !bundled.Embedded {
+		// Without embedding, we'd need to read all of the lib files out from disk into the MapFS.
+		// Just skip this for now.
+		t.Skip("bundled files are not embedded")
+	}
+
+	testCases := []*tscInput{
+		{
+			subScenario: "watch with no tsconfig",
+			sys: newTestSys(FileMap{
+				"/home/src/workspaces/project/index.ts": "",
+			}, "/home/src/workspaces/project"),
+			commandLineArgs: []string{"index.ts", "--watch"},
+		},
+	}
+
+	for _, test := range testCases {
+		verifyWatch(t, test, "commandLineWatch", nil)
+	}
+}
+
 func listToTsconfig(base string, tsconfigOpts ...string) (string, string) {
 	optionString := strings.Join(tsconfigOpts, ",\n            ")
 	tsconfigText := `{
diff --git a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-with-no-tsconfig.js b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-with-no-tsconfig.js
new file mode 100644
index 000000000..4e81e06b4
--- /dev/null
+++ b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-with-no-tsconfig.js
@@ -0,0 +1,19 @@
+
+currentDirectory::/home/src/workspaces/project
+useCaseSensitiveFileNames::true
+Input::index.ts --watch
+//// [/home/src/workspaces/project/index.ts] new file
+
+
+
+
+CompilerOptions::{
+    "watch": true
+}
+
+
+Output::
+No output
+//// [/home/src/workspaces/project/index.ts] new file
+
+