Skip to content
Open
13 changes: 13 additions & 0 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3794,6 +3794,19 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
// are ESM, there cannot be a synthetic default.
return false;
}
// For other files (not node16/nodenext with impliedNodeFormat), check if we can determine
// the module format from project references
if (!targetMode && file.isDeclarationFile) {
const redirect = host.getRedirectFromSourceFile(file.path);
if (redirect) {
Copy link

Copilot AI Jan 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For declaration files from project references, consider using getRedirectFromOutput instead of getRedirectFromSourceFile. The function getRedirectFromSourceFile maps source file paths to project references, while getRedirectFromOutput maps output file paths to project references. Since this code block handles declaration files (outputs), using getRedirectFromOutput(file.path)?.resolvedRef would be more semantically correct and consistent with the pattern in getRedirectReferenceForResolution (program.ts:2112).

Copilot uses AI. Check for mistakes.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot investigate whether this suggestion is correct, and apply the other suggestion above (#63038 (comment))

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Investigated and found that getRedirectFromOutput alone doesn't work for all test cases. Using the pattern from getRedirectReferenceForResolution, I now try both getRedirectFromSourceFile || getRedirectFromOutput since declaration files can be mapped either way depending on resolution context. This ensures the check works regardless of how the file was resolved. (commit d7ac3ad)

// This is a declaration file from a project reference, so we can determine
// its module format from the referenced project's options
const targetModuleKind = host.getEmitModuleFormatOfFile(file);
if (usageMode === ModuleKind.ESNext && targetModuleKind >= ModuleKind.ES2015) {
Copy link

Copilot AI Jan 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The condition should use a range check to only include ES module kinds (ES2015, ES2020, ES2022, ESNext) and explicitly exclude Node16+ and Preserve. The current check targetModuleKind >= ModuleKind.ES2015 would incorrectly include Node16 (100), Node18 (101), Node20 (102), NodeNext (199), and Preserve (200).

The condition should be changed to:

if (usageMode === ModuleKind.ESNext && ModuleKind.ES2015 <= targetModuleKind && targetModuleKind <= ModuleKind.ESNext)

This pattern is used elsewhere in the codebase (e.g., line 48656 of checker.ts) to specifically check for ES module kinds.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed the condition to use the proper range check: ModuleKind.ES2015 <= targetModuleKind && targetModuleKind <= ModuleKind.ESNext. This excludes Node16+ (100-199) and Preserve (200) which have higher numeric values but aren't pure ES modules. (commit d7ac3ad)

return false;
}
}
}
}
if (!allowSyntheticDefaultImports) {
return false;
Expand Down
29 changes: 29 additions & 0 deletions src/testRunner/unittests/tsc/projectReferences.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,35 @@ describe("unittests:: tsc:: projectReferences::", () => {
commandLineArgs: ["--p", "app", "--pretty", "false"],
});

verifyTsc({
scenario: "projectReferences",
subScenario: "referenced project with esnext module disallows synthetic default imports",
sys: () =>
TestServerHost.createWatchedSystem({
"/home/src/workspaces/project/lib/tsconfig.json": jsonToReadableText({
compilerOptions: {
composite: true,
declaration: true,
module: "esnext",
moduleResolution: "bundler",
},
}),
"/home/src/workspaces/project/lib/utils.ts": "export const test = () => 'test';",
"/home/src/workspaces/project/lib/utils.d.ts": "export declare const test: () => string;",
"/home/src/workspaces/project/app/tsconfig.json": jsonToReadableText({
compilerOptions: {
module: "esnext",
moduleResolution: "bundler",
},
references: [
{ path: "../lib" },
],
}),
"/home/src/workspaces/project/app/index.ts": `import Test from '../lib/utils';\nconsole.log(Test.test());`,
}),
commandLineArgs: ["--p", "app", "--pretty", "false"],
});

verifyTsc({
scenario: "projectReferences",
subScenario: "referencing ambient const enum from referenced project with preserveConstEnums",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ declare const console: { log(msg: any): void; };
Output::
app/src/index.ts(2,28): error TS2613: Module '"/home/src/workspaces/project/app/src/local"' has no default export. Did you mean to use 'import { local } from "/home/src/workspaces/project/app/src/local"' instead?
app/src/index.ts(3,28): error TS2613: Module '"/home/src/workspaces/project/node_modules/esm-package/index"' has no default export. Did you mean to use 'import { esm } from "/home/src/workspaces/project/node_modules/esm-package/index"' instead?
app/src/index.ts(4,28): error TS1192: Module '"/home/src/workspaces/project/lib/dist/a"' has no default export.
app/src/index.ts(5,28): error TS1192: Module '"/home/src/workspaces/project/lib/dist/a"' has no default export.


//// [/home/src/workspaces/project/app/dist/local.js]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
currentDirectory:: /home/src/workspaces/project useCaseSensitiveFileNames:: false
Input::
//// [/home/src/workspaces/project/lib/tsconfig.json]
{
"compilerOptions": {
"composite": true,
"declaration": true,
"module": "esnext",
"moduleResolution": "bundler"
}
}

//// [/home/src/workspaces/project/lib/utils.ts]
export const test = () => 'test';

//// [/home/src/workspaces/project/lib/utils.d.ts]
export declare const test: () => string;

//// [/home/src/workspaces/project/app/tsconfig.json]
{
"compilerOptions": {
"module": "esnext",
"moduleResolution": "bundler"
},
"references": [
{
"path": "../lib"
}
]
}

//// [/home/src/workspaces/project/app/index.ts]
import Test from '../lib/utils';
console.log(Test.test());

//// [/home/src/tslibs/TS/Lib/lib.d.ts]
interface Boolean {}
interface Function {}
interface CallableFunction {}
interface NewableFunction {}
interface IArguments {}
interface Number { toExponential: any; }
interface Object {}
interface RegExp {}
interface String { charAt: any; }
interface Array<T> { length: number; [n: number]: T; }
interface ReadonlyArray<T> {}
declare const console: { log(msg: any): void; };


/home/src/tslibs/TS/Lib/tsc.js --p app --pretty false
Output::
app/index.ts(1,8): error TS1192: Module '"/home/src/workspaces/project/lib/utils"' has no default export.


//// [/home/src/workspaces/project/app/index.js]
import Test from '../lib/utils';
console.log(Test.test());



exitCode:: ExitStatus.DiagnosticsPresent_OutputsGenerated
Loading