Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion packages/compiler/src/frontend/lowering/lower-classes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
);
}
Expand Down
1 change: 1 addition & 0 deletions packages/compiler/src/frontend/lowering/lower-exprs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
2 changes: 1 addition & 1 deletion packages/compiler/src/frontend/lowering/lower-stmts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> = new Set(["crypto"]);
const TOKEN_OPAQUE_GLOBALS: ReadonlySet<string> = new Set([]);
const CONSOLE_CALL_MEMBERS: ReadonlySet<string> = new Set(["log", "info", "debug", "error", "warn"]);

/** `const { subtle } = globalThis.crypto`, `const { Console } = console`,
Expand Down
27 changes: 24 additions & 3 deletions packages/compiler/src/frontend/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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<string, unknown>).paths as Record<string, string[]> | undefined;
const userBaseUrl = (options as Record<string, unknown>).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
Expand All @@ -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);
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down
67 changes: 66 additions & 1 deletion packages/compiler/src/frontend/resolve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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<string, string[]> | 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<string, string[]> | 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 {
Comment thread
3kaiu marked this conversation as resolved.
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;
Comment thread
3kaiu marked this conversation as resolved.
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
Expand Down
2 changes: 2 additions & 0 deletions packages/compiler/src/frontend/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
2 changes: 1 addition & 1 deletion packages/compiler/src/frontend/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down