diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index 7536ccaa..2556be60 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -5004,11 +5004,30 @@ export function lowerNew(L: Lowerer, expr: ts.NewExpression): IrExpr { } if (symbol && symbol.name === "URL" && L.isStdlibSymbol(symbol)) { const args = expr.arguments ?? []; + if (args.length === 2) { + const urlExpr = L.lowerExpr(args[0]!); + const baseExpr = L.lowerExpr(args[1]!); + if (urlExpr.kind === "strLit" && baseExpr.kind === "strLit") { + try { + const resolved = new URL(urlExpr.value, baseExpr.value).href; + return { kind: "libCall", fn: "url.new", args: [{ kind: "strLit", value: resolved, type: STRING, loc }], type: URL_T, loc }; + } catch { + L.noLowering("new URL with an unresolvable base URL", expr, "the base argument must be a valid absolute URL"); + return { kind: "libCall", fn: "url.new", args: [L.lowerExprExpecting(args[0]!, STRING)], type: URL_T, loc }; + } + } + L.noLowering( + "new URL with a non-literal argument", + expr, + "compile-time string literals for both url and base are required; resolve relative inputs against a base yourself, or use --dynamic for runtime URL resolution", + ); + return { kind: "libCall", fn: "url.new", args: [L.lowerExprExpecting(args[0]!, STRING)], type: URL_T, loc }; + } if (args.length !== 1) { L.noLowering( `new URL with ${args.length} argument${args.length === 1 ? "" : "s"}`, expr, - "one absolute-URL string is the supported form (resolve relative inputs against a base yourself)", + "one absolute-URL string or two string literals (url + base) are the supported forms", symbol, ); } diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts index d0ab06c4..f1908cb8 100644 --- a/packages/compiler/src/frontend/lowering/lower-exprs.ts +++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts @@ -923,6 +923,7 @@ function lowerExprInner(L: Lowerer, expr: ts.Expression): IrExpr { WeakRef: "deref()-after-collect exposes GC timing — genuinely dynamic; hold a strong reference instead", FinalizationRegistry: "finalization callbacks expose GC timing — genuinely dynamic; release resources explicitly instead", eval: "runtime code evaluation cannot be compiled ahead of time", + crypto: "the Web Crypto API (globalThis.crypto) has no static lowering; import named exports from 'node:crypto' instead — e.g. `import { randomUUID } from \"node:crypto\"`", }; L.noLowering(expr.text, expr, globalHints[expr.text], sym ?? undefined); } diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index 354c67c4..88539987 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -1825,7 +1825,7 @@ export function lowerStmt(L: Lowerer, stmt: ts.Statement): IrStmt | IrStmt[] | n * the one split surface: its five CALL members fence by name, while * the rest (`Console` — the suite's constructor-identity probe) have * no surface to lose and bind tokens. */ - const TOKEN_OPAQUE_GLOBALS: ReadonlySet = new Set(["crypto"]); + const TOKEN_OPAQUE_GLOBALS: ReadonlySet = new Set([]); const CONSOLE_CALL_MEMBERS: ReadonlySet = new Set(["log", "info", "debug", "error", "warn"]); /** `const { subtle } = globalThis.crypto`, `const { Console } = console`, diff --git a/packages/compiler/src/frontend/program.ts b/packages/compiler/src/frontend/program.ts index aa1145b1..3a06ce27 100644 --- a/packages/compiler/src/frontend/program.ts +++ b/packages/compiler/src/frontend/program.ts @@ -49,7 +49,7 @@ import { tscPassthroughDiag, unsupportedDiag, } from "../diagnostics/diagnostic.js"; -import { isNodeModulesPath, nearestPkgJsonPath, projectDtsRuntimeSibling, resolveBareModule, resolveProjectImport, resolveRelativeModule, resolveTypeDirective, setProjectRealm } from "./resolve.js"; +import { isNodeModulesPath, nearestPkgJsonPath, projectDtsRuntimeSibling, resolveBareModule, resolvePathsAlias, resolveProjectImport, resolveRelativeModule, resolveTypeDirective, setProjectRealm, setTsconfigPaths } from "./resolve.js"; import { probeNodeImportRefusal, probeNodeRequireRefusal } from "./npm.js"; import { isNpmStaticPackage, npmStaticActive, npmStaticFsShadow, npmStaticPackageOfPath, reportNpmStaticOffender, setNpmStaticPackages } from "./npm-static.js"; import { provenanceEntryFor, provenancePaths } from "./provenance-registry.js"; @@ -218,6 +218,13 @@ function loadProgram7(host: ts.Ts7Host, entryPath: string): LoadResult & { dispo // .d.ts-internal errors. Fence discipline never depended on it: the // lowerer checks provenance and forms at every use site. let options: ts.Ts7CompilerOptions = nodeTypes ? { ...config.options, skipLibCheck: true } : { ...config.options }; + // Adopt tsconfig "paths" for scriptc's own resolver (resolve.ts) — + // lowering uses them to resolve aliased import specifiers. + // Mapping targets are relative to baseUrl (or the tsconfig directory). + const userPaths = (options as Record).paths as Record | undefined; + const userBaseUrl = (options as Record).baseUrl as string | undefined; + const pathsBaseDir = userBaseUrl !== undefined ? resolve(dirname(config.configFile ?? entryPath), userBaseUrl) : config.configFile !== null ? dirname(config.configFile) : undefined; + setTsconfigPaths(userPaths ?? null, pathsBaseDir); // --npm-static: opted-in packages' shipped JS must be TYPE-INCLUDED (not // just resolved) — without maxNodeModuleJsDepth, node_modules JS types as // an implicit-any module (TS7016) and nothing infers. Only flagged @@ -227,8 +234,10 @@ function loadProgram7(host: ts.Ts7Host, entryPath: string): LoadResult & { dispo // so tsgo's OWN resolution of the bare specifiers lands on the same // source files the preflight resolver answers — the checker types the // driver against the package's real TypeScript, not its shipped .d.ts. - const paths = provenancePaths(); - if (paths !== null) options = { ...options, paths }; + const provenance = provenancePaths(); + if (provenance !== null) { + options = userPaths !== null ? { ...options, paths: { ...userPaths, ...provenance } } : { ...options, paths: provenance }; + } const coreRoots = [entryPath, ambientDtsPath(), nodeTypes ?? fallbackDtsPath()]; const program = ts.createProgram([...coreRoots, overridesDtsPath()], options, host); const entry = program.getSourceFile(entryPath); @@ -1112,6 +1121,9 @@ export function makeCycleAdmission( * resolveImport) for the lowering: CommonJS require statements lower to * guarded %init calls of exactly the module preflight resolved here. */ function resolveImport7(program: ts.Program, from: ts.SourceFile, specifier: string): ts.SourceFile | null { + // tsconfig "paths" aliases take precedence over relative and bare resolution. + const pathsResolved = resolvePathsAlias(from.fileName, specifier); + if (pathsResolved !== null) return program.getSourceFile(pathsResolved) ?? null; const resolved = resolveRelativeModule(from.fileName, specifier); if (resolved === null) return null; return program.getSourceFile(resolved) ?? null; @@ -2111,6 +2123,8 @@ function cjsNamedImportLinkCheck( // --npm-static packages, whose CJS entries face Node's lexer exactly // like program CJS files (their JS IS the program now). const resolveEdge = (from: ts.SourceFile, spec: string): ts.SourceFile | null => { + const pathsResolved = resolvePathsAlias(from.fileName, spec); + if (pathsResolved !== null) return program.getSourceFile(pathsResolved) ?? null; if (isRelative(spec)) return resolveImport7(program, from, spec); const npmStatic = npmStaticDepSf7(program, from, spec); if (npmStatic !== null) return npmStatic; @@ -2357,6 +2371,13 @@ export function orderedImportsOf( if (ts.isImportDeclaration(stmt) && erasedTypeOnlyImport(stmt)) continue; if (!stmt.moduleSpecifier || !ts.isStringLiteral(stmt.moduleSpecifier)) continue; const spec = stmt.moduleSpecifier.text; + // tsconfig "paths" aliases take precedence over all resolution strategies. + const pathsAlias = resolvePathsAlias(sf.fileName, spec); + if (pathsAlias !== null) { + const dep = program.getSourceFile(pathsAlias) ?? null; + out.push({ stmt, dep }); + continue; + } const isRelative = isRelativeSpecifier(spec); // Relative edges as ever; PROJECT imports (#alias/self-name — the // package.json-mediated specifiers preflight admits as user-module diff --git a/packages/compiler/src/frontend/resolve.ts b/packages/compiler/src/frontend/resolve.ts index a5114e2c..3f895c8f 100644 --- a/packages/compiler/src/frontend/resolve.ts +++ b/packages/compiler/src/frontend/resolve.ts @@ -15,7 +15,7 @@ * and these tables are wrong — that is what the suite is for. */ import { existsSync, readFileSync, readdirSync, realpathSync, statSync } from "node:fs"; -import { dirname, isAbsolute, join, resolve } from "node:path"; +import { basename, dirname, isAbsolute, join, resolve } from "node:path"; import { isNpmStaticPackage, npmStaticPackageOfPath, npmStaticTransformPkgJson } from "./npm-static.js"; import { provenanceEntryFor } from "./provenance-registry.js"; @@ -806,11 +806,76 @@ export function resolveTypeDirective(name: string, fromFile: string): string | n } } +/* ── tsconfig "paths" alias resolution ───────────────────────────────────── + * + * TypeScript's compilerOptions.paths maps import specifier patterns to file + * locations (most commonly `@/*` → `"./src/*"`). scriptc's own resolver + * (used by the lowering) normally never sees these — tsgo's checker resolves + * its own. The lowering's resolveImport7 / orderedImportsOf MUST apply the + * same mapping so that import sources line up. + * + * Setters below are one-way (once set they stay set for the load's lifetime; + * loadProgram calls them). clearResolveCaches reset included. */ + +let tsconfigPaths: Record | null = null; +let tsconfigBasePath: string | null = null; + +/** Set the active tsconfig "paths" mapping and base directory + * (baseUrl from tsconfig, or the tsconfig's directory). Called by loadProgram. */ +export function setTsconfigPaths(paths: Record | null, baseDir?: string): void { + tsconfigPaths = paths; + tsconfigBasePath = baseDir ?? null; +} + +/** Resolve an import specifier through tsconfig "paths" — `@/foo → ./src/foo` + * (with `*` wildcard substitution) — then through the normal relative-module + * resolver. Mapping targets are resolved relative to the tsconfig base directory + * (baseUrl), not the importing file. Returns the resolved absolute path, or null. */ +export function resolvePathsAlias(fromFile: string, specifier: string): string | null { + if (tsconfigPaths === null) return null; + const base = resolve(tsconfigBasePath ?? dirname(fromFile)); + for (const [pattern, mappings] of Object.entries(tsconfigPaths)) { + if (!Array.isArray(mappings)) continue; + const starIdx = pattern.indexOf("*"); + if (starIdx === -1) { + if (specifier !== pattern) continue; + for (const mapping of mappings) { + if (typeof mapping !== "string") continue; + const abs = resolve(base, mapping); + const r = resolveRelativeModule(abs, "./" + basename(abs)); + if (r !== null) return r; + } + continue; + } + const prefix = pattern.slice(0, starIdx); + const suffix = pattern.slice(starIdx + 1); + if (!specifier.startsWith(prefix) || !specifier.endsWith(suffix)) continue; + const wildcard = specifier.slice(prefix.length, specifier.length - suffix.length); + for (const mapping of mappings) { + if (typeof mapping !== "string") continue; + const mStarIdx = mapping.indexOf("*"); + if (mStarIdx === -1) { + const abs = resolve(base, mapping); + const r = resolveRelativeModule(abs, "./" + basename(abs)); + if (r !== null) return r; + continue; + } + const target = mapping.slice(0, mStarIdx) + wildcard + mapping.slice(mStarIdx + 1); + const abs = resolve(base, target); + const r = resolveRelativeModule(abs, "./" + basename(abs)); + if (r !== null) return r; + } + } + return null; +} + /** Test hook: the package.json cache holds across programs (fine within one * compile; a long-lived test process editing fixtures must reset it). */ export function clearResolveCaches(): void { pkgJsonCache.clear(); workspaceMembersCache.clear(); + tsconfigPaths = null; + tsconfigBasePath = null; } /** True when `path` is under a node_modules directory (the diff --git a/packages/compiler/src/frontend/shared.ts b/packages/compiler/src/frontend/shared.ts index 8bd5deea..5aa20567 100644 --- a/packages/compiler/src/frontend/shared.ts +++ b/packages/compiler/src/frontend/shared.ts @@ -252,6 +252,8 @@ export const ADOPTED_OPTIONS = [ // SC1012 "not supported yet" beats a raw TS1259 at the same site. "esModuleInterop", "allowSyntheticDefaultImports", + "paths", + "baseUrl", ] as const; /* The JAVASCRIPT strictness stance (the JS-input design made real): diff --git a/packages/compiler/src/frontend/types.ts b/packages/compiler/src/frontend/types.ts index 51a2ec86..f56c02e6 100644 --- a/packages/compiler/src/frontend/types.ts +++ b/packages/compiler/src/frontend/types.ts @@ -15,7 +15,7 @@ export { typeKey }; * signal), so under --dynamic they map to island handles (jsval) exactly * like npm-declared types. Checked with declaration provenance in mapType; * consumed by the lowerer's badType for the static-build wording. */ -export const ISLAND_AMBIENT_TYPES = ["Response", "RequestInit", "AbortSignal", "Headers"] as const; +export const ISLAND_AMBIENT_TYPES = ["Response", "RequestInit", "AbortSignal", "Headers", "HeadersInit"] as const; /** The frontend's record-shape interner. Records are monomorphic structural * shapes: fields sorted by name form the canonical identity, and two types