Skip to content

Commit a5dde88

Browse files
authored
--moduleResolution bundler (formerly known as hybrid) (#51669)
* WIP * Add extension error back unless noEmit is set * Add non-relative tests * Add error for importing from declaration file * Update unit test * Add explicit flag for importing from .ts extensions * Add module specifier resolution changes * Add auto-import tests * Disallow relative imports into node_modules * Ensure auto-imports don’t suggest ./node_modules; * Test a non-portable declaration emit issue * Test auto-importing TSX file * Update path completions * Fix lint due to merge * Remove minimal-specific stuff * Remove minimal tests * Update unit tests * Add options * Add customConditions option * Add first tests * CJS constructs are not allowed * Add another test * Fix extension adding/replacing priority * Update test to reflect the choice not to block on unrecognized extensions * Add auto-imports and string completions tests * Revamp string completions ending preferences * Comment test * Auto-imports of declaration files cannot use .ts extension * Have declaration file auto imports default to extensionless instead * Add test for custom conditions * Fix indentation * Add baseline showing resolvePackageJsonImports/Exports compatibility * Fix test and prevent CJS require from resolving * Update unit test baselines * Fix bad merge conflict resolution * Make resolvedUsingTsExtension optional * Update missed baselines * Revert now-unnecessary API implementation changes * Clean up * Update baselines to es5 emit * Rename to `bundler`
1 parent ad354c2 commit a5dde88

File tree

135 files changed

+4077
-481
lines changed

Some content is hidden

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

135 files changed

+4077
-481
lines changed

src/compiler/binder.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ import {
8484
getContainingClass,
8585
getEffectiveContainerForJSDocTemplateTag,
8686
getElementOrPropertyAccessName,
87+
getEmitModuleResolutionKind,
8788
getEmitScriptTarget,
8889
getEnclosingBlockScopeContainer,
8990
getErrorSpanForNode,
@@ -235,6 +236,7 @@ import {
235236
ModifierFlags,
236237
ModuleBlock,
237238
ModuleDeclaration,
239+
ModuleResolutionKind,
238240
Mutable,
239241
NamespaceExportDeclaration,
240242
Node,
@@ -3520,6 +3522,7 @@ function createBinder(): (file: SourceFile, options: CompilerOptions) => void {
35203522
if (!isBindingPattern(node.name)) {
35213523
const possibleVariableDecl = node.kind === SyntaxKind.VariableDeclaration ? node : node.parent.parent;
35223524
if (isInJSFile(node) &&
3525+
getEmitModuleResolutionKind(options) !== ModuleResolutionKind.Bundler &&
35233526
isVariableDeclarationInitializedToBareOrAccessedRequire(possibleVariableDecl) &&
35243527
!getJSDocTypeTag(node) &&
35253528
!(getCombinedModifierFlags(node) & ModifierFlags.Export)

src/compiler/checker.ts

Lines changed: 56 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ import {
139139
ElementFlags,
140140
EmitFlags,
141141
EmitHint,
142+
emitModuleKindIsNonNodeESM,
142143
EmitResolver,
143144
EmitTextWriter,
144145
emptyArray,
@@ -317,6 +318,7 @@ import {
317318
getResolutionModeOverrideForClause,
318319
getResolvedExternalModuleName,
319320
getResolvedModule,
321+
getResolveJsonModule,
320322
getRestParameterElementType,
321323
getRootDeclaration,
322324
getScriptTargetFeatures,
@@ -346,8 +348,8 @@ import {
346348
hasAccessorModifier,
347349
hasAmbientModifier,
348350
hasContextSensitiveParameters,
349-
HasDecorators,
350351
hasDecorators,
352+
HasDecorators,
351353
hasDynamicName,
352354
hasEffectiveModifier,
353355
hasEffectiveModifiers,
@@ -356,8 +358,8 @@ import {
356358
hasExtension,
357359
HasIllegalDecorators,
358360
HasIllegalModifiers,
359-
HasInitializer,
360361
hasInitializer,
362+
HasInitializer,
361363
hasJSDocNodes,
362364
hasJSDocParameterTags,
363365
hasJsonModuleEmitEnabled,
@@ -458,6 +460,7 @@ import {
458460
isConstructorTypeNode,
459461
isConstTypeReference,
460462
isDeclaration,
463+
isDeclarationFileName,
461464
isDeclarationName,
462465
isDeclarationReadonly,
463466
isDecorator,
@@ -897,6 +900,7 @@ import {
897900
setTextRangePosEnd,
898901
setValueDeclaration,
899902
ShorthandPropertyAssignment,
903+
shouldAllowImportingTsExtension,
900904
shouldPreserveConstEnums,
901905
Signature,
902906
SignatureDeclaration,
@@ -4543,6 +4547,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
45434547
if (
45444548
namespace.valueDeclaration &&
45454549
isInJSFile(namespace.valueDeclaration) &&
4550+
getEmitModuleResolutionKind(compilerOptions) !== ModuleResolutionKind.Bundler &&
45464551
isVariableDeclaration(namespace.valueDeclaration) &&
45474552
namespace.valueDeclaration.initializer &&
45484553
isCommonJsRequire(namespace.valueDeclaration.initializer)
@@ -4727,6 +4732,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
47274732
(isModuleDeclaration(location) ? location : location.parent && isModuleDeclaration(location.parent) && location.parent.name === location ? location.parent : undefined)?.name ||
47284733
(isLiteralImportTypeNode(location) ? location : undefined)?.argument.literal;
47294734
const mode = contextSpecifier && isStringLiteralLike(contextSpecifier) ? getModeForUsageLocation(currentSourceFile, contextSpecifier) : currentSourceFile.impliedNodeFormat;
4735+
const moduleResolutionKind = getEmitModuleResolutionKind(compilerOptions);
47304736
const resolvedModule = getResolvedModule(currentSourceFile, moduleReference, mode);
47314737
const resolutionDiagnostic = resolvedModule && getResolutionDiagnostic(compilerOptions, resolvedModule);
47324738
const sourceFile = resolvedModule
@@ -4737,11 +4743,28 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
47374743
if (resolutionDiagnostic) {
47384744
error(errorNode, resolutionDiagnostic, moduleReference, resolvedModule.resolvedFileName);
47394745
}
4746+
4747+
if (resolvedModule.resolvedUsingTsExtension && isDeclarationFileName(moduleReference)) {
4748+
const importOrExport =
4749+
findAncestor(location, isImportDeclaration)?.importClause ||
4750+
findAncestor(location, or(isImportEqualsDeclaration, isExportDeclaration));
4751+
if (importOrExport && !importOrExport.isTypeOnly || findAncestor(location, isImportCall)) {
4752+
error(
4753+
errorNode,
4754+
Diagnostics.A_declaration_file_cannot_be_imported_without_import_type_Did_you_mean_to_import_an_implementation_file_0_instead,
4755+
getSuggestedImportSource(Debug.checkDefined(tryExtractTSExtension(moduleReference))));
4756+
}
4757+
}
4758+
else if (resolvedModule.resolvedUsingTsExtension && !shouldAllowImportingTsExtension(compilerOptions, currentSourceFile.fileName)) {
4759+
const tsExtension = Debug.checkDefined(tryExtractTSExtension(moduleReference));
4760+
error(errorNode, Diagnostics.An_import_path_can_only_end_with_a_0_extension_when_allowImportingTsExtensions_is_enabled, tsExtension);
4761+
}
4762+
47404763
if (sourceFile.symbol) {
47414764
if (resolvedModule.isExternalLibraryImport && !resolutionExtensionIsTSOrJson(resolvedModule.extension)) {
47424765
errorOnImplicitAnyModule(/*isError*/ false, errorNode, resolvedModule, moduleReference);
47434766
}
4744-
if (getEmitModuleResolutionKind(compilerOptions) === ModuleResolutionKind.Node16 || getEmitModuleResolutionKind(compilerOptions) === ModuleResolutionKind.NodeNext) {
4767+
if (moduleResolutionKind === ModuleResolutionKind.Node16 || moduleResolutionKind === ModuleResolutionKind.NodeNext) {
47454768
const isSyncImport = (currentSourceFile.impliedNodeFormat === ModuleKind.CommonJS && !findAncestor(location, isImportCall)) || !!findAncestor(location, isImportEqualsDeclaration);
47464769
const overrideClauseHost = findAncestor(location, l => isImportTypeNode(l) || isExportDeclaration(l) || isImportDeclaration(l)) as ImportTypeNode | ImportDeclaration | ExportDeclaration | undefined;
47474770
const overrideClause = overrideClauseHost && isImportTypeNode(overrideClauseHost) ? overrideClauseHost.assertions?.assertClause : overrideClauseHost?.assertClause;
@@ -4849,25 +4872,14 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
48494872
else {
48504873
const tsExtension = tryExtractTSExtension(moduleReference);
48514874
const isExtensionlessRelativePathImport = pathIsRelative(moduleReference) && !hasExtension(moduleReference);
4852-
const moduleResolutionKind = getEmitModuleResolutionKind(compilerOptions);
48534875
const resolutionIsNode16OrNext = moduleResolutionKind === ModuleResolutionKind.Node16 ||
48544876
moduleResolutionKind === ModuleResolutionKind.NodeNext;
48554877
if (tsExtension) {
4856-
const diag = Diagnostics.An_import_path_cannot_end_with_a_0_extension_Consider_importing_1_instead;
4857-
const importSourceWithoutExtension = removeExtension(moduleReference, tsExtension);
4858-
let replacedImportSource = importSourceWithoutExtension;
4859-
/**
4860-
* Direct users to import source with .js extension if outputting an ES module.
4861-
* @see https://github.com/microsoft/TypeScript/issues/42151
4862-
*/
4863-
if (moduleKind >= ModuleKind.ES2015) {
4864-
replacedImportSource += tsExtension === Extension.Mts ? ".mjs" : tsExtension === Extension.Cts ? ".cjs" : ".js";
4865-
}
4866-
error(errorNode, diag, tsExtension, replacedImportSource);
4867-
}
4868-
else if (!compilerOptions.resolveJsonModule &&
4878+
errorOnTSExtensionImport(tsExtension);
4879+
}
4880+
else if (!getResolveJsonModule(compilerOptions) &&
48694881
fileExtensionIs(moduleReference, Extension.Json) &&
4870-
getEmitModuleResolutionKind(compilerOptions) !== ModuleResolutionKind.Classic &&
4882+
moduleResolutionKind !== ModuleResolutionKind.Classic &&
48714883
hasJsonModuleEmitEnabled(compilerOptions)) {
48724884
error(errorNode, Diagnostics.Cannot_find_module_0_Consider_using_resolveJsonModule_to_import_module_with_json_extension, moduleReference);
48734885
}
@@ -4889,6 +4901,23 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
48894901
}
48904902
}
48914903
return undefined;
4904+
4905+
function errorOnTSExtensionImport(tsExtension: string) {
4906+
const diag = Diagnostics.An_import_path_cannot_end_with_a_0_extension_Consider_importing_1_instead;
4907+
error(errorNode, diag, tsExtension, getSuggestedImportSource(tsExtension));
4908+
}
4909+
4910+
function getSuggestedImportSource(tsExtension: string) {
4911+
const importSourceWithoutExtension = removeExtension(moduleReference, tsExtension);
4912+
/**
4913+
* Direct users to import source with .js extension if outputting an ES module.
4914+
* @see https://github.com/microsoft/TypeScript/issues/42151
4915+
*/
4916+
if (emitModuleKindIsNonNodeESM(moduleKind) || mode === ModuleKind.ESNext) {
4917+
return importSourceWithoutExtension + (tsExtension === Extension.Mts ? ".mjs" : tsExtension === Extension.Cts ? ".cjs" : ".js");
4918+
}
4919+
return importSourceWithoutExtension;
4920+
}
48924921
}
48934922

48944923
function errorOnImplicitAnyModule(isError: boolean, errorNode: Node, { packageId, resolvedFileName }: ResolvedModuleFull, moduleReference: string): void {
@@ -33297,7 +33326,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
3329733326
}
3329833327

3329933328
// In JavaScript files, calls to any identifier 'require' are treated as external module imports
33300-
if (isInJSFile(node) && isCommonJsRequire(node)) {
33329+
if (isInJSFile(node) && getEmitModuleResolutionKind(compilerOptions) !== ModuleResolutionKind.Bundler && isCommonJsRequire(node)) {
3330133330
return resolveExternalModuleTypeByLiteral(node.arguments![0] as StringLiteral);
3330233331
}
3330333332

@@ -42699,6 +42728,9 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
4269942728
// Import equals declaration is deprecated in es6 or above
4270042729
grammarErrorOnNode(node, Diagnostics.Import_assignment_cannot_be_used_when_targeting_ECMAScript_modules_Consider_using_import_Asterisk_as_ns_from_mod_import_a_from_mod_import_d_from_mod_or_another_module_format_instead);
4270142730
}
42731+
else if (getEmitModuleResolutionKind(compilerOptions) === ModuleResolutionKind.Bundler) {
42732+
grammarErrorOnNode(node, Diagnostics.Import_assignment_is_not_allowed_when_moduleResolution_is_set_to_bundler_Consider_using_import_Asterisk_as_ns_from_mod_import_a_from_mod_import_d_from_mod_or_another_module_format_instead);
42733+
}
4270242734
}
4270342735
}
4270442736
}
@@ -42921,6 +42953,9 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
4292142953
// system modules does not support export assignment
4292242954
grammarErrorOnNode(node, Diagnostics.Export_assignment_is_not_supported_when_module_flag_is_system);
4292342955
}
42956+
else if (getEmitModuleResolutionKind(compilerOptions) === ModuleResolutionKind.Bundler) {
42957+
grammarErrorOnNode(node, Diagnostics.Export_assignment_cannot_be_used_when_moduleResolution_is_set_to_bundler_Consider_using_export_default_or_another_module_format_instead);
42958+
}
4292442959
}
4292542960
}
4292642961

@@ -44015,7 +44050,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
4401544050
// 4). type A = import("./f/*gotToDefinitionHere*/oo")
4401644051
if ((isExternalModuleImportEqualsDeclaration(node.parent.parent) && getExternalModuleImportEqualsDeclarationExpression(node.parent.parent) === node) ||
4401744052
((node.parent.kind === SyntaxKind.ImportDeclaration || node.parent.kind === SyntaxKind.ExportDeclaration) && (node.parent as ImportDeclaration).moduleSpecifier === node) ||
44018-
((isInJSFile(node) && isRequireCall(node.parent, /*checkArgumentIsStringLiteralLike*/ false)) || isImportCall(node.parent)) ||
44053+
((isInJSFile(node) && getEmitModuleResolutionKind(compilerOptions) !== ModuleResolutionKind.Bundler && isRequireCall(node.parent, /*checkArgumentIsStringLiteralLike*/ false)) || isImportCall(node.parent)) ||
4401944054
(isLiteralTypeNode(node.parent) && isLiteralImportTypeNode(node.parent.parent) && node.parent.parent.argument === node.parent)
4402044055
) {
4402144056
return resolveExternalModuleName(node, node as LiteralExpression, ignoreErrors);
@@ -47321,4 +47356,4 @@ class SymbolTrackerImpl implements SymbolTracker {
4732147356
private onDiagnosticReported() {
4732247357
this.context.reportedDiagnostic = true;
4732347358
}
47324-
}
47359+
}

src/compiler/commandLineParser.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -965,6 +965,7 @@ const commandOptionsWithoutBuild: CommandLineOption[] = [
965965
classic: ModuleResolutionKind.Classic,
966966
node16: ModuleResolutionKind.Node16,
967967
nodenext: ModuleResolutionKind.NodeNext,
968+
bundler: ModuleResolutionKind.Bundler,
968969
})),
969970
affectsModuleResolution: true,
970971
paramType: Diagnostics.STRATEGY,
@@ -1081,6 +1082,41 @@ const commandOptionsWithoutBuild: CommandLineOption[] = [
10811082
category: Diagnostics.Modules,
10821083
description: Diagnostics.List_of_file_name_suffixes_to_search_when_resolving_a_module,
10831084
},
1085+
{
1086+
name: "allowImportingTsExtensions",
1087+
type: "boolean",
1088+
affectsModuleResolution: true,
1089+
category: Diagnostics.Modules,
1090+
description: Diagnostics.Allow_imports_to_include_TypeScript_file_extensions_Requires_moduleResolution_bundler_and_either_noEmit_or_emitDeclarationOnly_to_be_set,
1091+
defaultValueDescription: false,
1092+
},
1093+
{
1094+
name: "resolvePackageJsonExports",
1095+
type: "boolean",
1096+
affectsModuleResolution: true,
1097+
category: Diagnostics.Modules,
1098+
description: Diagnostics.Use_the_package_json_exports_field_when_resolving_package_imports,
1099+
defaultValueDescription: Diagnostics.true_when_moduleResolution_is_node16_nodenext_or_bundler_otherwise_false,
1100+
},
1101+
{
1102+
name: "resolvePackageJsonImports",
1103+
type: "boolean",
1104+
affectsModuleResolution: true,
1105+
category: Diagnostics.Modules,
1106+
description: Diagnostics.Use_the_package_json_imports_field_when_resolving_imports,
1107+
defaultValueDescription: Diagnostics.true_when_moduleResolution_is_node16_nodenext_or_bundler_otherwise_false,
1108+
},
1109+
{
1110+
name: "customConditions",
1111+
type: "list",
1112+
element: {
1113+
name: "condition",
1114+
type: "string",
1115+
},
1116+
affectsModuleResolution: true,
1117+
category: Diagnostics.Modules,
1118+
description: Diagnostics.Conditions_to_set_in_addition_to_the_resolver_specific_defaults_when_resolving_imports,
1119+
},
10841120

10851121
// Source Maps
10861122
{

src/compiler/diagnosticMessages.json

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3563,6 +3563,10 @@
35633563
"category": "Error",
35643564
"code": 2845
35653565
},
3566+
"A declaration file cannot be imported without 'import type'. Did you mean to import an implementation file '{0}' instead?": {
3567+
"category": "Error",
3568+
"code": 2846
3569+
},
35663570

35673571
"Import declaration '{0}' is using private name '{1}'.": {
35683572
"category": "Error",
@@ -4221,6 +4225,26 @@
42214225
"category": "Error",
42224226
"code": 5095
42234227
},
4228+
"Option 'allowImportingTsExtensions' can only be used when 'moduleResolution' is set to 'bundler' and either 'noEmit' or 'emitDeclarationOnly' is set.": {
4229+
"category": "Error",
4230+
"code": 5096
4231+
},
4232+
"An import path can only end with a '{0}' extension when 'allowImportingTsExtensions' is enabled.": {
4233+
"category": "Error",
4234+
"code": 5097
4235+
},
4236+
"Option '{0}' can only be used when 'moduleResolution' is set to 'node16', 'nodenext', or 'bundler'.": {
4237+
"category": "Error",
4238+
"code": 5098
4239+
},
4240+
"Import assignment is not allowed when 'moduleResolution' is set to 'bundler'. Consider using 'import * as ns from \"mod\"', 'import {a} from \"mod\"', 'import d from \"mod\"', or another module format instead.": {
4241+
"category": "Error",
4242+
"code": 5099
4243+
},
4244+
"Export assignment cannot be used when 'moduleResolution' is set to 'bundler'. Consider using 'export default' or another module format instead.": {
4245+
"category": "Error",
4246+
"code": 5100
4247+
},
42244248

42254249
"Generates a sourcemap for each corresponding '.d.ts' file.": {
42264250
"category": "Message",
@@ -4443,10 +4467,6 @@
44434467
"category": "Message",
44444468
"code": 6066
44454469
},
4446-
"Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6).": {
4447-
"category": "Message",
4448-
"code": 6069
4449-
},
44504470
"Initializes a TypeScript project and creates a tsconfig.json file.": {
44514471
"category": "Message",
44524472
"code": 6070
@@ -5430,6 +5450,26 @@
54305450
"category": "Message",
54315451
"code": 6406
54325452
},
5453+
"Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set.": {
5454+
"category": "Message",
5455+
"code": 6407
5456+
},
5457+
"Use the package.json 'exports' field when resolving package imports.": {
5458+
"category": "Message",
5459+
"code": 6408
5460+
},
5461+
"Use the package.json 'imports' field when resolving imports.": {
5462+
"category": "Message",
5463+
"code": 6409
5464+
},
5465+
"Conditions to set in addition to the resolver-specific defaults when resolving imports.": {
5466+
"category": "Message",
5467+
"code": 6410
5468+
},
5469+
"`true` when 'moduleResolution' is 'node16', 'nodenext', or 'bundler'; otherwise `false`.": {
5470+
"category": "Message",
5471+
"code": 6411
5472+
},
54335473

54345474
"The expected type comes from property '{0}' which is declared here on type '{1}'": {
54355475
"category": "Message",

0 commit comments

Comments
 (0)