From dd26163c6a8956294c65b616e215cf0465118abf Mon Sep 17 00:00:00 2001 From: 3kaiu <3kaiu@users.noreply.github.com> Date: Wed, 29 Jul 2026 11:55:21 +0800 Subject: [PATCH 1/5] feat(compiler): support tsconfig paths aliases in resolver MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add tsconfig compilerOptions.paths support to scriptc's own module resolver (resolve.ts), used by the lowering pipeline. The TypeScript checker (tsgo) already resolves paths through the synthesized config; the lowering's import-edge resolution must agree. - shared.ts: add "paths" to ADOPTED_OPTIONS so adoptProjectConfig7 extracts it from the project tsconfig. - resolve.ts: add setTsconfigPaths/resolvePathsAlias — wildcard-aware path lookup that delegates to the existing relative-module resolver. - program.ts: call setTsconfigPaths in loadProgram7; try resolvePathsAlias in resolveImport7, orderedImportsOf, and resolveEdge before standard resolution strategies. All existing tests pass (251 passed, 7 skipped, 12 files). --- packages/compiler/src/frontend/program.ts | 24 ++++++++-- packages/compiler/src/frontend/resolve.ts | 58 +++++++++++++++++++++++ packages/compiler/src/frontend/shared.ts | 1 + 3 files changed, 80 insertions(+), 3 deletions(-) diff --git a/packages/compiler/src/frontend/program.ts b/packages/compiler/src/frontend/program.ts index aa1145b1f..e15f24b8e 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,10 @@ 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. + const userPaths = (options as Record).paths as Record | undefined; + setTsconfigPaths(userPaths ?? null); // --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 +231,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 +1118,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 +2120,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 +2368,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 a5114e2cc..4cc8519f7 100644 --- a/packages/compiler/src/frontend/resolve.ts +++ b/packages/compiler/src/frontend/resolve.ts @@ -806,11 +806,69 @@ 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; + +/** Set the active tsconfig "paths" mapping (called by loadProgram). */ +export function setTsconfigPaths(paths: Record | null): void { + tsconfigPaths = paths; +} + +/** Resolve an import specifier through tsconfig "paths" — `@/foo → ./src/foo` + * (with `*` wildcard substitution) — then through the normal relative-module + * resolver. Returns the resolved absolute path, or null. */ +export function resolvePathsAlias(fromFile: string, specifier: string): string | null { + if (tsconfigPaths === null) return null; + 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 target = mapping.startsWith("./") || mapping.startsWith("../") ? mapping : "./" + mapping; + const r = resolveRelativeModule(fromFile, target); + 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 r = resolveRelativeModule(fromFile, mapping); + if (r !== null) return r; + continue; + } + const target = mapping.slice(0, mStarIdx) + wildcard + mapping.slice(mStarIdx + 1); + const rel = target.startsWith("./") || target.startsWith("../") ? target : "./" + target; + const r = resolveRelativeModule(fromFile, rel); + 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; } /** 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 8bd5deea3..6a883149e 100644 --- a/packages/compiler/src/frontend/shared.ts +++ b/packages/compiler/src/frontend/shared.ts @@ -252,6 +252,7 @@ export const ADOPTED_OPTIONS = [ // SC1012 "not supported yet" beats a raw TS1259 at the same site. "esModuleInterop", "allowSyntheticDefaultImports", + "paths", ] as const; /* The JAVASCRIPT strictness stance (the JS-input design made real): From 7e653e5a79ea42fef8a955be64c63053c3691e6d Mon Sep 17 00:00:00 2001 From: 3kaiu <3kaiu@users.noreply.github.com> Date: Wed, 29 Jul 2026 13:48:56 +0800 Subject: [PATCH 2/5] feat(compiler): support new URL(url, base) with string literals Support two-argument URL constructor when both arguments are compile-time string literals. Resolves at compile time using Node's URL class and emits a single url.new libcall with the resolved href. Non-literal arguments keep the existing fence with an improved error message suggesting --dynamic as a workaround. 251 passed, 7 skipped (12 files). --- .../src/frontend/lowering/lower-classes.ts | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index 7536ccaa3..2556be608 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, ); } From 8db62f3ab8c59ee08ef94e02cb0d78e856c901a1 Mon Sep 17 00:00:00 2001 From: 3kaiu <3kaiu@users.noreply.github.com> Date: Wed, 29 Jul 2026 14:28:57 +0800 Subject: [PATCH 3/5] feat(compiler): improve global crypto diagnostic and remove opaque token - Add 'crypto' to globalHints with a clear migration hint suggesting import from 'node:crypto' instead of using the global Web Crypto API. - Remove 'crypto' from TOKEN_OPAQUE_GLOBALS (no other globals use this set now), so destructuring crypto members bypasses the opaque token path and receives the standard global fence instead. 251 passed, 7 skipped (12 files). --- packages/compiler/src/frontend/lowering/lower-exprs.ts | 1 + packages/compiler/src/frontend/lowering/lower-stmts.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts index d0ab06c4c..f1908cb80 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 354c67c4a..885399873 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`, From f447121325a19302b6b72d31f159e29f7dd9f957 Mon Sep 17 00:00:00 2001 From: 3kaiu <3kaiu@users.noreply.github.com> Date: Wed, 29 Jul 2026 14:30:29 +0800 Subject: [PATCH 4/5] fix: resolve paths aliases relative to baseUrl, not importer Mapping targets in tsconfig paths are relative to baseUrl (or the tsconfig directory), not the importing file. Fix the resolution by passing the config's baseDir to resolvePathsAlias and resolving targets against that instead of fromFile. --- packages/compiler/src/frontend/program.ts | 5 ++++- packages/compiler/src/frontend/resolve.ts | 25 +++++++++++++++-------- packages/compiler/src/frontend/shared.ts | 1 + packages/compiler/src/frontend/types.ts | 2 +- 4 files changed, 22 insertions(+), 11 deletions(-) diff --git a/packages/compiler/src/frontend/program.ts b/packages/compiler/src/frontend/program.ts index e15f24b8e..3a06ce27d 100644 --- a/packages/compiler/src/frontend/program.ts +++ b/packages/compiler/src/frontend/program.ts @@ -220,8 +220,11 @@ function loadProgram7(host: ts.Ts7Host, entryPath: string): LoadResult & { dispo 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; - setTsconfigPaths(userPaths ?? null); + 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 diff --git a/packages/compiler/src/frontend/resolve.ts b/packages/compiler/src/frontend/resolve.ts index 4cc8519f7..b64320d46 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"; @@ -818,17 +818,22 @@ export function resolveTypeDirective(name: string, fromFile: string): string | n * loadProgram calls them). clearResolveCaches reset included. */ let tsconfigPaths: Record | null = null; +let tsconfigBasePath: string | null = null; -/** Set the active tsconfig "paths" mapping (called by loadProgram). */ -export function setTsconfigPaths(paths: Record | null): void { +/** 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. Returns the resolved absolute path, or null. */ + * 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("*"); @@ -836,8 +841,8 @@ export function resolvePathsAlias(fromFile: string, specifier: string): string | if (specifier !== pattern) continue; for (const mapping of mappings) { if (typeof mapping !== "string") continue; - const target = mapping.startsWith("./") || mapping.startsWith("../") ? mapping : "./" + mapping; - const r = resolveRelativeModule(fromFile, target); + const target = mapping.startsWith("./") || mapping.startsWith("../") ? join(base, mapping) : mapping; + const r = resolveRelativeModule(dirname(target), "./" + basename(target)); if (r !== null) return r; } continue; @@ -850,13 +855,14 @@ export function resolvePathsAlias(fromFile: string, specifier: string): string | if (typeof mapping !== "string") continue; const mStarIdx = mapping.indexOf("*"); if (mStarIdx === -1) { - const r = resolveRelativeModule(fromFile, mapping); + const target = mapping.startsWith("./") || mapping.startsWith("../") ? join(base, mapping) : mapping; + const r = resolveRelativeModule(dirname(target), "./" + basename(target)); if (r !== null) return r; continue; } const target = mapping.slice(0, mStarIdx) + wildcard + mapping.slice(mStarIdx + 1); - const rel = target.startsWith("./") || target.startsWith("../") ? target : "./" + target; - const r = resolveRelativeModule(fromFile, rel); + const abs = target.startsWith("./") || target.startsWith("../") ? join(base, target) : target; + const r = resolveRelativeModule(dirname(abs), "./" + basename(abs)); if (r !== null) return r; } } @@ -869,6 +875,7 @@ 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 6a883149e..5aa20567e 100644 --- a/packages/compiler/src/frontend/shared.ts +++ b/packages/compiler/src/frontend/shared.ts @@ -253,6 +253,7 @@ export const ADOPTED_OPTIONS = [ "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 51a2ec867..f56c02e6d 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 From 28e4c350b1639485a38b663de25049ceedf0c4a1 Mon Sep 17 00:00:00 2001 From: 3kaiu <3kaiu@users.noreply.github.com> Date: Wed, 29 Jul 2026 15:16:27 +0800 Subject: [PATCH 5/5] fix: resolve paths aliases relative to baseUrl, not importer --- packages/compiler/src/frontend/resolve.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/compiler/src/frontend/resolve.ts b/packages/compiler/src/frontend/resolve.ts index b64320d46..3f895c8f7 100644 --- a/packages/compiler/src/frontend/resolve.ts +++ b/packages/compiler/src/frontend/resolve.ts @@ -841,8 +841,8 @@ export function resolvePathsAlias(fromFile: string, specifier: string): string | if (specifier !== pattern) continue; for (const mapping of mappings) { if (typeof mapping !== "string") continue; - const target = mapping.startsWith("./") || mapping.startsWith("../") ? join(base, mapping) : mapping; - const r = resolveRelativeModule(dirname(target), "./" + basename(target)); + const abs = resolve(base, mapping); + const r = resolveRelativeModule(abs, "./" + basename(abs)); if (r !== null) return r; } continue; @@ -855,14 +855,14 @@ export function resolvePathsAlias(fromFile: string, specifier: string): string | if (typeof mapping !== "string") continue; const mStarIdx = mapping.indexOf("*"); if (mStarIdx === -1) { - const target = mapping.startsWith("./") || mapping.startsWith("../") ? join(base, mapping) : mapping; - const r = resolveRelativeModule(dirname(target), "./" + basename(target)); + 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 = target.startsWith("./") || target.startsWith("../") ? join(base, target) : target; - const r = resolveRelativeModule(dirname(abs), "./" + basename(abs)); + const abs = resolve(base, target); + const r = resolveRelativeModule(abs, "./" + basename(abs)); if (r !== null) return r; } }