diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 963981dde4743..3672f6e3803d9 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -139,6 +139,7 @@ import { ElementFlags, EmitFlags, EmitHint, + emitModuleKindIsNonNodeESM, EmitResolver, EmitTextWriter, emptyArray, @@ -458,6 +459,7 @@ import { isConstructorTypeNode, isConstTypeReference, isDeclaration, + isDeclarationFileName, isDeclarationName, isDeclarationReadonly, isDecorator, @@ -897,6 +899,7 @@ import { setTextRangePosEnd, setValueDeclaration, ShorthandPropertyAssignment, + shouldAllowImportingTsExtension, shouldPreserveConstEnums, Signature, SignatureDeclaration, @@ -4727,6 +4730,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 +4741,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 +4870,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 && 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 +4899,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 { diff --git a/src/compiler/commandLineParser.ts b/src/compiler/commandLineParser.ts index 97a09f716345e..6003dbe036561 100644 --- a/src/compiler/commandLineParser.ts +++ b/src/compiler/commandLineParser.ts @@ -1081,6 +1081,14 @@ 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_minimal_and_either_noEmit_or_emitDeclarationOnly_to_be_set, + // defaultValueDescription: false, + // }, // Source Maps { diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index e146cce88c8cb..438bbf2f7014e 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -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,14 @@ "category": "Error", "code": 5095 }, + "Option 'allowImportingTsExtensions' can only be used when 'moduleResolution' is set to 'minimal' 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 + }, "Generates a sourcemap for each corresponding '.d.ts' file.": { "category": "Message", @@ -4443,10 +4455,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 +5438,10 @@ "category": "Message", "code": 6406 }, + "Allow imports to include TypeScript file extensions. Requires '--moduleResolution minimal' and either '--noEmit' or '--emitDeclarationOnly' to be set.": { + "category": "Message", + "code": 6407 + }, "The expected type comes from property '{0}' which is declared here on type '{1}'": { "category": "Message", diff --git a/src/compiler/moduleNameResolver.ts b/src/compiler/moduleNameResolver.ts index fd9cf539fc333..da355d8f05957 100644 --- a/src/compiler/moduleNameResolver.ts +++ b/src/compiler/moduleNameResolver.ts @@ -53,6 +53,7 @@ import { hasTrailingDirectorySeparator, hostGetCanonicalFileName, isArray, + isDeclarationFileName, isExternalModuleNameRelative, isRootedDiskPath, isString, @@ -94,6 +95,7 @@ import { startsWith, stringContains, supportedDeclarationExtensions, + supportedTSExtensionsFlat, supportedTSImplementationExtensions, toPath, tryExtractTSExtension, @@ -128,7 +130,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 +140,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 +159,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 +167,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 +219,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), @@ -1795,7 +1806,10 @@ 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 +1824,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 +1868,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 +1884,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 }; } } @@ -2179,9 +2200,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 +2393,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 +2780,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 +2840,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 +2915,18 @@ export function classicNameResolver(moduleName: string, containingFile: string, } } +export function moduleResolutionSupportsResolvingTsExtensions(_compilerOptions: CompilerOptions) { + return false; +} + +// 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. diff --git a/src/compiler/moduleSpecifiers.ts b/src/compiler/moduleSpecifiers.ts index c21a26c7dcb28..e6905e149908a 100644 --- a/src/compiler/moduleSpecifiers.ts +++ b/src/compiler/moduleSpecifiers.ts @@ -34,9 +34,9 @@ import { GetCanonicalFileName, getDirectoryPath, getEmitModuleResolutionKind, - getImpliedNodeFormatForFile, getModeForResolutionAtIndex, getModuleNameStringLiteralAt, + getModuleSpecifierEndingPreference, getNodeModulePathParts, getNormalizedAbsolutePath, getOwnKeys, @@ -71,9 +71,9 @@ import { ModuleDeclaration, ModuleKind, ModulePath, - ModuleResolutionHost, ModuleResolutionKind, ModuleSpecifierCache, + ModuleSpecifierEnding, ModuleSpecifierOptions, ModuleSpecifierResolutionHost, NodeFlags, @@ -89,6 +89,7 @@ import { ResolutionMode, resolvePath, ScriptKind, + shouldAllowImportingTsExtension, some, SourceFile, startsWith, @@ -107,61 +108,63 @@ 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. + */ + getAllowedEndingsInPrefererredOrder(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(), + getAllowedEndingsInPrefererredOrder: syntaxImpliedNodeFormat => { + if (syntaxImpliedNodeFormat === ModuleKind.ESNext || (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.TsExtension, ModuleSpecifierEnding.Minimal, 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 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; + 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); } - 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`? @@ -178,7 +181,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 +201,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 +334,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 +426,19 @@ 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, { getAllowedEndingsInPrefererredOrder, relativePreference }: Preferences, pathsOnly?: boolean): string | undefined { const { baseUrl, paths, rootDirs } = compilerOptions; if (pathsOnly && !paths) { return undefined; } const { sourceDirectory, getCanonicalFileName } = info; + const allowedEndings = getAllowedEndingsInPrefererredOrder(importMode); + const ending = allowedEndings[0]; const relativePath = rootDirs && tryGetModuleNameFromRootDirs(rootDirs, moduleFileName, sourceDirectory, getCanonicalFileName, ending, compilerOptions) || - removeExtensionAndIndexPostFix(ensurePathIsNonModuleName(getRelativePathFromDirectory(sourceDirectory, moduleFileName, getCanonicalFileName)), ending, compilerOptions); + processEnding(ensurePathIsNonModuleName(getRelativePathFromDirectory(sourceDirectory, moduleFileName, getCanonicalFileName)), ending, 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, ending, 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, allowedEndings: Ending[], host: ModuleSpecifierResolutionHost, compilerOptions: CompilerOptions): string | undefined { +function tryGetModuleNameFromPaths(relativeToBaseUrl: string, paths: MapLike, 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 ({ + 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 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, ending: 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, ending, 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.getAllowedEndingsInPrefererredOrder(); 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[0], 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,13 @@ function getPathsRelativeToRootDirs(path: string, rootDirs: readonly string[], g }); } -function removeExtensionAndIndexPostFix(fileName: string, ending: Ending, options: CompilerOptions, host?: ModuleSpecifierResolutionHost): string { +function processEnding(fileName: string, ending: 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: + 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,10 +1037,12 @@ 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: + return fileName; default: return Debug.assertNever(ending); } diff --git a/src/compiler/program.ts b/src/compiler/program.ts index 5cefcfdda324f..d94b2bc1a6503 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -215,6 +215,7 @@ import { ModuleResolutionHost, moduleResolutionIsEqualTo, ModuleResolutionKind, + moduleResolutionSupportsResolvingTsExtensions, Mutable, Node, NodeArray, @@ -4205,6 +4206,10 @@ 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_minimal_and_either_noEmit_or_emitDeclarationOnly_is_set); + } + // 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(); diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 773d64df907ae..7f806f860a1a8 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -6945,6 +6945,7 @@ export type CompilerOptionsValue = string | number | boolean | (string | number) export interface CompilerOptions { /** @internal */ all?: boolean; + allowImportingTsExtensions?: boolean; allowJs?: boolean; /** @internal */ allowNonTsExtensions?: boolean; allowSyntheticDefaultImports?: boolean; @@ -7501,6 +7502,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; } /** diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 5c65c652db3ef..a3df7748bca42 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -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; @@ -8430,6 +8437,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; diff --git a/src/harness/fourslashImpl.ts b/src/harness/fourslashImpl.ts index ac7c0c40ad3b5..8a2a4c65390d5 100644 --- a/src/harness/fourslashImpl.ts +++ b/src/harness/fourslashImpl.ts @@ -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\`\`\``; +} diff --git a/src/harness/fourslashInterfaceImpl.ts b/src/harness/fourslashInterfaceImpl.ts index af1044d6fee6b..b71d67b530d8a 100644 --- a/src/harness/fourslashInterfaceImpl.ts +++ b/src/harness/fourslashInterfaceImpl.ts @@ -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); } diff --git a/src/services/codefixes/importFixes.ts b/src/services/codefixes/importFixes.ts index 2f0c90737549a..1b753b8942dde 100644 --- a/src/services/codefixes/importFixes.ts +++ b/src/services/codefixes/importFixes.ts @@ -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); diff --git a/src/services/completions.ts b/src/services/completions.ts index 6e02fd1e567a4..b5ee425d6732c 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -548,7 +548,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; @@ -569,7 +569,7 @@ function resolvingModuleSpecifiers( 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 diff --git a/src/services/shims.ts b/src/services/shims.ts index 6f36697703396..4339931349053 100644 --- a/src/services/shims.ts +++ b/src/services/shims.ts @@ -449,7 +449,7 @@ export class LanguageServiceShimHostAdapter implements LanguageServiceHost { const resolutionsInFile = JSON.parse(this.shimHost.getModuleResolutionsForFile!(containingFile)) as MapLike; // TODO: GH#18217 return map(moduleNames, name => { const result = getProperty(resolutionsInFile, name); - return result ? { resolvedFileName: result, extension: extensionFromPath(result), isExternalLibraryImport: false } : undefined; + return result ? { resolvedFileName: result, extension: extensionFromPath(result), isExternalLibraryImport: false, resolvedUsingTsExtension: false } : undefined; }); }; } diff --git a/src/services/stringCompletions.ts b/src/services/stringCompletions.ts index c319ad3b8e92a..42c1af1c7938c 100644 --- a/src/services/stringCompletions.ts +++ b/src/services/stringCompletions.ts @@ -49,6 +49,7 @@ import { getEmitModuleResolutionKind, getLeadingCommentRanges, getModeForUsageLocation, + getModuleSpecifierEndingPreference, getOwnKeys, getPackageJsonTypesVersionsPaths, getPathComponents, @@ -89,7 +90,9 @@ import { mapDefined, MapLike, ModuleKind, - ModuleResolutionKind, + moduleResolutionRespectsExports, + 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) }; +function getFilenameWithExtensionOption(name: string, compilerOptions: CompilerOptions, extensionOptions: ExtensionOptions): { name: string, extension: Extension | undefined } { + if (extensionOptions.referenceKind === ReferenceKind.Filename) { + return { 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 }; + + 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) }; } - else { - return { 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 && moduleResolutionRespectsExports(moduleResolution)) { 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); } } } diff --git a/src/services/utilities.ts b/src/services/utilities.ts index ac93d22ba7699..1ef0c812790b6 100644 --- a/src/services/utilities.ts +++ b/src/services/utilities.ts @@ -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; diff --git a/src/testRunner/compilerRunner.ts b/src/testRunner/compilerRunner.ts index bad01ecd0bb01..8bd62f4f62e64 100644 --- a/src/testRunner/compilerRunner.ts +++ b/src/testRunner/compilerRunner.ts @@ -135,8 +135,10 @@ class CompilerTest { "module", "moduleResolution", "moduleDetection", + "allowImportingTsExtensions", "target", "jsx", + "noEmit", "removeComments", "importHelpers", "importHelpers", diff --git a/src/testRunner/unittests/helpers.ts b/src/testRunner/unittests/helpers.ts index 725fb7c4e60fb..4e5f2046a416a 100644 --- a/src/testRunner/unittests/helpers.ts +++ b/src/testRunner/unittests/helpers.ts @@ -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); -} \ No newline at end of file +} diff --git a/src/testRunner/unittests/moduleResolution.ts b/src/testRunner/unittests/moduleResolution.ts index 869a86894dc93..cebf0c1545491 100644 --- a/src/testRunner/unittests/moduleResolution.ts +++ b/src/testRunner/unittests/moduleResolution.ts @@ -169,6 +169,7 @@ describe("unittests:: moduleResolution:: Node module resolution - non-relative p resolvedFileName: "/sub/node_modules/a/index.ts", isExternalLibraryImport: true, extension: ts.Extension.Ts, + resolvedUsingTsExtension: false, }, failedLookupLocations: [], affectingLocations: [], @@ -184,6 +185,7 @@ describe("unittests:: moduleResolution:: Node module resolution - non-relative p resolvedFileName: "/sub/directory/node_modules/b/index.ts", isExternalLibraryImport: true, extension: ts.Extension.Ts, + resolvedUsingTsExtension: false, }, failedLookupLocations: [], affectingLocations: [], @@ -201,6 +203,7 @@ describe("unittests:: moduleResolution:: Node module resolution - non-relative p resolvedFileName: "/bar/node_modules/c/index.ts", isExternalLibraryImport: true, extension: ts.Extension.Ts, + resolvedUsingTsExtension: false, }, failedLookupLocations: [], affectingLocations: [], @@ -217,6 +220,7 @@ describe("unittests:: moduleResolution:: Node module resolution - non-relative p resolvedFileName: "/foo/index.ts", isExternalLibraryImport: true, extension: ts.Extension.Ts, + resolvedUsingTsExtension: false, }, failedLookupLocations: [], affectingLocations: [], @@ -232,6 +236,7 @@ describe("unittests:: moduleResolution:: Node module resolution - non-relative p resolvedFileName: "d:/bar/node_modules/e/index.ts", isExternalLibraryImport: true, extension: ts.Extension.Ts, + resolvedUsingTsExtension: false, }, failedLookupLocations: [], affectingLocations: [], diff --git a/src/testRunner/unittests/tscWatch/watchApi.ts b/src/testRunner/unittests/tscWatch/watchApi.ts index 09be991f9a308..49dc60cbd414f 100644 --- a/src/testRunner/unittests/tscWatch/watchApi.ts +++ b/src/testRunner/unittests/tscWatch/watchApi.ts @@ -50,6 +50,7 @@ describe("unittests:: tsc-watch:: watchAPI:: tsc-watch with custom module resolu resolvedFileName: resolvedModule.resolvedFileName, isExternalLibraryImport: resolvedModule.isExternalLibraryImport, originalFileName: resolvedModule.originalPath, + resolvedUsingTsExtension: false, }; }); const watch = ts.createWatchProgram(host); diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 0af2b39653975..a127bd35f7ab6 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -7022,6 +7022,7 @@ declare namespace ts { } type CompilerOptionsValue = string | number | boolean | (string | number)[] | string[] | MapLike | PluginImport[] | ProjectReference[] | null | undefined; interface CompilerOptions { + allowImportingTsExtensions?: boolean; allowJs?: boolean; allowSyntheticDefaultImports?: boolean; allowUmdGlobalAccess?: boolean; @@ -7266,6 +7267,11 @@ declare namespace ts { 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; } /** * ResolvedModule with an explicitly provided `extension` property. @@ -8772,6 +8778,7 @@ declare namespace ts { parent: ConstructorDeclaration; name: Identifier; }; + function emitModuleKindIsNonNodeESM(moduleKind: ModuleKind): boolean; function isJSDocOptionalParameter(node: ParameterDeclaration): boolean; function isOptionalDeclaration(declaration: Declaration): boolean; function createUnparsedSourceFile(text: string): UnparsedSource; @@ -9204,6 +9211,8 @@ declare namespace ts { function resolveModuleName(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost, cache?: ModuleResolutionCache, redirectedReference?: ResolvedProjectReference, resolutionMode?: ResolutionMode): ResolvedModuleWithFailedLookupLocations; function nodeModuleNameResolver(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost, cache?: ModuleResolutionCache, redirectedReference?: ResolvedProjectReference): ResolvedModuleWithFailedLookupLocations; function classicNameResolver(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost, cache?: NonRelativeModuleNameResolutionCache, redirectedReference?: ResolvedProjectReference): ResolvedModuleWithFailedLookupLocations; + function moduleResolutionSupportsResolvingTsExtensions(_compilerOptions: CompilerOptions): boolean; + function shouldAllowImportingTsExtension(compilerOptions: CompilerOptions, fromFileName?: string): boolean | "" | undefined; interface TypeReferenceDirectiveResolutionCache extends PerDirectoryResolutionCache, NonRelativeNameResolutionCache, PackageJsonInfoCache { } interface ModeAwareCache { diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index eaf5c1e2626fa..9669468b81d8f 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -3088,6 +3088,7 @@ declare namespace ts { } type CompilerOptionsValue = string | number | boolean | (string | number)[] | string[] | MapLike | PluginImport[] | ProjectReference[] | null | undefined; interface CompilerOptions { + allowImportingTsExtensions?: boolean; allowJs?: boolean; allowSyntheticDefaultImports?: boolean; allowUmdGlobalAccess?: boolean; @@ -3332,6 +3333,11 @@ declare namespace ts { 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; } /** * ResolvedModule with an explicitly provided `extension` property. @@ -4838,6 +4844,7 @@ declare namespace ts { parent: ConstructorDeclaration; name: Identifier; }; + function emitModuleKindIsNonNodeESM(moduleKind: ModuleKind): boolean; function isJSDocOptionalParameter(node: ParameterDeclaration): boolean; function isOptionalDeclaration(declaration: Declaration): boolean; function createUnparsedSourceFile(text: string): UnparsedSource; @@ -5270,6 +5277,8 @@ declare namespace ts { function resolveModuleName(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost, cache?: ModuleResolutionCache, redirectedReference?: ResolvedProjectReference, resolutionMode?: ResolutionMode): ResolvedModuleWithFailedLookupLocations; function nodeModuleNameResolver(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost, cache?: ModuleResolutionCache, redirectedReference?: ResolvedProjectReference): ResolvedModuleWithFailedLookupLocations; function classicNameResolver(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost, cache?: NonRelativeModuleNameResolutionCache, redirectedReference?: ResolvedProjectReference): ResolvedModuleWithFailedLookupLocations; + function moduleResolutionSupportsResolvingTsExtensions(_compilerOptions: CompilerOptions): boolean; + function shouldAllowImportingTsExtension(compilerOptions: CompilerOptions, fromFileName?: string): boolean | "" | undefined; interface TypeReferenceDirectiveResolutionCache extends PerDirectoryResolutionCache, NonRelativeNameResolutionCache, PackageJsonInfoCache { } interface ModeAwareCache { diff --git a/tests/baselines/reference/showConfig/Shows tsconfig for single option/allowImportingTsExtensions/tsconfig.json b/tests/baselines/reference/showConfig/Shows tsconfig for single option/allowImportingTsExtensions/tsconfig.json new file mode 100644 index 0000000000000..88c95f9eb8307 --- /dev/null +++ b/tests/baselines/reference/showConfig/Shows tsconfig for single option/allowImportingTsExtensions/tsconfig.json @@ -0,0 +1,5 @@ +{ + "compilerOptions": { + "allowImportingTsExtensions": true + } +} diff --git a/tests/cases/fourslash/fourslash.ts b/tests/cases/fourslash/fourslash.ts index 7d106808e7982..5bbbd00212ccb 100644 --- a/tests/cases/fourslash/fourslash.ts +++ b/tests/cases/fourslash/fourslash.ts @@ -375,6 +375,7 @@ declare namespace FourSlashInterface { getAndApplyCodeFix(errorCode?: number, index?: number): void; importFixAtPosition(expectedTextArray: string[], errorCode?: number, options?: UserPreferences): void; importFixModuleSpecifiers(marker: string, moduleSpecifiers: string[], options?: UserPreferences): void; + baselineAutoImports(marker: string, options?: UserPreferences): void; navigationBar(json: any, options?: { checkSpans?: boolean }): void; navigationTree(json: any, options?: { checkSpans?: boolean }): void; pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy