diff --git a/packages/typescript/test/sync/shutdown.test.ts b/packages/typescript/test/sync/shutdown.test.ts new file mode 100644 index 0000000000000..030e959af88fa --- /dev/null +++ b/packages/typescript/test/sync/shutdown.test.ts @@ -0,0 +1,36 @@ +import assert from "node:assert"; +import { spawn } from "node:child_process"; +import { test } from "node:test"; + +test("close shuts down the API process cleanly", async () => { + const script = ` + import { API } from "@typescript/typescript/unstable/sync"; + const api = new API({ cwd: process.cwd() }); + api.updateSnapshot().dispose(); + api.close(); + `; + + const results = await Promise.all(Array.from({ length: 20 }, () => runNode(script))); + for (const result of results) { + assert.equal(result.status, 0); + assert.equal(result.stderr, ""); + } +}); + +function runNode(script: string): Promise<{ status: number | null; stderr: string; }> { + return new Promise((resolve, reject) => { + const child = spawn(process.execPath, [ + "--conditions", + "@typescript/source", + "--experimental-strip-types", + "--input-type=module", + "--eval", + script, + ]); + let stderr = ""; + child.stderr.setEncoding("utf8"); + child.stderr.on("data", chunk => stderr += chunk); + child.on("error", reject); + child.on("close", status => resolve({ status, stderr })); + }); +} diff --git a/tsc/internal/api/server.go b/tsc/internal/api/server.go index 817ddebffa2e7..cb3c4a0672f34 100644 --- a/tsc/internal/api/server.go +++ b/tsc/internal/api/server.go @@ -120,5 +120,12 @@ func (s *StdioServer) Run(ctx context.Context) error { callbackFS.SetConnection(ctx, conn) } - return conn.Run(ctx) + return serverRunError(ctx, conn.Run(ctx)) +} + +func serverRunError(ctx context.Context, err error) error { + if ctx.Err() != nil { + return nil + } + return err } diff --git a/tsc/internal/api/server_test.go b/tsc/internal/api/server_test.go new file mode 100644 index 0000000000000..00607cdde5c90 --- /dev/null +++ b/tsc/internal/api/server_test.go @@ -0,0 +1,38 @@ +package api + +import ( + "context" + "errors" + "testing" + + "gotest.tools/v3/assert" +) + +func TestServerRunError(t *testing.T) { + t.Parallel() + + t.Run("EOF", func(t *testing.T) { + t.Parallel() + assert.NilError(t, serverRunError(context.Background(), nil)) + }) + + t.Run("context cancellation", func(t *testing.T) { + t.Parallel() + ctx, cancel := context.WithCancel(context.Background()) + cancel() + assert.NilError(t, serverRunError(ctx, context.Canceled)) + }) + + t.Run("unrelated cancellation", func(t *testing.T) { + t.Parallel() + err := context.Canceled + assert.ErrorIs(t, serverRunError(context.Background(), err), err) + }) + + t.Run("context cancellation supersedes server error", func(t *testing.T) { + t.Parallel() + ctx, cancel := context.WithCancel(context.Background()) + cancel() + assert.NilError(t, serverRunError(ctx, errors.New("server failed"))) + }) +}