--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`
This commit is contained in:
Andrew Branch
2022-12-13 13:35:16 -08:00
committed by GitHub
parent ad354c2d75
commit a5dde88dce
135 changed files with 4077 additions and 481 deletions
+3
View File
@@ -84,6 +84,7 @@ import {
getContainingClass,
getEffectiveContainerForJSDocTemplateTag,
getElementOrPropertyAccessName,
getEmitModuleResolutionKind,
getEmitScriptTarget,
getEnclosingBlockScopeContainer,
getErrorSpanForNode,
@@ -235,6 +236,7 @@ import {
ModifierFlags,
ModuleBlock,
ModuleDeclaration,
ModuleResolutionKind,
Mutable,
NamespaceExportDeclaration,
Node,
@@ -3520,6 +3522,7 @@ function createBinder(): (file: SourceFile, options: CompilerOptions) => void {
if (!isBindingPattern(node.name)) {
const possibleVariableDecl = node.kind === SyntaxKind.VariableDeclaration ? node : node.parent.parent;
if (isInJSFile(node) &&
getEmitModuleResolutionKind(options) !== ModuleResolutionKind.Bundler &&
isVariableDeclarationInitializedToBareOrAccessedRequire(possibleVariableDecl) &&
!getJSDocTypeTag(node) &&
!(getCombinedModifierFlags(node) & ModifierFlags.Export)
+55 -20
View File
@@ -139,6 +139,7 @@ import {
ElementFlags,
EmitFlags,
EmitHint,
emitModuleKindIsNonNodeESM,
EmitResolver,
EmitTextWriter,
emptyArray,
@@ -317,6 +318,7 @@ import {
getResolutionModeOverrideForClause,
getResolvedExternalModuleName,
getResolvedModule,
getResolveJsonModule,
getRestParameterElementType,
getRootDeclaration,
getScriptTargetFeatures,
@@ -346,8 +348,8 @@ import {
hasAccessorModifier,
hasAmbientModifier,
hasContextSensitiveParameters,
HasDecorators,
hasDecorators,
HasDecorators,
hasDynamicName,
hasEffectiveModifier,
hasEffectiveModifiers,
@@ -356,8 +358,8 @@ import {
hasExtension,
HasIllegalDecorators,
HasIllegalModifiers,
HasInitializer,
hasInitializer,
HasInitializer,
hasJSDocNodes,
hasJSDocParameterTags,
hasJsonModuleEmitEnabled,
@@ -458,6 +460,7 @@ import {
isConstructorTypeNode,
isConstTypeReference,
isDeclaration,
isDeclarationFileName,
isDeclarationName,
isDeclarationReadonly,
isDecorator,
@@ -897,6 +900,7 @@ import {
setTextRangePosEnd,
setValueDeclaration,
ShorthandPropertyAssignment,
shouldAllowImportingTsExtension,
shouldPreserveConstEnums,
Signature,
SignatureDeclaration,
@@ -4543,6 +4547,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
if (
namespace.valueDeclaration &&
isInJSFile(namespace.valueDeclaration) &&
getEmitModuleResolutionKind(compilerOptions) !== ModuleResolutionKind.Bundler &&
isVariableDeclaration(namespace.valueDeclaration) &&
namespace.valueDeclaration.initializer &&
isCommonJsRequire(namespace.valueDeclaration.initializer)
@@ -4727,6 +4732,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
(isModuleDeclaration(location) ? location : location.parent && isModuleDeclaration(location.parent) && location.parent.name === location ? location.parent : undefined)?.name ||
(isLiteralImportTypeNode(location) ? location : undefined)?.argument.literal;
const mode = contextSpecifier && isStringLiteralLike(contextSpecifier) ? getModeForUsageLocation(currentSourceFile, contextSpecifier) : currentSourceFile.impliedNodeFormat;
const moduleResolutionKind = getEmitModuleResolutionKind(compilerOptions);
const resolvedModule = getResolvedModule(currentSourceFile, moduleReference, mode);
const resolutionDiagnostic = resolvedModule && getResolutionDiagnostic(compilerOptions, resolvedModule);
const sourceFile = resolvedModule
@@ -4737,11 +4743,28 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
if (resolutionDiagnostic) {
error(errorNode, resolutionDiagnostic, moduleReference, resolvedModule.resolvedFileName);
}
if (resolvedModule.resolvedUsingTsExtension && isDeclarationFileName(moduleReference)) {
const importOrExport =
findAncestor(location, isImportDeclaration)?.importClause ||
findAncestor(location, or(isImportEqualsDeclaration, isExportDeclaration));
if (importOrExport && !importOrExport.isTypeOnly || findAncestor(location, isImportCall)) {
error(
errorNode,
Diagnostics.A_declaration_file_cannot_be_imported_without_import_type_Did_you_mean_to_import_an_implementation_file_0_instead,
getSuggestedImportSource(Debug.checkDefined(tryExtractTSExtension(moduleReference))));
}
}
else if (resolvedModule.resolvedUsingTsExtension && !shouldAllowImportingTsExtension(compilerOptions, currentSourceFile.fileName)) {
const tsExtension = Debug.checkDefined(tryExtractTSExtension(moduleReference));
error(errorNode, Diagnostics.An_import_path_can_only_end_with_a_0_extension_when_allowImportingTsExtensions_is_enabled, tsExtension);
}
if (sourceFile.symbol) {
if (resolvedModule.isExternalLibraryImport && !resolutionExtensionIsTSOrJson(resolvedModule.extension)) {
errorOnImplicitAnyModule(/*isError*/ false, errorNode, resolvedModule, moduleReference);
}
if (getEmitModuleResolutionKind(compilerOptions) === ModuleResolutionKind.Node16 || getEmitModuleResolutionKind(compilerOptions) === ModuleResolutionKind.NodeNext) {
if (moduleResolutionKind === ModuleResolutionKind.Node16 || moduleResolutionKind === ModuleResolutionKind.NodeNext) {
const isSyncImport = (currentSourceFile.impliedNodeFormat === ModuleKind.CommonJS && !findAncestor(location, isImportCall)) || !!findAncestor(location, isImportEqualsDeclaration);
const overrideClauseHost = findAncestor(location, l => isImportTypeNode(l) || isExportDeclaration(l) || isImportDeclaration(l)) as ImportTypeNode | ImportDeclaration | ExportDeclaration | undefined;
const overrideClause = overrideClauseHost && isImportTypeNode(overrideClauseHost) ? overrideClauseHost.assertions?.assertClause : overrideClauseHost?.assertClause;
@@ -4849,25 +4872,14 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
else {
const tsExtension = tryExtractTSExtension(moduleReference);
const isExtensionlessRelativePathImport = pathIsRelative(moduleReference) && !hasExtension(moduleReference);
const moduleResolutionKind = getEmitModuleResolutionKind(compilerOptions);
const resolutionIsNode16OrNext = moduleResolutionKind === ModuleResolutionKind.Node16 ||
moduleResolutionKind === ModuleResolutionKind.NodeNext;
if (tsExtension) {
const diag = Diagnostics.An_import_path_cannot_end_with_a_0_extension_Consider_importing_1_instead;
const importSourceWithoutExtension = removeExtension(moduleReference, tsExtension);
let replacedImportSource = importSourceWithoutExtension;
/**
* Direct users to import source with .js extension if outputting an ES module.
* @see https://github.com/microsoft/TypeScript/issues/42151
*/
if (moduleKind >= ModuleKind.ES2015) {
replacedImportSource += tsExtension === Extension.Mts ? ".mjs" : tsExtension === Extension.Cts ? ".cjs" : ".js";
}
error(errorNode, diag, tsExtension, replacedImportSource);
errorOnTSExtensionImport(tsExtension);
}
else if (!compilerOptions.resolveJsonModule &&
else if (!getResolveJsonModule(compilerOptions) &&
fileExtensionIs(moduleReference, Extension.Json) &&
getEmitModuleResolutionKind(compilerOptions) !== ModuleResolutionKind.Classic &&
moduleResolutionKind !== ModuleResolutionKind.Classic &&
hasJsonModuleEmitEnabled(compilerOptions)) {
error(errorNode, Diagnostics.Cannot_find_module_0_Consider_using_resolveJsonModule_to_import_module_with_json_extension, moduleReference);
}
@@ -4889,6 +4901,23 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}
}
return undefined;
function errorOnTSExtensionImport(tsExtension: string) {
const diag = Diagnostics.An_import_path_cannot_end_with_a_0_extension_Consider_importing_1_instead;
error(errorNode, diag, tsExtension, getSuggestedImportSource(tsExtension));
}
function getSuggestedImportSource(tsExtension: string) {
const importSourceWithoutExtension = removeExtension(moduleReference, tsExtension);
/**
* Direct users to import source with .js extension if outputting an ES module.
* @see https://github.com/microsoft/TypeScript/issues/42151
*/
if (emitModuleKindIsNonNodeESM(moduleKind) || mode === ModuleKind.ESNext) {
return importSourceWithoutExtension + (tsExtension === Extension.Mts ? ".mjs" : tsExtension === Extension.Cts ? ".cjs" : ".js");
}
return importSourceWithoutExtension;
}
}
function errorOnImplicitAnyModule(isError: boolean, errorNode: Node, { packageId, resolvedFileName }: ResolvedModuleFull, moduleReference: string): void {
@@ -33297,7 +33326,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}
// In JavaScript files, calls to any identifier 'require' are treated as external module imports
if (isInJSFile(node) && isCommonJsRequire(node)) {
if (isInJSFile(node) && getEmitModuleResolutionKind(compilerOptions) !== ModuleResolutionKind.Bundler && isCommonJsRequire(node)) {
return resolveExternalModuleTypeByLiteral(node.arguments![0] as StringLiteral);
}
@@ -42699,6 +42728,9 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
// Import equals declaration is deprecated in es6 or above
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);
}
else if (getEmitModuleResolutionKind(compilerOptions) === ModuleResolutionKind.Bundler) {
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);
}
}
}
}
@@ -42921,6 +42953,9 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
// system modules does not support export assignment
grammarErrorOnNode(node, Diagnostics.Export_assignment_is_not_supported_when_module_flag_is_system);
}
else if (getEmitModuleResolutionKind(compilerOptions) === ModuleResolutionKind.Bundler) {
grammarErrorOnNode(node, Diagnostics.Export_assignment_cannot_be_used_when_moduleResolution_is_set_to_bundler_Consider_using_export_default_or_another_module_format_instead);
}
}
}
@@ -44015,7 +44050,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
// 4). type A = import("./f/*gotToDefinitionHere*/oo")
if ((isExternalModuleImportEqualsDeclaration(node.parent.parent) && getExternalModuleImportEqualsDeclarationExpression(node.parent.parent) === node) ||
((node.parent.kind === SyntaxKind.ImportDeclaration || node.parent.kind === SyntaxKind.ExportDeclaration) && (node.parent as ImportDeclaration).moduleSpecifier === node) ||
((isInJSFile(node) && isRequireCall(node.parent, /*checkArgumentIsStringLiteralLike*/ false)) || isImportCall(node.parent)) ||
((isInJSFile(node) && getEmitModuleResolutionKind(compilerOptions) !== ModuleResolutionKind.Bundler && isRequireCall(node.parent, /*checkArgumentIsStringLiteralLike*/ false)) || isImportCall(node.parent)) ||
(isLiteralTypeNode(node.parent) && isLiteralImportTypeNode(node.parent.parent) && node.parent.parent.argument === node.parent)
) {
return resolveExternalModuleName(node, node as LiteralExpression, ignoreErrors);
@@ -47321,4 +47356,4 @@ class SymbolTrackerImpl implements SymbolTracker {
private onDiagnosticReported() {
this.context.reportedDiagnostic = true;
}
}
}
+36
View File
@@ -965,6 +965,7 @@ const commandOptionsWithoutBuild: CommandLineOption[] = [
classic: ModuleResolutionKind.Classic,
node16: ModuleResolutionKind.Node16,
nodenext: ModuleResolutionKind.NodeNext,
bundler: ModuleResolutionKind.Bundler,
})),
affectsModuleResolution: true,
paramType: Diagnostics.STRATEGY,
@@ -1081,6 +1082,41 @@ const commandOptionsWithoutBuild: CommandLineOption[] = [
category: Diagnostics.Modules,
description: Diagnostics.List_of_file_name_suffixes_to_search_when_resolving_a_module,
},
{
name: "allowImportingTsExtensions",
type: "boolean",
affectsModuleResolution: true,
category: Diagnostics.Modules,
description: Diagnostics.Allow_imports_to_include_TypeScript_file_extensions_Requires_moduleResolution_bundler_and_either_noEmit_or_emitDeclarationOnly_to_be_set,
defaultValueDescription: false,
},
{
name: "resolvePackageJsonExports",
type: "boolean",
affectsModuleResolution: true,
category: Diagnostics.Modules,
description: Diagnostics.Use_the_package_json_exports_field_when_resolving_package_imports,
defaultValueDescription: Diagnostics.true_when_moduleResolution_is_node16_nodenext_or_bundler_otherwise_false,
},
{
name: "resolvePackageJsonImports",
type: "boolean",
affectsModuleResolution: true,
category: Diagnostics.Modules,
description: Diagnostics.Use_the_package_json_imports_field_when_resolving_imports,
defaultValueDescription: Diagnostics.true_when_moduleResolution_is_node16_nodenext_or_bundler_otherwise_false,
},
{
name: "customConditions",
type: "list",
element: {
name: "condition",
type: "string",
},
affectsModuleResolution: true,
category: Diagnostics.Modules,
description: Diagnostics.Conditions_to_set_in_addition_to_the_resolver_specific_defaults_when_resolving_imports,
},
// Source Maps
{
+44 -4
View File
@@ -3563,6 +3563,10 @@
"category": "Error",
"code": 2845
},
"A declaration file cannot be imported without 'import type'. Did you mean to import an implementation file '{0}' instead?": {
"category": "Error",
"code": 2846
},
"Import declaration '{0}' is using private name '{1}'.": {
"category": "Error",
@@ -4221,6 +4225,26 @@
"category": "Error",
"code": 5095
},
"Option 'allowImportingTsExtensions' can only be used when 'moduleResolution' is set to 'bundler' and either 'noEmit' or 'emitDeclarationOnly' is set.": {
"category": "Error",
"code": 5096
},
"An import path can only end with a '{0}' extension when 'allowImportingTsExtensions' is enabled.": {
"category": "Error",
"code": 5097
},
"Option '{0}' can only be used when 'moduleResolution' is set to 'node16', 'nodenext', or 'bundler'.": {
"category": "Error",
"code": 5098
},
"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.": {
"category": "Error",
"code": 5099
},
"Export assignment cannot be used when 'moduleResolution' is set to 'bundler'. Consider using 'export default' or another module format instead.": {
"category": "Error",
"code": 5100
},
"Generates a sourcemap for each corresponding '.d.ts' file.": {
"category": "Message",
@@ -4443,10 +4467,6 @@
"category": "Message",
"code": 6066
},
"Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6).": {
"category": "Message",
"code": 6069
},
"Initializes a TypeScript project and creates a tsconfig.json file.": {
"category": "Message",
"code": 6070
@@ -5430,6 +5450,26 @@
"category": "Message",
"code": 6406
},
"Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set.": {
"category": "Message",
"code": 6407
},
"Use the package.json 'exports' field when resolving package imports.": {
"category": "Message",
"code": 6408
},
"Use the package.json 'imports' field when resolving imports.": {
"category": "Message",
"code": 6409
},
"Conditions to set in addition to the resolver-specific defaults when resolving imports.": {
"category": "Message",
"code": 6410
},
"`true` when 'moduleResolution' is 'node16', 'nodenext', or 'bundler'; otherwise `false`.": {
"category": "Message",
"code": 6411
},
"The expected type comes from property '{0}' which is declared here on type '{1}'": {
"category": "Message",
+140 -40
View File
@@ -2,6 +2,7 @@ import {
append,
appendIfUnique,
arrayFrom,
arrayIsEqualTo,
changeAnyExtension,
CharacterCodes,
combinePaths,
@@ -9,10 +10,12 @@ import {
comparePaths,
Comparison,
CompilerOptions,
concatenate,
contains,
containsPath,
createCompilerDiagnostic,
Debug,
deduplicate,
Diagnostic,
DiagnosticMessage,
DiagnosticReporter,
@@ -47,12 +50,14 @@ import {
getPathsBasePath,
getPossibleOriginalInputExtensionForExtension,
getRelativePathFromDirectory,
getResolveJsonModule,
getRootLength,
hasJSFileExtension,
hasProperty,
hasTrailingDirectorySeparator,
hostGetCanonicalFileName,
isArray,
isDeclarationFileName,
isExternalModuleNameRelative,
isRootedDiskPath,
isString,
@@ -94,6 +99,7 @@ import {
startsWith,
stringContains,
supportedDeclarationExtensions,
supportedTSExtensionsFlat,
supportedTSImplementationExtensions,
toPath,
tryExtractTSExtension,
@@ -128,7 +134,7 @@ function withPackageId(packageInfo: PackageJsonInfo | undefined, r: PathAndExten
};
}
}
return r && { path: r.path, extension: r.ext, packageId };
return r && { path: r.path, extension: r.ext, packageId, resolvedUsingTsExtension: r.resolvedUsingTsExtension };
}
function noPackageId(r: PathAndExtension | undefined): Resolved | undefined {
@@ -138,7 +144,7 @@ function noPackageId(r: PathAndExtension | undefined): Resolved | undefined {
function removeIgnoredPackageId(r: Resolved | undefined): PathAndExtension | undefined {
if (r) {
Debug.assert(r.packageId === undefined);
return { path: r.path, ext: r.extension };
return { path: r.path, ext: r.extension, resolvedUsingTsExtension: r.resolvedUsingTsExtension };
}
}
@@ -157,6 +163,7 @@ interface Resolved {
* Note: This is a file name with preserved original casing, not a normalized `Path`.
*/
originalPath?: string | true;
resolvedUsingTsExtension: boolean | undefined;
}
/** Result of trying to resolve a module at a file. Needs to have 'packageId' added later. */
@@ -164,6 +171,7 @@ interface PathAndExtension {
path: string;
// (Use a different name than `extension` to make sure Resolved isn't assignable to PathAndExtension.)
ext: Extension;
resolvedUsingTsExtension: boolean | undefined;
}
/**
@@ -215,7 +223,14 @@ function createResolvedModuleWithFailedLookupLocations(
return resultFromCache;
}
return {
resolvedModule: resolved && { resolvedFileName: resolved.path, originalPath: resolved.originalPath === true ? undefined : resolved.originalPath, extension: resolved.extension, isExternalLibraryImport, packageId: resolved.packageId },
resolvedModule: resolved && {
resolvedFileName: resolved.path,
originalPath: resolved.originalPath === true ? undefined : resolved.originalPath,
extension: resolved.extension,
isExternalLibraryImport,
packageId: resolved.packageId,
resolvedUsingTsExtension: !!resolved.resolvedUsingTsExtension,
},
failedLookupLocations: initializeResolutionField(failedLookupLocations),
affectingLocations: initializeResolutionField(affectingLocations),
resolutionDiagnostics: initializeResolutionField(diagnostics),
@@ -488,7 +503,7 @@ export function resolveTypeReferenceDirective(typeReferenceDirectiveName: string
const failedLookupLocations: string[] = [];
const affectingLocations: string[] = [];
let features = getDefaultNodeResolutionFeatures(options);
let features = getNodeResolutionFeatures(options);
// Unlike `import` statements, whose mode-calculating APIs are all guaranteed to return `undefined` if we're in an un-mode-ed module resolution
// setting, type references will return their target mode regardless of options because of how the parser works, so we guard against the mode being
// set in a non-modal module resolution setting here. Do note that our behavior is not particularly well defined when these mode-overriding imports
@@ -499,7 +514,7 @@ export function resolveTypeReferenceDirective(typeReferenceDirectiveName: string
if (resolutionMode === ModuleKind.ESNext && (getEmitModuleResolutionKind(options) === ModuleResolutionKind.Node16 || getEmitModuleResolutionKind(options) === ModuleResolutionKind.NodeNext)) {
features |= NodeResolutionFeatures.EsmMode;
}
const conditions = features & NodeResolutionFeatures.Exports ? features & NodeResolutionFeatures.EsmMode ? ["node", "import", "types"] : ["node", "require", "types"] : [];
const conditions = features & NodeResolutionFeatures.Exports ? getConditions(options, !!(features & NodeResolutionFeatures.EsmMode)) : [];
const diagnostics: Diagnostic[] = [];
const moduleResolutionState: ModuleResolutionState = {
compilerOptions: options,
@@ -614,10 +629,44 @@ export function resolveTypeReferenceDirective(typeReferenceDirectiveName: string
}
}
function getDefaultNodeResolutionFeatures(options: CompilerOptions) {
return getEmitModuleResolutionKind(options) === ModuleResolutionKind.Node16 ? NodeResolutionFeatures.Node16Default :
getEmitModuleResolutionKind(options) === ModuleResolutionKind.NodeNext ? NodeResolutionFeatures.NodeNextDefault :
NodeResolutionFeatures.None;
function getNodeResolutionFeatures(options: CompilerOptions) {
let features = NodeResolutionFeatures.None;
switch (getEmitModuleResolutionKind(options)) {
case ModuleResolutionKind.Node16:
features = NodeResolutionFeatures.Node16Default;
break;
case ModuleResolutionKind.NodeNext:
features = NodeResolutionFeatures.NodeNextDefault;
break;
case ModuleResolutionKind.Bundler:
features = NodeResolutionFeatures.BundlerDefault;
break;
}
if (options.resolvePackageJsonExports) {
features |= NodeResolutionFeatures.Exports;
}
else if (options.resolvePackageJsonExports === false) {
features &= ~NodeResolutionFeatures.Exports;
}
if (options.resolvePackageJsonImports) {
features |= NodeResolutionFeatures.Imports;
}
else if (options.resolvePackageJsonImports === false) {
features &= ~NodeResolutionFeatures.Imports;
}
return features;
}
function getConditions(options: CompilerOptions, esmMode?: boolean) {
// conditions are only used by the node16/nodenext/bundler resolvers - there's no priority order in the list,
// it's essentially a set (priority is determined by object insertion order in the object we look at).
const conditions = esmMode || getEmitModuleResolutionKind(options) === ModuleResolutionKind.Bundler
? ["node", "import"]
: ["node", "require"];
if (!options.noDtsResolution) {
conditions.push("types");
}
return concatenate(conditions, options.customConditions);
}
/**
@@ -1235,6 +1284,9 @@ export function resolveModuleName(moduleName: string, containingFile: string, co
case ModuleResolutionKind.Classic:
result = classicNameResolver(moduleName, containingFile, compilerOptions, host, cache, redirectedReference);
break;
case ModuleResolutionKind.Bundler:
result = bundlerModuleNameResolver(moduleName, containingFile, compilerOptions, host, cache, redirectedReference);
break;
default:
return Debug.fail(`Unexpected moduleResolution: ${moduleResolution}`);
}
@@ -1489,6 +1541,8 @@ export enum NodeResolutionFeatures {
NodeNextDefault = AllFeatures,
BundlerDefault = Imports | SelfName | Exports | ExportsPatternTrailers,
EsmMode = 1 << 5,
}
@@ -1528,7 +1582,7 @@ function nodeNextModuleNameResolverWorker(features: NodeResolutionFeatures, modu
// es module file or cjs-like input file, use a variant of the legacy cjs resolver that supports the selected modern features
const esmMode = resolutionMode === ModuleKind.ESNext ? NodeResolutionFeatures.EsmMode : 0;
let extensions = compilerOptions.noDtsResolution ? Extensions.ImplementationFiles : Extensions.TypeScript | Extensions.JavaScript | Extensions.Declaration;
if (compilerOptions.resolveJsonModule) {
if (getResolveJsonModule(compilerOptions)) {
extensions |= Extensions.Json;
}
return nodeModuleNameResolverWorker(features | esmMode, moduleName, containingDirectory, compilerOptions, host, cache, extensions, /*isConfigLookup*/ false, redirectedReference);
@@ -1547,6 +1601,15 @@ function tryResolveJSModuleWorker(moduleName: string, initialDir: string, host:
/*redirectedReferences*/ undefined);
}
export function bundlerModuleNameResolver(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost, cache?: ModuleResolutionCache, redirectedReference?: ResolvedProjectReference): ResolvedModuleWithFailedLookupLocations {
const containingDirectory = getDirectoryPath(containingFile);
let extensions = compilerOptions.noDtsResolution ? Extensions.ImplementationFiles : Extensions.TypeScript | Extensions.JavaScript | Extensions.Declaration;
if (getResolveJsonModule(compilerOptions)) {
extensions |= Extensions.Json;
}
return nodeModuleNameResolverWorker(getNodeResolutionFeatures(compilerOptions), moduleName, containingDirectory, compilerOptions, host, cache, extensions, /*isConfigLookup*/ false, redirectedReference);
}
export function nodeModuleNameResolver(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost, cache?: ModuleResolutionCache, redirectedReference?: ResolvedProjectReference): ResolvedModuleWithFailedLookupLocations;
/** @internal */ export function nodeModuleNameResolver(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost, cache?: ModuleResolutionCache, redirectedReference?: ResolvedProjectReference, lookupConfig?: boolean): ResolvedModuleWithFailedLookupLocations; // eslint-disable-line @typescript-eslint/unified-signatures
export function nodeModuleNameResolver(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost, cache?: ModuleResolutionCache, redirectedReference?: ResolvedProjectReference, isConfigLookup?: boolean): ResolvedModuleWithFailedLookupLocations {
@@ -1556,10 +1619,10 @@ export function nodeModuleNameResolver(moduleName: string, containingFile: strin
}
else if (compilerOptions.noDtsResolution) {
extensions = Extensions.ImplementationFiles;
if (compilerOptions.resolveJsonModule) extensions |= Extensions.Json;
if (getResolveJsonModule(compilerOptions)) extensions |= Extensions.Json;
}
else {
extensions = compilerOptions.resolveJsonModule
extensions = getResolveJsonModule(compilerOptions)
? Extensions.TypeScript | Extensions.JavaScript | Extensions.Declaration | Extensions.Json
: Extensions.TypeScript | Extensions.JavaScript | Extensions.Declaration;
}
@@ -1571,12 +1634,7 @@ function nodeModuleNameResolverWorker(features: NodeResolutionFeatures, moduleNa
const failedLookupLocations: string[] = [];
const affectingLocations: string[] = [];
// conditions are only used by the node16/nodenext resolver - there's no priority order in the list,
//it's essentially a set (priority is determined by object insertion order in the object we look at).
const conditions = features & NodeResolutionFeatures.EsmMode ? ["node", "import", "types"] : ["node", "require", "types"];
if (compilerOptions.noDtsResolution) {
conditions.pop();
}
const conditions = getConditions(compilerOptions, !!(features & NodeResolutionFeatures.EsmMode));
const diagnostics: Diagnostic[] = [];
const state: ModuleResolutionState = {
@@ -1720,7 +1778,7 @@ function nodeLoadModuleByRelativeName(extensions: Extensions, candidate: string,
}
}
// esm mode relative imports shouldn't do any directory lookups (either inside `package.json`
// files or implicit `index.js`es). This is a notable depature from cjs norms, where `./foo/pkg`
// files or implicit `index.js`es). This is a notable departure from cjs norms, where `./foo/pkg`
// could have been redirected by `./foo/pkg/package.json` to an arbitrary location!
if (!(state.features & NodeResolutionFeatures.EsmMode)) {
return loadNodeModuleFromDirectory(extensions, candidate, onlyRecordFailures, state, considerPackageJson);
@@ -1795,7 +1853,12 @@ function loadModuleFromFile(extensions: Extensions, candidate: string, onlyRecor
function loadModuleFromFileNoImplicitExtensions(extensions: Extensions, candidate: string, onlyRecordFailures: boolean, state: ModuleResolutionState): PathAndExtension | undefined {
// If that didn't work, try stripping a ".js" or ".jsx" extension and replacing it with a TypeScript one;
// e.g. "./foo.js" can be matched by "./foo.ts" or "./foo.d.ts"
if (hasJSFileExtension(candidate) || extensions & Extensions.Json && fileExtensionIs(candidate, Extension.Json)) {
if (hasJSFileExtension(candidate) ||
extensions & Extensions.Json && fileExtensionIs(candidate, Extension.Json) ||
extensions & (Extensions.TypeScript | Extensions.Declaration)
&& moduleResolutionSupportsResolvingTsExtensions(state.compilerOptions)
&& fileExtensionIsOneOf(candidate, supportedTSExtensionsFlat)
) {
const extensionless = removeFileExtension(candidate);
const extension = candidate.substring(extensionless.length);
if (state.traceEnabled) {
@@ -1810,7 +1873,7 @@ function loadJSOrExactTSFileName(extensions: Extensions, candidate: string, only
extensions & Extensions.Declaration && fileExtensionIsOneOf(candidate, supportedDeclarationExtensions)
) {
const result = tryFile(candidate, onlyRecordFailures, state);
return result !== undefined ? { path: candidate, ext: tryExtractTSExtension(candidate) as Extension } : undefined;
return result !== undefined ? { path: candidate, ext: tryExtractTSExtension(candidate) as Extension, resolvedUsingTsExtension: undefined } : undefined;
}
return loadModuleFromFileNoImplicitExtensions(extensions, candidate, onlyRecordFailures, state);
@@ -1854,6 +1917,13 @@ function tryAddingExtensions(candidate: string, extensions: Extensions, original
if (result) return result;
}
return undefined;
case Extension.Ts:
case Extension.Tsx:
case Extension.Dts:
if (moduleResolutionSupportsResolvingTsExtensions(state.compilerOptions) && extensionIsOk(extensions, originalExtension)) {
return tryExtension(originalExtension, /*resolvedUsingTsExtension*/ true);
}
// falls through
default:
return extensions & Extensions.TypeScript && (tryExtension(Extension.Ts) || tryExtension(Extension.Tsx))
|| extensions & Extensions.Declaration && tryExtension(Extension.Dts)
@@ -1863,9 +1933,9 @@ function tryAddingExtensions(candidate: string, extensions: Extensions, original
}
function tryExtension(ext: Extension): PathAndExtension | undefined {
function tryExtension(ext: Extension, resolvedUsingTsExtension?: boolean): PathAndExtension | undefined {
const path = tryFile(candidate + ext, onlyRecordFailures, state);
return path === undefined ? undefined : { path, ext };
return path === undefined ? undefined : { path, ext, resolvedUsingTsExtension };
}
}
@@ -1921,26 +1991,30 @@ export function getEntrypointsFromPackageJsonInfo(
let entrypoints: string[] | undefined;
const extensions = Extensions.TypeScript | Extensions.Declaration | (resolveJs ? Extensions.JavaScript : 0);
const features = getDefaultNodeResolutionFeatures(options);
const requireState = getTemporaryModuleResolutionState(cache?.getPackageJsonInfoCache(), host, options);
requireState.conditions = ["node", "require", "types"];
requireState.requestContainingDirectory = packageJsonInfo.packageDirectory;
const requireResolution = loadNodeModuleFromDirectoryWorker(
const features = getNodeResolutionFeatures(options);
const loadPackageJsonMainState = getTemporaryModuleResolutionState(cache?.getPackageJsonInfoCache(), host, options);
loadPackageJsonMainState.conditions = getConditions(options);
loadPackageJsonMainState.requestContainingDirectory = packageJsonInfo.packageDirectory;
const mainResolution = loadNodeModuleFromDirectoryWorker(
extensions,
packageJsonInfo.packageDirectory,
/*onlyRecordFailures*/ false,
requireState,
loadPackageJsonMainState,
packageJsonInfo.contents.packageJsonContent,
getVersionPathsOfPackageJsonInfo(packageJsonInfo, requireState));
entrypoints = append(entrypoints, requireResolution?.path);
getVersionPathsOfPackageJsonInfo(packageJsonInfo, loadPackageJsonMainState));
entrypoints = append(entrypoints, mainResolution?.path);
if (features & NodeResolutionFeatures.Exports && packageJsonInfo.contents.packageJsonContent.exports) {
for (const conditions of [["node", "import", "types"], ["node", "require", "types"]]) {
const exportState = { ...requireState, failedLookupLocations: [], conditions };
const conditionSets = deduplicate(
[getConditions(options, /*esmMode*/ true), getConditions(options, /*esmMode*/ false)],
arrayIsEqualTo
);
for (const conditions of conditionSets) {
const loadPackageJsonExportsState = { ...loadPackageJsonMainState, failedLookupLocations: [], conditions };
const exportResolutions = loadEntrypointsFromExportMap(
packageJsonInfo,
packageJsonInfo.contents.packageJsonContent.exports,
exportState,
loadPackageJsonExportsState,
extensions);
if (exportResolutions) {
for (const resolution of exportResolutions) {
@@ -2046,7 +2120,7 @@ export interface PackageJsonInfoContents {
*
* @internal
*/
export function getPackageScopeForPath(fileName: string, state: ModuleResolutionState): PackageJsonInfo | undefined {
export function getPackageScopeForPath(fileName: string, state: ModuleResolutionState): PackageJsonInfo | undefined {
const parts = getPathComponents(fileName);
parts.pop();
while (parts.length > 0) {
@@ -2179,9 +2253,9 @@ function loadNodeModuleFromDirectoryWorker(extensions: Extensions, candidate: st
}
/** Resolve from an arbitrarily specified file. Return `undefined` if it has an unsupported extension. */
function resolvedIfExtensionMatches(extensions: Extensions, path: string): PathAndExtension | undefined {
function resolvedIfExtensionMatches(extensions: Extensions, path: string, resolvedUsingTsExtension?: boolean): PathAndExtension | undefined {
const ext = tryGetExtensionFromPath(path);
return ext !== undefined && extensionIsOk(extensions, ext) ? { path, ext } : undefined;
return ext !== undefined && extensionIsOk(extensions, ext) ? { path, ext, resolvedUsingTsExtension } : undefined;
}
/** True if `extension` is one of the supported `extensions`. */
@@ -2372,7 +2446,13 @@ function getLoadModuleFromTargetImportOrExport(extensions: Extensions, state: Mo
traceIfEnabled(state, Diagnostics.Using_0_subpath_1_with_target_2, "imports", key, combinedLookup);
traceIfEnabled(state, Diagnostics.Resolving_module_0_from_1, combinedLookup, scope.packageDirectory + "/");
const result = nodeModuleNameResolverWorker(state.features, combinedLookup, scope.packageDirectory + "/", state.compilerOptions, state.host, cache, extensions, /*isConfigLookup*/ false, redirectedReference);
return toSearchResult(result.resolvedModule ? { path: result.resolvedModule.resolvedFileName, extension: result.resolvedModule.extension, packageId: result.resolvedModule.packageId, originalPath: result.resolvedModule.originalPath } : undefined);
return toSearchResult(result.resolvedModule ? {
path: result.resolvedModule.resolvedFileName,
extension: result.resolvedModule.extension,
packageId: result.resolvedModule.packageId,
originalPath: result.resolvedModule.originalPath,
resolvedUsingTsExtension: result.resolvedModule.resolvedUsingTsExtension
} : undefined);
}
if (state.traceEnabled) {
trace(state.host, Diagnostics.package_json_scope_0_has_invalid_type_for_target_of_specifier_1, scope.packageDirectory, moduleName);
@@ -2753,7 +2833,7 @@ function tryLoadModuleUsingPaths(extensions: Extensions, moduleName: string, bas
if (extension !== undefined) {
const path = tryFile(candidate, onlyRecordFailures, state);
if (path !== undefined) {
return noPackageId({ path, ext: extension });
return noPackageId({ path, ext: extension, resolvedUsingTsExtension: undefined });
}
}
return loader(extensions, candidate, onlyRecordFailures || !directoryProbablyExists(getDirectoryPath(candidate), state.host), state);
@@ -2813,7 +2893,15 @@ function tryFindNonRelativeModuleNameInCache(cache: NonRelativeModuleNameResolut
trace(state.host, Diagnostics.Resolution_for_module_0_was_found_in_cache_from_location_1, moduleName, containingDirectory);
}
state.resultFromCache = result;
return { value: result.resolvedModule && { path: result.resolvedModule.resolvedFileName, originalPath: result.resolvedModule.originalPath || true, extension: result.resolvedModule.extension, packageId: result.resolvedModule.packageId } };
return {
value: result.resolvedModule && {
path: result.resolvedModule.resolvedFileName,
originalPath: result.resolvedModule.originalPath || true,
extension: result.resolvedModule.extension,
packageId: result.resolvedModule.packageId,
resolvedUsingTsExtension: result.resolvedModule.resolvedUsingTsExtension
}
};
}
}
@@ -2880,6 +2968,18 @@ export function classicNameResolver(moduleName: string, containingFile: string,
}
}
export function moduleResolutionSupportsResolvingTsExtensions(compilerOptions: CompilerOptions) {
return getEmitModuleResolutionKind(compilerOptions) === ModuleResolutionKind.Bundler;
}
// Program errors validate that `noEmit` or `emitDeclarationOnly` is also set,
// so this function doesn't check them to avoid propagating errors.
export function shouldAllowImportingTsExtension(compilerOptions: CompilerOptions, fromFileName?: string) {
return moduleResolutionSupportsResolvingTsExtensions(compilerOptions) && (
!!compilerOptions.allowImportingTsExtensions ||
fromFileName && isDeclarationFileName(fromFileName));
}
/**
* A host may load a module from a global cache of typings.
* This is the minumum code needed to expose that functionality; the rest is in the host.
+100 -93
View File
@@ -34,9 +34,9 @@ import {
GetCanonicalFileName,
getDirectoryPath,
getEmitModuleResolutionKind,
getImpliedNodeFormatForFile,
getModeForResolutionAtIndex,
getModuleNameStringLiteralAt,
getModuleSpecifierEndingPreference,
getNodeModulePathParts,
getNormalizedAbsolutePath,
getOwnKeys,
@@ -54,6 +54,7 @@ import {
Identifier,
isAmbientModule,
isApplicableVersionedTypesKey,
isDeclarationFileName,
isExternalModuleAugmentation,
isExternalModuleNameRelative,
isModuleBlock,
@@ -71,9 +72,9 @@ import {
ModuleDeclaration,
ModuleKind,
ModulePath,
ModuleResolutionHost,
ModuleResolutionKind,
ModuleSpecifierCache,
ModuleSpecifierEnding,
ModuleSpecifierOptions,
ModuleSpecifierResolutionHost,
NodeFlags,
@@ -89,6 +90,7 @@ import {
ResolutionMode,
resolvePath,
ScriptKind,
shouldAllowImportingTsExtension,
some,
SourceFile,
startsWith,
@@ -107,63 +109,65 @@ import {
// Used by importFixes, getEditsForFileRename, and declaration emit to synthesize import module specifiers.
const enum RelativePreference { Relative, NonRelative, Shortest, ExternalNonRelative }
// See UserPreferences#importPathEnding
const enum Ending { Minimal, Index, JsExtension }
// Processed preferences
interface Preferences {
readonly relativePreference: RelativePreference;
readonly ending: Ending;
/**
* @param syntaxImpliedNodeFormat Used when the import syntax implies ESM or CJS irrespective of the mode of the file.
*/
getAllowedEndingsInPreferredOrder(syntaxImpliedNodeFormat?: SourceFile["impliedNodeFormat"]): ModuleSpecifierEnding[];
}
function getPreferences(host: ModuleSpecifierResolutionHost, { importModuleSpecifierPreference, importModuleSpecifierEnding }: UserPreferences, compilerOptions: CompilerOptions, importingSourceFile: SourceFile): Preferences {
function getPreferences(
{ importModuleSpecifierPreference, importModuleSpecifierEnding }: UserPreferences,
compilerOptions: CompilerOptions,
importingSourceFile: SourceFile,
oldImportSpecifier?: string,
): Preferences {
const preferredEnding = getPreferredEnding();
return {
relativePreference:
oldImportSpecifier !== undefined ? (isExternalModuleNameRelative(oldImportSpecifier) ?
RelativePreference.Relative :
RelativePreference.NonRelative) :
importModuleSpecifierPreference === "relative" ? RelativePreference.Relative :
importModuleSpecifierPreference === "non-relative" ? RelativePreference.NonRelative :
importModuleSpecifierPreference === "project-relative" ? RelativePreference.ExternalNonRelative :
RelativePreference.Shortest,
ending: getEnding(),
getAllowedEndingsInPreferredOrder: syntaxImpliedNodeFormat => {
if ((syntaxImpliedNodeFormat ?? importingSourceFile.impliedNodeFormat) === ModuleKind.ESNext) {
if (shouldAllowImportingTsExtension(compilerOptions, importingSourceFile.fileName)) {
return [ModuleSpecifierEnding.TsExtension, ModuleSpecifierEnding.JsExtension];
}
return [ModuleSpecifierEnding.JsExtension];
}
if (getEmitModuleResolutionKind(compilerOptions) === ModuleResolutionKind.Classic) {
return [ModuleSpecifierEnding.Index, ModuleSpecifierEnding.JsExtension];
}
switch (preferredEnding) {
case ModuleSpecifierEnding.JsExtension: return [ModuleSpecifierEnding.JsExtension, ModuleSpecifierEnding.Minimal, ModuleSpecifierEnding.Index];
case ModuleSpecifierEnding.TsExtension: return [ModuleSpecifierEnding.TsExtension, ModuleSpecifierEnding.Minimal, ModuleSpecifierEnding.JsExtension, ModuleSpecifierEnding.Index];
case ModuleSpecifierEnding.Index: return [ModuleSpecifierEnding.Index, ModuleSpecifierEnding.Minimal, ModuleSpecifierEnding.JsExtension];
case ModuleSpecifierEnding.Minimal: return [ModuleSpecifierEnding.Minimal, ModuleSpecifierEnding.Index, ModuleSpecifierEnding.JsExtension];
default: Debug.assertNever(preferredEnding);
}
},
};
function getEnding(): Ending {
switch (importModuleSpecifierEnding) {
case "minimal": return Ending.Minimal;
case "index": return Ending.Index;
case "js": return Ending.JsExtension;
default: return usesJsExtensionOnImports(importingSourceFile) || isFormatRequiringExtensions(compilerOptions, importingSourceFile.path, host) ? Ending.JsExtension
: getEmitModuleResolutionKind(compilerOptions) !== ModuleResolutionKind.NodeJs ? Ending.Index : Ending.Minimal;
function getPreferredEnding(): ModuleSpecifierEnding {
if (oldImportSpecifier !== undefined) {
if (hasJSFileExtension(oldImportSpecifier)) return ModuleSpecifierEnding.JsExtension;
if (endsWith(oldImportSpecifier, "/index")) return ModuleSpecifierEnding.Index;
}
return getModuleSpecifierEndingPreference(
importModuleSpecifierEnding,
importingSourceFile.impliedNodeFormat,
compilerOptions,
importingSourceFile);
}
}
function getPreferencesForUpdate(compilerOptions: CompilerOptions, oldImportSpecifier: string, importingSourceFileName: Path, host: ModuleSpecifierResolutionHost): Preferences {
return {
relativePreference: isExternalModuleNameRelative(oldImportSpecifier) ? RelativePreference.Relative : RelativePreference.NonRelative,
ending: hasJSFileExtension(oldImportSpecifier) || isFormatRequiringExtensions(compilerOptions, importingSourceFileName, host) ?
Ending.JsExtension :
getEmitModuleResolutionKind(compilerOptions) !== ModuleResolutionKind.NodeJs || endsWith(oldImportSpecifier, "index") ? Ending.Index : Ending.Minimal,
};
}
function isFormatRequiringExtensions(compilerOptions: CompilerOptions, importingSourceFileName: Path, host: ModuleSpecifierResolutionHost) {
if (getEmitModuleResolutionKind(compilerOptions) !== ModuleResolutionKind.Node16
&& getEmitModuleResolutionKind(compilerOptions) !== ModuleResolutionKind.NodeNext) {
return false;
}
return getImpliedNodeFormatForFile(importingSourceFileName, host.getPackageJsonInfoCache?.(), getModuleResolutionHost(host), compilerOptions) !== ModuleKind.CommonJS;
}
function getModuleResolutionHost(host: ModuleSpecifierResolutionHost): ModuleResolutionHost {
return {
fileExists: host.fileExists,
readFile: Debug.checkDefined(host.readFile),
directoryExists: host.directoryExists,
getCurrentDirectory: host.getCurrentDirectory,
realpath: host.realpath,
useCaseSensitiveFileNames: host.useCaseSensitiveFileNames?.(),
};
}
// `importingSourceFile` and `importingSourceFileName`? Why not just use `importingSourceFile.path`?
// Because when this is called by the file renamer, `importingSourceFile` is the file being renamed,
// while `importingSourceFileName` its *new* name. We need a source file just to get its
@@ -178,7 +182,7 @@ export function updateModuleSpecifier(
oldImportSpecifier: string,
options: ModuleSpecifierOptions = {},
): string | undefined {
const res = getModuleSpecifierWorker(compilerOptions, importingSourceFile, importingSourceFileName, toFileName, host, getPreferencesForUpdate(compilerOptions, oldImportSpecifier, importingSourceFileName, host), {}, options);
const res = getModuleSpecifierWorker(compilerOptions, importingSourceFile, importingSourceFileName, toFileName, host, getPreferences({}, compilerOptions, importingSourceFile, oldImportSpecifier), {}, options);
if (res === oldImportSpecifier) return undefined;
return res;
}
@@ -198,7 +202,7 @@ export function getModuleSpecifier(
host: ModuleSpecifierResolutionHost,
options: ModuleSpecifierOptions = {},
): string {
return getModuleSpecifierWorker(compilerOptions, importingSourceFile, importingSourceFileName, toFileName, host, getPreferences(host, {}, compilerOptions, importingSourceFile), {}, options);
return getModuleSpecifierWorker(compilerOptions, importingSourceFile, importingSourceFileName, toFileName, host, getPreferences({}, compilerOptions, importingSourceFile), {}, options);
}
/** @internal */
@@ -331,7 +335,7 @@ function computeModuleSpecifiers(
options: ModuleSpecifierOptions = {},
): readonly string[] {
const info = getInfo(importingSourceFile.path, host);
const preferences = getPreferences(host, userPreferences, compilerOptions, importingSourceFile);
const preferences = getPreferences(userPreferences, compilerOptions, importingSourceFile);
const existingSpecifier = forEach(modulePaths, modulePath => forEach(
host.getFileIncludeReasons().get(toPath(modulePath.path, host.getCurrentDirectory(), info.getCanonicalFileName)),
reason => {
@@ -423,17 +427,18 @@ function getInfo(importingSourceFileName: Path, host: ModuleSpecifierResolutionH
return { getCanonicalFileName, importingSourceFileName, sourceDirectory };
}
function getLocalModuleSpecifier(moduleFileName: string, info: Info, compilerOptions: CompilerOptions, host: ModuleSpecifierResolutionHost, importMode: ResolutionMode, { ending, relativePreference }: Preferences): string;
function getLocalModuleSpecifier(moduleFileName: string, info: Info, compilerOptions: CompilerOptions, host: ModuleSpecifierResolutionHost, importMode: ResolutionMode, { ending, relativePreference }: Preferences, pathsOnly?: boolean): string | undefined;
function getLocalModuleSpecifier(moduleFileName: string, info: Info, compilerOptions: CompilerOptions, host: ModuleSpecifierResolutionHost, importMode: ResolutionMode, { ending, relativePreference }: Preferences, pathsOnly?: boolean): string | undefined {
function getLocalModuleSpecifier(moduleFileName: string, info: Info, compilerOptions: CompilerOptions, host: ModuleSpecifierResolutionHost, importMode: ResolutionMode, preferences: Preferences): string;
function getLocalModuleSpecifier(moduleFileName: string, info: Info, compilerOptions: CompilerOptions, host: ModuleSpecifierResolutionHost, importMode: ResolutionMode, preferences: Preferences, pathsOnly?: boolean): string | undefined;
function getLocalModuleSpecifier(moduleFileName: string, info: Info, compilerOptions: CompilerOptions, host: ModuleSpecifierResolutionHost, importMode: ResolutionMode, { getAllowedEndingsInPreferredOrder: getAllowedEndingsInPrefererredOrder, relativePreference }: Preferences, pathsOnly?: boolean): string | undefined {
const { baseUrl, paths, rootDirs } = compilerOptions;
if (pathsOnly && !paths) {
return undefined;
}
const { sourceDirectory, getCanonicalFileName } = info;
const relativePath = rootDirs && tryGetModuleNameFromRootDirs(rootDirs, moduleFileName, sourceDirectory, getCanonicalFileName, ending, compilerOptions) ||
removeExtensionAndIndexPostFix(ensurePathIsNonModuleName(getRelativePathFromDirectory(sourceDirectory, moduleFileName, getCanonicalFileName)), ending, compilerOptions);
const allowedEndings = getAllowedEndingsInPrefererredOrder(importMode);
const relativePath = rootDirs && tryGetModuleNameFromRootDirs(rootDirs, moduleFileName, sourceDirectory, getCanonicalFileName, allowedEndings, compilerOptions) ||
processEnding(ensurePathIsNonModuleName(getRelativePathFromDirectory(sourceDirectory, moduleFileName, getCanonicalFileName)), allowedEndings, compilerOptions);
if (!baseUrl && !paths || relativePreference === RelativePreference.Relative) {
return pathsOnly ? undefined : relativePath;
}
@@ -444,12 +449,12 @@ function getLocalModuleSpecifier(moduleFileName: string, info: Info, compilerOpt
return pathsOnly ? undefined : relativePath;
}
const fromPaths = paths && tryGetModuleNameFromPaths(relativeToBaseUrl, paths, getAllowedEndings(ending, compilerOptions, importMode), host, compilerOptions);
const fromPaths = paths && tryGetModuleNameFromPaths(relativeToBaseUrl, paths, allowedEndings, host, compilerOptions);
if (pathsOnly) {
return fromPaths;
}
const maybeNonRelative = fromPaths === undefined && baseUrl !== undefined ? removeExtensionAndIndexPostFix(relativeToBaseUrl, ending, compilerOptions) : fromPaths;
const maybeNonRelative = fromPaths === undefined && baseUrl !== undefined ? processEnding(relativeToBaseUrl, allowedEndings, compilerOptions) : fromPaths;
if (!maybeNonRelative) {
return relativePath;
}
@@ -508,10 +513,6 @@ export function countPathComponents(path: string): number {
return count;
}
function usesJsExtensionOnImports({ imports }: SourceFile): boolean {
return firstDefined(imports, ({ text }) => pathIsRelative(text) ? hasJSFileExtension(text) : undefined) || false;
}
function comparePathsByRedirectAndNumberOfDirectorySeparators(a: ModulePath, b: ModulePath) {
return compareBooleans(b.isRedirect, a.isRedirect) || compareNumberOfDirectorySeparators(a.path, b.path);
}
@@ -698,19 +699,7 @@ function tryGetModuleNameFromAmbientModule(moduleSymbol: Symbol, checker: TypeCh
}
}
function getAllowedEndings(preferredEnding: Ending, compilerOptions: CompilerOptions, importMode: ResolutionMode) {
if (getEmitModuleResolutionKind(compilerOptions) >= ModuleResolutionKind.Node16 && importMode === ModuleKind.ESNext) {
return [Ending.JsExtension];
}
switch (preferredEnding) {
case Ending.JsExtension: return [Ending.JsExtension, Ending.Minimal, Ending.Index];
case Ending.Index: return [Ending.Index, Ending.Minimal, Ending.JsExtension];
case Ending.Minimal: return [Ending.Minimal, Ending.Index, Ending.JsExtension];
default: Debug.assertNever(preferredEnding);
}
}
function tryGetModuleNameFromPaths(relativeToBaseUrl: string, paths: MapLike<readonly string[]>, allowedEndings: Ending[], host: ModuleSpecifierResolutionHost, compilerOptions: CompilerOptions): string | undefined {
function tryGetModuleNameFromPaths(relativeToBaseUrl: string, paths: MapLike<readonly string[]>, allowedEndings: ModuleSpecifierEnding[], host: ModuleSpecifierResolutionHost, compilerOptions: CompilerOptions): string | undefined {
for (const key in paths) {
for (const patternText of paths[key]) {
const pattern = normalizePath(patternText);
@@ -751,9 +740,9 @@ function tryGetModuleNameFromPaths(relativeToBaseUrl: string, paths: MapLike<rea
// resolution, but as long criteria (b) above is met, I don't think its result needs to be the highest priority result
// in module specifier generation. I have included it last, as it's difficult to tell exactly where it should be
// sorted among the others for a particular value of `importModuleSpecifierEnding`.
const candidates: { ending: Ending | undefined, value: string }[] = allowedEndings.map(ending => ({
const candidates: { ending: ModuleSpecifierEnding | undefined, value: string }[] = allowedEndings.map(ending => ({
ending,
value: removeExtensionAndIndexPostFix(relativeToBaseUrl, ending, compilerOptions)
value: processEnding(relativeToBaseUrl, [ending], compilerOptions)
}));
if (tryGetExtensionFromPath(pattern)) {
candidates.push({ ending: undefined, value: relativeToBaseUrl });
@@ -774,23 +763,23 @@ function tryGetModuleNameFromPaths(relativeToBaseUrl: string, paths: MapLike<rea
}
}
else if (
some(candidates, c => c.ending !== Ending.Minimal && pattern === c.value) ||
some(candidates, c => c.ending === Ending.Minimal && pattern === c.value && validateEnding(c))
some(candidates, c => c.ending !== ModuleSpecifierEnding.Minimal && pattern === c.value) ||
some(candidates, c => c.ending === ModuleSpecifierEnding.Minimal && pattern === c.value && validateEnding(c))
) {
return key;
}
}
}
function validateEnding({ ending, value }: { ending: Ending | undefined, value: string }) {
function validateEnding({ ending, value }: { ending: ModuleSpecifierEnding | undefined, value: string }) {
// Optimization: `removeExtensionAndIndexPostFix` can query the file system (a good bit) if `ending` is `Minimal`, the basename
// is 'index', and a `host` is provided. To avoid that until it's unavoidable, we ran the function with no `host` above. Only
// here, after we've checked that the minimal ending is indeed a match (via the length and prefix/suffix checks / `some` calls),
// do we check that the host-validated result is consistent with the answer we got before. If it's not, it falls back to the
// `Ending.Index` result, which should already be in the list of candidates if `Minimal` was. (Note: the assumption here is
// `ModuleSpecifierEnding.Index` result, which should already be in the list of candidates if `Minimal` was. (Note: the assumption here is
// that every module resolution mode that supports dropping extensions also supports dropping `/index`. Like literally
// everything else in this file, this logic needs to be updated if that's not true in some future module resolution mode.)
return ending !== Ending.Minimal || value === removeExtensionAndIndexPostFix(relativeToBaseUrl, ending, compilerOptions, host);
return ending !== ModuleSpecifierEnding.Minimal || value === processEnding(relativeToBaseUrl, [ending], compilerOptions, host);
}
}
@@ -865,7 +854,7 @@ function tryGetModuleNameFromExports(options: CompilerOptions, targetFilePath: s
return undefined;
}
function tryGetModuleNameFromRootDirs(rootDirs: readonly string[], moduleFileName: string, sourceDirectory: string, getCanonicalFileName: (file: string) => string, ending: Ending, compilerOptions: CompilerOptions): string | undefined {
function tryGetModuleNameFromRootDirs(rootDirs: readonly string[], moduleFileName: string, sourceDirectory: string, getCanonicalFileName: (file: string) => string, allowedEndings: readonly ModuleSpecifierEnding[], compilerOptions: CompilerOptions): string | undefined {
const normalizedTargetPaths = getPathsRelativeToRootDirs(moduleFileName, rootDirs, getCanonicalFileName);
if (normalizedTargetPaths === undefined) {
return undefined;
@@ -879,10 +868,7 @@ function tryGetModuleNameFromRootDirs(rootDirs: readonly string[], moduleFileNam
if (!shortest) {
return undefined;
}
return getEmitModuleResolutionKind(compilerOptions) === ModuleResolutionKind.Classic
? removeFileExtension(shortest)
: removeExtensionAndIndexPostFix(shortest, ending, compilerOptions);
return processEnding(shortest, allowedEndings, compilerOptions);
}
function tryGetModuleNameAsNodeModule({ path, isRedirect }: ModulePath, { getCanonicalFileName, sourceDirectory }: Info, importingSourceFile: SourceFile, host: ModuleSpecifierResolutionHost, options: CompilerOptions, userPreferences: UserPreferences, packageNameOnly?: boolean, overrideMode?: ResolutionMode): string | undefined {
@@ -896,7 +882,8 @@ function tryGetModuleNameAsNodeModule({ path, isRedirect }: ModulePath, { getCan
// Simplify the full file path to something that can be resolved by Node.
const preferences = getPreferences(host, userPreferences, options, importingSourceFile);
const preferences = getPreferences(userPreferences, options, importingSourceFile);
const allowedEndings = preferences.getAllowedEndingsInPreferredOrder();
let moduleSpecifier = path;
let isPackageRootPath = false;
if (!packageNameOnly) {
@@ -923,7 +910,7 @@ function tryGetModuleNameAsNodeModule({ path, isRedirect }: ModulePath, { getCan
// try with next level of directory
packageRootIndex = path.indexOf(directorySeparator, packageRootIndex + 1);
if (packageRootIndex === -1) {
moduleSpecifier = removeExtensionAndIndexPostFix(moduleFileName, preferences.ending, options, host);
moduleSpecifier = processEnding(moduleFileName, allowedEndings, options, host);
break;
}
}
@@ -979,7 +966,7 @@ function tryGetModuleNameAsNodeModule({ path, isRedirect }: ModulePath, { getCan
const fromPaths = tryGetModuleNameFromPaths(
subModuleName,
versionPaths.paths,
getAllowedEndings(preferences.ending, options, importMode),
allowedEndings,
host,
options
);
@@ -1036,13 +1023,22 @@ function getPathsRelativeToRootDirs(path: string, rootDirs: readonly string[], g
});
}
function removeExtensionAndIndexPostFix(fileName: string, ending: Ending, options: CompilerOptions, host?: ModuleSpecifierResolutionHost): string {
if (fileExtensionIsOneOf(fileName, [Extension.Json, Extension.Mjs, Extension.Cjs])) return fileName;
function processEnding(fileName: string, allowedEndings: readonly ModuleSpecifierEnding[], options: CompilerOptions, host?: ModuleSpecifierResolutionHost): string {
if (fileExtensionIsOneOf(fileName, [Extension.Json, Extension.Mjs, Extension.Cjs])) {
return fileName;
}
const noExtension = removeFileExtension(fileName);
if (fileName === noExtension) return fileName;
if (fileExtensionIsOneOf(fileName, [Extension.Dmts, Extension.Mts, Extension.Dcts, Extension.Cts])) return noExtension + getJSExtensionForFile(fileName, options);
switch (ending) {
case Ending.Minimal:
if (fileName === noExtension) {
return fileName;
}
if (fileExtensionIsOneOf(fileName, [Extension.Dmts, Extension.Mts, Extension.Dcts, Extension.Cts])) {
return noExtension + getJSExtensionForFile(fileName, options);
}
switch (allowedEndings[0]) {
case ModuleSpecifierEnding.Minimal:
const withoutIndex = removeSuffix(noExtension, "/index");
if (host && withoutIndex !== noExtension && tryGetAnyFileFromPath(host, withoutIndex)) {
// Can't remove index if there's a file by the same name as the directory.
@@ -1050,12 +1046,23 @@ function removeExtensionAndIndexPostFix(fileName: string, ending: Ending, option
return noExtension;
}
return withoutIndex;
case Ending.Index:
case ModuleSpecifierEnding.Index:
return noExtension;
case Ending.JsExtension:
case ModuleSpecifierEnding.JsExtension:
return noExtension + getJSExtensionForFile(fileName, options);
case ModuleSpecifierEnding.TsExtension:
// For now, we don't know if this import is going to be type-only, which means we don't
// know if a .d.ts extension is valid, so use no extension or a .js extension
if (isDeclarationFileName(fileName)) {
const extensionlessPriority = allowedEndings.findIndex(e => e === ModuleSpecifierEnding.Minimal || e === ModuleSpecifierEnding.Index);
const jsPriority = allowedEndings.indexOf(ModuleSpecifierEnding.JsExtension);
return extensionlessPriority !== -1 && extensionlessPriority < jsPriority
? noExtension
: noExtension + getJSExtensionForFile(fileName, options);
}
return fileName;
default:
return Debug.assertNever(ending);
return Debug.assertNever(allowedEndings[0]);
}
}
+27 -5
View File
@@ -132,6 +132,7 @@ import {
getPropertyArrayElementValue,
getPropertyAssignment,
getResolvedModule,
getResolveJsonModule,
getRootLength,
getSetExternalModuleIndicator,
getSpellingSuggestion,
@@ -215,6 +216,8 @@ import {
ModuleResolutionHost,
moduleResolutionIsEqualTo,
ModuleResolutionKind,
moduleResolutionSupportsPackageJsonExportsAndImports,
moduleResolutionSupportsResolvingTsExtensions,
Mutable,
Node,
NodeArray,
@@ -3158,7 +3161,10 @@ export function createProgram(rootNamesOrOptions: readonly string[] | CreateProg
for (const node of file.statements) {
collectModuleReferences(node, /*inAmbientModule*/ false);
}
if ((file.flags & NodeFlags.PossiblyContainsDynamicImport) || isJavaScriptFile) {
// `require` has no special meaning in `--moduleResolution bundler`
const shouldProcessRequires = isJavaScriptFile && getEmitModuleResolutionKind(options) !== ModuleResolutionKind.Bundler;
if ((file.flags & NodeFlags.PossiblyContainsDynamicImport) || shouldProcessRequires) {
collectDynamicImportOrRequireCalls(file);
}
@@ -3220,7 +3226,7 @@ export function createProgram(rootNamesOrOptions: readonly string[] | CreateProg
const r = /import|require/g;
while (r.exec(file.text) !== null) { // eslint-disable-line no-null/no-null
const node = getNodeAtPosition(file, r.lastIndex);
if (isJavaScriptFile && isRequireCall(node, /*checkArgumentIsStringLiteralLike*/ true)) {
if (shouldProcessRequires && isRequireCall(node, /*checkArgumentIsStringLiteralLike*/ true)) {
setParentRecursive(node, /*incremental*/ false); // we need parent data on imports before the program is fully bound, so we ensure it's set here
imports = append(imports, node.arguments[0]);
}
@@ -4111,10 +4117,11 @@ export function createProgram(rootNamesOrOptions: readonly string[] | CreateProg
}
}
if (options.resolveJsonModule) {
if (getResolveJsonModule(options)) {
if (getEmitModuleResolutionKind(options) !== ModuleResolutionKind.NodeJs &&
getEmitModuleResolutionKind(options) !== ModuleResolutionKind.Node16 &&
getEmitModuleResolutionKind(options) !== ModuleResolutionKind.NodeNext) {
getEmitModuleResolutionKind(options) !== ModuleResolutionKind.NodeNext &&
getEmitModuleResolutionKind(options) !== ModuleResolutionKind.Bundler) {
createDiagnosticForOptionName(Diagnostics.Option_resolveJsonModule_cannot_be_specified_without_node_module_resolution_strategy, "resolveJsonModule");
}
// Any emit other than common js, amd, es2015 or esnext is error
@@ -4205,6 +4212,21 @@ export function createProgram(rootNamesOrOptions: readonly string[] | CreateProg
createOptionValueDiagnostic("importsNotUsedAsValues", Diagnostics.Option_preserveValueImports_can_only_be_used_when_module_is_set_to_es2015_or_later);
}
if (options.allowImportingTsExtensions && !(moduleResolutionSupportsResolvingTsExtensions(options) && (options.noEmit || options.emitDeclarationOnly))) {
createOptionValueDiagnostic("allowImportingTsExtensions", Diagnostics.Option_allowImportingTsExtensions_can_only_be_used_when_moduleResolution_is_set_to_bundler_and_either_noEmit_or_emitDeclarationOnly_is_set);
}
const moduleResolution = getEmitModuleResolutionKind(options);
if (options.resolvePackageJsonExports && !moduleResolutionSupportsPackageJsonExportsAndImports(moduleResolution)) {
createOptionValueDiagnostic("resolvePackageJsonExports", Diagnostics.Option_0_can_only_be_used_when_moduleResolution_is_set_to_node16_nodenext_or_bundler, "resolvePackageJsonExports");
}
if (options.resolvePackageJsonImports && !moduleResolutionSupportsPackageJsonExportsAndImports(moduleResolution)) {
createOptionValueDiagnostic("resolvePackageJsonImports", Diagnostics.Option_0_can_only_be_used_when_moduleResolution_is_set_to_node16_nodenext_or_bundler, "resolvePackageJsonImports");
}
if (options.customConditions && !moduleResolutionSupportsPackageJsonExportsAndImports(moduleResolution)) {
createOptionValueDiagnostic("customConditions", Diagnostics.Option_0_can_only_be_used_when_moduleResolution_is_set_to_node16_nodenext_or_bundler, "customConditions");
}
// If the emit is enabled make sure that every output file is unique and not overwriting any of the input files
if (!options.noEmit && !options.suppressOutputPathCheck) {
const emitHost = getEmitHost();
@@ -4900,7 +4922,7 @@ export function getResolutionDiagnostic(options: CompilerOptions, { extension }:
return getAllowJSCompilerOption(options) || !getStrictOptionValue(options, "noImplicitAny") ? undefined : Diagnostics.Could_not_find_a_declaration_file_for_module_0_1_implicitly_has_an_any_type;
}
function needResolveJsonModule() {
return options.resolveJsonModule ? undefined : Diagnostics.Module_0_was_resolved_to_1_but_resolveJsonModule_is_not_used;
return getResolveJsonModule(options) ? undefined : Diagnostics.Module_0_was_resolved_to_1_but_resolveJsonModule_is_not_used;
}
}
+10
View File
@@ -6887,6 +6887,7 @@ export enum ModuleResolutionKind {
// In turn, we offer both a `NodeNext` moving resolution target, and a `Node16` version-anchored resolution target
Node16 = 3,
NodeNext = 99, // Not simply `Node16` so that compiled code linked against TS can use the `Next` value reliably (same as with `ModuleKind`)
Bundler = 100,
}
export enum ModuleDetectionKind {
@@ -6946,6 +6947,7 @@ export type CompilerOptionsValue = string | number | boolean | (string | number)
export interface CompilerOptions {
/** @internal */ all?: boolean;
allowImportingTsExtensions?: boolean;
allowJs?: boolean;
/** @internal */ allowNonTsExtensions?: boolean;
allowSyntheticDefaultImports?: boolean;
@@ -6969,6 +6971,7 @@ export interface CompilerOptions {
* @internal
*/
readonly configFile?: TsConfigSourceFile;
customConditions?: string[];
declaration?: boolean;
declarationMap?: boolean;
emitDeclarationOnly?: boolean;
@@ -7055,6 +7058,8 @@ export interface CompilerOptions {
incremental?: boolean;
tsBuildInfoFile?: string;
removeComments?: boolean;
resolvePackageJsonExports?: boolean;
resolvePackageJsonImports?: boolean;
rootDir?: string;
rootDirs?: string[];
skipLibCheck?: boolean;
@@ -7502,6 +7507,11 @@ export interface ResolvedModule {
resolvedFileName: string;
/** True if `resolvedFileName` comes from `node_modules`. */
isExternalLibraryImport?: boolean;
/**
* True if the original module reference used a .ts extension to refer directly to a .ts file,
* which should produce an error during checking if emit is enabled.
*/
resolvedUsingTsExtension?: boolean;
}
/**
+150 -6
View File
@@ -11,6 +11,7 @@ import {
AnyImportOrReExport,
AnyImportSyntax,
AnyValidImportOrReExport,
append,
arrayFrom,
ArrayLiteralExpression,
ArrayTypeNode,
@@ -441,6 +442,7 @@ import {
semanticDiagnosticsOptionDeclarations,
SetAccessorDeclaration,
ShorthandPropertyAssignment,
shouldAllowImportingTsExtension,
Signature,
SignatureDeclaration,
SignatureFlags,
@@ -513,6 +515,7 @@ import {
TypeReferenceNode,
unescapeLeadingUnderscores,
UnionOrIntersectionTypeNode,
UserPreferences,
ValidImportTypeNode,
VariableDeclaration,
VariableDeclarationInitializedTo,
@@ -7693,6 +7696,10 @@ export function getEmitModuleKind(compilerOptions: {module?: CompilerOptions["mo
getEmitScriptTarget(compilerOptions) >= ScriptTarget.ES2015 ? ModuleKind.ES2015 : ModuleKind.CommonJS;
}
export function emitModuleKindIsNonNodeESM(moduleKind: ModuleKind) {
return moduleKind >= ModuleKind.ES2015 && moduleKind <= ModuleKind.ESNext;
}
/** @internal */
export function getEmitModuleResolutionKind(compilerOptions: CompilerOptions) {
let moduleResolution = compilerOptions.moduleResolution;
@@ -7768,11 +7775,62 @@ export function getESModuleInterop(compilerOptions: CompilerOptions) {
/** @internal */
export function getAllowSyntheticDefaultImports(compilerOptions: CompilerOptions) {
const moduleKind = getEmitModuleKind(compilerOptions);
return compilerOptions.allowSyntheticDefaultImports !== undefined
? compilerOptions.allowSyntheticDefaultImports
: getESModuleInterop(compilerOptions) ||
moduleKind === ModuleKind.System;
if (compilerOptions.allowSyntheticDefaultImports !== undefined) {
return compilerOptions.allowSyntheticDefaultImports;
}
return getESModuleInterop(compilerOptions)
|| getEmitModuleKind(compilerOptions) === ModuleKind.System
|| getEmitModuleResolutionKind(compilerOptions) === ModuleResolutionKind.Bundler;
}
/** @internal */
export function moduleResolutionSupportsPackageJsonExportsAndImports(moduleResolution: ModuleResolutionKind): boolean {
return moduleResolution >= ModuleResolutionKind.Node16 && moduleResolution <= ModuleResolutionKind.NodeNext
|| moduleResolution === ModuleResolutionKind.Bundler;
}
/** @internal */
export function getResolvePackageJsonExports(compilerOptions: CompilerOptions) {
const moduleResolution = getEmitModuleResolutionKind(compilerOptions);
if (!moduleResolutionSupportsPackageJsonExportsAndImports(moduleResolution)) {
return false;
}
if (compilerOptions.resolvePackageJsonExports !== undefined) {
return compilerOptions.resolvePackageJsonExports;
}
switch (moduleResolution) {
case ModuleResolutionKind.Node16:
case ModuleResolutionKind.NodeNext:
case ModuleResolutionKind.Bundler:
return true;
}
return false;
}
/** @internal */
export function getResolvePackageJsonImports(compilerOptions: CompilerOptions) {
const moduleResolution = getEmitModuleResolutionKind(compilerOptions);
if (!moduleResolutionSupportsPackageJsonExportsAndImports(moduleResolution)) {
return false;
}
if (compilerOptions.resolvePackageJsonExports !== undefined) {
return compilerOptions.resolvePackageJsonExports;
}
switch (moduleResolution) {
case ModuleResolutionKind.Node16:
case ModuleResolutionKind.NodeNext:
case ModuleResolutionKind.Bundler:
return true;
}
return false;
}
/** @internal */
export function getResolveJsonModule(compilerOptions: CompilerOptions) {
if (compilerOptions.resolveJsonModule !== undefined) {
return compilerOptions.resolveJsonModule;
}
return getEmitModuleResolutionKind(compilerOptions) === ModuleResolutionKind.Bundler;
}
/** @internal */
@@ -8410,7 +8468,7 @@ export function getSupportedExtensionsWithJsonIfResolveJsonModule(options: Compi
export function getSupportedExtensionsWithJsonIfResolveJsonModule(options: CompilerOptions | undefined, supportedExtensions: readonly string[][]): readonly string[][];
/** @internal */
export function getSupportedExtensionsWithJsonIfResolveJsonModule(options: CompilerOptions | undefined, supportedExtensions: readonly string[][]): readonly string[][] {
if (!options || !options.resolveJsonModule) return supportedExtensions;
if (!options || !getResolveJsonModule(options)) return supportedExtensions;
if (supportedExtensions === allSupportedExtensions) return allSupportedExtensionsWithJson;
if (supportedExtensions === supportedTSExtensions) return supportedTSExtensionsWithJson;
return [...supportedExtensions, [Extension.Json]];
@@ -8430,6 +8488,92 @@ export function hasTSFileExtension(fileName: string): boolean {
return some(supportedTSExtensionsFlat, extension => fileExtensionIs(fileName, extension));
}
/**
* @internal
* Corresponds to UserPreferences#importPathEnding
*/
export const enum ModuleSpecifierEnding {
Minimal,
Index,
JsExtension,
TsExtension,
}
/** @internal */
export function usesExtensionsOnImports({ imports }: SourceFile, hasExtension: (text: string) => boolean = or(hasJSFileExtension, hasTSFileExtension)): boolean {
return firstDefined(imports, ({ text }) => pathIsRelative(text) ? hasExtension(text) : undefined) || false;
}
/** @internal */
export function getModuleSpecifierEndingPreference(preference: UserPreferences["importModuleSpecifierEnding"], resolutionMode: ResolutionMode, compilerOptions: CompilerOptions, sourceFile: SourceFile): ModuleSpecifierEnding {
if (preference === "js" || resolutionMode === ModuleKind.ESNext) {
// Extensions are explicitly requested or required. Now choose between .js and .ts.
if (!shouldAllowImportingTsExtension(compilerOptions)) {
return ModuleSpecifierEnding.JsExtension;
}
// `allowImportingTsExtensions` is a strong signal, so use .ts unless the file
// already uses .js extensions and no .ts extensions.
return inferPreference() !== ModuleSpecifierEnding.JsExtension
? ModuleSpecifierEnding.TsExtension
: ModuleSpecifierEnding.JsExtension;
}
if (preference === "minimal") {
return ModuleSpecifierEnding.Minimal;
}
if (preference === "index") {
return ModuleSpecifierEnding.Index;
}
// No preference was specified.
// Look at imports and/or requires to guess whether .js, .ts, or extensionless imports are preferred.
// N.B. that `Index` detection is not supported since it would require file system probing to do
// accurately, and more importantly, literally nobody wants `Index` and its existence is a mystery.
if (!shouldAllowImportingTsExtension(compilerOptions)) {
// If .ts imports are not valid, we only need to see one .js import to go with that.
return usesExtensionsOnImports(sourceFile) ? ModuleSpecifierEnding.JsExtension : ModuleSpecifierEnding.Minimal;
}
return inferPreference();
function inferPreference() {
let usesJsExtensions = false;
const specifiers = sourceFile.imports.length ? sourceFile.imports.map(i => i.text) :
isSourceFileJS(sourceFile) ? getRequiresAtTopOfFile(sourceFile).map(r => r.arguments[0].text) :
emptyArray;
for (const specifier of specifiers) {
if (pathIsRelative(specifier)) {
if (hasTSFileExtension(specifier)) {
return ModuleSpecifierEnding.TsExtension;
}
if (hasJSFileExtension(specifier)) {
usesJsExtensions = true;
}
}
}
return usesJsExtensions ? ModuleSpecifierEnding.JsExtension : ModuleSpecifierEnding.Minimal;
}
}
function getRequiresAtTopOfFile(sourceFile: SourceFile): readonly RequireOrImportCall[] {
let nonRequireStatementCount = 0;
let requires: RequireOrImportCall[] | undefined;
for (const statement of sourceFile.statements) {
if (nonRequireStatementCount > 3) {
break;
}
if (isRequireVariableStatement(statement)) {
requires = concatenate(requires, statement.declarationList.declarations.map(d => d.initializer));
}
else if (isExpressionStatement(statement) && isRequireCall(statement.expression, /*requireStringLiteralLikeArgument*/ true)) {
requires = append(requires, statement.expression);
}
else {
nonRequireStatementCount++;
}
}
return requires || emptyArray;
}
/** @internal */
export function isSupportedSourceFileName(fileName: string, compilerOptions?: CompilerOptions, extraFileExtensions?: readonly FileExtensionInfo[]) {
if (!fileName) return false;
+70 -21
View File
@@ -804,18 +804,6 @@ export class TestState {
});
}
private renderMarkers(markers: { text: string, fileName: string, position: number }[]) {
const filesToDisplay = ts.deduplicate(markers.map(m => m.fileName), ts.equateValues);
return filesToDisplay.map(fileName => {
const markersToRender = markers.filter(m => m.fileName === fileName).sort((a, b) => b.position - a.position);
let fileContent = this.tryGetFileContent(fileName) || "";
for (const marker of markersToRender) {
fileContent = fileContent.slice(0, marker.position) + `\x1b[1;4m/*${marker.text}*/\x1b[0;31m` + fileContent.slice(marker.position);
}
return `// @Filename: ${fileName}\n${fileContent}`;
}).join("\n\n");
}
private verifyDefinitionTextSpan(defs: ts.DefinitionInfoAndBoundSpan, startMarkerName: string) {
const range = this.testData.ranges.find(range => this.markerName(range.marker!) === startMarkerName);
@@ -839,6 +827,22 @@ export class TestState {
}
}
private renderMarkers(markers: { text: string, fileName: string, position: number }[], useTerminalBoldSequence = true) {
const filesToDisplay = ts.deduplicate(markers.map(m => m.fileName), ts.equateValues);
return filesToDisplay.map(fileName => {
const markersToRender = markers.filter(m => m.fileName === fileName).sort((a, b) => b.position - a.position);
let fileContent = this.tryGetFileContent(fileName) || "";
for (const marker of markersToRender) {
fileContent = fileContent.slice(0, marker.position) + bold(`/*${marker.text}*/`) + fileContent.slice(marker.position);
}
return `// @Filename: ${fileName}\n${fileContent}`;
}).join("\n\n");
function bold(text: string) {
return useTerminalBoldSequence ? `\x1b[1;4m${text}\x1b[0;31m` : text;
}
}
public verifyGetEmitOutputForCurrentFile(expected: string): void {
const emit = this.languageService.getEmitOutput(this.activeFile.fileName);
if (emit.outputFiles.length !== 1) {
@@ -3188,12 +3192,53 @@ export class TestState {
if (!negative && !validBraceCompletion) {
this.raiseError(`${position} is not a valid brace completion position for ${openingBrace}`);
}
if (negative && validBraceCompletion) {
this.raiseError(`${position} is a valid brace completion position for ${openingBrace}`);
}
}
public baselineAutoImports(markerName: string, preferences?: ts.UserPreferences) {
const marker = this.getMarkerByName(markerName);
const baselineFile = this.getBaselineFileNameForContainingTestFile(`.baseline.md`);
const completionPreferences = {
includeCompletionsForModuleExports: true,
includeCompletionsWithInsertText: true,
allowIncompleteCompletions: true,
includeCompletionsWithSnippetText: true,
...preferences
};
const ext = ts.getAnyExtensionFromPath(this.activeFile.fileName).slice(1);
const lang = ["mts", "cts"].includes(ext) ? "ts" : ext;
let baselineText = codeFence(this.renderMarkers([{ text: "|", fileName: marker.fileName, position: marker.position }], /*useTerminalBoldSequence*/ false), lang) + "\n\n";
this.goToMarker(marker);
const completions = this.getCompletionListAtCaret(completionPreferences)!;
const autoImportCompletions = completions.entries.filter(c => c.hasAction && c.source && c.sortText === ts.Completions.SortText.AutoImportSuggestions);
if (autoImportCompletions.length) {
baselineText += `## From completions\n\n${autoImportCompletions.map(c => `- \`${c.name}\` from \`"${c.source}"\``).join("\n")}\n\n`;
autoImportCompletions.forEach(c => {
const details = this.getCompletionEntryDetails(c.name, c.source, c.data, completionPreferences);
assert(details?.codeActions, `Entry '${c.name}' from "${c.source}" returned no code actions from completion details request`);
assert(details.codeActions.length === 1, `Entry '${c.name}' from "${c.source}" returned more than one code action`);
assert(details.codeActions[0].changes.length === 1, `Entry '${c.name}' from "${c.source}" returned a code action changing more than one file`);
assert(details.codeActions[0].changes[0].fileName === this.activeFile.fileName, `Entry '${c.name}' from "${c.source}" returned a code action changing a different file`);
const changes = details.codeActions[0].changes[0].textChanges;
const completionChange: ts.TextChange = { newText: c.insertText || c.name, span: c.replacementSpan || completions.optionalReplacementSpan || { start: marker.position, length: 0 } };
const sortedChanges = [...changes, completionChange].sort((a, b) => a.span.start - b.span.start);
let newFileContent = this.activeFile.content;
for (let i = sortedChanges.length - 1; i >= 0; i--) {
newFileContent = newFileContent.substring(0, sortedChanges[i].span.start) + sortedChanges[i].newText + newFileContent.substring(sortedChanges[i].span.start + sortedChanges[i].span.length);
}
baselineText += codeFence(newFileContent, lang) + "\n\n";
});
}
// TODO: do codefixes too
Harness.Baseline.runBaseline(baselineFile, baselineText);
}
public verifyJsxClosingTag(map: { [markerName: string]: ts.JsxClosingTagInfo | undefined }): void {
for (const markerName in map) {
this.goToMarker(markerName);
@@ -4605,10 +4650,10 @@ function rangesOfDiffBetweenTwoStrings(source: string, target: string) {
else {
ranges.push({ start: index - 1, length: 1 });
}
}
else {
}
else {
ranges.push({ start: index - 1, length: 1 });
}
}
};
for (let index = 0; index < Math.max(source.length, target.length); index++) {
@@ -4618,10 +4663,10 @@ function rangesOfDiffBetweenTwoStrings(source: string, target: string) {
}
return ranges;
}
}
// Adds an _ when the source string and the target string have a whitespace difference
function highlightDifferenceBetweenStrings(source: string, target: string) {
// Adds an _ when the source string and the target string have a whitespace difference
function highlightDifferenceBetweenStrings(source: string, target: string) {
const ranges = rangesOfDiffBetweenTwoStrings(source, target);
let emTarget = target;
ranges.forEach((range, index) => {
@@ -4633,8 +4678,12 @@ function rangesOfDiffBetweenTwoStrings(source: string, target: string) {
range.start + 1 + additionalOffset,
range.start + range.length + 1 + additionalOffset
);
const after = emTarget.slice(range.start + range.length + 1 + additionalOffset, emTarget.length);
const after = emTarget.slice(range.start + range.length + 1 + additionalOffset, emTarget.length);
emTarget = before + lhs + between + rhs + after;
});
return emTarget;
}
}
function codeFence(code: string, lang?: string) {
return `\`\`\`${lang || ""}\n${code}\n\`\`\``;
}
+4
View File
@@ -488,6 +488,10 @@ export class Verify extends VerifyNegatable {
this.state.verifyImportFixModuleSpecifiers(marker, moduleSpecifiers, preferences);
}
public baselineAutoImports(marker: string, preferences?: ts.UserPreferences) {
this.state.baselineAutoImports(marker, preferences);
}
public navigationBar(json: any, options?: { checkSpans?: boolean }) {
this.state.verifyNavigationBar(json, options);
}
+2 -1
View File
@@ -814,7 +814,8 @@ function getNewImportFixes(
const compilerOptions = program.getCompilerOptions();
const moduleSpecifierResolutionHost = createModuleSpecifierResolutionHost(program, host);
const getChecker = createGetChecker(program, host);
const rejectNodeModulesRelativePaths = moduleResolutionUsesNodeModules(getEmitModuleResolutionKind(compilerOptions));
const moduleResolution = getEmitModuleResolutionKind(compilerOptions);
const rejectNodeModulesRelativePaths = moduleResolutionUsesNodeModules(moduleResolution);
const getModuleSpecifiers = fromCacheOnly
? (moduleSymbol: Symbol) => ({ moduleSpecifiers: moduleSpecifiers.tryGetModuleSpecifiersFromCache(moduleSymbol, sourceFile, moduleSpecifierResolutionHost, preferences), computedWithoutCache: false })
: (moduleSymbol: Symbol, checker: TypeChecker) => moduleSpecifiers.getModuleSpecifiersWithCacheInfo(moduleSymbol, checker, compilerOptions, sourceFile, moduleSpecifierResolutionHost, preferences);
+4 -4
View File
@@ -271,7 +271,7 @@ import {
modifierToFlag,
ModuleDeclaration,
ModuleReference,
moduleResolutionRespectsExports,
moduleResolutionSupportsPackageJsonExportsAndImports,
NamedImportBindings,
Node,
NodeArray,
@@ -555,7 +555,7 @@ const enum KeywordCompletionFilters {
const enum GlobalsSearch { Continue, Success, Fail }
interface ModuleSpecifierResolutioContext {
interface ModuleSpecifierResolutionContext {
tryResolve: (exportInfo: readonly SymbolExportInfo[], symbolName: string, isFromAmbientModule: boolean) => ModuleSpecifierResolutionResult;
resolvedAny: () => boolean;
skippedAny: () => boolean;
@@ -576,7 +576,7 @@ function resolvingModuleSpecifiers<TReturn>(
preferences: UserPreferences,
isForImportStatementCompletion: boolean,
isValidTypeOnlyUseSite: boolean,
cb: (context: ModuleSpecifierResolutioContext) => TReturn,
cb: (context: ModuleSpecifierResolutionContext) => TReturn,
): TReturn {
const start = timestamp();
// Under `--moduleResolution nodenext`, we have to resolve module specifiers up front, because
@@ -584,7 +584,7 @@ function resolvingModuleSpecifiers<TReturn>(
// relative path into node_modules), and we want to filter those completions out entirely.
// Import statement completions always need specifier resolution because the module specifier is
// part of their `insertText`, not the `codeActions` creating edits away from the cursor.
const needsFullResolution = isForImportStatementCompletion || moduleResolutionRespectsExports(getEmitModuleResolutionKind(program.getCompilerOptions()));
const needsFullResolution = isForImportStatementCompletion || moduleResolutionSupportsPackageJsonExportsAndImports(getEmitModuleResolutionKind(program.getCompilerOptions()));
let skippedAny = false;
let ambientCount = 0;
let resolvedCount = 0;
+87 -71
View File
@@ -49,10 +49,12 @@ import {
getEmitModuleResolutionKind,
getLeadingCommentRanges,
getModeForUsageLocation,
getModuleSpecifierEndingPreference,
getOwnKeys,
getPackageJsonTypesVersionsPaths,
getPathComponents,
getReplacementSpanForContextToken,
getResolvePackageJsonExports,
getSupportedExtensions,
getSupportedExtensionsWithJsonIfResolveJsonModule,
getTokenAtPosition,
@@ -89,7 +91,8 @@ import {
mapDefined,
MapLike,
ModuleKind,
ModuleResolutionKind,
moduleResolutionUsesNodeModules,
ModuleSpecifierEnding,
moduleSpecifiers,
Node,
normalizePath,
@@ -120,6 +123,7 @@ import {
StringLiteralLike,
StringLiteralType,
stripQuotes,
supportedTSImplementationExtensions,
Symbol,
SyntaxKind,
textPart,
@@ -525,49 +529,44 @@ function getStringLiteralCompletionsFromModuleNamesWorker(sourceFile: SourceFile
const scriptPath = sourceFile.path;
const scriptDirectory = getDirectoryPath(scriptPath);
const extensionOptions = getExtensionOptions(compilerOptions, ReferenceKind.ModuleSpecifier, sourceFile, preferences, mode);
return isPathRelativeToScript(literalValue) || !compilerOptions.baseUrl && (isRootedDiskPath(literalValue) || isUrl(literalValue))
? getCompletionEntriesForRelativeModules(literalValue, scriptDirectory, compilerOptions, host, scriptPath, getIncludeExtensionOption())
: getCompletionEntriesForNonRelativeModules(literalValue, scriptDirectory, mode, compilerOptions, host, getIncludeExtensionOption(), typeChecker);
function getIncludeExtensionOption() {
const mode = isStringLiteralLike(node) ? getModeForUsageLocation(sourceFile, node) : undefined;
return preferences.importModuleSpecifierEnding === "js" || mode === ModuleKind.ESNext ? IncludeExtensionsOption.ModuleSpecifierCompletion : IncludeExtensionsOption.Exclude;
}
? getCompletionEntriesForRelativeModules(literalValue, scriptDirectory, compilerOptions, host, scriptPath, extensionOptions)
: getCompletionEntriesForNonRelativeModules(literalValue, scriptDirectory, mode, compilerOptions, host, extensionOptions, typeChecker);
}
interface ExtensionOptions {
readonly extensions: readonly Extension[];
readonly includeExtensionsOption: IncludeExtensionsOption;
readonly extensionsToSearch: readonly Extension[];
readonly referenceKind: ReferenceKind;
readonly importingSourceFile: SourceFile;
readonly endingPreference?: UserPreferences["importModuleSpecifierEnding"];
readonly resolutionMode?: ResolutionMode;
}
function getExtensionOptions(compilerOptions: CompilerOptions, includeExtensionsOption = IncludeExtensionsOption.Exclude): ExtensionOptions {
return { extensions: flatten(getSupportedExtensionsForModuleResolution(compilerOptions)), includeExtensionsOption };
function getExtensionOptions(compilerOptions: CompilerOptions, referenceKind: ReferenceKind, importingSourceFile: SourceFile, preferences?: UserPreferences, resolutionMode?: ResolutionMode): ExtensionOptions {
return {
extensionsToSearch: flatten(getSupportedExtensionsForModuleResolution(compilerOptions)),
referenceKind,
importingSourceFile,
endingPreference: preferences?.importModuleSpecifierEnding,
resolutionMode,
};
}
function getCompletionEntriesForRelativeModules(literalValue: string, scriptDirectory: string, compilerOptions: CompilerOptions, host: LanguageServiceHost, scriptPath: Path, includeExtensions: IncludeExtensionsOption) {
const extensionOptions = getExtensionOptions(compilerOptions, includeExtensions);
function getCompletionEntriesForRelativeModules(literalValue: string, scriptDirectory: string, compilerOptions: CompilerOptions, host: LanguageServiceHost, scriptPath: Path, extensionOptions: ExtensionOptions) {
if (compilerOptions.rootDirs) {
return getCompletionEntriesForDirectoryFragmentWithRootDirs(
compilerOptions.rootDirs, literalValue, scriptDirectory, extensionOptions, compilerOptions, host, scriptPath);
}
else {
return arrayFrom(getCompletionEntriesForDirectoryFragment(literalValue, scriptDirectory, extensionOptions, host, scriptPath).values());
return arrayFrom(getCompletionEntriesForDirectoryFragment(literalValue, scriptDirectory, extensionOptions, host, /*moduleSpecifierIsRelative*/ false, scriptPath).values());
}
}
function isEmitResolutionKindUsingNodeModules(compilerOptions: CompilerOptions): boolean {
return getEmitModuleResolutionKind(compilerOptions) === ModuleResolutionKind.NodeJs ||
getEmitModuleResolutionKind(compilerOptions) === ModuleResolutionKind.Node16 ||
getEmitModuleResolutionKind(compilerOptions) === ModuleResolutionKind.NodeNext;
}
function isEmitModuleResolutionRespectingExportMaps(compilerOptions: CompilerOptions) {
return getEmitModuleResolutionKind(compilerOptions) === ModuleResolutionKind.Node16 ||
getEmitModuleResolutionKind(compilerOptions) === ModuleResolutionKind.NodeNext;
}
function getSupportedExtensionsForModuleResolution(compilerOptions: CompilerOptions): readonly Extension[][] {
const extensions = getSupportedExtensions(compilerOptions);
return isEmitResolutionKindUsingNodeModules(compilerOptions) ?
const moduleResolution = getEmitModuleResolutionKind(compilerOptions);
return moduleResolutionUsesNodeModules(moduleResolution) ?
getSupportedExtensionsWithJsonIfResolveJsonModule(compilerOptions, extensions) :
extensions;
}
@@ -595,22 +594,22 @@ function getCompletionEntriesForDirectoryFragmentWithRootDirs(rootDirs: string[]
const basePath = compilerOptions.project || host.getCurrentDirectory();
const ignoreCase = !(host.useCaseSensitiveFileNames && host.useCaseSensitiveFileNames());
const baseDirectories = getBaseDirectoriesFromRootDirs(rootDirs, basePath, scriptDirectory, ignoreCase);
return flatMap(baseDirectories, baseDirectory => arrayFrom(getCompletionEntriesForDirectoryFragment(fragment, baseDirectory, extensionOptions, host, exclude).values()));
return flatMap(baseDirectories, baseDirectory => arrayFrom(getCompletionEntriesForDirectoryFragment(fragment, baseDirectory, extensionOptions, host, /*moduleSpecifierIsRelative*/ true, exclude).values()));
}
const enum IncludeExtensionsOption {
Exclude,
Include,
ModuleSpecifierCompletion,
const enum ReferenceKind {
Filename,
ModuleSpecifier,
}
/**
* Given a path ending at a directory, gets the completions for the path, and filters for those entries containing the basename.
*/
function getCompletionEntriesForDirectoryFragment(
fragment: string,
scriptPath: string,
scriptDirectory: string,
extensionOptions: ExtensionOptions,
host: LanguageServiceHost,
moduleSpecifierIsRelative: boolean,
exclude?: string,
result = createNameAndKindSet()
): NameAndKindSet {
@@ -634,23 +633,25 @@ function getCompletionEntriesForDirectoryFragment(
fragment = ensureTrailingDirectorySeparator(fragment);
const absolutePath = resolvePath(scriptPath, fragment);
const absolutePath = resolvePath(scriptDirectory, fragment);
const baseDirectory = hasTrailingDirectorySeparator(absolutePath) ? absolutePath : getDirectoryPath(absolutePath);
// check for a version redirect
const packageJsonPath = findPackageJson(baseDirectory, host);
if (packageJsonPath) {
const packageJson = readJson(packageJsonPath, host as { readFile: (filename: string) => string | undefined });
const typesVersions = (packageJson as any).typesVersions;
if (typeof typesVersions === "object") {
const versionPaths = getPackageJsonTypesVersionsPaths(typesVersions)?.paths;
if (versionPaths) {
const packageDirectory = getDirectoryPath(packageJsonPath);
const pathInPackage = absolutePath.slice(ensureTrailingDirectorySeparator(packageDirectory).length);
if (addCompletionEntriesFromPaths(result, pathInPackage, packageDirectory, extensionOptions, host, versionPaths)) {
// A true result means one of the `versionPaths` was matched, which will block relative resolution
// to files and folders from here. All reachable paths given the pattern match are already added.
return result;
if (!moduleSpecifierIsRelative) {
// check for a version redirect
const packageJsonPath = findPackageJson(baseDirectory, host);
if (packageJsonPath) {
const packageJson = readJson(packageJsonPath, host as { readFile: (filename: string) => string | undefined });
const typesVersions = (packageJson as any).typesVersions;
if (typeof typesVersions === "object") {
const versionPaths = getPackageJsonTypesVersionsPaths(typesVersions)?.paths;
if (versionPaths) {
const packageDirectory = getDirectoryPath(packageJsonPath);
const pathInPackage = absolutePath.slice(ensureTrailingDirectorySeparator(packageDirectory).length);
if (addCompletionEntriesFromPaths(result, pathInPackage, packageDirectory, extensionOptions, host, versionPaths)) {
// A true result means one of the `versionPaths` was matched, which will block relative resolution
// to files and folders from here. All reachable paths given the pattern match are already added.
return result;
}
}
}
}
@@ -660,16 +661,16 @@ function getCompletionEntriesForDirectoryFragment(
if (!tryDirectoryExists(host, baseDirectory)) return result;
// Enumerate the available files if possible
const files = tryReadDirectory(host, baseDirectory, extensionOptions.extensions, /*exclude*/ undefined, /*include*/ ["./*"]);
const files = tryReadDirectory(host, baseDirectory, extensionOptions.extensionsToSearch, /*exclude*/ undefined, /*include*/ ["./*"]);
if (files) {
for (let filePath of files) {
filePath = normalizePath(filePath);
if (exclude && comparePaths(filePath, exclude, scriptPath, ignoreCase) === Comparison.EqualTo) {
if (exclude && comparePaths(filePath, exclude, scriptDirectory, ignoreCase) === Comparison.EqualTo) {
continue;
}
const { name, extension } = getFilenameWithExtensionOption(getBaseFileName(filePath), host.getCompilationSettings(), extensionOptions.includeExtensionsOption);
const { name, extension } = getFilenameWithExtensionOption(getBaseFileName(filePath), host.getCompilationSettings(), extensionOptions);
result.add(nameAndKind(name, ScriptElementKind.scriptElement, extension));
}
}
@@ -689,17 +690,32 @@ function getCompletionEntriesForDirectoryFragment(
return result;
}
function getFilenameWithExtensionOption(name: string, compilerOptions: CompilerOptions, includeExtensionsOption: IncludeExtensionsOption): { name: string, extension: Extension | undefined } {
const outputExtension = moduleSpecifiers.tryGetJSExtensionForFile(name, compilerOptions);
if (includeExtensionsOption === IncludeExtensionsOption.Exclude && !fileExtensionIsOneOf(name, [Extension.Json, Extension.Mts, Extension.Cts, Extension.Dmts, Extension.Dcts, Extension.Mjs, Extension.Cjs])) {
return { name: removeFileExtension(name), extension: tryGetExtensionFromPath(name) };
}
else if ((fileExtensionIsOneOf(name, [Extension.Mts, Extension.Cts, Extension.Dmts, Extension.Dcts, Extension.Mjs, Extension.Cjs]) || includeExtensionsOption === IncludeExtensionsOption.ModuleSpecifierCompletion) && outputExtension) {
return { name: changeExtension(name, outputExtension), extension: outputExtension };
}
else {
function getFilenameWithExtensionOption(name: string, compilerOptions: CompilerOptions, extensionOptions: ExtensionOptions): { name: string, extension: Extension | undefined } {
if (extensionOptions.referenceKind === ReferenceKind.Filename) {
return { name, extension: tryGetExtensionFromPath(name) };
}
const endingPreference = getModuleSpecifierEndingPreference(extensionOptions.endingPreference, extensionOptions.resolutionMode, compilerOptions, extensionOptions.importingSourceFile);
if (endingPreference === ModuleSpecifierEnding.TsExtension) {
if (fileExtensionIsOneOf(name, supportedTSImplementationExtensions)) {
return { name, extension: tryGetExtensionFromPath(name) };
}
const outputExtension = moduleSpecifiers.tryGetJSExtensionForFile(name, compilerOptions);
return outputExtension
? { name: changeExtension(name, outputExtension), extension: outputExtension }
: { name, extension: tryGetExtensionFromPath(name) };
}
if ((endingPreference === ModuleSpecifierEnding.Minimal || endingPreference === ModuleSpecifierEnding.Index) &&
fileExtensionIsOneOf(name, [Extension.Js, Extension.Jsx, Extension.Ts, Extension.Tsx, Extension.Dts])
) {
return { name: removeFileExtension(name), extension: tryGetExtensionFromPath(name) };
}
const outputExtension = moduleSpecifiers.tryGetJSExtensionForFile(name, compilerOptions);
return outputExtension
? { name: changeExtension(name, outputExtension), extension: outputExtension }
: { name, extension: tryGetExtensionFromPath(name) };
}
/** @returns whether `fragment` was a match for any `paths` (which should indicate whether any other path completions should be offered) */
@@ -786,17 +802,17 @@ function getCompletionEntriesForNonRelativeModules(
mode: ResolutionMode,
compilerOptions: CompilerOptions,
host: LanguageServiceHost,
includeExtensionsOption: IncludeExtensionsOption,
extensionOptions: ExtensionOptions,
typeChecker: TypeChecker,
): readonly NameAndKind[] {
const { baseUrl, paths } = compilerOptions;
const result = createNameAndKindSet();
const extensionOptions = getExtensionOptions(compilerOptions, includeExtensionsOption);
const moduleResolution = getEmitModuleResolutionKind(compilerOptions);
if (baseUrl) {
const projectDir = compilerOptions.project || host.getCurrentDirectory();
const absolute = normalizePath(combinePaths(projectDir, baseUrl));
getCompletionEntriesForDirectoryFragment(fragment, absolute, extensionOptions, host, /*exclude*/ undefined, result);
getCompletionEntriesForDirectoryFragment(fragment, absolute, extensionOptions, host, /*moduleSpecifierIsRelative*/ false, /*exclude*/ undefined, result);
if (paths) {
addCompletionEntriesFromPaths(result, fragment, absolute, extensionOptions, host, paths);
}
@@ -809,7 +825,7 @@ function getCompletionEntriesForNonRelativeModules(
getCompletionEntriesFromTypings(host, compilerOptions, scriptPath, fragmentDirectory, extensionOptions, result);
if (isEmitResolutionKindUsingNodeModules(compilerOptions)) {
if (moduleResolutionUsesNodeModules(moduleResolution)) {
// If looking for a global package name, don't just include everything in `node_modules` because that includes dependencies' own dependencies.
// (But do if we didn't find anything, e.g. 'package.json' missing.)
let foundGlobal = false;
@@ -826,10 +842,10 @@ function getCompletionEntriesForNonRelativeModules(
let ancestorLookup: (directory: string) => void | undefined = ancestor => {
const nodeModules = combinePaths(ancestor, "node_modules");
if (tryDirectoryExists(host, nodeModules)) {
getCompletionEntriesForDirectoryFragment(fragment, nodeModules, extensionOptions, host, /*exclude*/ undefined, result);
getCompletionEntriesForDirectoryFragment(fragment, nodeModules, extensionOptions, host, /*moduleSpecifierIsRelative*/ false, /*exclude*/ undefined, result);
}
};
if (fragmentDirectory && isEmitModuleResolutionRespectingExportMaps(compilerOptions)) {
if (fragmentDirectory && getResolvePackageJsonExports(compilerOptions)) {
const nodeModulesDirectoryLookup = ancestorLookup;
ancestorLookup = ancestor => {
const components = getPathComponents(fragment);
@@ -967,13 +983,13 @@ function getModulesForPathsPattern(
// done.
const includeGlob = normalizedSuffix ? "**/*" + normalizedSuffix : "./*";
const matches = mapDefined(tryReadDirectory(host, baseDirectory, extensionOptions.extensions, /*exclude*/ undefined, [includeGlob]), match => {
const matches = mapDefined(tryReadDirectory(host, baseDirectory, extensionOptions.extensionsToSearch, /*exclude*/ undefined, [includeGlob]), match => {
const trimmedWithPattern = trimPrefixAndSuffix(match);
if (trimmedWithPattern) {
if (containsSlash(trimmedWithPattern)) {
return directoryResult(getPathComponents(removeLeadingDirectorySeparator(trimmedWithPattern))[1]);
}
const { name, extension } = getFilenameWithExtensionOption(trimmedWithPattern, host.getCompilationSettings(), extensionOptions.includeExtensionsOption);
const { name, extension } = getFilenameWithExtensionOption(trimmedWithPattern, host.getCompilationSettings(), extensionOptions);
return nameAndKind(name, ScriptElementKind.scriptElement, extension);
}
});
@@ -1030,8 +1046,8 @@ function getTripleSlashReferenceCompletion(sourceFile: SourceFile, position: num
const [, prefix, kind, toComplete] = match;
const scriptPath = getDirectoryPath(sourceFile.path);
const names = kind === "path" ? getCompletionEntriesForDirectoryFragment(toComplete, scriptPath, getExtensionOptions(compilerOptions, IncludeExtensionsOption.Include), host, sourceFile.path)
: kind === "types" ? getCompletionEntriesFromTypings(host, compilerOptions, scriptPath, getFragmentDirectory(toComplete), getExtensionOptions(compilerOptions))
const names = kind === "path" ? getCompletionEntriesForDirectoryFragment(toComplete, scriptPath, getExtensionOptions(compilerOptions, ReferenceKind.Filename, sourceFile), host, /*moduleSpecifierIsRelative*/ true, sourceFile.path)
: kind === "types" ? getCompletionEntriesFromTypings(host, compilerOptions, scriptPath, getFragmentDirectory(toComplete), getExtensionOptions(compilerOptions, ReferenceKind.ModuleSpecifier, sourceFile))
: Debug.fail();
return addReplacementSpans(toComplete, range.pos + prefix.length, arrayFrom(names.values()));
}
@@ -1071,7 +1087,7 @@ function getCompletionEntriesFromTypings(host: LanguageServiceHost, options: Com
const baseDirectory = combinePaths(directory, typeDirectoryName);
const remainingFragment = tryRemoveDirectoryPrefix(fragmentDirectory, packageName, hostGetCanonicalFileName(host));
if (remainingFragment !== undefined) {
getCompletionEntriesForDirectoryFragment(remainingFragment, baseDirectory, extensionOptions, host, /*exclude*/ undefined, result);
getCompletionEntriesForDirectoryFragment(remainingFragment, baseDirectory, extensionOptions, host, /*moduleSpecifierIsRelative*/ false, /*exclude*/ undefined, result);
}
}
}
+1 -5
View File
@@ -2374,6 +2374,7 @@ export function programContainsModules(program: Program): boolean {
export function programContainsEsModules(program: Program): boolean {
return program.getSourceFiles().some(s => !s.isDeclarationFile && !program.isSourceFileFromExternalLibrary(s) && !!s.externalModuleIndicator);
}
// TODO: this function is, at best, poorly named. Use sites are pretty suspicious.
/** @internal */
export function compilerOptionsIndicateEsModules(compilerOptions: CompilerOptions): boolean {
return !!compilerOptions.module || getEmitScriptTarget(compilerOptions) >= ScriptTarget.ES2015 || !!compilerOptions.noEmit;
@@ -2408,11 +2409,6 @@ export function getModuleSpecifierResolverHost(program: Program, host: LanguageS
};
}
/** @internal */
export function moduleResolutionRespectsExports(moduleResolution: ModuleResolutionKind): boolean {
return moduleResolution >= ModuleResolutionKind.Node16 && moduleResolution <= ModuleResolutionKind.NodeNext;
}
/** @internal */
export function moduleResolutionUsesNodeModules(moduleResolution: ModuleResolutionKind): boolean {
return moduleResolution === ModuleResolutionKind.NodeJs || moduleResolution >= ModuleResolutionKind.Node16 && moduleResolution <= ModuleResolutionKind.NodeNext;
+4
View File
@@ -135,8 +135,10 @@ class CompilerTest {
"module",
"moduleResolution",
"moduleDetection",
"allowImportingTsExtensions",
"target",
"jsx",
"noEmit",
"removeComments",
"importHelpers",
"importHelpers",
@@ -161,6 +163,8 @@ class CompilerTest {
"useUnknownInCatchVariables",
"noUncheckedIndexedAccess",
"noPropertyAccessFromIndexSignature",
"resolvePackageJsonExports",
"resolvePackageJsonImports",
];
private fileName: string;
private justName: string;
+1 -1
View File
@@ -170,4 +170,4 @@ export function updateProgram(oldProgram: ProgramWithSourceTexts, rootNames: rea
export function updateProgramText(files: readonly NamedSourceText[], fileName: string, newProgramText: string) {
const file = ts.find(files, f => f.name === fileName)!;
file.text = file.text.updateProgram(newProgramText);
}
}