From c48a432568df16dee4f80a52bb57dc7da83a7905 Mon Sep 17 00:00:00 2001 From: kai392 Date: Fri, 31 Jul 2026 17:08:48 +0800 Subject: [PATCH] fix(scripts): hash dynamic import() reach in turbo typecheck inputs Widen collectCrossBoundaryReach to match dynamic import like the dead-source checker, and hash packages/loopover-miner/package.json on //#typecheck. Closes #10046 Co-authored-by: Cursor --- scripts/check-turbo-typecheck-inputs.ts | 5 ++- .../unit/check-turbo-typecheck-inputs.test.ts | 40 +++++++++++++++++++ turbo.json | 1 + 3 files changed, 44 insertions(+), 2 deletions(-) diff --git a/scripts/check-turbo-typecheck-inputs.ts b/scripts/check-turbo-typecheck-inputs.ts index 0963c954fa..0b576cdf52 100644 --- a/scripts/check-turbo-typecheck-inputs.ts +++ b/scripts/check-turbo-typecheck-inputs.ts @@ -77,8 +77,9 @@ export function collectCrossBoundaryReach(root: string): CrossBoundaryReach[] { const reach = new Map(); for (const file of files) { const source = readFileSync(file, "utf8"); - // Both `from "..."` and bare `import "..."`, since a side-effect import is type-checked too. - for (const match of source.matchAll(/(?:from|import)\s+"((?:\.\.\/)+[^"]+)"/g)) { + // Static `from "..."`, bare `import "..."`, and dynamic `import("...")` (incl. multi-line) — side-effect + // and JSON imports are type-checked too. Mirror check-dead-source-files.ts's optional-paren form (#10046). + for (const match of source.matchAll(/(?:from|import)\s*\(?\s*"((?:\.\.\/)+[^"]+)"/g)) { const specifier = match[1]; if (!specifier) continue; const segments = /(?:^|\/)(packages|apps)\/([^/]+)\/([^/"]+)/.exec(specifier); diff --git a/test/unit/check-turbo-typecheck-inputs.test.ts b/test/unit/check-turbo-typecheck-inputs.test.ts index 5624617f2c..a0892002d1 100644 --- a/test/unit/check-turbo-typecheck-inputs.test.ts +++ b/test/unit/check-turbo-typecheck-inputs.test.ts @@ -1,4 +1,7 @@ import { describe, expect, it } from "vitest"; +import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; import { collectCrossBoundaryReach, coveredWorkspacesFromDependsOn, findUnhashedReach, parseJsonc } from "../../scripts/check-turbo-typecheck-inputs"; // turbo.json's //#typecheck inputs are a hand-maintained approximation of tsc's real transitive surface. Its @@ -52,6 +55,43 @@ describe("collectCrossBoundaryReach", () => { expect(paths).toContain("packages/loopover-mcp/lib"); }); + it("REGRESSION (#10046): dynamic import() reaches package.json the same as static from", () => { + // The old regex required whitespace after `import` before `"`, so `import("...")` never matched. + // miner-cli.test.ts uses a multi-line dynamic JSON import — pin both forms against a synthetic tree. + const root = mkdtempSync(join(tmpdir(), "turbo-inputs-10046-")); + try { + mkdirSync(join(root, "test", "unit"), { recursive: true }); + mkdirSync(join(root, "packages", "loopover-miner", "lib"), { recursive: true }); + writeFileSync(join(root, "packages", "loopover-miner", "package.json"), '{"name":"@loopover/miner"}\n'); + writeFileSync(join(root, "packages", "loopover-miner", "lib", "version.ts"), "export const v = 1;\n"); + writeFileSync( + join(root, "test", "unit", "dyn-import.test.ts"), + [ + 'it("dyn", async () => {', + " const packageJson = await import(", + ' "../../packages/loopover-miner/package.json",', + ' { with: { type: "json" } }', + ");", + "});", + "", + ].join("\n"), + ); + writeFileSync( + join(root, "test", "unit", "static-from.test.ts"), + 'import { v } from "../../packages/loopover-miner/lib/version";\nvoid v;\n', + ); + + const reached = collectCrossBoundaryReach(root); + const paths = reached.map((entry) => entry.path); + expect(paths).toContain("packages/loopover-miner/package.json"); + expect(paths).toContain("packages/loopover-miner/lib"); + const dyn = reached.find((entry) => entry.path === "packages/loopover-miner/package.json"); + expect(dyn?.importedBy.replace(/\\/g, "/")).toMatch(/test\/unit\/dyn-import\.test\.ts$/); + } finally { + rmSync(root, { recursive: true, force: true }); + } + }); + it("INVARIANT: this repo's own turbo.json covers its real reach", async () => { // The check the CI script performs, asserted here too so a `turbo.json` edit that drops a path fails in // the test suite as well as the standalone checker. diff --git a/turbo.json b/turbo.json index 0fabc4a174..87bc60f8e8 100644 --- a/turbo.json +++ b/turbo.json @@ -49,6 +49,7 @@ "package-lock.json", "packages/loopover-miner/lib/**", "packages/loopover-miner/bin/**", + "packages/loopover-miner/package.json", "packages/loopover-mcp/package.json", "packages/discovery-index/src/**", "packages/discovery-index/scripts/**",