Repository navigation
[api] Static and callback module resolutions drop resolvedUsingTsExtension, raising TS2876聽#64630
Description
馃攷 Search Terms
createModuleResolver, moduleResolutions, resolveModuleName callback, StaticModuleResolution, resolvedUsingTsExtension, rewriteRelativeImportExtensions, TS2876
馃晽 Version & Regression Information
- This changed in commit or PR [api] Provide module resolution overrides聽#64299, which added module resolution overrides to the API. I reproduced it with
typescript@7.1.0-dev.20261004.1(built frommainat 50d70a3) andtypescript@7.1.0-dev.20261003.1, with the synchronous API on linux-x64.
馃捇 Code
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { API } from "typescript/unstable/sync";
const dir = join(process.cwd(), "proj");
rmSync(dir, { recursive: true, force: true });
mkdirSync(dir, { recursive: true });
writeFileSync(join(dir, "a.ts"), `import { b } from "./b.ts";\nexport const a = b;\n`);
writeFileSync(join(dir, "b.ts"), `export const b = 1;\n`);
const options = { module: 199, moduleResolution: 99, rewriteRelativeImportExtensions: true, strict: true, outDir: join(dir, "out") };
const rootFiles = [join(dir, "a.ts"), join(dir, "b.ts")];
const api = new API({ cwd: dir });
const errorsWith = (moduleResolver) => {
const snapshot = api.createSnapshot({ createPrograms: [{ rootFiles, compilerOptions: options, options: moduleResolver ? { moduleResolver } : {} }] });
const errors = snapshot.operation.createdPrograms[0].getSemanticDiagnostics().map((d) => `TS${d.code}: ${d.text}`);
snapshot.dispose();
return errors;
};
const plain = api.createModuleResolver(options);
const answer = plain.resolveModuleName("./b.ts", dir, 99).resolvedModule;
console.log("default answer resolvedUsingTsExtension:", answer.resolvedUsingTsExtension);
console.log("no resolver:", errorsWith(undefined));
const statics = api.createModuleResolver(options, {
moduleResolutions: { fallback: "resolve", entries: [{ moduleName: "./b.ts", containingDirectory: dir, result: answer }] },
});
console.log("static entry repeating it:", errorsWith(statics));
const callback = api.createModuleResolver(options, {
resolveModuleName: (name, containingDirectory, mode, { snapshot }) => plain.resolveModuleName(name, containingDirectory, mode, { snapshot }).resolvedModule,
});
console.log("callback passing it through:", errorsWith(callback));
api.close();馃檨 Actual behavior
A static entry and a callback that both return the default resolution of ./b.ts make the checker report TS2876 on the import:
default answer resolvedUsingTsExtension: true
no resolver: []
static entry repeating it: [
'TS2876: This relative import path is unsafe to rewrite because it looks like a file name, but actually resolves to "./b.ts".'
]
callback passing it through: [
'TS2876: This relative import path is unsafe to rewrite because it looks like a file name, but actually resolves to "./b.ts".'
]
馃檪 Expected behavior
A resolver that returns the default resolution gives the same result as no resolver, with no TS2876.
Additional information about the issue
Impact: a tool that answers module resolution itself, which #64299 added for resolving workspace packages from source, shows a false TS2876 on every ./x.ts import in a project that uses rewriteRelativeImportExtensions, where tsc reports none.
StaticModuleResolution carries resolvedFileName, originalPath and packageId, and staticModuleResolutionToResolvedModule builds the module.ResolvedModule from those alone, so ResolvedUsingTsExtension is always false for a static entry and for a callback result. Under rewriteRelativeImportExtensions, the checker then reports TS2876 on a ./x.ts import that such a resolver answers. The fix I propose adds an optional resolvedUsingTsExtension to StaticModuleResolution and copies it into the resolved module.
I met this while building deadset-ts, the TypeScript analyzer of deadset, on the TypeScript 7 API. A pull request with the fix and a test follows.
Activity