Skip to content

[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 from main at 50d70a3) and typescript@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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions