import { describe, it, before, after } from "node:test"; import * as assert from "node:assert/strict"; import * as fs from "node:fs"; import * as os from "node:os"; import * as path from "node:path"; import { setTestSocket, stopTestDaemon, runCliJson, requireServer, } from "../helpers.ts"; const skip = requireServer("typescript-language-server"); async function pollUntil( fn: () => Promise, predicate: (v: T) => boolean, timeoutMs: number, intervalMs = 250, ): Promise { const deadline = Date.now() + timeoutMs; let last: T = await fn(); while (Date.now() < deadline) { if (predicate(last)) return last; await new Promise((r) => setTimeout(r, intervalMs)); last = await fn(); } return last; } interface DiagResult { [serverId: string]: { diagnostics?: { message: string }[] }; } describe("watcher: typescript handles derived file patterns", { skip: skip ?? undefined }, () => { let tmpDir: string; let mainFile: string; let helperFile: string; const env = { ...process.env }; let cleanup: () => void; before(async () => { delete env.NODE_OPTIONS; cleanup = setTestSocket(env); await stopTestDaemon(env); tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-lsp-ts-watch-")); fs.writeFileSync( path.join(tmpDir, ".pi-lsp.json"), JSON.stringify({ disable: ["oxlint"] }), ); fs.writeFileSync( path.join(tmpDir, "tsconfig.json"), JSON.stringify( { compilerOptions: { target: "ES2022", module: "ESNext", moduleResolution: "Bundler", strict: true, noEmit: true, }, include: ["*.ts"], }, null, 2, ), ); mainFile = path.join(tmpDir, "main.ts"); helperFile = path.join(tmpDir, "helper.ts"); fs.writeFileSync( mainFile, 'import { helper } from "./helper";\n\nconsole.log(helper());\n', ); fs.writeFileSync(helperFile, "export function helper(): number {\n return 1;\n}\n"); }); after(async () => { await stopTestDaemon(env); cleanup(); fs.rmSync(tmpDir, { recursive: true, force: true }); }); it("clears missing module after an unopened imported file is created", async () => { fs.rmSync(helperFile); const missing = (await runCliJson( [mainFile, "diagnostics", '{"timeoutMs":5000}'], env, )) as DiagResult; assert.ok( (missing["typescript-language-server"]?.diagnostics ?? []).some((d) => d.message.includes("Cannot find module './helper'") ), ); fs.writeFileSync(helperFile, "export function helper(): number {\n return 1;\n}\n"); const result = await pollUntil( async () => (await runCliJson( [mainFile, "diagnostics", '{"timeoutMs":3000}'], env, )) as DiagResult, (r) => (r["typescript-language-server"]?.diagnostics ?? []).length === 0, 15000, 500, ); assert.deepEqual(result["typescript-language-server"]?.diagnostics ?? [], []); }); it("reports missing module after an opened imported file is deleted", async () => { const initial = (await runCliJson( [mainFile, "diagnostics", '{"timeoutMs":5000}'], env, )) as DiagResult; assert.deepEqual(initial["typescript-language-server"]?.diagnostics ?? [], []); await runCliJson([helperFile, "diagnostics", '{"timeoutMs":5000}'], env); fs.rmSync(helperFile); const result = await pollUntil( async () => (await runCliJson( [mainFile, "diagnostics", '{"timeoutMs":3000}'], env, )) as DiagResult, (r) => { const diags = r["typescript-language-server"]?.diagnostics ?? []; return diags.some((d) => d.message.includes("Cannot find module './helper'")); }, 15000, 500, ); const diags = result["typescript-language-server"]?.diagnostics ?? []; assert.ok( diags.some((d) => d.message.includes("Cannot find module './helper'")), `Expected missing-module diagnostic after deleting opened helper.ts, got: ${JSON.stringify(diags)}`, ); }); });