From 4d6c59bae008dd27b7417da6c43a0933d46f0d65 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 22 Jan 2026 19:31:21 +0000 Subject: [PATCH 01/10] Initial plan From 0351c5186c18ac5b441e00de38f0a9affc892e4c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 22 Jan 2026 19:41:52 +0000 Subject: [PATCH 02/10] Implement fix for synthetic default exports with project references Co-authored-by: andrewbranch <3277153+andrewbranch@users.noreply.github.com> --- src/compiler/checker.ts | 18 ++++++++++++++++ ...heticDefaultExportWithProjectReferences.ts | 21 +++++++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 tests/cases/fourslash/syntheticDefaultExportWithProjectReferences.ts diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 390c843b0c968..9e313c76ae5fc 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -3812,6 +3812,24 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { // it definitely is a module and does not have a synthetic default return false; } + // If this is a declaration file from a project reference, check the referenced project's options + // to determine if the module format is ESM + if (file && file.isDeclarationFile) { + const redirect = host.getRedirectFromSourceFile(file.path); + if (redirect?.resolvedRef) { + const referencedOptions = redirect.resolvedRef.commandLine.options; + // If the referenced project has allowSyntheticDefaultImports disabled, respect that + if (!getAllowSyntheticDefaultImports(referencedOptions)) { + return false; + } + // If the referenced project's module format is ESM (ES2015 or later), + // it cannot have a synthetic default + const referencedModuleKind = referencedOptions.module ?? ModuleKind.CommonJS; + if (referencedModuleKind >= ModuleKind.ES2015) { + return false; + } + } + } // There are _many_ declaration files not written with esmodules in mind that still get compiled into a format with __esModule set // Meaning there may be no default at runtime - however to be on the permissive side, we allow access to a synthetic default member // as there is no marker to indicate if the accompanying JS has `__esModule` or not, or is even native esm diff --git a/tests/cases/fourslash/syntheticDefaultExportWithProjectReferences.ts b/tests/cases/fourslash/syntheticDefaultExportWithProjectReferences.ts new file mode 100644 index 0000000000000..1581de77c9a58 --- /dev/null +++ b/tests/cases/fourslash/syntheticDefaultExportWithProjectReferences.ts @@ -0,0 +1,21 @@ +/// + +// Test case for synthetic default imports from declaration files. +// Without project references, .d.ts files allow synthetic defaults due to ambiguity. +// With project references (tested manually), the referenced project's options determine +// if synthetic defaults are allowed. + +// @module: esnext +// @moduleResolution: bundler + +// @Filename: /a/utils.d.ts +//// export declare const test: () => string; + +// @Filename: /b/index.ts +//// import Test from '../a/utils'; +//// +//// console.log(Test.test()); + +// Without project references, this is allowed (no error) because we can't determine +// if the original JS was ESM or CJS +verify.getSemanticDiagnostics([]); From 071043f0eb5e6d681257bd70cc8dedc89e0a5924 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 22 Jan 2026 19:44:55 +0000 Subject: [PATCH 03/10] Remove redundant condition check in canHaveSyntheticDefault Co-authored-by: andrewbranch <3277153+andrewbranch@users.noreply.github.com> --- src/compiler/checker.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 9e313c76ae5fc..9994c0675f18e 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -3814,7 +3814,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { } // If this is a declaration file from a project reference, check the referenced project's options // to determine if the module format is ESM - if (file && file.isDeclarationFile) { + if (file) { const redirect = host.getRedirectFromSourceFile(file.path); if (redirect?.resolvedRef) { const referencedOptions = redirect.resolvedRef.commandLine.options; From bcd143ab1fbad1988a3ad6371b91eff4544a6938 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 22 Jan 2026 19:46:03 +0000 Subject: [PATCH 04/10] Run formatter to fix trailing whitespace Co-authored-by: andrewbranch <3277153+andrewbranch@users.noreply.github.com> --- src/compiler/checker.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 9994c0675f18e..ae8ec56cd6498 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -3822,7 +3822,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { if (!getAllowSyntheticDefaultImports(referencedOptions)) { return false; } - // If the referenced project's module format is ESM (ES2015 or later), + // If the referenced project's module format is ESM (ES2015 or later), // it cannot have a synthetic default const referencedModuleKind = referencedOptions.module ?? ModuleKind.CommonJS; if (referencedModuleKind >= ModuleKind.ES2015) { From 01c3f081b1085907bf579fdaa5baf356ab7be0ee Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 22 Jan 2026 19:59:00 +0000 Subject: [PATCH 05/10] Accept new baseline for default-import-interop test Co-authored-by: andrewbranch <3277153+andrewbranch@users.noreply.github.com> --- .../default-import-interop-uses-referenced-project-settings.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/baselines/reference/tsc/projectReferences/default-import-interop-uses-referenced-project-settings.js b/tests/baselines/reference/tsc/projectReferences/default-import-interop-uses-referenced-project-settings.js index bf990179392da..79610a168d1f0 100644 --- a/tests/baselines/reference/tsc/projectReferences/default-import-interop-uses-referenced-project-settings.js +++ b/tests/baselines/reference/tsc/projectReferences/default-import-interop-uses-referenced-project-settings.js @@ -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] From 4e968bbc21ac7efa07a8a499e6317ab880a88170 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 22 Jan 2026 20:14:50 +0000 Subject: [PATCH 06/10] Replace fourslash test with proper tsc test that reproduces the bug Co-authored-by: andrewbranch <3277153+andrewbranch@users.noreply.github.com> --- .../unittests/tsc/projectReferences.ts | 29 +++++++++ ...ule-disallows-synthetic-default-imports.js | 62 +++++++++++++++++++ ...heticDefaultExportWithProjectReferences.ts | 21 ------- 3 files changed, 91 insertions(+), 21 deletions(-) create mode 100644 tests/baselines/reference/tsc/projectReferences/referenced-project-with-esnext-module-disallows-synthetic-default-imports.js delete mode 100644 tests/cases/fourslash/syntheticDefaultExportWithProjectReferences.ts diff --git a/src/testRunner/unittests/tsc/projectReferences.ts b/src/testRunner/unittests/tsc/projectReferences.ts index 3a5b1e09de7ad..9e1460e6ff6a6 100644 --- a/src/testRunner/unittests/tsc/projectReferences.ts +++ b/src/testRunner/unittests/tsc/projectReferences.ts @@ -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", diff --git a/tests/baselines/reference/tsc/projectReferences/referenced-project-with-esnext-module-disallows-synthetic-default-imports.js b/tests/baselines/reference/tsc/projectReferences/referenced-project-with-esnext-module-disallows-synthetic-default-imports.js new file mode 100644 index 0000000000000..f76acb14c6ead --- /dev/null +++ b/tests/baselines/reference/tsc/projectReferences/referenced-project-with-esnext-module-disallows-synthetic-default-imports.js @@ -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 { length: number; [n: number]: T; } +interface ReadonlyArray {} +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 diff --git a/tests/cases/fourslash/syntheticDefaultExportWithProjectReferences.ts b/tests/cases/fourslash/syntheticDefaultExportWithProjectReferences.ts deleted file mode 100644 index 1581de77c9a58..0000000000000 --- a/tests/cases/fourslash/syntheticDefaultExportWithProjectReferences.ts +++ /dev/null @@ -1,21 +0,0 @@ -/// - -// Test case for synthetic default imports from declaration files. -// Without project references, .d.ts files allow synthetic defaults due to ambiguity. -// With project references (tested manually), the referenced project's options determine -// if synthetic defaults are allowed. - -// @module: esnext -// @moduleResolution: bundler - -// @Filename: /a/utils.d.ts -//// export declare const test: () => string; - -// @Filename: /b/index.ts -//// import Test from '../a/utils'; -//// -//// console.log(Test.test()); - -// Without project references, this is allowed (no error) because we can't determine -// if the original JS was ESM or CJS -verify.getSemanticDiagnostics([]); From d392ebdbb8bd078705d9ac8120174091fb8620ae Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 22 Jan 2026 20:32:24 +0000 Subject: [PATCH 07/10] Use getEmitModuleKind for referenced project options Co-authored-by: andrewbranch <3277153+andrewbranch@users.noreply.github.com> --- src/compiler/checker.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index ae8ec56cd6498..a05f9ec26bbd4 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -3824,7 +3824,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { } // If the referenced project's module format is ESM (ES2015 or later), // it cannot have a synthetic default - const referencedModuleKind = referencedOptions.module ?? ModuleKind.CommonJS; + const referencedModuleKind = getEmitModuleKind(referencedOptions); if (referencedModuleKind >= ModuleKind.ES2015) { return false; } From 5a2ac972f7d433465307e338a014792e950c47d8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 22 Jan 2026 21:06:10 +0000 Subject: [PATCH 08/10] Use getEmitModuleFormatOfFile for project reference declaration files Co-authored-by: andrewbranch <3277153+andrewbranch@users.noreply.github.com> --- src/compiler/checker.ts | 26 ++++++++------------------ 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index a05f9ec26bbd4..6b4d89a74baca 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -3794,6 +3794,14 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { // are ESM, there cannot be a synthetic default. return false; } + // For declaration files from project references, check if the referenced project's + // emit format is ESM, in which case there cannot be a synthetic default + if (file.isDeclarationFile && host.getRedirectFromSourceFile(file.path)) { + const targetModuleKind = host.getEmitModuleFormatOfFile(file); + if (usageMode === ModuleKind.ESNext && targetModuleKind >= ModuleKind.ES2015) { + return false; + } + } } if (!allowSyntheticDefaultImports) { return false; @@ -3812,24 +3820,6 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { // it definitely is a module and does not have a synthetic default return false; } - // If this is a declaration file from a project reference, check the referenced project's options - // to determine if the module format is ESM - if (file) { - const redirect = host.getRedirectFromSourceFile(file.path); - if (redirect?.resolvedRef) { - const referencedOptions = redirect.resolvedRef.commandLine.options; - // If the referenced project has allowSyntheticDefaultImports disabled, respect that - if (!getAllowSyntheticDefaultImports(referencedOptions)) { - return false; - } - // If the referenced project's module format is ESM (ES2015 or later), - // it cannot have a synthetic default - const referencedModuleKind = getEmitModuleKind(referencedOptions); - if (referencedModuleKind >= ModuleKind.ES2015) { - return false; - } - } - } // There are _many_ declaration files not written with esmodules in mind that still get compiled into a format with __esModule set // Meaning there may be no default at runtime - however to be on the permissive side, we allow access to a synthetic default member // as there is no marker to indicate if the accompanying JS has `__esModule` or not, or is even native esm From 0d532eb66fa6522b3edfc0f12ad63c9a92fcc559 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 22 Jan 2026 21:21:55 +0000 Subject: [PATCH 09/10] Apply getEmitModuleFormatOfFile only for project reference declaration files Co-authored-by: andrewbranch <3277153+andrewbranch@users.noreply.github.com> --- src/compiler/checker.ts | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 6b4d89a74baca..9cf5532396b0d 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -3794,12 +3794,17 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { // are ESM, there cannot be a synthetic default. return false; } - // For declaration files from project references, check if the referenced project's - // emit format is ESM, in which case there cannot be a synthetic default - if (file.isDeclarationFile && host.getRedirectFromSourceFile(file.path)) { - const targetModuleKind = host.getEmitModuleFormatOfFile(file); - if (usageMode === ModuleKind.ESNext && targetModuleKind >= ModuleKind.ES2015) { - 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) { + // 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) { + return false; + } } } } From d7ac3ad13be50342217dba4049a8beb51213fe7b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 22 Jan 2026 22:04:51 +0000 Subject: [PATCH 10/10] Use ES module range check and try both redirect mappings Co-authored-by: andrewbranch <3277153+andrewbranch@users.noreply.github.com> --- src/compiler/checker.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 9cf5532396b0d..5d446f6799418 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -3797,12 +3797,14 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { // 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); + // Try to get the project reference - try both source file mapping and output file mapping + // since declaration files can be mapped either way depending on how they're resolved + const redirect = host.getRedirectFromSourceFile(file.path) || host.getRedirectFromOutput(file.path); if (redirect) { // 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) { + if (usageMode === ModuleKind.ESNext && ModuleKind.ES2015 <= targetModuleKind && targetModuleKind <= ModuleKind.ESNext) { return false; } }