diff --git a/.github/workflows/source-lints.yml b/.github/workflows/source-lints.yml index ffbca8fcfc30..4c4da3318013 100644 --- a/.github/workflows/source-lints.yml +++ b/.github/workflows/source-lints.yml @@ -25,6 +25,7 @@ on: - "test/harness.ts" - "test/tsconfig.json" - "test/_util/**" + - "test/integration/bun-types/bun-types.test.ts" - "test/internal/source-lints/**" - ".github/workflows/source-lints.yml" - ".github/actions/setup-bun/**" @@ -43,6 +44,7 @@ on: - "test/harness.ts" - "test/tsconfig.json" - "test/_util/**" + - "test/integration/bun-types/bun-types.test.ts" - "test/internal/source-lints/**" - ".github/workflows/source-lints.yml" - ".github/actions/setup-bun/**" diff --git a/test/integration/bun-types/bun-types.test.ts b/test/integration/bun-types/bun-types.test.ts index 05fa801f45e5..ab7e4812ad45 100644 --- a/test/integration/bun-types/bun-types.test.ts +++ b/test/integration/bun-types/bun-types.test.ts @@ -80,6 +80,14 @@ beforeAll(async () => { }, }), }); + + // Makes the fixture a project `tsc -p .` can be pointed at (the tsc case below). skipLibCheck + // is off for the same reason as in diagnose(): the .d.ts files are what is under test. The + // typeTest cases build their options from sourceTsconfig in-process and never read this file. + const tsconfig = structuredClone(sourceTsconfig); + tsconfig.compilerOptions.skipLibCheck = false; + tsconfig.include = ["*.ts", "*.tsx"]; + await Bun.write(join(BASE_FIXTURE_DIR, "tsconfig.json"), JSON.stringify(tsconfig, null, 2)); } catch (e) { if (e instanceof Bun.$.ShellError) { console.log(e.stderr.toString()); @@ -120,7 +128,8 @@ async function createIsolatedFixture(packages?: string[]): Promise { function typeTest(name: string, config: TypeTestConfig) { // This file only tests the bun-types .d.ts, not bun's own code. Driving the // TypeScript LanguageService in-process under a debug build is ~40x slower, - // so run the type-checking cases on release builds only. + // so run these cases on release builds only; on debug builds the fixture is + // still checked by the spawned tsc case below. test.skipIf(isDebug)(name, async () => { const fixtureDir = await createIsolatedFixture(config.packages); const { diagnostics, emptyInterfaces } = await diagnose(fixtureDir, { @@ -305,10 +314,6 @@ afterAll(async () => { if (TEMP_DIR) { if (Bun.env.TYPES_INTEGRATION_TEST_KEEP_TEMP_DIR === "true") { console.log(`Keeping temp dir ${TEMP_DIR} for debugging`); - // Write tsconfig with skipLibCheck disabled for proper type checking - const tsconfig = structuredClone(sourceTsconfig); - tsconfig.compilerOptions.skipLibCheck = false; - await Bun.write(join(TEMP_DIR, "base-fixture", "tsconfig.json"), JSON.stringify(tsconfig, null, 2)); } else { await rm(TEMP_DIR, { recursive: true, force: true }); } @@ -333,61 +338,20 @@ describe("@types/bun integration test", () => { }); }); - // TypeScript 7's native (Go-based) compiler does not expose a JS compiler API yet, - // so unlike the tests above we have to write a real tsconfig and spawn the CLI. - // https://devblogs.microsoft.com/typescript/announcing-typescript-7-0-beta/ - describe("tsgo (TypeScript 7 native preview)", () => { - test.skipIf(isDebug)("checks without lib.dom.d.ts", async () => { - const fixtureDir = await createIsolatedFixture(["@typescript/native-preview"]); - - const tsconfig = structuredClone(sourceTsconfig); - tsconfig.compilerOptions.skipLibCheck = false; - tsconfig.include = ["*.ts", "*.tsx"]; - await Bun.write(join(fixtureDir, "tsconfig.json"), JSON.stringify(tsconfig, null, 2)); - - // Resolve the entrypoint from the package's own bin field; the nightly - // has renamed it before (bin/tsgo.js -> bin/tsgo). - const tsgoPkgDir = join(fixtureDir, "node_modules", "@typescript", "native-preview"); - const tsgoPkg = await Bun.file(join(tsgoPkgDir, "package.json")).json(); - const tsgo = join(tsgoPkgDir, typeof tsgoPkg.bin === "string" ? tsgoPkg.bin : tsgoPkg.bin.tsgo); - - await using proc = Bun.spawn({ - cmd: [bunExe(), tsgo, "-p", "."], - env: bunEnv, - cwd: fixtureDir, - stdout: "pipe", - stderr: "pipe", - }); - - const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); - - expect(stderr.trim()).toBe(""); - expect(stdout.trim()).toBe(""); - expect(exitCode).toBe(0); - }); - }); - - // Runs on debug builds too: spawning tsc over a single file is cheap, - // unlike the in-process LanguageService runs above. - describe("Bun.mmap", () => { - test("MMapOptions accepts offset and size", async () => { - const checkDir = join(TEMP_DIR, "mmap-options-check"); - const tsconfig = structuredClone(sourceTsconfig); - tsconfig.include = ["mmap-options.ts"]; - tsconfig.compilerOptions.typeRoots = [join(BASE_FIXTURE_DIR, "node_modules", "@types")]; - await mkdir(checkDir, { recursive: true }); - await makeTree(checkDir, { - "tsconfig.json": JSON.stringify(tsconfig, null, 2), - "mmap-options.ts": `const view = Bun.mmap("./data.bin", { shared: true, sync: false, offset: 4096, size: 1024 }); - view satisfies Uint8Array; - Bun.mmap("./data.bin", { offset: 4096 }) satisfies Uint8Array; - Bun.mmap("./data.bin", { size: 1024 }) satisfies Uint8Array;`, - }); - + // The same fixture checked by the typescript the fixture installs (`latest`, the native + // compiler since 7.0; it ships no JS compiler API, so unlike typeTest this has to spawn the + // CLI, and typeTest keeps using the typescript in test/node_modules). The check itself runs + // in the native binary, so this is as cheap under a debug build as under a release one and + // is not skipped there. Assertions for a .d.ts change go in fixture/*.ts, where this case + // and the typeTest cases check them; this is deliberately the only spawned compiler in the + // file (test/internal/source-lints/bun-types-test-spawn-sites.test.ts). + describe("tsc (the fixture's typescript dependency)", () => { + test("checks without lib.dom.d.ts", async () => { + // The tsconfig sets noEmit, so this leaves the shared fixture untouched. await using proc = Bun.spawn({ cmd: [bunExe(), join(BASE_FIXTURE_DIR, "node_modules", "typescript", "bin", "tsc"), "-p", "."], env: bunEnv, - cwd: checkDir, + cwd: BASE_FIXTURE_DIR, stdout: "pipe", stderr: "pipe", }); diff --git a/test/internal/source-lints/bun-types-test-spawn-sites.test.ts b/test/internal/source-lints/bun-types-test-spawn-sites.test.ts new file mode 100644 index 000000000000..dc957f380455 --- /dev/null +++ b/test/internal/source-lints/bun-types-test-spawn-sites.test.ts @@ -0,0 +1,29 @@ +import { expect, test } from "bun:test"; +import { readFileSync } from "node:fs"; +import path from "node:path"; + +// test/integration/bun-types/bun-types.test.ts type-checks the whole fixture directory +// (test/integration/bun-types/fixture/*.ts), so the assertions for a .d.ts change belong +// in the fixture, where every case checks them. #34573 instead added a case that spawned +// tsc over an inline snippet for one API, and that block became the template: within a +// month a couple of dozen open PRs had each added a copy for their own API, duplicating +// assertions already in the fixture and conflicting with each other. #39270 replaced it +// with a single case that spawns the fixture's own tsc over the whole fixture. Every one +// of those copies spawned a compiler, so holding the file at one spawn site is what keeps +// the template from coming back. +const lintedFile = "test/integration/bun-types/bun-types.test.ts"; + +test(`${lintedFile} spawns a compiler in exactly one place`, () => { + const source = readFileSync(path.resolve(import.meta.dir, "..", "..", "..", lintedFile), "utf8"); + + const spawnSites = source + .split("\n") + .flatMap((line, index) => + /\bBun\.spawn(Sync)?\(/.test(line) ? [`${lintedFile}:${index + 1}: ${line.trim()}`] : [], + ); + + expect( + spawnSites, + `${lintedFile} should spawn a compiler once, over the whole fixture. To cover a .d.ts change, add the assertions to test/integration/bun-types/fixture/*.ts instead of a tsc run of their own. Spawn sites:\n${spawnSites.join("\n")}`, + ).toHaveLength(1); +});