Skip to content

Commit 7e63150

Browse files
authored
Synthesize namespace records for proper esm interop (#19675)
* Integrate importStar and importDefault helpers * Accept baselines * Support dynamic imports, write helpers for umd module (and amd is possible) kinds * Accept baselines * Support AMD, use same helper initialization as is normal * update typechecker to have errors on called imported namespaces and good error recovery with a quickfix * Overhaul allowSyntheticDefaultExports to be safer * Put the new behavior behind a flag * Rename strictESM to ESMInterop * ESMInterop -> ESModuleInterop, make default for tsc --init * Rename ESMInterop -> ESModuleInterop in module.ts, add emit test (since fourslash doesnt do that) * Remove erroneous semicolons from helper * Reword diagnostic * Change style * Edit followup diagnostic * Add secondary quickfix for call sites, tests forthcoming * Add synth default to namespace import type, enhance quickfix * Pair of spare tests for good measure * Fix typos in diagnostic message * Improve comment clarity * Actually accept the updated changes to the esmodule interop description * ESModule -> esModule * Use find and not forEach * Use guard * Rely on implicit falsiness of Result.False * These should have been emit flags
1 parent 859f0e3 commit 7e63150

File tree

65 files changed

+1019
-212
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

65 files changed

+1019
-212
lines changed

src/compiler/checker.ts

Lines changed: 120 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1471,6 +1471,43 @@ namespace ts {
14711471
return getSymbolOfPartOfRightHandSideOfImportEquals(<EntityName>node.moduleReference, dontResolveAlias);
14721472
}
14731473

1474+
function resolveExportByName(moduleSymbol: Symbol, name: __String, dontResolveAlias: boolean) {
1475+
const exportValue = moduleSymbol.exports.get(InternalSymbolName.ExportEquals);
1476+
return exportValue
1477+
? getPropertyOfType(getTypeOfSymbol(exportValue), name)
1478+
: resolveSymbol(moduleSymbol.exports.get(name), dontResolveAlias);
1479+
}
1480+
1481+
function canHaveSyntheticDefault(file: SourceFile | undefined, moduleSymbol: Symbol, dontResolveAlias: boolean) {
1482+
if (!allowSyntheticDefaultImports) {
1483+
return false;
1484+
}
1485+
// Declaration files (and ambient modules)
1486+
if (!file || file.isDeclarationFile) {
1487+
// Definitely cannot have a synthetic default if they have a default member specified
1488+
if (resolveExportByName(moduleSymbol, InternalSymbolName.Default, dontResolveAlias)) {
1489+
return false;
1490+
}
1491+
// It _might_ still be incorrect to assume there is no __esModule marker on the import at runtime, even if there is no `default` member
1492+
// So we check a bit more,
1493+
if (resolveExportByName(moduleSymbol, escapeLeadingUnderscores("__esModule"), dontResolveAlias)) {
1494+
// If there is an `__esModule` specified in the declaration (meaning someone explicitly added it or wrote it in their code),
1495+
// it definitely is a module and does not have a synthetic default
1496+
return false;
1497+
}
1498+
// There are _many_ declaration files not written with esmodules in mind that still get compiled into a format with __esModule set
1499+
// Meaning there may be no default at runtime - however to be on the permissive side, we allow access to a synthetic default member
1500+
// as there is no marker to indicate if the accompanying JS has `__esModule` or not, or is even native esm
1501+
return true;
1502+
}
1503+
// TypeScript files never have a synthetic default (as they are always emitted with an __esModule marker) _unless_ they contain an export= statement
1504+
if (!isSourceFileJavaScript(file)) {
1505+
return hasExportAssignmentSymbol(moduleSymbol);
1506+
}
1507+
// JS files have a synthetic default if they do not contain ES2015+ module syntax (export = is not valid in js) _and_ do not have an __esModule marker
1508+
return !file.externalModuleIndicator && !resolveExportByName(moduleSymbol, escapeLeadingUnderscores("__esModule"), dontResolveAlias);
1509+
}
1510+
14741511
function getTargetOfImportClause(node: ImportClause, dontResolveAlias: boolean): Symbol {
14751512
const moduleSymbol = resolveExternalModuleName(node, (<ImportDeclaration>node.parent).moduleSpecifier);
14761513

@@ -1480,16 +1517,16 @@ namespace ts {
14801517
exportDefaultSymbol = moduleSymbol;
14811518
}
14821519
else {
1483-
const exportValue = moduleSymbol.exports.get("export=" as __String);
1484-
exportDefaultSymbol = exportValue
1485-
? getPropertyOfType(getTypeOfSymbol(exportValue), InternalSymbolName.Default)
1486-
: resolveSymbol(moduleSymbol.exports.get(InternalSymbolName.Default), dontResolveAlias);
1520+
exportDefaultSymbol = resolveExportByName(moduleSymbol, InternalSymbolName.Default, dontResolveAlias);
14871521
}
14881522

1489-
if (!exportDefaultSymbol && !allowSyntheticDefaultImports) {
1523+
const file = find(moduleSymbol.declarations, isSourceFile);
1524+
const hasSyntheticDefault = canHaveSyntheticDefault(file, moduleSymbol, dontResolveAlias);
1525+
if (!exportDefaultSymbol && !hasSyntheticDefault) {
14901526
error(node.name, Diagnostics.Module_0_has_no_default_export, symbolToString(moduleSymbol));
14911527
}
1492-
else if (!exportDefaultSymbol && allowSyntheticDefaultImports) {
1528+
else if (!exportDefaultSymbol && hasSyntheticDefault) {
1529+
// per emit behavior, a synthetic default overrides a "real" .default member if `__esModule` is not present
14931530
return resolveExternalModuleSymbol(moduleSymbol, dontResolveAlias) || resolveSymbol(moduleSymbol, dontResolveAlias);
14941531
}
14951532
return exportDefaultSymbol;
@@ -1889,8 +1926,40 @@ namespace ts {
18891926
// combine other declarations with the module or variable (e.g. a class/module, function/module, interface/variable).
18901927
function resolveESModuleSymbol(moduleSymbol: Symbol, moduleReferenceExpression: Expression, dontResolveAlias: boolean): Symbol {
18911928
const symbol = resolveExternalModuleSymbol(moduleSymbol, dontResolveAlias);
1892-
if (!dontResolveAlias && symbol && !(symbol.flags & (SymbolFlags.Module | SymbolFlags.Variable))) {
1893-
error(moduleReferenceExpression, Diagnostics.Module_0_resolves_to_a_non_module_entity_and_cannot_be_imported_using_this_construct, symbolToString(moduleSymbol));
1929+
if (!dontResolveAlias && symbol) {
1930+
if (!(symbol.flags & (SymbolFlags.Module | SymbolFlags.Variable))) {
1931+
error(moduleReferenceExpression, Diagnostics.Module_0_resolves_to_a_non_module_entity_and_cannot_be_imported_using_this_construct, symbolToString(moduleSymbol));
1932+
return symbol;
1933+
}
1934+
if (compilerOptions.esModuleInterop) {
1935+
const referenceParent = moduleReferenceExpression.parent;
1936+
if (
1937+
(isImportDeclaration(referenceParent) && getNamespaceDeclarationNode(referenceParent)) ||
1938+
isImportCall(referenceParent)
1939+
) {
1940+
const type = getTypeOfSymbol(symbol);
1941+
let sigs = getSignaturesOfStructuredType(type, SignatureKind.Call);
1942+
if (!sigs || !sigs.length) {
1943+
sigs = getSignaturesOfStructuredType(type, SignatureKind.Construct);
1944+
}
1945+
if (sigs && sigs.length) {
1946+
const moduleType = getTypeWithSyntheticDefaultImportType(type, symbol, moduleSymbol);
1947+
// Create a new symbol which has the module's type less the call and construct signatures
1948+
const result = createSymbol(symbol.flags, symbol.escapedName);
1949+
result.declarations = symbol.declarations ? symbol.declarations.slice() : [];
1950+
result.parent = symbol.parent;
1951+
result.target = symbol;
1952+
result.originatingImport = referenceParent;
1953+
if (symbol.valueDeclaration) result.valueDeclaration = symbol.valueDeclaration;
1954+
if (symbol.constEnumOnlyModule) result.constEnumOnlyModule = true;
1955+
if (symbol.members) result.members = cloneMap(symbol.members);
1956+
if (symbol.exports) result.exports = cloneMap(symbol.exports);
1957+
const resolvedModuleType = resolveStructuredTypeMembers(moduleType as StructuredType); // Should already be resolved from the signature checks above
1958+
result.type = createAnonymousType(result, resolvedModuleType.members, emptyArray, emptyArray, resolvedModuleType.stringIndexInfo, resolvedModuleType.numberIndexInfo);
1959+
return result;
1960+
}
1961+
}
1962+
}
18941963
}
18951964
return symbol;
18961965
}
@@ -9452,6 +9521,17 @@ namespace ts {
94529521

94539522
diagnostics.add(createDiagnosticForNodeFromMessageChain(errorNode, errorInfo));
94549523
}
9524+
// Check if we should issue an extra diagnostic to produce a quickfix for a slightly incorrect import statement
9525+
if (headMessage && errorNode && !result && source.symbol) {
9526+
const links = getSymbolLinks(source.symbol);
9527+
if (links.originatingImport && !isImportCall(links.originatingImport)) {
9528+
const helpfulRetry = checkTypeRelatedTo(getTypeOfSymbol(links.target), target, relation, /*errorNode*/ undefined);
9529+
if (helpfulRetry) {
9530+
// Likely an incorrect import. Issue a helpful diagnostic to produce a quickfix to change the import
9531+
diagnostics.add(createDiagnosticForNode(links.originatingImport, Diagnostics.A_namespace_style_import_cannot_be_called_or_constructed_and_will_cause_a_failure_at_runtime));
9532+
}
9533+
}
9534+
}
94559535
return result !== Ternary.False;
94569536

94579537
function reportError(message: DiagnosticMessage, arg0?: string, arg1?: string, arg2?: string): void {
@@ -17320,7 +17400,7 @@ namespace ts {
1732017400
error(node, Diagnostics.Value_of_type_0_is_not_callable_Did_you_mean_to_include_new, typeToString(funcType));
1732117401
}
1732217402
else {
17323-
error(node, Diagnostics.Cannot_invoke_an_expression_whose_type_lacks_a_call_signature_Type_0_has_no_compatible_call_signatures, typeToString(apparentType));
17403+
invocationError(node, apparentType, SignatureKind.Call);
1732417404
}
1732517405
return resolveErrorCall(node);
1732617406
}
@@ -17410,7 +17490,7 @@ namespace ts {
1741017490
return signature;
1741117491
}
1741217492

17413-
error(node, Diagnostics.Cannot_use_new_with_an_expression_whose_type_lacks_a_call_or_construct_signature);
17493+
invocationError(node, expressionType, SignatureKind.Construct);
1741417494
return resolveErrorCall(node);
1741517495
}
1741617496

@@ -17457,6 +17537,28 @@ namespace ts {
1745717537
return true;
1745817538
}
1745917539

17540+
function invocationError(node: Node, apparentType: Type, kind: SignatureKind) {
17541+
error(node, kind === SignatureKind.Call
17542+
? Diagnostics.Cannot_invoke_an_expression_whose_type_lacks_a_call_signature_Type_0_has_no_compatible_call_signatures
17543+
: Diagnostics.Cannot_use_new_with_an_expression_whose_type_lacks_a_call_or_construct_signature
17544+
, typeToString(apparentType));
17545+
invocationErrorRecovery(apparentType, kind);
17546+
}
17547+
17548+
function invocationErrorRecovery(apparentType: Type, kind: SignatureKind) {
17549+
if (!apparentType.symbol) {
17550+
return;
17551+
}
17552+
const importNode = getSymbolLinks(apparentType.symbol).originatingImport;
17553+
// Create a diagnostic on the originating import if possible onto which we can attach a quickfix
17554+
// An import call expression cannot be rewritten into another form to correct the error - the only solution is to use `.default` at the use-site
17555+
if (importNode && !isImportCall(importNode)) {
17556+
const sigs = getSignaturesOfType(getTypeOfSymbol(getSymbolLinks(apparentType.symbol).target), kind);
17557+
if (!sigs || !sigs.length) return;
17558+
error(importNode, Diagnostics.A_namespace_style_import_cannot_be_called_or_constructed_and_will_cause_a_failure_at_runtime);
17559+
}
17560+
}
17561+
1746017562
function resolveTaggedTemplateExpression(node: TaggedTemplateExpression, candidatesOutArray: Signature[]): Signature {
1746117563
const tagType = checkExpression(node.tag);
1746217564
const apparentType = getApparentType(tagType);
@@ -17474,7 +17576,7 @@ namespace ts {
1747417576
}
1747517577

1747617578
if (!callSignatures.length) {
17477-
error(node, Diagnostics.Cannot_invoke_an_expression_whose_type_lacks_a_call_signature_Type_0_has_no_compatible_call_signatures, typeToString(apparentType));
17579+
invocationError(node, apparentType, SignatureKind.Call);
1747817580
return resolveErrorCall(node);
1747917581
}
1748017582

@@ -17531,6 +17633,7 @@ namespace ts {
1753117633
errorInfo = chainDiagnosticMessages(errorInfo, Diagnostics.Cannot_invoke_an_expression_whose_type_lacks_a_call_signature_Type_0_has_no_compatible_call_signatures, typeToString(apparentType));
1753217634
errorInfo = chainDiagnosticMessages(errorInfo, headMessage);
1753317635
diagnostics.add(createDiagnosticForNodeFromMessageChain(node, errorInfo));
17636+
invocationErrorRecovery(apparentType, SignatureKind.Call);
1753417637
return resolveErrorCall(node);
1753517638
}
1753617639

@@ -17785,25 +17888,27 @@ namespace ts {
1778517888
if (moduleSymbol) {
1778617889
const esModuleSymbol = resolveESModuleSymbol(moduleSymbol, specifier, /*dontRecursivelyResolve*/ true);
1778717890
if (esModuleSymbol) {
17788-
return createPromiseReturnType(node, getTypeWithSyntheticDefaultImportType(getTypeOfSymbol(esModuleSymbol), esModuleSymbol));
17891+
return createPromiseReturnType(node, getTypeWithSyntheticDefaultImportType(getTypeOfSymbol(esModuleSymbol), esModuleSymbol, moduleSymbol));
1778917892
}
1779017893
}
1779117894
return createPromiseReturnType(node, anyType);
1779217895
}
1779317896

17794-
function getTypeWithSyntheticDefaultImportType(type: Type, symbol: Symbol): Type {
17897+
function getTypeWithSyntheticDefaultImportType(type: Type, symbol: Symbol, originalSymbol: Symbol): Type {
1779517898
if (allowSyntheticDefaultImports && type && type !== unknownType) {
1779617899
const synthType = type as SyntheticDefaultModuleType;
1779717900
if (!synthType.syntheticType) {
17798-
if (!getPropertyOfType(type, InternalSymbolName.Default)) {
17901+
const file = find(originalSymbol.declarations, isSourceFile);
17902+
const hasSyntheticDefault = canHaveSyntheticDefault(file, originalSymbol, /*dontResolveAlias*/ false);
17903+
if (hasSyntheticDefault) {
1779917904
const memberTable = createSymbolTable();
1780017905
const newSymbol = createSymbol(SymbolFlags.Alias, InternalSymbolName.Default);
1780117906
newSymbol.target = resolveSymbol(symbol);
1780217907
memberTable.set(InternalSymbolName.Default, newSymbol);
1780317908
const anonymousSymbol = createSymbol(SymbolFlags.TypeLiteral, InternalSymbolName.Type);
1780417909
const defaultContainingObject = createAnonymousType(anonymousSymbol, memberTable, emptyArray, emptyArray, /*stringIndexInfo*/ undefined, /*numberIndexInfo*/ undefined);
1780517910
anonymousSymbol.type = defaultContainingObject;
17806-
synthType.syntheticType = getIntersectionType([type, defaultContainingObject]);
17911+
synthType.syntheticType = (type.flags & TypeFlags.StructuredType && type.symbol.flags & (SymbolFlags.Module | SymbolFlags.Variable)) ? getSpreadType(type, defaultContainingObject, anonymousSymbol, /*propegatedFlags*/ 0) : defaultContainingObject;
1780717912
}
1780817913
else {
1780917914
synthType.syntheticType = type;

src/compiler/commandLineParser.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -402,6 +402,13 @@ namespace ts {
402402
category: Diagnostics.Module_Resolution_Options,
403403
description: Diagnostics.Allow_default_imports_from_modules_with_no_default_export_This_does_not_affect_code_emit_just_typechecking
404404
},
405+
{
406+
name: "esModuleInterop",
407+
type: "boolean",
408+
showInSimplifiedHelpView: true,
409+
category: Diagnostics.Module_Resolution_Options,
410+
description: Diagnostics.Enables_emit_interoperability_between_CommonJS_and_ES_Modules_via_creation_of_namespace_objects_for_all_imports_Implies_allowSyntheticDefaultImports
411+
},
405412
{
406413
name: "preserveSymlinks",
407414
type: "boolean",
@@ -704,7 +711,8 @@ namespace ts {
704711
export const defaultInitCompilerOptions: CompilerOptions = {
705712
module: ModuleKind.CommonJS,
706713
target: ScriptTarget.ES5,
707-
strict: true
714+
strict: true,
715+
esModuleInterop: true
708716
};
709717

710718
let optionNameMapCache: OptionNameMap;

src/compiler/core.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2005,7 +2005,9 @@ namespace ts {
20052005
const moduleKind = getEmitModuleKind(compilerOptions);
20062006
return compilerOptions.allowSyntheticDefaultImports !== undefined
20072007
? compilerOptions.allowSyntheticDefaultImports
2008-
: moduleKind === ModuleKind.System;
2008+
: compilerOptions.esModuleInterop
2009+
? moduleKind !== ModuleKind.None && moduleKind < ModuleKind.ES2015
2010+
: moduleKind === ModuleKind.System;
20092011
}
20102012

20112013
export type StrictOptionName = "noImplicitAny" | "noImplicitThis" | "strictNullChecks" | "strictFunctionTypes" | "strictPropertyInitialization" | "alwaysStrict";

src/compiler/diagnosticMessages.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3548,6 +3548,14 @@
35483548
"category": "Error",
35493549
"code": 7036
35503550
},
3551+
"Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'.": {
3552+
"category": "Message",
3553+
"code": 7037
3554+
},
3555+
"A namespace-style import cannot be called or constructed, and will cause a failure at runtime.": {
3556+
"category": "Error",
3557+
"code": 7038
3558+
},
35513559

35523560
"You cannot rename this element.": {
35533561
"category": "Error",
@@ -3906,5 +3914,13 @@
39063914
"Install '{0}'": {
39073915
"category": "Message",
39083916
"code": 95014
3917+
},
3918+
"Replace import with '{0}'.": {
3919+
"category": "Message",
3920+
"code": 95015
3921+
},
3922+
"Use synthetic 'default' member.": {
3923+
"category": "Message",
3924+
"code": 95016
39093925
}
39103926
}

src/compiler/program.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1601,6 +1601,7 @@ namespace ts {
16011601
// synthesize 'import "tslib"' declaration
16021602
const externalHelpersModuleReference = createLiteral(externalHelpersModuleNameText);
16031603
const importDecl = createImportDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined, /*importClause*/ undefined);
1604+
addEmitFlags(importDecl, EmitFlags.NeverApplyImportHelper);
16041605
externalHelpersModuleReference.parent = importDecl;
16051606
importDecl.parent = file;
16061607
imports = [externalHelpersModuleReference];

src/compiler/transformers/module/es2015.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,14 @@ namespace ts {
2525
if (externalHelpersModuleName) {
2626
const statements: Statement[] = [];
2727
const statementOffset = addPrologue(statements, node.statements);
28-
append(statements,
29-
createImportDeclaration(
30-
/*decorators*/ undefined,
31-
/*modifiers*/ undefined,
32-
createImportClause(/*name*/ undefined, createNamespaceImport(externalHelpersModuleName)),
33-
createLiteral(externalHelpersModuleNameText)
34-
)
28+
const tslibImport = createImportDeclaration(
29+
/*decorators*/ undefined,
30+
/*modifiers*/ undefined,
31+
createImportClause(/*name*/ undefined, createNamespaceImport(externalHelpersModuleName)),
32+
createLiteral(externalHelpersModuleNameText)
3533
);
34+
addEmitFlags(tslibImport, EmitFlags.NeverApplyImportHelper);
35+
append(statements, tslibImport);
3636

3737
addRange(statements, visitNodes(node.statements, visitor, isStatement, statementOffset));
3838
return updateSourceFileNode(

0 commit comments

Comments
 (0)