diff --git a/packages/compiler/src/frontend/program.ts b/packages/compiler/src/frontend/program.ts index 4dbe00a2..76a4dc3e 100644 --- a/packages/compiler/src/frontend/program.ts +++ b/packages/compiler/src/frontend/program.ts @@ -40,7 +40,7 @@ * that path (no snapshot pins it). */ import { builtinModules } from "node:module"; -import { dirname, resolve } from "node:path"; +import { dirname, isAbsolute, join, resolve } from "node:path"; import * as ts from "./ts7/adapter.js"; import type { ScrDiagnostic } from "../diagnostics/diagnostic.js"; import { @@ -168,6 +168,36 @@ function adoptProjectConfig7( const value = parsed.options[key]; if (value !== undefined) adopted[key] = value; } + // `paths` is a real, well-supported tsgo checker option, but `baseUrl` + // itself is not — tsgo rejects it outright ("Option 'baseUrl' has been + // removed. Please remove it from your configuration. Use '"paths": {"*": + // ["./*"]}' instead."), so it never joins `adopted` on its own. The + // synthesized virtual tsconfig (ts7/program.ts) is also written BESIDE THE + // ENTRY FILE, not beside this real tsconfig.json, so relative `paths` + // targets can't simply pass through either: they're resolved to absolute + // paths here, against the real config's resolved `baseUrl` (tsgo's own + // parser already makes that absolute — hence the isAbsolute guard rather + // than a bare join) or, absent one, the config's own directory (tsc's + // default), or tsgo resolves them against the wrong base entirely. + const rawPaths = parsed.options["paths"]; + if (rawPaths !== undefined && typeof rawPaths === "object" && rawPaths !== null) { + const configDir = dirname(configFile); + const rawBaseUrl = parsed.options["baseUrl"]; + const base = + typeof rawBaseUrl !== "string" + ? configDir + : isAbsolute(rawBaseUrl) + ? rawBaseUrl + : join(configDir, rawBaseUrl); + const abs = (p: string): string => (isAbsolute(p) ? p : join(base, p)); + const paths: Record = {}; + for (const [key, value] of Object.entries(rawPaths as Record)) { + if (Array.isArray(value)) { + paths[key] = value.filter((v): v is string => typeof v === "string").map(abs); + } + } + adopted["paths"] = paths; + } const nullChecks = adopted["strictNullChecks"] ?? adopted["strict"] ?? false; if (nullChecks !== true) { diags.push(strictNullChecksFloorDiag(configFile)); diff --git a/packages/compiler/test/ts7/program.test.ts b/packages/compiler/test/ts7/program.test.ts index c44a3863..f4221d68 100644 --- a/packages/compiler/test/ts7/program.test.ts +++ b/packages/compiler/test/ts7/program.test.ts @@ -1,4 +1,4 @@ -import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { describe, expect, test } from "vitest"; @@ -67,3 +67,32 @@ console.log(required.value); rmSync(dir, { recursive: true, force: true }); } }); + +test("adopts tsconfig paths/baseUrl so tsgo resolves aliased imports", () => { + const tempRoot = process.platform === "win32" ? tmpdir() : "/tmp"; + const dir = mkdtempSync(join(tempRoot, "scriptc-preflight-paths-")); + writeFileSync( + join(dir, "tsconfig.json"), + JSON.stringify({ + compilerOptions: { strictNullChecks: true, baseUrl: ".", paths: { "@/*": ["./src/*"] } }, + }), + ); + const srcDir = join(dir, "src"); + mkdirSync(srcDir); + writeFileSync(join(dir, "entry.ts"), `import { value } from "@/dep";\nconsole.log(value);\n`); + writeFileSync(join(srcDir, "dep.ts"), "export const value = 1;\n"); + const entry = join(dir, "entry.ts"); + + const load = loadProgram(entry); + try { + // Before the fix, tsgo never learns about `paths`/`baseUrl` and reports + // SC0001 "Cannot find module '@/dep'". SC1010 (own resolver has no + // opinion on bare-specifier aliases outside npm/imports-field) is a + // separate, pre-existing limitation and is unaffected by this fix. + const codes = checkPreflight(load).map((diag) => diag.code); + expect(codes).not.toContain("SC0001"); + } finally { + load.dispose(); + rmSync(dir, { recursive: true, force: true }); + } +});