Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Synthesize namespace records for proper esm interop #19675

Merged
merged 32 commits into from
Jan 9, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
213146d
Integrate importStar and importDefault helpers
weswigham Nov 1, 2017
b5a7ef9
Accept baselines
weswigham Nov 1, 2017
8abc907
Support dynamic imports, write helpers for umd module (and amd is pos…
weswigham Nov 1, 2017
2d0c8d3
Accept baselines
weswigham Nov 1, 2017
a725916
Support AMD, use same helper initialization as is normal
weswigham Nov 2, 2017
e910603
update typechecker to have errors on called imported namespaces and g…
weswigham Nov 2, 2017
e84b051
Overhaul allowSyntheticDefaultExports to be safer
weswigham Nov 3, 2017
f23d41c
Put the new behavior behind a flag
weswigham Nov 3, 2017
ebd4c03
Merge branch 'master' into module-nodejs
weswigham Nov 7, 2017
3b33df0
Rename strictESM to ESMInterop
weswigham Nov 7, 2017
7ff11bb
Merge branch 'master' into module-nodejs
weswigham Nov 10, 2017
bcafdba
ESMInterop -> ESModuleInterop, make default for tsc --init
weswigham Nov 10, 2017
eaf2fc5
Merge branch 'master' into module-nodejs
weswigham Nov 17, 2017
29c731c
Rename ESMInterop -> ESModuleInterop in module.ts, add emit test (sin…
weswigham Nov 17, 2017
4350caf
Merge branch 'master' into module-nodejs
weswigham Nov 17, 2017
e20a548
Remove erroneous semicolons from helper
weswigham Nov 18, 2017
578141d
Merge branch 'master' into module-nodejs
weswigham Nov 28, 2017
dfe5675
Reword diagnostic
weswigham Nov 29, 2017
8c6c313
Change style
weswigham Nov 29, 2017
3d996d7
Edit followup diagnostic
weswigham Nov 29, 2017
2f9c240
Add secondary quickfix for call sites, tests forthcoming
weswigham Nov 29, 2017
2361f37
Add synth default to namespace import type, enhance quickfix
weswigham Nov 29, 2017
357996b
Pair of spare tests for good measure
weswigham Nov 29, 2017
a682476
Merge branch 'master' into module-nodejs
weswigham Dec 18, 2017
6e9e874
Fix typos in diagnostic message
weswigham Jan 5, 2018
a654b82
Improve comment clarity
weswigham Jan 5, 2018
ef6faf1
Actually accept the updated changes to the esmodule interop description
weswigham Jan 8, 2018
a8233b3
ESModule -> esModule
weswigham Jan 8, 2018
f4f4e84
Use find and not forEach
weswigham Jan 8, 2018
386c54d
Use guard
weswigham Jan 8, 2018
2663db7
Rely on implicit falsiness of Result.False
weswigham Jan 8, 2018
8068e2e
These should have been emit flags
weswigham Jan 8, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 120 additions & 15 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1464,6 +1464,43 @@ namespace ts {
return getSymbolOfPartOfRightHandSideOfImportEquals(<EntityName>node.moduleReference, dontResolveAlias);
}

function resolveExportByName(moduleSymbol: Symbol, name: __String, dontResolveAlias: boolean) {
const exportValue = moduleSymbol.exports.get(InternalSymbolName.ExportEquals);
return exportValue
? getPropertyOfType(getTypeOfSymbol(exportValue), name)
: resolveSymbol(moduleSymbol.exports.get(name), dontResolveAlias);
}

function canHaveSyntheticDefault(file: SourceFile | undefined, moduleSymbol: Symbol, dontResolveAlias: boolean) {
if (!allowSyntheticDefaultImports) {
return false;
}
// Declaration files (and ambient modules)
if (!file || file.isDeclarationFile) {
// Definitely cannot have a synthetic default if they have a default member specified
if (resolveExportByName(moduleSymbol, InternalSymbolName.Default, dontResolveAlias)) {
return false;
}
// It _might_ still be incorrect to assume there is no __esModule marker on the import at runtime, even if there is no `default` member
Copy link
Contributor

Choose a reason for hiding this comment

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

So this is a breaking change to --allowSyntheticDefaultImports, thought i doubt anyone would run into this, we should document it.

Copy link
Member Author

Choose a reason for hiding this comment

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

// So we check a bit more,
if (resolveExportByName(moduleSymbol, escapeLeadingUnderscores("__esModule"), dontResolveAlias)) {
// If there is an `__esModule` specified in the declaration (meaning someone explicitly added it or wrote it in their code),
// it definitely is a module and does not have a synthetic default
return false;
}
// There are _many_ declaration files not written with esmodules in mind that still get compiled into a format with __esModule set
Copy link
Member

Choose a reason for hiding this comment

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

Are we considering an .mts extension (and therefore a .d.mts extension) in the future? Should we consider adding a /// <es-module /> prologue comment to .d.ts output going forward when using --module es2015?

// 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
return true;
}
// TypeScript files never have a synthetic default (as they are always emitted with an __esModule marker) _unless_ they contain an export= statement
if (!isSourceFileJavaScript(file)) {
return hasExportAssignmentSymbol(moduleSymbol);
}
// 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
return !file.externalModuleIndicator && !resolveExportByName(moduleSymbol, escapeLeadingUnderscores("__esModule"), dontResolveAlias);
}

function getTargetOfImportClause(node: ImportClause, dontResolveAlias: boolean): Symbol {
const moduleSymbol = resolveExternalModuleName(node, (<ImportDeclaration>node.parent).moduleSpecifier);

Expand All @@ -1473,16 +1510,16 @@ namespace ts {
exportDefaultSymbol = moduleSymbol;
}
else {
const exportValue = moduleSymbol.exports.get("export=" as __String);
exportDefaultSymbol = exportValue
? getPropertyOfType(getTypeOfSymbol(exportValue), InternalSymbolName.Default)
: resolveSymbol(moduleSymbol.exports.get(InternalSymbolName.Default), dontResolveAlias);
exportDefaultSymbol = resolveExportByName(moduleSymbol, InternalSymbolName.Default, dontResolveAlias);
}

if (!exportDefaultSymbol && !allowSyntheticDefaultImports) {
const file = find(moduleSymbol.declarations, isSourceFile);
const hasSyntheticDefault = canHaveSyntheticDefault(file, moduleSymbol, dontResolveAlias);
if (!exportDefaultSymbol && !hasSyntheticDefault) {
error(node.name, Diagnostics.Module_0_has_no_default_export, symbolToString(moduleSymbol));
}
else if (!exportDefaultSymbol && allowSyntheticDefaultImports) {
else if (!exportDefaultSymbol && hasSyntheticDefault) {
// per emit behavior, a synthetic default overrides a "real" .default member if `__esModule` is not present
return resolveExternalModuleSymbol(moduleSymbol, dontResolveAlias) || resolveSymbol(moduleSymbol, dontResolveAlias);
}
return exportDefaultSymbol;
Expand Down Expand Up @@ -1882,8 +1919,40 @@ namespace ts {
// combine other declarations with the module or variable (e.g. a class/module, function/module, interface/variable).
function resolveESModuleSymbol(moduleSymbol: Symbol, moduleReferenceExpression: Expression, dontResolveAlias: boolean): Symbol {
const symbol = resolveExternalModuleSymbol(moduleSymbol, dontResolveAlias);
if (!dontResolveAlias && symbol && !(symbol.flags & (SymbolFlags.Module | SymbolFlags.Variable))) {
error(moduleReferenceExpression, Diagnostics.Module_0_resolves_to_a_non_module_entity_and_cannot_be_imported_using_this_construct, symbolToString(moduleSymbol));
if (!dontResolveAlias && symbol) {
if (!(symbol.flags & (SymbolFlags.Module | SymbolFlags.Variable))) {
error(moduleReferenceExpression, Diagnostics.Module_0_resolves_to_a_non_module_entity_and_cannot_be_imported_using_this_construct, symbolToString(moduleSymbol));
return symbol;
}
if (compilerOptions.esModuleInterop) {
const referenceParent = moduleReferenceExpression.parent;
if (
(isImportDeclaration(referenceParent) && getNamespaceDeclarationNode(referenceParent)) ||
isImportCall(referenceParent)
) {
const type = getTypeOfSymbol(symbol);
let sigs = getSignaturesOfStructuredType(type, SignatureKind.Call);
if (!sigs || !sigs.length) {
sigs = getSignaturesOfStructuredType(type, SignatureKind.Construct);
}
if (sigs && sigs.length) {
const moduleType = getTypeWithSyntheticDefaultImportType(type, symbol, moduleSymbol);
// Create a new symbol which has the module's type less the call and construct signatures
const result = createSymbol(symbol.flags, symbol.escapedName);
result.declarations = symbol.declarations ? symbol.declarations.slice() : [];
result.parent = symbol.parent;
result.target = symbol;
result.originatingImport = referenceParent;
if (symbol.valueDeclaration) result.valueDeclaration = symbol.valueDeclaration;
if (symbol.constEnumOnlyModule) result.constEnumOnlyModule = true;
if (symbol.members) result.members = cloneMap(symbol.members);
if (symbol.exports) result.exports = cloneMap(symbol.exports);
const resolvedModuleType = resolveStructuredTypeMembers(moduleType as StructuredType); // Should already be resolved from the signature checks above
result.type = createAnonymousType(result, resolvedModuleType.members, emptyArray, emptyArray, resolvedModuleType.stringIndexInfo, resolvedModuleType.numberIndexInfo);
return result;
}
}
}
}
return symbol;
}
Expand Down Expand Up @@ -9401,6 +9470,17 @@ namespace ts {

diagnostics.add(createDiagnosticForNodeFromMessageChain(errorNode, errorInfo));
}
// Check if we should issue an extra diagnostic to produce a quickfix for a slightly incorrect import statement
if (headMessage && errorNode && !result && source.symbol) {
const links = getSymbolLinks(source.symbol);
if (links.originatingImport && !isImportCall(links.originatingImport)) {
const helpfulRetry = checkTypeRelatedTo(getTypeOfSymbol(links.target), target, relation, /*errorNode*/ undefined);
if (helpfulRetry) {
// Likely an incorrect import. Issue a helpful diagnostic to produce a quickfix to change the import
diagnostics.add(createDiagnosticForNode(links.originatingImport, Diagnostics.A_namespace_style_import_cannot_be_called_or_constructed_and_will_cause_a_failure_at_runtime));
}
}
}
return result !== Ternary.False;

function reportError(message: DiagnosticMessage, arg0?: string, arg1?: string, arg2?: string): void {
Expand Down Expand Up @@ -17256,7 +17336,7 @@ namespace ts {
error(node, Diagnostics.Value_of_type_0_is_not_callable_Did_you_mean_to_include_new, typeToString(funcType));
}
else {
error(node, Diagnostics.Cannot_invoke_an_expression_whose_type_lacks_a_call_signature_Type_0_has_no_compatible_call_signatures, typeToString(apparentType));
invocationError(node, apparentType, SignatureKind.Call);
}
return resolveErrorCall(node);
}
Expand Down Expand Up @@ -17346,7 +17426,7 @@ namespace ts {
return signature;
}

error(node, Diagnostics.Cannot_use_new_with_an_expression_whose_type_lacks_a_call_or_construct_signature);
invocationError(node, expressionType, SignatureKind.Construct);
return resolveErrorCall(node);
}

Expand Down Expand Up @@ -17393,6 +17473,28 @@ namespace ts {
return true;
}

function invocationError(node: Node, apparentType: Type, kind: SignatureKind) {
error(node, kind === SignatureKind.Call
? Diagnostics.Cannot_invoke_an_expression_whose_type_lacks_a_call_signature_Type_0_has_no_compatible_call_signatures
: Diagnostics.Cannot_use_new_with_an_expression_whose_type_lacks_a_call_or_construct_signature
, typeToString(apparentType));
invocationErrorRecovery(apparentType, kind);
}

function invocationErrorRecovery(apparentType: Type, kind: SignatureKind) {
if (!apparentType.symbol) {
return;
}
const importNode = getSymbolLinks(apparentType.symbol).originatingImport;
// Create a diagnostic on the originating import if possible onto which we can attach a quickfix
// 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
if (importNode && !isImportCall(importNode)) {
const sigs = getSignaturesOfType(getTypeOfSymbol(getSymbolLinks(apparentType.symbol).target), kind);
if (!sigs || !sigs.length) return;
error(importNode, Diagnostics.A_namespace_style_import_cannot_be_called_or_constructed_and_will_cause_a_failure_at_runtime);
}
}

function resolveTaggedTemplateExpression(node: TaggedTemplateExpression, candidatesOutArray: Signature[]): Signature {
const tagType = checkExpression(node.tag);
const apparentType = getApparentType(tagType);
Expand All @@ -17410,7 +17512,7 @@ namespace ts {
}

if (!callSignatures.length) {
error(node, Diagnostics.Cannot_invoke_an_expression_whose_type_lacks_a_call_signature_Type_0_has_no_compatible_call_signatures, typeToString(apparentType));
invocationError(node, apparentType, SignatureKind.Call);
return resolveErrorCall(node);
}

Expand Down Expand Up @@ -17467,6 +17569,7 @@ namespace ts {
errorInfo = chainDiagnosticMessages(errorInfo, Diagnostics.Cannot_invoke_an_expression_whose_type_lacks_a_call_signature_Type_0_has_no_compatible_call_signatures, typeToString(apparentType));
errorInfo = chainDiagnosticMessages(errorInfo, headMessage);
diagnostics.add(createDiagnosticForNodeFromMessageChain(node, errorInfo));
invocationErrorRecovery(apparentType, SignatureKind.Call);
return resolveErrorCall(node);
}

Expand Down Expand Up @@ -17721,25 +17824,27 @@ namespace ts {
if (moduleSymbol) {
const esModuleSymbol = resolveESModuleSymbol(moduleSymbol, specifier, /*dontRecursivelyResolve*/ true);
if (esModuleSymbol) {
return createPromiseReturnType(node, getTypeWithSyntheticDefaultImportType(getTypeOfSymbol(esModuleSymbol), esModuleSymbol));
return createPromiseReturnType(node, getTypeWithSyntheticDefaultImportType(getTypeOfSymbol(esModuleSymbol), esModuleSymbol, moduleSymbol));
}
}
return createPromiseReturnType(node, anyType);
}

function getTypeWithSyntheticDefaultImportType(type: Type, symbol: Symbol): Type {
function getTypeWithSyntheticDefaultImportType(type: Type, symbol: Symbol, originalSymbol: Symbol): Type {
if (allowSyntheticDefaultImports && type && type !== unknownType) {
const synthType = type as SyntheticDefaultModuleType;
if (!synthType.syntheticType) {
if (!getPropertyOfType(type, InternalSymbolName.Default)) {
const file = find(originalSymbol.declarations, isSourceFile);
const hasSyntheticDefault = canHaveSyntheticDefault(file, originalSymbol, /*dontResolveAlias*/ false);
if (hasSyntheticDefault) {
const memberTable = createSymbolTable();
const newSymbol = createSymbol(SymbolFlags.Alias, InternalSymbolName.Default);
newSymbol.target = resolveSymbol(symbol);
memberTable.set(InternalSymbolName.Default, newSymbol);
const anonymousSymbol = createSymbol(SymbolFlags.TypeLiteral, InternalSymbolName.Type);
const defaultContainingObject = createAnonymousType(anonymousSymbol, memberTable, emptyArray, emptyArray, /*stringIndexInfo*/ undefined, /*numberIndexInfo*/ undefined);
anonymousSymbol.type = defaultContainingObject;
synthType.syntheticType = getIntersectionType([type, defaultContainingObject]);
synthType.syntheticType = (type.flags & TypeFlags.StructuredType && type.symbol.flags & (SymbolFlags.Module | SymbolFlags.Variable)) ? getSpreadType(type, defaultContainingObject, anonymousSymbol, /*propegatedFlags*/ 0) : defaultContainingObject;
}
else {
synthType.syntheticType = type;
Expand Down
10 changes: 9 additions & 1 deletion src/compiler/commandLineParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,13 @@ namespace ts {
category: Diagnostics.Module_Resolution_Options,
description: Diagnostics.Allow_default_imports_from_modules_with_no_default_export_This_does_not_affect_code_emit_just_typechecking
},
{
name: "esModuleInterop",
type: "boolean",
showInSimplifiedHelpView: true,
category: Diagnostics.Module_Resolution_Options,
description: Diagnostics.Enables_emit_interoperability_between_CommonJS_and_ES_Modules_via_creation_of_namespace_objects_for_all_imports_Implies_allowSyntheticDefaultImports
},
{
name: "preserveSymlinks",
type: "boolean",
Expand Down Expand Up @@ -702,7 +709,8 @@ namespace ts {
export const defaultInitCompilerOptions: CompilerOptions = {
module: ModuleKind.CommonJS,
target: ScriptTarget.ES5,
strict: true
strict: true,
esModuleInterop: true
};

let optionNameMapCache: OptionNameMap;
Expand Down
4 changes: 3 additions & 1 deletion src/compiler/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2016,7 +2016,9 @@ namespace ts {
const moduleKind = getEmitModuleKind(compilerOptions);
return compilerOptions.allowSyntheticDefaultImports !== undefined
? compilerOptions.allowSyntheticDefaultImports
: moduleKind === ModuleKind.System;
: compilerOptions.esModuleInterop
? moduleKind !== ModuleKind.None && moduleKind < ModuleKind.ES2015
: moduleKind === ModuleKind.System;
}

export type StrictOptionName = "noImplicitAny" | "noImplicitThis" | "strictNullChecks" | "strictFunctionTypes" | "strictPropertyInitialization" | "alwaysStrict";
Expand Down
16 changes: 16 additions & 0 deletions src/compiler/diagnosticMessages.json
Original file line number Diff line number Diff line change
Expand Up @@ -3536,6 +3536,14 @@
"category": "Error",
"code": 7036
},
"Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'.": {
"category": "Message",
"code": 7037
},
"A namespace-style import cannot be called or constructed, and will cause a failure at runtime.": {
"category": "Error",
"code": 7038
},

"You cannot rename this element.": {
"category": "Error",
Expand Down Expand Up @@ -3894,5 +3902,13 @@
"Install '{0}'": {
"category": "Message",
"code": 95014
},
"Replace import with '{0}'.": {
"category": "Message",
"code": 95015
},
"Use synthetic 'default' member.": {
"category": "Message",
"code": 95016
}
}
1 change: 1 addition & 0 deletions src/compiler/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1589,6 +1589,7 @@ namespace ts {
// synthesize 'import "tslib"' declaration
const externalHelpersModuleReference = createLiteral(externalHelpersModuleNameText);
const importDecl = createImportDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined, /*importClause*/ undefined);
addEmitFlags(importDecl, EmitFlags.NeverApplyImportHelper);
externalHelpersModuleReference.parent = importDecl;
importDecl.parent = file;
imports = [externalHelpersModuleReference];
Expand Down
14 changes: 7 additions & 7 deletions src/compiler/transformers/module/es2015.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,14 @@ namespace ts {
if (externalHelpersModuleName) {
const statements: Statement[] = [];
const statementOffset = addPrologue(statements, node.statements);
append(statements,
createImportDeclaration(
/*decorators*/ undefined,
/*modifiers*/ undefined,
createImportClause(/*name*/ undefined, createNamespaceImport(externalHelpersModuleName)),
createLiteral(externalHelpersModuleNameText)
)
const tslibImport = createImportDeclaration(
/*decorators*/ undefined,
/*modifiers*/ undefined,
createImportClause(/*name*/ undefined, createNamespaceImport(externalHelpersModuleName)),
createLiteral(externalHelpersModuleNameText)
);
addEmitFlags(tslibImport, EmitFlags.NeverApplyImportHelper);
append(statements, tslibImport);

addRange(statements, visitNodes(node.statements, visitor, isStatement, statementOffset));
return updateSourceFileNode(
Expand Down
Loading